@sema-agent/server 7.118.0 → 7.119.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 (101) hide show
  1. package/MIGRATION.md +85 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/USAGE.md +24 -13
  5. package/deploy/sema-up/README.md +2 -0
  6. package/deploy/sema-up/chart/templates/minio.yaml +2 -1
  7. package/deploy/sema-up/chart/templates/server-v4pro.yaml +1 -0
  8. package/deploy/sema-up/chart/templates/server.yaml +1 -0
  9. package/deploy/sema-up/chart/values.yaml +7 -1
  10. package/deploy/sema-up/cluster-up.sh +20 -8
  11. package/deploy/sema-up/kube-up.sh +29 -16
  12. package/deploy/sema-up/lib/buckets.sh +21 -0
  13. package/deploy/sema-up/lib/health.sh +33 -0
  14. package/deploy/sema-up/sema-up.sh +14 -9
  15. package/deploy/sema-up/smoke.sh +16 -6
  16. package/deploy/sema-up/stack/compose.yaml +1 -1
  17. package/deploy/sema-up/stack/sema-stack-cluster.yaml +4 -1
  18. package/dist/approval-ask-audit-store.js +2 -7
  19. package/dist/approval-card.d.ts +5 -5
  20. package/dist/approval.d.ts +51 -5
  21. package/dist/approval.js +76 -13
  22. package/dist/audit.d.ts +12 -1
  23. package/dist/audit.js +62 -2
  24. package/dist/auto-mode-face.d.ts +2 -3
  25. package/dist/bake-runner/main.js +1 -6
  26. package/dist/bake-runner/protocol.d.ts +18 -0
  27. package/dist/bake-runner/protocol.js +22 -2
  28. package/dist/bake-runner/runner.d.ts +12 -8
  29. package/dist/bake-runner/runner.js +5 -9
  30. package/dist/boot/config-center.js +1 -1
  31. package/dist/boot/coordinators.js +4 -1
  32. package/dist/boot/device-lane.d.ts +3 -3
  33. package/dist/boot/device-lane.js +1 -2
  34. package/dist/boot/leader.d.ts +4 -4
  35. package/dist/boot/leader.js +1 -2
  36. package/dist/boot/parked-revive-gate.d.ts +5 -1
  37. package/dist/boot/parked-revive-gate.js +8 -2
  38. package/dist/boot/resolve-spec.js +7 -4
  39. package/dist/boot/stage-07-capability-layer.js +5 -1
  40. package/dist/boot/stage-10-http-server.js +3 -2
  41. package/dist/boot/stores.js +12 -7
  42. package/dist/capabilities/center-plugins.d.ts +2 -0
  43. package/dist/capabilities/center-plugins.js +24 -6
  44. package/dist/capabilities/code-review-council.js +8 -11
  45. package/dist/capabilities/scenarios.d.ts +34 -7
  46. package/dist/capabilities/scenarios.js +33 -6
  47. package/dist/config-catalog.js +3 -3
  48. package/dist/config.js +2 -2
  49. package/dist/deployment-governance.d.ts +5 -2
  50. package/dist/deployment-governance.js +4 -3
  51. package/dist/host-decision.d.ts +52 -27
  52. package/dist/host-decision.js +14 -7
  53. package/dist/http/admission.js +6 -6
  54. package/dist/http/resume-legs.js +2 -1
  55. package/dist/http/routes/approvals-assistant.d.ts +25 -3
  56. package/dist/http/routes/approvals-assistant.js +9 -5
  57. package/dist/http/routes/capabilities.js +4 -3
  58. package/dist/http/routes/memory-origin.d.ts +1 -1
  59. package/dist/http/routes/runs.js +11 -5
  60. package/dist/http/routes/sessions.js +20 -16
  61. package/dist/http/wire-types.d.ts +12 -13
  62. package/dist/images/manifest.d.ts +7 -1
  63. package/dist/images/manifest.js +18 -16
  64. package/dist/memory-store-refusal.d.ts +7 -12
  65. package/dist/memory-store-refusal.js +4 -5
  66. package/dist/namespaced-rule-name.d.ts +4 -3
  67. package/dist/observability/fail-open.d.ts +5 -1
  68. package/dist/observability/fail-open.js +5 -1
  69. package/dist/operator-ask.d.ts +6 -5
  70. package/dist/plugins/approval-exemption-store.d.ts +4 -0
  71. package/dist/plugins/approval-exemption-store.js +1 -0
  72. package/dist/plugins/blob-backend.d.ts +13 -0
  73. package/dist/plugins/blob-backend.js +26 -0
  74. package/dist/plugins/local-session-store.d.ts +78 -47
  75. package/dist/plugins/local-session-store.js +152 -122
  76. package/dist/plugins/one-time-migrations.d.ts +6 -0
  77. package/dist/plugins/one-time-migrations.js +19 -1
  78. package/dist/plugins/snapshot-bucket-presence.d.ts +36 -0
  79. package/dist/plugins/snapshot-bucket-presence.js +40 -0
  80. package/dist/plugins/store-backend.d.ts +17 -6
  81. package/dist/plugins/store-backend.js +46 -16
  82. package/dist/plugins/store-open-wait.d.ts +33 -0
  83. package/dist/plugins/store-open-wait.js +73 -0
  84. package/dist/rule-entry-list.js +1 -1
  85. package/dist/run-local.js +4 -3
  86. package/dist/security.d.ts +5 -3
  87. package/dist/security.js +4 -0
  88. package/dist/session-policy-wire.js +7 -12
  89. package/dist/spec-fields.d.ts +6 -2
  90. package/dist/store-posture.d.ts +14 -6
  91. package/dist/store-posture.js +7 -4
  92. package/dist/task-settings.d.ts +27 -55
  93. package/dist/task-settings.js +6 -18
  94. package/dist/tool-approval.d.ts +97 -43
  95. package/dist/tool-approval.js +120 -31
  96. package/dist/trace/redact.d.ts +6 -0
  97. package/dist/trace/redact.js +9 -9
  98. package/dist/trace/self-minted-wire.d.ts +1 -1
  99. package/dist/user-input-ledger.d.ts +19 -15
  100. package/dist/user-input-ledger.js +12 -20
  101. package/package.json +2 -2
@@ -1,5 +1,5 @@
1
1
  import { CODE_AGENT_PROMPT, CODE_SYSTEM_PROMPT, DEFAULT_SYSTEM_PROMPT, WEB_FETCH_TOOL_NAME, assembleCodeTools, defaultTaskRegistry } from "@sema-agent/core";
2
- import { HttpError } from "../security.js";
2
+ import { HttpError, createCodedHttpError } from "../security.js";
3
3
  import { createSubagentExtraTools, nowTool } from "./builtin-tools.js";
4
4
  import { parseRepo, repoToolsFor } from "./repo-tools.js";
5
5
  import { createCouncilTool, clampRounds } from "./code-review-council.js";
@@ -120,8 +120,12 @@ export function buildScenarios(deps) {
120
120
  };
121
121
  }
122
122
  export function selectScenario(scenarios, name) {
123
- const resolved = name;
124
- return (resolved && Object.hasOwn(scenarios, resolved) && scenarios[resolved]) || scenarios.default;
123
+ if (name === undefined)
124
+ return scenarios.default;
125
+ const picked = Object.hasOwn(scenarios, name) ? scenarios[name] : undefined;
126
+ if (picked === undefined)
127
+ throw new Error(`scenario "${name}" is not registered — resolve a request's scenario through gateScenarioRequest, which only returns registered names`);
128
+ return picked;
125
129
  }
126
130
  function availabilityFields(deps, toolset) {
127
131
  const verdict = scenarioAvailability(deps, toolset);
@@ -295,11 +299,28 @@ export function mergeUserSkills(scenarioSkills, userSkills, logger) {
295
299
  logger?.warn("user_skills_dropped", { reason: "name reserved by scenario (or duplicate)", names: dropped });
296
300
  return out;
297
301
  }
298
- export function gateScenarioRequest(ruling, bodyScenario, defaultScenario, known, logger) {
302
+ function registeredScenariosClause(known) {
303
+ return `this deployment's scenarios are: ${known.join(", ")}`;
304
+ }
305
+ export function danglingScenarioRefusal(name, source, known) {
306
+ if (known.includes(name))
307
+ return undefined;
308
+ switch (source) {
309
+ case "ruling":
310
+ return `the scenario assigned to this principal, "${name}", is not a scenario of this deployment — ${registeredScenariosClause(known)}; nothing ran — re-point the assignment (or add that scenario to this deployment) and retry`;
311
+ case "deployment":
312
+ return `DEFAULT_SCENARIO "${name}" does not name a scenario of this deployment — ${registeredScenariosClause(known)}; set DEFAULT_SCENARIO to one of them`;
313
+ default: {
314
+ const unhandled = source;
315
+ return unhandled;
316
+ }
317
+ }
318
+ }
319
+ export function gateScenarioRequest(ruling, bodyScenario, defaultScenario, known, logger, principal) {
299
320
  if (typeof bodyScenario === "string") {
300
321
  const requested = bodyScenario;
301
322
  if (!known.includes(requested)) {
302
- throw new HttpError(400, `unknown scenario "${bodyScenario}" — this deployment's scenarios are: ${known.join(", ")}`, {
323
+ throw new HttpError(400, `unknown scenario "${bodyScenario}" — ${registeredScenariosClause(known)}`, {
303
324
  code: "scenario_unknown",
304
325
  extra: { known: [...known] },
305
326
  });
@@ -313,6 +334,12 @@ export function gateScenarioRequest(ruling, bodyScenario, defaultScenario, known
313
334
  }
314
335
  return requested;
315
336
  }
316
- return ruling?.scenario ?? defaultScenario;
337
+ const [name, source] = ruling?.scenario != null ? [ruling.scenario, "ruling"] : [defaultScenario, "deployment"];
338
+ const refusal = danglingScenarioRefusal(name, source, known);
339
+ if (refusal !== undefined) {
340
+ logger?.warn("scenario_assignment_unknown", { scenario: name, source, ...(principal !== undefined ? { principal } : {}) });
341
+ throw createCodedHttpError(503, "state.scenario_assignment_unknown", refusal, { detail: { scenario: name, source } });
342
+ }
343
+ return name;
317
344
  }
318
345
  //# sourceMappingURL=scenarios.js.map
@@ -104,7 +104,7 @@ export const CONFIG_CATALOG = [
104
104
  r("CROSS_SESSION_INBOUND", "approval", "enum", "入站跨会话消息的部署/组织层治理(CC settings 键名 crossSessionInbound 的 managed 层;未设=本层不表态≠ next-turn;词表外拒启)", { danger: SEC, enumValues: [...CROSS_SESSION_INBOUND_SETTINGS], configKey: "crossSessionInbound", derivedDefaultNote: "缺席 ⇒ managed 层不设,由其余层与 mode-parity 决定", resolve: (c) => c.crossSessionInbound ?? null }),
105
105
  r("DB_BACKEND", "store", "enum", "store 后端引擎选择(mysql 兼容/pg/本机文件店/纯内存)", { enumValues: STORE_BACKENDS, danger: DATA, derivedDefaultNote: "裸 boot ⇒ local(多租户裸 boot ⇒ memory + notice);有 SQL 信号 ⇒ mysql", configKey: "dbBackend", resolve: (c) => c.dbBackend }),
106
106
  r("DB_QUERY_TIMEOUT_MS", "store", "number", "per-query DB 超时(ms;坏值回默认姿态+warn,绝不 NaN 进 driver)", { derivedDefaultNote: "缺席 ⇒ 池级不设/计数族 30s(按引擎)", configKey: "dbQueryTimeoutMs" }),
107
- r("DEFAULT_SCENARIO", "integrations", "string", "出厂缺省场景(编码产品出厂态;通用中性 persona 用 default 显式选回)", { staticDefault: "code", configKey: "defaultScenario" }),
107
+ r("DEFAULT_SCENARIO", "integrations", "string", "出厂缺省场景(编码产品出厂态;通用中性 persona 用 default 显式选回);必须是本部署在册的场景名(内建 ∪ 配置中心声明的),不在册 ⇒ 拒启", { staticDefault: "code", configKey: "defaultScenario" }),
108
108
  r("DELEGATION_MAX_CONCURRENT", "orchestration", "number", "委派入口并发帽", { derivedDefaultNote: "缺席 ⇒ 不铸键,core 自缺省(CC parity 20)" }),
109
109
  r("DELEGATION_MAX_PER_SESSION", "orchestration", "number", "委派入口每会话累计帽", { derivedDefaultNote: "缺席 ⇒ 不铸键,core 自缺省(CC parity 200)" }),
110
110
  r("DEVICE_DELIVERY_TIMEOUT_MS", "orchestration", "number", "device 车道等在途槽上限(ms;到期 device.busy)", { derivedDefaultNote: "缺席 ⇒ adapter 默认 10s" }),
@@ -411,7 +411,7 @@ export const CONFIG_CATALOG = [
411
411
  r("SERVICE_AUTH_TOKEN", "auth", "secret", "共享 service token(写面门;缺席且未开 ALLOW_UNAUTHED_WRITES ⇒ 拒提交)", { danger: SEC }),
412
412
  r("SERVICE_AUTH_TOKENS", "auth", "secret", "per-system 凭据表(tok=system CSV;source 从凭据派生,绝不自报)", { danger: SEC }),
413
413
  r("SESSION_AUTO_TITLE", "orchestration", "boolean", "首次提交自动铸 session 标题(一次 cheap 调用)", { staticDefault: "true", danger: BILL, configKey: "sessionAutoTitle" }),
414
- r("SESSION_BACKEND", "store", "enum", "session 店引擎(auto=探到 DB 用 DB,探不到回内存;回显=boot 解析后的真引擎)", { enumValues: SESSION_BACKENDS, danger: DATA, derivedDefaultNote: "缺席:显式 DB_BACKEND=mysql|pg ⇒ mysql,否则 memory", configKey: "sessionBackend", resolve: (c) => (c.sessionBackend === "tidb" ? "mysql" : c.sessionBackend) }),
414
+ r("SESSION_BACKEND", "store", "enum", "session 店引擎(auto=配了该引擎的坐标就用那只库、没配就是内存;配了而此刻连不上 ⇒ 在 STORE_LOCK_WAIT_MS 窗内重试、到窗拒启,绝不落内存;回显=boot 解析后的真引擎)", { enumValues: SESSION_BACKENDS, danger: DATA, derivedDefaultNote: "缺席:显式 DB_BACKEND=mysql|pg ⇒ mysql,否则 memory", configKey: "sessionBackend", resolve: (c) => (c.sessionBackend === "tidb" ? "mysql" : c.sessionBackend) }),
415
415
  r("SESSION_CACHE_TTL_SEC", "store", "number", "session 读缓存 TTL(秒;0=关)", { staticDefault: "300", configKey: "sessionCacheTtlSec" }),
416
416
  r("SESSION_EVENTS_HOT_MS", "limitsHttp", "number", "session SSE 订阅热探针间隔(ms)", { derivedDefaultNote: "缺席 ⇒ 200" }),
417
417
  r("SESSION_EVENTS_MAX_CONNS", "limitsHttp", "number", "session SSE 并发连接帽", { derivedDefaultNote: "缺席 ⇒ 256" }),
@@ -431,7 +431,7 @@ export const CONFIG_CATALOG = [
431
431
  r("SSH_PORT", "orchestration", "number", "ssh 端口", { derivedDefaultNote: "缺席 ⇒ 22" }),
432
432
  r("SSH_PRIVATE_KEY", "orchestration", "secret", "ssh 私钥(adapter 持有,绝不进 workspace)", { danger: SEC }),
433
433
  r("SSH_USER", "orchestration", "string", "ssh 用户(必需件)"),
434
- r("STORE_LOCK_WAIT_MS", "store", "number", "local 数据根 LOCK 撞上活持锁人时的有界等待窗(ms;0=当场拒;到窗仍拿不到 ⇒ 原码拒启,永不放行第二个写者)", { staticDefault: "60000", danger: DATA, configKey: "storeLockWaitMs" }),
434
+ r("STORE_LOCK_WAIT_MS", "store", "number", "store 启动期的有界等待窗(ms):local 数据根 LOCK 撞上活持锁人,或已配置的 SQL 库此刻连不上 / 建表失败;0=当场拒;到窗仍开不了 ⇒ 拒启(local 保持原码,永不放行第二个写者;SQL 点名后端与 host:port,永不退成内存)", { staticDefault: "60000", danger: DATA, configKey: "storeLockWaitMs" }),
435
435
  r("STORE_PROBE_INTERVAL_MS", "store", "number", "/health 活体 store 后台探针间隔(ms;0=显式关;仅 SQL 后端挂环)", { staticDefault: "15000", configKey: "storeProbeIntervalMs" }),
436
436
  r("STREAM_APPROVAL_ADHOC_GRACE_MS", "approval", "number", "流内审批 adhoc 腿窗后宽限(ms;须 window+grace < orphanTtl,破坏形拒启)", { staticDefault: "60000" }),
437
437
  r("STREAM_APPROVAL_ADMIT_MAX_PER_OWNER", "approval", "number", "流内审批 per-owner 未决帽", { staticDefault: "256" }),
package/dist/config.js CHANGED
@@ -842,13 +842,13 @@ function buildUnusedCoordinatesRefusal(keys, auto) {
842
842
  const engine = keys.length === 1 ? (keys[0] === "PG_HOST" ? "pg" : "mysql") : "mysql|pg";
843
843
  const probedHostKey = auto?.autoProbes === "pg" ? "PG_HOST" : "MYSQL_HOST";
844
844
  const why = auto !== undefined
845
- ? `SESSION_BACKEND=auto probes the ${auto.autoProbes} engine (DB_BACKEND ${process.env.DB_BACKEND ? "names it" : "unset ⇒ mysql"}), whose host key ${probedHostKey} is not set`
845
+ ? `SESSION_BACKEND=auto follows the ${auto.autoProbes} engine (DB_BACKEND ${process.env.DB_BACKEND ? "names it" : "unset ⇒ mysql"}), whose host key ${probedHostKey} is not set`
846
846
  : "no durable session store is selected (neither DB_BACKEND=mysql|pg nor SESSION_BACKEND=mysql|auto), so the store would run IN MEMORY";
847
847
  return (`${keys.join(" and ")} ${keys.length === 1 ? "is" : "are"} set but the current configuration never uses ${keys.length === 1 ? "it" : "them"}: ${why}. ` +
848
848
  `Refusing to boot rather than running in memory silently. ` +
849
849
  `To persist: DB_BACKEND=${engine} (or SESSION_BACKEND=mysql for the MySQL-protocol engine); ` +
850
850
  `to run in memory: remove the coordinates, or set DB_BACKEND=memory; ` +
851
- `for best effort (persist while the store is reachable, memory otherwise): SESSION_BACKEND=auto with the engine pointed right (DB_BACKEND=pg with PG_HOST, DB_BACKEND=mysql with MYSQL_HOST).`);
851
+ `SESSION_BACKEND=auto persists exactly when the engine's coordinates are configured (DB_BACKEND=pg with PG_HOST, DB_BACKEND=mysql with MYSQL_HOST) — and then refuses to boot, rather than falling back to memory, while the store is unreachable.`);
852
852
  }
853
853
  function parseStoreDomain(ctx) {
854
854
  const { requirePrincipal } = ctx;
@@ -1,4 +1,5 @@
1
1
  import { FileError, StubExecutionEnv, type ExecutionEnv, type Result, type ToolPolicy } from "@sema-agent/core";
2
+ import { type DeploymentListExemption } from "./approval.js";
2
3
  import type { ServiceConfig } from "./config.js";
3
4
  import type { applyRuntimeGovernance } from "./runtime-governance.js";
4
5
  /** `applyRuntimeGovernance` 的 governance 实参 —— 从折叠属主的签名位**推导**,不复制形状
@@ -91,8 +92,10 @@ export interface DurableApprovalSeat {
91
92
  readonly kind: "durable";
92
93
  /** 活体问答面;`undefined` ⇒ 恒 durable park 的原形门。 */
93
94
  readonly question: LiveQuestionFace | undefined;
95
+ /** 本会话的「本会话放行」读面(部署名单那一问的站着的答案;铸点 `approval.ts` 的 `createDeploymentListExemption`)。缺席 = 这条腿没有会话 / 没有豁免店 ⇒ 名单照问。 */
96
+ readonly exemption?: DeploymentListExemption;
94
97
  }
95
- /** live 席:Runner 的活 onAsk 当场服 ask(无 park)。 */
98
+ /** live 席:Runner 的活 onAsk 当场服 ask(无 park、无会话豁免店:run-local 一次性腿)。 */
96
99
  export interface LiveApprovalSeat {
97
100
  readonly kind: "live";
98
101
  }
@@ -263,7 +266,7 @@ export declare function buildApprovalGrantNotice(config: {
263
266
  * `undefined`**:治理层是 tighten-only 的叠加层,没有基线可叠时它自己也产不出「门在场」这件事。
264
267
  *
265
268
  * 三形,按席穷举(S-694,席的定义见 {@link ApprovalSeat}):
266
- * · `durable` ⇒ AskUserQuestion 判决门 + 三张名单的审批门(deny/neverAuto/预算全在
269
+ * · `durable` ⇒ AskUserQuestion 判决门 + 三张名单的审批门(deny/neverAuto/预算/本会话的豁免行全在
267
270
  * `createDurableAskPolicy` 里)。gated `ask` 由 core 变成 durable checkpoint suspend。
268
271
  * · `live` ⇒ **同一只**三张名单审批门,不叠问答门。gated `ask` 由 core 交给 Runner 的活 onAsk 席
269
272
  * (本口**不**做 ask→deny 映射:无人可问时的拒由那只席说,判据与文案都归它)。
@@ -126,9 +126,9 @@ export function buildApprovalGrantNotice(config) {
126
126
  export function createApprovalBaselinePolicy(config, seat) {
127
127
  switch (seat.kind) {
128
128
  case "durable":
129
- return combinePolicies(createDurableQuestionGate(seat.question), createApprovalListPolicy(config));
129
+ return combinePolicies(createDurableQuestionGate(seat.question), createApprovalListPolicy(config, seat.exemption));
130
130
  case "live":
131
- return createApprovalListPolicy(config);
131
+ return createApprovalListPolicy(config, undefined);
132
132
  case "absent":
133
133
  if (hasApprovalListIntent(config)) {
134
134
  throw new Error("approval baseline: this leg has no approval seat (nobody can answer an ask), yet an approval list is in force " +
@@ -142,12 +142,13 @@ export function createApprovalBaselinePolicy(config, seat) {
142
142
  }
143
143
  }
144
144
  }
145
- function createApprovalListPolicy(config) {
145
+ function createApprovalListPolicy(config, exemption) {
146
146
  return createDurableAskPolicy({
147
147
  requireApproval: config.approvalRequire,
148
148
  deny: config.approvalDeny,
149
149
  autoBudget: config.approvalAutoBudget,
150
150
  neverAuto: config.approvalNeverAuto,
151
+ ...(exemption !== undefined ? { exemption } : {}),
151
152
  });
152
153
  }
153
154
  //# sourceMappingURL=deployment-governance.js.map
@@ -15,7 +15,7 @@
15
15
  * ⇒ 本仓的规则从「记住一张词×决定的相容表」变成「说出你知道的那两件事」。相容性也不再靠记忆:一次
16
16
  * sweep 不可能 `allow`,core pre-CAS 拒。
17
17
  */
18
- import { type AskOutcome, type ResumeOutcome } from "@sema-agent/core";
18
+ import { type AskOutcome, type HostDecider, type PermissionResult, type ResumeOutcome } from "@sema-agent/core";
19
19
  /**
20
20
  * 谁结束了这次等待,以及通道自报的归属。
21
21
  * · `decidedBy: "person"` —— 一个人经审批通道做的决定(决定体缺席 `decidedBy` 即此);
@@ -47,19 +47,17 @@ import { type AskOutcome, type ResumeOutcome } from "@sema-agent/core";
47
47
  * 🔴 **S-558:为什么这一格住在本叶子模块**(此前是 `tool-approval.ts` 的模块私有型):来源位随决议
48
48
  * **落持久行**之后,店(`src/plugins/approval-ask-store-sql.ts` 的 `AskRow.settledBy` /
49
49
  * `DecideAskInput.settledBy`)与协调器都要说同一个型 —— 而店不能反向依赖协调器。本模块只依赖
50
- * `@sema-agent/core`,与同住的 {@link HostDecidedBy}(耐久腿的同位事实)恰好是一对:一条决议的来源
50
+ * `@sema-agent/core`,与同住的 {@link HostDecision}(耐久腿的同位事实)恰好是一对:一条决议的来源
51
51
  * 在两条腿上是同一件事,型也该在同一处。
52
52
  */
53
53
  export type HostSettledBy = NonNullable<Extract<AskOutcome, {
54
54
  allow: boolean;
55
55
  }>["settledBy"]>;
56
- /** 直接取 core `ResumeOutcome` policy_ask 臂的 `hostDecision` 型 —— 本仓不复述那几个词(第二铸点)。 */
56
+ /** 直接取 core `ResumeOutcome` policy_ask 臂的 `hostDecision` 型 —— 本仓不复述那几个词(第二铸点)。它的 `decidedBy` 就是 core 的 `HostDecider`
57
+ * (core 7.45.0 #1387:三个型位读同一只闭集)。**谁结束了这次等待**的闭集只有 core 包根那一只(`HOST_DECIDERS` / `HostDecider` / `isHostDecider`)。 */
57
58
  export type HostDecision = Extract<ResumeOutcome, {
58
59
  gate: "policy_ask";
59
60
  }>["hostDecision"];
60
- /** **谁结束了这次等待**的闭集,型从 core 的那一格派生(本仓永不抄词表:core 加/退一个词,读它的每一处
61
- * 编译期先红)。今天 = `"person"` | `"sla_timeout"` | `"policy"`。 */
62
- export type HostDecidedBy = HostDecision["decidedBy"];
63
61
  /**
64
62
  * ══ 决定体的出处位 `decidedBy`:一只闭集、一只读法、一只映射 ═══════════════════════════════════════════════
65
63
  *
@@ -68,32 +66,55 @@ export type HostDecidedBy = HostDecision["decidedBy"];
68
66
  * `POST /v1/tasks/:id/asks/:askId/decision`、耐久 `POST /v1/approvals/:sessionId/decide`),把它交给引擎的只有
69
67
  * {@link liveSettledBy}(同步座 `AskOutcome.settledBy`)与 {@link hostDecisionFacts}(停驻座 `hostDecision.decidedBy`)。
70
68
  * · 词与决定动词**正交**:策略可以放行也可以拒(core 7.39.0 起引擎铸 `policy_allowed` / `policy_refused`)。唯一的例外是
71
- * 引擎自己的那一条 —— 一张要人的卡(`requiresRealApproval`)不归策略放行,见 {@link policyCannotAllow}。
69
+ * 引擎自己的那一条 —— 一张要人的卡(`requiresRealApproval`)不归策略答,见 {@link heldAskOf}(判官是 core 的 `policyCannotAnswer`)。
72
70
  * · `"sla_timeout"` **永不从 wire 收**:那是「等待到点、没人答」,只有本服务自己的截止扫说得出来(`approval-sweeps.ts`
73
71
  * 直接铸 `{decidedBy:"sla_timeout"}`,不经本读法)。调用方说它 = 替服务端伪证一件只有服务端看得见的事。
74
72
  * · `plan_review` 不读这只键:引擎的 plan_review 结局形没有决定者位(core 的结局规整会把别的键整只重建掉),收下它就是
75
73
  * 一句「已记」的空话。那一门照兄弟门字段那一族拒(`approvals-assistant.ts` 的回决门族派生),候 core 给座。
76
74
  *
77
- * 词表的来源:core 包根**没有**导出这只词表的运行期常量(只有类型 `ResumeOutcome[...]["hostDecision"]["decidedBy"]`),所以
78
- * 本仓一只闭集常量 + 两道编译期门**双向**咬着 core 的型:core 加 / 退一个词 ⇒ 这里先红,不会在 wire 上静默留一个两个座都已经
79
- * 不收的词,也不会把 core 新开的词静默 400 掉。core 导出词表之日,本常量改为直接镜像那一只(请托在案)。
75
+ * 词表的来源:core 包根的 `HOST_DECIDERS` / `HostDecider` / `isHostDecider`(core 7.45.0 #1387;此前包根只有型,本仓手写一只闭集常量 + 两道
76
+ * 编译门双向咬它 —— 那份手抄随本版退役)。wire 闭集 = 那只词表去掉扫的那一个词,运行期值与型都从它派生,没有第二份词表。
80
77
  */
81
- export declare const APPROVAL_DECIDED_BY: readonly ["person", "policy"];
82
- /** wire 上的出处词(决定体 `decidedBy` 的值域)。 */
83
- export type ApprovalDecidedBy = (typeof APPROVAL_DECIDED_BY)[number];
84
- /** 闭集外的拒句(三条决定口同一句;码按各口既有约定,见 {@link readDecidedBy})。 */
85
- export declare const DECIDED_BY_OUTSIDE_CLOSED_SET = "decidedBy must be \"person\" or \"policy\" when present \u2014 who made this decision: a person, or this deployment's own policy (absent = \"person\"); \"sla_timeout\" is never accepted from a caller (only this server's own deadline sweep says a wait ended with nobody answering)";
78
+ /** 只有本服务自己的截止扫说得出的那一个词 —— core 词表里唯一不上 wire 的成员(`satisfies`:core 改名 / 退役它 ⇒ 这里编译红)。 */
79
+ declare const SWEEP_ONLY_DECIDER = "sla_timeout";
80
+ /** wire 上的出处词(决定体 `decidedBy` 的值域)= core 闭集去掉扫的那一个。 */
81
+ export type ApprovalDecidedBy = Exclude<HostDecider, typeof SWEEP_ONLY_DECIDER>;
82
+ /** wire 闭集的运行期值:core `HOST_DECIDERS`(#1387)去掉扫的那一个,顺序随 core。core 加一个词 ⇒ 它在这里自动在场(决定口的门照收;两只座的映射
83
+ * {@link liveSettledBy} 的穷举 `switch` 编译红,由人裁它在同步座上怎么落)。 */
84
+ export declare const APPROVAL_DECIDED_BY: readonly ApprovalDecidedBy[];
85
+ /** 闭集外的拒句(三条决定口同一句;码按各口既有约定,见 {@link readDecidedBy})。成员表派生自 {@link APPROVAL_DECIDED_BY}(词表一个来源;今天字节与 7.118.0 同)。 */
86
+ export declare const DECIDED_BY_OUTSIDE_CLOSED_SET: string;
86
87
  /** 直连门上的拒句(与 `remember` / `updatedInput` / `checkpointToken` 同族:键不在决策证明的签名载荷里)。 */
87
88
  export declare const DECIDED_BY_NOT_IN_PROOF = "decidedBy is not supported on a direct-door worker (not covered by the decision proof)";
88
- /** 一张要人的卡上策略的放行(三条决定口同一句;判据 {@link policyCannotAllow})。 */
89
+ /** 一张要人的卡上策略的放行(两条决定口同一句;判官 core `policyCannotAnswer`,问形见 {@link heldAskOf})。 */
89
90
  export declare const POLICY_CANNOT_ALLOW_PERSON_ASK = "decidedBy \"policy\" cannot allow this ask \u2014 it demands a person's approval (risk.requiresRealApproval), and a policy's answer is not a person's; answer it as a person, or refuse it as the policy";
90
91
  /**
91
- * 退役键 `settledBy`(7.93.3–7.117.x 的「只配 deny」那一位)的拒句。耐久两口由各自的键闭集 / strict 体拒;live `/respond` 的解析不是
92
- * 键闭集的(未知键一律忽略),而这只键不是随便一个未知键:它是调用方在**明确陈述**「这是策略答的」—— 忽略它就是把缺席律(person)
93
- * 套在一个说了话的体上(旧壳的策略拒记成人拒;一张要人的卡上旧形的放行被读成人的放行,绕过 {@link policyCannotAllow})。所以那条口
94
- * 在裁决之前响亮拒它,与同一只 parser 里 `scope` 的拒同一条先例;那只 parser 收成键闭集之日,这一臂并进闭集门。
92
+ * 退役键 `settledBy`(7.93.3–7.117.x 的「只配 deny」那一位)的拒句。三条决定口共用这一句,出句的铸点是 {@link retiredDecisionKeyFacts}(各口不直接引用本常量)。
93
+ * live `/respond` 的解析不是键闭集的(未知键一律忽略),而这只键不是随便一个未知键:它是调用方在**明确陈述**「这是策略答的」—— 忽略它就是把缺席律(person)
94
+ * 套在一个说了话的体上(旧壳的策略拒记成人拒;一张要人的卡上旧形的放行被读成人的放行,绕过「要人的卡不归策略答」那一判)。所以那条口
95
+ * 在裁决之前响亮拒它,与同一只 parser 里 `scope` 的拒同一条先例;耐久两口本来就在键闭集 / strict 体上拒它,句子改由同一铸点出。
95
96
  */
96
97
  export declare const SETTLED_BY_RETIRED = "settledBy is retired \u2014 send decidedBy: \"person\" | \"policy\" (who made this decision; absent = \"person\"); ignoring it would record a policy's answer as a person's";
98
+ /**
99
+ * 退役键 `settledBy` 的**拒绝事实** —— 三条工具审批决定口(live `POST /v1/tool-approvals/:id/respond`、耐久 ask 账
100
+ * `POST /v1/tasks/:taskId/asks/:askId/decision`、`POST /v1/approvals/:sessionId/decide`)对它的拒,**句**与**机读位**同出自这一处:
101
+ * · `error` = {@link SETTLED_BY_RETIRED}(三口同一句,没有哪一口自说一句 —— 此前耐久 ask 账说 strict 体的通用句、`/decide` 说键闭集句);
102
+ * · `extra` = `{ field: "settledBy" }`,直接作 `sendError` 的 `extra` / 并进 400 体。`field` 是 400 体上既有的那一把键(`request.field_invalid` 的
103
+ * `settings.permissions.*` / `settings.hooks` / 模型位同键同读法:点名这只拒落在请求体里哪个字段上),不另造码、不另造键。
104
+ * 码三口同为 `request.body_shape`;`/decide` 照旧另带 `unknownKeys` / `unsupportedKeys` 两张键表(退役键仍在 `unknownKeys` 里)。
105
+ * 判在场的口径只有一个:值不是 `undefined`(JSON 里的 `null` / `""` 也算在场 —— 退役键是一句**陈述**,不看它陈述了什么)。
106
+ * 用法(两种出口,各口都只**带**、不改):① 拒的成因就是退役键(live 解析器撞上它 / 耐久 ask 账 strict 解析失败而体里有它 / `/decide` 键闭集门)⇒ 句用 `error`、
107
+ * 体并 `extra`;② 体里同时还有别的毛病而先撞上的是那一个(live 解析器的 `note` / `persistRule` / 决定词,`/decide` 的根形门)⇒ 句仍说那个毛病、体照并 `extra` ——
108
+ * 每个口在自己的**失败出口**调同一只函数,而不是在某条判定的分支里调,否则「机读位在不在」就取决于哪句先撞上。
109
+ */
110
+ export declare const RETIRED_DECISION_KEY: "settledBy";
111
+ export interface RetiredDecisionKeyFacts {
112
+ readonly error: typeof SETTLED_BY_RETIRED;
113
+ readonly extra: {
114
+ readonly field: typeof RETIRED_DECISION_KEY;
115
+ };
116
+ }
117
+ export declare function retiredDecisionKeyFacts(body: unknown): RetiredDecisionKeyFacts | undefined;
97
118
  /** {@link readDecidedBy} 的判决(调用点做穷举 `switch` + `never` 兜底,各按自己的码约定发出)。 */
98
119
  export type DecidedByReading = {
99
120
  kind: "accepted";
@@ -120,14 +141,17 @@ export declare function readDecidedBy(raw: unknown, directDoorActive: boolean):
120
141
  */
121
142
  export declare function liveSettledBy(decidedBy: ApprovalDecidedBy): Extract<HostSettledBy, "policy"> | undefined;
122
143
  /**
123
- * 引擎自己的那一条例外:**一张要人的卡不归策略放行**(core `policyCannotAnswer` / `unjudgedAllowRefused` —— 同步座上
124
- * `req.requiresRealApproval === true` 时策略的 allow 被拒成 `blanket_allow_refused`)。本仓**只在自己的副作用先于引擎落地的
125
- * 两条口**上于裁决之前说这一句(live `/respond`:会话放行 / 常驻规则 / 一条按 approve 回放的 DECIDED 行;耐久 ask 账:一条
126
- * 宣称 approve 的 DECIDED 行),读的是引擎读的同一个事实(卡上的 `requiresRealApproval`)。耐久 `/decide` 不判:引擎在 CAS 之前
127
- * 就拒(`checkpoint.invalid_outcome`,卡留 pending),本口的副作用全在引擎受理之后 —— 在那里再判一次就是第二个判官。
128
- * core 包根没有导出这只谓词(请托在案);导出之日本函数改为直接调它。
144
+ * 引擎自己的那一条例外:**一张要人的卡不归策略答** —— 判官是 core 的 `policyCannotAnswer(question, decidedBy)`(#1395 包根导出;引擎的续跑入口、
145
+ * 执行带与同步座读同一条)。本仓**只在自己的副作用先于引擎落地的两条口**上于裁决之前问它(live `/respond`:会话放行 / 常驻规则 / 一条按 approve
146
+ * 回放的 DECIDED 行;耐久 ask 账:一条宣称 approve 的 DECIDED 行),并且只问**放行**那一向(与引擎续跑入口同:拒永远答得了)。耐久 `/decide` 不判:
147
+ * 引擎在 CAS 之前就拒(`checkpoint.invalid_outcome`,卡留 pending),本口的副作用全在引擎受理之后 —— 在那里再问一次就是第二个判官。
148
+ *
149
+ * 交给判官的「问」= {@link heldAskOf}:本仓两条口持有的都不是引擎的原物(活卡条目记的是铸条目那一次从 `AskRequest` 读定的那一位;耐久 ask 账行上是
150
+ * 卡面 `risk.requiresRealApproval`,读不出按要人读),以引擎的问形(core `HeldQuestion` 的 `ask` 决定臂)交过去 —— 「谁答不了」只在引擎那一处判。
129
151
  */
130
- export declare function policyCannotAllow(decidedBy: ApprovalDecidedBy, allow: boolean, demandsAPerson: boolean): boolean;
152
+ export declare function heldAskOf(demandsAPerson: boolean): Extract<PermissionResult, {
153
+ action: "ask";
154
+ }>;
131
155
  /**
132
156
  * 一次 decide 的**停驻座事实**,一处成形(耐久 `/decide` 与活卡 PARKED 赎回席共用;截止扫直接铸 `{decidedBy:"sla_timeout"}`)。
133
157
  *
@@ -144,4 +168,5 @@ export declare function policyCannotAllow(decidedBy: ApprovalDecidedBy, allow: b
144
168
  * 迟到结算账本(`approval-ask-audit-store.ts`)记的就是它。按词删一格 = 给账本挖一个只在某个词上出现的洞。
145
169
  */
146
170
  export declare function hostDecisionFacts(decidedBy: ApprovalDecidedBy, principal: string | undefined): HostDecision;
171
+ export {};
147
172
  //# sourceMappingURL=host-decision.d.ts.map
@@ -1,11 +1,18 @@
1
- import { screenApproverAttribution } from "@sema-agent/core";
2
- export const APPROVAL_DECIDED_BY = ["person", "policy"];
3
- export const DECIDED_BY_OUTSIDE_CLOSED_SET = 'decidedBy must be "person" or "policy" when present — who made this decision: a person, or this deployment\'s own policy (absent = "person"); "sla_timeout" is never accepted from a caller (only this server\'s own deadline sweep says a wait ended with nobody answering)';
1
+ import { HOST_DECIDERS, isHostDecider, screenApproverAttribution } from "@sema-agent/core";
2
+ const SWEEP_ONLY_DECIDER = "sla_timeout";
3
+ function isApprovalDecidedBy(v) {
4
+ return isHostDecider(v) && v !== SWEEP_ONLY_DECIDER;
5
+ }
6
+ export const APPROVAL_DECIDED_BY = Object.freeze(HOST_DECIDERS.filter(isApprovalDecidedBy));
7
+ export const DECIDED_BY_OUTSIDE_CLOSED_SET = `decidedBy must be ${APPROVAL_DECIDED_BY.map((w) => `"${w}"`).join(" or ")} when present — who made this decision: a person, or this deployment's own policy (absent = "person"); "${SWEEP_ONLY_DECIDER}" is never accepted from a caller (only this server's own deadline sweep says a wait ended with nobody answering)`;
4
8
  export const DECIDED_BY_NOT_IN_PROOF = "decidedBy is not supported on a direct-door worker (not covered by the decision proof)";
5
9
  export const POLICY_CANNOT_ALLOW_PERSON_ASK = 'decidedBy "policy" cannot allow this ask — it demands a person\'s approval (risk.requiresRealApproval), and a policy\'s answer is not a person\'s; answer it as a person, or refuse it as the policy';
6
10
  export const SETTLED_BY_RETIRED = 'settledBy is retired — send decidedBy: "person" | "policy" (who made this decision; absent = "person"); ignoring it would record a policy\'s answer as a person\'s';
7
- function isApprovalDecidedBy(v) {
8
- return APPROVAL_DECIDED_BY.includes(v);
11
+ export const RETIRED_DECISION_KEY = "settledBy";
12
+ export function retiredDecisionKeyFacts(body) {
13
+ if (body === null || typeof body !== "object" || Array.isArray(body))
14
+ return undefined;
15
+ return body[RETIRED_DECISION_KEY] !== undefined ? { error: SETTLED_BY_RETIRED, extra: { field: RETIRED_DECISION_KEY } } : undefined;
9
16
  }
10
17
  export function readDecidedBy(raw, directDoorActive) {
11
18
  if (raw === undefined)
@@ -26,8 +33,8 @@ export function liveSettledBy(decidedBy) {
26
33
  }
27
34
  }
28
35
  }
29
- export function policyCannotAllow(decidedBy, allow, demandsAPerson) {
30
- return decidedBy === "policy" && allow && demandsAPerson;
36
+ export function heldAskOf(demandsAPerson) {
37
+ return demandsAPerson ? { action: "ask", requiresRealApproval: true } : { action: "ask" };
31
38
  }
32
39
  export function hostDecisionFacts(decidedBy, principal) {
33
40
  const screened = screenApproverAttribution(principal);
@@ -1,8 +1,8 @@
1
- import { APPROVAL_REACH, MCP_IMAGE_MAX_BASE64, isApprovalReach, isThinkingLevel, expandTiers } from "@sema-agent/core";
1
+ import { APPROVAL_REACH, MCP_IMAGE_MAX_BASE64, isApprovalReach, isPermissionMode, isThinkingLevel, expandTiers } from "@sema-agent/core";
2
2
  import { isModelAllowlisted, matchCatalogModel, refusedCatalogEntry, selectableCatalog } from "../model-select.js";
3
3
  import { modelUnavailableMessage } from "../model-entry-refusal.js";
4
4
  import { HttpError } from "../security.js";
5
- import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS, SETTINGS_PERMISSION_MODE_WORDS, isSettingsPermissionMode, parseTaskSettings, settingsPermissionBucketIssue, skillListingBudgetFractionOk, skillListingMaxDescOk, taskHooksHonored, taskSettingsKeyIssue } from "../task-settings.js";
5
+ import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS, PERMISSION_MODE_WORDS, parseTaskSettings, settingsPermissionBucketIssue, skillListingBudgetFractionOk, skillListingMaxDescOk, taskHooksHonored, taskSettingsKeyIssue } from "../task-settings.js";
6
6
  import { taskBodyKeyIssue } from "../request-key-closure.js";
7
7
  import { FILE_HISTORY_DISABLED_NO_RESTORE, fileHistoryRestoreConflict, inheritEnvFieldIssue, singleMemberOffFieldIssue } from "../request-value-closure.js";
8
8
  import * as facts from "../capability-facts.js";
@@ -524,8 +524,8 @@ export function createAdmission(ctx) {
524
524
  }
525
525
  {
526
526
  const v = body.permissionMode;
527
- if (v !== undefined && !isSettingsPermissionMode(v)) {
528
- sendError(res, 400, "request.field_invalid", `permissionMode must be one of ${SETTINGS_PERMISSION_MODE_WORDS}`);
527
+ if (v !== undefined && !isPermissionMode(v)) {
528
+ sendError(res, 400, "request.field_invalid", `permissionMode must be one of ${PERMISSION_MODE_WORDS}`);
529
529
  return null;
530
530
  }
531
531
  }
@@ -535,8 +535,8 @@ export function createAdmission(ctx) {
535
535
  const p = s.permissions;
536
536
  if (p !== null && typeof p === "object" && !Array.isArray(p)) {
537
537
  const m = p.defaultMode;
538
- if (m !== undefined && !isSettingsPermissionMode(m)) {
539
- sendError(res, 400, "request.field_invalid", `settings.permissions.defaultMode must be one of ${SETTINGS_PERMISSION_MODE_WORDS}`);
538
+ if (m !== undefined && !isPermissionMode(m)) {
539
+ sendError(res, 400, "request.field_invalid", `settings.permissions.defaultMode must be one of ${PERMISSION_MODE_WORDS}`);
540
540
  return null;
541
541
  }
542
542
  const d = p.disableAutoMode;
@@ -41,6 +41,7 @@ import { buildActiveRunConflict, resumeEntryForGate } from "./active-run-conflic
41
41
  import { commitPlacement, devicePlacementErrorBody } from "./device-placement.js";
42
42
  import { commitSessionInheritEnv } from "./session-inherit-env.js";
43
43
  import { ASK_USER_QUESTION_TOOL_NAME } from "../approval-content-kind.js";
44
+ import { deploymentListAsks } from "../approval.js";
44
45
  import { readResumeCtx, resumeCtxCorruptAnswer } from "./resume-ctx-read.js";
45
46
  import { resumeBlockedByPolicyAnswer, storedBodyReplayAnswer, storedBodyReplayLogFields, storedBodyReplayRefusal } from "./checkpoint-ctx.js";
46
47
  import { httpErrorCode } from "./send.js";
@@ -274,7 +275,7 @@ export function createResumeLegs(ctx) {
274
275
  const rememberableTool = paForGrant !== undefined &&
275
276
  paForGrant.toolName &&
276
277
  paForGrant.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
277
- standingAnswerDurablyRecordable(parkRowStandingAnswerFacts(paForGrant, cp.gate, deps.config))
278
+ standingAnswerDurablyRecordable(parkRowStandingAnswerFacts(paForGrant, cp.gate, deps.config, deploymentListAsks(deps.config, { toolName: paForGrant.toolName })))
278
279
  ? paForGrant.toolName
279
280
  : null;
280
281
  const boundGrant = onResumeCommitted
@@ -3,6 +3,7 @@ import type { QuestionAnswer } from "@sema-agent/core";
3
3
  import type { CheckpointStoreFull } from "../../plugins/store-backend.js";
4
4
  import { type ToolApprovalCoordinator } from "../../tool-approval.js";
5
5
  import { type GovernancePosture } from "../../runtime-governance.js";
6
+ import { type GateIntentView } from "../../approval.js";
6
7
  import { type RouteCtx, type RouteDecl, type RouteMatch, type RouteIdsOf } from "../route-ctx.js";
7
8
  import { type KeyClosureIssue } from "../../request-key-closure.js";
8
9
  /** 族的成员 = 路由表行 id(改名 / 删行 ⇒ 下表的键当场编译红)。 */
@@ -104,6 +105,27 @@ export declare function checkQuestionAnswer(v: unknown): QuestionAnswerCheck;
104
105
  /** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。裸 `return;`
105
106
  * = 「本域已应答」;走到函数尾才是「没匹配上」。跨域的东西一律在首行解构出来。 */
106
107
  export declare function handleApprovalsAssistant(req: IncomingMessage, res: ServerResponse, match: RouteMatch<ApprovalsAssistantRouteId>, ctx: RouteCtx): Promise<void>;
108
+ /**
109
+ * 🔴 [ref]-2 / [ref]②:两条 durable 读面(`GET /v1/approvals` + `/v1/approvals/stream`)的**唯一**
110
+ * 投影 —— 脱敏([ref] 件4)+ 出身归因([ref] 的另一半)一次做完。
111
+ *
112
+ * 出身格的病灶如实记:`governanceForced` 此前只在 409 体上算(`buildActiveRunConflict`),而运维队列
113
+ * 的行不算 —— 同一道 park 门,客户端从 409 看得见「这是运维治理层下的」,从队列看不见。它**不是**一个
114
+ * 落库缺口:判据的两半(行上 `riskDescriptor.shellGateDoctrine` + 本部署的治理姿态)在这条读面上都够得着,
115
+ * 缺的只是**没有人算**。所以补法 = 让两面共用 `governanceOriginOf` 这一个属主(在读面算的已知失真、
116
+ * 以及「根治要 core 在 mint 写进行里」的登记,逐字在 `runtime-governance.ts` 那个函数的顶注,不复述)。
117
+ *
118
+ * 🔴 `true` 才写键,恒不写 `false` —— 与活卡帧 / 409 体 / `card_json` 同一条纪律:缺席 = 「没有治理来源
119
+ * 的证据」,不是「这不是治理门」。
120
+ *
121
+ * ⚠️ **`delegation` 刻意不在这里**(同批如实登记,禁编造键):park 行上**没有**这条信息 —— core 的
122
+ * `Checkpoint` / `PendingAction` / `CheckpointGate` 三个面都不带 `{parentToolCallId, depth, agentName}`
123
+ * (`Checkpoint.state.delegationProvenance` 是 [ref] 的内容安全聚合 `{version, coverageStartTurn,
124
+ * sawExternal, incomplete}`,与 ask 的出处链是两件事)。同步腿的这一格来自 `AskRequest.delegation`,
125
+ * 那条路 park 时不随行。⇒ 无供给,诚实缺席;要它得先有 core 侧的属主面。
126
+ */
127
+ /** 运维队列行投影要的部署事实:治理姿态(出身归因的部署侧一半)+ 三张审批名单(问的属主:部署名单门问不问这一行的调用)。生产两处都递活配置。 */
128
+ export type PendingProjectionDeployment = GovernancePosture & Pick<GateIntentView, "approvalRequire" | "approvalDeny" | "approvalNeverAuto">;
107
129
  export declare function streamApprovals(req: IncomingMessage, res: ServerResponse, cs: Pick<CheckpointStoreFull, "listPending">,
108
130
  /**
109
131
  * 🔴 S-454:**第二条 diff 源** —— `GET /v1/approvals` 的 `livePending` 那**同一只**读函数
@@ -118,9 +140,9 @@ export declare function streamApprovals(req: IncomingMessage, res: ServerRespons
118
140
  * 与 `governance` 同一条理由:`pollMs` 只有测试传,而本参数是每个生产调用点都必须表态的那一个。
119
141
  */
120
142
  live: Pick<ToolApprovalCoordinator, "listLivePending"> | undefined, scope: string | undefined,
121
- /** [ref]②:出身归因的部署侧一半(缺席 ⇒ 归不出治理出身 ⇒ 键缺席,与 best-effort 同方向)。**位置在
122
- * `pollMs` 之前**是刻意的:`pollMs` 只有测试传,而本参数是每个生产调用点都必须给的那一个。 */
123
- governance?: GovernancePosture, pollMs?: number): Promise<void>;
143
+ /** [ref]②:出身归因的部署侧一半 + 三张审批名单(问的属主)。缺席 ⇒ 归不出治理出身(键缺席,与 best-effort 同方向)、没有一行读作
144
+ * 部署名单的问(记不住,方向是多问)。**位置在 `pollMs` 之前**是刻意的:`pollMs` 只有测试传,而本参数是每个生产调用点都必须给的那一个。 */
145
+ governance?: PendingProjectionDeployment, pollMs?: number): Promise<void>;
124
146
  export declare const APPROVALS_ASSISTANT_ROUTES: readonly [{
125
147
  readonly id: "approvals-list";
126
148
  readonly path: "/v1/approvals";
@@ -1,8 +1,8 @@
1
1
  import { screenedGate } from "../../trace/project.js";
2
- import { hostDecisionFacts, readDecidedBy, DECIDED_BY_NOT_IN_PROOF, DECIDED_BY_OUTSIDE_CLOSED_SET } from "../../host-decision.js";
2
+ import { hostDecisionFacts, readDecidedBy, retiredDecisionKeyFacts, DECIDED_BY_NOT_IN_PROOF, DECIDED_BY_OUTSIDE_CLOSED_SET } from "../../host-decision.js";
3
3
  import { sameExecutionRecord } from "@sema-agent/core";
4
4
  import { DECIDED_REPLAY_SCAN_CAP, approvalPayloadFingerprint } from "../../plugins/checkpoint-store-sql.js";
5
- import { APPROVAL_GATE_KINDS, standingAnswerDurablyRecordable } from "../../tool-approval.js";
5
+ import { APPROVAL_GATE_KINDS, carriesDeploymentListQuestion, standingAnswerDurablyRecordable } from "../../tool-approval.js";
6
6
  import { isParkedRunStatus } from "../../plugins/store-contracts.js";
7
7
  import { principalFrom, decodeCheckpointScope, PRINCIPAL_TOKEN_HEADER, APPROVAL_MAC_HEADER, APPROVAL_MAC_KID_HEADER } from "../../security.js";
8
8
  import { editedPlanBinding, isWellFormedUnicode, verifyDirectDoorProof } from "../../principal-jwt.js";
@@ -17,6 +17,7 @@ import { bindSseLifecycle } from "../sse-lifecycle.js";
17
17
  import { sendJson, sendError, sseHeaders, SSE_MAX_STREAM_MS, SSE_HEARTBEAT_IDLE_MS } from "../send.js";
18
18
  import { gatedPrincipal, explicitOperatorOk, isOperator, sendPrincipalRefusal } from "../principal-gate.js";
19
19
  import { governanceOriginOf } from "../../runtime-governance.js";
20
+ import { deploymentListAsks } from "../../approval.js";
20
21
  import { answerGrantsBeyondCall, grantBindingOf } from "../../standing-answer-grant.js";
21
22
  import { sendResumeOutcome } from "../route-ctx.js";
22
23
  import { assertNever } from "../route-ctx.js";
@@ -640,13 +641,14 @@ export async function handleApprovalsAssistant(req, res, match, ctx) {
640
641
  const body = (await readJson(req));
641
642
  const decision = body?.decision === "approve" ? "approve" : body?.decision === "deny" ? "deny" : undefined;
642
643
  if (!decision) {
643
- sendError(res, 400, "request.body_shape", "body must be { decision: 'approve' | 'deny', reason?, answer?, checkpointToken?, boundCallId?, boundInputHash?, updatedInput?, remember?, decidedBy? }");
644
+ sendError(res, 400, "request.body_shape", "body must be { decision: 'approve' | 'deny', reason?, answer?, checkpointToken?, boundCallId?, boundInputHash?, updatedInput?, remember?, decidedBy? }", retiredDecisionKeyFacts(body)?.extra);
644
645
  return;
645
646
  }
646
647
  {
647
648
  const keyIssue = decisionBodyKeyIssue("approval-decide", body);
648
649
  if (keyIssue) {
649
- sendError(res, 400, "request.body_shape", keyIssue.message, { unknownKeys: keyIssue.unknownKeys, unsupportedKeys: keyIssue.unsupportedKeys });
650
+ const retired = retiredDecisionKeyFacts(body);
651
+ sendError(res, 400, "request.body_shape", retired?.error ?? keyIssue.message, { unknownKeys: keyIssue.unknownKeys, unsupportedKeys: keyIssue.unsupportedKeys, ...retired?.extra });
650
652
  return;
651
653
  }
652
654
  }
@@ -819,7 +821,8 @@ function redactPendingDisclosures(row) {
819
821
  const next = redactRiskDescriptor(rd);
820
822
  return next === rd ? row : { ...row, riskDescriptor: next };
821
823
  }
822
- function projectPendingForWire(row, governance) {
824
+ function projectPendingForWire(row, deployment) {
825
+ const governance = deployment;
823
826
  const redacted = redactPendingDisclosures(row);
824
827
  const governanceForced = governanceOriginOf({ riskDescriptor: row.riskDescriptor ?? undefined }, governance);
825
828
  const rememberRefused = !standingAnswerDurablyRecordable({
@@ -828,6 +831,7 @@ function projectPendingForWire(row, governance) {
828
831
  mandated: row.mandated === true || row.ruleOffersAbsence === "mandated",
829
832
  grantBinding: grantBindingOf(row),
830
833
  classes: row.classes,
834
+ deploymentListQuestion: carriesDeploymentListQuestion(row.toolName !== null && deployment !== undefined && deploymentListAsks(deployment, { toolName: row.toolName }), row.classes),
831
835
  });
832
836
  return { ...redacted, ...(governanceForced ? { governanceForced } : {}), ...(rememberRefused ? { rememberRefused: true } : {}) };
833
837
  }
@@ -1,4 +1,4 @@
1
- import { expandTiers, reasoningEffortLevelsOf, routePairingStatus } from "@sema-agent/core";
1
+ import { PERMISSION_MODES, expandTiers, reasoningEffortLevelsOf, routePairingStatus } from "@sema-agent/core";
2
2
  import { modelImageInput, isModelAllowlisted, selectableCatalog } from "../../model-select.js";
3
3
  import { modelKeyPoisonOf, resolveModelApiKey } from "../../key-resolver.js";
4
4
  import { projectSealedKeyCapability } from "../../sealed-key.js";
@@ -23,7 +23,7 @@ import { workflowModelAllowlistFor } from "../../task-workflow.js";
23
23
  import { centerEntitlementSourceWired } from "../../runtime-caps-resolver.js";
24
24
  import * as facts from "../../capability-facts.js";
25
25
  import { judgeAutoModeArming } from "../../auto-mode-face.js";
26
- import { coercePermissionModeStrict, SETTINGS_PERMISSION_MODE_WORDS, taskHooksHonored } from "../../task-settings.js";
26
+ import { coercePermissionModeStrict, PERMISSION_MODE_WORDS, taskHooksHonored } from "../../task-settings.js";
27
27
  import { gatedPrincipal, sendPrincipalRefusal } from "../principal-gate.js";
28
28
  function routePairingOf(deps, name, m) {
29
29
  if (modelKeyPoisonOf(name, deps.config.modelApiKeyEnv ?? {}, process.env, deps.config.modelApiKeys ?? {}) !== undefined)
@@ -60,7 +60,7 @@ export async function handleCapabilities(req, res, match, ctx) {
60
60
  const requestedModeRaw = new URLSearchParams(match.search).get("permissionMode");
61
61
  const requestedMode = requestedModeRaw === null ? undefined : coercePermissionModeStrict(requestedModeRaw);
62
62
  if (requestedModeRaw !== null && requestedMode === undefined) {
63
- sendError(res, 400, "request.field_invalid", `permissionMode must be one of ${SETTINGS_PERMISSION_MODE_WORDS}`);
63
+ sendError(res, 400, "request.field_invalid", `permissionMode must be one of ${PERMISSION_MODE_WORDS}`);
64
64
  return;
65
65
  }
66
66
  const entitlementSource = centerEntitlementSourceWired(deps.config);
@@ -160,6 +160,7 @@ export async function handleCapabilities(req, res, match, ctx) {
160
160
  streamApproval: facts.streamApproval(deps).available,
161
161
  approvalDecisionNote: true,
162
162
  approvalDecidedBy: true,
163
+ permissionModes: [...PERMISSION_MODES],
163
164
  sideQuery: true,
164
165
  agentRoster: facts.agentRoster(deps).available,
165
166
  retention: retentionAdvertisement(deps.config),
@@ -114,7 +114,7 @@ export declare function buildClearanceRowWire(r: OriginClearanceRow): {
114
114
  baseRev: string;
115
115
  origin: {
116
116
  at: number;
117
- cause?: "static" | "observed" | "unattributed" | "derived" | undefined;
117
+ cause?: "static" | "unattributed" | "observed" | "derived" | undefined;
118
118
  taint: "external";
119
119
  };
120
120
  };