@sema-agent/server 7.101.1 → 7.102.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 (82) hide show
  1. package/MIGRATION.md +23 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/USAGE.md +58 -4
  5. package/dist/approval-card.d.ts +35 -2
  6. package/dist/approval-card.js +19 -1
  7. package/dist/background-session-reap.d.ts +55 -0
  8. package/dist/background-session-reap.js +90 -0
  9. package/dist/boot/config-center.d.ts +16 -4
  10. package/dist/boot/config-center.js +44 -15
  11. package/dist/boot/exit-boundary.js +17 -0
  12. package/dist/boot/runner-deps.js +1 -0
  13. package/dist/boot/runtime-caps.js +3 -2
  14. package/dist/boot/session-faces.js +3 -4
  15. package/dist/boot/shutdown.d.ts +2 -1
  16. package/dist/boot/shutdown.js +33 -12
  17. package/dist/boot/stage-07-capability-layer.d.ts +1 -1
  18. package/dist/boot/stage-07-capability-layer.js +6 -4
  19. package/dist/boot/stage-08-reapers.d.ts +1 -1
  20. package/dist/boot/stage-09-leader.d.ts +1 -1
  21. package/dist/boot/stage-10-http-server.d.ts +1 -1
  22. package/dist/boot/stage-10-http-server.js +7 -3
  23. package/dist/capabilities/builtin-tools.d.ts +14 -1
  24. package/dist/capabilities/builtin-tools.js +3 -0
  25. package/dist/capabilities/scenarios.d.ts +1 -1
  26. package/dist/capabilities/scenarios.js +3 -3
  27. package/dist/center-credential-renewer.d.ts +76 -0
  28. package/dist/center-credential-renewer.js +359 -0
  29. package/dist/center-credential.d.ts +76 -22
  30. package/dist/center-credential.js +119 -43
  31. package/dist/config-catalog.js +9 -4
  32. package/dist/config-center/apply-effective.js +5 -12
  33. package/dist/config-center/center-request.d.ts +13 -0
  34. package/dist/config-center/center-request.js +23 -3
  35. package/dist/config-center/facade.d.ts +1 -1
  36. package/dist/config-center/http-client.js +2 -5
  37. package/dist/config-center/read-warnings.d.ts +19 -48
  38. package/dist/config-center/read-warnings.js +7 -49
  39. package/dist/config-center/restart-signal.d.ts +8 -11
  40. package/dist/config-center/types.d.ts +4 -2
  41. package/dist/config-provider.js +1 -48
  42. package/dist/config-types.d.ts +77 -5
  43. package/dist/config-types.js +5 -0
  44. package/dist/config.js +49 -13
  45. package/dist/device-store.d.ts +1 -1
  46. package/dist/fleet-client.d.ts +21 -7
  47. package/dist/fleet-client.js +41 -12
  48. package/dist/fleet-lease.d.ts +1 -1
  49. package/dist/http/approval-sweeps.js +5 -20
  50. package/dist/http/dispatch.js +20 -14
  51. package/dist/http/readiness.d.ts +87 -0
  52. package/dist/http/readiness.js +54 -0
  53. package/dist/http/resume-legs.js +5 -4
  54. package/dist/http/route-table.d.ts +7 -0
  55. package/dist/http/route-table.js +6 -0
  56. package/dist/http/routes/a2a-serve.js +6 -14
  57. package/dist/http/routes/approvals-assistant.js +1 -0
  58. package/dist/http/routes/background.d.ts +80 -0
  59. package/dist/http/routes/background.js +94 -0
  60. package/dist/http/routes/capabilities.js +2 -0
  61. package/dist/http/server.d.ts +21 -8
  62. package/dist/http/server.js +2 -2
  63. package/dist/model-entry-refusal.d.ts +11 -10
  64. package/dist/model-entry-refusal.js +13 -10
  65. package/dist/observability/fail-open.d.ts +2 -2
  66. package/dist/observability/fail-open.js +2 -2
  67. package/dist/parked-decide.d.ts +11 -5
  68. package/dist/parked-decide.js +4 -1
  69. package/dist/parked-revive-surface.d.ts +47 -0
  70. package/dist/parked-revive-surface.js +20 -0
  71. package/dist/plugins/background-shell-support.d.ts +20 -1
  72. package/dist/plugins/background-shell-support.js +26 -0
  73. package/dist/plugins/host-platform.d.ts +11 -1
  74. package/dist/plugins/host-platform.js +11 -0
  75. package/dist/plugins/remote-env-host.d.ts +14 -1
  76. package/dist/plugins/remote-env-host.js +18 -5
  77. package/dist/tool-approval.d.ts +30 -1
  78. package/dist/tool-approval.js +31 -8
  79. package/dist/trace/core-keyset-guard.d.ts +3 -3
  80. package/dist/trace/engine-notice-wire.d.ts +1 -1
  81. package/dist/trace/engine-notice-wire.js +2 -0
  82. package/package.json +3 -3
@@ -7,7 +7,7 @@ import { openSync, closeSync, readSync, statSync, truncateSync, unlinkSync, mkdi
7
7
  import { StringDecoder } from "node:string_decoder";
8
8
  import { armPipeDestroyGrace, numEnvOr, SYMLINK_CHAIN_MAX_HOPS } from "./remote-shell.js";
9
9
  import { hostBackgroundShellEnabled, hostExecSpoolEnabled } from "../config.js";
10
- import { resolveHostShell, hostShell, spawnGroupOptions, killTreeHard, killTreeSoft, collapseWin32EnvKeys, adoptHostForegroundChild } from "./host-platform.js";
10
+ import { resolveHostShell, hostShell, spawnGroupOptions, killTreeHard, killTreeSoft, hardKillGroupNow, collapseWin32EnvKeys, adoptHostForegroundChild } from "./host-platform.js";
11
11
  import { BackgroundShellManager, seedMemStream, feedMemStream, drainMemStream } from "./background-shell-support.js";
12
12
  import { FileError, ExecutionError, RemoteExecutionError, RollingTailBuffer, markTruncated, SchedulerError, BackgroundShellError, } from "@sema-agent/core";
13
13
  import { recordSecretEnvScrub } from "../observability/secret-env-scrub.js";
@@ -21,7 +21,7 @@ const HOST_BG_DEFAULT_TIMEOUT_SEC = numEnvOr("HOST_BG_DEFAULT_TIMEOUT_SEC", 300,
21
21
  const HOST_BG_MAX_TIMEOUT_SEC = numEnvOr("HOST_BG_MAX_TIMEOUT_SEC", 1800, HOST_BG_DEFAULT_TIMEOUT_SEC);
22
22
  const HOST_BG_READ_CAP = numEnvOr("HOST_BG_READ_CAP", 1024 * 1024, 64 * 1024);
23
23
  const HOST_BG_FILE_CAP = numEnvOr("HOST_BG_FILE_CAP", 64 * 1024 * 1024, HOST_BG_READ_CAP);
24
- const HOST_BG_KILL_GRACE_MS = numEnvOr("HOST_BG_KILL_GRACE_MS", 1000, 0);
24
+ export const HOST_BG_KILL_GRACE_MS = numEnvOr("HOST_BG_KILL_GRACE_MS", 1000, 0);
25
25
  const HOST_BG_MEM_CAP = numEnvOr("HOST_BG_MEM_CAP", 8 * 1024 * 1024, 64 * 1024);
26
26
  const ok = (value) => ({ ok: true, value });
27
27
  const UNC_SPELLING = /^[\\/]{2}[^\\/]+[\\/]+[^\\/]/;
@@ -253,7 +253,7 @@ export class RemoteHostExecutionEnv {
253
253
  await fs.rm(dir, { recursive: true, force: true }).catch(() => { });
254
254
  throw new BackgroundShellError("spawn_failed", "host background spawn produced no pid");
255
255
  }
256
- return { pid, dir, outPath, errPath, stdoutCursor: 0, stderrCursor: 0 };
256
+ return { pid, child, dir, outPath, errPath, stdoutCursor: 0, stderrCursor: 0 };
257
257
  });
258
258
  },
259
259
  async read(state) {
@@ -334,6 +334,16 @@ export class RemoteHostExecutionEnv {
334
334
  if (state.dir)
335
335
  await fs.rm(state.dir, { recursive: true, force: true }).catch(() => { });
336
336
  },
337
+ killNow(state) {
338
+ if (state.hardKilled === true)
339
+ return "already_gone";
340
+ if (state.child.exitCode !== null || state.child.signalCode !== null)
341
+ return "already_gone";
342
+ const outcome = hardKillGroupNow(state.pid);
343
+ if (outcome === "killed")
344
+ state.hardKilled = true;
345
+ return outcome;
346
+ },
337
347
  };
338
348
  }
339
349
  spawnBackground(command, options) {
@@ -362,7 +372,7 @@ export class RemoteHostExecutionEnv {
362
372
  child.off("close", onClose);
363
373
  child.off("error", onErrEvt);
364
374
  };
365
- return { pid, stdoutCursor: 0, stderrCursor: 0, mem, detachCleanup };
375
+ return { pid, child, stdoutCursor: 0, stderrCursor: 0, mem, detachCleanup };
366
376
  });
367
377
  return adopted?.shellId;
368
378
  }
@@ -372,6 +382,9 @@ export class RemoteHostExecutionEnv {
372
382
  killBackground(shellId) {
373
383
  return this.bgManager.kill(shellId);
374
384
  }
385
+ killBackgroundNow(shellId) {
386
+ return this._bgManager?.killNow(shellId) ?? "already_gone";
387
+ }
375
388
  disposeBackgroundShells(opts) {
376
389
  return this._bgManager ? this._bgManager.dispose(opts) : Promise.resolve();
377
390
  }
@@ -782,7 +795,7 @@ export class RemoteHostExecutionEnv {
782
795
  child.off("close", onClose);
783
796
  child.off("error", onErrEvt);
784
797
  };
785
- return { pid, dir: spool.dir, outPath: spool.outPath, errPath: spool.errPath, stdoutCursor: cursors.out, stderrCursor: cursors.err, detachCleanup, ...(fgRemovedBytes > 0 ? { preSpoolBase: fgRemovedBytes, pendingDropped: fgRemovedBytes } : {}) };
798
+ return { pid, child, dir: spool.dir, outPath: spool.outPath, errPath: spool.errPath, stdoutCursor: cursors.out, stderrCursor: cursors.err, detachCleanup, ...(fgRemovedBytes > 0 ? { preSpoolBase: fgRemovedBytes, pendingDropped: fgRemovedBytes } : {}) };
786
799
  });
787
800
  return adopted?.shellId;
788
801
  }
@@ -1,6 +1,6 @@
1
1
  import type { HostDecidedBy, HostSettledBy } from "./host-decision.js";
2
2
  import type { IncomingMessage } from "node:http";
3
- import { type AskRequest, type AskOutcome, type CheckpointGate, type PendingAction, type ReadRootGrantCandidate, type RuleOffer } from "@sema-agent/core";
3
+ import { type AskRequest, type AskOutcome, type CheckpointGate, type PendingAction, type PersistedRuleMandate, type ReadRootGrantCandidate, type RuleOffer } from "@sema-agent/core";
4
4
  import { type CardRulePersisted, type RuleConsentLane } from "./rules-consent.js";
5
5
  import { type DenialLimitFallback, type RuleOffersAbsence, type ApprovalRequestFrame, type ApprovalRevokeFrame, type RuleEvidenceProjection } from "./approval-card.js";
6
6
  import { type ApprovalAskStore, type AskRow } from "./plugins/approval-ask-store-sql.js";
@@ -244,6 +244,16 @@ export interface ToolApprovalFrame {
244
244
  * 席照常答得了」说的是**这一次调用的应答者**,本仓不改它。
245
245
  */
246
246
  mandated?: true;
247
+ /**
248
+ * **S-721(core 7.32.0 #1092;ADDITIVE,`"tool_approval"` only)** —— 强制位**站在哪一个词上**:core `PERSISTED_RULE_MANDATES`
249
+ * 六词之一(`operator_always` / `tool_marks` / `probe_mandate` / `probe_unanswered` / `write_protection` /
250
+ * `write_protection_unresolved`,词义属主在 core)。只在 `mandated: true` 的**整因**是那一个词时在场;另一扇门(治理 / hook /
251
+ * 祖先)或不止一个理由 ⇒ 缺席 —— **缺席从不命名一个词**,也不等于「不强制」(读 `mandated`)。值经 `approval-card.ts` 的
252
+ * `readClosedWordSeat(req, "mandate")` 窄读(闭集筛 = core `isPersistedRuleMandate`,本仓不抄词表;集外词读作缺席 + 计一次)。echo-only:
253
+ * 本仓不按词分支。耐久面同一个词:运维队列行(`parkRowFacts`,park 行孪生座)与 inbox 行(core `CheckpointSummary.mandate`)。
254
+ * 与 `mandated` 同一条:`card_json` 卡面不带本键(`ApprovalCardSchema` `.strict()`)。
255
+ */
256
+ mandate?: PersistedRuleMandate;
247
257
  /**
248
258
  * **S-125③/[ref]**(core 7.5.0;**ADDITIVE**,`"tool_approval"` only)—— 这只 ask 的**出身**:
249
259
  * 谁提的这一问,一个 core 闭集词(`ASK_ORIGINS` **11 员**,core 7.14.0 现值:content_question /
@@ -263,6 +273,12 @@ export interface ToolApprovalFrame {
263
273
  * 🔴 **不枚举词表**(`string` 而不是本仓自铸的联合):词表单一属主在 core,在这里抄一份会把 core 加的
264
274
  * 新词吞成缺席 —— 与 `wiring_manifest.autoMode.reason` 的透传纪律逐字同规。缺席 = 老引擎 / 非 ask 路径。
265
275
  *
276
+ * 同一个词的其余四面(S-755 起全部在场):`livePending` 行顶层 / `live_pending` 帧(条目铸定时与本键**同一次**读定);运维队列行
277
+ * (`GET /v1/approvals` 的 `pending[]` + `/stream` 的 `pending` 帧,读 park 行孪生座 `PendingAction.tool_approval.origin`,经
278
+ * `approval-card.ts` 的 `readClosedWordSeat(…, "origin")` —— 那一面读的是持久层原字节、中间没有 core 投影,所以亲手调 core 的
279
+ * `isAskOrigin`);inbox 行(core `CheckpointSummary.origin`,core 投影处已筛)。本键读的是引擎在门上刚盖好的值(盖章点即 core
280
+ * 的筛点),所以只过「非空串」形门、不再筛第二遍 —— 对 core 盖得出的每一个词,五面逐字同值。
281
+ *
266
282
  * ⚠️ 与 {@link governanceForced} **不是**一回事,别互相顶替:本键答「哪一类权威提的问」,那一键答
267
283
  * 「**本部署运维治理层**是不是这只 ask 的门」。`origin: "policy"` 覆盖的是「任何部署 ToolPolicy 的
268
284
  * ask」,治理策略产的与普通策略产的在这一个词上同形 ⇒ 顶替不了(逐字论证见 `governance-ask-marks.ts` 顶注)。
@@ -530,6 +546,13 @@ export interface LivePendingRow {
530
546
  * 值 = 条目上那一位(`askBroadcast` 铸条目时取自这一次过闸的 `req.mandated`,与 {@link frame} 上的同名键同源)。此前只在
531
547
  * `frame` 里:同一个 `GET /v1/approvals` 响应的 durable `pending[]` 行顶层带它(S-663 二段),live 行却要进帧里读。 */
532
548
  mandated?: true;
549
+ /** S-721(core 7.32.0 #1092):强制位站在哪一个词上 —— 与 `mandated` 同层同形(在场逐字,缺席不编词),值 = 条目上那一个词
550
+ * (与 {@link frame} 上同名键同源;帧型该键顶注是全文)。 */
551
+ mandate?: PersistedRuleMandate;
552
+ /** S-755(cli L-695 / [ref]):ask 的**出身词**(谁提的这一问,core `ASK_ORIGINS` 闭集一词)—— 与 `mandate` 同层同形(在场逐字,
553
+ * 缺席不编词),值 = 条目上那一个词(铸条目那一次与 {@link frame} 上同名键**同一次**读定;帧型该键顶注是全文)。同一个
554
+ * `GET /v1/approvals` 响应里 durable `pending[]` 行的 `origin` 也在顶层(park 行孪生座),两族行同一种读法。 */
555
+ origin?: string;
533
556
  /** 发起者为委派子代(originTaskId 在场)。 */
534
557
  fromSubagent?: true;
535
558
  /** [ref] C2(additive):委派子代的 **canonical taskId**。值域精确成文(codex R1-F3 问询后按 core dist
@@ -695,6 +718,11 @@ export declare const PERSIST_RULE_BATCH_EXCLUSIVE_ERROR = "persistRule.batchOffe
695
718
  */
696
719
  export declare const SETTLED_BY_MUST_BE_STRING = "settledBy must be a string \u2014 the settlement source is one word from the engine's closed set, and this layer judges only the SHAPE (which word it is, the engine decides)";
697
720
  export declare const SETTLED_BY_DENY_ONLY = "settledBy is only meaningful with decision \"deny\" \u2014 only a person approves an action that EXECUTES, so a self-reported settlement source on an allow is a contradiction the engine refuses; the refusal would arrive too late to undo this door's own side effects (a session allow-all grant, a persisted rule, a DECIDED row that replays as a plain approval), so it is refused here, before the decision";
721
+ /** S-720(core 7.32.0 #1093)—— `readRootGrant` 只随 `decision:"allow"`(CC 卡的第三选项「Yes, and add directory」= 放行这一次
722
+ * + 加目录;`allow_session` 不是那一个选项,`deny` 上它没有意义)。裁决之前响亮拒,不静默丢。 */
723
+ export declare const READ_ROOT_GRANT_ALLOW_ONLY = "readRootGrant is only meaningful with decision \"allow\" \u2014 it is the card's \"Yes, and add directory\" answer (allow this call and add the card's readRootCandidate.dir to this run's read directories)";
724
+ /** S-720:`readRootGrant` 的形 —— 卡上 `readRootCandidate.dir` 的原串(非空,≤ core `READ_ROOT_CANDIDATE_DIR_MAX`;超限整只拒,不截:截过的目录是另一个目录)。 */
725
+ export declare const READ_ROOT_GRANT_SHAPE = "readRootGrant must be the card's readRootCandidate.dir sent back byte for byte \u2014 a non-empty string of at most 1024 characters";
698
726
  export declare const PERSIST_RULE_SCOPE_REFUSED = "this endpoint does not accept a rule scope \u2014 the server mints it from where the approval happened (send neither `scope` nor `persistRule.scope`)";
699
727
  /** Validate the respond body — the closed three-choice enum (rationale in the module header). */
700
728
  export declare function parseToolApprovalResponse(body: unknown): {
@@ -704,6 +732,7 @@ export declare function parseToolApprovalResponse(body: unknown): {
704
732
  persistRule?: ParsedPersistRule;
705
733
  note?: string;
706
734
  settledBy?: string;
735
+ readRootGrant?: string;
707
736
  } | {
708
737
  ok: false;
709
738
  error: string;
@@ -1,6 +1,6 @@
1
1
  import { contentKindOf } from "./approval-content-kind.js";
2
2
  import { AsyncLocalStorage } from "node:async_hooks";
3
- import { uuidv7, MAX_RULE_TEXT_CHARS, parseRuleText, pathConfinableWriteToolNames } from "@sema-agent/core";
3
+ import { uuidv7, MAX_RULE_TEXT_CHARS, READ_ROOT_CANDIDATE_DIR_MAX, parseRuleText, pathConfinableWriteToolNames } from "@sema-agent/core";
4
4
  import { precheckCardRuleText } from "./rules-consent.js";
5
5
  import { redactDeep, redactSecrets, redactSecretsWithArms } from "./trace/redact.js";
6
6
  import { hasBidiControls } from "./text-bidi.js";
@@ -8,7 +8,7 @@ import { approvalPayloadFingerprint } from "./plugins/approval-payload-fingerpri
8
8
  import { createLogger } from "./observability/logger.js";
9
9
  import { recordFailOpen } from "./observability/fail-open.js";
10
10
  import { deriveAskId, deriveAskScopedBatchId, deriveBatchId, MAX_DECISION_NOTE_CHARS } from "./approval-ask-machine.js";
11
- import { ApprovalCardEnvelopeSchema, MAX_RULE_OFFERS, buildApprovalCard, buildApprovalCardEnvelope, buildApprovalRequestFrame, buildRevokeFrame, readDenialLimitFallback, readProbeCause, readReadRootCandidate, readRuleEvidence, readRuleOffersAbsence, readRuleOffersAbsenceSeat, readParkRowTrueSeat, RuleOfferRawSchema, backfillLegacyBatchMemberKind, screenUncoveredDetailSeat, } from "./approval-card.js";
11
+ import { ApprovalCardEnvelopeSchema, MAX_RULE_OFFERS, buildApprovalCard, buildApprovalCardEnvelope, buildApprovalRequestFrame, buildRevokeFrame, readDenialLimitFallback, readProbeCause, readReadRootCandidate, readRuleEvidence, readRuleOffersAbsence, readRuleOffersAbsenceSeat, readParkRowTrueSeat, readClosedWordSeat, RuleOfferRawSchema, backfillLegacyBatchMemberKind, screenUncoveredDetailSeat, } from "./approval-card.js";
12
12
  import { POLICY_REFUSAL_DECISION, projectAskTerminalClaimTruth } from "./plugins/approval-ask-store-sql.js";
13
13
  import { settleByPolicy } from "./approval-policy-settlement.js";
14
14
  import { governanceAskMarksFor, runWithGovernanceAskScope } from "./governance-ask-marks.js";
@@ -111,6 +111,8 @@ function respondSettledBy(parsed) {
111
111
  return parsed.settledBy;
112
112
  }
113
113
  export const SETTLED_BY_DENY_ONLY = 'settledBy is only meaningful with decision "deny" — only a person approves an action that EXECUTES, so a self-reported settlement source on an allow is a contradiction the engine refuses; the refusal would arrive too late to undo this door\'s own side effects (a session allow-all grant, a persisted rule, a DECIDED row that replays as a plain approval), so it is refused here, before the decision';
114
+ export const READ_ROOT_GRANT_ALLOW_ONLY = 'readRootGrant is only meaningful with decision "allow" — it is the card\'s "Yes, and add directory" answer (allow this call and add the card\'s readRootCandidate.dir to this run\'s read directories)';
115
+ export const READ_ROOT_GRANT_SHAPE = `readRootGrant must be the card's readRootCandidate.dir sent back byte for byte — a non-empty string of at most ${READ_ROOT_CANDIDATE_DIR_MAX} characters`;
114
116
  export const PERSIST_RULE_SCOPE_REFUSED = "this endpoint does not accept a rule scope — the server mints it from where the approval happened (send neither `scope` nor `persistRule.scope`)";
115
117
  export function parseToolApprovalResponse(body) {
116
118
  if (body === null || typeof body !== "object" || Array.isArray(body))
@@ -161,6 +163,12 @@ export function parseToolApprovalResponse(body) {
161
163
  if (rawSettledBy !== undefined && typeof rawSettledBy !== "string")
162
164
  return { ok: false, error: SETTLED_BY_MUST_BE_STRING };
163
165
  const settledBy = rawSettledBy;
166
+ const rawGrant = body.readRootGrant;
167
+ if (rawGrant !== undefined && d !== "allow")
168
+ return { ok: false, error: READ_ROOT_GRANT_ALLOW_ONLY };
169
+ if (rawGrant !== undefined && (typeof rawGrant !== "string" || rawGrant === "" || rawGrant.length > READ_ROOT_CANDIDATE_DIR_MAX))
170
+ return { ok: false, error: READ_ROOT_GRANT_SHAPE };
171
+ const readRootGrant = rawGrant;
164
172
  return {
165
173
  ok: true,
166
174
  value: d,
@@ -168,6 +176,7 @@ export function parseToolApprovalResponse(body) {
168
176
  ...(persistRule !== undefined ? { persistRule } : {}),
169
177
  ...(note !== undefined ? { note } : {}),
170
178
  ...(rawSettledBy !== undefined ? { settledBy } : {}),
179
+ ...(readRootGrant !== undefined ? { readRootGrant } : {}),
171
180
  };
172
181
  }
173
182
  return { ok: false, error: 'decision must be one of "allow" | "allow_session" | "deny"' };
@@ -938,6 +947,8 @@ export class ToolApprovalCoordinator {
938
947
  const bounded = boundArgs(req.args);
939
948
  let effectiveGovernanceForced = governanceForced;
940
949
  let ruleLaneMaterial = this.buildRuleLaneMaterial(req, origin.owner, governanceForced, origin.sessionId);
950
+ const mandateWord = readClosedWordSeat(req, "mandate");
951
+ const originWord = typeof req.origin === "string" && req.origin.length > 0 ? req.origin : undefined;
941
952
  const frame = {
942
953
  type: "tool_approval",
943
954
  approvalId: id,
@@ -963,7 +974,8 @@ export class ToolApprovalCoordinator {
963
974
  ...(governanceForced ? { governanceForced: true } : {}),
964
975
  ...(req.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
965
976
  ...(req.mandated === true ? { mandated: true } : {}),
966
- ...(typeof req.origin === "string" && req.origin.length > 0 ? { origin: req.origin } : {}),
977
+ ...(mandateWord !== undefined ? { mandate: mandateWord } : {}),
978
+ ...(originWord !== undefined ? { origin: originWord } : {}),
967
979
  ...(typeof req.persistedRuleShadowed === "string" && req.persistedRuleShadowed !== ""
968
980
  ? { persistedRuleShadowed: redactSecrets(req.persistedRuleShadowed).slice(0, MAX_RULE_TEXT_CHARS) }
969
981
  : {}),
@@ -1170,11 +1182,17 @@ export class ToolApprovalCoordinator {
1170
1182
  return unattended === "unavailable" ? unattended : shapeDeny(unattended);
1171
1183
  }
1172
1184
  if (settled.allowed) {
1173
- return settled.updatedInput !== undefined ? { allow: true, updatedInput: settled.updatedInput } : true;
1185
+ if (settled.updatedInput === undefined && settled.readRootGrant === undefined)
1186
+ return true;
1187
+ return {
1188
+ allow: true,
1189
+ ...(settled.updatedInput !== undefined ? { updatedInput: settled.updatedInput } : {}),
1190
+ ...(settled.readRootGrant !== undefined ? { readRootGrant: settled.readRootGrant } : {}),
1191
+ };
1174
1192
  }
1175
1193
  return shapeDeny(false);
1176
1194
  };
1177
- const settle = (allowed, outcome, updatedInput, hostSettledBy, denyReason) => {
1195
+ const settle = (allowed, outcome, updatedInput, hostSettledBy, denyReason, readRootGrant) => {
1178
1196
  if (done)
1179
1197
  return;
1180
1198
  done = true;
@@ -1208,7 +1226,7 @@ export class ToolApprovalCoordinator {
1208
1226
  ...(hostSettledBy !== undefined ? { settledBy: hostSettledBy } : {}),
1209
1227
  settledAtMs: Date.now(),
1210
1228
  });
1211
- resolveAllowed({ allowed, ...(allowed && updatedInput !== undefined ? { updatedInput } : {}) });
1229
+ resolveAllowed({ allowed, ...(allowed && updatedInput !== undefined ? { updatedInput } : {}), ...(allowed && readRootGrant !== undefined ? { readRootGrant } : {}) });
1212
1230
  };
1213
1231
  const broadcastClaimTruth = (read) => {
1214
1232
  if (askId === undefined)
@@ -1483,6 +1501,8 @@ export class ToolApprovalCoordinator {
1483
1501
  ...(effectiveGovernanceForced ? { governanceForced: true } : {}),
1484
1502
  ...(req.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
1485
1503
  ...(req.mandated === true ? { mandated: true } : {}),
1504
+ ...(mandateWord !== undefined ? { mandate: mandateWord } : {}),
1505
+ ...(originWord !== undefined ? { origin: originWord } : {}),
1486
1506
  ...(askId !== undefined && batchId !== undefined ? { askId, batchId } : {}),
1487
1507
  };
1488
1508
  selfEntry = pendingEntry;
@@ -2051,7 +2071,7 @@ export class ToolApprovalCoordinator {
2051
2071
  approvalPayloadFingerprint({ updatedInput: ambiguousConfirmedRow.updatedInput }) === approvalPayloadFingerprint({ updatedInput: parsed.updatedInput });
2052
2072
  const result = this.finishRespond(id, entry, parsed, {
2053
2073
  noteRecorded: noteLanded,
2054
- ...(winnerRow !== undefined ? { settleOutcome: outcomeOfDecidedRow(winnerRow) } : {}),
2074
+ ...(winnerRow !== undefined ? { settleOutcome: outcomeOfDecidedRow(winnerRow), settleOutcomeIsThisRequest: ambiguousConfirmedRow === undefined } : {}),
2055
2075
  ...(ambiguousConfirmedRow !== undefined ? { updatedInputDelivered: payloadIsMine } : {}),
2056
2076
  });
2057
2077
  if (winnerRow !== undefined)
@@ -2077,7 +2097,8 @@ export class ToolApprovalCoordinator {
2077
2097
  }
2078
2098
  const allowed = parsed.value !== "deny";
2079
2099
  const settleArgs = opts?.settleOutcome !== undefined ? settleArgsOfDecided(opts.settleOutcome) : { allowed, updatedInput: parsed.updatedInput, settledBy: respondSettledBy(parsed) };
2080
- entry.settle(settleArgs.allowed, settleArgs.allowed ? "allowed" : "denied", settleArgs.updatedInput, settleArgs.settledBy, settleArgs.allowed ? undefined : parsed.note);
2100
+ const readRootGrant = opts?.settleOutcome === undefined || opts.settleOutcomeIsThisRequest === true ? parsed.readRootGrant : undefined;
2101
+ entry.settle(settleArgs.allowed, settleArgs.allowed ? "allowed" : "denied", settleArgs.updatedInput, settleArgs.settledBy, settleArgs.allowed ? undefined : parsed.note, readRootGrant);
2081
2102
  const updatedInputDelivered = opts?.updatedInputDelivered ?? true;
2082
2103
  const noteRecorded = opts?.noteRecorded ?? false;
2083
2104
  return {
@@ -2107,6 +2128,8 @@ export class ToolApprovalCoordinator {
2107
2128
  ...(e.requiresRealApproval ? { requiresRealApproval: true } : {}),
2108
2129
  ...(e.governanceForced ? { governanceForced: true } : {}),
2109
2130
  ...(e.mandated ? { mandated: true } : {}),
2131
+ ...(e.mandate !== undefined ? { mandate: e.mandate } : {}),
2132
+ ...(e.origin !== undefined ? { origin: e.origin } : {}),
2110
2133
  ...(e.originTaskId !== undefined ? { fromSubagent: true, originTaskId: e.originTaskId } : {}),
2111
2134
  ...(contentKind !== undefined ? { contentKind } : {}),
2112
2135
  frame: e.frame.approvalId === id ? e.frame : { ...e.frame, approvalId: id },
@@ -43,7 +43,7 @@ type _GuardBgNotifReverse = AssertAllKeysHandled<Exclude<BgNotifProjected | BgNo
43
43
  type RosterProjected = "name" | "agentId" | "sessionId" | "toolUseId" | "owner" | "scope" | "sessionScoped" | "rootSessionId" | "model" | "modelFallback" | "createdAt";
44
44
  type _GuardRoster = AssertAllKeysHandled<Exclude<keyof RosterEntry, RosterProjected>>;
45
45
  type _GuardRosterReverse = AssertAllKeysHandled<Exclude<RosterProjected, keyof RosterEntry>>;
46
- type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation" | "ruleOffers" | "ruleOffersAbsence" | "persistedRuleShadowed" | "probeCause" | "ruleEvidence" | "requiresRealApproval" | "denialLimitFallback" | "origin" | "ruleStoreUnreadable" | "readRootCandidate" | "mandated";
46
+ type AskProjected = "toolName" | "toolCallId" | "args" | "message" | "sourceTaskId" | "fromSubagent" | "sourceAgentName" | "delegation" | "ruleOffers" | "ruleOffersAbsence" | "persistedRuleShadowed" | "probeCause" | "ruleEvidence" | "requiresRealApproval" | "denialLimitFallback" | "origin" | "ruleStoreUnreadable" | "readRootCandidate" | "mandated" | "mandate";
47
47
  type AskExcluded = "preview" | "principal" | "riskAxes" | "boundInputHash" | "isDelegatedChild" | "probeReason" | "hasBidiControls" | "previewWithheld" | "execCwd";
48
48
  type _GuardAsk = AssertAllKeysHandled<Exclude<keyof AskRequest, AskProjected | AskExcluded>>;
49
49
  type _GuardAskReverse = AssertAllKeysHandled<Exclude<AskProjected | AskExcluded, keyof AskRequest>>;
@@ -130,7 +130,7 @@ type _GuardPermissionTraceReverse = AssertAllKeysHandled<Exclude<PermissionTrace
130
130
  /** `T` 非 `never` 时收敛到 `never`(空集 ⇒ 残留 `"EMPTY"` ⇒ tsc 红)。 */
131
131
  type AssertNonEmpty<T> = [T] extends [never] ? "EMPTY" : never;
132
132
  type _GuardPermissionTraceNonEmpty = AssertAllKeysHandled<AssertNonEmpty<PermissionTraceKind>>;
133
- type SummaryProjected = "sessionId" | "scope" | "createdAt" | "gateKind" | "severity" | "spentMicroUsd" | "deadline" | "hasBidiControls" | "requiresRealApproval" | "denialLimitFallback" | "origin" | "ruleStoreUnreadable" | "mandated";
133
+ type SummaryProjected = "sessionId" | "scope" | "createdAt" | "gateKind" | "severity" | "spentMicroUsd" | "deadline" | "hasBidiControls" | "requiresRealApproval" | "denialLimitFallback" | "origin" | "ruleStoreUnreadable" | "mandated" | "mandate";
134
134
  type SummaryExcluded = "token" | "toolInput" | "toolName" | "toolCallId" | "preview" | "principal" | "sourceTaskId" | "contentKind" | "restoreMode" | "checkpointId" | "previewWithheld";
135
135
  type _GuardInboxSummary = AssertAllKeysHandled<Exclude<keyof CheckpointSummary, SummaryProjected | SummaryExcluded>>;
136
136
  type _GuardInboxSummaryReverse = AssertAllKeysHandled<Exclude<SummaryProjected | SummaryExcluded, keyof CheckpointSummary>>;
@@ -138,7 +138,7 @@ type ParkArmKeys = keyof Extract<PendingAction, {
138
138
  kind: "tool_approval";
139
139
  }>;
140
140
  type ParkArmQueueProjected = "toolName" | "toolCallId" | "boundInputHash" | "ruleOffers" | "hasBidiControls" | keyof import("../approval-card.js").ParkRowFacts;
141
- type ParkArmQueueExcluded = "kind" | "args" | "batchToolCallIds" | "completedCallIds" | "capabilityId" | "contractId" | "shapeDigest" | "implementationRevision" | "rosterDigest" | "family" | "pathTarget" | "preview" | "previewWithheld" | "execCwd" | "denialLimitFallback" | "origin" | "ruleStoreUnreadable";
141
+ type ParkArmQueueExcluded = "kind" | "args" | "batchToolCallIds" | "completedCallIds" | "capabilityId" | "contractId" | "shapeDigest" | "implementationRevision" | "rosterDigest" | "family" | "pathTarget" | "preview" | "previewWithheld" | "execCwd" | "denialLimitFallback" | "ruleStoreUnreadable";
142
142
  type _GuardParkArmQueue = AssertAllKeysHandled<Exclude<ParkArmKeys, ParkArmQueueProjected | ParkArmQueueExcluded>>;
143
143
  type _GuardParkArmQueueReverse = AssertAllKeysHandled<Exclude<ParkArmQueueProjected | ParkArmQueueExcluded, ParkArmKeys>>;
144
144
  type _NoDoubleBookParkArmQueue = AssertAllKeysHandled<Extract<ParkArmQueueProjected, ParkArmQueueExcluded>>;
@@ -16,7 +16,7 @@
16
16
  * 同文件 G1 e2e 的全码扇出夹具、以及契约文档附录 D.3 那张表(含**小节标题里的码数** —— 那道门本批
17
17
  * 刚立,立完就在下一次加码时自己咬住了)。
18
18
  */
19
- export declare const ENGINE_NOTICE_WIRE_CODES: readonly ["memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "task.turn_interrupted", "steering.parked_input_blocked", "task.user_steer_undrained", "task.user_followup_undrained", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "task.halt_unconsumed", "task.late_approval", "config.durable_gate_unavailable", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "mcp.injection_dropped", "delegation.ask_unresolvable", "task.interrupt_unconsumed", "instructions.source_changed"];
19
+ export declare const ENGINE_NOTICE_WIRE_CODES: readonly ["memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "task.turn_interrupted", "steering.parked_input_blocked", "task.user_steer_undrained", "task.user_followup_undrained", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "task.halt_unconsumed", "task.late_approval", "config.durable_gate_unavailable", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "mcp.injection_dropped", "delegation.ask_unresolvable", "task.interrupt_unconsumed", "approval.read_root_granted", "approval.read_root_grant_rejected", "instructions.source_changed"];
20
20
  export type EngineNoticeWireCode = (typeof ENGINE_NOTICE_WIRE_CODES)[number];
21
21
  /**
22
22
  * S-690 —— **本仓自铸**的通告码与它们的受众(码 / 铸点 / 受众三者的属主都是本仓)。
@@ -23,6 +23,8 @@ export const ENGINE_NOTICE_WIRE_CODES = [
23
23
  "mcp.injection_dropped",
24
24
  "delegation.ask_unresolvable",
25
25
  "task.interrupt_unconsumed",
26
+ "approval.read_root_granted",
27
+ "approval.read_root_grant_rejected",
26
28
  "instructions.source_changed",
27
29
  ];
28
30
  export const SERVER_NOTICE_AUDIENCE = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.101.1",
3
+ "version": "7.102.0",
4
4
  "description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -61,8 +61,8 @@
61
61
  "build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
62
62
  },
63
63
  "dependencies": {
64
- "@sema-agent/core": "7.31.1",
65
- "@sema-agent/settings-schema": "6.0.0",
64
+ "@sema-agent/core": "7.32.1",
65
+ "@sema-agent/settings-schema": "6.1.0",
66
66
  "e2b": "^2.28.0",
67
67
  "libsodium-wrappers": "^0.8.4",
68
68
  "mysql2": "^3.22.4",