@sema-agent/server 7.69.0 → 7.71.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +4 -1
  2. package/README.zh-CN.md +4 -1
  3. package/USAGE.md +109 -10
  4. package/dist/approval-ask-machine.d.ts +1 -1
  5. package/dist/approval-card.d.ts +3 -2
  6. package/dist/approval-content-kind.d.ts +13 -7
  7. package/dist/approval-content-kind.js +3 -1
  8. package/dist/approval-reconciler.d.ts +0 -54
  9. package/dist/approval-reconciler.js +2 -2
  10. package/dist/approval.js +3 -2
  11. package/dist/bench/s1/live-deps.js +2 -0
  12. package/dist/boot/execution-env.js +12 -2
  13. package/dist/boot/memory-consolidation.d.ts +2 -1
  14. package/dist/boot/parked-revive-gate.d.ts +2 -0
  15. package/dist/boot/parked-revive-gate.js +15 -10
  16. package/dist/boot/resolve-spec.js +8 -3
  17. package/dist/boot/runner-deps.d.ts +50 -46
  18. package/dist/boot/runner-deps.js +12 -10
  19. package/dist/brain.d.ts +3 -1
  20. package/dist/brain.js +2 -1
  21. package/dist/budget.js +3 -2
  22. package/dist/config-catalog.d.ts +31 -2
  23. package/dist/config-catalog.js +44 -24
  24. package/dist/config-types.d.ts +31 -1
  25. package/dist/config-types.js +1 -0
  26. package/dist/config.js +15 -3
  27. package/dist/deployment-governance.d.ts +6 -3
  28. package/dist/deployment-governance.js +2 -1
  29. package/dist/execution-lane-caps.d.ts +51 -37
  30. package/dist/execution-lane-caps.js +23 -0
  31. package/dist/fleet/fleet-bus.d.ts +22 -1
  32. package/dist/fleet/fleet-bus.js +2 -1
  33. package/dist/http/routes/approvals-assistant.js +3 -12
  34. package/dist/http/routes/diagnostics.js +4 -0
  35. package/dist/http/routes/runs.js +2 -3
  36. package/dist/http/routes/tasks.js +22 -18
  37. package/dist/http/server.js +21 -7
  38. package/dist/index.d.ts +1 -1
  39. package/dist/leader/wire.js +4 -3
  40. package/dist/main.js +6 -4
  41. package/dist/memory-operator-faces.d.ts +1 -1
  42. package/dist/memory-scope.d.ts +24 -0
  43. package/dist/memory-scope.js +12 -3
  44. package/dist/observability/metrics.d.ts +4 -6
  45. package/dist/observability/metrics.js +5 -1
  46. package/dist/observability/permission-rule-events.d.ts +20 -0
  47. package/dist/observability/permission-rule-events.js +28 -0
  48. package/dist/observability/run-terminal-log.d.ts +13 -22
  49. package/dist/observability/run-terminal-log.js +187 -47
  50. package/dist/observability/tool-trace.d.ts +12 -0
  51. package/dist/observability/tool-trace.js +5 -1
  52. package/dist/orchestration/workflow-agent-session-index.d.ts +85 -2
  53. package/dist/orchestration/workflow-agent-session-index.js +38 -5
  54. package/dist/parked-decide.d.ts +40 -1
  55. package/dist/parked-decide.js +19 -2
  56. package/dist/plugins/checkpoint-store-sql.js +5 -5
  57. package/dist/plugins/local-checkpoint-store.js +3 -3
  58. package/dist/plugins/remote-env-adb.d.ts +11 -0
  59. package/dist/plugins/remote-env-adb.js +2 -0
  60. package/dist/plugins/remote-env-device.d.ts +9 -0
  61. package/dist/plugins/remote-env-device.js +3 -0
  62. package/dist/plugins/remote-env-e2b.d.ts +11 -0
  63. package/dist/plugins/remote-env-e2b.js +2 -0
  64. package/dist/plugins/remote-env-host.d.ts +11 -0
  65. package/dist/plugins/remote-env-host.js +2 -0
  66. package/dist/plugins/remote-env-k8s.d.ts +11 -0
  67. package/dist/plugins/remote-env-k8s.js +2 -0
  68. package/dist/plugins/remote-env-local-docker.d.ts +11 -0
  69. package/dist/plugins/remote-env-local-docker.js +2 -0
  70. package/dist/plugins/remote-env-ssh.d.ts +11 -0
  71. package/dist/plugins/remote-env-ssh.js +2 -0
  72. package/dist/plugins/store-backend.d.ts +36 -13
  73. package/dist/plugins/store-backend.js +12 -3
  74. package/dist/plugins/store-contracts.d.ts +54 -18
  75. package/dist/plugins/store-contracts.js +17 -30
  76. package/dist/plugins/workflow-journal-store-sql.d.ts +1 -1
  77. package/dist/plugins/workflow-journal-store-sql.js +3 -10
  78. package/dist/plugins/workflow-run-store-sql.d.ts +31 -9
  79. package/dist/plugins/workflow-run-store-sql.js +28 -1
  80. package/dist/posture-source.d.ts +23 -3
  81. package/dist/posture-source.js +1 -1
  82. package/dist/read-face-posture.d.ts +8 -1
  83. package/dist/read-face-posture.js +1 -0
  84. package/dist/run-cancel-context.d.ts +50 -1
  85. package/dist/run-cancel-context.js +22 -0
  86. package/dist/run-local.d.ts +14 -6
  87. package/dist/run-local.js +4 -1
  88. package/dist/runs.d.ts +0 -51
  89. package/dist/runs.js +40 -25
  90. package/dist/task-a2a.js +2 -2
  91. package/dist/task-mcp.js +2 -2
  92. package/dist/task-settings.d.ts +21 -8
  93. package/dist/task-settings.js +2 -2
  94. package/dist/tool-approval.d.ts +14 -8
  95. package/dist/tool-approval.js +3 -3
  96. package/dist/trace/core-keyset-guard.d.ts +135 -11
  97. package/dist/trace/engine-notice-wire.d.ts +1 -1
  98. package/dist/trace/engine-notice-wire.js +1 -0
  99. package/dist/trace/ledger-events.d.ts +22 -2
  100. package/dist/trace/ledger-events.js +6 -1
  101. package/dist/trace/ledger-sink.d.ts +10 -8
  102. package/dist/trace/ledger-sink.js +10 -2
  103. package/dist/trace/project.d.ts +24 -6
  104. package/dist/trace/project.js +5 -3
  105. package/dist/trace/redact.d.ts +28 -0
  106. package/dist/trace/redact.js +58 -30
  107. package/package.json +3 -3
@@ -96,17 +96,25 @@ export interface LocalApproverIo {
96
96
  /**
97
97
  * [ref] 件三:**onAsk 座**(§0 潜雷 / §5 锁 6)。
98
98
  *
99
- * 🔴 为什么这个座位是接治理链的**同窗必做项**:run-local 有 checkpointStore(file backend),所以
100
- * `AUTONOMY=ask` ⇒ `shellGate:"always"` ⇒ Bash 进 `irreversibleTools` ⇒ park lane effective ⇒ **每一次
101
- * shell 调用都 durable park 成一张永远没人 resume 的 checkpoint**,任务挂起、CLI 退 1。一次性 CLI 里
102
- * 没有任何赎回腿,那不是「等人来批」,是卡死。core 的 `isLiveApproverSeat(onAsk)` 为真时 `suspendAsk`
103
- * 早退(prepare-task dist 亲读),ask 于是落回本回调 —— 把 park 换成「问人 / 当场拒」的唯一正当接法。
99
+ * 🔴 为什么这个座位是接治理链的**同窗必做项**(成因,留档):彼时 run-local 的主 runner 带着
100
+ * checkpointStore(file backend),于是 `AUTONOMY=ask` ⇒ `shellGate:"always"` ⇒ Bash 进
101
+ * `irreversibleTools` ⇒ park lane effective ⇒ **每一次 shell 调用都 durable park 成一张永远没人 resume
102
+ * 的 checkpoint**,任务挂起、CLI 退 1。一次性 CLI 里没有任何赎回腿,那不是「等人来批」,是卡死。
103
+ * core 的 `isLiveApproverSeat(onAsk)` 为真时 `suspendAsk` 早退(prepare-task dist 亲读),ask 于是落回
104
+ * 本回调 —— 把 park 换成「问人 / 当场拒」的正当接法。
105
+ *
106
+ * 🔴 **本席不是那颗雷的唯一防线,也从来不该是**(B-060 批 codex 交叉复审 [medium] 的教训):它只挡
107
+ * **审批**那条车道;core 的**平台/资源停驻**(`suspendForPlatformLimit`)由「店在不在」一位武装,
108
+ * 压根不经过 onAsk。所以自 B-060 起本腿的结构性防线是**整条腿不接 checkpointStore**(装配点在
109
+ * `createSharedRunnerDeps` 调用处,理由全文在那里),本席退回它本来的职责:**给 TTY 前的人一次真的
110
+ * 问答**。两道并存不是冗余 —— 一道管「谁来答」,一道管「答不了时会不会留下无人兑付的行」。
104
111
  *
105
112
  * ⛔ **不在 toolPolicy 层做 ask→deny 映射**:core 自己就有 headless auto-deny 的语义与文案,在策略层
106
113
  * 复刻一份 = 同源谎(两处判据日后各改各的)。裁决归 core,座位归这里。
107
114
  *
108
115
  * 🔴 非 TTY 臂答 `false`(真 deny),**绝不答 `"unavailable"`**:后者被 core 读成「席位够不到」并**改道
109
- * `suspendAsk`**(dist `core/hooks.js:296` 的 `approverUnavailable` 重投),上面那颗雷会原样回来。
116
+ * `suspendAsk`**(dist `core/hooks.js:296` 的 `approverUnavailable` 重投)。今天本腿无店 ⇒ 那条改道无处
117
+ * 可去(fail-closed 拒),但这一条仍是本席的契约:答案是「不批」,不是「找不到人」。
110
118
  */
111
119
  export declare function createLocalApprover(io: LocalApproverIo): OnAsk;
112
120
  /** 默认的 TTY 问人器:一问一答,回答写 **stderr**(stdout 是任务结果的专用管道,别污染它)。
package/dist/run-local.js CHANGED
@@ -19,6 +19,7 @@ import { loadConfig, logConfigDiagnostics, parseNumOrFailNonNegative } from "./c
19
19
  import { createConfigProvider } from "./config-provider.js";
20
20
  import { applyEffective, applyCenterReadFace, resolveMcpServers, mcpForScenario, resolveA2aPeers, a2aForScenario } from "./config-center/facade.js";
21
21
  import { hostExecutionEnvFactory } from "./plugins/remote-env-host.js";
22
+ import { executionLaneHomeDir } from "./execution-lane-caps.js";
22
23
  import { makeLoadProjectMemory, makeProbeInstructionSources } from "./project-memory.js";
23
24
  import { loadSkills } from "./capabilities/skills.js";
24
25
  import { createRepoClient } from "./capabilities/repo-tools.js";
@@ -273,6 +274,7 @@ export async function runLocal(argv, deps = {}) {
273
274
  const executionEnvFactory = hostRx
274
275
  ? hostExecutionEnvFactory({
275
276
  workspaceDir,
277
+ ...((h) => (h === undefined ? {} : { homeDir: h }))(executionLaneHomeDir(config)),
276
278
  backgroundShell: config.requirePrincipal !== true,
277
279
  ...(hostRx.commandTimeoutMs != null ? { commandTimeoutMs: hostRx.commandTimeoutMs } : {}),
278
280
  })
@@ -369,6 +371,7 @@ export async function runLocal(argv, deps = {}) {
369
371
  ],
370
372
  });
371
373
  const sharedRunnerDeps = createSharedRunnerDeps({
374
+ metrics,
372
375
  config,
373
376
  governanceSeams,
374
377
  logger,
@@ -387,6 +390,7 @@ export async function runLocal(argv, deps = {}) {
387
390
  ...(config.hooksTimeoutMs !== undefined ? { timeoutMs: config.hooksTimeoutMs } : {}),
388
391
  },
389
392
  toolResultStore: fileBackend.toolResultStore,
393
+ checkpointStore: undefined,
390
394
  sessionPolicyStore: fileBackend.sessionPolicyStore,
391
395
  usageWindowStore: config.usageWindows ? fileBackend.usageWindowStore : undefined,
392
396
  orgMemoryAdmission,
@@ -397,7 +401,6 @@ export async function runLocal(argv, deps = {}) {
397
401
  ...sharedRunnerDeps,
398
402
  sessionStore,
399
403
  ...(memoryEngine ? { memoryBackend: memoryEngine.backend, memoryEngineDir: memoryEngine.root } : {}),
400
- checkpointStore: fileBackend.checkpointStore,
401
404
  onError: engineOnError,
402
405
  onAsk,
403
406
  ...(config.projectMemoryEnabled
package/dist/runs.d.ts CHANGED
@@ -97,57 +97,6 @@ export declare function markChildrenStoppedByUserOnAbort(signal: AbortSignal, ta
97
97
  export declare function isSessionConflictResult(result: {
98
98
  terminal: TerminalCause;
99
99
  }): boolean;
100
- /**
101
- * E18 resume-at CALLER error → an HTTP 4xx. A bad/stale resume-at target (the resolved entryId is gone or is not a
102
- * settled message boundary) surfaces as a FAILED TaskResult carrying a `resume_at.*` errorCode (core swallows the
103
- * prepare-throw into the result, exactly like a session conflict — runtask catch → status:"failed"+errorCode). The
104
- * sync path otherwise 200s a failed result; map these to 4xx so a caller mistake reads as one. `not_found` (the
105
- * entry is genuinely absent) → 404; everything else (not_a_message / conflicts_resume / no_session) → 422.
106
- *
107
- * [ref] Phase3: `requireExistingSession` on a genuinely-missing/purged session ALSO surfaces as a failed result
108
- * (core prepare-task maps the store's `not_found` → `resume.session_not_found`); map it to 404 (the session is gone),
109
- * the same "caller mistake reads as a 4xx, not a silent 200 fresh run" discipline.
110
- *
111
- * [ref] rewind exclusive mode (core 1.292 resumeAtMode:"before"): its two edge rejections ride the same `resume_at.*`
112
- * prefix (before_target_not_user / before_root_unsupported → 422 via the existing branch, errorCode passed through
113
- * UNCHANGED for the shell). `rewind_snapshot.unresolvable` (rewindFiles + "before": no snapshot at/above the branch
114
- * point) is core's third rejection with a DIFFERENT prefix — same caller-mistake shape (prepare-throw, nothing
115
- * billed), map it to 422 explicitly so it doesn't fall through to a 200-with-failed body.
116
- *
117
- * [ref] 件A([ref] 件4): core 5.13.0's org-memory admission rides the SAME prepare-throw pipe
118
- * (GOVERNANCE_CODES). `memory.admission_denied` is TERMINAL — the principal has no grant on that tenant
119
- * plane → 403 (a retry cannot change the verdict). `memory.admission_required` is TRANSIENT fail-closed —
120
- * the directory is unreachable / no resolver is wired → 503 (retry later; the sync leg forwards the
121
- * result's `retryAfterMs` as body `retryAfterSec`, same family shape as usage.window_exhausted). Exact
122
- * codes only, no memory.* family grab — an unknown sibling stays a 200-with-failed-body until cataloged.
123
- *
124
- * [ref] (core 7.0.0 片2/3, [ref] S-3): the session capture opt-out's four prepare-throw refusals ride the
125
- * same pipe. `memory.capture_optout_denied` is TERMINAL (the principal's `allowMemoryOptOut` verdict is an
126
- * explicit `false`, or the posture is `capture-required` / `governed`-with-absent-verdict — a retry cannot
127
- * change it) → 403, the `memory.admission_denied` twin. `memory.capture_optout_unpersisted` is the
128
- * declaration arm's "control-plane record could not be written" refusal — a store outage, retry may succeed
129
- * → 503 (transient fail-closed, the `memory.admission_required` twin; no retryAfterMs on this one — core
130
- * mints none, so the sync leg forwards no hint rather than inventing one). `config.memory_capture_spelling`
131
- * is a TaskSpec authoring mistake (the only legal spelling is the string "off") → 400, the
132
- * `config.memory_erasure_request` family shape. `config.memory_capture_unsupported` is the DEPLOYMENT SHAPE
133
- * refusing the declaration (remote/per-run execution with no durable capture-record carrier: the record
134
- * would not survive to a resume replica) → 409 — the request is well-formed, the deployment's state
135
- * conflicts with it, the `config.refresh_unavailable` family shape ("change the deployment, not the request").
136
- * Status choices are pinned by the `config.`/`memory.` family status sets in error-code-catalog-live.test.ts.
137
- *
138
- * S-125 件⑥(core 7.4.0 [ref]):`rewind.child_scope_unsupported` —— 这条腿是一个**委派子代**,它的文件编辑
139
- * 记在**根 session** 的文件历史里,所以「把文件还原到某个 entry」是根 session 的动作、不是子代的;core 在
140
- * prepare 期响亮拒(零计费,文件一个字节没动)。映射 **422**:请求形式合法、语义上找错了执行者 —— 与
141
- * `resume_at.*` 的「调用方指错了锚」同一族同一档,而不是 404(目标在,只是不归你)也不是 409(部署状态没冲突)。
142
- * ⚠️ **前缀不吞**:本码前缀是 `rewind.`,与下面那条 `resume_at.` **开集前缀臂**不同族 —— 它不会被那一臂顺手
143
- * 兜走(兜走会把一条「找错执行者」的拒答成「锚解析不了」),也不是 `rewind_snapshot.`(那是「快照找不到」)。
144
- * 三个前缀各自成族,逐码对表见 `test/resume-anchor.test.ts` 的 S-125 `rewind.` 族格。
145
- * 🔴 **可达性如实**:本仓的 HTTP 面**今天铸不出**这条腿 —— 子代由 core 内部的 Agent 工具派生,
146
- * `fileHistoryLineage` 是 core prepare 的 internals(不在 `TaskSpec` 上),server 从不直接 prepare 一条带
147
- * lineage 的子代腿。本臂是**分诊面的完备性**(core 说 X ⇒ wire 形是 Y),不是一条今天在跑的路;将来的
148
- * 委派入口接线那天零改动即生效。同族其余五码今天**仍落 200-with-failed-body**,那是本批**之前**就存在的
149
- * 缺口(不是本批回归),已在上述对表格里逐码显式登记为债务,并由那道门看住不许陈腐。
150
- */
151
100
  export declare function resumeAtHttpStatus(result: {
152
101
  terminal: TerminalCause;
153
102
  }): number | undefined;
package/dist/runs.js CHANGED
@@ -7,7 +7,7 @@ import { taskNotificationEventData, appendModelUsageDelta, appendPromptManifest,
7
7
  import { createLedgerSink } from "./trace/ledger-sink.js";
8
8
  import { registerEngineNoticeLeg } from "./trace/engine-notice-wire.js";
9
9
  import { recordTurnActivity } from "./turn-activity.js";
10
- import { buildCancelContext } from "./run-cancel-context.js";
10
+ import { CANCELLED_CODE, mintCancelledResult } from "./run-cancel-context.js";
11
11
  import { createApprovalCardEmitter, resolveApprovalLeg } from "./tool-approval.js";
12
12
  import { fleetRunResiduals, isFleetAgentTerminalNotification } from "./fleet/fleet-bus.js";
13
13
  import { defaultSubagentTailBus, projectTailFrame } from "./fleet/subagent-tail-bus.js";
@@ -66,30 +66,35 @@ export function isSessionConflictResult(result) {
66
66
  return result.terminal.code === "conflict";
67
67
  return /stale write|concurrent write|conflict/i.test(result.terminal.message ?? "");
68
68
  }
69
+ const GOVERNANCE_HTTP_STATUS = {
70
+ "memory.admission_denied": 403,
71
+ "memory.admission_required": 503,
72
+ "memory.capture_optout_denied": 403,
73
+ "config.tool_mount_denied": 400,
74
+ "config.locked_key": 400,
75
+ "usage.window_exhausted": null,
76
+ "config.compliance_required": null,
77
+ "config.compliance_denied": null,
78
+ };
69
79
  export function resumeAtHttpStatus(result) {
70
80
  const code = failureCodeOf(result.terminal);
71
81
  if (code === undefined)
72
82
  return undefined;
83
+ if (Object.hasOwn(GOVERNANCE_HTTP_STATUS, code)) {
84
+ return GOVERNANCE_HTTP_STATUS[code] ?? undefined;
85
+ }
73
86
  if (code === "resume.session_not_found")
74
87
  return 404;
75
88
  if (code === "rewind_snapshot.unresolvable")
76
89
  return 422;
77
90
  if (code === "rewind.child_scope_unsupported")
78
91
  return 422;
79
- if (code === "memory.admission_denied")
80
- return 403;
81
- if (code === "memory.admission_required")
82
- return 503;
83
- if (code === "memory.capture_optout_denied")
84
- return 403;
85
92
  if (code === "memory.capture_optout_unpersisted")
86
93
  return 503;
87
94
  if (code === "config.memory_capture_spelling")
88
95
  return 400;
89
96
  if (code === "config.invalid_checkpoint_store")
90
97
  return 400;
91
- if (code === "config.tool_mount_denied")
92
- return 400;
93
98
  if (code === "config.memory_capture_unsupported")
94
99
  return 409;
95
100
  if (!code.startsWith("resume_at."))
@@ -166,10 +171,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
166
171
  const preemptCtrl = new AbortController();
167
172
  if (preemptEligible)
168
173
  preemptable?.set(taskId, preemptCtrl);
169
- const cancelledResult = () => {
170
- const ctx = buildCancelContext(taskId, startedAt);
171
- return { taskId, sessionId: spec.sessionId, terminal: { kind: "failed", code: "cancelled", message: "cancelled by user" }, stats: { turns: 0, tokens: 0 }, ...(ctx ? { cancelContext: ctx } : {}) };
172
- };
174
+ const cancelledResult = (engineResult) => mintCancelledResult({ taskId, sessionId: spec.sessionId ?? "", startedAtMs: startedAt, message: "cancelled by user", ...(engineResult !== undefined ? { engineResult } : {}) });
173
175
  const recordDone = (result, status) => {
174
176
  metrics?.addGauge("runs_active", -1);
175
177
  metrics?.inc("tasks_total", { status });
@@ -216,6 +218,9 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
216
218
  heartbeat.unref?.();
217
219
  const anchor = new TurnAnchorCapture(captureTurnAnchor, () => metrics?.inc("resume_anchor_capture_failed"), captureUserMessageAnchor);
218
220
  let reached;
221
+ let observedDone;
222
+ let cancelledAtDone = false;
223
+ let settledAsCancel = false;
219
224
  let steerableStreamRef;
220
225
  let legLive = true;
221
226
  const notifiedKeys = new NotifiedKeys();
@@ -343,6 +348,10 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
343
348
  steerableStreamRef = stream;
344
349
  steerable?.set(taskId, stream);
345
350
  for await (const ev of stream) {
351
+ if (ev.type === "done") {
352
+ observedDone = ev.result;
353
+ cancelledAtDone = cancelCtrl.signal.aborted;
354
+ }
346
355
  fleetPublisher?.onEvent(ev);
347
356
  await flushPromptManifest();
348
357
  switch (ev.type) {
@@ -377,12 +386,14 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
377
386
  }
378
387
  evictIfConflict(runner, spec.sessionId, ev.result);
379
388
  {
380
- const safe = await withModelUsage(stripResumeToken(ev.result));
389
+ const settled = cancelledAtDone ? cancelledResult(ev.result) : ev.result;
390
+ settledAsCancel = failureCodeOf(settled.terminal) === CANCELLED_CODE;
391
+ const safe = await withModelUsage(stripResumeToken(settled));
381
392
  await sink.appendDone(safe);
382
393
  reached = { kind: "done", result: safe };
383
394
  const plane = terminalProjection(safe.terminal);
384
395
  await runStore.setTerminal(taskId, plane.status, safe, plane.errorMessage ?? null);
385
- recordDone(safe, plane.status);
396
+ recordDone(safe, failureCodeOf(safe.terminal) === CANCELLED_CODE ? CANCELLED_CODE : plane.status);
386
397
  }
387
398
  continue;
388
399
  }
@@ -429,15 +440,16 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
429
440
  recordFailOpen("server.runs.terminal-ledger-write-failed", `arm=flush-clean-cancel ${errText(e)}`);
430
441
  }
431
442
  const c = cancelledResult();
443
+ settledAsCancel = failureCodeOf(c.terminal) === CANCELLED_CODE;
432
444
  try {
433
- await append("failed", { errorMessage: "cancelled by user", errorCode: "cancelled" });
445
+ await append("failed", { errorMessage: "cancelled by user", errorCode: CANCELLED_CODE });
434
446
  }
435
447
  catch (e) {
436
448
  recordFailOpen("server.runs.terminal-ledger-write-failed", `arm=append-clean-cancel ${errText(e)}`);
437
449
  }
438
450
  reached = { kind: "done", result: c };
439
451
  await runStore.setTerminal(taskId, "failed", c, terminalProjection(c.terminal).errorMessage ?? null);
440
- recordDone(c, "cancelled");
452
+ recordDone(c, CANCELLED_CODE);
441
453
  }
442
454
  }
443
455
  catch (err) {
@@ -451,6 +463,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
451
463
  }
452
464
  if (reached?.kind === "done") {
453
465
  const plane = terminalProjection(reached.result.terminal);
466
+ settledAsCancel = failureCodeOf(reached.result.terminal) === CANCELLED_CODE;
454
467
  await runStore.setTerminal(taskId, plane.status, reached.result, plane.errorMessage ?? null);
455
468
  }
456
469
  else if (reached?.kind === "suspended") {
@@ -459,10 +472,11 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
459
472
  else if (reached?.kind === "needs_review") {
460
473
  await runStore.setNeedsReview(taskId);
461
474
  }
462
- else if (cancelCtrl.signal.aborted) {
475
+ else if (cancelCtrl.signal.aborted && observedDone === undefined) {
463
476
  const c = cancelledResult();
477
+ settledAsCancel = failureCodeOf(c.terminal) === CANCELLED_CODE;
464
478
  try {
465
- await append("failed", { errorMessage: "cancelled by user", errorCode: "cancelled" });
479
+ await append("failed", { errorMessage: "cancelled by user", errorCode: CANCELLED_CODE });
466
480
  }
467
481
  catch (e) {
468
482
  recordFailOpen("server.runs.terminal-ledger-write-failed", `arm=append-thrown-cancel ${errText(e)}`);
@@ -470,6 +484,7 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
470
484
  await runStore.setTerminal(taskId, "failed", c, terminalProjection(c.terminal).errorMessage ?? null);
471
485
  }
472
486
  else {
487
+ settledAsCancel = false;
473
488
  const safeMsg = redactErrorMessage(message) ?? message;
474
489
  try {
475
490
  await append("failed", { errorMessage: safeMsg });
@@ -483,13 +498,13 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
483
498
  catch {
484
499
  }
485
500
  if (reached?.kind === "done")
486
- recordDone(reached.result, failureCodeOf(reached.result.terminal) === "cancelled" ? "cancelled" : terminalProjection(reached.result.terminal).status);
501
+ recordDone(reached.result, failureCodeOf(reached.result.terminal) === CANCELLED_CODE ? CANCELLED_CODE : terminalProjection(reached.result.terminal).status);
487
502
  else if (reached?.kind === "suspended") {
488
503
  metrics?.addGauge("runs_active", -1);
489
504
  metrics?.inc("tasks_total", { status: "suspended" });
490
505
  }
491
- else if (cancelCtrl.signal.aborted) {
492
- recordDone(null, "cancelled");
506
+ else if (settledAsCancel) {
507
+ recordDone(null, CANCELLED_CODE);
493
508
  }
494
509
  else {
495
510
  recordDone(null, "failed");
@@ -500,14 +515,14 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
500
515
  unregisterEngineNotice();
501
516
  if (fleetPublisher) {
502
517
  if (reached?.kind === "done")
503
- fleetPublisher.onTerminal(failureCodeOf(reached.result.terminal) === "cancelled" ? "cancelled" : terminalProjection(reached.result.terminal).status, fleetRunResiduals(reached.result));
518
+ fleetPublisher.onTerminal(failureCodeOf(reached.result.terminal) === CANCELLED_CODE ? CANCELLED_CODE : terminalProjection(reached.result.terminal).status, fleetRunResiduals(reached.result));
504
519
  else if (reached?.kind === "suspended")
505
520
  fleetPublisher.onTerminal("suspended");
506
521
  else if (reached?.kind === "needs_review")
507
522
  fleetPublisher.onTerminal("needs_review");
508
523
  else
509
- fleetPublisher.onTerminal(cancelCtrl.signal.aborted ? "cancelled" : "failed", {
510
- ...(cancelCtrl.signal.aborted ? { stoppedBy: "user" } : {}),
524
+ fleetPublisher.onTerminal(settledAsCancel ? CANCELLED_CODE : "failed", {
525
+ ...(settledAsCancel ? { stoppedBy: "user" } : {}),
511
526
  ...(spec.sessionId ? { transcriptId: spec.sessionId } : {}),
512
527
  });
513
528
  }
package/dist/task-a2a.js CHANGED
@@ -1,4 +1,4 @@
1
- import { resolveComplianceDenies } from "@sema-agent/core";
1
+ import { resolveComplianceDenies, TOOL_WIRE_NAME_MAX_CHARS } from "@sema-agent/core";
2
2
  import { HttpError } from "./security.js";
3
3
  export const MAX_REQUEST_A2A_PEERS = 32;
4
4
  export function a2aInjectionHonored(config) {
@@ -19,7 +19,7 @@ function normalizeA2aPeer(e) {
19
19
  if (!e || typeof e !== "object")
20
20
  return null;
21
21
  const o = e;
22
- if (typeof o.name !== "string" || o.name.length === 0 || o.name.length > 128)
22
+ if (typeof o.name !== "string" || o.name.length === 0 || o.name.length > TOOL_WIRE_NAME_MAX_CHARS)
23
23
  return null;
24
24
  if (typeof o.url !== "string" || !HTTP_URL_RE.test(o.url))
25
25
  return null;
package/dist/task-mcp.js CHANGED
@@ -1,4 +1,4 @@
1
- import { mcpInjectionDroppedNotice, resolveComplianceDenies } from "@sema-agent/core";
1
+ import { mcpInjectionDroppedNotice, resolveComplianceDenies, TOOL_WIRE_NAME_MAX_CHARS } from "@sema-agent/core";
2
2
  import { HttpError } from "./security.js";
3
3
  import { CONTENT_ORIGIN_WORDS, describeBadContentOrigin, isContentOriginWord } from "./mcp-content-origin.js";
4
4
  import { readMcpSourceLabel } from "./mcp-source-label.js";
@@ -26,7 +26,7 @@ function normalizeServer(e, noteBadOrigin) {
26
26
  if (!e || typeof e !== "object")
27
27
  return bad("entry");
28
28
  const o = e;
29
- if (typeof o.name !== "string" || o.name.length === 0 || o.name.length > 128)
29
+ if (typeof o.name !== "string" || o.name.length === 0 || o.name.length > TOOL_WIRE_NAME_MAX_CHARS)
30
30
  return bad("name");
31
31
  const t = o.transport;
32
32
  if (!t || typeof t !== "object")
@@ -312,15 +312,28 @@ export interface FsWriteGateWiring {
312
312
  /**
313
313
  * 🔴 S-177 / codex r2 [high](验真后修)—— **执行环境的 home**(core `PathRuleBases.home`,`~/…` 形的基)。
314
314
  *
315
- * core 对缺席的 `home` 回落 `os.homedir()` —— 那是**这个 server 进程**的 home。引擎与执行同机时它就是
316
- * 对的;**远端车道**(SSH / 容器 / 沙箱)上执行用户的 home 与 server 的 home 是两个目录,于是
317
- * `deny Edit(~/.sema/**)` 编译成功、却拿 server 的 home 去匹配远端的目标 ⇒ 一条收紧规则**静默失效**
318
- * (与 `taskRoot` 那条哨兵病同族、同方向)。
315
+ * 🔴 **S-213④ 起本字段在远端车道上也可以有值**:四只远端 adapter 此前一个都不声明 `ExecutionEnv.homeDir`,
316
+ * 而本字段那时只在 `remoteExec` 整个缺席时才铸 —— 于是**每一条远端部署的 `~/` 规则都恒 422**(接入审计
317
+ * M13)。修法不是放松下面那道门,而是让部署**真的知道** home:`execution-lane-caps.ts` 的
318
+ * `executionLaneHomeDir` 是唯一属主,`boot/execution-env.ts` 把它递给 adapter 的 `homeDir` 座、
319
+ * `boot/resolve-spec.ts` / `boot/parked-revive-gate.ts` 把**同一个值**递成本字段。两处同源是承重的:
320
+ * 各算一份就会出现「422 门放行而引擎判 unreadable」或反过来。
319
321
  *
320
- * ⇒ 本字段只在 home **真知道**时在场(引擎与执行同机)。缺席时,`~/` 形的规则由
321
- * {@link rejectUnmatchableSettingsNames} **响亮拒**(422),绝不回落到 server 的 home。
322
- * ⚠️ 上游缺口如实登记:core 今天没有「这条规则需要哪些 base」的查询口,也不把 `home` 当必答项 ——
323
- * 所以「这条规则用了 `~/` 吗」由本仓读**core 自己解析出的规范 command**(`~/` 前缀)来判,不自铸文法。
322
+ * 🔴 **S-213⑦ 改真话**(亲读 core 7.12.0 `dist/core/permission-rule-model.js:365-372` /
323
+ * `:340-350`,推翻本注此前的两句):
324
+ * · core 对缺席的 `home` **不**回落 `os.homedir()` —— `resolvePathPattern` 走
325
+ * `lexicalNormalAbsolutePathOf(baseValueOf("home", bases) ?? "")`,base 缺席 ⇒ `undefined` ⇒ 直接回
326
+ * `{missingBase:"home"}`,该调用的射程判 `unreadable`(fail-closed 的 ask,人来清),从不猜一个目录。
327
+ * 换言之引擎侧本来就没有「拿 server 的 home 去匹配远端目标」这条静默腿(那句旧注是本仓自己的推断)。
328
+ * · core **有**「这条规则需要哪些 base」的查询口 —— 根导出的 `ruleBasesNeeded(rule)`
329
+ * (`//abs` ⇒ 空、`~/` ⇒ `["home"]`、`/x` ⇒ `["root"]`、其余 ⇒ `["cwd"]`),`resolvePathPattern`
330
+ * 自己也读它。本仓的判据腿今天读的是 core 解析出的规范 command 前缀,与该查询口**同一分类**、
331
+ * 不是第二套文法;两者哪天分叉由这条注负责被人看见。
332
+ *
333
+ * ⇒ 本字段在**这条部署的执行车道 home 已知**时在场(`executionLaneHomeDir`,见 `execution-lane-caps.ts`)。
334
+ * 缺席时 `~/` 形规则由 {@link rejectUnmatchableSettingsNames} **响亮拒**(422),而不是让它编译成一条
335
+ * core 只会判 `unreadable` 的规则 —— 两道门同向(都不猜目录),422 只是把同一个拒挪到更早、更响的地方:
336
+ * 运维在**下单那一刻**就知道这条规则这条车道读不了,而不是等到跑起来每次都弹一张 ask。
324
337
  */
325
338
  taskHome?: string;
326
339
  /** The session scratchpad dir (envFacts.scratchpadDir, [ref]③) — writes there are always auto-allowed. */
@@ -279,8 +279,8 @@ function rejectUnmatchableSettingsNames(perms, pathBases) {
279
279
  const parsed = parseRuleText(name, behavior);
280
280
  if ("rule" in parsed && ruleToolGrammarOf(parsed.rule.tool) === "path" && parsed.rule.command.startsWith("~/")) {
281
281
  throw new Error(`${field} has a home-relative path rule (${name}) but this deployment cannot resolve the EXECUTION environment's home directory — ` +
282
- `on a remote lane the engine's own home is a different user's directory, so the rule would compile and then match nothing that runs. ` +
283
- `Spell the path absolutely (${"Edit(//abs/path/**)"} form), or run the task on a lane whose home this worker knows.`);
282
+ `the engine never guesses one: without a home base the rule resolves to "unreadable" and every matching call stops to ask, so it guards nothing. ` +
283
+ `Spell the path absolutely (${"Edit(//abs/path/**)"} form), declare the lane's home with REMOTE_EXEC_HOME_DIR, or run the task on a lane whose home this worker knows.`);
284
284
  }
285
285
  }
286
286
  }
@@ -145,9 +145,10 @@ export interface ToolApprovalFrame {
145
145
  origin?: string;
146
146
  /**
147
147
  * **S-185 / [ref]**(core 7.10.0;**ADDITIVE**,`"tool_approval"` only)—— 这一问**为什么**到了人手上:
148
- * auto 档的分类器被咨询过、而它**跑不了**。`cause` = core 的闭三词
149
- * (`AUTO_MODE_UNAVAILABLE_CAUSES` = `error` / `timeout` / `breaker_open`),`AskRequest.classifierUnavailable`
150
- * 逐字透传。
148
+ * 分类器被咨询过、而它**跑不了**。`cause` = core 的闭集词
149
+ * (`AUTO_MODE_UNAVAILABLE_CAUSES`;core 7.12.0 起 = `error` / `timeout`),`AskRequest.classifierUnavailable`
150
+ * 逐字透传。core 7.12.0 / [ref] 起 ask 上这一位只由**策略层自声明**铸(引擎自己的那份改骑 deny —— 全文
151
+ * 在 `approval-card.ts` 同名键的顶注)。
151
152
  *
152
153
  * 🔴 **缺席 ≠「分类器好着呢」**:缺席同时覆盖三形 —— 分类器答上了 / 这只 ask 没资格走分类器
153
154
  * (`classifierMayAnswer` 的闭表)/ 本部署压根没接分类器。消费端**禁**把缺席读成对分类器健康的断言。
@@ -176,13 +177,18 @@ export interface ToolApprovalFrame {
176
177
  * 执行的那串字节(卡上显示的与真跑的不是同一个东西),**比不披露更坏**;所以 server 只报「有」,显形
177
178
  * 归壳的渲染面(转义/高亮/加标记,壳自己定)。
178
179
  *
179
- * **判据素材** = `boundArgs` 那**一次**序列化的结果(脱敏后的 args),且在**字节帽判定之前**算:
180
+ * **判据素材** = `boundArgs` 里对**原字节**(`redactDeep` 之前)的那一次序列化,且在**字节帽判定之前**算:
180
181
  * · 超帽时 args 以 `argsOmitted:true` 缺席上帧(卡上根本没有 args)—— 那正是人最看不见输入的一格,
181
182
  * 更需要知道「你看不到的那份里有隐形字符」,所以帽不影响本键;
182
- * · 序列化失败(循环引用 / 抛错的 `toJSON` / BigInt)⇒ 两件事一起交白卷:args 不上帧,本键**不铸**;
183
- * · 用脱敏后那份而不是原文,是因为它与卡面/`card_json` 是**同一份**素材(一次算定,三面同源),
184
- * 且人眼看到的正是它。理论残余如实记:一段**被遮蔽掉的密钥**里的 bidi 会随遮蔽一起消失 —— 已知、
185
- * 可忽略(密钥形不含 bidi),且方向安全(少报一次,不会误报干净为脏)。
183
+ * · 序列化失败(循环引用 / 抛错的 `toJSON` / BigInt)⇒ 两件事一起交白卷:args 不上帧,本键**不铸**
184
+ * (两次 `JSON.stringify` 共用同一个 try/catch —— 顶注反对的是第二个**静默降级点**,不是第二次序列化);
185
+ * · 🔴 **7.70.0(S-103)改素材**:此前用的是脱敏后那份(理由=与卡面同源),而本版 `redactSecrets` 入口
186
+ * 先剥全部 `\p{Cf}` —— bidi 控制符**整族都是** `\p{Cf}` ⇒ 在脱敏后的文本上扫,本键恒缺席、E-14 整条
187
+ * 失效(六格红先复现)。所以判据素材改回原字节;同源那条理由让位于「信号必须真」。
188
+ * · ⚠️ **上面「字节零改」那一条自 7.70.0 起对 `\p{Cf}` 族不再成立**:帧/卡上的 args 是**剥后**字节
189
+ * (与 core 显示面 `renderUntrustedCommandText` 同律,显示的是真正执行的逻辑顺序),壳因此**拿不到
190
+ * 「在哪一位」**、只能按整只 args 打标。这是有意的取舍(夹层密钥的根治要求单一剥点),不是遗漏;
191
+ * 要恢复定位面需要给脱敏器一条 span/偏移表回填路(S-103 定案明确不做),不是在这条腿上加特判。
186
192
  *
187
193
  * 判据**不看 `message`**(引擎/策略写的说明文本,不是待执行输入):本键被壳读作「**待执行的输入**可能
188
194
  * 在骗你的眼睛」,把两个来源折进一个布尔会让壳无法判断该给哪一段加显形标记。
@@ -2,7 +2,7 @@ import { contentKindOf } from "./approval-content-kind.js";
2
2
  import { AsyncLocalStorage } from "node:async_hooks";
3
3
  import { uuidv7, MAX_RULE_TEXT_CHARS, parseRuleText, pathConfinableWriteToolNames } from "@sema-agent/core";
4
4
  import { precheckCardRuleText } from "./rules-consent.js";
5
- import { redactDeep, redactSecrets } from "./trace/redact.js";
5
+ import { normalizeForRedaction, redactDeep, redactSecrets } from "./trace/redact.js";
6
6
  import { hasBidiControls } from "./text-bidi.js";
7
7
  import { approvalPayloadFingerprint } from "./plugins/approval-payload-fingerprint.js";
8
8
  import { createLogger } from "./observability/logger.js";
@@ -34,7 +34,7 @@ const PARKING_RETRY_AFTER_MS = 250;
34
34
  function durableRuleMaterial(material) {
35
35
  if (material === undefined)
36
36
  return { ruleCommand: null, ruleScopeRoot: null };
37
- if (redactSecrets(material.command, { observe: false }) !== material.command) {
37
+ if (redactSecrets(material.command, { observe: false }) !== normalizeForRedaction(material.command)) {
38
38
  defaultLogger.warn("approval rule material withheld: the adjudicated command carries credential-shaped bytes", {
39
39
  commandChars: material.command.length,
40
40
  });
@@ -141,8 +141,8 @@ function boundArgs(args) {
141
141
  const redacted = redactDeep(args);
142
142
  let hasBidi = false;
143
143
  try {
144
+ hasBidi = hasBidiControls(JSON.stringify(args));
144
145
  const text = JSON.stringify(redacted);
145
- hasBidi = hasBidiControls(text);
146
146
  if (Buffer.byteLength(text, "utf8") > MAX_APPROVAL_ARGS_BYTES)
147
147
  return { omitted: true, hasBidi };
148
148
  }