@sema-agent/server 7.104.0 → 7.106.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 (206) hide show
  1. package/MIGRATION.md +114 -0
  2. package/USAGE.md +193 -52
  3. package/deploy/sema-up/chart/templates/server-v4pro.yaml +2 -0
  4. package/deploy/sema-up/cluster-up.sh +15 -2
  5. package/deploy/sema-up/kube-up.sh +8 -3
  6. package/deploy/sema-up/lib/flagshape.sh +29 -0
  7. package/deploy/sema-up/lib/parse-machines.py +4 -1
  8. package/deploy/sema-up/sema-up.sh +11 -9
  9. package/deploy/sema-up/stack/sema-stack-cluster.yaml +3 -1
  10. package/dist/approval-card.d.ts +1 -1
  11. package/dist/approval.d.ts +6 -1
  12. package/dist/approval.js +4 -4
  13. package/dist/auth-bridge.d.ts +14 -0
  14. package/dist/auth-bridge.js +7 -0
  15. package/dist/auto-mode-face.d.ts +49 -40
  16. package/dist/auto-mode-face.js +6 -68
  17. package/dist/bench/s1/live-deps.d.ts +9 -2
  18. package/dist/bench/s1/live-deps.js +13 -8
  19. package/dist/boot/budget-tracing.js +3 -2
  20. package/dist/boot/config-center.d.ts +4 -0
  21. package/dist/boot/config-center.js +95 -37
  22. package/dist/boot/coordinators.d.ts +1 -0
  23. package/dist/boot/coordinators.js +3 -1
  24. package/dist/boot/device-lane.d.ts +2 -1
  25. package/dist/boot/execution-env.d.ts +11 -0
  26. package/dist/boot/execution-env.js +30 -6
  27. package/dist/boot/execution-lane.js +2 -3
  28. package/dist/boot/leader.d.ts +2 -1
  29. package/dist/boot/leader.js +2 -0
  30. package/dist/boot/parked-revive-gate.d.ts +10 -7
  31. package/dist/boot/parked-revive-gate.js +4 -11
  32. package/dist/boot/resolve-spec.d.ts +1 -4
  33. package/dist/boot/resolve-spec.js +44 -21
  34. package/dist/boot/runner-deps.d.ts +5 -8
  35. package/dist/boot/runner-deps.js +5 -10
  36. package/dist/boot/side-query-lane.d.ts +2 -2
  37. package/dist/boot/stage-03-store-backend.d.ts +1 -1
  38. package/dist/boot/stage-03-store-backend.js +2 -2
  39. package/dist/boot/stage-04-stores.d.ts +1 -1
  40. package/dist/boot/stage-05-execution-env.d.ts +2 -1
  41. package/dist/boot/stage-05-execution-env.js +2 -2
  42. package/dist/boot/stage-06-runners.d.ts +3 -2
  43. package/dist/boot/stage-06-runners.js +2 -5
  44. package/dist/boot/stage-07-capability-layer.d.ts +2 -2
  45. package/dist/boot/stage-07-capability-layer.js +1 -2
  46. package/dist/boot/stage-08-reapers.d.ts +2 -2
  47. package/dist/boot/stage-09-leader.d.ts +2 -2
  48. package/dist/boot/stage-09-leader.js +2 -2
  49. package/dist/boot/stage-10-http-server.d.ts +2 -2
  50. package/dist/boot/stage-10-http-server.js +11 -7
  51. package/dist/boot/stores.d.ts +1 -1
  52. package/dist/boot/stores.js +6 -4
  53. package/dist/brain.d.ts +24 -2
  54. package/dist/brain.js +29 -10
  55. package/dist/budget.js +3 -0
  56. package/dist/capabilities/center-wiring.d.ts +64 -0
  57. package/dist/capabilities/center-wiring.js +24 -0
  58. package/dist/capabilities/composite-members.d.ts +89 -0
  59. package/dist/capabilities/composite-members.js +20 -0
  60. package/dist/capabilities/execution-lane.d.ts +3 -0
  61. package/dist/capabilities/execution-lane.js +5 -2
  62. package/dist/config-catalog.d.ts +30 -5
  63. package/dist/config-catalog.js +75 -26
  64. package/dist/config-center/apply-effective.d.ts +1 -1
  65. package/dist/config-center/apply-effective.js +7 -8
  66. package/dist/config-center/apply-ledger.d.ts +17 -0
  67. package/dist/config-center/apply-ledger.js +5 -0
  68. package/dist/config-center/center-request.d.ts +17 -0
  69. package/dist/config-center/center-request.js +4 -0
  70. package/dist/config-center/pull-state.d.ts +29 -0
  71. package/dist/config-center/pull-state.js +2 -0
  72. package/dist/config-types.d.ts +41 -14
  73. package/dist/config-types.js +1 -0
  74. package/dist/config.d.ts +50 -2
  75. package/dist/config.js +111 -23
  76. package/dist/cross-session-settings.d.ts +1 -1
  77. package/dist/deployment-governance.d.ts +12 -1
  78. package/dist/deployment-governance.js +2 -0
  79. package/dist/execution-lane-caps.d.ts +30 -2
  80. package/dist/execution-lane-caps.js +12 -0
  81. package/dist/file-history-disclosure.d.ts +7 -31
  82. package/dist/file-history-disclosure.js +12 -0
  83. package/dist/fleet/fleet-bus.d.ts +5 -1
  84. package/dist/fleet-client.d.ts +18 -13
  85. package/dist/fleet-client.js +32 -22
  86. package/dist/fleet-lease.d.ts +72 -47
  87. package/dist/fleet-lease.js +66 -14
  88. package/dist/governance-http-status.d.ts +0 -1
  89. package/dist/governance-http-status.js +0 -1
  90. package/dist/hosted-posture.d.ts +6 -17
  91. package/dist/hosted-posture.js +11 -6
  92. package/dist/http/admission.d.ts +1 -1
  93. package/dist/http/admission.js +14 -9
  94. package/dist/http/approval-sweeps.js +13 -1
  95. package/dist/http/dispatch.d.ts +18 -0
  96. package/dist/http/dispatch.js +18 -5
  97. package/dist/http/durable-run-leg.js +2 -0
  98. package/dist/http/request-guards.d.ts +25 -0
  99. package/dist/http/request-guards.js +25 -5
  100. package/dist/http/resume-legs.d.ts +1 -1
  101. package/dist/http/resume-legs.js +19 -11
  102. package/dist/http/route-ctx.d.ts +9 -0
  103. package/dist/http/route-table.d.ts +15 -4
  104. package/dist/http/route-table.js +6 -5
  105. package/dist/http/routes/approvals-assistant.js +0 -1
  106. package/dist/http/routes/capabilities.js +12 -11
  107. package/dist/http/routes/config-catalog.js +2 -1
  108. package/dist/http/routes/diagnostics.js +2 -0
  109. package/dist/http/routes/fleet.d.ts +3 -2
  110. package/dist/http/routes/images.js +0 -1
  111. package/dist/http/routes/runs.js +2 -8
  112. package/dist/http/routes/session-sync.js +6 -1
  113. package/dist/http/routes/sessions.js +5 -2
  114. package/dist/http/routes/side-query.js +8 -1
  115. package/dist/http/routes/tasks.js +4 -1
  116. package/dist/http/routes/trace-usage.d.ts +2 -0
  117. package/dist/http/routes/trace-usage.js +4 -9
  118. package/dist/http/server.d.ts +29 -8
  119. package/dist/http/server.js +4 -1
  120. package/dist/http/session-inherit-env.d.ts +37 -0
  121. package/dist/http/session-inherit-env.js +6 -0
  122. package/dist/http/sse-lifecycle.d.ts +1 -1
  123. package/dist/http/sse-log.d.ts +12 -9
  124. package/dist/http/sse-log.js +1 -8
  125. package/dist/http/wire-types.d.ts +19 -28
  126. package/dist/index.d.ts +2 -1
  127. package/dist/index.js +2 -1
  128. package/dist/key-resolver.d.ts +74 -19
  129. package/dist/key-resolver.js +65 -7
  130. package/dist/leader/leader.d.ts +6 -0
  131. package/dist/leader/leader.js +2 -1
  132. package/dist/leader/repair-wire.js +2 -1
  133. package/dist/leader/wire.d.ts +9 -1
  134. package/dist/leader/wire.js +10 -6
  135. package/dist/observability/fail-open.d.ts +9 -1
  136. package/dist/observability/fail-open.js +9 -1
  137. package/dist/observability/metrics.js +3 -1
  138. package/dist/observability/permission-rule-events.d.ts +1 -1
  139. package/dist/observability/permission-rule-events.js +1 -1
  140. package/dist/observability/run-terminal.d.ts +82 -9
  141. package/dist/observability/run-terminal.js +112 -9
  142. package/dist/observability/secret-env-scrub.js +1 -1
  143. package/dist/observability/tool-trace.d.ts +2 -0
  144. package/dist/operator-ask.d.ts +29 -1
  145. package/dist/operator-ask.js +6 -0
  146. package/dist/parked-decide.d.ts +14 -1
  147. package/dist/parked-decide.js +8 -1
  148. package/dist/plugins/image-bake-store-sql.d.ts +0 -2
  149. package/dist/plugins/image-bake-store-sql.js +0 -5
  150. package/dist/plugins/retention-store-sql.js +1 -0
  151. package/dist/plugins/run-store-sql.d.ts +3 -2
  152. package/dist/plugins/schema-indexes.js +1 -0
  153. package/dist/plugins/session-policy-store-sql.d.ts +38 -7
  154. package/dist/plugins/session-policy-store-sql.js +73 -18
  155. package/dist/plugins/sql-driver.d.ts +1 -1
  156. package/dist/plugins/store-backend.d.ts +17 -6
  157. package/dist/plugins/store-backend.js +16 -5
  158. package/dist/plugins/tidb-pool.js +5 -4
  159. package/dist/read-face-posture.d.ts +25 -4
  160. package/dist/read-face-posture.js +9 -0
  161. package/dist/request-key-closure.js +2 -2
  162. package/dist/request-value-closure.d.ts +17 -5
  163. package/dist/request-value-closure.js +12 -0
  164. package/dist/router/route-orchestration.d.ts +11 -0
  165. package/dist/router/route-orchestration.js +7 -6
  166. package/dist/rules-consent.js +0 -11
  167. package/dist/run-local.js +4 -2
  168. package/dist/runs.d.ts +2 -1
  169. package/dist/runs.js +4 -4
  170. package/dist/runtime-caps-resolver.d.ts +4 -10
  171. package/dist/runtime-governance.d.ts +8 -11
  172. package/dist/runtime-governance.js +10 -41
  173. package/dist/sealed-key.d.ts +0 -40
  174. package/dist/sealed-key.js +0 -20
  175. package/dist/server-secret-env.d.ts +11 -12
  176. package/dist/session-policy-wire.d.ts +100 -12
  177. package/dist/session-policy-wire.js +71 -8
  178. package/dist/session-titler.d.ts +11 -0
  179. package/dist/session-titler.js +6 -2
  180. package/dist/sql-engine-posture.d.ts +2 -2
  181. package/dist/store-posture.d.ts +53 -0
  182. package/dist/store-posture.js +32 -0
  183. package/dist/task-settings.d.ts +34 -45
  184. package/dist/task-settings.js +46 -25
  185. package/dist/tool-approval.d.ts +51 -22
  186. package/dist/tool-approval.js +44 -8
  187. package/dist/tool-declaration-http-status.d.ts +12 -0
  188. package/dist/tool-declaration-http-status.js +12 -0
  189. package/dist/trace/core-keyset-guard.d.ts +7 -7
  190. package/dist/trace/engine-notice-wire.d.ts +1 -1
  191. package/dist/trace/engine-notice-wire.js +5 -0
  192. package/dist/trace/project.d.ts +13 -7
  193. package/dist/trace/project.js +54 -6
  194. package/dist/trace/redact.d.ts +7 -0
  195. package/dist/trace/redact.js +153 -61
  196. package/dist/trace/wire-projection-faces.d.ts +1 -1
  197. package/dist/trace/wire-projection-faces.js +1 -1
  198. package/dist/unattended-permission-mode.d.ts +110 -0
  199. package/dist/unattended-permission-mode.js +65 -0
  200. package/package.json +3 -3
  201. package/dist/boot/session-shell-gate-registry.d.ts +0 -39
  202. package/dist/boot/session-shell-gate-registry.js +0 -21
  203. package/dist/plugins/memory-session-policy-store.d.ts +0 -46
  204. package/dist/plugins/memory-session-policy-store.js +0 -52
  205. package/dist/roster-bound-http-status.d.ts +0 -6
  206. package/dist/roster-bound-http-status.js +0 -6
@@ -1,27 +1,79 @@
1
1
  import type { Model } from "@sema-agent/core";
2
- import { type SealedKeyPoison } from "./sealed-key.js";
2
+ /** 托管密钥解不开的三个成因(**托管**失败;密文形错归 S-704 `config.model_entry_invalid`,旧词 `unsupported_alg` 已随之退役)。 */
3
+ export type SealedCustodyPoisonReason = "sealed_decrypt_failed" | "stale_ciphertext" | "no_key_store";
4
+ /** 毒丸成因全集(闭集;拒句按它穷举)。 */
5
+ export type ModelKeyPoisonReason = SealedCustodyPoisonReason | "env_missing";
6
+ /**
7
+ * 毒丸 —— **只带诊断**(来源身份 + 成因 + 检测时刻),从不带密文、明文或变量的值(每一个序列化面平凡地零秘密)。
8
+ * · `source:"sealed"`:`publicKeyId` = 密文指向的钥匙对;`errKind` = 解封失败的错误**类名**(不是消息;消息在植入那一刻已进日志)。
9
+ * · `source:"env"`:`apiKeyEnv` = 目录声明的变量**名**。
10
+ */
11
+ export type ModelKeyPoison = {
12
+ readonly modelKeyPoison: true;
13
+ readonly source: "sealed";
14
+ readonly reason: SealedCustodyPoisonReason;
15
+ readonly publicKeyId: string;
16
+ readonly errKind?: string;
17
+ /** ISO timestamp of the apply that detected the failure. */
18
+ readonly at: string;
19
+ } | {
20
+ readonly modelKeyPoison: true;
21
+ readonly source: "env";
22
+ readonly reason: "env_missing";
23
+ readonly apiKeyEnv: string;
24
+ /** ISO timestamp of the resolve that detected the absence. */
25
+ readonly at: string;
26
+ };
27
+ /** `config.modelApiKeys` 里能植的那一种(env 来源的毒丸由链现判,不进表)。 */
28
+ export type SealedCustodyPoison = Extract<ModelKeyPoison, {
29
+ source: "sealed";
30
+ }>;
31
+ /** 铸毒丸(纯数据 ⇒ 加上标记与检测时刻)。重载让调用方拿到它那一种的窄型。 */
32
+ export declare function modelKeyPoison(p: {
33
+ source: "sealed";
34
+ reason: SealedCustodyPoisonReason;
35
+ publicKeyId: string;
36
+ errKind?: string;
37
+ }): SealedCustodyPoison;
38
+ export declare function modelKeyPoison(p: {
39
+ source: "env";
40
+ reason: "env_missing";
41
+ apiKeyEnv: string;
42
+ }): ModelKeyPoison;
43
+ export declare function isModelKeyPoison(v: unknown): v is ModelKeyPoison;
44
+ /** 毒丸的来源身份(托管 ⇒ `publicKeyId`;env ⇒ 变量名)—— 指纹与日志的读口,穷举 switch。 */
45
+ export declare function modelKeyPoisonIdentity(p: ModelKeyPoison): string;
46
+ /**
47
+ * The task-level fail-loud face of a poison pill: thrown by the key resolver on EVERY brain call that would otherwise
48
+ * authenticate this model, so an unusable declared key fails the task visibly instead of silently burning the gateway
49
+ * account. Carries the poison (diagnosis only) — nothing secret, by construction of {@link ModelKeyPoison}.
50
+ */
51
+ export declare class ModelKeyPoisonedError extends Error {
52
+ readonly poison: ModelKeyPoison;
53
+ readonly reason: ModelKeyPoisonReason;
54
+ readonly at: string;
55
+ constructor(modelName: string, poison: ModelKeyPoison);
56
+ }
3
57
  /**
4
58
  * Build a per-model API-key resolver for `TaskSpec.getApiKeyAndHeaders` from the catalog's
5
59
  * name → env-var-NAME map (sema-registry `apiKeyEnv`). core threads this per brain call — and per cascade
6
60
  * rung — so each model/rung authenticates with its OWN upstream account/key.
7
61
  *
8
- * Additive by construction: a model absent from the map — or whose env var is unset — returns
9
- * `undefined`, so core falls back to the brain's construction-time gateway key (today's behavior, no
10
- * regression). When no per-model keys are configured at all this returns `undefined`, leaving the spec
11
- * field unset so core's default path is byte-for-byte unchanged.
62
+ * A model absent from BOTH maps returns `undefined`, so core falls back to the brain's construction-time
63
+ * gateway key — that is the operator's choice (the entry declared no key of its own). A model that DECLARED
64
+ * a key it cannot get (poisoned sealed key, or an `apiKeyEnv` whose variable is missing / empty in this
65
+ * process) THROWS {@link ModelKeyPoisonedError} — never `undefined` (S-896). When no per-model keys are
66
+ * configured at all this returns `undefined`, leaving the spec field unset so core's default path is
67
+ * byte-for-byte unchanged.
12
68
  *
13
69
  * Only the api KEY is per-model; the base URL stays brain-owned ([ref]) — core's seam carries
14
- * `{ apiKey, headers? }`, not a baseUrl. Same-account-rotation could round-robin inside this function.
70
+ * `{ apiKey, headers? }`, not a baseUrl.
15
71
  *
16
72
  * `modelApiKeys` (sealed-box custody) carries per-model entries unsealed from `ModelEntry.sealedApiKey` —
17
- * resolved FIRST (sealed outranks the env-NAME reference, the registry-core mutual-exclusion ruling). Each
18
- * entry is either the PLAINTEXT key (successful unseal) or a `SealedKeyPoison` marker (unseal FAILED at
19
- * apply time): a poisoned model THROWS `SealedKeyPoisonedError` on every resolve — task-level fail-loud —
20
- * because returning `undefined` here means "use the gateway key", and a broken sealed key must NEVER
21
- * silently burn the shared gateway account (the core invariant of the poison-pill fix; a poisoned model
22
- * skips its env reference too, same mutual-exclusion as a healthy sealed key). The maps are disjoint by
23
- * construction in applyEffective (a sealed model never lands in modelApiKeyEnv); the ordering here is
24
- * belt-and-suspenders. 🔴 Plaintext values are live secrets: memory-only, never log them.
73
+ * resolved FIRST (sealed outranks the env-NAME reference, the registry-core mutual-exclusion ruling; a
74
+ * poisoned sealed model skips its env reference too). The maps are disjoint by construction in
75
+ * applyEffective (a sealed model never lands in modelApiKeyEnv); the ordering here is belt-and-suspenders.
76
+ * 🔴 Plaintext values are live secrets: memory-only, never log them.
25
77
  */
26
78
  /** The ONE field the per-model key plane keys on. The resolver's parameter is deliberately this minimal
27
79
  * named ref (not the full core `Model`): full models are structural supersets and pass unchanged, while
@@ -29,12 +81,14 @@ import { type SealedKeyPoison } from "./sealed-key.js";
29
81
  * can mint `{ name }` legally instead of forcing it through an `as never` escape hatch (B1). Contravariance
30
82
  * keeps the resolver assignable wherever `(model: Model) => …` is expected. */
31
83
  export type ModelKeyRef = Pick<Model, "name">;
32
- export declare function createKeyResolver(modelApiKeyEnv: Record<string, string>, env?: NodeJS.ProcessEnv, modelApiKeys?: Record<string, string | SealedKeyPoison>): ((model: ModelKeyRef) => Promise<{
84
+ export declare function createKeyResolver(modelApiKeyEnv: Record<string, string>, env?: NodeJS.ProcessEnv, modelApiKeys?: Record<string, string | SealedCustodyPoison>): ((model: ModelKeyRef) => Promise<{
33
85
  apiKey: string;
34
86
  } | undefined>) | undefined;
87
+ /** 「这只模型声明的钥匙来源今天拿不到」⇒ 那一只毒丸;拿得到 / 没声明 ⇒ `undefined`。能力面的配对标注据它答 `"unknown"`(判别,不是 catch)。 */
88
+ export declare function modelKeyPoisonOf(modelName: string, modelApiKeyEnv: Record<string, string>, env?: NodeJS.ProcessEnv, modelApiKeys?: Record<string, string | SealedCustodyPoison>): ModelKeyPoison | undefined;
35
89
  /**
36
90
  * The ONE per-model key chain, synchronous: **sealed custody → env-NAME reference → (caller's) gateway key**,
37
- * with a poisoned sealed entry THROWING instead of returning undefined. `createKeyResolver` is the async
91
+ * with a declared-but-unusable source THROWING instead of returning undefined. `createKeyResolver` is the async
38
92
  * `getApiKeyAndHeaders` face of exactly this function — every other consumer must call it rather than
39
93
  * re-implement a subset of the chain.
40
94
  *
@@ -42,8 +96,9 @@ export declare function createKeyResolver(modelApiKeyEnv: Record<string, string>
42
96
  * 降级目标(按互斥契约**永不**落 modelApiKeyEnv)于是静默拿网关 key 打自己的网关(错 key 打上游),毒丸更
43
97
  * 被当成"没配 key"。链只有一条,重复实现就会长出这种半条链的偏差,故此处抽出同步内核共用。
44
98
  *
45
- * `undefined` = this model has no per-model key ⇒ the CALLER's gateway/default credential applies (the
46
- * additive contract). An empty env value counts as unset (unchanged from the original inline chain).
99
+ * `undefined` = this model DECLARED no per-model key ⇒ the CALLER's gateway/default credential applies (the
100
+ * additive contract). A declared env reference whose variable is missing or empty throws (S-896; 7.105.0 and
101
+ * earlier answered `undefined` there — the silent gateway fallback this family removes).
47
102
  */
48
- export declare function resolveModelApiKey(modelName: string, modelApiKeyEnv: Record<string, string>, env?: NodeJS.ProcessEnv, modelApiKeys?: Record<string, string | SealedKeyPoison>): string | undefined;
103
+ export declare function resolveModelApiKey(modelName: string, modelApiKeyEnv: Record<string, string>, env?: NodeJS.ProcessEnv, modelApiKeys?: Record<string, string | SealedCustodyPoison>): string | undefined;
49
104
  //# sourceMappingURL=key-resolver.d.ts.map
@@ -1,4 +1,46 @@
1
- import { isSealedKeyPoison, SealedKeyPoisonedError } from "./sealed-key.js";
1
+ export function modelKeyPoison(p) {
2
+ const at = new Date().toISOString();
3
+ if (p.source === "sealed")
4
+ return { modelKeyPoison: true, source: "sealed", reason: p.reason, publicKeyId: p.publicKeyId, ...(p.errKind ? { errKind: p.errKind } : {}), at };
5
+ return { modelKeyPoison: true, source: "env", reason: p.reason, apiKeyEnv: p.apiKeyEnv, at };
6
+ }
7
+ export function isModelKeyPoison(v) {
8
+ return typeof v === "object" && v !== null && v.modelKeyPoison === true;
9
+ }
10
+ export function modelKeyPoisonIdentity(p) {
11
+ switch (p.source) {
12
+ case "sealed":
13
+ return p.publicKeyId;
14
+ case "env":
15
+ return p.apiKeyEnv;
16
+ }
17
+ }
18
+ function poisonedMessage(modelName, p) {
19
+ switch (p.reason) {
20
+ case "sealed_decrypt_failed":
21
+ case "stale_ciphertext":
22
+ case "no_key_store":
23
+ return (`sealed api key for model "${modelName}" is unusable (${p.reason}; publicKeyId "${modelKeyPoisonIdentity(p)}"; detected ${p.at}) — ` +
24
+ `refusing the gateway-key fallback. Fix custody: re-paste the key in the web UI (it seals against the newest registered public key), ` +
25
+ `or restore this host's private key for that id / complete the rotation so the ciphertext targets a held key.`);
26
+ case "env_missing":
27
+ return (`api key for model "${modelName}" is unusable (env_missing: the catalog entry declares apiKeyEnv "${modelKeyPoisonIdentity(p)}" but this service's environment ` +
28
+ `has no non-empty ${modelKeyPoisonIdentity(p)}; detected ${p.at}) — refusing the gateway-key fallback. Set ${modelKeyPoisonIdentity(p)} in the environment of the ` +
29
+ `process that runs this service (and restart it), or drop apiKeyEnv from the catalog entry to authenticate this model with the deployment's gateway key on purpose.`);
30
+ }
31
+ }
32
+ export class ModelKeyPoisonedError extends Error {
33
+ poison;
34
+ reason;
35
+ at;
36
+ constructor(modelName, poison) {
37
+ super(poisonedMessage(modelName, poison));
38
+ this.name = "ModelKeyPoisonedError";
39
+ this.poison = poison;
40
+ this.reason = poison.reason;
41
+ this.at = poison.at;
42
+ }
43
+ }
2
44
  export function createKeyResolver(modelApiKeyEnv, env = process.env, modelApiKeys = {}) {
3
45
  if (Object.keys(modelApiKeyEnv).length === 0 && Object.keys(modelApiKeys).length === 0)
4
46
  return undefined;
@@ -7,15 +49,31 @@ export function createKeyResolver(modelApiKeyEnv, env = process.env, modelApiKey
7
49
  return apiKey ? { apiKey } : undefined;
8
50
  };
9
51
  }
10
- export function resolveModelApiKey(modelName, modelApiKeyEnv, env = process.env, modelApiKeys = {}) {
52
+ function chainLookup(modelName, modelApiKeyEnv, env, modelApiKeys) {
11
53
  const sealed = modelApiKeys[modelName];
12
- if (isSealedKeyPoison(sealed))
13
- throw new SealedKeyPoisonedError(modelName, sealed);
54
+ if (isModelKeyPoison(sealed))
55
+ return { kind: "poison", poison: sealed };
14
56
  if (sealed)
15
- return sealed;
57
+ return { kind: "key", key: sealed };
16
58
  const envName = modelApiKeyEnv[modelName];
17
59
  if (!envName)
18
- return undefined;
19
- return env[envName] || undefined;
60
+ return { kind: "undeclared" };
61
+ const value = env[envName];
62
+ return value ? { kind: "key", key: value } : { kind: "poison", poison: modelKeyPoison({ source: "env", reason: "env_missing", apiKeyEnv: envName }) };
63
+ }
64
+ export function modelKeyPoisonOf(modelName, modelApiKeyEnv, env = process.env, modelApiKeys = {}) {
65
+ const r = chainLookup(modelName, modelApiKeyEnv, env, modelApiKeys);
66
+ return r.kind === "poison" ? r.poison : undefined;
67
+ }
68
+ export function resolveModelApiKey(modelName, modelApiKeyEnv, env = process.env, modelApiKeys = {}) {
69
+ const r = chainLookup(modelName, modelApiKeyEnv, env, modelApiKeys);
70
+ switch (r.kind) {
71
+ case "key":
72
+ return r.key;
73
+ case "poison":
74
+ throw new ModelKeyPoisonedError(modelName, r.poison);
75
+ case "undeclared":
76
+ return undefined;
77
+ }
20
78
  }
21
79
  //# sourceMappingURL=key-resolver.js.map
@@ -16,6 +16,7 @@ import { type RunnerLike, type WorkerReport } from "./fanout.js";
16
16
  import { type ExecStreamLike } from "./diffout.js";
17
17
  import { type MergeDeps, type MergeResult } from "./merge.js";
18
18
  import { type TaskSpec, type VerifyConfig, type CheckpointToken, type TerminalCause, type ToolSpec, type RepairTerminal, type OracleTier, type RepairResult } from "@sema-agent/core";
19
+ import { type UnattendedLeg } from "../unattended-permission-mode.js";
19
20
  /** [ref] Slice 6: drive a worker's resource-suspend auto-resume to a terminal (or bound) state. Exported
20
21
  * for unit tests. Loops `resume(token, slice)` WHILE the result's terminal cause is a park on a
21
22
  * `resource_limit` gate. Exits on: a terminal cause that is not a park, a park on any OTHER gate
@@ -67,6 +68,11 @@ export interface ProvisionedWorker {
67
68
  export interface LeaderDeps {
68
69
  /** Plan a task into N disjoint-file sub-tasks (MVP: simple/hand-authored; adaptive replan = scale). */
69
70
  plan: (task: string) => Promise<SubtaskSpec[]>;
71
+ /** S-877 候裁 15 / 18 —— 本车道有手的 run 并进的无人值守腿(模式 = 运维旋钮 `UNATTENDED_PERMISSION_MODE`、写面 roots、模式释放写保护时的
72
+ * 写保护拒层),由 `wire.ts` 经 `createUnattendedLeg` 铸一次递进来。本文件自己起的有手 run(模块门修复)与 `repair-wire.ts` 的单代理修复环按
73
+ * `sub.spec` 起 run、不经 provisionWorker —— 两处都经 `withUnattendedLeg` 并进这一份。**必填**(缺席 = core「Absent = default」⇒ 无人可答的腿上
74
+ * 写与非只读命令被当场拒,codex F1 的病)。 */
75
+ unattendedLeg: UnattendedLeg;
70
76
  /** Provision a leader-owned worker (its env seeded with base at `branch` + an env-bound runner). */
71
77
  provisionWorker: (sub: SubtaskSpec) => Promise<ProvisionedWorker>;
72
78
  /** Ephemeral no-creds integration sandbox + the credentialed Coordinator push (merge(b) deps). */
@@ -4,6 +4,7 @@ import { pullDiff } from "./diffout.js";
4
4
  import { mergeBranches } from "./merge.js";
5
5
  import { Runner, runWithVerification, resumeWithVerification } from "@sema-agent/core";
6
6
  import { pausedCauseOf, outwardPlane } from "../terminal.js";
7
+ import { withUnattendedLeg } from "../unattended-permission-mode.js";
7
8
  const MAX_RESUME_SLICES = 200;
8
9
  export async function autoResumeOnResource(initial, signal, resume, maxSlices = MAX_RESUME_SLICES) {
9
10
  let vr = initial;
@@ -200,7 +201,7 @@ export async function runLeaderTask(task, durableBaseSha, deps) {
200
201
  }
201
202
  deps.logger?.info?.("module_repair", { workerId: r.workerId, attempt: attempt + 1, reason: redactHead(verdict.reason ?? "", 500) });
202
203
  try {
203
- const rr = await w.runner.runTaskStream({ ...sub.spec, objective: deps.moduleGate.repairObjective(sub, safeVerdictReason), sessionId: `${w.sessionId}-r${attempt + 1}` }).result();
204
+ const rr = await w.runner.runTaskStream(withUnattendedLeg({ ...sub.spec, objective: deps.moduleGate.repairObjective(sub, safeVerdictReason), sessionId: `${w.sessionId}-r${attempt + 1}` }, deps.unattendedLeg)).result();
204
205
  if (r.stats && rr?.stats)
205
206
  r.stats = { ...r.stats, tokens: (r.stats.tokens ?? 0) + (rr.stats.tokens ?? 0), costMicroUsd: (r.stats.costMicroUsd ?? 0) + (rr.stats.costMicroUsd ?? 0) };
206
207
  }
@@ -2,6 +2,7 @@ import { runRepairLoop, delimitUntrusted, } from "@sema-agent/core";
2
2
  import { mkRepairOracle } from "./repair-oracle.js";
3
3
  import { outwardPlane } from "../terminal.js";
4
4
  import { redactHead } from "../observability/run-terminal.js";
5
+ import { withUnattendedLeg } from "../unattended-permission-mode.js";
5
6
  function statusForTerminal(t) {
6
7
  return t === "fixed" || t === "candidate_only" ? "completed" : "failed";
7
8
  }
@@ -86,7 +87,7 @@ export function attachRepairLoopDeps(deps, resolve) {
86
87
  deps.repairLoop = {
87
88
  run: async (solo, sub) => {
88
89
  const cfg = await resolve(solo, sub);
89
- const implSpec = { ...sub.spec, sessionId: solo.sessionId };
90
+ const implSpec = withUnattendedLeg({ ...sub.spec, sessionId: solo.sessionId }, deps.unattendedLeg);
90
91
  return runRepairLoopForLeader(solo, implSpec, cfg);
91
92
  },
92
93
  };
@@ -1,4 +1,4 @@
1
- import { type Brain, type Model, type ModelRoles, type ModelPricing, type TaskSpec, type ExecutionEnvFactory, type RemoteExecutionEnv, type ToolResultStore, type SessionStore, type ExecutionEnv, type RunnerDeps } from "@sema-agent/core";
1
+ import { type Brain, type Model, type ModelRoles, type ModelPricing, type TaskSpec, type ExecutionEnvFactory, type RemoteExecutionEnv, type ToolResultStore, type SessionStore, type ExecutionEnv, type RunnerDeps, type PermissionMode } from "@sema-agent/core";
2
2
  import type { CheckpointStoreFull } from "../plugins/store-backend.js";
3
3
  import { type LeaderDeps, type LeaderResult } from "./leader.js";
4
4
  import { type DeploymentPostureSeats } from "../boot/runner-deps.js";
@@ -19,6 +19,14 @@ export interface LeaderWireConfig {
19
19
  * REMOTE_EXEC (e2b or k8s/Kata) — workers and the integration sandbox run on envs from THIS factory.
20
20
  * Requires `s3` (diff-out is the worker's self-upload; there is no leader-held env to pull from). */
21
21
  envFactory?: ExecutionEnvFactory;
22
+ /**
23
+ * S-877 候裁 15 → B —— 本车道**有手**的每一条 run(worker 两车道、集成沙箱修复、模块门修复、单代理修复环)的
24
+ * `TaskSpec.permissionMode`。**必填**:缺席 = core「Absent = `default`」⇒ 写与非只读命令在无人可答的这条腿上被
25
+ * headless 当场拒(codex F1 的病);必填让每一个构造本配置的地方都得表态,不许静默落回那一形。
26
+ * 唯一取值点 = `boot/leader.ts` 调 `unattendedPermissionMode(config)`(`src/unattended-permission-mode.ts`,与 run-local 同一只);
27
+ * 本文件经 `createUnattendedLeg` / `withUnattendedLeg` 铸并(模式 + 写面 roots + 候裁 18 的写保护拒层)。planner 路由无手,不读。
28
+ */
29
+ unattendedPermissionMode: PermissionMode;
22
30
  /** Durable suspend/resume for SUB-WORKERS ([ref] C4): when the seat yields a value, that worker task carries
23
31
  * the durable question/ask policy + checkpoint store, so a gated ask/AskUserQuestion SUSPENDS the worker
24
32
  * (checkpoint + paused env) instead of failing — the trigger replan-lite C4 arbitrates on. Mirrors main.ts's
@@ -5,6 +5,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
5
5
  import { join } from "node:path";
6
6
  import { tmpdir } from "node:os";
7
7
  import { Runner, combinePolicies, createDurableQuestionPolicy, QUESTION_AWAITS_RESUME, delimitUntrusted, hasDestroy, } from "@sema-agent/core";
8
+ import { createUnattendedLeg, withUnattendedLeg } from "../unattended-permission-mode.js";
8
9
  import { RemoteContainerExecutionEnv } from "../plugins/remote-env-e2b.js";
9
10
  import { E2B_SANDBOX_HOME } from "../execution-lane-caps.js";
10
11
  import { createDurableAskPolicy } from "../approval.js";
@@ -138,6 +139,7 @@ export function createLeaderRunner(cfg) {
138
139
  const onNoticeSeat = createEngineNoticeSeat(cfg.logger);
139
140
  const governanceSeat = cfg.governance ?? {};
140
141
  const deploymentPostureSeat = () => cfg.deploymentPosture?.() ?? {};
142
+ const unattendedLeg = () => createUnattendedLeg(cfg.unattendedPermissionMode, deploymentPostureSeat());
141
143
  const tracerSeat = cfg.tracer ? { tracer: cfg.tracer } : {};
142
144
  const usageWindowSeat = cfg.usageWindows && cfg.usageWindowStore ? { usageWindows: cfg.usageWindows, usageWindowStore: cfg.usageWindowStore } : {};
143
145
  const promptSourceSeat = cfg.promptSource ? { promptSource: cfg.promptSource } : {};
@@ -169,12 +171,12 @@ export function createLeaderRunner(cfg) {
169
171
  delimitUntrusted("FAILING OUTPUT (untrusted)", errorText),
170
172
  ].join("\n");
171
173
  const res = await runner
172
- .runTaskStream({
174
+ .runTaskStream(withUnattendedLeg({
173
175
  objective,
174
176
  ...principalSpec(),
175
177
  sessionId: `leader-repair-${Date.now()}-${round}`,
176
178
  limits: { maxCostUsd: repairBudgetUsd, maxWalltimeMs: Math.max(300_000, Math.floor(leaderTimeoutMs / 2)) },
177
- })
179
+ }, unattendedLeg()))
178
180
  .result();
179
181
  if (res.terminal.kind !== "completed")
180
182
  throw new Error(`repair round ${round} did not complete (status=${outwardPlane(res.terminal).status})`);
@@ -243,6 +245,7 @@ export function createLeaderRunner(cfg) {
243
245
  const provisionWorker = async (sub) => {
244
246
  const sessionId = `leader-${sub.workerId}-${Date.now()}`;
245
247
  const durable = cfg.durable?.();
248
+ const resumeCtx = { body: { objective: sub.spec.objective }, unattendedLeg: true };
246
249
  let durableSpec = {};
247
250
  if (durable) {
248
251
  const d = durable;
@@ -252,7 +255,7 @@ export function createLeaderRunner(cfg) {
252
255
  durableApproval: { scope: "_leader", ...(d.ttlMs ? { ttlMs: d.ttlMs } : {}) },
253
256
  onQuestion: QUESTION_AWAITS_RESUME,
254
257
  };
255
- await d.checkpointStore.putCtx(sessionId, { body: { objective: sub.spec.objective } }).catch((e) => {
258
+ await d.checkpointStore.putCtx(sessionId, resumeCtx).catch((e) => {
256
259
  cfg.logger?.warn?.("leader_putctx_failed", { workerId: sub.workerId, err: redactHead(e instanceof Error ? e.message : String(e), 2000) });
257
260
  });
258
261
  }
@@ -287,7 +290,7 @@ export function createLeaderRunner(cfg) {
287
290
  runner.runTaskStream = (spec, resume) => {
288
291
  const stream = orig(spec, resume);
289
292
  const iv = setInterval(() => {
290
- void d.checkpointStore.putCtx(sessionId, { body: { objective: sub.spec.objective } }).catch(() => { });
293
+ void d.checkpointStore.putCtx(sessionId, resumeCtx).catch(() => { });
291
294
  }, 45_000);
292
295
  iv.unref?.();
293
296
  void stream.result().then(() => clearInterval(iv), () => clearInterval(iv));
@@ -322,7 +325,7 @@ export function createLeaderRunner(cfg) {
322
325
  }),
323
326
  })),
324
327
  fetchDiff: () => fetchUploadedDiff(getUrl),
325
- spec: { ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, ...principalSpec(), objective: `${sub.spec.objective}${uploadStepSuffix(W)}` },
328
+ spec: withUnattendedLeg({ ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, ...principalSpec(), objective: `${sub.spec.objective}${uploadStepSuffix(W)}` }, unattendedLeg()),
326
329
  };
327
330
  }
328
331
  if (!cfg.e2bApiKey)
@@ -337,7 +340,7 @@ export function createLeaderRunner(cfg) {
337
340
  runner: keepCtxWarm(new Runner({ brain: cfg.brain, models: cfg.models, roles: cfg.roles, pricing: cfg.pricing, ...onNoticeSeat, ...governanceSeat, ...deploymentPostureSeat(), ...tracerSeat, ...usageWindowSeat, ...promptSourceSeat, ...onErrorSeat, ...(cfg.toolResultStore ? { toolResultStore: cfg.toolResultStore } : {}), ...(cfg.sessionStore ? { sessionStore: cfg.sessionStore } : {}), executionEnv: env })),
338
341
  diffEnv: env,
339
342
  destroy: () => env.destroy().then(() => { }),
340
- spec: { ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, ...principalSpec() },
343
+ spec: withUnattendedLeg({ ...workerLimits, ...sub.spec, ...durableSpec, ...resourceSpec, ...principalSpec() }, unattendedLeg()),
341
344
  };
342
345
  };
343
346
  const provisionIntegrationSandbox = async () => {
@@ -379,6 +382,7 @@ export function createLeaderRunner(cfg) {
379
382
  const deps = {
380
383
  plan,
381
384
  provisionWorker,
385
+ unattendedLeg: unattendedLeg(),
382
386
  provisionIntegrationSandbox,
383
387
  push,
384
388
  testCmd: body.testCmd,
@@ -25,6 +25,10 @@ export declare const FAIL_OPEN_TAGS: {
25
25
  readonly cls: "F";
26
26
  readonly note: "S-297(`http/routes/sessions.ts lastLegMcpFor`,`/mcp` 面板的可选键 `lastLegMcp`):按会话取最近 run 的索引读 / 那条 run 的账本读**抛错或超期** ⇒ 该键缺席,面板本体照常 200。放行的最坏后果=这一屏少一格「上一次真挂上了什么」(壳退回翻账本),不是门、不是权限、不是凭证 —— 而反方向(让附加键的读失败打掉整张面板)会给一口此前零 MCP 配置下根本不碰账本的**既有**读面新加一条挂掉的理由。缺席本就是本键 wire 上的一等状态(消费端必须会读),所以这条兜底不产生任何新的消费端形态;计数让「某台部署的账本一直读不出来」显形。";
27
27
  };
28
+ readonly "server.unattended-leg.mode-releases-protective-asks": {
29
+ readonly cls: "P-DEBT";
30
+ readonly note: "S-877 四续 ⑧(候裁 18 D 的已披露残余,`unattended-permission-mode.ts` 的 `createUnattendedLeg`):无人值守腿(leader worker / 修复腿 / run-local / 续跑重挂)的旋钮 `UNATTENDED_PERMISSION_MODE` 缺省 `bypassPermissions` 释放六类保护问(`probe` / `tool_marks_mcp` / `tool_marks_a2a` / `transcript_write` / `rule_unreadable` / `artifact_shared_read`;写保护一类已改拒)—— 7.105.0 这些腿上它们照问、无人答即拒。放行的最坏后果=无人值守的写 / 读 / MCP·A2A 标记工具调用不再被这几道保护问拦下,`rule_unreadable` 是「规则店读不出照放」(安全轴 fail-open 方向)。core 对这几类没有拒座;S-906(core 7.37.0 按类作答)到货后本臂与旋钮一并硬删。计数 = 铸出的这类腿条数,detail = `mode=<词> released=<类…>`。";
31
+ };
28
32
  readonly "server.workflow-park-index.sync-failed": {
29
33
  readonly cls: "P-DEBT";
30
34
  readonly note: "S-185 车CM(codex r1-F2 [high],验真后登记):workflow 出身 park 的 join 索引(`workflow_agent_session` / local 账本)在**一次成功的 run 写之后**同步失败 ⇒ 该次同步被吞,重试要等这条 run 的**下一次**写。而 park 恰好常常是这条 run 的**最后**一次写(park = run 终局 `failed`)⇒ 那条 park 可能永久 join 不到,`/decide` 结构性退回本批之前的行为(409 `conflict.resume_context_unavailable`)——**方向是 fail-closed**(不误配、不伪造、不放行任何东西),丢的是「人能不能把这条批准送出去」这个能力。归 P 类的理由是它决定**车道选择**(缺行 ⇒ 选中 legacy 腿),归 DEBT 的理由是真正的解要一条**按子代 checkpoint 键的耐久决议回执 + 重启期回填**(与 codex r1-F3 同一件欠账,另车)。计数非零 = 有 park 因此决不掉,运维要按 detail 里的 runId 去查。";
@@ -259,7 +263,11 @@ export declare const FAIL_OPEN_TAGS: {
259
263
  };
260
264
  readonly "server.run-terminal.redact-threw": {
261
265
  readonly cls: "F";
262
- readonly note: "三个发射座(errorMessage 终态文案 / 诊断文本 diag / 工件 artifact,probe 前缀判别;普查 56 行;补丁头 patch-head 那座随 7.83.0 / S-113 的 redactPatchHead 一并退役):S-105 最终轮 [medium]:终态 errorMessage/blockedReason/remoteEnvFailures[].message 脱敏抛出(RangeError:关键字/前缀臂那一形已由 S-219 的 atLeast 铸点闭掉,仍能抛的是 JSON 秘密字段臂的转义对循环,既有未闭,S-106)⇒ 落固定占位 «redacted:oversized»——绝不落原文(安全轴 fail-closed),也绝不阻断终态翻转与 claim 释放(可用性)。方向刻意:一条超大错误体不得把 run 卡在 running。";
266
+ readonly note: "三个发射座(errorMessage 终态文案 / 诊断文本 diag / 工件 artifact,probe 前缀判别;普查 56 行;补丁头 patch-head 那座随 7.83.0 / S-113 的 redactPatchHead 一并退役):S-105 最终轮 [medium]:终态 errorMessage/blockedReason/remoteEnvFailures[].message 脱敏抛出(RangeError:关键字/前缀臂那一形由 S-219 的 atLeast 铸点闭掉;JSON 秘密字段臂的转义对循环与 PEM 栅栏臂的标签交替循环由 S-265 换成手写线性扫描器闭掉 —— 已知形全部闭掉,本臂留作结构性兜底,计数恒零是证据、不归零即新形)⇒ 落固定占位 «redacted:oversized»——绝不落原文(安全轴 fail-closed),也绝不阻断终态翻转与 claim 释放(可用性)。方向刻意:一条超大错误体不得把 run 卡在 running。";
267
+ };
268
+ readonly "server.run-terminal.provider-echo-shaped": {
269
+ readonly cls: "F";
270
+ readonly note: "S-283(7.105.0,`observability/run-terminal.ts` 的 `admitProviderEcho`,唯一判决口;S-889 起动作与 core #1204 源头闸同一条;S-877 / core 7.35.0 起谓词读 core 包根 `isProviderEchoShape`,core brain 写侧先判(越界 ⇒ trace `brain.echo_dropped`)⇒ 真 brain 链上本 tag 零命中,命中只剩存量行与不经 core brain 写侧的 producer):模型服务商回声、引擎逐字转交的不透明 id(`apiFailure.requestId` / `responseModel`,以及账本孪生 `turn_end.responseModel`、子代通告 `task_notification.apiFailure.requestId` 与 `POST /v1/side-query` 200 体的 `responseModel`;铸点 + 账本汇点两口)越出形状(字符串、长度 1–128、每个码元是可见 ASCII 0x21–0x7E;非串值在外)⇒ **缺席**(键删掉 / 不铸)+ 计数;空串 = 服务商什么都没说 ⇒ 缺席、**不计数**(S-877,core `providerEchoReading` 的读法)。方向是 **fail-closed**(越界字节不上 wire / 不落库 / 不进日志),不截断、不过脱敏器(不透明 id,脱敏会改坏它 —— 与 S-270 structuredOutput 同方向)。放行的最坏后果 = 这枚 id / 模型名在 wire 上缺席(`apiFailure` 留 `{}` = 「provider 故障、无可补充标签」,core 契约原句),运维拿不到它去服务商那边对单;detail 只带座位 / 原长或类型,**不带值**。计数非零 = 这台部署接的端点(或中间网关)在回显越界字节,运维按座位去查那一跳;读面不改写存量行,存量越界行每次回放各计一次。";
263
271
  };
264
272
  readonly "server.run-terminal.redact-capped": {
265
273
  readonly cls: "F";
@@ -16,6 +16,10 @@ export const FAIL_OPEN_TAGS = {
16
16
  cls: "F",
17
17
  note: "S-297(`http/routes/sessions.ts lastLegMcpFor`,`/mcp` 面板的可选键 `lastLegMcp`):按会话取最近 run 的索引读 / 那条 run 的账本读**抛错或超期** ⇒ 该键缺席,面板本体照常 200。放行的最坏后果=这一屏少一格「上一次真挂上了什么」(壳退回翻账本),不是门、不是权限、不是凭证 —— 而反方向(让附加键的读失败打掉整张面板)会给一口此前零 MCP 配置下根本不碰账本的**既有**读面新加一条挂掉的理由。缺席本就是本键 wire 上的一等状态(消费端必须会读),所以这条兜底不产生任何新的消费端形态;计数让「某台部署的账本一直读不出来」显形。",
18
18
  },
19
+ "server.unattended-leg.mode-releases-protective-asks": {
20
+ cls: "P-DEBT",
21
+ note: "S-877 四续 ⑧(候裁 18 D 的已披露残余,`unattended-permission-mode.ts` 的 `createUnattendedLeg`):无人值守腿(leader worker / 修复腿 / run-local / 续跑重挂)的旋钮 `UNATTENDED_PERMISSION_MODE` 缺省 `bypassPermissions` 释放六类保护问(`probe` / `tool_marks_mcp` / `tool_marks_a2a` / `transcript_write` / `rule_unreadable` / `artifact_shared_read`;写保护一类已改拒)—— 7.105.0 这些腿上它们照问、无人答即拒。放行的最坏后果=无人值守的写 / 读 / MCP·A2A 标记工具调用不再被这几道保护问拦下,`rule_unreadable` 是「规则店读不出照放」(安全轴 fail-open 方向)。core 对这几类没有拒座;S-906(core 7.37.0 按类作答)到货后本臂与旋钮一并硬删。计数 = 铸出的这类腿条数,detail = `mode=<词> released=<类…>`。",
22
+ },
19
23
  "server.workflow-park-index.sync-failed": {
20
24
  cls: "P-DEBT",
21
25
  note: "S-185 车CM(codex r1-F2 [high],验真后登记):workflow 出身 park 的 join 索引(`workflow_agent_session` / local 账本)在**一次成功的 run 写之后**同步失败 ⇒ 该次同步被吞,重试要等这条 run 的**下一次**写。而 park 恰好常常是这条 run 的**最后**一次写(park = run 终局 `failed`)⇒ 那条 park 可能永久 join 不到,`/decide` 结构性退回本批之前的行为(409 `conflict.resume_context_unavailable`)——**方向是 fail-closed**(不误配、不伪造、不放行任何东西),丢的是「人能不能把这条批准送出去」这个能力。归 P 类的理由是它决定**车道选择**(缺行 ⇒ 选中 legacy 腿),归 DEBT 的理由是真正的解要一条**按子代 checkpoint 键的耐久决议回执 + 重启期回填**(与 codex r1-F3 同一件欠账,另车)。计数非零 = 有 park 因此决不掉,运维要按 detail 里的 runId 去查。",
@@ -250,7 +254,11 @@ export const FAIL_OPEN_TAGS = {
250
254
  },
251
255
  "server.run-terminal.redact-threw": {
252
256
  cls: "F",
253
- note: "三个发射座(errorMessage 终态文案 / 诊断文本 diag / 工件 artifact,probe 前缀判别;普查 56 行;补丁头 patch-head 那座随 7.83.0 / S-113 的 redactPatchHead 一并退役):S-105 最终轮 [medium]:终态 errorMessage/blockedReason/remoteEnvFailures[].message 脱敏抛出(RangeError:关键字/前缀臂那一形已由 S-219 的 atLeast 铸点闭掉,仍能抛的是 JSON 秘密字段臂的转义对循环,既有未闭,S-106)⇒ 落固定占位 «redacted:oversized»——绝不落原文(安全轴 fail-closed),也绝不阻断终态翻转与 claim 释放(可用性)。方向刻意:一条超大错误体不得把 run 卡在 running。",
257
+ note: "三个发射座(errorMessage 终态文案 / 诊断文本 diag / 工件 artifact,probe 前缀判别;普查 56 行;补丁头 patch-head 那座随 7.83.0 / S-113 的 redactPatchHead 一并退役):S-105 最终轮 [medium]:终态 errorMessage/blockedReason/remoteEnvFailures[].message 脱敏抛出(RangeError:关键字/前缀臂那一形由 S-219 的 atLeast 铸点闭掉;JSON 秘密字段臂的转义对循环与 PEM 栅栏臂的标签交替循环由 S-265 换成手写线性扫描器闭掉 —— 已知形全部闭掉,本臂留作结构性兜底,计数恒零是证据、不归零即新形)⇒ 落固定占位 «redacted:oversized»——绝不落原文(安全轴 fail-closed),也绝不阻断终态翻转与 claim 释放(可用性)。方向刻意:一条超大错误体不得把 run 卡在 running。",
258
+ },
259
+ "server.run-terminal.provider-echo-shaped": {
260
+ cls: "F",
261
+ note: "S-283(7.105.0,`observability/run-terminal.ts` 的 `admitProviderEcho`,唯一判决口;S-889 起动作与 core #1204 源头闸同一条;S-877 / core 7.35.0 起谓词读 core 包根 `isProviderEchoShape`,core brain 写侧先判(越界 ⇒ trace `brain.echo_dropped`)⇒ 真 brain 链上本 tag 零命中,命中只剩存量行与不经 core brain 写侧的 producer):模型服务商回声、引擎逐字转交的不透明 id(`apiFailure.requestId` / `responseModel`,以及账本孪生 `turn_end.responseModel`、子代通告 `task_notification.apiFailure.requestId` 与 `POST /v1/side-query` 200 体的 `responseModel`;铸点 + 账本汇点两口)越出形状(字符串、长度 1–128、每个码元是可见 ASCII 0x21–0x7E;非串值在外)⇒ **缺席**(键删掉 / 不铸)+ 计数;空串 = 服务商什么都没说 ⇒ 缺席、**不计数**(S-877,core `providerEchoReading` 的读法)。方向是 **fail-closed**(越界字节不上 wire / 不落库 / 不进日志),不截断、不过脱敏器(不透明 id,脱敏会改坏它 —— 与 S-270 structuredOutput 同方向)。放行的最坏后果 = 这枚 id / 模型名在 wire 上缺席(`apiFailure` 留 `{}` = 「provider 故障、无可补充标签」,core 契约原句),运维拿不到它去服务商那边对单;detail 只带座位 / 原长或类型,**不带值**。计数非零 = 这台部署接的端点(或中间网关)在回显越界字节,运维按座位去查那一跳;读面不改写存量行,存量越界行每次回放各计一次。",
254
262
  },
255
263
  "server.run-terminal.redact-capped": { cls: "F", note: "S-107:终态自由文本(errorMessage/blockedReason/remoteEnvFailures[].message/error 列/done·failed 行)长度 > REDACT_INPUT_CAP(64KB)⇒ 整段换 «redacted:oversized»(fail-closed 取证丢弃,不截断不扫);计数=多少条终态文案被整段丢弃" },
256
264
  "server.run-terminal.artifact-capped": { cls: "F", note: "S-107:leader_run 行上的工件字节(salvaged[].patch / candidatePatch.patch / merge.candidatePatch / merge.rej)长度 > ARTIFACT_INPUT_CAP(256KB)⇒ 整份换 «redacted:oversized-artifact»(fail-closed 取证丢弃;原字节仍在 worker 分支/merge 咽喉);计数=多少份工件被整份丢弃" },
@@ -215,6 +215,7 @@ export function createMetrics() {
215
215
  m.counter("http_requests_total", "HTTP requests by route/method/status");
216
216
  m.histogram("http_request_duration_seconds", "HTTP request duration");
217
217
  m.counter("rate_limited_total", "Requests rejected by the rate limiter");
218
+ m.counter("brain_echo_dropped_total", "Provider echo fields (response request-id header / reported model name) the engine's brain dropped at write time for failing the echo shape (visible ASCII, 1-128 chars), by field (requestId|responseModel); non-zero = the endpoint or a gateway in front of it echoes out-of-shape bytes");
218
219
  m.counter("mcp_probe_total", `No-run MCP status face outcomes by outcome (${MCP_PROBE_OUTCOMES.join("/")} — see src/capabilities/mcp-probe.ts MCP_PROBE_OUTCOMES; rate_limited has TWO causes: the per-identity fixed window AND the replica-wide in-flight cap, which can refuse caller B because of caller A)`);
219
220
  m.counter("tasks_total", "Finished tasks by terminal status");
220
221
  m.counter("task_tokens_total", "Total model tokens consumed by tasks");
@@ -241,7 +242,7 @@ export function createMetrics() {
241
242
  m.counter("task_model_not_allowlisted_resume_total", "Resumed legs whose model/compactionModel is outside the @-mention allowlist (allowed to keep running by design — the allowlist gates fresh picks only)");
242
243
  m.counter("task_model_unavailable_fallback_total", "Resumed legs whose explicit model pick names a catalog entry that is unavailable on this deployment (no brain mounted for its provider / entry fails its schema) — the leg runs on the default model; fresh submits get 400 request.model_unavailable");
243
244
  m.counter("prompt_constitution_total", "Prompt assembly constitution mode per task (core 1.243 提示词主权): steady state = core; provider-assembled/replaced/legacy warrant eyes");
244
- m.counter("rewind_snapshot_failed_total", "Per-turn rewind file snapshots that failed, by the engine's FileHistoryResult.error.code (snapshot_failed/aborted/…; `unknown` = the disclosure text carried no readable code). COARSE on purpose — whether a failure is permanent (the same file will fail every turn until the deployment changes) is counted by file_history_permanent_failure_total instead, from the typed error object rather than from prose");
245
+ m.counter("rewind_snapshot_failed_total", "Per-turn rewind file snapshots that failed, by the engine's FileHistoryResult.error.code (snapshot_failed/aborted/…; `unknown` = the text carried no word of that closed set, e.g. the boundary-timeout arm). COARSE on purpose — whether a failure is permanent (the same file will fail every turn until the deployment changes) is counted by file_history_permanent_failure_total instead, from the typed error object rather than from prose");
245
246
  m.counter("verifications_total", "Tasks run through the verification gate (1.44), by final verdict");
246
247
  m.counter("cascade_total", "Tasks run through the model cascade (1.45), by outcome (passed/exhausted)");
247
248
  m.counter("degenerate_total", "Degenerate-output failures (1.59), by salvage class (a/b/unknown)");
@@ -296,6 +297,7 @@ export function createMetrics() {
296
297
  m.counter("fleet_lease_exhausted_total", "Fleet trace-lease requests rejected because the pool was exhausted");
297
298
  m.counter("fleet_lease_auth_error_total", "Fleet trace-lease auth failures");
298
299
  m.counter("fleet_lease_fail_open_total", "Fleet trace-lease fail-open grants (lease store unavailable)");
300
+ m.counter("fleet_lease_fail_closed_total", "Fleet quota-lease refusals because the center was unreachable / 5xx / timed out / answered malformed while FLEET_LEASE_FAIL_CLOSED is in effect (billed submissions answered 503 fleet.lease_unavailable)");
299
301
  m.counter("metric_label_overflow_total", "Label values collapsed to 'other' by the cardinality guard, by label");
300
302
  m.counter("remote_env_exec_idle_timeout_total", "Remote-env exec idle timeouts, by provider");
301
303
  m.counter("remote_env_file_timeout_total", "Remote-env file-op timeouts, by provider");
@@ -4,7 +4,7 @@ export declare const PERMISSION_RULE_EVENT_TABLE: readonly [{
4
4
  readonly meaning: "a persisted allow rule authorized the call and nobody was asked";
5
5
  }, {
6
6
  readonly event: "rule_store_unreadable";
7
- readonly meaning: "the rule store failed to read, so the call was adjudicated with ZERO rules (expect an approval storm whose cause is the store, not the model)";
7
+ readonly meaning: "the rule store failed to read, so the failed partition's rows were SKIPPED for that read (core C-R172: no store card; its allow rows clear nothing and its deny rows protect nothing until a read succeeds — expect extra cards and missing refusals whose cause is the store, not the model)";
8
8
  }, {
9
9
  readonly event: "read_only_allowed";
10
10
  readonly meaning: "the engine's read-only shell verdict cleared an ask that was NOT an operator-written rule (core decisionReason \"read_only\"; an operator-declared ask carries matchedAskRule and is never cleared this way)";
@@ -6,7 +6,7 @@ export const PERMISSION_RULE_EVENT_TABLE = [
6
6
  },
7
7
  {
8
8
  event: "rule_store_unreadable",
9
- meaning: "the rule store failed to read, so the call was adjudicated with ZERO rules (expect an approval storm whose cause is the store, not the model)",
9
+ meaning: "the rule store failed to read, so the failed partition's rows were SKIPPED for that read (core C-R172: no store card; its allow rows clear nothing and its deny rows protect nothing until a read succeeds — expect extra cards and missing refusals whose cause is the store, not the model)",
10
10
  },
11
11
  {
12
12
  event: "read_only_allowed",