@sema-agent/server 7.105.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 (174) hide show
  1. package/MIGRATION.md +82 -0
  2. package/USAGE.md +134 -42
  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 +3 -9
  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 +35 -0
  75. package/dist/config.js +69 -16
  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/fleet-client.d.ts +18 -13
  82. package/dist/fleet-client.js +32 -22
  83. package/dist/fleet-lease.d.ts +72 -47
  84. package/dist/fleet-lease.js +66 -14
  85. package/dist/governance-http-status.d.ts +0 -1
  86. package/dist/governance-http-status.js +0 -1
  87. package/dist/hosted-posture.d.ts +6 -17
  88. package/dist/hosted-posture.js +11 -6
  89. package/dist/http/admission.js +14 -9
  90. package/dist/http/approval-sweeps.js +13 -1
  91. package/dist/http/dispatch.js +1 -1
  92. package/dist/http/durable-run-leg.js +2 -0
  93. package/dist/http/request-guards.d.ts +25 -0
  94. package/dist/http/request-guards.js +25 -5
  95. package/dist/http/resume-legs.js +19 -11
  96. package/dist/http/route-ctx.d.ts +9 -0
  97. package/dist/http/routes/approvals-assistant.js +0 -1
  98. package/dist/http/routes/capabilities.js +12 -11
  99. package/dist/http/routes/config-catalog.js +2 -1
  100. package/dist/http/routes/diagnostics.js +2 -0
  101. package/dist/http/routes/tasks.js +4 -1
  102. package/dist/http/server.d.ts +20 -4
  103. package/dist/http/session-inherit-env.d.ts +37 -0
  104. package/dist/http/session-inherit-env.js +6 -0
  105. package/dist/http/wire-types.d.ts +19 -28
  106. package/dist/index.d.ts +2 -1
  107. package/dist/index.js +2 -1
  108. package/dist/key-resolver.d.ts +74 -19
  109. package/dist/key-resolver.js +65 -7
  110. package/dist/leader/leader.d.ts +6 -0
  111. package/dist/leader/leader.js +2 -1
  112. package/dist/leader/repair-wire.js +2 -1
  113. package/dist/leader/wire.d.ts +9 -1
  114. package/dist/leader/wire.js +10 -6
  115. package/dist/observability/fail-open.d.ts +5 -1
  116. package/dist/observability/fail-open.js +5 -1
  117. package/dist/observability/metrics.js +2 -0
  118. package/dist/observability/permission-rule-events.d.ts +1 -1
  119. package/dist/observability/permission-rule-events.js +1 -1
  120. package/dist/observability/run-terminal.d.ts +38 -1
  121. package/dist/observability/run-terminal.js +2 -5
  122. package/dist/observability/secret-env-scrub.js +1 -1
  123. package/dist/observability/tool-trace.d.ts +2 -0
  124. package/dist/operator-ask.d.ts +29 -1
  125. package/dist/operator-ask.js +6 -0
  126. package/dist/parked-decide.d.ts +14 -1
  127. package/dist/parked-decide.js +8 -1
  128. package/dist/plugins/session-policy-store-sql.d.ts +38 -7
  129. package/dist/plugins/session-policy-store-sql.js +64 -22
  130. package/dist/plugins/sql-driver.d.ts +1 -1
  131. package/dist/plugins/store-backend.d.ts +12 -3
  132. package/dist/plugins/store-backend.js +16 -5
  133. package/dist/plugins/tidb-pool.js +1 -1
  134. package/dist/read-face-posture.d.ts +4 -4
  135. package/dist/request-key-closure.js +2 -2
  136. package/dist/request-value-closure.d.ts +17 -5
  137. package/dist/request-value-closure.js +12 -0
  138. package/dist/router/route-orchestration.d.ts +11 -0
  139. package/dist/router/route-orchestration.js +7 -6
  140. package/dist/run-local.js +4 -2
  141. package/dist/runs.d.ts +2 -1
  142. package/dist/runs.js +4 -4
  143. package/dist/runtime-caps-resolver.d.ts +4 -10
  144. package/dist/runtime-governance.d.ts +8 -11
  145. package/dist/runtime-governance.js +10 -41
  146. package/dist/sealed-key.d.ts +0 -40
  147. package/dist/sealed-key.js +0 -20
  148. package/dist/server-secret-env.d.ts +11 -12
  149. package/dist/session-policy-wire.d.ts +5 -3
  150. package/dist/session-titler.d.ts +11 -0
  151. package/dist/session-titler.js +6 -2
  152. package/dist/sql-engine-posture.d.ts +2 -2
  153. package/dist/store-posture.d.ts +53 -0
  154. package/dist/store-posture.js +32 -0
  155. package/dist/task-settings.d.ts +34 -45
  156. package/dist/task-settings.js +46 -25
  157. package/dist/tool-approval.d.ts +51 -22
  158. package/dist/tool-approval.js +44 -8
  159. package/dist/tool-declaration-http-status.d.ts +12 -0
  160. package/dist/tool-declaration-http-status.js +12 -0
  161. package/dist/trace/core-keyset-guard.d.ts +7 -7
  162. package/dist/trace/engine-notice-wire.d.ts +1 -1
  163. package/dist/trace/engine-notice-wire.js +5 -0
  164. package/dist/trace/project.d.ts +3 -2
  165. package/dist/trace/project.js +51 -3
  166. package/dist/trace/wire-projection-faces.d.ts +1 -1
  167. package/dist/trace/wire-projection-faces.js +1 -1
  168. package/dist/unattended-permission-mode.d.ts +110 -0
  169. package/dist/unattended-permission-mode.js +65 -0
  170. package/package.json +3 -3
  171. package/dist/boot/session-shell-gate-registry.d.ts +0 -39
  172. package/dist/boot/session-shell-gate-registry.js +0 -21
  173. package/dist/roster-bound-http-status.d.ts +0 -6
  174. package/dist/roster-bound-http-status.js +0 -6
@@ -150,12 +150,12 @@ export declare function autonomyOverrides(autonomy: Autonomy | undefined): Parti
150
150
  */
151
151
  export declare function governanceMandatesShellGateAlways(governance: {
152
152
  autonomy?: Autonomy;
153
- manualModeShellGate?: "always" | "classify";
153
+ manualModeShellGate?: ShellGateDoctrine;
154
154
  }): boolean;
155
155
  /** [ref] 归因的部署侧入参(两个治理旋钮;判据属主见 {@link governanceMandatesShellGateAlways})。 */
156
156
  export interface GovernancePosture {
157
157
  autonomy?: Autonomy;
158
- manualModeShellGate?: "always" | "classify";
158
+ manualModeShellGate?: ShellGateDoctrine;
159
159
  }
160
160
  /**
161
161
  * [ref]:推「这道门是运维治理层下的吗」。判据是**合取**,两半各管一件事:
@@ -172,8 +172,9 @@ export interface GovernancePosture {
172
172
  * ⚠️ 残留(双向失真的窄在场向 + 根治路径)与「只收窄」的那一个裁决读者,都登记在
173
173
  * {@link governanceMandatesShellGateAlways} 的顶注,别在这里复述。
174
174
  *
175
- * 🔴 `"classify"` 档**刻意不标**:那一档由 core 的分类器逐调用裁决,一次 shell ask 可能出自分类器(治理)
176
- * 也可能出自 `APPROVAL_REQUIRE` 这类别的门,行上无从分辨 —— 与活卡腿同一条「宁缺毋假」纪律。
175
+ * 🔴 **形状档的 ask 刻意不标**(S-877 起它不再是一个档位词:core 7.34.0 把形状档装在**每一种模式**的全权 shell 上,
176
+ * 行上的 `shellGateDoctrine` 只在运维地板 `"always"` 铸门时出现):形状档的一次 shell ask 出自引擎自己的判断,
177
+ * 也可能叠着 `APPROVAL_REQUIRE` 这类别的门,行上无从分辨 —— 与活卡腿同一条「宁缺毋假」纪律。
177
178
  *
178
179
  * 🔴 **单一属主,四个读者**(不在任何读面另写一份判据 —— 这一位恰恰是靠「行上取证 ∧ 部署姿态」的合取才不
179
180
  * 谎报的,两份必漂):409 体的待批门材料(`http/active-run-conflict.ts`)、两条 durable 读面
@@ -196,10 +197,6 @@ export declare function governanceOriginOf(gate: {
196
197
  shellGateDoctrine?: ShellGateDoctrine;
197
198
  };
198
199
  } | undefined, governance: GovernancePosture | undefined): true | undefined;
199
- /** spec 序里的秩(0 起,越大越严)。入参收 `string`(fail-loud 边界:schema `trustRank` 同款姿势)——词表外 ⇒
200
- * RangeError,绝不 undefined/NaN 让比较静默成 false。导出只为对表钉(test/safety-merge-spec-consumption.test.ts,
201
- * [ref] 件4)——运行时消费仍走上表(键类型闭集)。 */
202
- export declare function shellGateRank(word: string): number;
203
200
  /**
204
201
  * Apply the operator's runtime governance (autonomy + commandPolicy + manualModeShellGate) onto a base
205
202
  * `TaskSpec`, TIGHTEN-ONLY, in a SINGLE {@link tightenTaskSpec} call: commandPolicy compiles to a `toolPolicy`
@@ -212,8 +209,8 @@ export declare function shellGateRank(word: string): number;
212
209
  * (task-settings deriveSettingsPolicy 的 default/auto/acceptEdits 三臂)让一个**部署级 tighten-only 旋钮
213
210
  * 的生死由客户端表态在场性决定**:body 无 settings/permissionMode(cli print/headless 腿按设计不 stamp)
214
211
  * ⇒ 旋钮静默失效(clay 实机 Monitor 案「门根本没铸」);bypassPermissions 一句话掀掉部署闸;非 host lane
215
- * fsWriteGate wiring 不建 ⇒ 沙箱 lane 全失效。governance 层与 autonomy 同拍施加后三者全闭。rank 合成:
216
- * autonomy 派生值与旋钮取大(ask→"always" 压过 "classify"),再对 base 只升不降(防 tightenTaskSpec throw)。
212
+ * fsWriteGate wiring 不建 ⇒ 沙箱 lane 全失效。governance 层与 autonomy 同拍施加后三者全闭。S-877 起地板只有
213
+ * `"always"` 一个词,合成退化成在场性(两个来源同词),不再有秩比较。
217
214
  *
218
215
  * [ref]([ref] issue #29,**邻仓 test AI 的 46 格真机矩阵**实证——不是本仓的钉,别到本仓文件里数 46;
219
216
  * 本仓的类级门是 test/operator-knob-client-posture-matrix.test.ts 的 旋钮×表态×lane 矩阵):
@@ -231,7 +228,7 @@ export declare function shellGateRank(word: string): number;
231
228
  export declare function applyRuntimeGovernance(base: TaskSpec, governance: {
232
229
  autonomy?: Autonomy;
233
230
  commandPolicy?: CommandRule[];
234
- manualModeShellGate?: "always" | "classify";
231
+ manualModeShellGate?: ShellGateDoctrine;
235
232
  sensitivePathPolicy?: ToolPolicy;
236
233
  /** [ref]/[ref] `governanceForced` 的标记表(缺省 = **当前 run 腿的 ALS 表**,见
237
234
  * `governance-ask-marks.ts` 顶注「作用域」段)。注入口只为测试与将来的多实例形。 */
@@ -1,7 +1,7 @@
1
- import { combinePolicies, COARSE_SHELL_TOOLS as CORE_COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, DEFAULT_SUBAGENT_TOOL_NAME, parseLeadingCommandName, tightenTaskSpec, } from "@sema-agent/core";
2
- import { GovernanceConfig, SAFETY_MERGE_SPEC } from "@sema-agent/settings-schema";
1
+ import { combinePolicies, COARSE_SHELL_TOOLS as CORE_COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, DEFAULT_SUBAGENT_TOOL_NAME, parseLeadingCommandName, tightenTaskSpec, wrapToolPolicy, } from "@sema-agent/core";
2
+ import { GovernanceConfig } from "@sema-agent/settings-schema";
3
3
  import { currentGovernanceAskMarks } from "./governance-ask-marks.js";
4
- import { operatorAsk } from "./operator-ask.js";
4
+ import { operatorAsk, withDeploymentAuthorship } from "./operator-ask.js";
5
5
  const COMMAND_RULE_SHAPE = GovernanceConfig.shape.commandPolicy.unwrap().element.shape;
6
6
  const COMMAND_DECISIONS = COMMAND_RULE_SHAPE.decision.options;
7
7
  export const AUTONOMY_MODES = GovernanceConfig.shape.autonomy.unwrap().options;
@@ -124,14 +124,12 @@ export function governanceOriginOf(gate, governance) {
124
124
  function createGovernanceAskMarkingPolicy(inner, marksOf) {
125
125
  const note = (d, req) => {
126
126
  marksOf()?.setMarked(req.toolCallId, d.action === "ask");
127
- return d;
128
- };
129
- return {
130
- check(req, signal) {
131
- const decision = inner.check(req, signal);
132
- return decision instanceof Promise ? decision.then((d) => note(d, req)) : note(decision, req);
133
- },
127
+ return d.action === "ask" ? withDeploymentAuthorship(d) : d;
134
128
  };
129
+ return wrapToolPolicy(inner, (req, signal) => {
130
+ const decision = inner.check(req, signal);
131
+ return decision instanceof Promise ? decision.then((d) => note(d, req)) : note(decision, req);
132
+ });
135
133
  }
136
134
  function createGovernanceShellGateMarkPolicy(marksOf, tools = COARSE_SHELL_TOOLS) {
137
135
  const shellTools = new Set(tools);
@@ -143,28 +141,6 @@ function createGovernanceShellGateMarkPolicy(marksOf, tools = COARSE_SHELL_TOOLS
143
141
  },
144
142
  };
145
143
  }
146
- const SHELL_GATE_RANK = (() => {
147
- const tighten = SAFETY_MERGE_SPEC.shellGate.tighten;
148
- const rule = tighten.rule;
149
- if (tighten.rule !== "enumRank")
150
- throw new Error(`SAFETY_MERGE_SPEC.shellGate.tighten.rule=${rule} — 不是 enumRank,shellGate 秩无从派生`);
151
- const table = { off: -1, classify: -1, always: -1 };
152
- for (const [i, w] of tighten.order.entries()) {
153
- if (!(w in table))
154
- throw new Error(`SAFETY_MERGE_SPEC.shellGate 序里有本仓词表外的词:${w}`);
155
- table[w] = i;
156
- }
157
- for (const [w, r] of Object.entries(table))
158
- if (r < 0)
159
- throw new Error(`SAFETY_MERGE_SPEC.shellGate 序缺词:${w}`);
160
- return table;
161
- })();
162
- export function shellGateRank(word) {
163
- const r = SHELL_GATE_RANK[word];
164
- if (r === undefined)
165
- throw new RangeError(`unknown shellGate word: ${word}`);
166
- return r;
167
- }
168
144
  export function applyRuntimeGovernance(base, governance) {
169
145
  const marksOf = () => governance.askMarks ?? currentGovernanceAskMarks();
170
146
  const overrides = { ...autonomyOverrides(governance.autonomy) };
@@ -175,15 +151,8 @@ export function applyRuntimeGovernance(base, governance) {
175
151
  : undefined;
176
152
  if (governancePolicy !== undefined)
177
153
  overrides.toolPolicy = governancePolicy;
178
- if (governance.manualModeShellGate !== undefined) {
179
- const candidate = overrides.shellGate !== undefined && SHELL_GATE_RANK[overrides.shellGate] >= SHELL_GATE_RANK[governance.manualModeShellGate]
180
- ? overrides.shellGate
181
- : governance.manualModeShellGate;
182
- if (SHELL_GATE_RANK[candidate] >= SHELL_GATE_RANK[base.shellGate ?? "off"])
183
- overrides.shellGate = candidate;
184
- else
185
- delete overrides.shellGate;
186
- }
154
+ if (governance.manualModeShellGate === "always")
155
+ overrides.shellGate = "always";
187
156
  if (overrides.shellGate === "always") {
188
157
  const shellObserver = createGovernanceShellGateMarkPolicy(marksOf);
189
158
  overrides.toolPolicy = governancePolicy === undefined ? shellObserver : combinePolicies(governancePolicy, shellObserver);
@@ -50,46 +50,6 @@ export interface SealedKeyCapability {
50
50
  * omits the key entirely (present-iff this process built its custody store — see the route for the two meanings
51
51
  * of absence). */
52
52
  export declare function projectSealedKeyCapability(face: SealedKeyPublicFace | undefined): SealedKeyCapability | undefined;
53
- /** Why a model's sealed key could not be unsealed (the poison-pill diagnosis, see SealedKeyPoison). CUSTODY failures only:
54
- * a sealedApiKey that is malformed (alg other than the pinned v1 SEALED_KEY_ALG, non-base64 ciphertext) makes the whole
55
- * catalog entry unavailable instead (S-704 `config.model_entry_invalid`, settings-schema `SealedApiKey` is the shape
56
- * owner) — the former `unsupported_alg` reason was retired with that move (it could no longer be reached). */
57
- export type SealedKeyPoisonReason = "sealed_decrypt_failed" | "stale_ciphertext" | "no_key_store";
58
- /**
59
- * Poison-pill entry for the per-model key map (`config.modelApiKeys`): planted by applyEffective when
60
- * a model CARRIES `sealedApiKey` but the ciphertext cannot be unsealed. We deliberately do NOT drop
61
- * the model from the map — dropping made "sealed key broken" indistinguishable from "no per-model key
62
- * configured", and when the deployment also has a gateway MODEL_API_KEY the model's tasks silently
63
- * fell back onto the shared account (the exact hole the no-apiKeyEnv-fallback contract exists to
64
- * close). createKeyResolver turns this marker into a task-level `SealedKeyPoisonedError` — never
65
- * `undefined`, because undefined IS the "use the gateway key" semantic. Carries DIAGNOSIS only:
66
- * never the ciphertext, never any plaintext.
67
- */
68
- export interface SealedKeyPoison {
69
- readonly sealedKeyPoison: true;
70
- readonly publicKeyId: string;
71
- readonly reason: SealedKeyPoisonReason;
72
- /** Error CLASS of the unseal failure (e.g. "Error") — never the message (message text is logged
73
- * at plant time; keeping the object minimal keeps every serialization face trivially secret-free). */
74
- readonly errKind?: string;
75
- /** ISO timestamp of the apply that detected the failure. */
76
- readonly at: string;
77
- }
78
- export declare function sealedKeyPoison(publicKeyId: string, reason: SealedKeyPoisonReason, errKind?: string): SealedKeyPoison;
79
- export declare function isSealedKeyPoison(v: unknown): v is SealedKeyPoison;
80
- /**
81
- * The task-level fail-loud face of a poison pill: thrown by the key resolver on EVERY brain call that
82
- * would otherwise authenticate this model, so a broken sealed key fails the task visibly (same family
83
- * as a rotation-leg auth failure) instead of silently burning the gateway account. Message carries the
84
- * sealed-failure marker (reason + publicKeyId + detection time) plus the operator fix — and nothing
85
- * secret (no ciphertext, no plaintext, by construction of SealedKeyPoison).
86
- */
87
- export declare class SealedKeyPoisonedError extends Error {
88
- readonly publicKeyId: string;
89
- readonly reason: SealedKeyPoisonReason;
90
- readonly at: string;
91
- constructor(modelName: string, poison: SealedKeyPoison);
92
- }
93
53
  /** Default id = fingerprint of the RAW public-key bytes: `exec-<sha256(pk)[0..12) hex>`.
94
54
  * Deterministic → regenerating the id from the same key file is stable across boots. */
95
55
  export declare function deriveKeyId(publicKeyRaw: Uint8Array): string;
@@ -17,26 +17,6 @@ export function projectSealedKeyCapability(face) {
17
17
  return undefined;
18
18
  return { alg: SEALED_KEY_ALG, publicKeyId: face.latest.publicKeyId, publicKey: face.latest.publicKey };
19
19
  }
20
- export function sealedKeyPoison(publicKeyId, reason, errKind) {
21
- return { sealedKeyPoison: true, publicKeyId, reason, ...(errKind ? { errKind } : {}), at: new Date().toISOString() };
22
- }
23
- export function isSealedKeyPoison(v) {
24
- return typeof v === "object" && v !== null && v.sealedKeyPoison === true;
25
- }
26
- export class SealedKeyPoisonedError extends Error {
27
- publicKeyId;
28
- reason;
29
- at;
30
- constructor(modelName, poison) {
31
- super(`sealed api key for model "${modelName}" is unusable (${poison.reason}; publicKeyId "${poison.publicKeyId}"; detected ${poison.at}) — ` +
32
- `refusing the gateway-key fallback. Fix custody: re-paste the key in the web UI (it seals against the newest registered public key), ` +
33
- `or restore this host's private key for that id / complete the rotation so the ciphertext targets a held key.`);
34
- this.name = "SealedKeyPoisonedError";
35
- this.publicKeyId = poison.publicKeyId;
36
- this.reason = poison.reason;
37
- this.at = poison.at;
38
- }
39
- }
40
20
  export function deriveKeyId(publicKeyRaw) {
41
21
  return `exec-${createHash("sha256").update(publicKeyRaw).digest("hex").slice(0, 12)}`;
42
22
  }
@@ -69,9 +69,9 @@ export declare const CATALOG_SECRET_ENV_NAMES: ReadonlySet<string>;
69
69
  * 目录臂的计数恰好就是 core 形状规则的缺口读数;名单型旋钮臂只记前两臂都漏下的(今天 = 不在服务目录里的 `BAKE_ENV_ALLOW`)。
70
70
  * 各腿不直接调它 —— 经 {@link hostSubprocessEnv}(剥 + 放回 + 留痕一只口)。
71
71
  *
72
- * **只为宿主子进程组合**:子进程与本进程同平台 ⇒ core `scrubSecretEnv` 的平台尾参(core 7.33.6 #1159:git 的 env 配置记录按子进程的平台读 ——
73
- * POSIX 只认大写名,win32 大小写折叠)取缺省 `process.platform` 即对。本包跑在别的平台上的子进程(ssh / k8s / adb / e2b / docker 车道)的 env
74
- * 不经本函数:那些车道不继承本进程 env,只交调用方显式给的 env 与部署方点名的键(`E2B_SANDBOX_ENV` / `DOCKER_SANDBOX_ENV`,经 {@link allowlistedEnv})。
72
+ * **只为宿主子进程组合**。本包跑在别的平台上的子进程(ssh / k8s / adb / e2b / docker 车道)的 env 不经本函数:那些车道不继承本进程 env,只交
73
+ * 调用方显式给的 env 与部署方点名的键(`E2B_SANDBOX_ENV` / `DOCKER_SANDBOX_ENV`,经 {@link allowlistedEnv})。(core 7.33.6 #1159 的平台尾参随
74
+ * core 7.35.0 #1201 删除 —— git 的 env 配置族成了豁免族,不再有「按子进程平台读记录」这件事,S-877。)
75
75
  */
76
76
  export declare function scrubServerSecrets(env: NodeJS.ProcessEnv, findings: ServerSecretEnvFinding[]): NodeJS.ProcessEnv;
77
77
  /**
@@ -87,15 +87,14 @@ export declare function scrubServerSecrets(env: NodeJS.ProcessEnv, findings: Ser
87
87
  * 返回 `NodeJS.ProcessEnv` 形(值可能是 `undefined` 的键本来就不会进子进程);调用方显式要交的键(`LC_ALL`、`cfg.env` …)叠在它之上。
88
88
  * 调用时机 = 每次要交 env 时现算一次(`process.env` 在本仓是 boot 期输入,各次同值;计数因此是「每组合一次」)。
89
89
  *
90
- * 🔴 **git 的 env 配置组(`GIT_CONFIG_COUNT` + `GIT_CONFIG_KEY_<i>` / `GIT_CONFIG_VALUE_<i>`)只有 core 一只写手**(7.103.1:车PG 退役本层,车PK 提 core 7.33.6):
91
- * 一条配置(KEY + VALUE)一次判定;COUNT 以内被扣的一条**原位**换占位(`GIT_CONFIG_KEY_<i>=sema.withheld` / `GIT_CONFIG_VALUE_<i>=""`,git 读成一条没人用的
92
- * 配置)、不重排、COUNT 原样(#1160);COUNT 之外 / 没有 COUNT 的被扣条目整条不交;COUNT 按 git 的 `strtoul` 读(#1162);记录按子进程的平台读(#1159,见
93
- * {@link scrubServerSecrets})。本层 7.103.0 的「整组扣」与 7.103.1 中间态的「按 pair 扣留 + 重编号」(D-7)都已退役,本层不重判(同一语义面上不留第二只写手)。
94
- * 本仓按名两臂(目录 secret 名 / 名单型旋钮)不含这一族的名字(钉:`test/secret-env-git-config-pair.test.ts`「一只写手」格)。
95
- * 点名放回按名叠在 scrub 结果之上 ⇒ 放回一条被扣配置要把 KEY 与 VALUE 两只一并点名(只点一只 = 与另一只的占位半条配成一对)。
96
- * 🔴 **披露谓词 = 「这个名字没有按原样交给子进程」**(输入值 vs 输出值;core 7.33.6 pickup 注:「not handed on as written」)。被扣的记录成员在输出里
97
- * **仍在**(装着占位半条)⇒「组合结果里有没有这个名字」恒为有;点名放回的名字取回源值 ⇒ 与输入同值 ⇒ 不是剥除;占位半条恰与输入同值的成员(KEY 里
98
- * 带凭据、VALUE 本就是空串)子进程拿到的正是原值 ⇒ 也不披露。一只谓词盖住「剥掉」「换占位」「点名放回」三形,不按形分臂。
90
+ * 🔴 **git 的 env 配置族与代理变量一族只有 core 一只写手**:core 7.35.0 #1201(S-877,CC 2.1.280 形)起它们是 core 的**豁免族** ——
91
+ * `GIT_CONFIG_COUNT` / `GIT_CONFIG_PARAMETERS` / `GIT_CONFIG_KEY_<数字>` / `GIT_CONFIG_VALUE_<数字>`(大写)与 `http(s)_proxy` / `all_proxy` / `no_proxy` /
92
+ * `npm_config_*proxy` …(任意大小写),名与值都不读、**原样交出,凭据在内**(7.33.2–7.33.8 的逐条扣 / 原位占位 `sema.withheld` / 整只扣全部退役;
93
+ * 部署面:凭据不放本进程 env —— `docs/DEPLOY-PREREQS.md`)。本层 7.103.0 的「整组扣」与 7.103.1 中间态的「按 pair 扣留 + 重编号」(D-7)更早退役,
94
+ * 本层不重判(同一语义面上不留第二只写手;在下游把 core 的豁免改回扣 = 源头修复宪法禁止的改写)。本仓按名两臂(目录 secret 名 / 名单型旋钮)不含
95
+ * `GIT_CONFIG_` 这一族的名字(钉:`test/secret-env-git-config-pair.test.ts`「一只写手」格)。
96
+ * 🔴 **披露谓词 = 「这个名字没有按原样交给子进程」**(输入值 vs 输出值)。core 对一个变量的动作恒为「整只删」(从不改写值,#1201),点名放回的名字
97
+ * 取回源值 ⇒ 与输入同值 ⇒ 不是剥除。一只谓词盖住「剥掉」「点名放回」两形,不按形分臂(7.33.6–7.33.8 还有第三形「换占位」,随 #1201 消失)。
99
98
  */
100
99
  export declare function hostSubprocessEnv(o: {
101
100
  site: SecretEnvScrubSite;
@@ -61,15 +61,17 @@ export type ToolNameRuleField = {
61
61
  * ⚠️ **比准备阶段严一格**(方向安全,与 client-core 0.83.6 的 KL-157 同向):准备阶段对**本 run 名册里挂着**的
62
62
  * 同名工具放行(部署自己挂了一只叫 `Task` 的工具就不审计它),写口看不到将来哪一跑的名册 ⇒ 一律拒。
63
63
  * 受伤的是「部署自带与退役名同名的工具、又要在会话规则里点它」的形;出路 = 部署级工具策略(不经会话规则店)。
64
- * ⚠️ **过渡形**:core 已表态(#1156,7.34.1)把这两臂抽成一只导出纯函数并在会话规则店写口即拒 —— 到货后本函数
65
- * 换成调 core 那只(拒因原样透传),三份判定收成一份。
64
+ * ⚠️ **过渡形**:core 已表态(#1156)把这两臂抽成一只导出纯函数并在会话规则店写口即拒 —— 到货后本函数
65
+ * 换成调 core 那只(拒因原样透传),三份判定收成一份。**S-877 提货复核(core 7.35.0)**:#1156 **未随版**(发车帖 [ref] 逐字
66
+ * 「照 #1156 未随本版」);同版新导出的 `rosterToolName` / `ruleNamesOf`(#1194)是**别名归一**(一个名字在名册上落到哪只行的线名 /
67
+ * 一次调用可被规则拼成的名字集),不回答「这个名字会不会让准备阶段拒启」,不能替本函数 ⇒ 本函数照留,等 #1156。
66
68
  *
67
69
  * 🔴 **为什么不复用 `config.ts` 的 `findUnmatchableToolNames`**(本仓另一只按名字判的单点;读过实现后裁):两只回答的
68
70
  * 不是同一个问题。那一只问「部署名单里这条名字会不会**永远匹配不上**活工具」—— 它刻意豁免 `allow`(白名单匹配不上 =
69
71
  * fail-closed 且当场可见),并且拒两段 MCP 名 `mcp__srv`(一名两义)与 core 文法编不出的串。本函数问的是「引擎见到这条
70
72
  * 会话规则会不会**拒启**」:引擎准备阶段的名审计对 `allow` 桶照样抛(豁免它 = 放过一条砖化路径),对 `mcp__srv` 却
71
73
  * **不**抛(会话层自 core 5.55 / 5.56 起把它当整对端覆盖形真命中)。判据只能逐字跟 core 那两臂走,不能借另一只的口径。
72
- * 两只在「退役名」「无前缀 `__` 名」两臂上同源(都读 core 导出的同两件),7.34.1 提货时一并换调 core 的导出函数。
74
+ * 两只在「退役名」「无前缀 `__` 名」两臂上同源(都读 core 导出的同两件),#1156 到货的那一批一并换调 core 的导出函数。
73
75
  */
74
76
  export type LegacyToolName = {
75
77
  why: "retired";
@@ -54,6 +54,17 @@ export interface SessionTitler {
54
54
  export declare const TITLE_MAX_CHARS = 80;
55
55
  /** Single line, control chars stripped, hard cap; surrounding quotes (a common LLM tic) trimmed. */
56
56
  export declare function sanitizeTitle(raw: string): string;
57
+ /**
58
+ * S-236 ②(7.106.0,cli L-687)—— objective 里**用户自己写的**那部分:剥掉全部 `<system-reminder>…</system-reminder>` 块(跨行、
59
+ * 多块、夹在正文中间都剥;**未闭合**的块剥到末尾 —— 被截断的附件同样不是用户写的)后 trim。
60
+ *
61
+ * 为什么:壳把附件(技能清单一类的 `<system-reminder>` 块)拼在正文**之前**提交,标题器取前 {@link OBJECTIVE_SNIPPET_CHARS} 字
62
+ * 去起名 ⇒ 标题成了技能清单。判据只认这一个**字面标签**(CC 的附件信封),不按内容猜「像不像用户写的」——
63
+ * 正文里提到这个词的普通文字不是块,原样保留。剥完是空串 ⇒ 调用方走「空 objective 不起名」的既有臂。
64
+ * ⚠️ 只治这一类:S-236 原件(fresh `/decide` 腿把指路文本当 objective 提交)是另一类非用户文本,没有信封可剥,本函数刻意不加
65
+ * 按内容猜的第二臂(要么 fresh 腿传席,要么另立一臂 —— 候裁,见车RA 收车档)。
66
+ */
67
+ export declare function userAuthoredObjectiveText(objective: string): string;
57
68
  export declare function buildTitlePrompt(objective: string): string;
58
69
  export declare function createSessionTitler(deps: SessionTitlerDeps): SessionTitler;
59
70
  //# sourceMappingURL=session-titler.d.ts.map
@@ -12,8 +12,12 @@ export function sanitizeTitle(raw) {
12
12
  const unquoted = oneLine.replace(/^["'「『]+/, "").replace(/["'」』]+$/, "").trim();
13
13
  return unquoted.length > TITLE_MAX_CHARS ? `${unquoted.slice(0, TITLE_MAX_CHARS - 1)}…` : unquoted;
14
14
  }
15
+ export function userAuthoredObjectiveText(objective) {
16
+ return objective.replace(/<system-reminder>[\s\S]*?(?:<\/system-reminder>|$)/g, "").trim();
17
+ }
15
18
  export function buildTitlePrompt(objective) {
16
- const snippet = objective.length > OBJECTIVE_SNIPPET_CHARS ? `${objective.slice(0, OBJECTIVE_SNIPPET_CHARS)}…` : objective;
19
+ const text = userAuthoredObjectiveText(objective);
20
+ const snippet = text.length > OBJECTIVE_SNIPPET_CHARS ? `${text.slice(0, OBJECTIVE_SNIPPET_CHARS)}…` : text;
17
21
  const fenced = snippet.replace(/<(\/?)request>/gi, "<\u200b$1request>");
18
22
  return ("Write a short title (at most 8 words) summarizing the user request below. " +
19
23
  "Reply with ONLY the title on a single line — no quotes, no preamble. " +
@@ -34,7 +38,7 @@ export function createSessionTitler(deps) {
34
38
  .catch(() => ({ state: continueWord, unreadable: true }));
35
39
  return {
36
40
  maybeTitle(sessionId, objective, model) {
37
- if (!sessionId || !objective.trim())
41
+ if (!sessionId || !userAuthoredObjectiveText(objective))
38
42
  return;
39
43
  if (seen.has(sessionId))
40
44
  return;
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * `sql-driver.ts` 的连接初始化在**每条池连接**上钉死这两件事并**回读复核**(`@contract txn.read-semantics`)。
13
13
  * 本模块把那份读数拿出来,一次推导两个读面:operator 面(`GET /v1/diagnostics/wiring` 的 `sqlEngine` 段,
14
- * 全量)与消费端面(`GET /v1/capabilities` 的 `sql` 位,窄投影)。两面同源 —— 「诊断页说悲观、能力位说
14
+ * 全量)与消费端面(`GET /v1/capabilities` 的 `store.sql` 位(7.106.0 起;此前顶层 `sql`,S-866 ①),窄投影)。两面同源 —— 「诊断页说悲观、能力位说
15
15
  * 乐观」在结构上不可能。
16
16
  *
17
17
  * ## 两条诚实纪律(与 memory-posture 同源)
@@ -27,7 +27,7 @@
27
27
  import { type SqlEngineFacts } from "./plugins/sql-driver.js";
28
28
  import type { Pool as MySqlPool } from "mysql2/promise";
29
29
  import type { Pool as PgPool } from "pg";
30
- /** 消费端窄投影(`GET /v1/capabilities` 的 `sql` 位)。`null` = 本部署没有 SQL 后端。 */
30
+ /** 消费端窄投影(`GET /v1/capabilities` 的 `store.sql` 位;7.106.0 前是顶层 `sql`,S-866 ①)。`null` = 驱动此刻给不出事实 ⇒ `store.sql` 缺席。 */
31
31
  export interface SqlEngineCapability {
32
32
  engine: SqlEngineFacts["engine"];
33
33
  isolation: string;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * S-866 ①(设计稿 v1.1 §2①)—— 「这台副本**真的在持久化吗**」的单一推导点:caps `store`、`/health.storeDegraded` / `durable:false`、
3
+ * gauge `store_backend_degraded` 三面读**同一只装配值**({@link StorePosture},`boot/stores.ts` 铸一次)。
4
+ *
5
+ * ## 病
6
+ * 7.105.0 及以前,店降级的痕迹是 gauge + `/health` 两位 + caps `sql:null` —— 而 `sql:null` 同时是「本来就没配 SQL」「local 文件店」
7
+ * 「降级到内存」「最近一次连接初始化被拒」四件事;调用方分不清「以为在持久化,其实在内存」这最坏的一形。
8
+ *
9
+ * ## 形
10
+ * `declared` = 运维的声明(`DB_BACKEND` 词,含缺省推导);`backend` = **装配出的**后端 kind(没装出后端 = `memory`);两者不等是合法的
11
+ * 形(`DB_BACKEND=pg` + `SESSION_BACKEND=memory` = 声明 pg、刻意不持久;auto 降级;缺省推导 local 起不来)—— 两位都上 wire,
12
+ * 由调用方自己读出落差。`degradedReason` present-iff 降级臂走过(按后端族定词,{@link storeDegradedReasonFor})。
13
+ * 顶层 `sql` 并入 `store.sql`(硬 breaking,零别名;对象逐字同形)。
14
+ */
15
+ import { type DbBackendWord } from "./config-types.js";
16
+ import type { StoreBackend } from "./plugins/store-backend.js";
17
+ import type { SqlEngineFacts } from "./plugins/sql-driver.js";
18
+ import { type SqlEngineCapability } from "./sql-engine-posture.js";
19
+ /**
20
+ * 降级词闭集(**按后端族**,不按失败发生在哪一步 —— SQL 池懒连,连接失败第一次就出现在 `ensureSchema` 里,与 DDL 失败同一个调用,
21
+ * 按调用位置分不出「连不上」与「建表失败」):
22
+ * · `db_unreachable` —— SQL 族(`mysql` / `pg`)在 `SESSION_BACKEND=auto` 下打不开(连接或建表);
23
+ * · `local_unwritable` —— **缺省推导**的 local 文件店构造期失败(mkdir / 数据根锁 / 权限 / 只读挂载 / 磁盘满)。
24
+ */
25
+ export declare const STORE_DEGRADED_REASONS: readonly ["db_unreachable", "local_unwritable"];
26
+ export type StoreDegradedReason = (typeof STORE_DEGRADED_REASONS)[number];
27
+ /** 降级臂按后端族定词(穷举 switch:词表加员 ⇒ 编译红)。`memory` 从不尝试打开后端,走不到降级臂 —— 到了就是装配缺陷,响亮抛。 */
28
+ export declare function storeDegradedReasonFor(declared: DbBackendWord): StoreDegradedReason;
29
+ /** 装配值(纯数据;`boot/stores.ts` 铸一次,gauge / `/health` / caps 三面同读)。 */
30
+ export interface StorePosture {
31
+ readonly declared: DbBackendWord;
32
+ readonly backend: DbBackendWord;
33
+ readonly degradedReason?: StoreDegradedReason;
34
+ }
35
+ /** `build*`(纯数据)。`assembled` = 装配出的 `StoreBackend.kind`;缺席 = 没装出后端 ⇒ `memory`。 */
36
+ export declare function buildStorePosture(a: {
37
+ readonly declared: DbBackendWord;
38
+ readonly assembled: StoreBackend["kind"] | undefined;
39
+ readonly degradedReason: StoreDegradedReason | undefined;
40
+ }): StorePosture;
41
+ /** caps `store` 的形。 */
42
+ export interface StoreCapability {
43
+ readonly declared: DbBackendWord;
44
+ readonly backend: DbBackendWord;
45
+ readonly durable: boolean;
46
+ readonly degradedReason?: StoreDegradedReason;
47
+ /** 7.105.0 及以前的顶层 `sql` 对象,原样搬入(`{engine, isolation, txnMode}`)。present-iff `backend ∈ {mysql, pg}` ∧ 驱动此刻给得出
48
+ * 回读复核过的事实 —— 最近一次连接初始化被拒(S-137 ④)时驱动交 `null`,本键随之缺席(此前是顶层 `sql:null`)。 */
49
+ readonly sql?: SqlEngineCapability;
50
+ }
51
+ /** 装配值 → caps 投影(纯函数)。 */
52
+ export declare function projectStoreCapability(posture: StorePosture, sqlFacts: SqlEngineFacts | null | undefined): StoreCapability;
53
+ //# sourceMappingURL=store-posture.d.ts.map
@@ -0,0 +1,32 @@
1
+ import {} from "./config-types.js";
2
+ import { projectSqlEngineCapability } from "./sql-engine-posture.js";
3
+ export const STORE_DEGRADED_REASONS = ["db_unreachable", "local_unwritable"];
4
+ export function storeDegradedReasonFor(declared) {
5
+ switch (declared) {
6
+ case "mysql":
7
+ case "pg":
8
+ return "db_unreachable";
9
+ case "local":
10
+ return "local_unwritable";
11
+ case "memory":
12
+ throw new Error("store posture: DB_BACKEND=memory never opens a backend, so it cannot degrade — this is a wiring defect");
13
+ default: {
14
+ const unhandled = declared;
15
+ throw new Error(`store posture: unknown DB_BACKEND word ${String(unhandled)}`);
16
+ }
17
+ }
18
+ }
19
+ export function buildStorePosture(a) {
20
+ return { declared: a.declared, backend: a.assembled ?? "memory", ...(a.degradedReason !== undefined ? { degradedReason: a.degradedReason } : {}) };
21
+ }
22
+ export function projectStoreCapability(posture, sqlFacts) {
23
+ const sql = posture.backend === "mysql" || posture.backend === "pg" ? projectSqlEngineCapability(sqlFacts) : null;
24
+ return {
25
+ declared: posture.declared,
26
+ backend: posture.backend,
27
+ durable: posture.backend !== "memory",
28
+ ...(posture.degradedReason !== undefined ? { degradedReason: posture.degradedReason } : {}),
29
+ ...(sql !== null ? { sql } : {}),
30
+ };
31
+ }
32
+ //# sourceMappingURL=store-posture.js.map
@@ -124,6 +124,12 @@ export declare function effectiveThinking(reasoningEffort: unknown, ultracode: b
124
124
  * field is the explicit per-turn intent → it WINS over a bundle defaultMode). Creates a minimal settings object when
125
125
  * no `body.settings` bundle was sent. The result flows through {@link applyTaskSettings} (tighten-only). */
126
126
  export declare function withPermissionMode(settings: ParsedTaskSettings | undefined, mode: SettingsPermissionMode): ParsedTaskSettings;
127
+ /**
128
+ * 7.106.0 合并复审 X8(opus O-6)—— 请求**没有表态**模式时引擎实际跑的那个词。core 7.34.0 [ref]「Absent = `default`」
129
+ * (装树 `dist/core/runner/prepare-task.js` 的 `requested ?? "default"`)。只给**观测面**用(hook 载荷的 `permission_mode`,CC 恒带这个键):
130
+ * spec 过境点照旧不写键(`effectivePermissionMode` 缺席 ⇒ 不替调用方选 —— 那是引擎的默认,不是本仓的表态)。
131
+ */
132
+ export declare const ENGINE_ABSENT_PERMISSION_MODE: "default";
127
133
  /**
128
134
  * [ref] §6-1 —— 本请求的**生效** permission mode,单点。优先序逐字同 {@link withPermissionMode}:
129
135
  * 顶层 `body.permissionMode`(本轮显式表态)赢过 bundle 的 `settings.permissions.defaultMode`;两处
@@ -137,51 +143,6 @@ export declare function withPermissionMode(settings: ParsedTaskSettings | undefi
137
143
  export declare function effectivePermissionMode(body: {
138
144
  permissionMode?: unknown;
139
145
  }, settings: ParsedTaskSettings | undefined): SettingsPermissionMode | undefined;
140
- /**
141
- * [ref] §2 —— 显式 permission mode → `TaskSpec.shellGate` 档位的**翻译表(单一真源)**。
142
- *
143
- * · `bypassPermissions` ⇒ `"off"` —— CC `--dangerously-skip-permissions` 的对位裁定
144
- * (与 fs-write 面的 bypass 臂两面归一,见 {@link deriveSettingsPolicy})。
145
- * · `auto` ⇒ `"classify"` —— **[ref] §8 开口按施工期新披露亲裁改译**(原裁定字面是 off,
146
- * §8 保留「core auto 分类器覆盖面有新披露时可独立再裁,表驱动一行」——新披露见下段):
147
- *
148
- * ⚠️ **`auto` 这一行有一条待裁的开口([ref] §8 明列「core auto 分类器覆盖面有新披露时可独立再裁,
149
- * 表驱动一行」)。施工期交叉复审给出了那条新披露,已亲读安装包核实**:
150
- * · core 只在**已经产生 `ask`** 之后才咨询分类器(`dist/core/hooks.js`:`if (input.autoMode &&
151
- * decision.action === "ask" …)`);
152
- * · 而 `shellGate:"off"` 下 core **不铸任何 shell 面的门**(`dist/core/runner/prepare-task.js` 的
153
- * `shellGate === "off"` 臂反而发一条 `classification:"shell-gate-off"` 的 onError:「真可写 Bash 挂着
154
- * 却没有 shell 安全轴折叠」)。
155
- * 两条合起来:`auto` × off ⇒ Bash 既不产 ask、分类器也就永不被咨询 —— off 档下「筛选权交给分类器」
156
- * 不成立。故 auto 归 classify 组:分类器坐在 classify 产的 ask 之上,语义才真是「交给分类器」
157
- * (良性只读命令由分类器自动放行,其余 ask;这正是 auto 模式的本义)。发车帖向 [ref] 裁定链披露此
158
- * 一行偏离;core auto 分类器若来日在 off 档下也有咨询点,可再裁回(表驱动一行)。
159
- * · `default` / `acceptEdits` / `plan` ⇒ `"classify"` —— 这一档原先由壳无条件注入 `MANUAL_MODE_SHELL_GATE`
160
- * 供给(车道缺省走了 operator 通道),现在归位到表态轴。`plan` 本身 handsReadOnly(core 明写此档下
161
- * shellGate 无效),给它 classify 纯为一致性。
162
- *
163
- * 🔴 **入参非可选**:缺席(无表态)不是这张表的一行 —— 它的语义是「不写这个键」,只能在调用点判。
164
- * 把它折进来会逼出一个 `undefined` 返回值,而那正是「写 off」与「不写」被混同的入口(core 的
165
- * 缺席默认是 off,但**写**一个 off 会成为 governance 的 base,与缺席不是同一件事)。
166
- *
167
- * 🔴 **闭集穷举表,无 default 臂**:五个模式词是封闭词表,新增一个模式而不更新本表
168
- * 是**编译错误**([ref] 的安全轴纪律:词表的未知项不许有静默兜底臂)。
169
- */
170
- export declare function shellGateForMode(effMode: SettingsPermissionMode): "off" | "classify";
171
- /**
172
- * [ref] —— 生效模式 → `TaskSpec.writeFace`(**写**面的 roots 容纳判断)。`"open"` = 工作区根之外也能写;
173
- * `undefined` = **不写这个键**(core 缺省 `"roots"`,与 7.77.0 及以前逐字节相同)。表逐字在
174
- * {@link MODE_ENGINE_FACES},裁定与硬边界写在 `resolve-spec.ts` 的写点上。
175
- *
176
- * 🔴 与 {@link shellGateForMode} 同律:入参非可选 —— 「表态缺席」不是这张表的一行(那是调用点判的
177
- * 「整个键都不写」),把它折进来就会逼出第二个 `undefined` 语义。
178
- */
179
- export declare function writeFaceForMode(effMode: SettingsPermissionMode): "open" | undefined;
180
- /**
181
- * [ref] ① —— 生效模式 → `TaskSpec.autoModeRequested`(auto **意图**座,CC 250「用户开」半场)。
182
- * `true` 只有 auto 一行;`undefined` = 不写键。表逐字在 {@link MODE_ENGINE_FACES}。
183
- */
184
- export declare function autoModeRequestedForMode(effMode: SettingsPermissionMode): true | undefined;
185
146
  /**
186
147
  * S-346 —— [ref] 技能清单预算两旋钮的**值域谓词**(一对具名单铸点)。
187
148
  *
@@ -262,6 +223,34 @@ export declare const MAX_SETTINGS_PERMISSION_RULES = 1000;
262
223
  export declare const MAX_SETTINGS_ENV_VARS = 256;
263
224
  export declare const MAX_SETTINGS_ENV_VALUE_CHARS = 32768;
264
225
  export declare const MAX_SETTINGS_ENV_KEY_CHARS = 256;
226
+ /** 三桶(闭集;判据按此序逐桶判)。 */
227
+ export declare const SETTINGS_PERMISSION_BUCKETS: readonly ["allow", "deny", "ask"];
228
+ export type SettingsPermissionBucket = (typeof SETTINGS_PERMISSION_BUCKETS)[number];
229
+ /** 拒因(闭集;wire 字段 `reason` 的全部词)。 */
230
+ export declare const SETTINGS_PERMISSION_BUCKET_REFUSALS: readonly ["over_cap", "bad_entry"];
231
+ export type SettingsPermissionBucketRefusal = (typeof SETTINGS_PERMISSION_BUCKET_REFUSALS)[number];
232
+ /** 拒体的机读字段(按 `reason` 定形;原样进 wire 错误体 / `HttpError.extra`)。 */
233
+ export type SettingsPermissionBucketIssueFields = {
234
+ readonly field: `settings.permissions.${SettingsPermissionBucket}`;
235
+ readonly reason: "over_cap";
236
+ readonly count: number;
237
+ readonly cap: number;
238
+ } | {
239
+ readonly field: `settings.permissions.${SettingsPermissionBucket}`;
240
+ readonly reason: "bad_entry";
241
+ readonly index?: number;
242
+ };
243
+ /** 一次裁决的产物:wire 文案 + 机读字段。 */
244
+ export interface SettingsPermissionBucketIssue {
245
+ readonly message: string;
246
+ readonly fields: SettingsPermissionBucketIssueFields;
247
+ }
248
+ /**
249
+ * 三桶的**唯一**判据(纯函数)。`perms` 不是对象 ⇒ 不判(那一形归「settings.permissions must be an object」那道门);桶缺席 / `null`
250
+ * ⇒ 不判(与 `permissions: null` 的「容器缺席」同一约定)。逐桶按 {@link SETTINGS_PERMISSION_BUCKETS} 的序:不是数组 ⇒ `bad_entry`(无
251
+ * `index`);超帽 ⇒ `over_cap`(先判,拒体有界);首个非串 / 空串项 ⇒ `bad_entry` + 它的下标。全合法 ⇒ `null`。
252
+ */
253
+ export declare function settingsPermissionBucketIssue(perms: unknown): SettingsPermissionBucketIssue | null;
265
254
  /** 本 service 真正**消费**的 `settings` 顶层键 —— 闭集。每一个都有消费点:
266
255
  * `env`/`hooks`/`model`/`outputStyle`/`permissions`/`ultracode` 与两个 `skillListing*` 在
267
256
  * {@link parseTaskSettings};`webSearch` 在 `capabilities/scenarios.ts`(判决 `judgeRequestWebSearch`,单用户闸;形错在受理面 400,S-199)。
@@ -37,6 +37,7 @@ export function withPermissionMode(settings, mode) {
37
37
  const base = settings ?? {};
38
38
  return { ...base, permissions: { ...base.permissions, defaultMode: mode } };
39
39
  }
40
+ export const ENGINE_ABSENT_PERMISSION_MODE = "default";
40
41
  export function effectivePermissionMode(body, settings) {
41
42
  const requested = coercePermissionMode(body.permissionMode) ?? settings?.permissions?.defaultMode;
42
43
  const kill = settings?.permissions?.disableAutoMode;
@@ -44,22 +45,6 @@ export function effectivePermissionMode(body, settings) {
44
45
  return "default";
45
46
  return requested;
46
47
  }
47
- export function shellGateForMode(effMode) {
48
- return MODE_ENGINE_FACES[effMode].shellGate;
49
- }
50
- const MODE_ENGINE_FACES = {
51
- bypassPermissions: { shellGate: "off", writeFace: "open", autoModeRequested: undefined },
52
- auto: { shellGate: "classify", writeFace: undefined, autoModeRequested: true },
53
- default: { shellGate: "classify", writeFace: undefined, autoModeRequested: undefined },
54
- acceptEdits: { shellGate: "classify", writeFace: undefined, autoModeRequested: undefined },
55
- plan: { shellGate: "classify", writeFace: undefined, autoModeRequested: undefined },
56
- };
57
- export function writeFaceForMode(effMode) {
58
- return MODE_ENGINE_FACES[effMode].writeFace;
59
- }
60
- export function autoModeRequestedForMode(effMode) {
61
- return MODE_ENGINE_FACES[effMode].autoModeRequested;
62
- }
63
48
  export function skillListingBudgetFractionOk(v) {
64
49
  return typeof v === "number" && Number.isFinite(v) && v > 0 && v <= 1;
65
50
  }
@@ -88,11 +73,44 @@ function cleanEnvMap(v) {
88
73
  }
89
74
  return n > 0 ? out : undefined;
90
75
  }
91
- function cleanStringArray(v) {
92
- if (!Array.isArray(v))
93
- return undefined;
94
- const out = v.filter((x) => typeof x === "string" && x.length > 0).slice(0, MAX_SETTINGS_PERMISSION_RULES);
95
- return out.length > 0 ? out : undefined;
76
+ export const SETTINGS_PERMISSION_BUCKETS = ["allow", "deny", "ask"];
77
+ export const SETTINGS_PERMISSION_BUCKET_REFUSALS = ["over_cap", "bad_entry"];
78
+ function bucketIssueMessage(f) {
79
+ switch (f.reason) {
80
+ case "over_cap":
81
+ return (`${f.field} has ${f.count} entries — at most ${f.cap} are accepted. Refused rather than truncated: a truncated list silently drops the rules past the cap, ` +
82
+ `and a dropped deny / ask rule is a tightening that never happened. Merge rules into broader patterns or trim the list`);
83
+ case "bad_entry":
84
+ return f.index === undefined
85
+ ? `${f.field} must be an array of non-empty rule strings — refused rather than ignored, because an ignored list reads as "in effect" to the caller`
86
+ : `${f.field}[${f.index}] is not a non-empty rule string — every entry must be one. Refused rather than dropped, because a dropped entry reads as "in effect" to the caller`;
87
+ }
88
+ }
89
+ export function settingsPermissionBucketIssue(perms) {
90
+ if (perms === null || typeof perms !== "object" || Array.isArray(perms))
91
+ return null;
92
+ for (const bucket of SETTINGS_PERMISSION_BUCKETS) {
93
+ const v = perms[bucket];
94
+ if (v === undefined || v === null)
95
+ continue;
96
+ const field = `settings.permissions.${bucket}`;
97
+ let fields;
98
+ if (!Array.isArray(v))
99
+ fields = { field, reason: "bad_entry" };
100
+ else if (v.length > MAX_SETTINGS_PERMISSION_RULES)
101
+ fields = { field, reason: "over_cap", count: v.length, cap: MAX_SETTINGS_PERMISSION_RULES };
102
+ else {
103
+ const index = v.findIndex((x) => typeof x !== "string" || x.length === 0);
104
+ if (index >= 0)
105
+ fields = { field, reason: "bad_entry", index };
106
+ }
107
+ if (fields !== undefined)
108
+ return { message: bucketIssueMessage(fields), fields };
109
+ }
110
+ return null;
111
+ }
112
+ function acceptedRuleList(v) {
113
+ return Array.isArray(v) && v.length > 0 ? v.slice() : undefined;
96
114
  }
97
115
  export const TASK_SETTINGS_KEYS = ["env", "hooks", "model", "outputStyle", "permissions", "skillListingBudgetFraction", "skillListingMaxDescChars", "ultracode", "webSearch"];
98
116
  export const TASK_SETTINGS_PERMISSION_KEYS = ["allow", "ask", "defaultMode", "deny", "disableAutoMode"];
@@ -154,9 +172,12 @@ export function parseTaskSettings(raw) {
154
172
  }
155
173
  if (perms !== null && typeof perms === "object" && !Array.isArray(perms)) {
156
174
  const p = perms;
157
- const allow = cleanStringArray(p.allow);
158
- const deny = cleanStringArray(p.deny);
159
- const ask = cleanStringArray(p.ask);
175
+ const bucketsOk = settingsPermissionBucketIssue(p) === null;
176
+ if (!bucketsOk)
177
+ deferred.push("permissions");
178
+ const allow = bucketsOk ? acceptedRuleList(p.allow) : undefined;
179
+ const deny = bucketsOk ? acceptedRuleList(p.deny) : undefined;
180
+ const ask = bucketsOk ? acceptedRuleList(p.ask) : undefined;
160
181
  const defaultMode = coercePermissionModeStrict(p.defaultMode);
161
182
  const rawKill = p.disableAutoMode;
162
183
  const disableAutoMode = rawKill === "disable" || rawKill === true || rawKill === false ? rawKill : undefined;
@@ -348,7 +369,7 @@ export function deriveSettingsPolicy(settings, gate, workflowGate) {
348
369
  }
349
370
  case "default":
350
371
  case "auto": {
351
- const p = compose(fsWriteGate(false), createWorkflowAskPolicy(workflowGate?.isExempt));
372
+ const p = compose(createWorkflowAskPolicy(workflowGate?.isExempt));
352
373
  return p ? { toolPolicy: p } : {};
353
374
  }
354
375
  case "bypassPermissions":