@sema-agent/server 7.115.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 (116) hide show
  1. package/MIGRATION.md +167 -4
  2. package/USAGE.md +6 -3
  3. package/dist/approval-ask-audit-store.d.ts +25 -6
  4. package/dist/approval-ask-audit-store.js +6 -1
  5. package/dist/approval-card.d.ts +8 -4
  6. package/dist/approval-card.js +2 -0
  7. package/dist/approval-crash-convergence.d.ts +60 -0
  8. package/dist/approval-crash-convergence.js +75 -0
  9. package/dist/approval-deny-reasons.d.ts +13 -0
  10. package/dist/approval-deny-reasons.js +2 -0
  11. package/dist/approval.js +2 -1
  12. package/dist/boot/coordinators.js +0 -12
  13. package/dist/boot/execution-env.js +2 -1
  14. package/dist/boot/resolve-spec.js +1 -0
  15. package/dist/boot/stage-07-capability-layer.js +21 -15
  16. package/dist/boot/stores.js +4 -4
  17. package/dist/capabilities/hands-lane.d.ts +0 -23
  18. package/dist/capabilities/hands-lane.js +3 -2
  19. package/dist/capabilities/send-user-file-tool.d.ts +10 -22
  20. package/dist/capabilities/send-user-file-tool.js +1 -8
  21. package/dist/capability-facts.d.ts +296 -0
  22. package/dist/capability-facts.js +354 -0
  23. package/dist/config-center/apply-effective.js +1 -1
  24. package/dist/config.d.ts +16 -13
  25. package/dist/config.js +12 -12
  26. package/dist/http/admission.d.ts +6 -0
  27. package/dist/http/admission.js +22 -5
  28. package/dist/http/principal-gate.d.ts +3 -0
  29. package/dist/http/principal-gate.js +3 -0
  30. package/dist/http/resume-legs.js +10 -1
  31. package/dist/http/retired-skill-manifest.d.ts +23 -0
  32. package/dist/http/retired-skill-manifest.js +22 -0
  33. package/dist/http/route-table.d.ts +3 -2
  34. package/dist/http/route-table.js +5 -5
  35. package/dist/http/routes/a2a-serve.js +14 -21
  36. package/dist/http/routes/admin-config-refresh.js +7 -2
  37. package/dist/http/routes/admin-drain.js +7 -2
  38. package/dist/http/routes/admin-memory-optout.js +20 -6
  39. package/dist/http/routes/adoption.js +19 -5
  40. package/dist/http/routes/agents-roster.js +4 -2
  41. package/dist/http/routes/approvals-assistant.js +41 -18
  42. package/dist/http/routes/background.d.ts +0 -2
  43. package/dist/http/routes/background.js +4 -5
  44. package/dist/http/routes/capabilities.d.ts +2 -1
  45. package/dist/http/routes/capabilities.js +62 -65
  46. package/dist/http/routes/config-catalog.d.ts +4 -5
  47. package/dist/http/routes/config-catalog.js +16 -2
  48. package/dist/http/routes/devices.js +17 -11
  49. package/dist/http/routes/diagnostics.js +6 -1
  50. package/dist/http/routes/fleet.js +4 -2
  51. package/dist/http/routes/images.js +30 -3
  52. package/dist/http/routes/leader.js +7 -1
  53. package/dist/http/routes/mcp-reconnect.js +17 -10
  54. package/dist/http/routes/memory-bundle.js +20 -6
  55. package/dist/http/routes/memory-compliance.js +19 -5
  56. package/dist/http/routes/memory-consolidation.d.ts +9 -12
  57. package/dist/http/routes/memory-consolidation.js +17 -3
  58. package/dist/http/routes/memory-origin.js +19 -5
  59. package/dist/http/routes/notify-wake.js +5 -3
  60. package/dist/http/routes/observability.js +19 -5
  61. package/dist/http/routes/retention-ops.js +6 -1
  62. package/dist/http/routes/rules.js +49 -21
  63. package/dist/http/routes/run-memory-capture.js +5 -2
  64. package/dist/http/routes/runs.js +90 -60
  65. package/dist/http/routes/session-sync.js +64 -53
  66. package/dist/http/routes/sessions-list.js +8 -5
  67. package/dist/http/routes/sessions.js +39 -24
  68. package/dist/http/routes/shared-memory.js +5 -3
  69. package/dist/http/routes/tasks.js +2 -1
  70. package/dist/http/routes/trace-usage.js +18 -11
  71. package/dist/http/routes/workflows.js +36 -13
  72. package/dist/http/server.js +4 -1
  73. package/dist/http/wire-types.d.ts +6 -0
  74. package/dist/namespaced-rule-name.d.ts +54 -0
  75. package/dist/namespaced-rule-name.js +33 -0
  76. package/dist/observability/corrupt-read-seat.d.ts +4 -3
  77. package/dist/observability/corrupt-read-seat.js +4 -3
  78. package/dist/observability/fail-open.d.ts +5 -1
  79. package/dist/observability/fail-open.js +5 -1
  80. package/dist/observability/permission-rule-events.d.ts +3 -0
  81. package/dist/observability/permission-rule-events.js +4 -0
  82. package/dist/plugins/file-run-store.d.ts +7 -1
  83. package/dist/plugins/file-run-store.js +20 -7
  84. package/dist/plugins/memory-run-store.js +2 -1
  85. package/dist/plugins/one-time-migrations.d.ts +4 -2
  86. package/dist/plugins/one-time-migrations.js +26 -3
  87. package/dist/plugins/pg-session-storage.js +1 -1
  88. package/dist/plugins/run-store-sql.js +4 -4
  89. package/dist/plugins/session-policy-store-sql.js +4 -2
  90. package/dist/plugins/store-backend.d.ts +6 -1
  91. package/dist/plugins/store-backend.js +2 -1
  92. package/dist/plugins/store-contracts.d.ts +27 -8
  93. package/dist/plugins/store-contracts.js +12 -3
  94. package/dist/plugins/tidb-session-store.js +1 -1
  95. package/dist/request-key-closure.js +1 -1
  96. package/dist/run-local.d.ts +37 -3
  97. package/dist/run-local.js +48 -5
  98. package/dist/runs.d.ts +0 -58
  99. package/dist/runs.js +15 -20
  100. package/dist/session-policy-wire.d.ts +40 -60
  101. package/dist/session-policy-wire.js +60 -32
  102. package/dist/spec-fields.d.ts +32 -0
  103. package/dist/spec-fields.js +31 -16
  104. package/dist/standing-answer-grant.d.ts +8 -0
  105. package/dist/standing-answer-grant.js +5 -0
  106. package/dist/task-settings.d.ts +13 -3
  107. package/dist/tool-approval.d.ts +16 -0
  108. package/dist/tool-approval.js +29 -7
  109. package/dist/trace/core-keyset-guard.d.ts +4 -4
  110. package/dist/trace/self-minted-wire.d.ts +42 -0
  111. package/dist/trace/self-minted-wire.js +13 -0
  112. package/dist/user-input-ledger.d.ts +8 -5
  113. package/package.json +4 -2
  114. package/scripts/drain-check-local.mjs +115 -25
  115. package/scripts/drain-check.mysql.sql +67 -0
  116. package/scripts/drain-check.pg.sql +71 -0
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
- import { loosenReasons, SessionPolicyError, normalizeRules, stripRev } from "@sema-agent/core";
3
- import { readSessionPermissionRules, describeSessionPermissionRulesProblem } from "../session-policy-wire.js";
2
+ import { assertSessionRuleNamesCurrent, loosenReasons, SessionPolicyError, normalizeRules, stripRev } from "@sema-agent/core";
3
+ import { assertSessionRuleNamesReachable, readSessionPermissionRules, describeSessionPermissionRulesProblem } from "../session-policy-wire.js";
4
4
  import { decodeJsonColumn, isJsonColumnBodyProblem, jsonColumnText } from "./sql-json-column.js";
5
5
  import { pgHasUnstorable } from "./pg-safe-json.js";
6
6
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
@@ -104,6 +104,8 @@ export class SqlSessionPolicyStore {
104
104
  throw new SessionPolicyError("corrupt", `the session rules rev mark for session ${JSON.stringify(sessionId)} is unreadable (rev ${String(markRows[0].rev)}, gen ${String(markRows[0].gen)}) — refusing rather than stamp a rev/lineage nothing proves`);
105
105
  }
106
106
  const clean = normalizeRules(stripRev(rules));
107
+ assertSessionRuleNamesCurrent(clean);
108
+ assertSessionRuleNamesReachable(clean);
107
109
  if (prior && opts?.operator !== true) {
108
110
  const loosens = loosenReasons(prior, clean);
109
111
  if (loosens.length > 0) {
@@ -1,6 +1,6 @@
1
1
  import type { Pool as MysqlPool } from "mysql2/promise";
2
2
  import type { Pool as PgPool } from "pg";
3
- import type { SessionStore, SessionPolicyStore, FileHistoryStore, WorkflowJournalStore, ToolResultStore, SessionCaptureRecordStore } from "@sema-agent/core";
3
+ import type { SessionStore, SessionPolicyStore, FileHistoryStore, WorkflowJournalStore, ToolResultStore, SessionCaptureRecordStore, UsageWindowStore } from "@sema-agent/core";
4
4
  import { type OutcomeSink } from "./file-outcome-sink.js";
5
5
  import { type InboxWarn } from "./workflow-run-store-sql.js";
6
6
  import { FileRunStore } from "./file-run-store.js";
@@ -355,6 +355,11 @@ export interface StoreBackend {
355
355
  * clock to expose replica clock skew (the write-behind counters bucket on LOCAL floor(now/windowMs); skew
356
356
  * splits a fleet window into disjoint buckets = soft-limit leak). OPTIONAL: local backend omits (no fleet). */
357
357
  dbNowMs?(): Promise<number>;
358
+ /** S-1014(core 7.41.0 #1308):**local 后端专有**的 usage-window 账本店 = core `FileStorageBackend` 自己那一只(`usageWindowStore`)——
359
+ * 它的坏读披露座是 backend 构造时那**一只** `onCorruptRead`(core:「`FileStorageBackend` forwards its one sink」),所以坏账本拒(`usage_window.store_corrupt`,
360
+ * 拒绝句不带盘上位置)的同时,路径进本 backend 的 `file_store_corrupt_read` 披露座(逐命中 warn)。修前 `boot/stores.ts` 在同一数据根上自己
361
+ * `new FileUsageWindowStore(root)` 第二只实例、不接座 ⇒ 坏值只拒不披露,运维找不到是哪只文件。SQL 后端省略(它们有自己的 usage-window 双方言店)。 */
362
+ fileUsageWindowStore?(): UsageWindowStore;
358
363
  }
359
364
  /** (c)(clay 拍 a+c,2026-07-27;[ref] 换店后域=file_history_blob,门与旋钮名不变)云形 blob 姿势门——
360
365
  * boot 时调用(boot/stores.ts),纯函数可单测。SQL 后端(mysql|pg)把 blob 字节落单行的形已被两役
@@ -265,7 +265,7 @@ class LocalBackend {
265
265
  }
266
266
  async ensureSchema() { }
267
267
  async runOneTimeMigrations() {
268
- return { ...NO_SQL_MIGRATIONS, askOriginRowsMigrated: await this.runStore.migrateRetiredAskOriginRows() };
268
+ return { ...NO_SQL_MIGRATIONS, askOriginRowsMigrated: await this.runStore.migrateRetiredAskOriginRows(), holderDeathCodeRowsBackfilled: this.runStore.migrateLegacyHolderDeathCodes() };
269
269
  }
270
270
  async close() {
271
271
  await this.sessionStore.dispose();
@@ -315,6 +315,7 @@ class LocalBackend {
315
315
  }
316
316
  checkpointStoreInst;
317
317
  sessionPolicy() { return this.fileBackend.sessionPolicyStore; }
318
+ fileUsageWindowStore() { return this.fileBackend.usageWindowStore; }
318
319
  fileHistory() { return this.fileBackend.fileHistoryStore; }
319
320
  memoryOptOutGrant() { return undefined; }
320
321
  sessionCaptureRecords() { return this.captureRecordsSource; }
@@ -140,7 +140,7 @@ export type PersistedTaskResult = TaskResult & PersistedResultSideKeys;
140
140
  /**
141
141
  * 🔴 run 店「持有它的实例丢了」那一判的**行上落款**(S-928 codex r1-F1 提为常量):三只 run 店的 `reapStale` 把心跳过期的 running 行翻成
142
142
  * `failed` 并写下这一句。它是 run 店死亡判决留在行上的证据之一(另一只 = {@link RUN_ORPHANED_ERROR})—— 用户输入受理账本的收敛判词
143
- * (S-969 L8 起两车道同一只)据 {@link isHolderDeathVerdict} 在 reap **之后**仍能判出「写者死了」。改这一句 = 改那条判据;所以三个写者与那一个读者都只引用本常量。读时折叠成同一句话的 wire 展示站点
143
+ * (S-969 L8 起两车道同一只)据 {@link isHolderDeathVerdict} 在 reap **之后**仍能判出「写者死了」—— 7.116.0 起判据是同行的机读码 `run.stalled`({@link RUN_HOLDER_DEATH_CODES}),本句只给人读;三个写者同写句子与码。读时折叠成同一句话的 wire 展示站点
144
144
  * (`routes/runs.ts` 的 stale 折叠、`routes/trace-usage.ts` 的 WORKER_DOWN 帧)刻意留字面量:它们由文案冻结门逐字钉着,且不参与任何判据。
145
145
  */
146
146
  export declare const RUN_STALLED_ERROR = "run stalled (instance lost?)";
@@ -148,19 +148,38 @@ export declare const RUN_STALLED_ERROR = "run stalled (instance lost?)";
148
148
  * 🔴 File 车道「持有它的进程随重启没了」那一判的行上落款(S-969 L8 提为常量):boot 孤儿回收(`FileRunStore.reclaimOrphanedAtBoot`,
149
149
  * 数据根 LOCK 作证前世进程已死 ⇒ 仍 running 的行无条件判孤儿)与运行期 claim 接管的 running 臂(`judgeClaim`,外来 + owner 已死 + 无可续)
150
150
  * 都写这一句;接管臂在括号里附上前任持有者的进程指纹(取证用,形 = {@link buildRunOrphanedError})。与 {@link RUN_STALLED_ERROR} 同一种身份:
151
- * run 店写在行上的**死亡判决**,读者 = {@link isHolderDeathVerdict}。
151
+ * run 店写在行上的**死亡判决**,读者 = {@link isHolderDeathVerdict}(按同行的机读码 `run.orphaned` 判,本句只给人读)。
152
152
  */
153
153
  export declare const RUN_ORPHANED_ERROR = "run orphaned by engine restart";
154
154
  /** File 车道两个孤儿落款写口共用的那一句(`heldBy` = 接管臂的前任持有者描述;boot 回收臂不带)。 */
155
155
  export declare function buildRunOrphanedError(heldBy?: string): string;
156
156
  /**
157
- * run 行上是不是一份**持有者死亡判决**:`failed`,且 `error` 是 run 店的两只死亡落款之一 —— {@link RUN_STALLED_ERROR}(三只 run 店的
158
- * `reapStale`:心跳过期 ⇒ 实例丢了)或 {@link RUN_ORPHANED_ERROR}(File 车道:前世进程随重启没了;接管臂带括注)。判决只说「持有它的那个进程
159
- * 死在这条腿收场之前」,不说是谁 —— 持有者是谁读行上的 `instanceId`(两只落款都不改它)。唯一读者 = 用户输入受理账本的收敛判词
160
- * (`src/user-input-ledger.ts` `judgeOpen`);改任一句落款 = 改那条判据,所以写者与读者都只引用本模块的常量。
161
- * 正常失败(别的 error)/ park 过期收割(`approval expired before decision`:腿 park 过)都**不是**死亡判决。
157
+ * run 店的两只**死亡判决机读码**(行上 `errorCode` 列;`GET /v1/runs/:id` 的 `errorCode` 读它)—— 与两句死亡落款一一对应:
158
+ * {@link RUN_STALLED_ERROR} ⇔ `run.stalled`(三只 run 店的 `reapStale`:心跳过期 ⇒ 实例丢了)、{@link RUN_ORPHANED_ERROR}(含接管臂的括注形)⇔
159
+ * `run.orphaned`(File 车道:前世进程随重启没了)。码是判据,句子只给人读。族 `run.` = run 店自己写在行上的生命周期判决(core 不铸这一族)。
162
160
  */
163
- export declare function isHolderDeathVerdict(run: Pick<RunRecord, "status" | "error">): boolean;
161
+ export declare const RUN_HOLDER_DEATH_CODES: {
162
+ readonly stalled: "run.stalled";
163
+ readonly orphaned: "run.orphaned";
164
+ };
165
+ export type RunHolderDeathCode = (typeof RUN_HOLDER_DEATH_CODES)[keyof typeof RUN_HOLDER_DEATH_CODES];
166
+ /**
167
+ * 一次性迁移 **M6** 的判据(窗、删法与删后谁接住:窗口见 `plugins/one-time-migrations.ts` 登记表):7.116.0 之前写下的死亡判决行只有句子、码列为空 ——
168
+ * 该行应有的码。两处用:local 腿的起服写回(`FileRunStore.migrateLegacyHolderDeathCodes`)与 SQL 两孪生行映射里的**读侧半场**(滚动升级窗里旧副本
169
+ * 还会写这种行,起服写回覆盖不到;本函数随 M6 一起删)。只认两句落款的原字节(接管臂括注形按前缀认);码列已有值 / 非 `failed` / 别的句子 ⇒ 原样。
170
+ */
171
+ export declare function readLegacyHolderDeathCode(row: {
172
+ readonly status: string;
173
+ readonly error: string | null;
174
+ readonly errorCode: string | null;
175
+ }): string | null;
176
+ /**
177
+ * run 行上是不是一份**持有者死亡判决**:`failed`,且行上的码是 {@link RUN_HOLDER_DEATH_CODES} 之一。判决只说「持有它的那个进程死在这条腿收场之前」,
178
+ * 不说是谁 —— 持有者是谁读行上的 `instanceId`(判决不改它)。读者 = 用户输入受理账本的收敛判词(`src/user-input-ledger.ts` `judgeOpen`)。
179
+ * 旧行(只有句子)由一次性迁移 M6 补上码(起服写回 + SQL 滚动窗的读侧半场,{@link readLegacyHolderDeathCode}),所以这里不再按句子判。
180
+ * 正常失败(别的码 / 无码)/ park 过期收割(`approval.expired`:腿 park 过)都**不是**死亡判决。
181
+ */
182
+ export declare function isHolderDeathVerdict(run: Pick<RunRecord, "status" | "errorCode">): boolean;
164
183
  export interface RunRecord {
165
184
  taskId: string;
166
185
  sessionId: string;
@@ -6,10 +6,19 @@ export const RUN_ORPHANED_ERROR = "run orphaned by engine restart";
6
6
  export function buildRunOrphanedError(heldBy) {
7
7
  return heldBy === undefined ? RUN_ORPHANED_ERROR : `${RUN_ORPHANED_ERROR} (claim held by ${heldBy}, which is gone)`;
8
8
  }
9
+ export const RUN_HOLDER_DEATH_CODES = { stalled: "run.stalled", orphaned: "run.orphaned" };
10
+ const RUN_HOLDER_DEATH_CODE_WORDS = new Set(Object.values(RUN_HOLDER_DEATH_CODES));
11
+ export function readLegacyHolderDeathCode(row) {
12
+ if (row.errorCode !== null || row.status !== "failed" || row.error === null)
13
+ return row.errorCode;
14
+ if (row.error === RUN_STALLED_ERROR)
15
+ return RUN_HOLDER_DEATH_CODES.stalled;
16
+ if (row.error === RUN_ORPHANED_ERROR || row.error.startsWith(`${RUN_ORPHANED_ERROR} (`))
17
+ return RUN_HOLDER_DEATH_CODES.orphaned;
18
+ return null;
19
+ }
9
20
  export function isHolderDeathVerdict(run) {
10
- if (run.status !== "failed" || run.error === null)
11
- return false;
12
- return run.error === RUN_STALLED_ERROR || run.error === RUN_ORPHANED_ERROR || run.error.startsWith(`${RUN_ORPHANED_ERROR} (`);
21
+ return run.status === "failed" && run.errorCode !== null && RUN_HOLDER_DEATH_CODE_WORDS.has(run.errorCode);
13
22
  }
14
23
  export const RUN_STATUS_CLASS = {
15
24
  running: "live",
@@ -141,7 +141,7 @@ export class TiDBSessionStore {
141
141
  async listSessions(opts) {
142
142
  const params = [];
143
143
  const where = ["sm.placement_kind IS NULL"];
144
- if (opts.owner) {
144
+ if (opts.owner !== undefined) {
145
145
  where.push(opts.includeUnowned ? "(sm.owner = ? OR sm.owner IS NULL)" : "sm.owner = ?");
146
146
  params.push(opts.owner);
147
147
  }
@@ -28,7 +28,7 @@ const TASK_BODY_DECLARED_KEYS = [
28
28
  "resilience", "restoreFiles", "resumeAt", "resumeAtMode", "retainBackgroundProcesses", "retainSubagentSessions",
29
29
  "rewindFiles", "rewindFilesTo", "sandboxImageProfile", "scenario", "scratchpadDir", "selfOrchestration",
30
30
  "sessionId", "settings", "skills", "suggestNextPrompts", "systemPrompt", "taskId", "toolMaterializeStrategy",
31
- "verify", "verifyRounds",
31
+ "userInvokedSkills", "verify", "verifyRounds",
32
32
  ];
33
33
  const TASK_BODY_SCENARIO_KEYS = ["businessContext", "lenses", "repo", "rounds", "team"];
34
34
  const ACCEPTED_BODY = new Set([...TASK_BODY_DECLARED_KEYS, ...TASK_BODY_SCENARIO_KEYS]);
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { type AskRequest, type Brain, type TaskSpec } from "@sema-agent/core";
2
+ import { type AskClass, type AskRequest, type Brain, type TaskSpec } from "@sema-agent/core";
3
3
  import { type UnattendedPolicyRefusal } from "./tool-approval.js";
4
4
  import { type ApprovalBaselineConfigView, type DeploymentGovernanceConfigView } from "./deployment-governance.js";
5
5
  import { type ServiceConfig } from "./config.js";
@@ -131,8 +131,10 @@ export interface LocalApproverIo {
131
131
  *
132
132
  * 🔴 S-694 起本席也服**三张审批名单**产的 ask(审批基线取 live 席,见 {@link RUN_LOCAL_APPROVAL_SEAT}):
133
133
  * `APPROVAL_REQUIRE` / `APPROVAL_NEVER_AUTO` 命中 ⇒ 策略 `ask` ⇒ 落到这里。`AskRequest` 不带是哪条声明产的
134
- * ask(core 把 `matchedAskRule` 刻意留在闸内、不上请求),所以无 TTY 拒句**列出**可能产门的全部旋钮,
135
- * `APPROVAL_DENY` 也点名(它当场拒、不经本席 —— 运维排查「为什么这只工具不跑」时要在同一句里看到它)。
134
+ * ask(core 把 `matchedAskRule` 刻意留在闸内、不上请求),但带这一问的**类**(`classes`,core 7.37.0 起)。
135
+ * 🔴 无 TTY 拒句的出路**按这一问的类说话**({@link NO_TTY_ASK_CLASS_ROW} 一张闭集表:每类 ⇒ 一种出路):没人表态的服务器工具调用指 `APPROVAL_GRANT`,
136
+ * 写下的 ask 名单指名单旋钮(`APPROVAL_DENY` 也点名 —— 它当场拒、不经本席),部署强制要人答的问(`operator_forced`)列「one of」能铸它的来源、shell 地板(`shell_floor`)另一句只列铸它的旋钮,其余各类**不点任何旋钮**、只说去终端答;
137
+ * 类缺席 ⇒ 说实话(「引擎没有说明这一问的类」),不回落成全量旋钮清单(那句对多数类是错的出路)。
136
138
  *
137
139
  * 🔴 非 TTY 臂答**拒**(真 deny),**绝不答 `"unavailable"`**:后者被 core 读成「席位够不到」并**改道
138
140
  * `suspendAsk`**(dist `core/hooks.js` 的 `approverUnavailable` 重投)。今天本腿无店 ⇒ 那条改道无处
@@ -149,6 +151,37 @@ export interface LocalApproverIo {
149
151
  /** 本席的形:core `OnAsk` 的函数臂(`"allow"` / `"deny"` 两个字面臂本席从不用)—— 人的答是布尔,没有人被问的拒是部署政策拒对象;
150
152
  * 结构上可赋给 `OnAsk`。 */
151
153
  export type LocalApprover = (req: AskRequest, signal?: AbortSignal) => Promise<boolean | UnattendedPolicyRefusal>;
154
+ /**
155
+ * 无 TTY 拒句里**出路**的种类(闭集)。每一类问对应一种({@link NO_TTY_ASK_CLASS_ROW});`terminal_only` = 没有本部署的旋钮能放行这一问,只剩去终端答。
156
+ * 说出口的三种按 {@link NO_TTY_SPOKEN_REMEDIES} 的顺序各说一次(一问可以折叠多类 ⇒ 多种出路,顺序恒定)。
157
+ */
158
+ declare const NO_TTY_SPOKEN_REMEDIES: readonly ["grant", "approval_lists", "deployment_mandate", "shell_gate"];
159
+ type NoTtySpokenRemedy = (typeof NO_TTY_SPOKEN_REMEDIES)[number];
160
+ type NoTtyRemedy = NoTtySpokenRemedy | "terminal_only";
161
+ /**
162
+ * 一类问 ⇒ 这条腿上它的出路。**穷尽表**:core 的 `ASK_CLASSES` 加一类 ⇒ 这里 tsc 红(`Record<AskClass, …>` 少一键),新类必须在这里表态,
163
+ * 不会悄悄落成「没有出路」。本仓知道而 core 不知道的只有一件:**哪只部署旋钮会产哪一类问**(逐类的产地见各行):
164
+ * · `unstated`:没有任何层表态的服务器工具调用(core 7.40.0)—— 运维为自己接的服务器表态的地方是 `APPROVAL_GRANT`(`run-local` 没有请求体,写不了 `settings.permissions.allow`);
165
+ * · `ask_rule`:写下的 ask 规则 —— `APPROVAL_REQUIRE` / `APPROVAL_NEVER_AUTO`(`operatorAsk`)与治理层的 commandPolicy ask 名单(同一只铸点);请求体的
166
+ * `settings.permissions.ask` 到不了这条腿(本文件不 import 任何请求受理 / `task-settings`,`run-local` 的 spec 自铸);
167
+ * · `operator_forced`:部署**强制**要人答的问 —— 能铸它的来源(真 Runner × `applyLocalGovernance` 亲读类集,读数在 `test/run-local.test.ts` S-1100 ⑨ 各格):`APPROVAL_NEVER_AUTO`
168
+ * (`operatorAlwaysHumanAsk` 盖部署出身;类集 `[ask_rule, operator_forced]`)与 `runtime.commandPolicy`(ask 条目 ⇒ `[ask_rule, operator_forced]`,同一只铸点;allowlist 形里没列的命令 ⇒ `[operator_forced]`)。
169
+ * 读码补全(未亲跑):耐久资源耗尽的「恒问人」同走 `operatorAlwaysHumanAsk`、SUP 路由的 capability-danger 门同走它 —— 两者都要 checkpoint 店 / 路由器,run-local 没有;本仓没有给 `spec.tools` 盖部署工具标记的旋钮
170
+ * (grep `egress: true` / `irreversibility` 字面量零命中),core 的 A2A 隔离对端形本仓也不设。所以「one of」两项就是这条腿上能铸它的全部来源;
171
+ * · `shell_floor`:shell 地板被抬到 always —— `AUTONOMY=ask`(`autonomyOverrides`)与 `MANUAL_MODE_SHELL_GATE=always`,真 Runner 读数两者的类集都是 `[shell_floor]`(**不是** `operator_forced`):
172
+ * 单独一句,不与 `operator_forced` 的来源混在一起 —— 混说会把 `APPROVAL_NEVER_AUTO` 的问指到一定没设的 `AUTONOMY`;
173
+ * · 其余:引擎自己的问(写门 / 形状层 / 写保护 / 探针 / 钩子 / 组织规则 / 引擎的守卫 / 工具标记 …)没有一只本部署旋钮能放行 —— 只说去终端答。
174
+ * 不借 core 的 `headlessApprovalRemedy`:那一句是给**库的调用方**的出路(「wire an approver (`onAsk`)」「run with `permissionMode: "bypassPermissions"`」),
175
+ * 而本腿的席**已经接好**(缺的是终端),也没有任何命令行旗给 `permissionMode` —— 照搬会把运维指向一个不存在的旋钮。
176
+ *
177
+ * `autoBudgetReleases`:`APPROVAL_AUTO_BUDGET` 能放行这类问吗(读 `createDurableAskPolicy` 的预算臂:只放行**没进 `APPROVAL_NEVER_AUTO`、耐久资源没耗尽**的
178
+ * `APPROVAL_REQUIRE` 命中,换成一句带理由的 allow)。按 `classes` **整体**读:集里每一类都放得行才建议预算 —— 折叠了别的类(`operator_forced` 等)说明另一层的问仍在,
179
+ * 预算放不掉它(与 {@link unattendedAnswers} 读整体同一律)。
180
+ */
181
+ export declare const NO_TTY_ASK_CLASS_ROW: Readonly<Record<AskClass, {
182
+ readonly remedy: NoTtyRemedy;
183
+ readonly autoBudgetReleases: boolean;
184
+ }>>;
152
185
  export declare function createLocalApprover(io: LocalApproverIo): LocalApprover;
153
186
  export declare function promptYesNoOnTty(prompt: string, signal?: AbortSignal, streams?: {
154
187
  input: NodeJS.ReadableStream;
@@ -176,4 +209,5 @@ export declare function installRunLocalExitBoundary(deps: {
176
209
  logger: Logger;
177
210
  exit?: (code: number) => void;
178
211
  }): RunLocalExitBoundary;
212
+ export {};
179
213
  //# sourceMappingURL=run-local.d.ts.map
package/dist/run-local.js CHANGED
@@ -8,6 +8,7 @@ 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";
@@ -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);
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;
@@ -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,54 +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` 的 `unmatchableToolNamesRefusal`**(本仓另一只按名字判的单点;读过实现后裁):两只回答的
70
- * 不是同一个问题。那一只问「这张表里这条条目会不会**永远不生效**」(匹配不上活工具,或编不成一条规则)—— 7.115.0 起它对 `allow` 也照判
71
- * (修前豁免 `allow`:那时 allow 是白名单,匹配不上 = fail-closed 且当场可见;allow 改成 allow 规则后豁免臂已删),并且拒两段 MCP 名
72
- * `mcp__srv`(一名两义)与 core 文法编不出的串。本函数问的是「引擎见到这条会话规则会不会**拒启**」:引擎准备阶段的名审计对会话规则的
73
- * `allow` 桶照样抛,对 `mcp__srv` 却
74
- * **不**抛(会话层自 core 5.55 / 5.56 起把它当整对端覆盖形真命中)。判据只能逐字跟 core 那两臂走,不能借另一只的口径。
75
- * 两只在「退役名」「无前缀 `__` 名」两臂上同源(都读 core 导出的同两件),#1156 到货的那一批一并换调 core 的导出函数。
60
+ * SQL 会话规则店写口的同一问(core 判官之后、收紧门之前,与 `assertSessionRuleNamesCurrent` 同点):够不着 ⇒ 抛,一条写语句都不发。
61
+ * 抛的是普通 `Error`(不是 `SessionPolicyError`:core 那张闭集里没有这个词,也不往里塞)—— 本服务的两条 HTTP 写口在读者那一层先拒,
62
+ * 走到这里 = 有一个绕过写口读者的写者,HTTP 上落 500(服务端自身的失误,响亮)。
76
63
  */
77
- export type LegacyToolName = {
78
- why: "retired";
79
- guidance: string;
80
- } | {
81
- why: "unprefixed-namespace";
82
- };
83
- export declare function legacyToolNameOf(name: string): LegacyToolName | undefined;
64
+ export declare function assertSessionRuleNamesReachable(rules: SessionPermissionRules): void;
84
65
  /** 一条规则体的**形**问题。判词而非文案(见文件头注的「文案属主」一段)。 */
85
66
  export type SessionPermissionRulesShapeProblem =
86
67
  /** 整体不是一只普通对象(`null` / 数组 / 标量 / 缺席)。 */
@@ -92,14 +73,17 @@ export type SessionPermissionRulesShapeProblem =
92
73
  kind: "field-not-string-array";
93
74
  field: keyof SessionPermissionRules;
94
75
  };
95
- /** 一条**要写进店**的规则体的问题 = 形问题 ∪ S-800 的名字问题(判据 {@link legacyToolNameOf})。 */
76
+ /** 一条**要写进店**的规则体的问题 = 形问题 ∪ S-800 的名字问题(判官 = core 的 `assertSessionRuleNamesCurrent`)。 */
96
77
  export type SessionPermissionRulesProblem = SessionPermissionRulesShapeProblem
97
- /** S-800:形对,但工具名桶里有一条写进去就跑不了的名字。 */
78
+ /** S-800 / S-1047:形对,但工具名桶里有一条写进去就跑不了的名字;`refusal` = core 判官交回的 detail(原样上 wire)。 */
98
79
  | {
99
80
  kind: "legacy-tool-name";
100
- field: ToolNameRuleField;
101
- name: string;
102
- legacy: LegacyToolName;
81
+ refusal: SessionRuleNameRefusal;
82
+ }
83
+ /** 形对、core 判官也放行,但工具名桶里有一条够不着任何挂载工具的协议名(缺段 / 对端段铸不出)。 */
84
+ | {
85
+ kind: "unreachable-tool-name";
86
+ unreachable: SessionRuleUnreachableName;
103
87
  };
104
88
  /** 两只读者的读数:有问题给判词,没问题给**已收窄**的规则体(调用方零 cast)。`P` = 这只读者会给的判词集。 */
105
89
  export type SessionPermissionRulesRead<P extends SessionPermissionRulesProblem = SessionPermissionRulesProblem> = {
@@ -124,9 +108,9 @@ export declare function readSessionPermissionRules(raw: unknown): SessionPermiss
124
108
  * **写口读者**(S-800)= 形读者 + 名字判官:两条写入腿(`PUT /v1/sessions/:id/policy`、sync/import 的 `policy`
125
109
  * 面经 {@link readSessionRulesRecords})往会话规则店写之前的**唯一**判据。两问,按序:
126
110
  * ① 形(同 {@link readSessionPermissionRules};一份坏形的体答形的 400);
127
- * ② 工具名桶里的每一条过 {@link legacyToolNameOf} —— 写进去之后属主撤不掉、会话之后每一跑都起不来的名字,
128
- * 写口是唯一来得及拒的地方。
129
- * 🔴 **只在写口,不在读回**:SQL 店读回同一行时只判形(形读者)。一条早先写进去的退役名行必须**读得出来** ——
111
+ * ② 工具名桶里的每一条过 core 的判官({@link legacyToolNameRefusalOf})—— 写进去就让会话之后每一跑都起不来的名字,在写口拒;
112
+ * ③ 工具名桶里的每一条过协议规则名判官({@link unreachableSessionRuleNameOf})—— 写进去永远匹配不到任何挂载工具的协议名,在写口拒。
113
+ * 🔴 **只在写口,不在读回**:SQL 店读回同一行时只判形(形读者)。一条早先写进去的退役名行(或够不着的协议名行)必须**读得出来** ——
130
114
  * 那是运营方清掉它的唯一路径(operator 的 PUT 先读写前快照),也是 core 准备阶段报 `config.legacy_tool_name`
131
115
  * 而不是一只 500 `corrupt` 的前提。把判官放进形读者会让读回把它判坏(7.105.0 车QQ 双库套件实抓,修在同批)。
132
116
  */
@@ -179,10 +163,7 @@ export declare function describeSessionPermissionRulesProblem(p: SessionPermissi
179
163
  * 调用点据此发 `rules.legacy_tool_name`,否则发形的码。闭集穷尽两层:记录级判词或桶级判词加一种 ⇒ 编译红,
180
164
  * 新判词不会静默落进某一个码。
181
165
  */
182
- export declare function legacyToolNameProblemOf(p: SessionRulesRecordProblem): {
183
- field: ToolNameRuleField;
184
- name: string;
185
- } | undefined;
166
+ export declare function legacyToolNameProblemOf(p: SessionRulesRecordProblem): SessionRuleNameRefusal | undefined;
186
167
  /** `policy[]` 判词 → 人话(sync/import 腿在它前面拼 `policy: `)。闭集穷尽同上。 */
187
168
  export declare function describeSessionRulesRecordProblem(p: SessionRulesRecordProblem): string;
188
169
  /**
@@ -212,5 +193,4 @@ export declare function sessionPolicyErrorHttp(code: SessionPolicyError["code"])
212
193
  status: number;
213
194
  errorCode: string;
214
195
  };
215
- export {};
216
196
  //# sourceMappingURL=session-policy-wire.d.ts.map