@sema-agent/server 7.114.0 → 7.116.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 (159) hide show
  1. package/MIGRATION.md +237 -2
  2. package/USAGE.md +39 -3
  3. package/deploy/sema-up/cluster-up.sh +13 -1
  4. package/deploy/sema-up/kube-up.sh +13 -1
  5. package/deploy/sema-up/preflight.sh +13 -1
  6. package/deploy/sema-up/sema-up.sh +13 -1
  7. package/deploy/sema-up/smoke.sh +13 -1
  8. package/dist/adoption/wire.d.ts +0 -3
  9. package/dist/approval-ask-audit-store.d.ts +25 -6
  10. package/dist/approval-ask-audit-store.js +6 -1
  11. package/dist/approval-card.d.ts +25 -24
  12. package/dist/approval-card.js +14 -0
  13. package/dist/approval-crash-convergence.d.ts +60 -0
  14. package/dist/approval-crash-convergence.js +75 -0
  15. package/dist/approval-deny-reasons.d.ts +13 -0
  16. package/dist/approval-deny-reasons.js +2 -0
  17. package/dist/approval.js +7 -3
  18. package/dist/boot/coordinators.js +0 -12
  19. package/dist/boot/execution-env.js +2 -1
  20. package/dist/boot/exit-boundary.d.ts +3 -2
  21. package/dist/boot/exit-boundary.js +3 -0
  22. package/dist/boot/resolve-spec.js +5 -1
  23. package/dist/boot/shutdown.d.ts +19 -0
  24. package/dist/boot/shutdown.js +12 -1
  25. package/dist/boot/stage-07-capability-layer.js +21 -15
  26. package/dist/boot/stores.js +4 -4
  27. package/dist/capabilities/composite-members.d.ts +1 -0
  28. package/dist/capabilities/composite-members.js +1 -0
  29. package/dist/capabilities/hands-lane.d.ts +0 -23
  30. package/dist/capabilities/hands-lane.js +3 -2
  31. package/dist/capabilities/send-user-file-tool.d.ts +10 -22
  32. package/dist/capabilities/send-user-file-tool.js +1 -8
  33. package/dist/capability-facts.d.ts +296 -0
  34. package/dist/capability-facts.js +354 -0
  35. package/dist/config-catalog.js +2 -1
  36. package/dist/config-center/apply-effective.js +11 -11
  37. package/dist/config-provider.d.ts +16 -0
  38. package/dist/config-provider.js +11 -2
  39. package/dist/config-types.d.ts +20 -1
  40. package/dist/config.d.ts +55 -29
  41. package/dist/config.js +48 -25
  42. package/dist/deployment-governance.d.ts +15 -4
  43. package/dist/deployment-governance.js +20 -2
  44. package/dist/fleet/fleet-bus.d.ts +1 -22
  45. package/dist/hooks/hook-runner.d.ts +29 -3
  46. package/dist/hooks/hook-runner.js +30 -17
  47. package/dist/http/admission.d.ts +6 -0
  48. package/dist/http/admission.js +29 -7
  49. package/dist/http/approval-sweeps.js +20 -0
  50. package/dist/http/checkpoint-ctx.d.ts +68 -0
  51. package/dist/http/checkpoint-ctx.js +36 -0
  52. package/dist/http/durable-run-leg.js +2 -1
  53. package/dist/http/principal-gate.d.ts +3 -0
  54. package/dist/http/principal-gate.js +3 -0
  55. package/dist/http/resume-legs.js +19 -1
  56. package/dist/http/retired-skill-manifest.d.ts +23 -0
  57. package/dist/http/retired-skill-manifest.js +22 -0
  58. package/dist/http/route-ctx.d.ts +7 -0
  59. package/dist/http/route-table.d.ts +3 -2
  60. package/dist/http/route-table.js +5 -5
  61. package/dist/http/routes/a2a-serve.d.ts +1 -3
  62. package/dist/http/routes/a2a-serve.js +14 -23
  63. package/dist/http/routes/admin-config-refresh.js +7 -2
  64. package/dist/http/routes/admin-drain.js +7 -2
  65. package/dist/http/routes/admin-memory-optout.d.ts +0 -2
  66. package/dist/http/routes/admin-memory-optout.js +20 -7
  67. package/dist/http/routes/adoption.js +19 -5
  68. package/dist/http/routes/agents-roster.js +4 -2
  69. package/dist/http/routes/approvals-assistant.js +41 -18
  70. package/dist/http/routes/background.d.ts +0 -2
  71. package/dist/http/routes/background.js +4 -5
  72. package/dist/http/routes/capabilities.d.ts +2 -1
  73. package/dist/http/routes/capabilities.js +63 -65
  74. package/dist/http/routes/config-catalog.d.ts +4 -5
  75. package/dist/http/routes/config-catalog.js +16 -2
  76. package/dist/http/routes/devices.js +17 -11
  77. package/dist/http/routes/diagnostics.d.ts +5 -0
  78. package/dist/http/routes/diagnostics.js +8 -1
  79. package/dist/http/routes/fleet.js +4 -2
  80. package/dist/http/routes/images.js +30 -3
  81. package/dist/http/routes/leader.js +7 -1
  82. package/dist/http/routes/mcp-reconnect.js +17 -10
  83. package/dist/http/routes/memory-bundle.js +20 -6
  84. package/dist/http/routes/memory-compliance.js +19 -5
  85. package/dist/http/routes/memory-consolidation.d.ts +9 -12
  86. package/dist/http/routes/memory-consolidation.js +17 -3
  87. package/dist/http/routes/memory-origin.js +19 -5
  88. package/dist/http/routes/notify-wake.js +5 -3
  89. package/dist/http/routes/observability.js +19 -5
  90. package/dist/http/routes/retention-ops.js +6 -1
  91. package/dist/http/routes/rules.js +49 -21
  92. package/dist/http/routes/run-memory-capture.js +5 -2
  93. package/dist/http/routes/runs.js +90 -60
  94. package/dist/http/routes/session-sync.js +64 -53
  95. package/dist/http/routes/sessions-list.js +8 -5
  96. package/dist/http/routes/sessions.js +40 -25
  97. package/dist/http/routes/shared-memory.js +5 -3
  98. package/dist/http/routes/tasks.js +5 -3
  99. package/dist/http/routes/trace-usage.js +18 -11
  100. package/dist/http/routes/workflows.js +36 -13
  101. package/dist/http/server.js +4 -1
  102. package/dist/http/wire-types.d.ts +6 -0
  103. package/dist/index.d.ts +1 -1
  104. package/dist/leader/wire.js +2 -1
  105. package/dist/local-data-root.d.ts +1 -1
  106. package/dist/mcp-content-origin.d.ts +0 -8
  107. package/dist/model-entry-refusal.d.ts +10 -2
  108. package/dist/model-entry-refusal.js +19 -3
  109. package/dist/namespaced-rule-name.d.ts +54 -0
  110. package/dist/namespaced-rule-name.js +33 -0
  111. package/dist/observability/corrupt-read-seat.d.ts +4 -3
  112. package/dist/observability/corrupt-read-seat.js +4 -3
  113. package/dist/observability/fail-open.d.ts +10 -9
  114. package/dist/observability/fail-open.js +5 -1
  115. package/dist/observability/permission-rule-events.d.ts +3 -0
  116. package/dist/observability/permission-rule-events.js +4 -0
  117. package/dist/permission-rule-list.d.ts +81 -0
  118. package/dist/permission-rule-list.js +17 -0
  119. package/dist/plugins/file-run-store.d.ts +7 -1
  120. package/dist/plugins/file-run-store.js +20 -7
  121. package/dist/plugins/memory-run-store.js +2 -1
  122. package/dist/plugins/one-time-migrations.d.ts +4 -2
  123. package/dist/plugins/one-time-migrations.js +26 -3
  124. package/dist/plugins/pg-session-storage.js +1 -1
  125. package/dist/plugins/run-store-sql.js +4 -4
  126. package/dist/plugins/session-policy-store-sql.js +4 -2
  127. package/dist/plugins/store-backend.d.ts +6 -1
  128. package/dist/plugins/store-backend.js +2 -1
  129. package/dist/plugins/store-contracts.d.ts +27 -8
  130. package/dist/plugins/store-contracts.js +12 -3
  131. package/dist/plugins/tidb-session-store.js +1 -1
  132. package/dist/request-key-closure.js +1 -1
  133. package/dist/run-local.d.ts +37 -3
  134. package/dist/run-local.js +52 -6
  135. package/dist/runs.d.ts +0 -58
  136. package/dist/runs.js +15 -20
  137. package/dist/runtime-governance.d.ts +21 -1
  138. package/dist/runtime-governance.js +6 -1
  139. package/dist/security.js +2 -2
  140. package/dist/session-policy-wire.d.ts +40 -59
  141. package/dist/session-policy-wire.js +60 -32
  142. package/dist/spec-fields.d.ts +32 -0
  143. package/dist/spec-fields.js +31 -16
  144. package/dist/standing-answer-grant.d.ts +8 -0
  145. package/dist/standing-answer-grant.js +5 -0
  146. package/dist/task-settings.d.ts +17 -6
  147. package/dist/task-settings.js +16 -30
  148. package/dist/tool-approval.d.ts +32 -4
  149. package/dist/tool-approval.js +31 -8
  150. package/dist/trace/core-keyset-guard.d.ts +13 -8
  151. package/dist/trace/project.js +9 -0
  152. package/dist/trace/self-minted-wire.d.ts +42 -0
  153. package/dist/trace/self-minted-wire.js +13 -0
  154. package/dist/user-input-ledger.d.ts +8 -5
  155. package/package.json +6 -4
  156. package/scripts/drain-check-local.mjs +528 -0
  157. package/scripts/drain-check.mysql.sql +67 -0
  158. package/scripts/drain-check.pg.sql +71 -0
  159. package/scripts/retired-mode-seed-drain-local.mjs +0 -317
package/dist/run-local.js CHANGED
@@ -8,11 +8,12 @@ import { realpathSync } from "node:fs";
8
8
  import { createInterface } from "node:readline/promises";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { Runner, uuidv7, FileStorageBackend, NodeExecutionEnv, TtlSessionStore, CenterPromptSource, FilePromptArtifactStore, FilePromptSourceStateStore, MemoryPromptArtifactStore, MemoryPromptSourceStateStore, defaultTaskRegistry } from "@sema-agent/core";
11
+ import { readAskClassPair } from "./approval-card.js";
11
12
  import { singleUserAutoAcceptBaselineApplies } from "./approval.js";
12
13
  import { withUnattendedLeg } from "./unattended-permission-mode.js";
13
14
  import { buildUnattendedPolicyRefusal } from "./tool-approval.js";
14
15
  import { createBrain } from "./brain.js";
15
- import { assertGuardPatternsUsable, buildOnlySensitiveBaselineWarning, createApprovalBaselinePolicy, createDeploymentGovernanceInputs, } from "./deployment-governance.js";
16
+ import { assertGuardPatternsUsable, buildOnlySensitiveBaselineWarning, buildApprovalGrantNotice, createApprovalBaselinePolicy, createDeploymentGovernanceInputs, } from "./deployment-governance.js";
16
17
  import { applyRuntimeGovernance } from "./runtime-governance.js";
17
18
  import { ForkRoutingSessionStore } from "./plugins/fork-routing-session-store.js";
18
19
  import { taskWallClockSec } from "./task-workflow.js";
@@ -133,6 +134,52 @@ export function applyLocalGovernance(base, config, workspaceDir, engineDataRoot)
133
134
  }
134
135
  const NO_TTY_REFUSAL_REASON = "this run-local invocation has no TTY, so there is no one to put this approval to; it is refused fail-closed instead of being asked.";
135
136
  const UNDISPLAYABLE_ARGS_REFUSAL_REASON = "the call's arguments cannot be displayed, so there is nothing a person could review; it is refused fail-closed instead of being asked.";
137
+ const NO_TTY_SPOKEN_REMEDIES = ["grant", "approval_lists", "deployment_mandate", "shell_gate"];
138
+ export const NO_TTY_ASK_CLASS_ROW = {
139
+ content_question: { remedy: "terminal_only", autoBudgetReleases: false },
140
+ hook: { remedy: "terminal_only", autoBudgetReleases: false },
141
+ ancestor: { remedy: "terminal_only", autoBudgetReleases: false },
142
+ unverifiable_delete: { remedy: "terminal_only", autoBudgetReleases: false },
143
+ transcript_write: { remedy: "terminal_only", autoBudgetReleases: false },
144
+ org_rule: { remedy: "terminal_only", autoBudgetReleases: false },
145
+ governance_unavailable: { remedy: "terminal_only", autoBudgetReleases: false },
146
+ rule_unreadable: { remedy: "terminal_only", autoBudgetReleases: false },
147
+ ask_rule: { remedy: "approval_lists", autoBudgetReleases: true },
148
+ denial_limit_fallback: { remedy: "terminal_only", autoBudgetReleases: false },
149
+ operator_forced: { remedy: "deployment_mandate", autoBudgetReleases: false },
150
+ shell_floor: { remedy: "shell_gate", autoBudgetReleases: false },
151
+ write_protection_unresolved: { remedy: "terminal_only", autoBudgetReleases: false },
152
+ probe: { remedy: "terminal_only", autoBudgetReleases: false },
153
+ write_protection: { remedy: "terminal_only", autoBudgetReleases: false },
154
+ tool_marks_mcp: { remedy: "terminal_only", autoBudgetReleases: false },
155
+ tool_marks_a2a: { remedy: "terminal_only", autoBudgetReleases: false },
156
+ policy_rra: { remedy: "terminal_only", autoBudgetReleases: false },
157
+ artifact_shared_read: { remedy: "terminal_only", autoBudgetReleases: false },
158
+ mode: { remedy: "terminal_only", autoBudgetReleases: false },
159
+ unstated: { remedy: "grant", autoBudgetReleases: false },
160
+ ordinary: { remedy: "terminal_only", autoBudgetReleases: false },
161
+ requires_user_interaction: { remedy: "terminal_only", autoBudgetReleases: false },
162
+ };
163
+ const NO_TTY_REMEDY_SENTENCE = {
164
+ grant: () => `No rule, policy or hook granted this server tool and it states no permission of its own: the operator states one with APPROVAL_GRANT ` +
165
+ `(an allow rule for server tools — an exact tool name, mcp__<server>__* or a2a__<peer>__*).`,
166
+ approval_lists: (budget) => `The gate comes from the approval lists: APPROVAL_REQUIRE / APPROVAL_NEVER_AUTO (APPROVAL_DENY refuses outright without asking); ` +
167
+ `a separate .env for this CLI keeps a server's lists off it.` +
168
+ (budget ? ` APPROVAL_AUTO_BUDGET auto-approves that many APPROVAL_REQUIRE calls.` : ""),
169
+ deployment_mandate: () => `This deployment mandates a person for it — one of: APPROVAL_NEVER_AUTO (always a human), or a runtime.commandPolicy entry in config.d/governance.json ` +
170
+ `(an \`ask\` entry, or — when that policy has any \`allow\` entry — a command it does not list).`,
171
+ shell_gate: () => `This deployment raises the shell gate to always-ask — one of: AUTONOMY=ask, MANUAL_MODE_SHELL_GATE=always.`,
172
+ };
173
+ function buildNoTtyRemedySentence(req) {
174
+ const terminal = "Run it in a terminal to answer interactively.";
175
+ const pair = readAskClassPair(req);
176
+ if (pair === undefined)
177
+ return `The engine did not say what kind of question this is, so no setting can be named here. ${terminal}`;
178
+ const rows = pair.classes.map((c) => NO_TTY_ASK_CLASS_ROW[c]);
179
+ const budget = rows.every((r) => r.autoBudgetReleases);
180
+ const spoken = NO_TTY_SPOKEN_REMEDIES.filter((k) => rows.some((r) => r.remedy === k)).map((k) => NO_TTY_REMEDY_SENTENCE[k](budget));
181
+ return [...spoken, terminal].join(" ");
182
+ }
136
183
  export function createLocalApprover(io) {
137
184
  let tail = Promise.resolve();
138
185
  const refuseWithoutPerson = (reason) => {
@@ -141,11 +188,7 @@ export function createLocalApprover(io) {
141
188
  };
142
189
  return async (req, signal) => {
143
190
  if (!io.isTty) {
144
- io.printErr(`denied "${terminalSafeText(req.toolName)}" (${terminalSafeText(req.message)}) — this run-local invocation has no TTY, so there is nobody to ask. ` +
145
- `The gate comes from this deployment's governance knobs: APPROVAL_REQUIRE / APPROVAL_NEVER_AUTO (the approval lists; ` +
146
- `APPROVAL_DENY refuses outright without asking), AUTONOMY, MANUAL_MODE_SHELL_GATE, runtime.commandPolicy (config.d/governance.json). ` +
147
- `Run it in a terminal to answer interactively, or relax the knob (APPROVAL_AUTO_BUDGET auto-approves that many APPROVAL_REQUIRE calls; ` +
148
- `a separate .env for this CLI keeps a server's lists off it).`);
191
+ io.printErr(`denied "${terminalSafeText(req.toolName)}" (${terminalSafeText(req.message)}) — this run-local invocation has no TTY, so there is nobody to ask. ${buildNoTtyRemedySentence(req)}`);
149
192
  return refuseWithoutPerson(NO_TTY_REFUSAL_REASON);
150
193
  }
151
194
  const argsShown = argsDisplayOf(req.args);
@@ -334,6 +377,9 @@ export async function runLocal(argv, deps = {}) {
334
377
  });
335
378
  if (ungatedCompensation)
336
379
  logger.warn(ungatedCompensation.event, ungatedCompensation.fields);
380
+ const approvalGrantNotice = buildApprovalGrantNotice(config);
381
+ if (approvalGrantNotice)
382
+ logger.info(approvalGrantNotice.event, approvalGrantNotice.fields);
337
383
  if (!deps.brain) {
338
384
  const missing = missingGatewayRequirements(config);
339
385
  if (missing.length > 0) {
package/dist/runs.d.ts CHANGED
@@ -99,64 +99,6 @@ export declare function markChildrenStoppedByUserOnAbort(signal: AbortSignal, ta
99
99
  export declare function isSessionConflictResult(result: {
100
100
  terminal: TerminalCause;
101
101
  }): boolean;
102
- /**
103
- * E18 resume-at CALLER error → an HTTP 4xx. A bad/stale resume-at target (the resolved entryId is gone or is not a
104
- * settled message boundary) surfaces as a FAILED TaskResult carrying a `resume_at.*` errorCode (core swallows the
105
- * prepare-throw into the result, exactly like a session conflict — runtask catch → status:"failed"+errorCode). The
106
- * sync path otherwise 200s a failed result; map these to 4xx so a caller mistake reads as one. `not_found` (the
107
- * entry is genuinely absent) → 404; everything else (not_a_message / conflicts_resume / no_session) → 422.
108
- *
109
- * [ref] Phase3: `requireExistingSession` on a genuinely-missing/purged session ALSO surfaces as a failed result
110
- * (core prepare-task maps the store's `not_found` → `resume.session_not_found`); map it to 404 (the session is gone),
111
- * the same "caller mistake reads as a 4xx, not a silent 200 fresh run" discipline.
112
- *
113
- * [ref] rewind exclusive mode (core 1.292 resumeAtMode:"before"): its two edge rejections ride the same `resume_at.*`
114
- * prefix (before_target_not_user / before_root_unsupported → 422 via the existing branch, errorCode passed through
115
- * UNCHANGED for the shell). `rewind_snapshot.unresolvable` (rewindFiles + "before": no snapshot at/above the branch
116
- * point) is core's third rejection with a DIFFERENT prefix — same caller-mistake shape (prepare-throw, nothing
117
- * billed), map it to 422 explicitly so it doesn't fall through to a 200-with-failed body.
118
- *
119
- * [ref] 件A([ref] 件4): core 5.13.0's org-memory admission rides the SAME prepare-throw pipe
120
- * (GOVERNANCE_CODES). `memory.admission_denied` is TERMINAL — the principal has no grant on that tenant
121
- * plane → 403 (a retry cannot change the verdict). `memory.admission_required` is TRANSIENT fail-closed —
122
- * the directory is unreachable / no resolver is wired → 503 (retry later; the sync leg forwards the
123
- * result's `retryAfterMs` as body `retryAfterSec`, same family shape as usage.window_exhausted). Exact
124
- * codes only, no memory.* family grab — an unknown sibling stays a 200-with-failed-body until cataloged.
125
- *
126
- * [ref] (core 7.0.0 片2/3, [ref] S-3): the session capture opt-out's four prepare-throw refusals ride the
127
- * same pipe. `memory.capture_optout_denied` is TERMINAL (the principal's `allowMemoryOptOut` verdict is an
128
- * explicit `false`, or the posture is `capture-required` / `governed`-with-absent-verdict — a retry cannot
129
- * change it) → 403, the `memory.admission_denied` twin. `memory.capture_optout_unpersisted` is the
130
- * declaration arm's "control-plane record could not be written" refusal — a store outage, retry may succeed
131
- * → 503 (transient fail-closed, the `memory.admission_required` twin; no retryAfterMs on this one — core
132
- * mints none, so the sync leg forwards no hint rather than inventing one). `config.memory_capture_spelling`
133
- * is a TaskSpec authoring mistake (the only legal spelling is the string "off") → 400, the
134
- * `config.memory_erasure_request` family shape. `config.memory_capture_unsupported` is the DEPLOYMENT SHAPE
135
- * refusing the declaration (remote/per-run execution with no durable capture-record carrier: the record
136
- * would not survive to a resume replica) → 409 — the request is well-formed, the deployment's state
137
- * conflicts with it, the `config.refresh_unavailable` family shape ("change the deployment, not the request").
138
- * Status choices are pinned by the `config.`/`memory.` family status sets in error-code-catalog-live.test.ts.
139
- *
140
- * S-125 件⑥(core 7.4.0 [ref]):`rewind.child_scope_unsupported` —— 这条腿是一个**委派子代**,它的文件编辑
141
- * 记在**根 session** 的文件历史里,所以「把文件还原到某个 entry」是根 session 的动作、不是子代的;core 在
142
- * prepare 期响亮拒(零计费,文件一个字节没动)。映射 **422**:请求形式合法、语义上找错了执行者 —— 与
143
- * `resume_at.*` 的「调用方指错了锚」同一族同一档,而不是 404(目标在,只是不归你)也不是 409(部署状态没冲突)。
144
- * ⚠️ **前缀不吞**:本码前缀是 `rewind.`,与下面那条 `resume_at.` **开集前缀臂**不同族 —— 它不会被那一臂顺手
145
- * 兜走(兜走会把一条「找错执行者」的拒答成「锚解析不了」),也不是 `rewind_snapshot.`(那是「快照找不到」)。
146
- * 三个前缀各自成族,逐码对表见 `test/resume-anchor.test.ts` 的 S-125 `rewind.` 族格。
147
- * 🔴 **可达性如实**:本仓的 HTTP 面**今天铸不出**这条腿 —— 子代由 core 内部的 Agent 工具派生,
148
- * `fileHistoryLineage` 是 core prepare 的 internals(不在 `TaskSpec` 上),server 从不直接 prepare 一条带
149
- * lineage 的子代腿。本臂是**分诊面的完备性**(core 说 X ⇒ wire 形是 Y),不是一条今天在跑的路;将来的
150
- * 委派入口接线那天零改动即生效。同族其余码(`rewind_files_retired` / `invalid_spec` / `conflicting_targets` /
151
- * `restore_failed`)今天**仍落 200-with-failed-body**,那是各自提货批**之前**就存在的缺口(不是回归),已在上述对表格里
152
- * 逐码显式登记为债务,并由那道门看住不许陈腐。
153
- *
154
- * S-724(core 7.31.0 #1048 / #1050):`rewind.capture_disabled` → **422**(关断词与还原同体 = 自相矛盾;本服务的请求面上
155
- * 走不到它 —— fresh 受理门先 400 `request.field_conflict`(立门理由:core 这一拒落在会话分叉之后、不回滚;S-865 / core 7.35.0
156
- * #1195 起 core 在分叉之前拒);映射是分诊面完备性)、`rewind.store_unconfigured`
157
- * → **409**(部署没有历史店;`FILE_HISTORY_CAPTURE=false` 自本版不再摘店 ⇒ 可达面只剩没有后端的部署形,那里
158
- * `capabilities.rewindFiles` 恒报 false —— 三问在 CHANGELOG 7.104.0)。
159
- */
160
102
  export declare function resumeAtHttpStatus(result: {
161
103
  terminal: TerminalCause;
162
104
  }): number | undefined;
package/dist/runs.js CHANGED
@@ -71,6 +71,19 @@ export function isSessionConflictResult(result) {
71
71
  return result.terminal.code === "conflict";
72
72
  return /stale write|concurrent write|conflict/i.test(result.terminal.message ?? "");
73
73
  }
74
+ const PREPARE_REFUSAL_HTTP_STATUS = {
75
+ "resume.session_not_found": 404,
76
+ "rewind_snapshot.unresolvable": 422,
77
+ "rewind.child_scope_unsupported": 422,
78
+ "rewind.capture_disabled": 422,
79
+ "rewind.store_unconfigured": 409,
80
+ "memory.capture_optout_unpersisted": 503,
81
+ "config.memory_capture_spelling": 400,
82
+ "config.invalid_checkpoint_store": 400,
83
+ "config.skill_listing_budget_invalid": 400,
84
+ "config.user_invoked_skill_unknown": 400,
85
+ "config.memory_capture_unsupported": 409,
86
+ };
74
87
  export function resumeAtHttpStatus(result) {
75
88
  const code = failureCodeOf(result.terminal);
76
89
  if (code === undefined)
@@ -86,26 +99,8 @@ export function resumeAtHttpStatus(result) {
86
99
  if (wire !== undefined)
87
100
  return wire.status;
88
101
  }
89
- if (code === "resume.session_not_found")
90
- return 404;
91
- if (code === "rewind_snapshot.unresolvable")
92
- return 422;
93
- if (code === "rewind.child_scope_unsupported")
94
- return 422;
95
- if (code === "rewind.capture_disabled")
96
- return 422;
97
- if (code === "rewind.store_unconfigured")
98
- return 409;
99
- if (code === "memory.capture_optout_unpersisted")
100
- return 503;
101
- if (code === "config.memory_capture_spelling")
102
- return 400;
103
- if (code === "config.invalid_checkpoint_store")
104
- return 400;
105
- if (code === "config.skill_listing_budget_invalid")
106
- return 400;
107
- if (code === "config.memory_capture_unsupported")
108
- return 409;
102
+ if (Object.hasOwn(PREPARE_REFUSAL_HTTP_STATUS, code))
103
+ return PREPARE_REFUSAL_HTTP_STATUS[code];
109
104
  if (!code.startsWith("resume_at."))
110
105
  return undefined;
111
106
  return code === "resume_at.not_found" ? 404 : 422;
@@ -97,6 +97,19 @@ export declare function validateCommandRules(rules: CommandRule[]): string[];
97
97
  * 闭集本身则由契约 zod 派生 ⇒ 契约加一个判词,`RANK` 与桶分配的穷尽 switch 双双**编译红**。
98
98
  */
99
99
  export declare function compileCommandPolicyLayers(rules: CommandRule[] | undefined): ToolPolicy[];
100
+ /** 运维 allow 规则表的出处名 = 它的 env 名(盖进每条规则;编不出来 / 不合文法时拒启句的主语)。 */
101
+ export declare const APPROVAL_GRANT_SOURCE = "APPROVAL_GRANT";
102
+ /**
103
+ * 7.115.0 —— 运维的 allow 规则(`APPROVAL_GRANT`)编成治理层的**叶**(空 / 缺席 ⇒ `[]`,本层一片叶都不铸,折叠逐字与修前同)。
104
+ *
105
+ * 编译口是**请求腿 `settings.permissions.allow` 的同一只**(`permission-rule-list.ts` `createRuleListPolicy`,`behavior: "allow"`):命中 = core 带
106
+ * `decisionReason: "rule"` 的放行(折叠把这句表态带到终局,引擎据此不再为没人表态的服务器工具调用出 `unstated` 卡);未命中 = 裸 allow =
107
+ * 不表态。它是一片**不收紧**的叶 —— 与 commandPolicy / 守卫集同住一个折叠点,只因为三者是同一个「部署说的话」:折叠 deny > ask > allow,
108
+ * 同折叠里任何一片叶的 deny / ask、`tightenTaskSpec` 之下的审批基线、之上的请求 settings 与会话规则,照旧压过它。
109
+ *
110
+ * 启动校验(`config.ts` `approvalGrantRefusal`)试编的也是本函数 —— 拒启判据与运行期编译是同一只,不会出现「启动收下、任务里编不出来」。
111
+ */
112
+ export declare function compileApprovalGrantLayers(grant: readonly string[] | undefined): ToolPolicy[];
100
113
  /**
101
114
  * Expand an autonomy mode into TIGHTEN-ONLY `TaskSpec` safety overrides (the primitives — TRAP #2):
102
115
  * - `read-only` / `plan` → `handsReadOnly: true` **+ `writeFace: "roots"`** (read + propose, never mutate; the
@@ -203,13 +216,18 @@ export declare function governanceOriginOf(gate: {
203
216
  };
204
217
  } | undefined, governance: GovernancePosture | undefined): true | undefined;
205
218
  /**
206
- * Apply the operator's runtime governance (autonomy + commandPolicy + manualModeShellGate) onto a base
219
+ * Apply the operator's runtime governance (autonomy + commandPolicy + manualModeShellGate + the guard set + the
220
+ * `APPROVAL_GRANT` allow rules) onto a base
207
221
  * `TaskSpec`, TIGHTEN-ONLY, in a SINGLE {@link tightenTaskSpec} call: commandPolicy compiles to a `toolPolicy`
208
222
  * override (combined onto the baseline by `tightenTaskSpec` — TRAP #1), and autonomy expands to
209
223
  * `handsReadOnly` / `shellGate` overrides (TRAP #2). `tightenTaskSpec` THROWS `TaskSpecTightenError` if any
210
224
  * override would LOOSEN a stricter base safety field — surfacing a misconfiguration loudly instead of silently
211
225
  * weakening the gate. Returns `base` untouched when there is nothing to apply.
212
226
  *
227
+ * 7.115.0:`APPROVAL_GRANT` 是这里唯一一片**不收紧**的叶 —— 它只给出 allow 的那句表态(core 7.40.0 起没人表态的服务器工具调用要问,
228
+ * 运维的规则就是那句答),从不清掉任何 deny / ask:折叠 deny > ask > allow,所以「部署 ⊇ 操作员 ⊇ 客户端只能收窄」的不变量逐字成立。
229
+ * 编译口与请求腿的 allow 桶同一只({@link compileApprovalGrantLayers})。
230
+ *
213
231
  * [ref]([ref] server 分单②):`MANUAL_MODE_SHELL_GATE` 从 settings 折叠搬到这里**无条件施加**。旧家
214
232
  * (task-settings deriveSettingsPolicy 的 default/auto/acceptEdits 三臂)让一个**部署级 tighten-only 旋钮
215
233
  * 的生死由客户端表态在场性决定**:body 无 settings/permissionMode(cli print/headless 腿按设计不 stamp)
@@ -242,6 +260,8 @@ export declare function applyRuntimeGovernance(base: TaskSpec, governance: {
242
260
  /** 守卫集的**叶**(`deployment-governance.ts` `createDeploymentGovernanceInputs` 预铸;S-958 起是叶的列表,不是一只预折的策略)。
243
261
  * 空列表与缺席同义。 */
244
262
  sensitivePathLayers?: readonly ToolPolicy[];
263
+ /** 7.115.0:运维的 allow 规则(`APPROVAL_GRANT` 原串;本函数经 {@link compileApprovalGrantLayers} 编成叶)。空列表与缺席同义。 */
264
+ approvalGrant?: readonly string[];
245
265
  /** [ref]/[ref] `governanceForced` 的标记表(缺省 = **当前 run 腿的 ALS 表**,见
246
266
  * `governance-ask-marks.ts` 顶注「作用域」段)。注入口只为测试与将来的多实例形。 */
247
267
  askMarks?: GovernanceAskMarks;
@@ -2,6 +2,7 @@ import { combinePolicies, COARSE_SHELL_TOOLS as CORE_COARSE_SHELL_TOOLS, createC
2
2
  import { GovernanceConfig } from "@sema-agent/settings-schema";
3
3
  import { currentGovernanceAskMarks } from "./governance-ask-marks.js";
4
4
  import { createDeploymentAuthoredPolicy, operatorAsk } from "./operator-ask.js";
5
+ import { createRuleListPolicy } from "./permission-rule-list.js";
5
6
  import { heldRuleName } from "./tool-rule-names.js";
6
7
  const COMMAND_RULE_SHAPE = GovernanceConfig.shape.commandPolicy.unwrap().element.shape;
7
8
  const COMMAND_DECISIONS = COMMAND_RULE_SHAPE.decision.options;
@@ -100,6 +101,10 @@ export function compileCommandPolicyLayers(rules) {
100
101
  }
101
102
  return policies;
102
103
  }
104
+ export const APPROVAL_GRANT_SOURCE = "APPROVAL_GRANT";
105
+ export function compileApprovalGrantLayers(grant) {
106
+ return grant !== undefined && grant.length > 0 ? [createRuleListPolicy(grant, "allow", APPROVAL_GRANT_SOURCE, undefined)] : [];
107
+ }
103
108
  export function autonomyOverrides(autonomy) {
104
109
  switch (autonomy) {
105
110
  case "read-only":
@@ -143,7 +148,7 @@ function createGovernanceShellGateMarkPolicy(marksOf, tools = COARSE_SHELL_TOOLS
143
148
  export function applyRuntimeGovernance(base, governance) {
144
149
  const marksOf = () => governance.askMarks ?? currentGovernanceAskMarks();
145
150
  const overrides = { ...autonomyOverrides(governance.autonomy) };
146
- const layers = [...compileCommandPolicyLayers(governance.commandPolicy), ...(governance.sensitivePathLayers ?? [])];
151
+ const layers = [...compileCommandPolicyLayers(governance.commandPolicy), ...(governance.sensitivePathLayers ?? []), ...compileApprovalGrantLayers(governance.approvalGrant)];
147
152
  const governancePolicy = layers.length > 0 ? createGovernanceAskMarkingPolicy(combinePolicies(...layers.map(createDeploymentAuthoredPolicy)), marksOf) : undefined;
148
153
  if (governancePolicy !== undefined)
149
154
  overrides.toolPolicy = governancePolicy;
package/dist/security.js CHANGED
@@ -65,7 +65,7 @@ export function createAuthorizer(config, sessionStore) {
65
65
  let owner = await ownerOf(requested);
66
66
  if (owner === undefined) {
67
67
  if (requireExisting) {
68
- throw new HttpError(404, "requireExistingSession: session does not exist (resume.session_not_found)");
68
+ throw createCodedHttpError(404, "resume.session_not_found", "requireExistingSession: session does not exist (resume.session_not_found)");
69
69
  }
70
70
  if (register) {
71
71
  await register(requested, principal ?? null);
@@ -89,7 +89,7 @@ export function createAuthorizer(config, sessionStore) {
89
89
  }
90
90
  else {
91
91
  if (requireExisting) {
92
- throw new HttpError(422, "requireExistingSession requires a sessionId — there is no existing session to resume (resume.session_not_found)");
92
+ throw createCodedHttpError(422, "request.field_conflict", "requireExistingSession requires a sessionId — there is no existing session to resume");
93
93
  }
94
94
  sessionId = uuidv7();
95
95
  if (register)
@@ -16,15 +16,25 @@
16
16
  * 自陈「Malformed shape → 400(never a downstream TypeError)」,这一半没守);
17
17
  * · `loosenReasons` 的每一臂都从 `prior` 出发,**prior 为空时恒回空集** ⇒ tighten-only 门对一条
18
18
  * 全新的会话放行任何内容,坏形因此连「收紧门」这一层都碰不到。
19
- * ⇒ 类修:一只读者,键集由 `keyof SessionPermissionRules` **派生**(下方 `RULE_FIELD_ROLE` 的
20
- * `satisfies Record<keyof SessionPermissionRules, RuleFieldRole>`:core 加桶 ⇒ 缺键**编译红**;core 删桶 ⇒
19
+ * ⇒ 类修:一只读者,键集由 `keyof SessionPermissionRules` **派生**(下方 `RULE_FIELDS` 的
20
+ * `satisfies Record<keyof SessionPermissionRules, true>`:core 加桶 ⇒ 缺键**编译红**;core 删桶 ⇒
21
21
  * 多键**编译红**),两条腿同调。
22
22
  *
23
23
  * ── S-800(7.105.0):同一只读者多判一问 —— 写得进却跑不了的工具名 ─────────────────────────────────
24
24
  * 两条写入腿是 server 往会话规则店写的**全部**写口,所以「写口 fail-closed」收成一只**写口读者**
25
- * ({@link readWritableSessionPermissionRules} = 形读者 + {@link legacyToolNameOf}),两腿同调,将来第三条写入腿
25
+ * ({@link readWritableSessionPermissionRules} = 形读者 + 名字判官),两腿同调,将来第三条写入腿
26
26
  * 调同一只也同得。判词多一种(`legacy-tool-name`),两腿各把它映射成 `400 rules.legacy_tool_name`(映射点仍在各自
27
27
  * 站点:码位只放字面量)。形读者本身不变 —— SQL 店的读回也用它,读回不许判名字(理由见写口读者头注)。
28
+ * S-1047(7.116.0 / core 7.41.0 #1156):名字判官 = core 导出的 `assertSessionRuleNamesCurrent`(两只 bundled 店与本仓 SQL 双生在 `putRules`
29
+ * 里同点同调它;准备阶段的名审计与本仓配置表读的是同一只 `judgeRuleToolName`)—— 本仓手写的那一只(`legacyToolNameOf`)删除,零双读;
30
+ * 「哪几桶装工具名」归 core(它的 `HOLDS_TOOL_NAMES`)。
31
+ *
32
+ * ── 7.116.0:写口读者再多判一问 —— 够不着挂载工具的协议名 ─────────────────────────────────────
33
+ * 缺段(`mcp__srv__` / `mcp__`)与对端段铸不出(`mcp__github.com__*`)的协议名 core 的判官判 `current`(它只看退役名与无前缀 `__` 名),
34
+ * 而配置表一直拒缺段名 —— 一个名字两张表两种答案,对端段那一半则两张表都照收、永不命中。判官与句子是 `namespaced-rule-name.ts` 的那一只
35
+ * (配置表同调),码 `400 request.field_invalid`(会话规则写口既有的字段值码;不另铸新码,也不往 core 的判词闭集里塞本仓的词)。
36
+ * 这一问要逐桶判,而 core 不导出 `HOLDS_TOOL_NAMES` 的运行期值 ⇒ 桶表 {@link TOOL_NAME_FIELDS} 的键集从 core 判官交回的 `field` 型派生(两向 tsc 配对),
37
+ * 随这一问在 core 自己给出判词那天整只退役。SQL 店写口经 {@link assertSessionRuleNamesReachable} 同判。
28
38
  *
29
39
  * ── 文案也在这里(codex 一轮追加:两条腿各铸一份句子 = 第二份手抄)───────────────────────────────
30
40
  * 初版把 wire 文案留在两个路由各自铸,理由是错误文案冻结门(`test/api-error-text-freeze.test.ts`)锚的是
@@ -33,53 +43,25 @@
33
43
  * 「常量锚」):句子收进本文件**单一属主**,冻结职责搬到冻结门的**常量锚格** —— 那一格直接断言这两只
34
44
  * 渲染器对每一种判词的输出字节,且对判词闭集穷尽(加一种判词 ⇒ 那一格编译红)。文案一个字节未改。
35
45
  */
36
- import type { SessionPermissionRules, SessionPolicyError, SessionRulesRecord } from "@sema-agent/core";
37
- declare const RULE_FIELD_ROLE: {
38
- readonly toolAllow: "tool-name";
39
- readonly toolDeny: "tool-name";
40
- readonly allowDirs: "path";
41
- readonly commandAllow: "command";
42
- readonly commandDeny: "command";
43
- };
46
+ import { SessionPolicyError } from "@sema-agent/core";
47
+ import type { SessionPermissionRules, SessionRuleNameRefusal, SessionRulesRecord } from "@sema-agent/core";
48
+ import { type UnreachableNamespacedRuleName } from "./namespaced-rule-name.js";
44
49
  /** core `SessionPermissionRules` 的桶名全集(派生,不是手抄)。 */
45
50
  export declare const SESSION_PERMISSION_RULE_FIELDS: readonly (keyof SessionPermissionRules)[];
46
- /** 装工具名的那几桶(由 {@link RULE_FIELD_ROLE} 派生的型)。 */
47
- export type ToolNameRuleField = {
48
- [K in keyof typeof RULE_FIELD_ROLE]: (typeof RULE_FIELD_ROLE)[K] extends "tool-name" ? K : never;
49
- }[keyof typeof RULE_FIELD_ROLE];
51
+ /** 工具名桶里一条够不着任何挂载工具的协议名(缺段 / 对端段铸不出);`verdict` = 判官的判词。 */
52
+ export interface SessionRuleUnreachableName {
53
+ readonly field: SessionRuleNameRefusal["field"];
54
+ readonly name: string;
55
+ readonly verdict: UnreachableNamespacedRuleName;
56
+ }
57
+ /** 逐桶逐条过协议规则名判官;第一条够不着的交回,全部够得着 ⇒ `undefined`。 */
58
+ export declare function unreachableSessionRuleNameOf(rules: SessionPermissionRules): SessionRuleUnreachableName | undefined;
50
59
  /**
51
- * S-800(client-core [ref] CC-226;server [ref];core [ref])—— 「写得进、跑不了、撤不掉」的工具名。
52
- *
53
- * 两臂,逐字对应 core 7.33.8 准备阶段名审计抛 `config.legacy_tool_name` 的那两臂(判据只用 core **导出**的
54
- * 两件,零镜像表 —— 表随 core 钉版走):
55
- * · `"retired"`:名字在 core 的 `RETIRED_TOOL_NAMES` 里;`guidance` = 那张表给它的一句现役指引(原样);
56
- * · `"unprefixed-namespace"`:名字里有 `"__"` 分隔符,而 `protocolOf(name)` 认不出它属于哪个协议。
57
- *
58
- * 为什么在**写口**判:会话规则店只准非 operator 收紧,删掉一条 deny 算放宽 ⇒ 写进去的这种名字属主自己删不掉,
59
- * 而准备阶段见它就整只拒启 ⇒ 这个会话之后的每一跑都起不来。写口是唯一还来得及说「不」的地方。
60
- *
61
- * ⚠️ **比准备阶段严一格**(方向安全,与 client-core 0.83.6 的 KL-157 同向):准备阶段对**本 run 名册里挂着**的
62
- * 同名工具放行(部署自己挂了一只叫 `Task` 的工具就不审计它),写口看不到将来哪一跑的名册 ⇒ 一律拒。
63
- * 受伤的是「部署自带与退役名同名的工具、又要在会话规则里点它」的形;出路 = 部署级工具策略(不经会话规则店)。
64
- * ⚠️ **过渡形**:core 已表态(#1156)把这两臂抽成一只导出纯函数并在会话规则店写口即拒 —— 到货后本函数
65
- * 换成调 core 那只(拒因原样透传),三份判定收成一份。**S-877 提货复核(core 7.35.0)**:#1156 **未随版**(发车帖 [ref] 逐字
66
- * 「照 #1156 未随本版」);同版新导出的 `rosterToolName` / `ruleNamesOf`(#1194)是**别名归一**(一个名字在名册上落到哪只行的线名 /
67
- * 一次调用可被规则拼成的名字集),不回答「这个名字会不会让准备阶段拒启」,不能替本函数 ⇒ 本函数照留,等 #1156。
68
- *
69
- * 🔴 **为什么不复用 `config.ts` 的 `findUnmatchableToolNames`**(本仓另一只按名字判的单点;读过实现后裁):两只回答的
70
- * 不是同一个问题。那一只问「部署名单里这条名字会不会**永远匹配不上**活工具」—— 它刻意豁免 `allow`(白名单匹配不上 =
71
- * fail-closed 且当场可见),并且拒两段 MCP 名 `mcp__srv`(一名两义)与 core 文法编不出的串。本函数问的是「引擎见到这条
72
- * 会话规则会不会**拒启**」:引擎准备阶段的名审计对 `allow` 桶照样抛(豁免它 = 放过一条砖化路径),对 `mcp__srv` 却
73
- * **不**抛(会话层自 core 5.55 / 5.56 起把它当整对端覆盖形真命中)。判据只能逐字跟 core 那两臂走,不能借另一只的口径。
74
- * 两只在「退役名」「无前缀 `__` 名」两臂上同源(都读 core 导出的同两件),#1156 到货的那一批一并换调 core 的导出函数。
60
+ * SQL 会话规则店写口的同一问(core 判官之后、收紧门之前,与 `assertSessionRuleNamesCurrent` 同点):够不着 ⇒ 抛,一条写语句都不发。
61
+ * 抛的是普通 `Error`(不是 `SessionPolicyError`:core 那张闭集里没有这个词,也不往里塞)—— 本服务的两条 HTTP 写口在读者那一层先拒,
62
+ * 走到这里 = 有一个绕过写口读者的写者,HTTP 上落 500(服务端自身的失误,响亮)。
75
63
  */
76
- export type LegacyToolName = {
77
- why: "retired";
78
- guidance: string;
79
- } | {
80
- why: "unprefixed-namespace";
81
- };
82
- export declare function legacyToolNameOf(name: string): LegacyToolName | undefined;
64
+ export declare function assertSessionRuleNamesReachable(rules: SessionPermissionRules): void;
83
65
  /** 一条规则体的**形**问题。判词而非文案(见文件头注的「文案属主」一段)。 */
84
66
  export type SessionPermissionRulesShapeProblem =
85
67
  /** 整体不是一只普通对象(`null` / 数组 / 标量 / 缺席)。 */
@@ -91,14 +73,17 @@ export type SessionPermissionRulesShapeProblem =
91
73
  kind: "field-not-string-array";
92
74
  field: keyof SessionPermissionRules;
93
75
  };
94
- /** 一条**要写进店**的规则体的问题 = 形问题 ∪ S-800 的名字问题(判据 {@link legacyToolNameOf})。 */
76
+ /** 一条**要写进店**的规则体的问题 = 形问题 ∪ S-800 的名字问题(判官 = core 的 `assertSessionRuleNamesCurrent`)。 */
95
77
  export type SessionPermissionRulesProblem = SessionPermissionRulesShapeProblem
96
- /** S-800:形对,但工具名桶里有一条写进去就跑不了的名字。 */
78
+ /** S-800 / S-1047:形对,但工具名桶里有一条写进去就跑不了的名字;`refusal` = core 判官交回的 detail(原样上 wire)。 */
97
79
  | {
98
80
  kind: "legacy-tool-name";
99
- field: ToolNameRuleField;
100
- name: string;
101
- legacy: LegacyToolName;
81
+ refusal: SessionRuleNameRefusal;
82
+ }
83
+ /** 形对、core 判官也放行,但工具名桶里有一条够不着任何挂载工具的协议名(缺段 / 对端段铸不出)。 */
84
+ | {
85
+ kind: "unreachable-tool-name";
86
+ unreachable: SessionRuleUnreachableName;
102
87
  };
103
88
  /** 两只读者的读数:有问题给判词,没问题给**已收窄**的规则体(调用方零 cast)。`P` = 这只读者会给的判词集。 */
104
89
  export type SessionPermissionRulesRead<P extends SessionPermissionRulesProblem = SessionPermissionRulesProblem> = {
@@ -123,9 +108,9 @@ export declare function readSessionPermissionRules(raw: unknown): SessionPermiss
123
108
  * **写口读者**(S-800)= 形读者 + 名字判官:两条写入腿(`PUT /v1/sessions/:id/policy`、sync/import 的 `policy`
124
109
  * 面经 {@link readSessionRulesRecords})往会话规则店写之前的**唯一**判据。两问,按序:
125
110
  * ① 形(同 {@link readSessionPermissionRules};一份坏形的体答形的 400);
126
- * ② 工具名桶里的每一条过 {@link legacyToolNameOf} —— 写进去之后属主撤不掉、会话之后每一跑都起不来的名字,
127
- * 写口是唯一来得及拒的地方。
128
- * 🔴 **只在写口,不在读回**:SQL 店读回同一行时只判形(形读者)。一条早先写进去的退役名行必须**读得出来** ——
111
+ * ② 工具名桶里的每一条过 core 的判官({@link legacyToolNameRefusalOf})—— 写进去就让会话之后每一跑都起不来的名字,在写口拒;
112
+ * ③ 工具名桶里的每一条过协议规则名判官({@link unreachableSessionRuleNameOf})—— 写进去永远匹配不到任何挂载工具的协议名,在写口拒。
113
+ * 🔴 **只在写口,不在读回**:SQL 店读回同一行时只判形(形读者)。一条早先写进去的退役名行(或够不着的协议名行)必须**读得出来** ——
129
114
  * 那是运营方清掉它的唯一路径(operator 的 PUT 先读写前快照),也是 core 准备阶段报 `config.legacy_tool_name`
130
115
  * 而不是一只 500 `corrupt` 的前提。把判官放进形读者会让读回把它判坏(7.105.0 车QQ 双库套件实抓,修在同批)。
131
116
  */
@@ -178,10 +163,7 @@ export declare function describeSessionPermissionRulesProblem(p: SessionPermissi
178
163
  * 调用点据此发 `rules.legacy_tool_name`,否则发形的码。闭集穷尽两层:记录级判词或桶级判词加一种 ⇒ 编译红,
179
164
  * 新判词不会静默落进某一个码。
180
165
  */
181
- export declare function legacyToolNameProblemOf(p: SessionRulesRecordProblem): {
182
- field: ToolNameRuleField;
183
- name: string;
184
- } | undefined;
166
+ export declare function legacyToolNameProblemOf(p: SessionRulesRecordProblem): SessionRuleNameRefusal | undefined;
185
167
  /** `policy[]` 判词 → 人话(sync/import 腿在它前面拼 `policy: `)。闭集穷尽同上。 */
186
168
  export declare function describeSessionRulesRecordProblem(p: SessionRulesRecordProblem): string;
187
169
  /**
@@ -211,5 +193,4 @@ export declare function sessionPolicyErrorHttp(code: SessionPolicyError["code"])
211
193
  status: number;
212
194
  errorCode: string;
213
195
  };
214
- export {};
215
196
  //# sourceMappingURL=session-policy-wire.d.ts.map
@@ -1,21 +1,41 @@
1
- import { PROTOCOL_TABLE, RETIRED_TOOL_NAMES, protocolOf } from "@sema-agent/core";
2
- const RULE_FIELD_ROLE = {
3
- toolAllow: "tool-name",
4
- toolDeny: "tool-name",
5
- allowDirs: "path",
6
- commandAllow: "command",
7
- commandDeny: "command",
1
+ import { PROTOCOL_TABLE, SessionPolicyError, assertSessionRuleNamesCurrent, judgeRuleToolName } from "@sema-agent/core";
2
+ import { describeUnreachableNamespacedRuleName, unreachableNamespacedRuleNameOf } from "./namespaced-rule-name.js";
3
+ const RULE_FIELDS = {
4
+ toolAllow: true,
5
+ toolDeny: true,
6
+ allowDirs: true,
7
+ commandAllow: true,
8
+ commandDeny: true,
8
9
  };
9
- export const SESSION_PERMISSION_RULE_FIELDS = Object.keys(RULE_FIELD_ROLE);
10
- const TOOL_NAME_RULE_FIELDS = SESSION_PERMISSION_RULE_FIELDS.filter((f) => RULE_FIELD_ROLE[f] === "tool-name");
11
- export function legacyToolNameOf(name) {
12
- const guidance = RETIRED_TOOL_NAMES.get(name);
13
- if (guidance !== undefined)
14
- return { why: "retired", guidance };
15
- if (name.includes("__") && protocolOf(name) === undefined)
16
- return { why: "unprefixed-namespace" };
10
+ export const SESSION_PERMISSION_RULE_FIELDS = Object.keys(RULE_FIELDS);
11
+ function legacyToolNameRefusalOf(rules) {
12
+ try {
13
+ assertSessionRuleNamesCurrent(rules);
14
+ return undefined;
15
+ }
16
+ catch (e) {
17
+ if (e instanceof SessionPolicyError && e.code === "legacy_tool_name" && e.detail !== undefined)
18
+ return e.detail;
19
+ throw e;
20
+ }
21
+ }
22
+ const TOOL_NAME_FIELD_TABLE = { toolAllow: "toolAllow", toolDeny: "toolDeny" };
23
+ const TOOL_NAME_FIELDS = Object.values(TOOL_NAME_FIELD_TABLE);
24
+ export function unreachableSessionRuleNameOf(rules) {
25
+ for (const field of TOOL_NAME_FIELDS) {
26
+ for (const name of rules[field] ?? []) {
27
+ const verdict = unreachableNamespacedRuleNameOf(name);
28
+ if (verdict !== undefined)
29
+ return { field, name, verdict };
30
+ }
31
+ }
17
32
  return undefined;
18
33
  }
34
+ export function assertSessionRuleNamesReachable(rules) {
35
+ const unreachable = unreachableSessionRuleNameOf(rules);
36
+ if (unreachable !== undefined)
37
+ throw new Error(describeUnreachableSessionRuleName(unreachable));
38
+ }
19
39
  function protocolToolNameShapes() {
20
40
  return PROTOCOL_TABLE.map((ns) => `${ns.prefix}<peer>__<tool>`).join(" or ");
21
41
  }
@@ -38,13 +58,12 @@ export function readWritableSessionPermissionRules(raw) {
38
58
  const read = readSessionPermissionRules(raw);
39
59
  if (read.problem !== undefined)
40
60
  return read;
41
- for (const field of TOOL_NAME_RULE_FIELDS) {
42
- for (const name of read.rules[field] ?? []) {
43
- const legacy = legacyToolNameOf(name);
44
- if (legacy !== undefined)
45
- return { problem: { kind: "legacy-tool-name", field, name, legacy } };
46
- }
47
- }
61
+ const refusal = legacyToolNameRefusalOf(read.rules);
62
+ if (refusal !== undefined)
63
+ return { problem: { kind: "legacy-tool-name", refusal } };
64
+ const unreachable = unreachableSessionRuleNameOf(read.rules);
65
+ if (unreachable !== undefined)
66
+ return { problem: { kind: "unreachable-tool-name", unreachable } };
48
67
  return read;
49
68
  }
50
69
  export function readSessionRulesRecords(raw) {
@@ -71,22 +90,29 @@ export function describeSessionPermissionRulesProblem(p) {
71
90
  switch (p.kind) {
72
91
  case "not-an-object": return "rules must be a SessionPermissionRules object";
73
92
  case "field-not-string-array": return `${p.field} must be an array of strings`;
74
- case "legacy-tool-name": return describeLegacyToolName(p.field, p.name, p.legacy);
93
+ case "legacy-tool-name": return describeLegacyToolName(p.refusal);
94
+ case "unreachable-tool-name": return describeUnreachableSessionRuleName(p.unreachable);
75
95
  default: {
76
96
  const unhandled = p;
77
97
  throw new Error(`unhandled SessionPermissionRules problem: ${JSON.stringify(unhandled)}`);
78
98
  }
79
99
  }
80
100
  }
81
- function describeLegacyToolName(field, name, legacy) {
82
- switch (legacy.why) {
83
- case "retired":
84
- return `${field} names "${name}", a retired tool name (${legacy.guidance}); with that rule in place this session could not start another run, so nothing was written — use the current tool name`;
85
- case "unprefixed-namespace":
86
- return `${field} names "${name}", which contains "__" but no protocol prefix; with that rule in place this session could not start another run, so nothing was written — a protocol tool is named ${protocolToolNameShapes()}`;
101
+ function describeUnreachableSessionRuleName(u) {
102
+ return `${u.field} names "${u.name}", which can never match a tool (${describeUnreachableNamespacedRuleName(u.verdict)}), so nothing was written`;
103
+ }
104
+ function describeLegacyToolName(r) {
105
+ switch (r.verdict) {
106
+ case "retired": {
107
+ const v = judgeRuleToolName(r.name, new Set());
108
+ const guidance = v.kind === "retired" ? v.guidance : `renamed "${r.currentName ?? "?"}"`;
109
+ return `${r.field} names "${r.name}", a retired tool name (${guidance}); with that rule in place this session could not start another run, so nothing was written — use the current tool name${r.currentName !== undefined ? ` ("${r.currentName}")` : ""}`;
110
+ }
111
+ case "unprefixed_namespace":
112
+ return `${r.field} names "${r.name}", which contains "__" but no protocol prefix; with that rule in place this session could not start another run, so nothing was written — a protocol tool is named ${protocolToolNameShapes()}`;
87
113
  default: {
88
- const unhandled = legacy;
89
- throw new Error(`unhandled legacy tool name arm: ${JSON.stringify(unhandled)}`);
114
+ const unhandled = r.verdict;
115
+ throw new Error(`unhandled legacy tool name verdict: ${JSON.stringify(unhandled)}`);
90
116
  }
91
117
  }
92
118
  }
@@ -100,9 +126,10 @@ export function legacyToolNameProblemOf(p) {
100
126
  switch (p.problem.kind) {
101
127
  case "not-an-object":
102
128
  case "field-not-string-array":
129
+ case "unreachable-tool-name":
103
130
  return undefined;
104
131
  case "legacy-tool-name":
105
- return { field: p.problem.field, name: p.problem.name };
132
+ return p.problem.refusal;
106
133
  default: {
107
134
  const unhandled = p.problem;
108
135
  throw new Error(`unhandled SessionPermissionRules problem: ${JSON.stringify(unhandled)}`);
@@ -131,6 +158,7 @@ export function sessionPolicyErrorHttp(code) {
131
158
  case "conflict": return { status: 409, errorCode: "conflict" };
132
159
  case "loosen_forbidden": return { status: 403, errorCode: "loosen_forbidden" };
133
160
  case "corrupt": return { status: 500, errorCode: "corrupt" };
161
+ case "legacy_tool_name": return { status: 400, errorCode: "rules.legacy_tool_name" };
134
162
  default: {
135
163
  const unhandled = code;
136
164
  throw new Error(`unhandled SessionPolicyError code: ${JSON.stringify(unhandled)}`);