From 357ac5e67a13918bd38c2f4a583f45fd115a7218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com> Date: Sun, 26 Jul 2026 15:26:21 +0800 Subject: [PATCH] feat(authz): add resident agent operation receipts --- .../OPERATION-RECEIPT-PROTOCOL.md | 23 +++++++ .../lake-lamp-authz/authorize-repo-push.js | 7 ++ .../authorize-repo-push.test.js | 8 +-- server-tools/lake-lamp-authz/server.js | 66 +++++++++++++++---- server-tools/lake-lamp-authz/server.test.js | 32 +++++++++ .../lake-lamp-authz/workorder-manager.js | 18 ++++- 6 files changed, 136 insertions(+), 18 deletions(-) create mode 100644 server-tools/lake-lamp-authz/OPERATION-RECEIPT-PROTOCOL.md diff --git a/server-tools/lake-lamp-authz/OPERATION-RECEIPT-PROTOCOL.md b/server-tools/lake-lamp-authz/OPERATION-RECEIPT-PROTOCOL.md new file mode 100644 index 0000000..7a1b454 --- /dev/null +++ b/server-tools/lake-lamp-authz/OPERATION-RECEIPT-PROTOCOL.md @@ -0,0 +1,23 @@ +# 小湖灯操作回执协议 v1 + +这是语言人格层、工程层与服务器常驻 Agent 之间的共同事实层。常驻 Agent 只读取本机真实的授权、导航图、固定动作与健康结果;它不替人格体决定目标,也不读取或回传密码、密钥和令牌。 + +每个回执使用 `guanghu.operation-receipt/v1`: + +```json +{"state":"failed","diagnostic_code":"action_execution_failed","workorder_id":"...","target":"JD-FD-PRIMARY","action":"deploy-registered-service","evidence":{"exit_code":1,"stderr":"..."},"next_step":"..."} +``` + +固定流程:创建工单 → 主人邮箱批准 → 领取会话 → 读取/确认实时导航图 → 查询 `/api/session/status` → 执行已登记固定动作 → 读取回执。 + +`/api/session/status` 是人格体判断“是否真的进入服务器、当前卡在哪里、最后一次执行是否成功”的唯一接口。它返回会话绑定的目标、范围、允许动作、导航图确认状态、最后回执与下一步。 + +稳定诊断码: + +- `owner_handoff_required`:交给主人打开 `request_url`;不要索要密码或验证码。 +- `map_ack_required`:先读并确认实时导航图;不要跳到别的服务器猜路径。 +- `session_expired`:同一目标和范围重新申请工单。 +- `action_execution_failed`:根据受限 `evidence` 和 `next_step` 修复,再执行同一登记动作。 +- `repo_push_transport_unavailable`:推送许可不是 Git 登录凭证。受限 bundle 接收器未部署前,不得重试裸 `git push`。 + +推送的下一阶段必须是服务器端 bundle 接收器:验证短时会话、仓库、分支和预期基线后,经代码频道支持的接收路径写入并回执提交 SHA。禁止再次用裸仓库 `update-ref` 绕过代码频道数据库。 diff --git a/server-tools/lake-lamp-authz/authorize-repo-push.js b/server-tools/lake-lamp-authz/authorize-repo-push.js index 38b0c37..456ff03 100755 --- a/server-tools/lake-lamp-authz/authorize-repo-push.js +++ b/server-tools/lake-lamp-authz/authorize-repo-push.js @@ -50,6 +50,13 @@ async function authorizeRepoPush(options, deps = {}) { const map = await requestJson(fetchImpl, `${baseUrl}/api/navigation-map/read`, common, session.session_token); await requestJson(fetchImpl, `${baseUrl}/api/navigation-map/ack`, { ...common, map_hash: map.map_hash }, session.session_token); const grant = await requestJson(fetchImpl, `${baseUrl}/api/repo-push/grant`, { ...common, repo }, session.session_token); + if (grant.transport && grant.transport.status !== "ready") { + output(`[LL-REPO-PUSH-TRANSPORT-BLOCKED] ${grant.transport.diagnostic_code || "repo_push_transport_unavailable"}`); + output(`NEXT_STEP=${grant.transport.next_step || "读取服务器 operation receipt。"}`); + const error = new Error(grant.transport.diagnostic_code || "repo_push_transport_unavailable"); + error.receipt = grant.receipt; + throw error; + } output(`[LL-REPO-PUSH-GRANTED] ${grant.repo} 已获得限时推送许可。现在重试原 git push。`); return grant; } diff --git a/server-tools/lake-lamp-authz/authorize-repo-push.test.js b/server-tools/lake-lamp-authz/authorize-repo-push.test.js index 7adc62d..ddbae9a 100644 --- a/server-tools/lake-lamp-authz/authorize-repo-push.test.js +++ b/server-tools/lake-lamp-authz/authorize-repo-push.test.js @@ -8,7 +8,7 @@ const path = require("node:path"); const { createApp } = require("./server"); const { authorizeRepoPush } = require("./authorize-repo-push"); -test("one helper command completes owner handoff, map acknowledgement, and repo grant", async () => { +test("repo-push helper stops with a server receipt when no safe transport is deployed", async () => { const mail = []; const directory = fs.mkdtempSync(path.join(os.tmpdir(), "lake-lamp-repo-push-")); const mapGate = { @@ -48,10 +48,8 @@ test("one helper command completes owner handoff, map acknowledgement, and repo const approvalPath = new URL(mail[0].approvalUrl).pathname.replace("/authz", ""); assert.equal((await fetch(`${base}${approvalPath}`, { method: "POST" })).status, 200); - const grant = await grantPromise; - assert.equal(grant.repo, "bingshuo/fifth-domain"); - assert.equal(grant.target, "JD-FD-PRIMARY"); - assert.ok(lines.some(line => line.startsWith("[LL-REPO-PUSH-GRANTED]"))); + await assert.rejects(grantPromise, /repo_push_transport_unavailable/); + assert.ok(lines.some(line => line.startsWith("[LL-REPO-PUSH-TRANSPORT-BLOCKED]"))); assert.ok(fs.existsSync(path.join(directory, "bingshuo__fifth-domain.json"))); } finally { await new Promise(resolve => app.close(resolve)); diff --git a/server-tools/lake-lamp-authz/server.js b/server-tools/lake-lamp-authz/server.js index 762c47a..b72f579 100644 --- a/server-tools/lake-lamp-authz/server.js +++ b/server-tools/lake-lamp-authz/server.js @@ -112,6 +112,8 @@ function createApp(options = {}) { targets: [...targets], scopes: actions, owner_handoff: "open request_url and request pre-registered mailbox verification", + workflow: ["create_workorder", "owner_handoff", "claim_session", "read_navigation_map", "ack_navigation_map", "check_session_status", "execute_registered_action", "read_operation_receipt"], + diagnostics: diagnosticCatalog(), approval_ttl: manager.approvalTtl, session_ttl: manager.sessionTtl, max_session_lifetime: manager.maxSessionLifetime, @@ -168,6 +170,7 @@ function createApp(options = {}) { request_url: `${publicBaseUrl}/request/${created.handoffToken}`, expires_in: created.expiresIn, status: "waiting_for_owner_handoff", + receipt: receipt({ state: "waiting_for_owner_handoff", diagnostic_code: "owner_handoff_required", workorder_id: created.id, next_step: "把 request_url 交给主人打开一次;页面会向预登记邮箱发送批准链接。不要索要密码、验证码或令牌。" }), }); } @@ -191,7 +194,20 @@ function createApp(options = {}) { const token = bearer(req); const claimed = manager.claim(claimMatch[1], token); if (!claimed.ok) return json(res, claimed.reason === "approval_pending" ? 202 : 403, { error: claimed.reason }); - return json(res, 200, { ok: true, session_token: claimed.sessionToken, expires_in: claimed.expiresIn, target: claimed.target, scope: claimed.scope, action: claimed.action, resource: claimed.resource || "" }); + return json(res, 200, { ok: true, session_token: claimed.sessionToken, expires_in: claimed.expiresIn, target: claimed.target, scope: claimed.scope, action: claimed.action, resource: claimed.resource || "", receipt: claimed.receipt || receipt({ state: "session_issued", diagnostic_code: "session_issued", workorder_id: claimed.workorderId, next_step: "读取并确认实时导航图。" }) }); + } + + if (req.method === "POST" && url.pathname === "/api/session/status") { + const body = await readJson(req); + if (!body) return json(res, 400, failure("invalid_json")); + const token = bearer(req); + const target = String(body.target || ""); + const scope = String(body.scope || ""); + const verified = manager.verifySession(token, { pid: String(body.persona_id || "") }, target, scope, "read-navigation-map"); + if (!verified.ok) return json(res, 403, failure(verified.reason)); + const map = mapGate.read(target); + const mapVerified = mapGate.verify(token, target, map.hash); + return json(res, 200, { ok: true, state: mapVerified.ok ? "ready_to_execute" : "map_ack_required", workorder_id: verified.session.workorderId || "", target, scope, allowed_actions: verified.session.actions || [verified.session.action], expires_at: verified.session.expiresAt, map: { hash: map.hash, acknowledged: mapVerified.ok }, last_receipt: verified.session.lastReceipt || null, next_step: mapVerified.ok ? "只执行 allowed_actions 中已登记的动作;每次执行后读取 operation receipt。" : "先读取 /api/navigation-map/read,再提交同一 map_hash 至 /api/navigation-map/ack。" }); } if (req.method === "POST" && url.pathname === "/api/session/verify") { @@ -238,47 +254,54 @@ function createApp(options = {}) { const verified = manager.verifySession(token, { pid: String(body.persona_id || "") }, String(body.target || ""), String(body.scope || ""), "read-navigation-map"); if (!verified.ok) return json(res, 403, { error: verified.reason }); const acked = mapGate.ack(token, String(body.target || ""), String(body.map_hash || ""), Date.now() / 1000, Math.max(1, verified.session.expiresAt - Date.now() / 1000)); - return json(res, acked.ok ? 200 : 409, acked.ok ? { ok: true, target: body.target, map_hash: body.map_hash } : { error: acked.reason }); + if (!acked.ok) return json(res, 409, failure(acked.reason)); + const operationReceipt = receipt({ state: "map_acknowledged", diagnostic_code: "map_acknowledged", workorder_id: verified.session.workorderId, target: body.target, next_step: "可查询 session/status,再执行本会话 allowed_actions 内的固定动作。" }); + manager.recordReceipt(token, operationReceipt); + return json(res, 200, { ok: true, target: body.target, map_hash: body.map_hash, receipt: operationReceipt }); } if (req.method === "POST" && url.pathname === "/api/actions/execute") { const body = await readJson(req); - if (!body) return json(res, 400, { error: "invalid_json" }); - if (body.cmd || body.command || body.shell || body.args) return json(res, 400, { error: "arbitrary_command_forbidden" }); + if (!body) return json(res, 400, failure("invalid_json")); + if (body.cmd || body.command || body.shell || body.args) return json(res, 400, failure("arbitrary_command_forbidden")); const token = bearer(req); const target = String(body.target || ""); const scope = String(body.scope || ""); const action = String(body.action || ""); const resource = String(body.resource || ""); const verified = manager.verifySession(token, { pid: String(body.persona_id || "") }, target, scope, action, Date.now() / 1000, resource); - if (!verified.ok) return json(res, 403, { error: verified.reason }); + if (!verified.ok) return json(res, 403, failure(verified.reason)); const map = mapGate.read(target); const mapVerified = mapGate.verify(token, target, map.hash); - if (!mapVerified.ok) return json(res, 423, { error: mapVerified.reason, required_action: "read-navigation-map" }); + if (!mapVerified.ok) return json(res, 423, failure(mapVerified.reason, "先读取并确认导航图。", { required_action: "read-navigation-map" })); const result = await executeAction(resource ? { action, target, resource } : { action, target }); - return json(res, result.ok ? 200 : 502, result); + const operationReceipt = receipt({ state: result.ok ? "succeeded" : "failed", diagnostic_code: result.ok ? "action_succeeded" : String(result.error || "action_execution_failed"), workorder_id: verified.session.workorderId, target, action, evidence: safeEvidence(result), next_step: result.ok ? "读取 session/status 确认当前回执;如需新范围、目标或资源,重新发起工单。" : "读取 diagnostic_code 与 evidence;仅按 next_step 修复,不要切换到其他服务器或猜测凭证。" }); + manager.recordReceipt(token, operationReceipt); + return json(res, result.ok ? 200 : 502, { ...result, receipt: operationReceipt }); } if (req.method === "POST" && url.pathname === "/api/repo-push/grant") { const body = await readJson(req); - if (!body) return json(res, 400, { error: "invalid_json" }); + if (!body) return json(res, 400, failure("invalid_json")); const token = bearer(req); const target = String(body.target || ""); const scope = String(body.scope || "repo-push"); const repo = String(body.repo || "").toLowerCase(); - if (!/^bingshuo\/[a-z0-9._-]+$/.test(repo)) return json(res, 400, { error: "repo_not_allowlisted" }); + if (!/^bingshuo\/[a-z0-9._-]+$/.test(repo)) return json(res, 400, failure("repo_not_allowlisted")); const verified = manager.verifySession(token, { pid: String(body.persona_id || "") }, target, scope, "push-repository"); - if (!verified.ok) return json(res, 403, { error: verified.reason }); + if (!verified.ok) return json(res, 403, failure(verified.reason)); const map = mapGate.read(target); const mapVerified = mapGate.verify(token, target, map.hash); - if (!mapVerified.ok) return json(res, 423, { error: mapVerified.reason, required_action: "read-navigation-map" }); + if (!mapVerified.ok) return json(res, 423, failure(mapVerified.reason, "先读取并确认导航图。", { required_action: "read-navigation-map" })); fs.mkdirSync(repoGrantDir, { recursive: true, mode: 0o2770 }); const grant = { schema: "guanghu.repo-push-grant/v1", repo, target, persona_id: body.persona_id, map_hash: map.hash, issued_at: Date.now() / 1000, expires_at: verified.session.expiresAt }; const grantFile = path.join(repoGrantDir, `${repo.replace("/", "__")}.json`); const temp = `${grantFile}.${process.pid}.tmp`; fs.writeFileSync(temp, JSON.stringify(grant), { mode: 0o640 }); fs.renameSync(temp, grantFile); - return json(res, 200, { ok: true, repo, target, expires_at: grant.expires_at }); + const operationReceipt = receipt({ state: "blocked", diagnostic_code: "repo_push_transport_unavailable", workorder_id: verified.session.workorderId, target, action: "push-repository", next_step: "服务器已登记本次推送许可,但安全推送接收器尚未部署;不要重试裸 git push、不要索要账号密码。等待受限 bundle 接收器上线后按同一工单回执执行。" }); + manager.recordReceipt(token, operationReceipt); + return json(res, 200, { ok: true, repo, target, expires_at: grant.expires_at, transport: { status: "not_configured", diagnostic_code: "repo_push_transport_unavailable", next_step: operationReceipt.next_step }, receipt: operationReceipt }); } return json(res, 404, { error: "not_found" }); @@ -386,6 +409,25 @@ function clientAddress(req) { const forwarded = String(req.headers["x-forwarded-for"] || "").split(",").map(value => value.trim()).filter(Boolean); return String(forwarded[forwarded.length - 1] || req.socket.remoteAddress || "unknown").slice(0, 96); } +function receipt({ state, diagnostic_code, workorder_id = "", target = "", action = "", evidence = null, next_step = "" }) { + return { schema: "guanghu.operation-receipt/v1", state, diagnostic_code, workorder_id, target, action, occurred_at: Date.now() / 1000, ...(evidence ? { evidence } : {}), next_step }; +} +function safeEvidence(result) { + const clip = value => String(value || "").replace(/(password|token|secret|authorization)\s*[:=]\s*\S+/gi, "$1=[redacted]").slice(0, 1200); + return { exit_code: Number.isInteger(result.exit_code) ? result.exit_code : null, stdout: clip(result.stdout), stderr: clip(result.stderr) }; +} +function failure(error, next_step = "读取 diagnostic_code;按 next_step 处理,勿猜测凭证或切换服务器。", extra = {}) { + return { ok: false, error, receipt: receipt({ state: "blocked", diagnostic_code: error, next_step }), ...extra }; +} +function diagnosticCatalog() { + return { + owner_handoff_required: "工单已创建,等待主人打开申请页并完成预登记邮箱批准。", + map_ack_required: "会话有效,但尚未确认此目标节点的实时导航图。", + action_execution_failed: "服务器固定动作已执行但失败;回执会包含受限证据与下一步。", + repo_push_transport_unavailable: "许可已登记,但安全推送接收器尚未部署,禁止把它误判为 git 凭证。", + session_expired: "会话已过期;以同一目标和范围重新申请工单。", + }; +} function validateWorkorderBody(body, targets, actions) { if (body.email || body.recipient || body.smtp_pass) return { ok: false, status: 400, error: "direct_recipient_forbidden" }; if (!body.persona_id || !body.target || !body.scope || !body.action) return { ok: false, status: 400, error: "missing_required_field" }; diff --git a/server-tools/lake-lamp-authz/server.test.js b/server-tools/lake-lamp-authz/server.test.js index 19b6475..a68295b 100644 --- a/server-tools/lake-lamp-authz/server.test.js +++ b/server-tools/lake-lamp-authz/server.test.js @@ -75,6 +75,7 @@ test("cross-device request is powerless until the owner asks for an email and ap assert.equal(requested.status, 201); const order = await requested.json(); assert.equal(order.status, "waiting_for_owner_handoff"); + assert.equal(order.receipt.diagnostic_code, "owner_handoff_required"); assert.equal(mail.length, 0); assert.match(order.request_url, /\/request\//); assert.doesNotMatch(JSON.stringify(order), /approve\//); @@ -233,6 +234,8 @@ test("registered action bridge executes only after session and map checks", asyn await fetch(`${base}${approvalPath}`, { method: "POST" }); const claimed = await fetch(`${base}/api/workorders/${order.workorder_id}/claim`, { method: "POST", headers: { authorization: `Bearer ${order.claim_token}` } }); const session = await claimed.json(); + const initialStatus = await fetch(`${base}/api/session/status`, { method: "POST", headers: { authorization: `Bearer ${session.session_token}`, "content-type": "application/json" }, body: JSON.stringify({ persona_id: "ICE-GL-ZY001", target: "JD-FD-PRIMARY", scope: "server-login" }) }); + assert.equal((await initialStatus.json()).state, "map_ack_required"); const common = { persona_id: "ICE-GL-ZY001", target: "JD-FD-PRIMARY", scope: "server-login", action: "inspect-services" }; const beforeAck = await fetch(`${base}/api/actions/execute`, { method: "POST", headers: { authorization: `Bearer ${session.session_token}`, "content-type": "application/json" }, body: JSON.stringify(common) }); assert.equal(beforeAck.status, 423); @@ -242,11 +245,40 @@ test("registered action bridge executes only after session and map checks", asyn assert.equal(forbidden.status, 400); const executed = await fetch(`${base}/api/actions/execute`, { method: "POST", headers: { authorization: `Bearer ${session.session_token}`, "content-type": "application/json" }, body: JSON.stringify(common) }); assert.equal(executed.status, 200); + assert.equal((await executed.clone().json()).receipt.state, "succeeded"); + const finalStatus = await fetch(`${base}/api/session/status`, { method: "POST", headers: { authorization: `Bearer ${session.session_token}`, "content-type": "application/json" }, body: JSON.stringify({ persona_id: "ICE-GL-ZY001", target: "JD-FD-PRIMARY", scope: "server-login" }) }); + const finalPayload = await finalStatus.json(); + assert.equal(finalPayload.state, "ready_to_execute"); + assert.equal(finalPayload.last_receipt.diagnostic_code, "action_succeeded"); assert.deepEqual(calls, [{ action: "inspect-services", target: "JD-FD-PRIMARY" }]); }, { mapsDir, mapStateFile: path.join(dir, "acks.json"), executeAction: async request => { calls.push(request); return { ok: true, stdout: "healthy" }; } }); } finally { fs.rmSync(dir, { recursive: true, force: true }); } }); +test("failed server actions return a durable diagnosis instead of making a persona guess", async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "lake-lamp-action-failure-")); + const mapsDir = path.join(dir, "maps"); fs.mkdirSync(mapsDir); + fs.writeFileSync(path.join(mapsDir, "JD-FD-PRIMARY.json"), JSON.stringify({ node_id: "JD-FD-PRIMARY" })); + try { + await withServer(async ({ base, mail }) => { + const requested = await fetch(`${base}/api/public/workorders`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ persona_id: "ICE-GL-ZY001", target: "JD-FD-PRIMARY", scope: "server-login", action: "read-navigation-map" }) }); + const order = await requested.json(); + await fetch(`${base}${new URL(order.request_url).pathname.replace("/authz", "")}`, { method: "POST" }); + await fetch(`${base}${new URL(mail[0].approvalUrl).pathname.replace("/authz", "")}`, { method: "POST" }); + const session = await (await fetch(`${base}/api/workorders/${order.workorder_id}/claim`, { method: "POST", headers: { authorization: `Bearer ${order.claim_token}` } })).json(); + const common = { persona_id: "ICE-GL-ZY001", target: "JD-FD-PRIMARY", scope: "server-login" }; + const map = await (await fetch(`${base}/api/navigation-map/read`, { method: "POST", headers: { authorization: `Bearer ${session.session_token}`, "content-type": "application/json" }, body: JSON.stringify(common) })).json(); + await fetch(`${base}/api/navigation-map/ack`, { method: "POST", headers: { authorization: `Bearer ${session.session_token}`, "content-type": "application/json" }, body: JSON.stringify({ ...common, map_hash: map.map_hash }) }); + const failed = await fetch(`${base}/api/actions/execute`, { method: "POST", headers: { authorization: `Bearer ${session.session_token}`, "content-type": "application/json" }, body: JSON.stringify({ ...common, action: "inspect-services" }) }); + assert.equal(failed.status, 502); + const payload = await failed.json(); + assert.equal(payload.receipt.state, "failed"); + assert.equal(payload.receipt.diagnostic_code, "agent_socket_unavailable"); + assert.match(payload.receipt.next_step, /diagnostic_code/); + }, { mapsDir, mapStateFile: path.join(dir, "acks.json"), executeAction: async () => ({ ok: false, error: "agent_socket_unavailable", stderr: "socket offline" }) }); + } finally { fs.rmSync(dir, { recursive: true, force: true }); } +}); + test("request endpoint rejects direct email target switching and unknown actions", async () => { await withServer(async ({ base }) => { const common = { method: "POST", headers: { authorization: "Bearer request-only-secret", "content-type": "application/json" } }; diff --git a/server-tools/lake-lamp-authz/workorder-manager.js b/server-tools/lake-lamp-authz/workorder-manager.js index 464e7fc..73553f9 100644 --- a/server-tools/lake-lamp-authz/workorder-manager.js +++ b/server-tools/lake-lamp-authz/workorder-manager.js @@ -105,6 +105,7 @@ class WorkOrderManager { const sessionToken = randomToken(); this.sessions.set(hash(sessionToken), { + workorderId: id, persona: order.persona, target: order.target, scope: order.scope, @@ -115,12 +116,19 @@ class WorkOrderManager { expiresAt: now + this.sessionTtl, maxExpiresAt: now + this.maxSessionLifetime, renewals: 0, + lastReceipt: { + schema: "guanghu.operation-receipt/v1", + state: "session_issued", + diagnostic_code: "session_issued", + occurred_at: now, + next_step: "读取并确认该目标节点的实时导航图,然后才可执行已登记动作。", + }, }); order.claimed = true; order.state = "claimed"; order.claimHash = ""; this.persist(); - return { ok: true, sessionToken, expiresIn: this.sessionTtl, target: order.target, scope: order.scope, action: order.action, resource: order.resource || "" }; + return { ok: true, sessionToken, expiresIn: this.sessionTtl, target: order.target, scope: order.scope, action: order.action, resource: order.resource || "", workorderId: id, receipt: { ...this.sessions.get(hash(sessionToken)).lastReceipt } }; } verifySession(sessionToken, persona, target, scope, action, now = Date.now() / 1000, resource = undefined) { @@ -168,6 +176,14 @@ class WorkOrderManager { return { ok: true, expiresAt, renewals: session.renewals, session: { ...session } }; } + recordReceipt(sessionToken, receipt) { + const session = this.sessions.get(hash(sessionToken || "")); + if (!session) return { ok: false, reason: "session_not_found" }; + session.lastReceipt = { ...receipt }; + this.persist(); + return { ok: true, receipt: { ...session.lastReceipt } }; + } + findByApproval(token) { const needle = hash(token || ""); for (const order of this.workorders.values()) {