@sema-agent/server 7.63.0 → 7.65.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 (67) hide show
  1. package/MIGRATION.md +8 -0
  2. package/USAGE.md +35 -20
  3. package/deploy/sema-up/smoke.sh +2 -2
  4. package/dist/approval-ask-audit-store.d.ts +149 -3
  5. package/dist/approval-ask-audit-store.js +11 -4
  6. package/dist/bench/s1/arms.d.ts +2 -2
  7. package/dist/bench/s1/arms.js +20 -15
  8. package/dist/bench/s1/live-deps.js +3 -1
  9. package/dist/bench/s1/run-firm.js +2 -2
  10. package/dist/boot/resolve-spec.d.ts +0 -5
  11. package/dist/boot/resolve-spec.js +3 -4
  12. package/dist/capabilities/hands-lane.d.ts +52 -12
  13. package/dist/capabilities/hands-lane.js +19 -8
  14. package/dist/config-catalog.js +1 -1
  15. package/dist/config-center/read-face.js +10 -8
  16. package/dist/config-types.d.ts +12 -3
  17. package/dist/config.js +7 -4
  18. package/dist/degenerate-instrument.d.ts +0 -33
  19. package/dist/degenerate-instrument.js +2 -1
  20. package/dist/fleet/fleet-bus.d.ts +9 -4
  21. package/dist/fleet/fleet-bus.js +2 -1
  22. package/dist/host-decision.d.ts +39 -0
  23. package/dist/host-decision.js +6 -0
  24. package/dist/http/route-ctx.d.ts +8 -5
  25. package/dist/http/routes/a2a-serve.js +4 -2
  26. package/dist/http/routes/approvals-assistant.d.ts +0 -13
  27. package/dist/http/routes/approvals-assistant.js +2 -1
  28. package/dist/http/routes/diagnostics.d.ts +0 -11
  29. package/dist/http/routes/diagnostics.js +2 -0
  30. package/dist/http/routes/runs.js +3 -2
  31. package/dist/http/routes/tasks.js +33 -29
  32. package/dist/http/routes/workflows.d.ts +0 -11
  33. package/dist/http/routes/workflows.js +18 -14
  34. package/dist/http/server.js +50 -46
  35. package/dist/leader/fanout.d.ts +6 -23
  36. package/dist/leader/fanout.js +8 -19
  37. package/dist/leader/leader.d.ts +13 -10
  38. package/dist/leader/leader.js +30 -22
  39. package/dist/leader/repair-wire.d.ts +4 -1
  40. package/dist/leader/repair-wire.js +7 -5
  41. package/dist/leader/wire.js +5 -4
  42. package/dist/main.js +7 -7
  43. package/dist/observability/fail-open.d.ts +8 -0
  44. package/dist/observability/fail-open.js +8 -0
  45. package/dist/observability/metrics.js +1 -1
  46. package/dist/observability/run-terminal-log.d.ts +25 -7
  47. package/dist/observability/run-terminal-log.js +15 -4
  48. package/dist/observability/tool-trace.d.ts +13 -12
  49. package/dist/observability/tool-trace.js +8 -13
  50. package/dist/parked-decide.d.ts +8 -4
  51. package/dist/parked-decide.js +1 -1
  52. package/dist/plugins/file-run-store.js +12 -21
  53. package/dist/plugins/memory-run-store.js +3 -2
  54. package/dist/plugins/run-store-sql.js +3 -2
  55. package/dist/read-face-posture.d.ts +52 -0
  56. package/dist/read-face-posture.js +20 -0
  57. package/dist/run-local.js +6 -4
  58. package/dist/runs.d.ts +4 -19
  59. package/dist/runs.js +49 -47
  60. package/dist/terminal.d.ts +100 -0
  61. package/dist/terminal.js +55 -0
  62. package/dist/trace/core-keyset-guard.d.ts +20 -4
  63. package/dist/trace/ledger-sink.d.ts +2 -1
  64. package/dist/trace/ledger-sink.js +14 -7
  65. package/dist/trace/project.d.ts +9 -12
  66. package/dist/trace/project.js +54 -9
  67. package/package.json +4 -4
@@ -12,7 +12,8 @@
12
12
  * 不是静默继承 full(词表闭集纪律)。
13
13
  * ② {@link HANDS_EXCLUSIVE_TOOL_NAMES} + {@link handsDenyPolicy} —— 纵深:hands=none 的任务即便被误接到
14
14
  * 有手 Runner 上,手独占名单逐一 deny。**不是**主保证(主保证是根本不挂 factory ⇒ 工具面不可见)。
15
- * ③ {@link createHandsLaneRegistry} —— resolveSpec 产出 spec 与执行点选 Runner 之间的 lane 传递。
15
+ * ③ {@link stampHandsLane} / {@link handsLaneOf} —— 车道作为 TaskSpec 上的**显式字段**(S-149):
16
+ * resolveSpec 出口铸一次,执行点读一次,**缺席即拒**(不是回落最宽车道)。
16
17
  */
17
18
  import { type Runner, type TaskSpec, type ToolPolicy } from "@sema-agent/core";
18
19
  /** 场景对手带工具面的表态(闭集)。`full` = core 手带 band 照常 mount;`none` = 不挂 executionEnvFactory 的 Runner。 */
@@ -82,19 +83,58 @@ export declare function tightenWithHandsDeny(base: ToolPolicy | undefined): Tool
82
83
  /** 装配点的取用形:`full` 原样放行基线,`none` 叠 deny。**穷举 switch**,无缺省臂。 */
83
84
  export declare function toolPolicyForHands(hands: ScenarioHands, base: ToolPolicy | undefined): ToolPolicy | undefined;
84
85
  /**
85
- * 请求级 lane 登记簿 —— resolveSpec 在产出 TaskSpec 时登记本请求的场景表态,执行点凭 spec 取回选 Runner。
86
+ * 执行车道在 TaskSpec 上的**显式字段键**(S-149;修前是一张以 spec **对象身份**为键的 WeakMap 旁表)。
86
87
  *
87
- * 为什么不把 lane 写进 TaskSpec:那是 core 的类型,server 无自有位;而 `resolveSpec` 的返回形是
88
- * ServiceDeps 的公开签名,80+ 测试文件在桩它 —— 改返回形的射程远大于本件。登记簿以 spec **对象身份**
89
- * 为键(每请求一只新对象,并发无串台;WeakMap 弱引用,零清理欠账),由同一只 boot 装配同时交给
90
- * resolveSpec 与 HTTP 执行点,配对是结构性的。
88
+ * 为什么是 symbol 而不是字符串键:`TaskSpec` 是**上游 core 的类型面**,一个字符串键会进
89
+ * `Object.keys` / `JSON.stringify` / core 自己的键审计,等于 server 在别人的契约上私自加一个键;
90
+ * symbol 键随 spread(`{...spec}`)与 rest 解构(`const {objective, ...taskConfig} = spec`)结构性
91
+ * 地跟着走,却对上游的键面**不可见**(结构化克隆与 JSON 同样跳过 symbol 键)。于是「车道是 spec 上
92
+ * 的一个字段、复制 spec 不丢车道」与「不污染上游契约」两件事同时成立。
91
93
  *
92
- * 未登记 ⇒ `full`:唯一可达形是「这只 spec 不是本 resolver 产的」(测试桩)。生产链上 stamp 与 laneOf
93
- * 同一实例,不存在半接;真误配时纵深 deny 仍在(见 {@link tightenWithHandsDeny})。
94
+ * 修前的旁表要求「从 resolveSpec 出来的那一只对象必须一路原样活到执行点」——一条**隐形**的对象身份
95
+ * 契约:任何一次 `{...spec}` 都会静默把车道降级成最宽的 `full`(实测:登记 `none` 的 spec 复制后
96
+ * `laneOf` 答 `full`)。字段化把那条隐形要求整条删掉。
94
97
  */
95
- export interface HandsLaneRegistry {
96
- stamp(spec: TaskSpec, hands: ScenarioHands): void;
97
- laneOf(spec: TaskSpec): ScenarioHands;
98
+ export declare const HANDS_LANE_FIELD: unique symbol;
99
+ /** 已铸车道的 TaskSpec。铸点唯一(`boot/resolve-spec.ts` 的 resolveSpec 出口),读点唯一({@link handsLaneOf})。 */
100
+ export type LanedTaskSpec = TaskSpec & {
101
+ readonly [HANDS_LANE_FIELD]: ScenarioHands;
102
+ };
103
+ /**
104
+ * 铸车道(**单一属主**:只允许 resolveSpec 的出口调用)。
105
+ *
106
+ * 字段是 `enumerable`(所以复制得走)、`writable:false` + `configurable:false`(所以同一只 spec 被
107
+ * 第二次铸车道会当场 TypeError —— 两个写者是漂移的成因,响亮好过后到者悄悄赢)。
108
+ */
109
+ export declare function stampHandsLane<T extends TaskSpec>(spec: T, hands: ScenarioHands): T & LanedTaskSpec;
110
+ /**
111
+ * 车道字段缺席的拒因 —— 具名类,便于调用点/测试按类型判,而不是嗅文案。
112
+ *
113
+ * 🔴 **刻意不是 `HttpError`、刻意不铸新的 wire 错误码**:它落到 `http/server.ts` 的兜底 catch,
114
+ * wire 上是 500 `internal.error`(内部文本不回显,只进 `request_error` 日志行)。理由两条:
115
+ * ① 对**消费端**它与任何一次服务端内部故障无差别可操作性(重试一次,仍复现即报运维)—— 附录 A 多一个
116
+ * 码却给不出不同的动作,是没有价值的契约面;
117
+ * ② 本仓的中继码登记表(`test/error-code-catalog-live.test.ts` 的 `RELAYED`,上限 24 条)明写「超了说明
118
+ * 『码不写字面量』正在变成默认写法,该反过来改代码」—— 一个只能靠登记才看得见的新码正是它拦的东西。
119
+ * 需要分辨的是**运维**,而运维读的是日志行(本 message 逐字在 `request_error` 里)。
120
+ */
121
+ export declare class HandsLaneUnstampedError extends Error {
122
+ constructor();
98
123
  }
99
- export declare function createHandsLaneRegistry(): HandsLaneRegistry;
124
+ /**
125
+ * 读车道 —— **缺席即拒**([ref] P 类:车道选择是保护型量,fail-closed;`observability/fail-open.ts`
126
+ * 头注把「车道选择」逐字列在「正解是 fail-closed,不是留痕后继续放行」那一族里)。
127
+ *
128
+ * 修前此处是 `?? "full"`:一只没经过本仓 resolver 的 spec 会**静默拿到最宽的车道**(全量可写手带)。
129
+ * 「唯一可达形是测试桩」这个论证不是防线 —— 它是一句关于今天调用图的断言,而防线要防的正是明天多出
130
+ * 来的第二个执行点。缺席现在整只拒(500 + 日志留原句),而不是把一个未经裁定的任务放进有手车道。
131
+ */
132
+ export declare function handsLaneOf(spec: TaskSpec): ScenarioHands;
133
+ /**
134
+ * 装配点的选路席:`{full, handsless}` 两只 Runner + 车道字段 ⇒ 本任务跑哪一只。
135
+ *
136
+ * 具名工厂而不是在 main.ts 手搓闭包:选路规则(读字段 → 穷举 switch)于是只有**一份实现**,
137
+ * 测试与生产装配拿的是同一只(手搓等价闭包 = 第二真源,正是本件要删的那一类)。
138
+ */
139
+ export declare function createHandsRunnerSelector(pair: HandsRunnerPair): (spec: TaskSpec) => Runner;
100
140
  //# sourceMappingURL=hands-lane.d.ts.map
@@ -36,13 +36,24 @@ export function toolPolicyForHands(hands, base) {
36
36
  return tightenWithHandsDeny(base);
37
37
  }
38
38
  }
39
- export function createHandsLaneRegistry() {
40
- const lanes = new WeakMap();
41
- return {
42
- stamp: (spec, hands) => {
43
- lanes.set(spec, hands);
44
- },
45
- laneOf: (spec) => lanes.get(spec) ?? "full",
46
- };
39
+ export const HANDS_LANE_FIELD = Symbol("sema-server.handsLane");
40
+ export function stampHandsLane(spec, hands) {
41
+ Object.defineProperty(spec, HANDS_LANE_FIELD, { value: hands, enumerable: true, writable: false, configurable: false });
42
+ return spec;
43
+ }
44
+ export class HandsLaneUnstampedError extends Error {
45
+ constructor() {
46
+ super("execution lane is not stamped on this task spec — refusing to run it rather than falling back to the widest (full-hands) lane");
47
+ this.name = "HandsLaneUnstampedError";
48
+ }
49
+ }
50
+ export function handsLaneOf(spec) {
51
+ const lane = spec[HANDS_LANE_FIELD];
52
+ if (lane === undefined)
53
+ throw new HandsLaneUnstampedError();
54
+ return lane;
55
+ }
56
+ export function createHandsRunnerSelector(pair) {
57
+ return (spec) => pickHandsRunner(handsLaneOf(spec), pair);
47
58
  }
48
59
  //# sourceMappingURL=hands-lane.js.map
@@ -321,7 +321,7 @@ export const CONFIG_CATALOG = [
321
321
  r("READ_DENY_BUILTIN_EXCLUDE", "orchestration", "csv", "按行名删内建 deny 条目(档位之后生效;绝不削任务显式 deny)", { danger: SEC, configKey: "readDenyBuiltinExclude", center: cRead("denyBuiltinExclude", "readDenyBuiltinExclude") }),
322
322
  r("READ_DENY_BUILTIN_TIERS", "orchestration", "csv", "内建 deny 表档位选择(闭集四档;none=显式空姿态)", { danger: SEC, configKey: "readDenyBuiltinTiers", center: cRead("denyBuiltinTiers", "readDenyBuiltinTiers") }),
323
323
  r("READ_DENY_PATTERNS", "orchestration", "csv", "READ 面额外 deny pattern 表", { danger: SEC, configKey: "readDenyPatterns", center: cRead("denyPatterns", "readDenyPatterns") }),
324
- r("READ_FACE", "orchestration", "enum", "READ 容纳面档位(env 逐键恒赢 center —— 部署主权表达)", { enumValues: ["open", "roots"], danger: SEC, derivedDefaultNote: "缺席 ⇒ 不铸键,core 默认 roots", configKey: "readFace", center: cRead("face", "readFace") }),
324
+ r("READ_FACE", "orchestration", "enum", "READ 容纳面档位(env 逐键恒赢 center —— 部署主权表达)", { enumValues: ["open", "roots"], danger: SEC, derivedDefaultNote: "缺席 ⇒ 按 posture 派生(S-167):单用户 turnkey(REQUIRE_PRINCIPAL 未设)铸 open;多租户不铸键、core 默认 roots。生效来源见 GET /v1/diagnostics/wiring 的 readFace.source 四词", configKey: "readFace", center: cRead("face", "readFace") }),
325
325
  r("REAP_INTERVAL_SEC", "limitsHttp", "number", "reaper 周期(秒)", { staticDefault: "60", configKey: "reapIntervalSec" }),
326
326
  r("REAP_RUN_STALE_SEC", "limitsHttp", "number", "run 活性判陈旧阈(秒)", { staticDefault: "120", configKey: "runStaleSec", center: cLimits("runStaleSec") }),
327
327
  r("REMOTE_EXEC", "orchestration", "enum", "执行车道选择(点名而配不齐/认不出 ⇒ 拒启,绝不静默降级到 host/stub)", { enumValues: ["e2b", "k8s", "ssh", "adb", "host", "local-docker", "device"], danger: SEC, derivedDefaultNote: "缺席:CONFIG_PROVIDER=local ⇒ host;其余 ⇒ 进程内 stub", configKey: "remoteExec", resolve: (c) => c.remoteExec?.provider ?? null }),
@@ -1,9 +1,9 @@
1
1
  import { compileReadDeny, resolveReadDenyBuiltins } from "@sema-agent/core";
2
2
  const KEY_MAP = [
3
- ["face", "readFace"],
4
- ["denyPatterns", "readDenyPatterns"],
5
- ["denyBuiltinTiers", "readDenyBuiltinTiers"],
6
- ["denyBuiltinExclude", "readDenyBuiltinExclude"],
3
+ { centerKey: "face", cfgKey: "readFace", envHolds: (c) => c.readFaceSource === "env" },
4
+ { centerKey: "denyPatterns", cfgKey: "readDenyPatterns", envHolds: (c) => c.readDenyPatterns !== undefined },
5
+ { centerKey: "denyBuiltinTiers", cfgKey: "readDenyBuiltinTiers", envHolds: (c) => c.readDenyBuiltinTiers !== undefined },
6
+ { centerKey: "denyBuiltinExclude", cfgKey: "readDenyBuiltinExclude", envHolds: (c) => c.readDenyBuiltinExclude !== undefined },
7
7
  ];
8
8
  class ReadFaceDomainError extends Error {
9
9
  }
@@ -43,7 +43,7 @@ function normalizeNameList(field, raw) {
43
43
  });
44
44
  }
45
45
  export function applyCenterReadFace(config, eff, logger) {
46
- const envHeldKeys = KEY_MAP.filter(([, cfgKey]) => config[cfgKey] !== undefined).map(([centerKey]) => centerKey);
46
+ const envHeldKeys = KEY_MAP.filter((r) => r.envHolds(config)).map((r) => r.centerKey);
47
47
  const domain = eff.readFace;
48
48
  if (domain === undefined || domain === null)
49
49
  return envHeldKeys;
@@ -55,7 +55,7 @@ export function applyCenterReadFace(config, eff, logger) {
55
55
  return envHeldKeys;
56
56
  }
57
57
  const d = domain;
58
- const envHeld = KEY_MAP.filter(([centerKey, cfgKey]) => d[centerKey] !== undefined && config[cfgKey] !== undefined).map(([centerKey, cfgKey]) => `${cfgKey} (readFace.${centerKey})`);
58
+ const envHeld = KEY_MAP.filter((r) => d[r.centerKey] !== undefined && r.envHolds(config)).map((r) => `${r.cfgKey} (readFace.${r.centerKey})`);
59
59
  let staged;
60
60
  try {
61
61
  const normalized = {
@@ -69,7 +69,7 @@ export function applyCenterReadFace(config, eff, logger) {
69
69
  if (normalized.readDenyBuiltinTiers !== undefined || normalized.readDenyBuiltinExclude !== undefined) {
70
70
  resolveReadDenyBuiltins(builtinsArg(normalized.readDenyBuiltinTiers, normalized.readDenyBuiltinExclude));
71
71
  }
72
- staged = Object.fromEntries(KEY_MAP.filter(([, cfgKey]) => config[cfgKey] === undefined).map(([, cfgKey]) => [cfgKey, normalized[cfgKey]]).filter(([, v]) => v !== undefined));
72
+ staged = Object.fromEntries(KEY_MAP.filter((r) => !r.envHolds(config)).map((r) => [r.cfgKey, normalized[r.cfgKey]]).filter(([, v]) => v !== undefined));
73
73
  const finalPatterns = staged.readDenyPatterns ?? config.readDenyPatterns;
74
74
  const finalTiers = staged.readDenyBuiltinTiers ?? config.readDenyBuiltinTiers;
75
75
  const finalExclude = staged.readDenyBuiltinExclude ?? config.readDenyBuiltinExclude;
@@ -85,8 +85,10 @@ export function applyCenterReadFace(config, eff, logger) {
85
85
  });
86
86
  return envHeldKeys;
87
87
  }
88
- if (staged.readFace !== undefined)
88
+ if (staged.readFace !== undefined) {
89
89
  config.readFace = staged.readFace;
90
+ config.readFaceSource = "center";
91
+ }
90
92
  if (staged.readDenyPatterns !== undefined)
91
93
  config.readDenyPatterns = staged.readDenyPatterns;
92
94
  if (staged.readDenyBuiltinTiers !== undefined)
@@ -5,6 +5,7 @@
5
5
  * in config.ts, which re-exports these names so existing `from "./config.js"` importers keep
6
6
  * compiling unchanged. All imports below are top-level `import type` (erased — never a load edge,
7
7
  * see test/module-cycle-gate.test.ts header on verbatimModuleSyntax). */
8
+ import type { ReadFaceSource } from "./read-face-posture.js";
8
9
  import type { SealedKeyPoison } from "./sealed-key.js";
9
10
  import type { A2aServerSpec, CompliancePosture, CrossSessionDialogExpiry, CrossSessionInboundSetting, FileHistoryRetentionPolicy, LockedKey, McpServerSpec, Model, ModelRoles, RetentionPolicy, WriteProtectedEntry } from "@sema-agent/core";
10
11
  import type { ApprovalHmacKey, PrincipalJwtKey } from "./auth-keys.js";
@@ -1539,9 +1540,17 @@ export interface ServiceConfigFlat {
1539
1540
  /** Task Trace API (S1): persist the agent's reasoning/thinking into the durable run event log (for the CC-style
1540
1541
  * trace UI). Default ON; `TRACE_THINKING=false` disables capture for sensitive deployments. Redacted on persist. */
1541
1542
  traceThinking: boolean;
1542
- /** [ref](core 5.30.0 readFace 部署座席):READ 容纳面。缺席=不铸(core 引擎默认 roots,server 不写死
1543
- * 上游默认);显式坏词启动期响亮拒。只影响读面,写面永不受它管(core 契约)。 */
1543
+ /** [ref](core 5.30.0 readFace 部署座席):READ 容纳面。显式坏词启动期响亮拒;只影响读面,写面永不
1544
+ * 受它管(core 契约)。
1545
+ * 🔴 **S-167(7.65.0)起「缺席」按 posture 分两形**(clay 裁定 2):单用户 turnkey(`REQUIRE_PRINCIPAL`
1546
+ * 未设)⇒ 铸 `open`(工作区外读默认放行);多租户 ⇒ 仍**不铸**(core 引擎默认 roots,server 不写死
1547
+ * 上游默认)。谁定的这一档由 {@link ServiceConfigFlat.readFaceSource} 答 —— 值与来源**同一处写**。 */
1544
1548
  readFace?: "open" | "roots";
1549
+ /** S-167:上面那一档**是谁定的**(闭集四词,见 `read-face-posture.ts`)。不变量:
1550
+ * `readFace === undefined` ⟺ `readFaceSource === "engine-default"`。
1551
+ * 🔴 承重面**不是**装饰:`config-center/read-face.ts` 的「env 恒赢」逐键判据读的就是它(修前读的是
1552
+ * 「键在不在场」这个代理,posture 会铸键 ⇒ 代理当场失真,center 的 face 会被从没设过 env 的机器挡掉)。 */
1553
+ readFaceSource: ReadFaceSource;
1545
1554
  /** [ref]:部署级 READ deny 追加表(additive-only,core 内建表永不可缩)。env=JSON 数组
1546
1555
  * [{pattern, caseSensitive?}],boot 时用 core `compileReadDeny` 预验——坏形拒启。缺席=不铸。 */
1547
1556
  readDenyPatterns?: {
@@ -1674,7 +1683,7 @@ export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled"
1674
1683
  * config 这一层它只是一句声明。 */
1675
1684
  export type ServiceAuthConfig = Pick<ServiceConfigFlat, "authToken" | "authTokens" | "allowUnauthedWrites" | "corsOrigins" | "principalHeader" | "requirePrincipal" | "autonomy" | "commandPolicy" | "compliancePosture" | "lockedConfigKeys" | "configCatalogEnabled" | "retentionPolicy" | "retentionSweep" | "operatorPrincipals" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "principalJwtMaxTtlSec" | "bindHost" | "bindHostSource">;
1676
1685
  /** 组:orchestration(编排 + 执行车道 + 沙箱面 + workflow/后台 agent 存留)。 */
1677
- export type ServiceOrchestrationConfig = Pick<ServiceConfigFlat, "remoteExec" | "worktreeIsolation" | "leaderEnabled" | "leaderFanoutEnabled" | "routerEnabled" | "selfOrchestrationEnabled" | "selfOrchestrationModels" | "selfOrchestrationWorkerIsolation" | "forkEnabled" | "experimentalObserverAgents" | "schedulerEnabled" | "schedulerSessionWakeup" | "schedulerSessionLifetime" | "schedulerStorePath" | "planModeEnabled" | "hooksTimeoutMs" | "workflowRunStoreBackend" | "workflowOrphanGraceMs" | "workflowJournalRetentionMs" | "workflowRunRetentionMs" | "usageWindows" | "workflowAgentsReadOnly" | "workflowSizeGuideline" | "backgroundAgentRetentionMs" | "backgroundAgentStaleRunningMs" | "backgroundAgentParkClaimStaleMs" | "delegationEntryCaps" | "rosterRetentionMs" | "leaderRunStaleMs" | "scratchpadSweepTtlMs" | "sandboxPkgSource" | "sessionAutoTitle" | "selectEnvironmentTool" | "envFactsEnabled" | "toolDeferLongtail" | "lspEnabled" | "lspHostEnabled" | "imageBakes" | "readFace" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude">;
1686
+ export type ServiceOrchestrationConfig = Pick<ServiceConfigFlat, "remoteExec" | "worktreeIsolation" | "leaderEnabled" | "leaderFanoutEnabled" | "routerEnabled" | "selfOrchestrationEnabled" | "selfOrchestrationModels" | "selfOrchestrationWorkerIsolation" | "forkEnabled" | "experimentalObserverAgents" | "schedulerEnabled" | "schedulerSessionWakeup" | "schedulerSessionLifetime" | "schedulerStorePath" | "planModeEnabled" | "hooksTimeoutMs" | "workflowRunStoreBackend" | "workflowOrphanGraceMs" | "workflowJournalRetentionMs" | "workflowRunRetentionMs" | "usageWindows" | "workflowAgentsReadOnly" | "workflowSizeGuideline" | "backgroundAgentRetentionMs" | "backgroundAgentStaleRunningMs" | "backgroundAgentParkClaimStaleMs" | "delegationEntryCaps" | "rosterRetentionMs" | "leaderRunStaleMs" | "scratchpadSweepTtlMs" | "sandboxPkgSource" | "sessionAutoTitle" | "selectEnvironmentTool" | "envFactsEnabled" | "toolDeferLongtail" | "lspEnabled" | "lspHostEnabled" | "imageBakes" | "readFace" | "readFaceSource" | "readDenyPatterns" | "readDenyBuiltinTiers" | "readDenyBuiltinExclude">;
1678
1687
  /** 组:limitsHttp(HTTP 面 + 各类上限/配额/回收窗)。 */
1679
1688
  export type ServiceLimitsHttpConfig = Pick<ServiceConfigFlat, "port" | "attachmentOrphanGraceMs" | "workspaceFileMaxBytes" | "attachmentMaxBytes" | "attachmentMimeAllowlist" | "attachmentUnboundTtlMs" | "infraCostRates" | "drainGraceMs" | "sighupIdleGraceMs" | "parentPid" | "engineAutoExit" | "engineLingerMs" | "rateLimitPerMin" | "maxTaskCostUsd" | "maxTaskTokens" | "maxPrincipalCostUsd" | "costQuotaWindowSec" | "reapIntervalSec" | "runStaleSec" | "toolResultTtlSec">;
1680
1689
  /** 组:observability(可观测)。 */
package/dist/config.js CHANGED
@@ -12,6 +12,7 @@ import { GIT_API_KINDS, isGitApiKind } from "./git-api-kind.js";
12
12
  import { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC } from "./config-types.js";
13
13
  import { buildWriteProtectionPosture } from "./write-protection.js";
14
14
  import { normalizeSshFingerprint } from "./ssh-host-key.js";
15
+ import { readFaceForPosture } from "./read-face-posture.js";
15
16
  import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
16
17
  export { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC } from "./config-types.js";
17
18
  function csv(name) {
@@ -1458,7 +1459,7 @@ function parseAuthDomain(ctx) {
1458
1459
  };
1459
1460
  }
1460
1461
  function parseOrchestrationDomain(ctx) {
1461
- const { postureOn } = ctx;
1462
+ const { postureOn, singleUserTurnkey } = ctx;
1462
1463
  const workflowSizeGuidelineRaw = process.env.WORKFLOW_SIZE_GUIDELINE;
1463
1464
  if (workflowSizeGuidelineRaw !== undefined && !["small", "medium", "large", "unrestricted"].includes(workflowSizeGuidelineRaw)) {
1464
1465
  throw new Error(`WORKFLOW_SIZE_GUIDELINE must be small|medium|large|unrestricted, got "${workflowSizeGuidelineRaw}"`);
@@ -1501,7 +1502,9 @@ function parseOrchestrationDomain(ctx) {
1501
1502
  }
1502
1503
  }
1503
1504
  return {
1504
- ...(process.env.READ_FACE !== undefined ? { readFace: enumEnv("READ_FACE", "roots", ["open", "roots"]) } : {}),
1505
+ ...(process.env.READ_FACE !== undefined
1506
+ ? { readFace: enumEnv("READ_FACE", "roots", ["open", "roots"]), readFaceSource: "env" }
1507
+ : readFaceForPosture(singleUserTurnkey)),
1505
1508
  ...(process.env.READ_DENY_PATTERNS !== undefined ? { readDenyPatterns: parseReadDenyPatternsEnv(process.env.READ_DENY_PATTERNS) } : {}),
1506
1509
  ...(() => {
1507
1510
  const tiers = process.env.READ_DENY_BUILTIN_TIERS !== undefined ? parseNameListEnv("READ_DENY_BUILTIN_TIERS", process.env.READ_DENY_BUILTIN_TIERS, { emptySentinel: "none" }) : undefined;
@@ -1905,7 +1908,7 @@ const ORCHESTRATION_GROUP_KEYS = [
1905
1908
  "workflowSizeGuideline", "backgroundAgentRetentionMs", "backgroundAgentStaleRunningMs",
1906
1909
  "backgroundAgentParkClaimStaleMs", "delegationEntryCaps", "rosterRetentionMs", "leaderRunStaleMs", "scratchpadSweepTtlMs", "sandboxPkgSource",
1907
1910
  "sessionAutoTitle", "selectEnvironmentTool", "envFactsEnabled", "toolDeferLongtail", "lspEnabled", "lspHostEnabled",
1908
- "imageBakes", "readFace", "readDenyPatterns", "readDenyBuiltinTiers", "readDenyBuiltinExclude",
1911
+ "imageBakes", "readFace", "readFaceSource", "readDenyPatterns", "readDenyBuiltinTiers", "readDenyBuiltinExclude",
1909
1912
  ];
1910
1913
  const LIMITS_HTTP_GROUP_KEYS = [
1911
1914
  "port", "attachmentOrphanGraceMs", "workspaceFileMaxBytes", "attachmentMaxBytes", "attachmentMimeAllowlist",
@@ -1973,7 +1976,7 @@ export function loadConfig() {
1973
1976
  const approval = parseApprovalDomain({ postureOn });
1974
1977
  const memory = parseMemoryDomain({ requirePrincipal });
1975
1978
  const auth = parseAuthDomain({ requirePrincipal });
1976
- const orchestration = parseOrchestrationDomain({ postureOn });
1979
+ const orchestration = parseOrchestrationDomain({ postureOn, singleUserTurnkey });
1977
1980
  const limitsHttp = parseLimitsHttpDomain();
1978
1981
  const observability = parseObservabilityDomain();
1979
1982
  const integrations = parseIntegrationsDomain();
@@ -1,36 +1,3 @@
1
- /**
2
- * Degenerate-repetition instrument (a/b classifier).
3
- *
4
- * When a task fails with `errorCode === "output.degenerate"` (core 1.59), core hands back
5
- * `salvagedOutput` = the degenerate turn's text — **already tail-TRIMMED at the brain stream layer**.
6
- * 🔴 [ref] correction (this note used to claim "the **whole** text, good head + looped garbage tail";
7
- * that was true of core 1.59 and false of every engine that ships `trimDegenerateTail` — verified against
8
- * the installed dist, not against JSDoc): on the main path `assemble-result` fills the seat from the same
9
- * final message the brain already trimmed, so what arrives here is `head + exactly ONE copy of the
10
- * repeating unit` (unit ≤ `MAX_PERIOD` = 100 chars). Untrimmed text only reaches us on the narrow
11
- * fallbacks — the cut landed on the reasoning face, or `trimDegenerateTail` bailed (`reps < 2`, empty
12
- * unit, unit-loop period mismatch). Consequence for the numbers below: {@link repetitionTail} is a
13
- * **no-op** on the main-path input (one copy is not ≥ {@link MIN_REPEATS}), so `uniquePrefixLen`
14
- * collapses onto `salvagedLen` and the a/unknown split degrades to a pure length test. The a-vs-b
15
- * decision is unaffected (it is decided by `priorChars`, independently); the residual bias on a/unknown
16
- * is bounded by one unit ≤ 100 chars. Pin: the "已裁形" case in `test/degenerate-instrument.test.ts`
17
- * drives the REAL `inspectDegenerate` + `trimDegenerateTail` and asserts the tail measures 0.
18
- * Core's *salvage ②* — recovering the "last substantive turn" instead of the current one — is being
19
- * gated on REAL data: how often is the useful answer actually in an EARLIER turn vs. in the degenerate
20
- * turn itself? This instrument answers that, per event, without changing any behaviour.
21
- *
22
- * Classification (since the last user message = this task's own turns):
23
- * - **a** good+garbage SAME turn — the degenerate turn carries a substantive unique head before the
24
- * loop. Whole-turn salvage ① + a tail-trim is enough; ② would add nothing.
25
- * - **b** good EARLIER / degenerate LATER — a substantive assistant turn already exists before the
26
- * degenerate one. ② ("last-substantive-turn") would recover it; ① (current turn) throws it away.
27
- * - **unknown** — degenerate from the start, nothing substantive anywhere (neither ① nor ② helps).
28
- *
29
- * Output is measurement only: a `degenerate_total{class}` counter (→ `/metrics/summary`) plus one
30
- * structured log line carrying the raw signals. The aggregate a/b/unknown frequencies are what gets
31
- * reported back to the core AI to decide whether ② is worth building. The repetition-boundary estimate
32
- * here is for *measuring* the unique head — it is NOT the production tail-trim (that lives in core).
33
- */
34
1
  import { type TaskResult } from "@sema-agent/core";
35
2
  import type { Pool } from "mysql2/promise";
36
3
  import type { Metrics } from "./observability/metrics.js";
@@ -1,3 +1,4 @@
1
+ import { failureCodeOf } from "./terminal.js";
1
2
  import { StoredSession } from "@sema-agent/core";
2
3
  import { TiDBSessionStorage } from "./plugins/tidb-session-storage.js";
3
4
  const SUBSTANTIVE_CHARS = 200;
@@ -95,7 +96,7 @@ async function readPriorTurns(pool, sessionId, salvaged) {
95
96
  }
96
97
  export function makeDegenerateInstrument(pool, metrics, logger) {
97
98
  return (result) => {
98
- if (result.errorCode !== "output.degenerate")
99
+ if (failureCodeOf(result.terminal) !== "output.degenerate")
99
100
  return;
100
101
  void (async () => {
101
102
  const salvaged = result.salvagedOutput ?? "";
@@ -1,4 +1,4 @@
1
- import type { TaskNotificationPayload, WorkflowRun } from "@sema-agent/core";
1
+ import type { TaskNotificationPayload, TerminalCause, WorkflowRun } from "@sema-agent/core";
2
2
  import type { RunRecord } from "../plugins/store-contracts.js";
3
3
  /** [ref] 幽灵行案的单源判别:一条 `task_notification` 只有在 **agent 族 × 终态** 时才允许打
4
4
  * `onChildTerminal`(fleet「subagent 树」只渲 agent 子代)。`background_bash`/`monitor`/`external`
@@ -546,11 +546,16 @@ export declare function fleetRunPublisher(bus: FleetEventBus | undefined, run: {
546
546
  export type FleetRunPublisher = ReturnType<typeof fleetRunPublisher>;
547
547
  /** [ref]①/(a) 案:TaskResult → 终态行帧残局键映射(sync/background/resume 三条腿各自 settle,共用一处
548
548
  * 映射——键名映射写三遍必漂移)。usage 与 {@link BgNotification.usage} 同形:stats.tokens→totalTokens、
549
- * stats.toolCalls→toolUses(名对齐,消费端一个形状)。errorCode "cancelled" ⇒ stoppedBy:"user"(与
550
- * markStopSource 的归因值同源);transcriptId 顶层 = run 的 sessionId([ref]§二拍)。缺源的键不铸。 */
549
+ * stats.toolCalls→toolUses(名对齐,消费端一个形状)。失败码 "cancelled" ⇒ stoppedBy:"user"(与
550
+ * markStopSource 的归因值同源);transcriptId 顶层 = run 的 sessionId([ref]§二拍)。缺源的键不铸。
551
+ *
552
+ * 🔴 S-136(core 7.6.0 D-8):入参从**结构性可缺席** `errorCode?: string` 改成 `terminal?: TerminalCause`。
553
+ * 留着旧形不会红 —— 那正是危险所在:`TaskResult` 上已经没有 `errorCode` 这个键了,可结构性可选参数照样
554
+ * 编译通过,于是 `stoppedBy:"user"` 会**静默地永远不再铸**(取消的 run 在舰队行上从此看不出是被人杀的)。
555
+ * 终局事实一律走因由,`failureCodeOf` 是本仓读「这次失败带的是哪个码」的唯一形。 */
551
556
  export declare function fleetRunResiduals(result: {
552
557
  sessionId?: string;
553
- errorCode?: string;
558
+ terminal?: TerminalCause;
554
559
  stats?: {
555
560
  turns?: number;
556
561
  tokens?: number;
@@ -1,5 +1,6 @@
1
1
  import { EventEmitter } from "node:events";
2
2
  import { deriveAgentDisplayStatus, isDelegatedAgentTerminal } from "@sema-agent/core";
3
+ import { persistedPlaneOf } from "../terminal.js";
3
4
  import { recordFailOpen } from "../observability/fail-open.js";
4
5
  import { redactSecrets } from "../trace/redact.js";
5
6
  export function isFleetAgentTerminalNotification(n) {
@@ -341,7 +342,7 @@ export function fleetRunResiduals(result) {
341
342
  : {};
342
343
  return {
343
344
  ...(Object.keys(usage).length > 0 ? { usage } : {}),
344
- ...(result.errorCode === "cancelled" ? { stoppedBy: "user" } : {}),
345
+ ...(persistedPlaneOf(result).errorCode === "cancelled" ? { stoppedBy: "user" } : {}),
345
346
  ...(result.sessionId ? { transcriptId: result.sessionId } : {}),
346
347
  };
347
348
  }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * **宿主的决策事实** —— 一条耐久 park 被结算时,本服务能如实陈述的那半件事:谁结束了这次等待,以及
3
+ * 审批通道自报的归属。一个叶子模块(只依赖 `@sema-agent/core`),四个消费点共用一个定义。
4
+ *
5
+ * ## 为什么是「事实」而不是「结算词」(core 7.6.0 / [ref] S6-A,BREAKING)
6
+ *
7
+ * 旧形里 server 要往 `ResumeOutcome` 上写 core 的**结算词**(`ApprovalSettledBy` = `"human"` /
8
+ * `"timeout"` / `"aborted"`),于是「哪个词配哪个 decision」这条相容规则得由 server 自己记住 —— 而记错的
9
+ * 方向恰恰是这条规则存在的理由:**「窗到期」被报成「另一个人拒绝了」**。7.6.0 起 core 的
10
+ * `@contract settlement.single_mint` 逐字写着「宿主永不铸词」:宿主交事实,core 在 resume 入口铸
11
+ * `human_allowed` / `human_refused` / `park_sla_expired`,写进行的 `resolvedOutcome.gateOutcome`,再投影到
12
+ * 那次调用的 `tool_end.gate`。
13
+ *
14
+ * ⇒ 本仓的规则从「记住一张词×决定的相容表」变成「说出你知道的那两件事」。相容性也不再靠记忆:一次
15
+ * sweep 不可能 `allow`,core pre-CAS 拒。
16
+ */
17
+ import { type ResumeOutcome } from "@sema-agent/core";
18
+ /**
19
+ * 谁结束了这次等待,以及通道自报的归属。
20
+ * · `decidedBy: "person"` —— 运维经审批通道做的决定(两条 HTTP `/decide` 腿);
21
+ * · `decidedBy: "sla_timeout"` —— 宿主自己的 SLA 扫在行的 deadline 上判的(FACET A;**只有** sweep 能这么说,
22
+ * 而 sweep 结构上不能 `allow`)。
23
+ * · `approver` —— 通道自报的结算方标识串。**缺席永不读成**「没人批」或「有人批」,只意味着通道没报名字。
24
+ */
25
+ /** 直接取 core `ResumeOutcome` policy_ask 臂的 `hostDecision` 型 —— 本仓不复述那两个词(第二铸点)。 */
26
+ export type HostDecision = Extract<ResumeOutcome, {
27
+ gate: "policy_ask";
28
+ }>["hostDecision"];
29
+ /**
30
+ * 一次**人为** decide 的事实。`principal` = 那条腿已经验过身份的决策者(缺席 = 匿名档的部署,通道确实
31
+ * 没有名字可报)。
32
+ *
33
+ * 归属串走 core 的 `screenApproverAttribution` **先筛后带**:core 对形不合的归属是 pre-CAS 响亮拒
34
+ * (`checkpoint.invalid_outcome`),而一个奇形怪状的 principal 把一次合法的人工决策变成 400 是纯回归。
35
+ * 筛不过 ⇒ **不带这一格**(而不是带一个被改造过的串):归属是转录,不是可以「修一修」的东西 —— 修出来的
36
+ * 名字既不是通道报的那个,也不是「通道没报」,两头都不真。
37
+ */
38
+ export declare function personDecision(principal: string | undefined): HostDecision;
39
+ //# sourceMappingURL=host-decision.d.ts.map
@@ -0,0 +1,6 @@
1
+ import { screenApproverAttribution } from "@sema-agent/core";
2
+ export function personDecision(principal) {
3
+ const screened = screenApproverAttribution(principal);
4
+ return { decidedBy: "person", ...(screened.approver !== undefined ? { approver: screened.approver } : {}) };
5
+ }
6
+ //# sourceMappingURL=host-decision.js.map
@@ -15,7 +15,8 @@
15
15
  * 本文件对 server.ts 的引用一律 `import type`(tsc 擦除,非装载边)。
16
16
  */
17
17
  import type { IncomingMessage, ServerResponse } from "node:http";
18
- import type { ApprovalSettledBy, TaskStream, TaskSpec, TaskResult, QuestionAnswer, CheckpointToken, ResumeOutcome, Runner } from "@sema-agent/core";
18
+ import type { HostDecision } from "../host-decision.js";
19
+ import type { TaskStream, TaskSpec, TaskResult, QuestionAnswer, CheckpointToken, ResumeOutcome, Runner } from "@sema-agent/core";
19
20
  import type { FlatServiceDeps, RequestAuth } from "./server.js";
20
21
  import type { TaskRequestBody, DecideBinding } from "./wire-types.js";
21
22
  import type { IdempotencyCache } from "./idempotency.js";
@@ -244,10 +245,12 @@ export interface RouteLegs {
244
245
  /** 同步腿的终局记账(计费/配额/指标),tasks 域用。 */
245
246
  finalizeTaskResult(result: TaskResult, principal: string | undefined, objective: string, sessionId: string | undefined): void;
246
247
  /** approvals 决策腿:session → pending checkpoint → markResuming CAS → 驱动续跑。
247
- * `settledBy` = **这次结算的出处**(core `ApprovalSettledBy`),必填:本腿有两个调用方(HTTP
248
- * `/decide` = 人;内部 D-D SLA deny-sweep = 窗到期),而函数内部分辨不出谁在叫它 —— 必填参数把
249
- * 「漏报出处」变成编译错,不是一条下游把「没人答」渲染成「有人拒」的静默假出处。 */
250
- resumeCheckpoint(sessionId: string, decision: "approve" | "deny", reason: string | undefined, settledBy: ApprovalSettledBy, req?: IncomingMessage, answer?: QuestionAnswer, binding?: DecideBinding,
248
+ * `hostDecision` = **这次结算的事实**(谁结束了这次等待 + 通道自报的归属),必填:本腿有两个调用方
249
+ * (HTTP `/decide` = 人;内部 D-D SLA deny-sweep = 窗到期),而函数内部分辨不出谁在叫它 —— 必填参数把
250
+ * 「漏报出处」变成编译错,不是一条下游把「没人答」渲染成「有人拒」的静默假出处。
251
+ * 🔴 S-136(core 7.6.0 S6-A):宿主报事实、core 铸词 —— server 不再传结算词(旧形传 core 的三词之一,
252
+ * 于是「哪个词配哪个 decision」这条相容规则要 server 自己记,而记错的方向恰是上面那句要防的)。 */
253
+ resumeCheckpoint(sessionId: string, decision: "approve" | "deny", reason: string | undefined, hostDecision: HostDecision, req?: IncomingMessage, answer?: QuestionAnswer, binding?: DecideBinding,
251
254
  /** [ref] codex R4(验真后修):remember 豁免的 grant 挂点。**工具名不再由路由自己读**(那是与本函数
252
255
  * 载入行分歧的第三次 scope-less 读,三读分歧下会给他人工具落豁免=提权)——由本函数把**已属主校验的
253
256
  * 载入 cp** 的工具名(`validatedToolName`,非 tool_approval / AskUserQuestion / legacy null ⇒ `null`
@@ -1,3 +1,4 @@
1
+ import { persistedPlaneOf } from "../../terminal.js";
1
2
  import { createHash } from "node:crypto";
2
3
  import { memoRedactedRead, redactErrorMessage } from "../../observability/run-terminal-log.js";
3
4
  import { A2A_TASK_STATES } from "@sema-agent/core";
@@ -84,8 +85,9 @@ function statusTextForRun(run, state) {
84
85
  const memo = (seat, raw) => (isTerminalRunStatus(run.status) ? memoRedactedRead(`a2a.${seat}`, raw, (v) => redactErrorMessage(v) ?? "") : (redactErrorMessage(raw) ?? ""));
85
86
  if (run.error)
86
87
  return memo("error", run.error);
87
- if (run.result?.errorMessage)
88
- return memo("errorMessage", run.result.errorMessage);
88
+ const runPlaneMessage = persistedPlaneOf(run.result).errorMessage;
89
+ if (runPlaneMessage)
90
+ return memo("errorMessage", runPlaneMessage);
89
91
  return undefined;
90
92
  }
91
93
  function taskFromRun(run, stale) {
@@ -1,16 +1,3 @@
1
- /**
2
- * [ref] A9 域模块 —— 人在环审批面(durable F4 + legacy)与 assistant-scheduler 三动词。
3
- *
4
- * 路由(前缀 `/v1/approvals` 与 `/v1/assistant`):
5
- * · durable 腿(接了 checkpointStore 时优先):list / stream(SSE 原生推送)/ decide|answer / preempt /
6
- * resume / plan_review;
7
- * · legacy 腿(approvalStore 轮询车道)已于 5.0.0 整体退役([ref]/[ref] A-5)——无 checkpointStore 时
8
- * `/v1/approvals` 落 404(诚实缺席),不再有 trusted-header 决裁面。
9
- *
10
- * 所有会**烧模型**的续跑都经 `ctx.legs.*`(resumeCheckpoint / resumePreempted / resumePlanReview)走 resume
11
- * 家族的同一条腿:lease admission、markResuming CAS、写 run log、发 fleet 帧都在腿里,本域只做入站鉴权/形状门。
12
- * 本域零可变状态(抢占登记簿 `preemptableRuns` 属跨域 registry)。
13
- */
14
1
  import type { IncomingMessage, ServerResponse } from "node:http";
15
2
  import type { QuestionAnswer } from "@sema-agent/core";
16
3
  import type { CheckpointStoreFull } from "../../plugins/store-backend.js";
@@ -1,3 +1,4 @@
1
+ import { personDecision } from "../../host-decision.js";
1
2
  import { DECIDED_REPLAY_SCAN_CAP, approvalPayloadFingerprint } from "../../plugins/checkpoint-store-sql.js";
2
3
  import { APPROVAL_GATE_KINDS } from "../../tool-approval.js";
3
4
  import { isParkedRunStatus } from "../../plugins/store-contracts.js";
@@ -483,7 +484,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
483
484
  pendingGrant = { sessionId: grant.overrideSessionId ?? sessionId, toolName: canonical };
484
485
  }
485
486
  : undefined;
486
- const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined, "human", req, answer, binding, grantOnCommit, true, { principal: deciderPrincipal, explicitOperator });
487
+ const out = await resumeCheckpoint(sessionId, decision, body.reason ?? undefined, personDecision(deciderPrincipal), req, answer, binding, grantOnCommit, true, { principal: deciderPrincipal, explicitOperator });
487
488
  const outErrorCode = out.body.errorCode;
488
489
  if (pendingGrant !== undefined && out.status === 200 && outErrorCode !== "cancelled" && deps.approvalExemptionStore) {
489
490
  try {
@@ -1,14 +1,3 @@
1
- /**
2
- * [ref] 件二+件三 —— **装配自证**域:启动期静态自检(拒启判据)与 operator 诊断读面
3
- * (`GET /v1/diagnostics/wiring`)。
4
- *
5
- * 🔴 两件为什么同住一个模块:它们回答的是同一个问题(「这台 worker 到底接了什么线」),只是一个在 boot
6
- * 用它拒绝启动、一个在运行期把它读给 operator。分成两处 = 拒启用的判据与诊断报的读数会各自演化,而
7
- * 「拒启说没矛盾、诊断页显示矛盾」正是本设计要消灭的那类装配谎言。故 boot 半场是本文件导出的**纯函数**
8
- * (可单测、无 I/O),路由半场只是它的读口。
9
- *
10
- * 分层:本模块不值 import `server.ts`(那条边会闭合运行时装载环),对它只有 `import type`。
11
- */
12
1
  import type { IncomingMessage, ServerResponse } from "node:http";
13
2
  import type { WiringManifest } from "@sema-agent/core";
14
3
  import { type StreamApprovalGate, type StreamApprovalGateInput } from "../../tool-approval.js";
@@ -1,3 +1,4 @@
1
+ import { buildReadFaceWiring } from "../../read-face-posture.js";
1
2
  import { resolveStreamApprovalGate } from "../../tool-approval.js";
2
3
  import { sendJson, sendError } from "../send.js";
3
4
  import { gatedPrincipal, explicitOperatorOk, isLoopbackAddress, verifiedIdentitySource } from "../principal-gate.js";
@@ -90,6 +91,7 @@ async function handleDiagnosticsBody(req, res, url, ctx, miss) {
90
91
  memoryPosture: deps.memoryPosture ?? null,
91
92
  sqlEngine: deps.sqlEngineFacts?.() ?? null,
92
93
  writeProtection: deps.writeProtection ?? null,
94
+ readFace: buildReadFaceWiring(deps.config),
93
95
  configApply: deps.configApplyState?.() ?? null,
94
96
  mcpRevocations: deps.mcpRevocationState?.() ?? null,
95
97
  serverGates: buildServerWiringGates({
@@ -1,3 +1,4 @@
1
+ import { persistedPlaneOf } from "../../terminal.js";
1
2
  import { memoRedactedRead, memoRedactTerminalResult, redactErrorMessage, redactHead, redactLedgerEventData, redactTerminalResult } from "../../observability/run-terminal-log.js";
2
3
  import { z } from "zod";
3
4
  import { uuidv7, mintCheckpointToken, CheckpointError, validatePendingSteer } from "@sema-agent/core";
@@ -260,7 +261,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
260
261
  result: isTerminalRunStatus(run.status) ? memoRedactTerminalResult("run.result", run.result ?? undefined) : redactTerminalResult(run.result ?? undefined),
261
262
  supervisorCost,
262
263
  suggestions,
263
- errorCode: stale ? undefined : (run.result?.errorCode ?? undefined),
264
+ errorCode: stale ? undefined : (persistedPlaneOf(run.result).errorCode ?? undefined),
264
265
  error: stale ? "run stalled (instance lost?)" : isTerminalRunStatus(run.status) ? memoRedactedRead("run.error", run.error ?? undefined, redactErrorMessage) : redactErrorMessage(run.error ?? undefined),
265
266
  jobId: run.jobId ?? undefined,
266
267
  source: run.source ?? undefined,
@@ -331,7 +332,7 @@ async function handleRunsBody(req, res, url, ctx, miss) {
331
332
  const err = note;
332
333
  try {
333
334
  const cancelContext = buildCancelContext(taskId, Date.parse(run.createdAt));
334
- const result = { taskId, sessionId: run.sessionId, status: "failed", errorCode: "cancelled", errorMessage: err, stats: { turns: 0, tokens: 0 }, ...(cancelContext ? { cancelContext } : {}) };
335
+ const result = { taskId, sessionId: run.sessionId, terminal: { kind: "failed", code: "cancelled", message: err }, stats: { turns: 0, tokens: 0 }, ...(cancelContext ? { cancelContext } : {}) };
335
336
  let lastErr;
336
337
  for (let attempt = 0;; attempt++) {
337
338
  try {