@sema-agent/server 7.118.0 → 7.120.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 (139) hide show
  1. package/MIGRATION.md +139 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/USAGE.md +26 -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 +6 -6
  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/execution-env.js +3 -2
  35. package/dist/boot/fatal-exit.d.ts +7 -1
  36. package/dist/boot/fatal-exit.js +2 -1
  37. package/dist/boot/leader.d.ts +4 -4
  38. package/dist/boot/leader.js +1 -2
  39. package/dist/boot/parked-revive-gate.d.ts +5 -1
  40. package/dist/boot/parked-revive-gate.js +8 -2
  41. package/dist/boot/resolve-spec.js +7 -4
  42. package/dist/boot/runner-deps.d.ts +4 -3
  43. package/dist/boot/runner-deps.js +7 -3
  44. package/dist/boot/stage-01-config.js +2 -0
  45. package/dist/boot/stage-07-capability-layer.js +5 -1
  46. package/dist/boot/stage-10-http-server.js +3 -2
  47. package/dist/boot/stores.js +12 -7
  48. package/dist/capabilities/builtin-tools.d.ts +31 -8
  49. package/dist/capabilities/builtin-tools.js +22 -1
  50. package/dist/capabilities/center-plugins.d.ts +2 -0
  51. package/dist/capabilities/center-plugins.js +24 -6
  52. package/dist/capabilities/code-review-council.js +8 -11
  53. package/dist/capabilities/scenarios.d.ts +42 -13
  54. package/dist/capabilities/scenarios.js +36 -9
  55. package/dist/capabilities/send-user-file-tool.d.ts +6 -3
  56. package/dist/capabilities/send-user-file-tool.js +1 -1
  57. package/dist/config-catalog.js +5 -5
  58. package/dist/config-lkg.d.ts +33 -5
  59. package/dist/config-lkg.js +55 -11
  60. package/dist/config-types.d.ts +8 -1
  61. package/dist/config.js +11 -11
  62. package/dist/deployment-governance.d.ts +8 -3
  63. package/dist/deployment-governance.js +7 -4
  64. package/dist/device-store.d.ts +7 -0
  65. package/dist/device-store.js +4 -0
  66. package/dist/device-ws-hub.js +17 -9
  67. package/dist/device-ws-protocol.d.ts +15 -0
  68. package/dist/host-decision.d.ts +52 -27
  69. package/dist/host-decision.js +14 -7
  70. package/dist/http/admission.js +6 -6
  71. package/dist/http/resume-legs.js +6 -1
  72. package/dist/http/routes/approvals-assistant.d.ts +25 -3
  73. package/dist/http/routes/approvals-assistant.js +9 -5
  74. package/dist/http/routes/capabilities.js +5 -3
  75. package/dist/http/routes/devices.js +3 -3
  76. package/dist/http/routes/memory-origin.d.ts +1 -1
  77. package/dist/http/routes/rules.js +13 -6
  78. package/dist/http/routes/runs.js +11 -5
  79. package/dist/http/routes/sessions.js +20 -16
  80. package/dist/http/wire-types.d.ts +12 -13
  81. package/dist/images/manifest.d.ts +7 -1
  82. package/dist/images/manifest.js +18 -16
  83. package/dist/list-entry-display.d.ts +14 -0
  84. package/dist/list-entry-display.js +14 -0
  85. package/dist/memory-store-refusal.d.ts +7 -12
  86. package/dist/memory-store-refusal.js +4 -5
  87. package/dist/namespaced-rule-name.d.ts +4 -3
  88. package/dist/namespaced-rule-name.js +2 -6
  89. package/dist/observability/fail-open.d.ts +9 -1
  90. package/dist/observability/fail-open.js +9 -1
  91. package/dist/operator-ask.d.ts +7 -6
  92. package/dist/plugins/approval-exemption-store.d.ts +4 -0
  93. package/dist/plugins/approval-exemption-store.js +1 -0
  94. package/dist/plugins/blob-backend.d.ts +13 -0
  95. package/dist/plugins/blob-backend.js +26 -0
  96. package/dist/plugins/local-session-store.d.ts +78 -47
  97. package/dist/plugins/local-session-store.js +152 -122
  98. package/dist/plugins/one-time-migrations.d.ts +14 -0
  99. package/dist/plugins/one-time-migrations.js +90 -3
  100. package/dist/plugins/permission-rule-store-file.js +2 -6
  101. package/dist/plugins/permission-rule-store-sql.d.ts +64 -6
  102. package/dist/plugins/permission-rule-store-sql.js +166 -59
  103. package/dist/plugins/remote-env-device.d.ts +12 -1
  104. package/dist/plugins/remote-env-device.js +13 -5
  105. package/dist/plugins/scheduler-support.d.ts +8 -4
  106. package/dist/plugins/scheduler-support.js +5 -5
  107. package/dist/plugins/snapshot-bucket-presence.d.ts +36 -0
  108. package/dist/plugins/snapshot-bucket-presence.js +40 -0
  109. package/dist/plugins/store-backend.d.ts +17 -6
  110. package/dist/plugins/store-backend.js +46 -16
  111. package/dist/plugins/store-open-wait.d.ts +33 -0
  112. package/dist/plugins/store-open-wait.js +73 -0
  113. package/dist/rule-entry-list.d.ts +0 -1
  114. package/dist/rule-entry-list.js +5 -10
  115. package/dist/rules-consent.d.ts +12 -3
  116. package/dist/rules-consent.js +5 -4
  117. package/dist/run-local.js +6 -3
  118. package/dist/security.d.ts +5 -3
  119. package/dist/security.js +4 -0
  120. package/dist/sema-config-dir.d.ts +19 -0
  121. package/dist/sema-config-dir.js +33 -0
  122. package/dist/session-policy-wire.js +10 -14
  123. package/dist/spec-fields.d.ts +7 -2
  124. package/dist/store-posture.d.ts +14 -6
  125. package/dist/store-posture.js +7 -4
  126. package/dist/task-settings.d.ts +30 -58
  127. package/dist/task-settings.js +6 -18
  128. package/dist/tool-approval.d.ts +97 -43
  129. package/dist/tool-approval.js +120 -31
  130. package/dist/trace/engine-notice-wire.d.ts +6 -4
  131. package/dist/trace/engine-notice-wire.js +16 -9
  132. package/dist/trace/redact.d.ts +6 -0
  133. package/dist/trace/redact.js +9 -9
  134. package/dist/trace/self-minted-wire.d.ts +15 -1
  135. package/dist/trace/self-minted-wire.js +2 -0
  136. package/dist/user-input-ledger.d.ts +19 -15
  137. package/dist/user-input-ledger.js +12 -20
  138. package/docs/DEVICE-EXECUTOR-PROTOCOL.md +56 -14
  139. package/package.json +2 -2
package/dist/config.js CHANGED
@@ -20,6 +20,8 @@ import {} from "./posture-source.js";
20
20
  import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
21
21
  import { describeUnmatchableRuleName, ruleEntryToolName, unmatchableRuleNameOf } from "./namespaced-rule-name.js";
22
22
  import { bakeOperatorPrincipalsRefusal, bakeRunnerCredentialRefusal, halfOpenDirectDoorRefusal, leaderDurableStoreRefusal, operatorPrincipalsRequiredRefusal } from "./config-invariants.js";
23
+ import { declaredSemaConfigDir } from "./sema-config-dir.js";
24
+ import { lkgPathOverrideOf } from "./config-lkg.js";
23
25
  import { REMOTE_EXEC_PROVIDER_WORDS, executionLaneOf, executionLaneRequiredRefusal, isSingleMachineDefaultLane, laneRequiredEnv } from "./execution-lane-caps.js";
24
26
  import { MEMORY_ENGINE_BACKENDS, SESSION_BACKENDS, STORE_BACKENDS } from "@sema-agent/settings-schema/execution-lanes";
25
27
  import { AUTONOMY_MODES } from "./runtime-governance.js";
@@ -29,6 +31,7 @@ import { parseCenterCredentialRenewLeadMs } from "./center-credential-renewer.js
29
31
  import { modelProviderRefusal } from "./model-provider.js";
30
32
  import { modelRouteUrlRefusal } from "./model-route-url.js";
31
33
  import { modelRouteEndpointRefusal } from "./model-route-endpoint.js";
34
+ import { displayListEntry } from "./list-entry-display.js";
32
35
  import { buildDisplaySafeUrl, redactSecrets } from "./trace/redact.js";
33
36
  import { allowlistedEnv } from "./server-secret-env.js";
34
37
  import { ENV_NAME_ALLOWLIST_KNOBS, ENV_NAME_ALLOWLIST_KNOB_NAMES, describeAllowlistPostures } from "./env-name-allowlist-knobs.js";
@@ -842,13 +845,13 @@ function buildUnusedCoordinatesRefusal(keys, auto) {
842
845
  const engine = keys.length === 1 ? (keys[0] === "PG_HOST" ? "pg" : "mysql") : "mysql|pg";
843
846
  const probedHostKey = auto?.autoProbes === "pg" ? "PG_HOST" : "MYSQL_HOST";
844
847
  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`
848
+ ? `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
849
  : "no durable session store is selected (neither DB_BACKEND=mysql|pg nor SESSION_BACKEND=mysql|auto), so the store would run IN MEMORY";
847
850
  return (`${keys.join(" and ")} ${keys.length === 1 ? "is" : "are"} set but the current configuration never uses ${keys.length === 1 ? "it" : "them"}: ${why}. ` +
848
851
  `Refusing to boot rather than running in memory silently. ` +
849
852
  `To persist: DB_BACKEND=${engine} (or SESSION_BACKEND=mysql for the MySQL-protocol engine); ` +
850
853
  `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).`);
854
+ `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
855
  }
853
856
  function parseStoreDomain(ctx) {
854
857
  const { requirePrincipal } = ctx;
@@ -1209,17 +1212,11 @@ function parseDegradeOn(words) {
1209
1212
  }
1210
1213
  return words;
1211
1214
  }
1212
- const PRESENTED_NAME_MAX = 160;
1213
- function presentToolName(name) {
1214
- const clean = name.replace(CONTROL_AND_BIDI_STRIP_RE, "");
1215
- return clean.length <= PRESENTED_NAME_MAX ? clean : `${clean.slice(0, PRESENTED_NAME_MAX)}…(${name.length} chars)`;
1216
- }
1217
1215
  function ccRuleFormGuidance(name) {
1218
1216
  if (!name.includes("(") && !name.includes(")"))
1219
1217
  return undefined;
1220
- const shown = presentToolName(name);
1221
- return (`CC-form rule specifier — this list matches the WHOLE tool name verbatim, so it can only ever match a tool literally named "${shown}". ` +
1222
- `Drop the parentheses to gate the tool itself ("${presentToolName(name.split("(")[0] ?? name)}"); ` +
1218
+ return (`CC-form rule specifier — this list matches the WHOLE tool name verbatim, so it can only ever match a tool literally named ${displayListEntry(name)}. ` +
1219
+ `Drop the parentheses to gate the tool itself (${displayListEntry(name.split("(")[0] ?? name)}); ` +
1223
1220
  `to decide per COMMAND use \`runtime.commandPolicy\` ({command, decision} — bare argv[0] names); ` +
1224
1221
  `to put every shell call behind an approval use MANUAL_MODE_SHELL_GATE=always`);
1225
1222
  }
@@ -1264,7 +1261,7 @@ export function unmatchableToolNamesRefusal(source, names, judgment) {
1264
1261
  return undefined;
1265
1262
  const message = (`${source} has ${bad.length === 1 ? "an entry" : "entries"} that can never take effect — ${bad.length === 1 ? "it" : "each"} would never match a live tool or cannot be compiled into a rule ` +
1266
1263
  `(names are read exactly as written: no alias folding, no protocol prefix added): ` +
1267
- bad.map((b) => `${presentToolName(b.name)} → ${b.guidance}`).join("; ")).replace(CONTROL_AND_BIDI_STRIP_RE, "");
1264
+ bad.map((b) => `${displayListEntry(b.name)} → ${b.guidance}`).join("; ")).replace(CONTROL_AND_BIDI_STRIP_RE, "");
1268
1265
  return { message, names: bad.map((b) => b.name) };
1269
1266
  }
1270
1267
  const APPROVAL_GRANT_ENV = "APPROVAL_GRANT";
@@ -1899,6 +1896,8 @@ function parseOrchestrationDomain(ctx) {
1899
1896
  assertReadDenyBuiltinsResolvable(tiers, exclude);
1900
1897
  return { ...(tiers !== undefined ? { readDenyBuiltinTiers: tiers } : {}), ...(exclude !== undefined ? { readDenyBuiltinExclude: exclude } : {}) };
1901
1898
  })(),
1899
+ ...((dir) => (dir !== undefined ? { agentConfigDir: dir } : {}))(declaredSemaConfigDir()),
1900
+ ...((p) => (p !== undefined ? { configLkgPath: p } : {}))(lkgPathOverrideOf(process.env.CONFIG_LKG_PATH)),
1902
1901
  ...((h) => (h !== undefined ? { remoteExecHomeDir: h } : {}))((() => {
1903
1902
  const raw = process.env.REMOTE_EXEC_HOME_DIR;
1904
1903
  if (raw === undefined || raw === "")
@@ -2337,6 +2336,7 @@ const ORCHESTRATION_GROUP_KEYS = [
2337
2336
  "backgroundAgentParkClaimStaleMs", "delegationEntryCaps", "rosterRetentionMs", "leaderRunStaleMs", "scratchpadSweepTtlMs", "sandboxPkgSource",
2338
2337
  "sessionAutoTitle", "sessionAutoTitleSource", "selectEnvironmentTool", "envFactsEnabled", "toolDeferLongtail", "lspEnabled", "lspHostEnabled", "lspEnvAllow",
2339
2338
  "imageBakes", "readFace", "readFaceSource", "taskWriteFaceEscalation", "readDenyPatterns", "readDenyBuiltinTiers", "readDenyBuiltinExclude",
2339
+ "agentConfigDir", "configLkgPath",
2340
2340
  ];
2341
2341
  const LIMITS_HTTP_GROUP_KEYS = [
2342
2342
  "port", "attachmentOrphanGraceMs", "workspaceFileMaxBytes", "attachmentMaxBytes", "attachmentMimeAllowlist",
@@ -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 实参 —— 从折叠属主的签名位**推导**,不复制形状
@@ -10,6 +11,8 @@ export interface DeploymentGovernanceConfigView {
10
11
  readonly commandPolicy?: ServiceConfig["commandPolicy"];
11
12
  readonly manualModeShellGate?: ServiceConfig["manualModeShellGate"];
12
13
  readonly sensitiveWritePatterns: ServiceConfig["sensitiveWritePatterns"];
14
+ /** core 7.46.0 #1419:部署声明的配置根 —— 守卫集的 `.sema/…` 模式在根下同样守(与 Runner 的同名座同一个值,`loadConfig` 铸)。 */
15
+ readonly agentConfigDir?: ServiceConfig["agentConfigDir"];
13
16
  /**
14
17
  * 本部署的**本地店根**(`LOCAL_DATA_ROOT → CONFIG_LOCAL_DIR → AGENT_DATA_DIR → ~/.ai-agent`)—— 在本口只喂
15
18
  * **誊本门**(`<localDataRoot>/sessions`)。S-133 起它**不再**当守卫集的 `dataRoot`(那是引擎记忆根,见
@@ -91,8 +94,10 @@ export interface DurableApprovalSeat {
91
94
  readonly kind: "durable";
92
95
  /** 活体问答面;`undefined` ⇒ 恒 durable park 的原形门。 */
93
96
  readonly question: LiveQuestionFace | undefined;
97
+ /** 本会话的「本会话放行」读面(部署名单那一问的站着的答案;铸点 `approval.ts` 的 `createDeploymentListExemption`)。缺席 = 这条腿没有会话 / 没有豁免店 ⇒ 名单照问。 */
98
+ readonly exemption?: DeploymentListExemption;
94
99
  }
95
- /** live 席:Runner 的活 onAsk 当场服 ask(无 park)。 */
100
+ /** live 席:Runner 的活 onAsk 当场服 ask(无 park、无会话豁免店:run-local 一次性腿)。 */
96
101
  export interface LiveApprovalSeat {
97
102
  readonly kind: "live";
98
103
  }
@@ -182,7 +187,7 @@ type WriteTargetForm = "unstamped-relative" | "absolute-or-stamped";
182
187
  * · `writeTargetGuard`(守卫集策略自带,shell 写站问「这个绝对目标守不守」)—— 只在本层的形**覆盖守卫的入参形**时带上
183
188
  * ({@link WRITE_TARGET_GUARD_DOMAIN}):真身层带;词法层不带(它的 env 对绝对形报错 ⇒ core 的「判不了就守」会把每一次
184
189
  * shell 写都判成受守,正与两层射程互补的原设计相反)。这不是给某一形开的特例,是同一条「成员描述它守得住的那部分」的读法;
185
- * · `projection` / `pathDenyRules` / `nameSets` —— 守卫集策略不带,本包装也不报(报了就是替它声称一张它没有的拒表)。
190
+ * · `projection` / `pathRules` / `nameSets` —— 守卫集策略不带,本包装也不报(报了就是替它声称一张它没有的拒表)。
186
191
  * 修前(core 7.33.x)这里是一只 `{ check }` 字面量,7.34.0 起那只字面量把真身层的守卫剥掉了 —— 沙箱 lane 的 shell 写站
187
192
  * 因此看不见部署守卫集(`cp x .env` 在 acceptEdits 下被引擎 place 并答掉)。
188
193
  */
@@ -263,7 +268,7 @@ export declare function buildApprovalGrantNotice(config: {
263
268
  * `undefined`**:治理层是 tighten-only 的叠加层,没有基线可叠时它自己也产不出「门在场」这件事。
264
269
  *
265
270
  * 三形,按席穷举(S-694,席的定义见 {@link ApprovalSeat}):
266
- * · `durable` ⇒ AskUserQuestion 判决门 + 三张名单的审批门(deny/neverAuto/预算全在
271
+ * · `durable` ⇒ AskUserQuestion 判决门 + 三张名单的审批门(deny/neverAuto/预算/本会话的豁免行全在
267
272
  * `createDurableAskPolicy` 里)。gated `ask` 由 core 变成 durable checkpoint suspend。
268
273
  * · `live` ⇒ **同一只**三张名单审批门,不叠问答门。gated `ask` 由 core 交给 Runner 的活 onAsk 席
269
274
  * (本口**不**做 ask→deny 映射:无人可问时的拒由那只席说,判据与文案都归它)。
@@ -87,18 +87,20 @@ export function createDeploymentGovernanceInputs(config, pathAdjudication, roots
87
87
  const sensitivePathLayers = config.sensitiveWritePatterns.length > 0
88
88
  ? (() => {
89
89
  const dataRootOpt = roots.engineDataRoot !== undefined ? { dataRoot: roots.engineDataRoot } : {};
90
+ const configRootOpt = config.agentConfigDir !== undefined ? { agentConfigDir: config.agentConfigDir } : {};
90
91
  const realTarget = createSensitivePathPolicy({
91
92
  env: pathAdjudication.env,
92
93
  patterns: config.sensitiveWritePatterns,
93
94
  ...(pathAdjudication.cwd !== undefined ? { rootPath: pathAdjudication.cwd } : {}),
94
95
  ...dataRootOpt,
96
+ ...configRootOpt,
95
97
  });
96
98
  const transcriptGuard = config.localDataRoot !== undefined ? [createTranscriptIntegrityPolicy({ sessionsDirs: [join(config.localDataRoot, "sessions")] })] : [];
97
99
  if (pathAdjudication.cwd !== undefined)
98
100
  return [realTarget, ...transcriptGuard];
99
101
  return [
100
102
  scopedToWriteTargetForm(realTarget, "absolute-or-stamped"),
101
- scopedToWriteTargetForm(createSensitivePathPolicy({ env: new RelativeTargetLexicalEnv(), patterns: config.sensitiveWritePatterns, ...dataRootOpt }), "unstamped-relative"),
103
+ scopedToWriteTargetForm(createSensitivePathPolicy({ env: new RelativeTargetLexicalEnv(), patterns: config.sensitiveWritePatterns, ...dataRootOpt, ...configRootOpt }), "unstamped-relative"),
102
104
  ...transcriptGuard,
103
105
  ];
104
106
  })()
@@ -126,9 +128,9 @@ export function buildApprovalGrantNotice(config) {
126
128
  export function createApprovalBaselinePolicy(config, seat) {
127
129
  switch (seat.kind) {
128
130
  case "durable":
129
- return combinePolicies(createDurableQuestionGate(seat.question), createApprovalListPolicy(config));
131
+ return combinePolicies(createDurableQuestionGate(seat.question), createApprovalListPolicy(config, seat.exemption));
130
132
  case "live":
131
- return createApprovalListPolicy(config);
133
+ return createApprovalListPolicy(config, undefined);
132
134
  case "absent":
133
135
  if (hasApprovalListIntent(config)) {
134
136
  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 +144,13 @@ export function createApprovalBaselinePolicy(config, seat) {
142
144
  }
143
145
  }
144
146
  }
145
- function createApprovalListPolicy(config) {
147
+ function createApprovalListPolicy(config, exemption) {
146
148
  return createDurableAskPolicy({
147
149
  requireApproval: config.approvalRequire,
148
150
  deny: config.approvalDeny,
149
151
  autoBudget: config.approvalAutoBudget,
150
152
  neverAuto: config.approvalNeverAuto,
153
+ ...(exemption !== undefined ? { exemption } : {}),
151
154
  });
152
155
  }
153
156
  //# sourceMappingURL=deployment-governance.js.map
@@ -161,6 +161,13 @@ export declare const DEVICE_TEXT_LIMITS: {
161
161
  readonly connEpoch: 64;
162
162
  readonly replicaId: 128;
163
163
  };
164
+ /**
165
+ * 设备侧路径「是不是绝对路径」的**唯一判据**(v1 设备都是 POSIX:`platformOs` 闭集 `darwin` | `linux`;用 `posix` 而不是 `node:path`,
166
+ * server 跑在什么平台上与设备路径语义无关)。两处读它、不写第二份:入籍口(`http/routes/devices.ts`,`workspaceRoot` 不过 ⇒ 400)与
167
+ * 铸 env(`plugins/remote-env-device.ts` `effectiveDeviceCwd`,有效工作目录不过 ⇒ 响亮拒;那一道守的是入籍口收紧之前落库的存量行与
168
+ * 会话 / 委派链来的 cwd)。
169
+ */
170
+ export declare function isDeviceAbsolutePath(p: string): boolean;
164
171
  export declare function assertDeviceOwnerShape(owner: DeviceOwner): void;
165
172
  export declare function assertDeviceIdShape(deviceId: string): void;
166
173
  export declare function assertRootSessionIdShape(rootSessionId: string): void;
@@ -1,4 +1,5 @@
1
1
  import { createHash, randomBytes, randomInt } from "node:crypto";
2
+ import { posix } from "node:path";
2
3
  export const DEVICE_ERROR_CODES = {
3
4
  NOT_BOUND: "device.not_bound",
4
5
  OFFLINE: "device.offline",
@@ -86,6 +87,9 @@ export const DEVICE_TEXT_LIMITS = {
86
87
  connEpoch: 64,
87
88
  replicaId: 128,
88
89
  };
90
+ export function isDeviceAbsolutePath(p) {
91
+ return posix.isAbsolute(p);
92
+ }
89
93
  export function assertDeviceOwnerShape(owner) {
90
94
  assertIdentityKey("owner.tenant", owner.tenant, DEVICE_KEY_MAX_BYTES);
91
95
  assertIdentityKey("owner.subject", owner.subject, DEVICE_KEY_MAX_BYTES);
@@ -425,7 +425,20 @@ export function createDeviceWsHub(options) {
425
425
  clearTimeout(conn.handshakeTimer);
426
426
  conn.handshakeTimer = null;
427
427
  releasePreAuthSlot(conn);
428
- sendServerFrame(conn, { t: "helloAck", ok: true, heartbeatIntervalMs, maxInflight: maxInflightPerDevice, gen: conn.genStr, pendingInstructions: pendingIds, chunkWatermarks });
428
+ const helloAck = {
429
+ t: "helloAck",
430
+ ok: true,
431
+ heartbeatIntervalMs,
432
+ maxInflight: maxInflightPerDevice,
433
+ gen: conn.genStr,
434
+ pendingInstructions: pendingIds,
435
+ chunkWatermarks,
436
+ reconnectGraceMs,
437
+ cancelGraceMs,
438
+ chunkGapTimeoutMs,
439
+ executionCeilingGraceMs: execGraceMs,
440
+ };
441
+ sendServerFrame(conn, helloAck);
429
442
  logger?.info?.("device_connected", { deviceId: row.deviceId, generation: acquired.generation, epoch: frame.epoch, replicaId });
430
443
  metrics?.inc("device_connected_total");
431
444
  }
@@ -469,12 +482,7 @@ export function createDeviceWsHub(options) {
469
482
  }
470
483
  }
471
484
  function failOnChunkGap(p) {
472
- const missing = p.nextStreamSeq;
473
- settle(p, {
474
- ok: false,
475
- errorCode: "outcome_unknown",
476
- message: `the output stream had a gap (missing chunk streamSeq ${missing}) so the result cannot be reported faithfully — ${OUTCOME_UNKNOWN_SENTENCE}`,
477
- });
485
+ settle(p, outcomeUnknown(`the output stream had a gap (missing chunk streamSeq ${p.nextStreamSeq}) so the result cannot be reported faithfully`));
478
486
  }
479
487
  function chunkPayloadBytes(frame) {
480
488
  return frame.dataB64 === undefined ? 0 : Math.floor((frame.dataB64.length * 3) / 4);
@@ -549,11 +557,11 @@ export function createDeviceWsHub(options) {
549
557
  if (complaint !== null) {
550
558
  metrics?.inc("device_stream_terminator_missing_total");
551
559
  sendServerFrame(conn, { t: "resultAck", gen: conn.genStr, instructionId: p.instructionId });
552
- settle(p, { ok: false, errorCode: "outcome_unknown", message: `${complaint} — ${OUTCOME_UNKNOWN_SENTENCE}` });
560
+ settle(p, outcomeUnknown(complaint));
553
561
  return;
554
562
  }
555
563
  sendServerFrame(conn, { t: "resultAck", gen: conn.genStr, instructionId: p.instructionId });
556
- settle(p, outcome);
564
+ settle(p, !outcome.ok && outcome.errorCode === "outcome_unknown" ? outcomeUnknown(outcome.message) : outcome);
557
565
  }
558
566
  function onDeviceText(conn, text) {
559
567
  conn.lastFrameAt = Date.now();
@@ -235,6 +235,21 @@ export type DeviceHelloAckFrame = {
235
235
  gen: string;
236
236
  pendingInstructions: string[];
237
237
  chunkWatermarks: Record<string, number>;
238
+ /**
239
+ * 四个**宽限位**(additive,协议 §5.6 不升版):本连接所在 server 副本**此刻生效**的四只预算,值与汇聚端
240
+ * 计时器读的是同一只常量 —— 不是文档里的「默认值」,是这台部署的实际值。executor 据此定自己的重连
241
+ * 退避上界 / 取消应答时限 / 补片节奏 / 执行兜底预算,不必抄文档里的数。
242
+ * · `reconnectGraceMs`:断连后在途不判丢的窗(R4;运维旋钮 `DEVICE_RECONNECT_GRACE_MS`);
243
+ * · `cancelGraceMs`:`cancel` 送达后等终局回执的窗(R11);
244
+ * · `chunkGapTimeoutMs`:等缺失 `streamSeq` 的上限(R8);
245
+ * · `executionCeilingGraceMs`:`timeoutMs` 之后云侧兜底再等的宽限(R12:`timeoutMs` + 本值)。
246
+ * **可选位**:7.120+ 服务端恒发;缺席 = 老服务端,按协议 §1.5 回落文档缺省(不得当 0)。型上标可选,是为了让拿这只型读老服务端帧的
247
+ * 消费方被编译器逼着处理缺席;本服务自己的铸点按 `Required<DeviceHelloAckFrame>` 构造(`device-ws-hub.ts`),漏一位编译红。
248
+ */
249
+ reconnectGraceMs?: number;
250
+ cancelGraceMs?: number;
251
+ chunkGapTimeoutMs?: number;
252
+ executionCeilingGraceMs?: number;
238
253
  };
239
254
  export type DeviceHelloRejectFrame = {
240
255
  t: "helloReject";
@@ -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";
@@ -68,6 +69,10 @@ const WORKER_SWAP_REDEEMABLE = {
68
69
  write_face_damaged: false,
69
70
  skill_grants_damaged: false,
70
71
  constraint_chain_missing: false,
72
+ hands_coordinates_damaged: false,
73
+ installation_record_absent: true,
74
+ installation_record_misaligned: false,
75
+ installation_record_invalid: false,
71
76
  };
72
77
  function checkpointRowRedeemableElsewhere(reason) {
73
78
  return reason !== undefined && WORKER_SWAP_REDEEMABLE[reason] === true;
@@ -274,7 +279,7 @@ export function createResumeLegs(ctx) {
274
279
  const rememberableTool = paForGrant !== undefined &&
275
280
  paForGrant.toolName &&
276
281
  paForGrant.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
277
- standingAnswerDurablyRecordable(parkRowStandingAnswerFacts(paForGrant, cp.gate, deps.config))
282
+ standingAnswerDurablyRecordable(parkRowStandingAnswerFacts(paForGrant, cp.gate, deps.config, deploymentListAsks(deps.config, { toolName: paForGrant.toolName })))
278
283
  ? paForGrant.toolName
279
284
  : null;
280
285
  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";