@sema-agent/server 7.133.0 → 7.134.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 (185) hide show
  1. package/MIGRATION.md +87 -0
  2. package/README.md +2 -2
  3. package/README.zh-CN.md +2 -2
  4. package/USAGE.md +129 -11
  5. package/deploy/sema-up/README.md +16 -0
  6. package/deploy/sema-up/chart/Chart.yaml +1 -1
  7. package/deploy/sema-up/chart/templates/NOTES.txt +15 -0
  8. package/deploy/sema-up/chart/templates/_helpers.tpl +129 -0
  9. package/deploy/sema-up/chart/templates/sealed-key.yaml +24 -0
  10. package/deploy/sema-up/chart/templates/server-v4pro.yaml +14 -0
  11. package/deploy/sema-up/chart/templates/server.yaml +14 -0
  12. package/deploy/sema-up/chart/values-full-single.yaml +5 -1
  13. package/deploy/sema-up/chart/values.yaml +34 -0
  14. package/dist/approval-ask-audit-store.d.ts +1 -1
  15. package/dist/approval-card.d.ts +4 -4
  16. package/dist/approval-card.js +1 -2
  17. package/dist/approval-content-kind.d.ts +5 -2
  18. package/dist/approval-content-kind.js +16 -3
  19. package/dist/approval-gate-currency.d.ts +72 -0
  20. package/dist/approval-gate-currency.js +94 -0
  21. package/dist/approval.d.ts +6 -0
  22. package/dist/approval.js +31 -14
  23. package/dist/boot/coordinators.d.ts +6 -1
  24. package/dist/boot/coordinators.js +6 -3
  25. package/dist/boot/leader.d.ts +2 -0
  26. package/dist/boot/leader.js +2 -1
  27. package/dist/boot/read-deny-anchor-audit.js +2 -2
  28. package/dist/boot/runner-deps.d.ts +18 -5
  29. package/dist/boot/runner-deps.js +9 -0
  30. package/dist/boot/stage-06-runners.d.ts +4 -3
  31. package/dist/boot/stage-06-runners.js +15 -2
  32. package/dist/boot/stage-07-capability-layer.d.ts +2 -1
  33. package/dist/boot/stage-08-reapers.d.ts +2 -1
  34. package/dist/boot/stage-09-leader.d.ts +2 -1
  35. package/dist/boot/stage-09-leader.js +2 -2
  36. package/dist/boot/stage-10-http-server.d.ts +2 -1
  37. package/dist/boot/stage-10-http-server.js +2 -1
  38. package/dist/boot/stores.d.ts +8 -0
  39. package/dist/boot/stores.js +8 -12
  40. package/dist/boot/web-admin.d.ts +6 -0
  41. package/dist/boot/web-admin.js +11 -2
  42. package/dist/budget.js +5 -1
  43. package/dist/caller-text-echo.d.ts +2 -0
  44. package/dist/caller-text-echo.js +2 -1
  45. package/dist/canonical-json.d.ts +2 -41
  46. package/dist/canonical-json.js +2 -13
  47. package/dist/capabilities/composite-members.d.ts +1 -1
  48. package/dist/capabilities/composite-members.js +1 -1
  49. package/dist/capabilities/mcp-probe.js +2 -2
  50. package/dist/capability-facts.d.ts +2 -0
  51. package/dist/capability-facts.js +4 -4
  52. package/dist/checkpoint-ctx-seal-m28.d.ts +42 -0
  53. package/dist/checkpoint-ctx-seal-m28.js +19 -0
  54. package/dist/field-conflict.d.ts +2 -1
  55. package/dist/field-conflict.js +1 -0
  56. package/dist/file-history-disclosure.d.ts +34 -5
  57. package/dist/file-history-disclosure.js +2 -2
  58. package/dist/fleet/fleet-bus.d.ts +4 -0
  59. package/dist/fleet/fleet-bus.js +1 -0
  60. package/dist/hooks/hook-runner.js +2 -2
  61. package/dist/http/admission.js +8 -0
  62. package/dist/http/approval-sweeps.js +3 -1
  63. package/dist/http/checkpoint-ctx.d.ts +157 -2
  64. package/dist/http/checkpoint-ctx.js +158 -2
  65. package/dist/http/dispatch.js +3 -3
  66. package/dist/http/durable-run-leg.js +3 -2
  67. package/dist/http/idempotency.d.ts +2 -2
  68. package/dist/http/idempotency.js +2 -2
  69. package/dist/http/principal-gate.d.ts +32 -6
  70. package/dist/http/principal-gate.js +27 -4
  71. package/dist/http/refusal-body.d.ts +12 -0
  72. package/dist/http/refusal-body.js +11 -0
  73. package/dist/http/request-guards.d.ts +0 -9
  74. package/dist/http/request-guards.js +2 -16
  75. package/dist/http/resume-ctx-read.d.ts +48 -1
  76. package/dist/http/resume-ctx-read.js +26 -3
  77. package/dist/http/resume-legs.d.ts +2 -1
  78. package/dist/http/resume-legs.js +37 -22
  79. package/dist/http/route-ctx.d.ts +0 -9
  80. package/dist/http/routes/agents-roster.js +2 -3
  81. package/dist/http/routes/approvals-assistant.js +43 -9
  82. package/dist/http/routes/capabilities.js +1 -1
  83. package/dist/http/routes/diagnostics.d.ts +3 -3
  84. package/dist/http/routes/diagnostics.js +1 -1
  85. package/dist/http/routes/fleet.js +4 -4
  86. package/dist/http/routes/memory-origin.d.ts +1 -1
  87. package/dist/http/routes/rules.js +4 -4
  88. package/dist/http/routes/runs.js +30 -9
  89. package/dist/http/routes/session-sync.d.ts +3 -2
  90. package/dist/http/routes/session-sync.js +38 -20
  91. package/dist/http/routes/sessions-list.d.ts +1 -1
  92. package/dist/http/routes/sessions-list.js +3 -4
  93. package/dist/http/routes/sessions.js +4 -4
  94. package/dist/http/routes/tasks.js +21 -16
  95. package/dist/http/routes/trace-usage.js +7 -9
  96. package/dist/http/run-declared-families.d.ts +16 -0
  97. package/dist/http/run-declared-families.js +12 -0
  98. package/dist/http/server.d.ts +6 -0
  99. package/dist/http/server.js +5 -2
  100. package/dist/http/submit-ctx.d.ts +4 -3
  101. package/dist/http/submit-ctx.js +19 -3
  102. package/dist/http/wire-types.d.ts +7 -3
  103. package/dist/index.d.ts +2 -0
  104. package/dist/index.js +1 -0
  105. package/dist/leader/wire.d.ts +4 -0
  106. package/dist/leader/wire.js +8 -7
  107. package/dist/main.js +1 -1
  108. package/dist/memory-origin-fence.js +2 -2
  109. package/dist/model-compat.d.ts +1 -1
  110. package/dist/observability/prompt-manifest.d.ts +19 -8
  111. package/dist/observability/prompt-manifest.js +9 -8
  112. package/dist/observability/tool-trace.js +8 -9
  113. package/dist/orchestration/workflow-park-decision.d.ts +1 -1
  114. package/dist/orchestration/workflow-park-decision.js +1 -0
  115. package/dist/parked-decide.d.ts +4 -5
  116. package/dist/parked-decide.js +3 -1
  117. package/dist/permission-rule-vocab.d.ts +1 -23
  118. package/dist/permission-rule-vocab.js +0 -13
  119. package/dist/plugins/approval-payload-fingerprint.d.ts +14 -6
  120. package/dist/plugins/approval-payload-fingerprint.js +1 -1
  121. package/dist/plugins/caching-session-store.d.ts +1 -1
  122. package/dist/plugins/caching-session-store.js +4 -3
  123. package/dist/plugins/checkpoint-store-sql.d.ts +22 -1
  124. package/dist/plugins/checkpoint-store-sql.js +45 -1
  125. package/dist/plugins/local-checkpoint-store.d.ts +13 -2
  126. package/dist/plugins/local-checkpoint-store.js +26 -0
  127. package/dist/plugins/local-session-store.d.ts +12 -9
  128. package/dist/plugins/local-session-store.js +23 -14
  129. package/dist/plugins/local-session-window.d.ts +12 -6
  130. package/dist/plugins/local-session-window.js +1 -17
  131. package/dist/plugins/one-time-migrations.d.ts +11 -0
  132. package/dist/plugins/one-time-migrations.js +12 -0
  133. package/dist/plugins/permission-rule-store-file.js +10 -6
  134. package/dist/plugins/permission-rule-store-sql.js +8 -8
  135. package/dist/plugins/pg-session-storage.d.ts +1 -1
  136. package/dist/plugins/pg-session-storage.js +39 -17
  137. package/dist/plugins/remote-env-device.d.ts +14 -7
  138. package/dist/plugins/remote-env-device.js +23 -15
  139. package/dist/plugins/session-ownership.d.ts +11 -0
  140. package/dist/plugins/session-ownership.js +11 -0
  141. package/dist/plugins/session-placement.d.ts +12 -14
  142. package/dist/plugins/session-placement.js +2 -15
  143. package/dist/plugins/tidb-session-store.d.ts +1 -1
  144. package/dist/plugins/tidb-session-store.js +39 -17
  145. package/dist/request-tool-axes.d.ts +12 -0
  146. package/dist/request-tool-axes.js +20 -0
  147. package/dist/run-local.js +1 -0
  148. package/dist/runs.d.ts +2 -1
  149. package/dist/runs.js +4 -3
  150. package/dist/sealed-key.d.ts +32 -0
  151. package/dist/sealed-key.js +55 -1
  152. package/dist/security.d.ts +9 -7
  153. package/dist/session-error-wire.d.ts +12 -5
  154. package/dist/session-error-wire.js +4 -1
  155. package/dist/session-sync-kernel.d.ts +37 -10
  156. package/dist/session-sync-kernel.js +10 -0
  157. package/dist/task-a2a.js +4 -3
  158. package/dist/task-mcp.d.ts +2 -2
  159. package/dist/task-mcp.js +20 -8
  160. package/dist/task-settings.d.ts +5 -1
  161. package/dist/task-settings.js +1 -1
  162. package/dist/tool-approval.d.ts +151 -52
  163. package/dist/tool-approval.js +113 -20
  164. package/dist/tool-rule-names.d.ts +6 -0
  165. package/dist/trace/core-keyset-guard.d.ts +4 -3
  166. package/dist/trace/exit-seat.d.ts +13 -1
  167. package/dist/trace/exit-seat.js +55 -1
  168. package/dist/trace/ledger-events.d.ts +3 -1
  169. package/dist/trace/project.d.ts +18 -1
  170. package/dist/trace/project.js +16 -4
  171. package/dist/trace/redact.d.ts +6 -3
  172. package/dist/trace/redact.js +191 -23
  173. package/dist/trace/self-minted-wire.d.ts +4 -0
  174. package/dist/trace/self-minted-wire.js +1 -0
  175. package/dist/trace/wire-projection-faces.d.ts +6 -0
  176. package/dist/trace/wire-projection-faces.js +6 -0
  177. package/dist/web-admin-client/skills-mcp.d.ts +1 -1
  178. package/dist/web-admin-client/skills-mcp.js +2 -3
  179. package/dist/web-admin-client/types.d.ts +1 -1
  180. package/package.json +2 -2
  181. package/scripts/drain-check-local.mjs +34 -2
  182. package/scripts/drain-check.mysql.sql +20 -2
  183. package/scripts/drain-check.pg.sql +20 -2
  184. package/dist/mcp-tool-faces.d.ts +0 -69
  185. package/dist/mcp-tool-faces.js +0 -104
@@ -4,6 +4,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
4
4
  import { uuidv7, classifyAsk, foldArbitrationRow, policyCannotAnswer, screenAskClassMembers, MAX_RULE_TEXT_CHARS, READ_ROOT_CANDIDATE_DIR_MAX, parseRuleText, pathConfinableWriteToolNames } from "@sema-agent/core";
5
5
  import { precheckCardRuleText } from "./rules-consent.js";
6
6
  import { redactDeep, redactSecrets, redactSecretsWithArms } from "./trace/redact.js";
7
+ import { seatText } from "./trace/exit-seat.js";
7
8
  import { hasBidiControls } from "./text-bidi.js";
8
9
  import { approvalPayloadFingerprint } from "./plugins/approval-payload-fingerprint.js";
9
10
  import { createLogger } from "./observability/logger.js";
@@ -327,9 +328,14 @@ function parseToolApprovalFields(body, retired) {
327
328
  export function buildUnattendedPolicyRefusal(reason) {
328
329
  return { allow: false, settledBy: POLICY_REFUSAL_DECISION.settledBy, reason };
329
330
  }
330
- function unattendedRefusalReason(arm) {
331
+ function unattendedRefusalReason(site) {
332
+ if (site.arm === "frame_family_undeclared") {
333
+ return (`UNATTENDED_APPROVAL_POLICY=deny: this run has no live approval channel — the client that started it did not declare the "${site.family}" ` +
334
+ "approval frame family (settings.approvalFrameFamilies), so no approval card is shown to it, and an approval request no one can answer " +
335
+ `is refused here instead of being parked for a person. Upgrade the client, or declare the family it renders (arm: ${site.arm}).`);
336
+ }
331
337
  return ("UNATTENDED_APPROVAL_POLICY=deny: this deployment declares that no one attends its approvals, so an approval " +
332
- `request no one can answer is refused here instead of being parked for a person (arm: ${arm}).`);
338
+ `request no one can answer is refused here instead of being parked for a person (arm: ${site.arm}).`);
333
339
  }
334
340
  function isChildAsk(req) {
335
341
  return req.fromSubagent === true;
@@ -361,11 +367,53 @@ export function resolveStreamApprovalGate(input) {
361
367
  return { active: false, reason: "protocol_disabled" };
362
368
  if (!input.backend)
363
369
  return { active: false, reason: "no_backend" };
364
- if (!input.parkFacility)
370
+ if (input.checkpointStore === undefined)
365
371
  return { active: false, reason: "no_park_facility" };
366
- return { active: true, askStore: input.backend.approvalAsk() };
372
+ return { active: true, askStore: input.backend.approvalAsk(), checkpointStore: input.checkpointStore };
367
373
  }
368
374
  export const APPROVAL_FRAME_FAMILIES = ["tool_approval", "approval_request"];
375
+ export function presentedApprovalFrameFamily(streamApprovalOn) {
376
+ return streamApprovalOn ? "approval_request" : "tool_approval";
377
+ }
378
+ const APPROVAL_FRAME_FAMILIES_FIELD = "settings.approvalFrameFamilies";
379
+ const APPROVAL_FRAME_FAMILY_WORDS = APPROVAL_FRAME_FAMILIES.map((w) => `"${w}"`).join(" | ");
380
+ export function approvalFrameFamiliesIssue(settings) {
381
+ if (settings === null || typeof settings !== "object" || Array.isArray(settings))
382
+ return null;
383
+ const raw = Reflect.get(settings, "approvalFrameFamilies");
384
+ if (raw === undefined)
385
+ return null;
386
+ if (!Array.isArray(raw)) {
387
+ return {
388
+ message: `${APPROVAL_FRAME_FAMILIES_FIELD} must be an array of approval frame family words (${APPROVAL_FRAME_FAMILY_WORDS}) — the families of approval cards this client renders. ` +
389
+ 'Refused rather than ignored, because an ignored declaration reads as "in effect" to the caller',
390
+ fields: { field: APPROVAL_FRAME_FAMILIES_FIELD, reason: "bad_entry" },
391
+ };
392
+ }
393
+ const seen = new Set();
394
+ for (const [index, entry] of raw.entries()) {
395
+ if (!APPROVAL_FRAME_FAMILIES.includes(entry)) {
396
+ return {
397
+ message: `${APPROVAL_FRAME_FAMILIES_FIELD}[${index}] is not one of ${APPROVAL_FRAME_FAMILY_WORDS} — every entry must be an approval frame family word. Refused rather than dropped, because a dropped entry reads as "in effect" to the caller`,
398
+ fields: { field: APPROVAL_FRAME_FAMILIES_FIELD, reason: "bad_entry", index },
399
+ };
400
+ }
401
+ if (seen.has(entry)) {
402
+ return {
403
+ message: `${APPROVAL_FRAME_FAMILIES_FIELD}[${index}] repeats a family declared earlier in the list — declare each family once`,
404
+ fields: { field: APPROVAL_FRAME_FAMILIES_FIELD, reason: "bad_entry", index },
405
+ };
406
+ }
407
+ seen.add(entry);
408
+ }
409
+ return null;
410
+ }
411
+ export function declaredApprovalFrameFamilies(settings) {
412
+ if (approvalFrameFamiliesIssue(settings) !== null || settings === null || typeof settings !== "object")
413
+ return new Set();
414
+ const raw = Reflect.get(settings, "approvalFrameFamilies");
415
+ return new Set(Array.isArray(raw) ? raw.filter((w) => APPROVAL_FRAME_FAMILIES.includes(w)) : []);
416
+ }
369
417
  export function createApprovalCardEmitter(sinks) {
370
418
  const present = [sinks.appendDurable, sinks.writeLive].filter((w) => w !== undefined);
371
419
  const attempt = (write, frame) => (async () => {
@@ -410,17 +458,22 @@ export function createApprovalCardEmitter(sinks) {
410
458
  };
411
459
  }
412
460
  export function resolveApprovalLeg(input) {
413
- if (!input.streamApprovalOn)
414
- return { active: false, windowZero: input.windowMs === 0 };
415
- const tightDeadline = input.legWalltimeMs !== undefined && input.legWalltimeMs - input.windowMarginMs <= 0;
461
+ const active = input.streamApprovalOn;
462
+ const tightDeadline = active && input.legWalltimeMs !== undefined && input.legWalltimeMs - input.windowMarginMs <= 0;
416
463
  if (input.windowMs === 0 || tightDeadline)
417
- return { active: true, windowZero: true };
464
+ return { active, unattended: { arm: "window_zero" } };
465
+ const family = presentedApprovalFrameFamily(active);
466
+ if (!input.declaredFamilies.has(family))
467
+ return { active, unattended: { arm: "frame_family_undeclared", family } };
418
468
  return {
419
- active: true,
420
- windowZero: false,
421
- ...(input.legWalltimeMs !== undefined ? { legDeadlineMonotonic: input.nowMonotonicMs + input.legWalltimeMs } : {}),
469
+ active,
470
+ ...(active && input.legWalltimeMs !== undefined ? { legDeadlineMonotonic: input.nowMonotonicMs + input.legWalltimeMs } : {}),
422
471
  };
423
472
  }
473
+ export function approvalPreambleReplays(input) {
474
+ const leg = resolveApprovalLeg({ ...input, nowMonotonicMs: performance.now() });
475
+ return leg.active && leg.unattended === undefined;
476
+ }
424
477
  function effectiveAskWindowMs(ttlMs, windowMarginMs, legDeadlineMonotonic, nowMonotonicMs) {
425
478
  if (legDeadlineMonotonic === undefined)
426
479
  return ttlMs;
@@ -513,6 +566,7 @@ export class ToolApprovalCoordinator {
513
566
  parkRedemptionTtlMs;
514
567
  parkRedemptionPerPrincipalMax;
515
568
  parkedRedeem;
569
+ parkedGate;
516
570
  askAudit;
517
571
  sessionGrants;
518
572
  deploymentListAsks;
@@ -532,6 +586,7 @@ export class ToolApprovalCoordinator {
532
586
  this.admitMaxPerOwner = opts?.admitMaxPerOwner ?? DEFAULT_ADMIT_MAX_PER_OWNER;
533
587
  this.unattendedPolicy = opts?.unattendedPolicy ?? "park";
534
588
  this.parkedRedeem = opts?.parkedRedeem;
589
+ this.parkedGate = opts?.parkedGate;
535
590
  this.parkRedemptionMax = opts?.parkRedemptionMax ?? DEFAULT_PARK_REDEMPTION_MAX;
536
591
  this.parkRedemptionTtlMs = opts?.parkRedemptionTtlMs ?? DEFAULT_PARK_REDEMPTION_TTL_MS;
537
592
  this.parkRedemptionPerPrincipalMax = opts?.parkRedemptionPerPrincipalMax ?? DEFAULT_PARK_REDEMPTION_PER_PRINCIPAL_MAX;
@@ -553,7 +608,7 @@ export class ToolApprovalCoordinator {
553
608
  outcomeWord = "unavailable";
554
609
  break;
555
610
  case "deny":
556
- outcome = buildUnattendedPolicyRefusal(unattendedRefusalReason(site.arm));
611
+ outcome = buildUnattendedPolicyRefusal(unattendedRefusalReason(site));
557
612
  outcomeWord = "deny";
558
613
  break;
559
614
  default: {
@@ -579,12 +634,12 @@ export class ToolApprovalCoordinator {
579
634
  return this.unattendedRoutesToPark ? deriveBatchId(sourceTaskId, runId, legKey) : deriveAskScopedBatchId(askId);
580
635
  }
581
636
  unattendedAskOutcome(site) {
582
- return this.unattendedOutcome({
583
- arm: "window_zero",
637
+ const facts = {
584
638
  ...(site.taskId !== undefined ? { taskId: site.taskId } : {}),
585
639
  ...(site.owner !== undefined && site.owner !== null ? { owner: site.owner } : {}),
586
640
  ...(site.toolName !== undefined ? { toolName: site.toolName } : {}),
587
- });
641
+ };
642
+ return this.unattendedOutcome(site.arm === "frame_family_undeclared" ? { arm: site.arm, family: site.family, ...facts } : { arm: site.arm, ...facts });
588
643
  }
589
644
  async landUnattendedOutcome(outcome, row) {
590
645
  const store = this.askStore;
@@ -933,12 +988,12 @@ export class ToolApprovalCoordinator {
933
988
  return false;
934
989
  }
935
990
  }
936
- boundUnattendedAsk = (identity) => {
991
+ boundUnattendedAsk = (identity, why) => {
937
992
  return async (req) => {
938
993
  const origin = { owner: identity.owner, taskId: identity.taskId, ...(identity.sessionId !== undefined ? { sessionId: identity.sessionId } : {}), legKey: "" };
939
994
  if (await this.standingGrantSettles(origin, req, this.governanceForcedOf(origin, req)))
940
995
  return true;
941
- return this.unattendedAskOutcome({ taskId: identity.taskId, owner: identity.owner, toolName: req.toolName });
996
+ return this.unattendedAskOutcome({ ...why, taskId: identity.taskId, owner: identity.owner, toolName: req.toolName });
942
997
  };
943
998
  };
944
999
  async sessionGrantUncontestedByRow(origin, req) {
@@ -2142,7 +2197,19 @@ export class ToolApprovalCoordinator {
2142
2197
  async readAskRowState(store, id, principal) {
2143
2198
  return this.readStateOfRow(await this.askRowFor(store, id, "getAsk(read)"), principal);
2144
2199
  }
2145
- readStateOfRow(row, principal) {
2200
+ async readParkedGate(row) {
2201
+ const gate = this.parkedGate;
2202
+ if (gate === undefined || row.gateToken === null)
2203
+ return { kind: "open" };
2204
+ try {
2205
+ return await this.withStoreDeadline(gate({ gateToken: row.gateToken, gateBoundCallId: row.gateBoundCallId }), "readParkedGate", DURABLE_CONVERGE_READ_TIMEOUT_MS);
2206
+ }
2207
+ catch (err) {
2208
+ this.noteStoreError(err, "readParkedGate");
2209
+ return { kind: "open" };
2210
+ }
2211
+ }
2212
+ async readStateOfRow(row, principal) {
2146
2213
  if (!askRowVisibleTo(row, principal))
2147
2214
  return this.notPending(NOT_PENDING_UNKNOWN_CAUSE);
2148
2215
  const projection = buildAskRowReadProjection(row, Date.now());
@@ -2157,8 +2224,34 @@ export class ToolApprovalCoordinator {
2157
2224
  }
2158
2225
  case "PARKING":
2159
2226
  case "PARKED": {
2160
- const parkSession = parkSessionOf(row);
2161
- body = { state: "parked", ...projection, ...(parkSession !== undefined ? { decidePath: approvalDecidePath(parkSession) } : {}) };
2227
+ const gate = row.state === "PARKED" ? await this.readParkedGate(row) : { kind: "open" };
2228
+ switch (gate.kind) {
2229
+ case "open": {
2230
+ const parkSession = parkSessionOf(row);
2231
+ body = { state: "parked", ...projection, ...(parkSession !== undefined ? { decidePath: approvalDecidePath(parkSession) } : {}) };
2232
+ break;
2233
+ }
2234
+ case "decided":
2235
+ body = { state: "decided", ...projection, decision: gate.decision, decidedAtMs: gate.decidedAtMs, ...(gate.decisionNote !== undefined ? { decisionNote: gate.decisionNote } : {}) };
2236
+ break;
2237
+ case "void":
2238
+ body = { state: "void", ...projection };
2239
+ break;
2240
+ case "unknown":
2241
+ if (gate.why === "decision_unreadable") {
2242
+ defaultLogger.error("tool approval read saw a PARKED row whose gate is settled but carries no usable decision", {
2243
+ askId: row.askId,
2244
+ defect: seatText("operator-log-text", gate.defects[0]),
2245
+ defectCount: gate.defects.length,
2246
+ });
2247
+ }
2248
+ return this.notPending();
2249
+ default: {
2250
+ const unhandled = gate;
2251
+ defaultLogger.error("tool approval read hit an unknown gate reading", { askId: row.askId, gate: unhandled });
2252
+ return this.notPending();
2253
+ }
2254
+ }
2162
2255
  break;
2163
2256
  }
2164
2257
  case "DENIED":
@@ -17,6 +17,12 @@
17
17
  * 修前(≤7.107.0)本仓自写的这些臂只比 `req.toolName`:模型按别名调嵌入方工具时,运维名单一个都不拦(core 同族策略判 deny、
18
18
  * 本仓判 allow)。本仓自身不声明带别名的工具,所以只有经 `resolveSpec` 席挂别名工具的嵌入方走得到。
19
19
  *
20
+ * ⚠️ **别名只是名字,不是标记的载体**(core 7.54.0 #2105,[ref] 已定案):上面这条读法只回答「这次调用答得上哪些名字」;嵌入方工具自己的
21
+ * 安全标记(`egress`、不可逆层级 `irreversibility`、可逆探针)**不再**因别名恰是引擎工具名(`MyShell{aliases:["Bash"]}`)而镜像到引擎 shell 上 ——
22
+ * 7.53.x 及以前,`MyShell` 的 `egress: true` / `irreversibility: "always"` 会让引擎 `Bash` 的调用多问一次(收紧),它的恒可逆探针会让引擎 `Bash` 的
23
+ * `touch` 不问就跑(放宽);7.54.0 起两者都不再作用于引擎 shell。要收紧引擎 shell:给**正名**叫 `Bash` 的工具声明标记(同名标记仍作用),或请求
24
+ * `TaskSpec.shellGate: "always"`(本仓的入口 `MANUAL_MODE_SHELL_GATE` / 请求 `shellGate`)。
25
+ *
20
26
  * 返回**命中的那个名字**(调用答得上的名字里第一个被规则够到的,拼写在先)—— 规则归因(`matchedAskRule`)写运维名单里的那个名,
21
27
  * 不写一个名单里查不到的 wire 名;够不到 ⇒ `undefined`。判定式(精确集 / covering 规则)由调用方给,本函数只管「读哪些名字」。
22
28
  */
@@ -145,8 +145,9 @@ type _GuardWiringGovernanceReverse = AssertAllKeysHandled<Exclude<WiringGovernan
145
145
  type PermissionTraceKind = Extract<TraceEvent["kind"], `permission.${string}`>;
146
146
  export type PermissionTraceProjected = "permission.persisted_rule_allowed" | "permission.rule_store_unreadable" | "permission.read_only_allowed" | "permission.skill_allowed" | "permission.allowed_tools_allowed";
147
147
  type PermissionTraceDropped = "permission.sandbox_admitted" | "permission.rule_sync_resurrected" | "permission.rule_sync_dropped" | "permission.org_snapshot_unavailable";
148
- type _GuardPermissionTrace = AssertAllKeysHandled<Exclude<PermissionTraceKind, PermissionTraceProjected | PermissionTraceDropped>>;
149
- type _GuardPermissionTraceReverse = AssertAllKeysHandled<Exclude<PermissionTraceProjected | PermissionTraceDropped, PermissionTraceKind>>;
148
+ type PermissionTraceRunEvent = "permission.cross_boundary_taint";
149
+ type _GuardPermissionTrace = AssertAllKeysHandled<Exclude<PermissionTraceKind, PermissionTraceProjected | PermissionTraceDropped | PermissionTraceRunEvent>>;
150
+ type _GuardPermissionTraceReverse = AssertAllKeysHandled<Exclude<PermissionTraceProjected | PermissionTraceDropped | PermissionTraceRunEvent, PermissionTraceKind>>;
150
151
  /** `T` 非 `never` 时收敛到 `never`(空集 ⇒ 残留 `"EMPTY"` ⇒ tsc 红)。 */
151
152
  type AssertNonEmpty<T> = [T] extends [never] ? "EMPTY" : never;
152
153
  type _GuardPermissionTraceNonEmpty = AssertAllKeysHandled<AssertNonEmpty<PermissionTraceKind>>;
@@ -215,7 +216,7 @@ type ParkArmKeys = keyof Extract<PendingAction, {
215
216
  kind: "tool_approval";
216
217
  }>;
217
218
  type ParkArmQueueProjected = "toolName" | "toolCallId" | "boundInputHash" | "ruleOffers" | "hasBidiControls" | keyof import("../approval-card.js").ParkRowFacts;
218
- type ParkArmQueueExcluded = "kind" | "args" | "batchToolCallIds" | "completedCallIds" | "capabilityId" | "contractId" | "shapeDigest" | "implementationRevision" | "rosterDigest" | "family" | "pathTarget" | "names" | "preview" | "previewWithheld" | "execCwd" | "denialLimitFallback" | "authoredBy" | "boundGrantDigest" | "boundGrants" | "classifierNoRuling";
219
+ type ParkArmQueueExcluded = "kind" | "args" | "batchToolCallIds" | "completedCallIds" | "capabilityId" | "contractId" | "shapeDigest" | "implementationRevision" | "family" | "pathTarget" | "targets" | "names" | "preview" | "previewWithheld" | "execCwd" | "denialLimitFallback" | "authoredBy" | "boundGrantDigest" | "boundGrants" | "classifierNoRuling";
219
220
  type _GuardParkArmQueue = AssertAllKeysHandled<Exclude<ParkArmKeys, ParkArmQueueProjected | ParkArmQueueExcluded>>;
220
221
  type _GuardParkArmQueueReverse = AssertAllKeysHandled<Exclude<ParkArmQueueProjected | ParkArmQueueExcluded, ParkArmKeys>>;
221
222
  type _NoDoubleBookParkArmQueue = AssertAllKeysHandled<Extract<ParkArmQueueProjected, ParkArmQueueExcluded>>;
@@ -7,7 +7,9 @@ export type SafeText = string & {
7
7
  /**
8
8
  * **座表(闭集;一行 = 一类出口)**。加一类出口 = 加一行;表外的座名是编译错误。
9
9
  * · `trace-frame-text`:trace 帧的自由文本位(子代名 / 动作行 / 通告正文与结果 / 诊断 / 注入摘录 / 压缩说明 / 人类输入的署名与附件标签 …),不设界;
10
- * · `trace-frame-line`:trace 帧里一行宽的展示位(`workspace_changed.cwd` / `brain_status.detail`),界 300 码元。
10
+ * · `trace-frame-line`:trace 帧里一行宽的展示位(`workspace_changed.cwd` / `brain_status.detail`),界 300 码元;
11
+ * · `operator-log-text`:运维日志字段里的一句话 —— 读口坏行的缺陷句(core 的筛按位置点名并回显落盘标量)、`tool_trace` 行里工具的输出 / 失败句 / 拒因;日志字段行,界 200 码元(含截断记号);
12
+ * · `operator-log-arg`:运维日志字段里模型写的工具入参(`tool_trace` 的 `arg.*`:路径 / 命令 / 模式 / 非对象入参的串形);日志字段行,界 120 码元(含截断记号)。
11
13
  */
12
14
  declare const EXIT_SEATS: {
13
15
  readonly "trace-frame-text": {
@@ -17,6 +19,16 @@ declare const EXIT_SEATS: {
17
19
  readonly scope: "free-text";
18
20
  readonly maxChars: 300;
19
21
  };
22
+ readonly "operator-log-text": {
23
+ readonly scope: "free-text";
24
+ readonly maxChars: 200;
25
+ readonly logField: true;
26
+ };
27
+ readonly "operator-log-arg": {
28
+ readonly scope: "free-text";
29
+ readonly maxChars: 120;
30
+ readonly logField: true;
31
+ };
20
32
  };
21
33
  export type ExitSeat = keyof typeof EXIT_SEATS;
22
34
  /**
@@ -1,3 +1,5 @@
1
+ import { CONTROL_AND_BIDI_CHARS, stripControlAndBidi } from "@sema-agent/settings-schema";
2
+ import { CALLER_TEXT_CUT_MARK } from "../caller-text-echo.js";
1
3
  import { errorKindOf } from "../observability/err-text.js";
2
4
  import { recordFailOpen } from "../observability/fail-open.js";
3
5
  import { OVERSIZED_REDACTION_PLACEHOLDER } from "../observability/run-terminal.js";
@@ -5,12 +7,17 @@ import { proposeRedactionSpans, REDACTION_TOKEN_RE_SRC } from "./redact.js";
5
7
  const EXIT_SEATS = {
6
8
  "trace-frame-text": { scope: "free-text" },
7
9
  "trace-frame-line": { scope: "free-text", maxChars: 300 },
10
+ "operator-log-text": { scope: "free-text", maxChars: 200, logField: true },
11
+ "operator-log-arg": { scope: "free-text", maxChars: 120, logField: true },
8
12
  };
9
13
  const SEAT_ROWS = new Map(Object.entries(EXIT_SEATS));
10
14
  for (const [name, row] of SEAT_ROWS) {
11
15
  if (row.maxChars !== undefined && row.maxChars < OVERSIZED_REDACTION_PLACEHOLDER.length) {
12
16
  throw new Error(`exit seat "${name}": maxChars ${row.maxChars} cannot hold the failure placeholder (${OVERSIZED_REDACTION_PLACEHOLDER.length} code units)`);
13
17
  }
18
+ if (row.logField === true && row.maxChars === undefined) {
19
+ throw new Error(`exit seat "${name}": a log-field row must declare maxChars (the cut mark counts inside the bound)`);
20
+ }
14
21
  }
15
22
  function mergeSpans(spans) {
16
23
  const order = spans.map((s, i) => ({ s, i })).sort((a, b) => a.s.start - b.s.start || b.s.end - a.s.end || a.i - b.i);
@@ -59,13 +66,60 @@ function renderOnce(text, groups, maxChars) {
59
66
  parts.push(maxChars === undefined || len + tail.length <= maxChars ? tail : cutPlain(tail, maxChars - len));
60
67
  return parts.join("");
61
68
  }
69
+ const STRIPPED_UNIT = (() => {
70
+ const inSet = new RegExp(`[${CONTROL_AND_BIDI_CHARS}]`);
71
+ const table = new Uint8Array(0x10000);
72
+ for (let unit = 0; unit < table.length; unit++)
73
+ if (inSet.test(String.fromCharCode(unit)))
74
+ table[unit] = 1;
75
+ return table;
76
+ })();
77
+ function mapSpansIntoStripped(text, spans) {
78
+ const marks = [...new Set(spans.flatMap((sp) => [sp.start, sp.end]))].sort((a, b) => a - b);
79
+ const strippedBefore = new Map();
80
+ let removed = 0;
81
+ let at = 0;
82
+ for (const mark of marks) {
83
+ for (; at < mark; at++)
84
+ if (STRIPPED_UNIT[text.charCodeAt(at)] === 1)
85
+ removed += 1;
86
+ strippedBefore.set(mark, mark - removed);
87
+ }
88
+ const mapped = [];
89
+ for (const sp of spans) {
90
+ const start = strippedBefore.get(sp.start);
91
+ const end = strippedBefore.get(sp.end);
92
+ if (end > start)
93
+ mapped.push({ start, end, repl: sp.repl });
94
+ }
95
+ return mapped;
96
+ }
97
+ function proposeLogFieldSpans(text, scope) {
98
+ const read = stripControlAndBidi(text);
99
+ if (read === text)
100
+ return { read, spans: proposeRedactionSpans(text, scope) };
101
+ if (read.length === 0)
102
+ return { read, spans: [] };
103
+ const original = proposeRedactionSpans(text, scope);
104
+ const stripped = proposeRedactionSpans(read, scope, { countHits: original.length === 0 });
105
+ return { read, spans: [...stripped, ...mapSpansIntoStripped(text, original)] };
106
+ }
107
+ function renderRow(text, groups, row) {
108
+ if (row.logField !== true || row.maxChars === undefined)
109
+ return renderOnce(text, groups, row.maxChars);
110
+ const whole = renderOnce(text, groups, undefined);
111
+ if (whole.length <= row.maxChars)
112
+ return whole;
113
+ return renderOnce(text, groups, row.maxChars - CALLER_TEXT_CUT_MARK.length) + CALLER_TEXT_CUT_MARK;
114
+ }
62
115
  export function seatText(seat, text) {
63
116
  const row = SEAT_ROWS.get(seat);
64
117
  if (row === undefined)
65
118
  throw new Error(`exit seat: ${JSON.stringify(seat)} is not a row of the seat table`);
66
119
  let out;
67
120
  try {
68
- out = renderOnce(text, mergeSpans(proposeRedactionSpans(text, row.scope)), row.maxChars);
121
+ const { read, spans } = row.logField === true ? proposeLogFieldSpans(text, row.scope) : { read: text, spans: proposeRedactionSpans(text, row.scope) };
122
+ out = renderRow(read, mergeSpans(spans), row);
69
123
  }
70
124
  catch (err) {
71
125
  recordFailOpen("server.run-terminal.redact-threw", `seat=${seat} len=${typeof text === "string" ? text.length : -1} err=${errorKindOf(err)}`);
@@ -52,7 +52,9 @@ export type LedgerEventType = "reasoning" | "text" | "tool_start" | "tool_end" |
52
52
  | "task_started" | "task_progress" | "task_notification" | "turn_end"
53
53
  /** park 行名**两个**(`appendParked(kind)` 逐字取自 `result.status`)—— 运行期闭集见
54
54
  * {@link PARK_LEDGER_EVENT_TYPES}(本联合是它的类型面,两者由下方的双向断言锁步)。 */
55
- | "suspended" | "needs_review" | "done" | "failed" | "suggestions" | "model_usage" | "prompt_assembled" | "config_assembled" | "file_link"
55
+ | "suspended" | "needs_review" | "done" | "failed" | "suggestions" | "model_usage" | "prompt_assembled" | "config_assembled"
56
+ /** core 7.54.0 [ref] O9b S1:`permission.cross_boundary_taint` 帧的耐久行(伴随记录,与 `config_assembled` 同一只队列、同一批 drain 点;只在执行于目标设备的树上有)。 */
57
+ | "cross_boundary_taint" | "file_link"
56
58
  /** 人在环三族的 open/close 帧对(投递面 = 本腿的 durable events tail)。 */
57
59
  | "elicitation" | "elicitation_complete" | "question" | "question_complete" | "tool_approval" | "tool_approval_complete"
58
60
  /** [ref] §4.3 呈卡族的开卡帧与单卡消卡帧(与上面两只 tool_approval 帧同一条投递面;一条腿只发其中一族,
@@ -26,6 +26,7 @@
26
26
  import type { GateOutcome, McpDelivered, TaskResult, SystemInjectionPriority } from "@sema-agent/core";
27
27
  import type { RunEvent, RunRecord } from "../plugins/store-contracts.js";
28
28
  import type { ModelUsageDelta, ModelUsageTracker, PromptManifestRecord } from "../budget.js";
29
+ import type { RunCompanionRecord } from "../observability/prompt-manifest.js";
29
30
  import { type SafeText } from "./exit-seat.js";
30
31
  import { type SemaProvenanceInput } from "./sema-provenance.js";
31
32
  import type { LedgerEventType } from "./ledger-events.js";
@@ -51,7 +52,7 @@ export declare function appendModelUsageDelta(append: AppendFn, modelUsage: Mode
51
52
  * (one Map.get), so callers fire it liberally. */
52
53
  export declare function appendPromptManifest(append: AppendFn, tracker: {
53
54
  drain(taskId: string): PromptManifestRecord[] | undefined;
54
- drainConfig?(taskId: string): Array<Record<string, unknown>> | undefined;
55
+ drainCompanions?(taskId: string): RunCompanionRecord[] | undefined;
55
56
  } | undefined, taskId: string): Promise<void>;
56
57
  /** E8: at a TERMINAL done — flush the final delta, then SUM all `model_usage` events (every process leg) into the
57
58
  * result's `stats.modelUsage` echo. Returns the result unchanged when no usage was tracked (shell soft-degrades). */
@@ -1083,6 +1084,22 @@ export declare function contextUsageEventData(ev: {
1083
1084
  * 与结果面的孪生 `TaskResult.responseModel` 同一只闸 —— 同一个名字在两张脸上不许两种待遇。非串 / 空串同样不铸键(空串在
1084
1085
  * 那条谓词的长度下界之外;core 的读律 `providerReportedModel` 对空串也答「没报」)。
1085
1086
  */
1087
+ /**
1088
+ * 🔴 core 7.54.0 [ref] O9b S1 —— 设备车道发射闸的 `permission.cross_boundary_taint` 帧 → 耐久行 `cross_boundary_taint`(`GET /v1/runs/:id/events` 账本回放同名交出;
1089
+ * trace SSE `GET /v1/tasks/:id/stream` 上是 `cross-boundary-taint` 帧,按存下的行原样交出)。帧只在**执行于目标设备的树**上发(武装腿 prepare / 续跑腿 resume 各一帧,离目标读第一次放进树时 observed 一帧),
1090
+ * 宿主车道零帧 ⇒ 本行在宿主车道上永不出现。纯元数据(core 逐字「Metadata only」):树的污染位(`taint`,core `CROSS_BOUNDARY_TAINT_BITS` 的子集)、
1091
+ * 树是否受治理(`governed`)、阶段(`phase`)、腿的 `runId`,observed 时那一只调用(`toolName` / `toolCallId`)。
1092
+ * 信封(`kind` / `version` / `taskId` / `ts`)不上行:行自带 seq / ts,run 就是 taskId(与清单行同一条律;登记表 `cross_boundary_taint` 行)。
1093
+ * 值:`taint` 交一份数组副本(只收串);`toolName` / `toolCallId` 在场才铸。词按开集串过境(属主是引擎的闭集;本仓不判词表)。
1094
+ */
1095
+ export declare function crossBoundaryTaintEventData(ev: {
1096
+ runId?: unknown;
1097
+ phase?: unknown;
1098
+ taint?: unknown;
1099
+ governed?: unknown;
1100
+ toolName?: unknown;
1101
+ toolCallId?: unknown;
1102
+ }): Record<string, unknown>;
1086
1103
  export declare function turnEndEventData(ev: {
1087
1104
  usage?: unknown;
1088
1105
  usageMissing?: true;
@@ -24,10 +24,10 @@ export async function appendPromptManifest(append, tracker, taskId) {
24
24
  if (records)
25
25
  for (const m of records)
26
26
  await append("prompt_assembled", m);
27
- const cfgs = tracker?.drainConfig?.(taskId);
28
- if (cfgs)
29
- for (const cfg of cfgs)
30
- await append("config_assembled", cfg);
27
+ const companions = tracker?.drainCompanions?.(taskId);
28
+ if (companions)
29
+ for (const c of companions)
30
+ await append(c.type, c.data);
31
31
  }
32
32
  export async function attachModelUsage(result, ctx) {
33
33
  if (!ctx.modelUsage)
@@ -846,6 +846,16 @@ function contextUsageSectionsField(sections) {
846
846
  }
847
847
  return out.length > 0 ? { sections: out } : {};
848
848
  }
849
+ export function crossBoundaryTaintEventData(ev) {
850
+ return {
851
+ ...(typeof ev.runId === "string" ? { runId: ev.runId } : {}),
852
+ ...(typeof ev.phase === "string" ? { phase: ev.phase } : {}),
853
+ ...(Array.isArray(ev.taint) ? { taint: ev.taint.filter((b) => typeof b === "string") } : {}),
854
+ ...(typeof ev.governed === "boolean" ? { governed: ev.governed } : {}),
855
+ ...(typeof ev.toolName === "string" ? { toolName: ev.toolName } : {}),
856
+ ...(typeof ev.toolCallId === "string" ? { toolCallId: ev.toolCallId } : {}),
857
+ };
858
+ }
849
859
  export function turnEndEventData(ev) {
850
860
  return {
851
861
  ...(ev.usage ? { usage: ev.usage } : {}),
@@ -1004,6 +1014,8 @@ function mapTraceEventInner(type, seq, data) {
1004
1014
  }
1005
1015
  case "prompt_assembled":
1006
1016
  return { event: "prompt-assembled", data: { seq, ...data } };
1017
+ case "cross_boundary_taint":
1018
+ return { event: "cross-boundary-taint", data: { seq, ...data } };
1007
1019
  case "done":
1008
1020
  return { event: "done", data: { seq } };
1009
1021
  case "suspended":
@@ -165,8 +165,9 @@ export declare function redactionSpansForTest(text: string, opts?: {
165
165
  /**
166
166
  * 🔴 **出口座的提案口**(S-485 / RH-1 第一期;座 = `src/trace/exit-seat.ts` 的 `seatText`)。
167
167
  *
168
- * 同一只 {@link runRedaction}、同一遍扫描、同一次观察者计数(`observe` 与 {@link redactSecrets} 的缺省同口 —— 座上的一次脱敏就是一次
169
- * 主扫),交出**原文坐标**上的跨度与各自的替换串:臂认什么、档里有哪些臂,仍然只由本模块答。座拿它做**一次区间合并 + 一次渲染**
168
+ * 同一只 {@link runRedaction}、同一遍扫描、同一次观察者计数(座上的一次脱敏就是一次主扫),交出**原文坐标**上的跨度与各自的替换串:臂认什么、档里有哪些臂,仍然只由本模块答。
169
+ * `opts.countHits` 缺省 `true`;`false` = 这一遍照常提案、照常留吃尾一类的 fail-open 痕,只是不把命中表喂观察者(`redactions_applied_total`)——
170
+ * 座对同一段文本要扫两种读法时,让其中一遍计数。与 {@link redactSecrets} 的 `opts.observe:false` 不同:那是影子重扫,计数与留痕一并不记。座拿它做**一次区间合并 + 一次渲染**
170
171
  * (合并律与渲染律住在座里,本模块不拼第二份产物给座)。
171
172
  *
172
173
  * 第一期交的是池解完重叠之后的那一份(升序、两两不交);第二期改交未解重叠的原始候选,池内并集律 {@link resolveSpanOverlaps} 与
@@ -178,7 +179,9 @@ export type RedactionSpan = {
178
179
  readonly repl: string;
179
180
  };
180
181
  export type { RedactionScope };
181
- export declare function proposeRedactionSpans(text: string, scope: RedactionScope): readonly RedactionSpan[];
182
+ export declare function proposeRedactionSpans(text: string, scope: RedactionScope, opts?: {
183
+ readonly countHits?: boolean;
184
+ }): readonly RedactionSpan[];
182
185
  /**
183
186
  * 🔴 环 + 深度预算(2026-07-25 补)。原实现是纯递归、**既不记已访问节点也不限深度**,而它的调用位置是
184
187
  * **最不可信的边界**:`toolStart`/`toolEnd` 把工具的 `args`/`output`/`structured` 直接喂进来。后果两条: