@sema-agent/server 7.118.0 → 7.119.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/MIGRATION.md +85 -0
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/USAGE.md +24 -13
  5. package/deploy/sema-up/README.md +2 -0
  6. package/deploy/sema-up/chart/templates/minio.yaml +2 -1
  7. package/deploy/sema-up/chart/templates/server-v4pro.yaml +1 -0
  8. package/deploy/sema-up/chart/templates/server.yaml +1 -0
  9. package/deploy/sema-up/chart/values.yaml +7 -1
  10. package/deploy/sema-up/cluster-up.sh +20 -8
  11. package/deploy/sema-up/kube-up.sh +29 -16
  12. package/deploy/sema-up/lib/buckets.sh +21 -0
  13. package/deploy/sema-up/lib/health.sh +33 -0
  14. package/deploy/sema-up/sema-up.sh +14 -9
  15. package/deploy/sema-up/smoke.sh +16 -6
  16. package/deploy/sema-up/stack/compose.yaml +1 -1
  17. package/deploy/sema-up/stack/sema-stack-cluster.yaml +4 -1
  18. package/dist/approval-ask-audit-store.js +2 -7
  19. package/dist/approval-card.d.ts +5 -5
  20. package/dist/approval.d.ts +51 -5
  21. package/dist/approval.js +76 -13
  22. package/dist/audit.d.ts +12 -1
  23. package/dist/audit.js +62 -2
  24. package/dist/auto-mode-face.d.ts +2 -3
  25. package/dist/bake-runner/main.js +1 -6
  26. package/dist/bake-runner/protocol.d.ts +18 -0
  27. package/dist/bake-runner/protocol.js +22 -2
  28. package/dist/bake-runner/runner.d.ts +12 -8
  29. package/dist/bake-runner/runner.js +5 -9
  30. package/dist/boot/config-center.js +1 -1
  31. package/dist/boot/coordinators.js +4 -1
  32. package/dist/boot/device-lane.d.ts +3 -3
  33. package/dist/boot/device-lane.js +1 -2
  34. package/dist/boot/leader.d.ts +4 -4
  35. package/dist/boot/leader.js +1 -2
  36. package/dist/boot/parked-revive-gate.d.ts +5 -1
  37. package/dist/boot/parked-revive-gate.js +8 -2
  38. package/dist/boot/resolve-spec.js +7 -4
  39. package/dist/boot/stage-07-capability-layer.js +5 -1
  40. package/dist/boot/stage-10-http-server.js +3 -2
  41. package/dist/boot/stores.js +12 -7
  42. package/dist/capabilities/center-plugins.d.ts +2 -0
  43. package/dist/capabilities/center-plugins.js +24 -6
  44. package/dist/capabilities/code-review-council.js +8 -11
  45. package/dist/capabilities/scenarios.d.ts +34 -7
  46. package/dist/capabilities/scenarios.js +33 -6
  47. package/dist/config-catalog.js +3 -3
  48. package/dist/config.js +2 -2
  49. package/dist/deployment-governance.d.ts +5 -2
  50. package/dist/deployment-governance.js +4 -3
  51. package/dist/host-decision.d.ts +52 -27
  52. package/dist/host-decision.js +14 -7
  53. package/dist/http/admission.js +6 -6
  54. package/dist/http/resume-legs.js +2 -1
  55. package/dist/http/routes/approvals-assistant.d.ts +25 -3
  56. package/dist/http/routes/approvals-assistant.js +9 -5
  57. package/dist/http/routes/capabilities.js +4 -3
  58. package/dist/http/routes/memory-origin.d.ts +1 -1
  59. package/dist/http/routes/runs.js +11 -5
  60. package/dist/http/routes/sessions.js +20 -16
  61. package/dist/http/wire-types.d.ts +12 -13
  62. package/dist/images/manifest.d.ts +7 -1
  63. package/dist/images/manifest.js +18 -16
  64. package/dist/memory-store-refusal.d.ts +7 -12
  65. package/dist/memory-store-refusal.js +4 -5
  66. package/dist/namespaced-rule-name.d.ts +4 -3
  67. package/dist/observability/fail-open.d.ts +5 -1
  68. package/dist/observability/fail-open.js +5 -1
  69. package/dist/operator-ask.d.ts +6 -5
  70. package/dist/plugins/approval-exemption-store.d.ts +4 -0
  71. package/dist/plugins/approval-exemption-store.js +1 -0
  72. package/dist/plugins/blob-backend.d.ts +13 -0
  73. package/dist/plugins/blob-backend.js +26 -0
  74. package/dist/plugins/local-session-store.d.ts +78 -47
  75. package/dist/plugins/local-session-store.js +152 -122
  76. package/dist/plugins/one-time-migrations.d.ts +6 -0
  77. package/dist/plugins/one-time-migrations.js +19 -1
  78. package/dist/plugins/snapshot-bucket-presence.d.ts +36 -0
  79. package/dist/plugins/snapshot-bucket-presence.js +40 -0
  80. package/dist/plugins/store-backend.d.ts +17 -6
  81. package/dist/plugins/store-backend.js +46 -16
  82. package/dist/plugins/store-open-wait.d.ts +33 -0
  83. package/dist/plugins/store-open-wait.js +73 -0
  84. package/dist/rule-entry-list.js +1 -1
  85. package/dist/run-local.js +4 -3
  86. package/dist/security.d.ts +5 -3
  87. package/dist/security.js +4 -0
  88. package/dist/session-policy-wire.js +7 -12
  89. package/dist/spec-fields.d.ts +6 -2
  90. package/dist/store-posture.d.ts +14 -6
  91. package/dist/store-posture.js +7 -4
  92. package/dist/task-settings.d.ts +27 -55
  93. package/dist/task-settings.js +6 -18
  94. package/dist/tool-approval.d.ts +97 -43
  95. package/dist/tool-approval.js +120 -31
  96. package/dist/trace/redact.d.ts +6 -0
  97. package/dist/trace/redact.js +9 -9
  98. package/dist/trace/self-minted-wire.d.ts +1 -1
  99. package/dist/user-input-ledger.d.ts +19 -15
  100. package/dist/user-input-ledger.js +12 -20
  101. package/package.json +2 -2
@@ -1,7 +1,7 @@
1
1
  import { contentKindOf } from "./approval-content-kind.js";
2
- import { liveSettledBy, policyCannotAllow, readDecidedBy, DECIDED_BY_OUTSIDE_CLOSED_SET, POLICY_CANNOT_ALLOW_PERSON_ASK, SETTLED_BY_RETIRED } from "./host-decision.js";
2
+ import { heldAskOf, liveSettledBy, readDecidedBy, retiredDecisionKeyFacts, DECIDED_BY_OUTSIDE_CLOSED_SET, POLICY_CANNOT_ALLOW_PERSON_ASK } from "./host-decision.js";
3
3
  import { AsyncLocalStorage } from "node:async_hooks";
4
- import { uuidv7, classifyAsk, foldArbitrationRow, screenAskClassMembers, MAX_RULE_TEXT_CHARS, READ_ROOT_CANDIDATE_DIR_MAX, parseRuleText, pathConfinableWriteToolNames } from "@sema-agent/core";
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
7
  import { hasBidiControls } from "./text-bidi.js";
@@ -15,6 +15,8 @@ import { settleByPolicy } from "./approval-policy-settlement.js";
15
15
  import { governanceAskMarksFor, runWithGovernanceAskScope } from "./governance-ask-marks.js";
16
16
  import { governanceOriginOf } from "./runtime-governance.js";
17
17
  import { answerGrantsBeyondCall, grantBindingOf } from "./standing-answer-grant.js";
18
+ import { operatorAsk } from "./operator-ask.js";
19
+ import { APPROVAL_EXEMPTED_EVENT } from "./plugins/approval-exemption-store.js";
18
20
  const defaultLogger = createLogger();
19
21
  export const APPROVAL_GATE_KINDS = ["human", "irreversible_ask"];
20
22
  export function isApprovalGateKind(k) {
@@ -54,7 +56,7 @@ function durableRuleMaterial(material) {
54
56
  export const ASK_DECISION_CONSUMER_ABSENT = "absent";
55
57
  const MAX_INFLIGHT_TERMINAL_CLAIMS = 2;
56
58
  const DURABLE_CONVERGE_BACKOFF_MS = [50, 100, 200, 400, 800, 1600];
57
- const DURABLE_CONVERGE_READ_TIMEOUT_MS = 2_000;
59
+ export const DURABLE_CONVERGE_READ_TIMEOUT_MS = 2_000;
58
60
  const DURABLE_CALL_TIMEOUT_MS = 5_000;
59
61
  const TERMINAL_LADDER_WALL_MS = DURABLE_CALL_TIMEOUT_MS + DURABLE_CONVERGE_READ_TIMEOUT_MS + DURABLE_CONVERGE_BACKOFF_MS.reduce((a, b) => a + b, 0);
60
62
  const TERMINAL_CLAIM_CAP_POLL_MS = 50;
@@ -85,43 +87,65 @@ function sessionAllowCategory(toolName) {
85
87
  function sessionAllowKey(owner, sessionId, category, binding) {
86
88
  return JSON.stringify([owner, sessionId, category, binding.kind === "bound" ? binding.digest : null]);
87
89
  }
88
- function askStandingAnswerFacts(req, governanceForced) {
90
+ function askStandingAnswerFacts(req, governanceForced, deploymentListAsks) {
91
+ const classes = screenAskClassMembers(req.askClass, req.classes)?.classes;
89
92
  return {
90
93
  governanceForced,
91
94
  requiresRealApproval: req.requiresRealApproval === true,
92
95
  mandated: req.mandated === true,
93
96
  grantBinding: grantBindingOf(req),
94
- classes: screenAskClassMembers(req.askClass, req.classes)?.classes,
97
+ classes,
98
+ deploymentListQuestion: carriesDeploymentListQuestion(deploymentListAsks, classes),
95
99
  };
96
100
  }
97
- export function policyAskStandingAnswerFacts(req, ask) {
101
+ const NO_GATE_FACTS = { contentQuestion: false, markedUnresolvable: false, org: undefined, tightened: undefined, mandateMarks: {} };
102
+ const DEPLOYMENT_LIST_ASK_CLASSES = new Set(classifyAsk(operatorAsk("APPROVAL_REQUIRE", ""), NO_GATE_FACTS).classes);
103
+ export function carriesDeploymentListQuestion(deploymentListAsks, classes) {
104
+ return deploymentListAsks && classes !== undefined && classes.some((c) => DEPLOYMENT_LIST_ASK_CLASSES.has(c));
105
+ }
106
+ export function policyAskStandingAnswerFacts(req, ask, deploymentListQuestion) {
98
107
  return {
99
108
  governanceForced: false,
100
109
  requiresRealApproval: ask.requiresRealApproval === true,
101
110
  mandated: ask.mandated === true,
102
111
  grantBinding: grantBindingOf(req),
103
- classes: [...classifyAsk(ask, { contentQuestion: false, markedUnresolvable: false, org: undefined, tightened: undefined, mandateMarks: {} }).classes],
112
+ classes: [...classifyAsk(ask, NO_GATE_FACTS).classes],
113
+ deploymentListQuestion,
104
114
  };
105
115
  }
106
- export function standingAnswerMayRetire(facts) {
107
- return (!facts.governanceForced &&
108
- !facts.requiresRealApproval &&
109
- !facts.mandated &&
110
- facts.grantBinding.kind !== "unreadable" &&
111
- facts.classes !== undefined &&
112
- foldArbitrationRow(new Set(facts.classes)).ruleMayClear);
116
+ export function standingAnswerHome(facts) {
117
+ if (facts.governanceForced || facts.requiresRealApproval || facts.mandated || facts.classes === undefined)
118
+ return "none";
119
+ if (facts.deploymentListQuestion) {
120
+ return facts.grantBinding.kind === "none" && facts.classes.every((c) => DEPLOYMENT_LIST_ASK_CLASSES.has(c)) ? "server_store" : "none";
121
+ }
122
+ return facts.grantBinding.kind !== "unreadable" && foldArbitrationRow(new Set(facts.classes)).ruleMayClear ? "seat" : "none";
113
123
  }
114
124
  export function standingAnswerDurablyRecordable(facts) {
115
- return standingAnswerMayRetire(facts) && facts.grantBinding.kind === "none";
125
+ const home = standingAnswerHome(facts);
126
+ switch (home) {
127
+ case "none":
128
+ return false;
129
+ case "seat":
130
+ return facts.grantBinding.kind === "none";
131
+ case "server_store":
132
+ return true;
133
+ default: {
134
+ const unhandled = home;
135
+ throw new Error(`unhandled standing-answer home: ${String(unhandled)}`);
136
+ }
137
+ }
116
138
  }
117
- export function parkRowStandingAnswerFacts(pending, gate, governance) {
139
+ export function parkRowStandingAnswerFacts(pending, gate, governance, deploymentListAsks) {
118
140
  const riskDescriptor = "riskDescriptor" in gate ? gate.riskDescriptor : undefined;
141
+ const classes = screenAskClassMembers(pending.askClass, pending.classes)?.classes;
119
142
  return {
120
143
  governanceForced: governanceOriginOf({ riskDescriptor }, governance) === true,
121
144
  requiresRealApproval: readParkRowTrueSeat(pending, "requiresRealApproval"),
122
145
  mandated: readParkRowTrueSeat(pending, "mandated") || mandatedFromSeat(readRuleOffersAbsenceSeat(pending)),
123
146
  grantBinding: grantBindingOf(pending),
124
- classes: screenAskClassMembers(pending.askClass, pending.classes)?.classes,
147
+ classes,
148
+ deploymentListQuestion: carriesDeploymentListQuestion(deploymentListAsks, classes),
125
149
  };
126
150
  }
127
151
  function pendingEntryStandingAnswerFacts(entry) {
@@ -131,6 +155,7 @@ function pendingEntryStandingAnswerFacts(entry) {
131
155
  mandated: entry.mandated === true,
132
156
  grantBinding: entry.grantBinding,
133
157
  classes: entry.classes,
158
+ deploymentListQuestion: entry.deploymentListQuestion === true,
134
159
  };
135
160
  }
136
161
  function mandatedFromSeat(seat) {
@@ -151,6 +176,11 @@ export const READ_ROOT_GRANT_ALLOW_ONLY = 'readRootGrant is only meaningful with
151
176
  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`;
152
177
  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`)";
153
178
  export function parseToolApprovalResponse(body) {
179
+ const retired = retiredDecisionKeyFacts(body);
180
+ const parsed = parseToolApprovalFields(body, retired);
181
+ return parsed.ok || retired === undefined ? parsed : { ...parsed, ...retired.extra };
182
+ }
183
+ function parseToolApprovalFields(body, retired) {
154
184
  if (body === null || typeof body !== "object" || Array.isArray(body))
155
185
  return { ok: false, error: "body must be an object" };
156
186
  const d = body.decision;
@@ -193,8 +223,8 @@ export function parseToolApprovalResponse(body) {
193
223
  }
194
224
  note = rawNote;
195
225
  }
196
- if (body.settledBy !== undefined)
197
- return { ok: false, error: SETTLED_BY_RETIRED };
226
+ if (retired !== undefined)
227
+ return { ok: false, error: retired.error };
198
228
  const decidedByReading = readDecidedBy(body.decidedBy, false);
199
229
  let decidedBy;
200
230
  switch (decidedByReading.kind) {
@@ -386,6 +416,7 @@ export class ToolApprovalCoordinator {
386
416
  parkedRedeem;
387
417
  askAudit;
388
418
  sessionGrants;
419
+ deploymentListAsks;
389
420
  shutdownDraining = false;
390
421
  asksInFlight = 0;
391
422
  parkCommits;
@@ -407,6 +438,10 @@ export class ToolApprovalCoordinator {
407
438
  this.parkTombstonePerPrincipalMax = opts?.parkTombstonePerPrincipalMax ?? DEFAULT_PARK_TOMBSTONE_PER_PRINCIPAL_MAX;
408
439
  this.askAudit = opts?.askAudit;
409
440
  this.sessionGrants = opts?.sessionGrants;
441
+ if (opts?.deploymentListAsks !== undefined && opts.sessionGrants === undefined) {
442
+ throw new Error("ToolApprovalCoordinator: deploymentListAsks needs sessionGrants — the deployment list's standing answers live in the session exemption store");
443
+ }
444
+ this.deploymentListAsks = opts?.deploymentListAsks;
410
445
  this.parkCommits = opts?.parkCommits;
411
446
  this.parkCommits?.onParkCommitted((calls) => this.noteParkCommitted(calls));
412
447
  }
@@ -758,9 +793,19 @@ export class ToolApprovalCoordinator {
758
793
  return (this.governanceAskMarks ?? governanceAskMarksFor(this.streamKey(origin.owner, origin.sessionId, origin.taskId))).isMarked(req.toolCallId);
759
794
  }
760
795
  async standingGrantSettles(origin, req, governanceForced) {
761
- const facts = askStandingAnswerFacts(req, governanceForced);
762
- if (!standingAnswerMayRetire(facts))
763
- return false;
796
+ const facts = askStandingAnswerFacts(req, governanceForced, this.deploymentListAsks?.(req) === true);
797
+ const home = standingAnswerHome(facts);
798
+ switch (home) {
799
+ case "seat":
800
+ break;
801
+ case "none":
802
+ case "server_store":
803
+ return false;
804
+ default: {
805
+ const unhandled = home;
806
+ throw new Error(`unhandled standing-answer home: ${String(unhandled)}`);
807
+ }
808
+ }
764
809
  const sessionId = origin.sessionId;
765
810
  if (!sessionId)
766
811
  return false;
@@ -771,7 +816,7 @@ export class ToolApprovalCoordinator {
771
816
  if (!(await this.sessionGrantUncontestedByRow(origin, req)))
772
817
  return false;
773
818
  if (durable)
774
- defaultLogger.info("approval_exempted", { sessionId, toolName: req.toolName, seat: "approval_seat" });
819
+ defaultLogger.info(APPROVAL_EXEMPTED_EVENT, { sessionId, toolName: req.toolName, seat: "approval_seat" });
775
820
  return true;
776
821
  }
777
822
  async durableGrantHas(sessionId, canonicalToolName) {
@@ -1052,6 +1097,7 @@ export class ToolApprovalCoordinator {
1052
1097
  if (signal?.aborted)
1053
1098
  return false;
1054
1099
  const governanceForced = this.governanceForcedOf(origin, req);
1100
+ const deploymentListAsks = this.deploymentListAsks?.(req) === true;
1055
1101
  if (await this.standingGrantSettles(origin, req, governanceForced))
1056
1102
  return true;
1057
1103
  const aliveCtxs = ctxs.filter((c) => !c.abortSignal?.aborted);
@@ -1107,7 +1153,7 @@ export class ToolApprovalCoordinator {
1107
1153
  ...(governanceForced ? { governanceForced: true } : {}),
1108
1154
  ...(req.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
1109
1155
  ...(req.mandated === true ? { mandated: true } : {}),
1110
- ...(standingAnswerMayRetire(askStandingAnswerFacts(req, governanceForced)) ? {} : { rememberRefused: true }),
1156
+ ...(standingAnswerHome(askStandingAnswerFacts(req, governanceForced, deploymentListAsks)) === "none" ? { rememberRefused: true } : {}),
1111
1157
  ...(typeof req.boundGrantDigest === "string" ? { boundGrantDigest: req.boundGrantDigest, ...(req.boundGrants !== undefined ? { boundGrants: req.boundGrants } : {}) } : {}),
1112
1158
  ...(noRulingCause !== undefined ? { classifierNoRuling: noRulingCause } : {}),
1113
1159
  ...(mandateWord !== undefined ? { mandate: mandateWord } : {}),
@@ -1637,13 +1683,15 @@ export class ToolApprovalCoordinator {
1637
1683
  }
1638
1684
  const entryGrantBinding = grantBindingOf(req);
1639
1685
  const entryClasses = askClassPair?.classes;
1640
- const rememberRefused = !standingAnswerMayRetire(pendingEntryStandingAnswerFacts({
1686
+ const entryDeploymentListQuestion = carriesDeploymentListQuestion(deploymentListAsks, entryClasses);
1687
+ const rememberRefused = standingAnswerHome(pendingEntryStandingAnswerFacts({
1641
1688
  ...(effectiveGovernanceForced ? { governanceForced: true } : {}),
1642
1689
  ...(req.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
1643
1690
  ...(req.mandated === true ? { mandated: true } : {}),
1644
1691
  grantBinding: entryGrantBinding,
1645
1692
  classes: entryClasses,
1646
- }));
1693
+ ...(entryDeploymentListQuestion ? { deploymentListQuestion: true } : {}),
1694
+ })) === "none";
1647
1695
  if (rememberRefused)
1648
1696
  frame.rememberRefused = true;
1649
1697
  else
@@ -1665,6 +1713,7 @@ export class ToolApprovalCoordinator {
1665
1713
  toolName: req.toolName,
1666
1714
  grantBinding: entryGrantBinding,
1667
1715
  ...(entryClasses !== undefined ? { classes: entryClasses } : {}),
1716
+ ...(entryDeploymentListQuestion ? { deploymentListQuestion: true } : {}),
1668
1717
  registeredAtMs: Date.now(),
1669
1718
  expiresAtMs: persistedExpiresAtMs,
1670
1719
  ...(ruleLaneMaterial !== undefined ? { ruleLane: ruleLaneMaterial } : {}),
@@ -1794,7 +1843,7 @@ export class ToolApprovalCoordinator {
1794
1843
  respond(id, principal, body, httpReq) {
1795
1844
  const parsed = parseToolApprovalResponse(body);
1796
1845
  if (!parsed.ok)
1797
- return { status: 400, body: { error: parsed.error, errorCode: "request.body_shape" } };
1846
+ return { status: 400, body: { error: parsed.error, errorCode: "request.body_shape", ...(parsed.field !== undefined ? { field: parsed.field } : {}) } };
1798
1847
  const entry = this.pending.get(id);
1799
1848
  if (!entry || (entry.owner !== null && entry.owner !== principal)) {
1800
1849
  return this.respondLate(id, principal, parsed, httpReq);
@@ -1802,17 +1851,43 @@ export class ToolApprovalCoordinator {
1802
1851
  const preRule = this.precheckEditedRuleBeforeDecision(entry, parsed);
1803
1852
  if (preRule !== undefined)
1804
1853
  return preRule;
1805
- if (policyCannotAllow(parsed.decidedBy, parsed.value !== "deny", entry.requiresRealApproval === true)) {
1854
+ if (parsed.value !== "deny" && policyCannotAnswer(heldAskOf(entry.requiresRealApproval === true), parsed.decidedBy)) {
1806
1855
  return { status: 400, body: { error: POLICY_CANNOT_ALLOW_PERSON_ASK, errorCode: "request.body_shape" } };
1807
1856
  }
1808
1857
  const ruleEligibility = { eligible: true };
1809
- const settled = this.askStore && entry.askId !== undefined && entry.batchId !== undefined
1858
+ const decided = this.askStore && entry.askId !== undefined && entry.batchId !== undefined
1810
1859
  ? this.respondWithCas(this.askStore, entry.askId, entry.batchId, id, entry, parsed, ruleEligibility)
1811
1860
  : this.finishRespond(id, entry, parsed);
1861
+ const settled = this.recordDeploymentListAnswer(decided, entry, parsed, principal, ruleEligibility);
1812
1862
  if (parsed.persistRule === undefined)
1813
1863
  return settled;
1814
1864
  return this.persistRuleAfterDecision(settled, entry, parsed.persistRule, parsed.updatedInput !== undefined, ruleEligibility);
1815
1865
  }
1866
+ recordDeploymentListAnswer(decided, entry, parsed, principal, eligibility) {
1867
+ const store = this.sessionGrants;
1868
+ const sessionId = entry.sessionId;
1869
+ if (parsed.value !== "allow_session" || !sessionId || store === undefined)
1870
+ return decided;
1871
+ if (standingAnswerHome(pendingEntryStandingAnswerFacts(entry)) !== "server_store")
1872
+ return decided;
1873
+ return (async () => {
1874
+ const result = await decided;
1875
+ if (result.status !== 200)
1876
+ return result;
1877
+ if (!eligibility.eligible)
1878
+ return { ...result, body: { ...result.body, rememberApplied: false } };
1879
+ let rememberApplied = false;
1880
+ try {
1881
+ await this.withStoreDeadline(store.grant(sessionId, entry.toolName, principal ?? entry.owner), "sessionGrants.grant(deployment-list)", DURABLE_CALL_TIMEOUT_MS);
1882
+ rememberApplied = true;
1883
+ defaultLogger.info("approval_exemption_granted", { sessionId, toolName: entry.toolName, grantedBy: principal ?? entry.owner });
1884
+ }
1885
+ catch (err) {
1886
+ defaultLogger.warn("approval_exemption_grant_failed", { sessionId, toolName: entry.toolName, error: String(err).slice(0, 200) });
1887
+ }
1888
+ return { ...result, body: { ...result.body, rememberApplied } };
1889
+ })();
1890
+ }
1816
1891
  precheckEditedRuleBeforeDecision(entry, parsed) {
1817
1892
  const persistRule = parsed.persistRule;
1818
1893
  if (persistRule === undefined || persistRule.kind !== "text" || !persistRule.edited)
@@ -2255,7 +2330,21 @@ export class ToolApprovalCoordinator {
2255
2330
  }
2256
2331
  finishRespond(id, entry, parsed, opts) {
2257
2332
  let rememberApplied;
2258
- const grantable = standingAnswerMayRetire(pendingEntryStandingAnswerFacts(entry));
2333
+ const home = standingAnswerHome(pendingEntryStandingAnswerFacts(entry));
2334
+ let grantable;
2335
+ switch (home) {
2336
+ case "seat":
2337
+ grantable = true;
2338
+ break;
2339
+ case "none":
2340
+ case "server_store":
2341
+ grantable = false;
2342
+ break;
2343
+ default: {
2344
+ const unhandled = home;
2345
+ throw new Error(`unhandled standing-answer home: ${String(unhandled)}`);
2346
+ }
2347
+ }
2259
2348
  if (parsed.value === "allow_session")
2260
2349
  rememberApplied = Boolean(entry.sessionId) && grantable;
2261
2350
  if (parsed.value === "allow_session" && entry.sessionId && grantable) {
@@ -2299,7 +2388,7 @@ export class ToolApprovalCoordinator {
2299
2388
  ...(e.requiresRealApproval ? { requiresRealApproval: true } : {}),
2300
2389
  ...(e.governanceForced ? { governanceForced: true } : {}),
2301
2390
  ...(e.mandated ? { mandated: true } : {}),
2302
- ...(standingAnswerMayRetire(pendingEntryStandingAnswerFacts(e)) ? {} : { rememberRefused: true }),
2391
+ ...(standingAnswerHome(pendingEntryStandingAnswerFacts(e)) === "none" ? { rememberRefused: true } : {}),
2303
2392
  ...(e.mandate !== undefined ? { mandate: e.mandate } : {}),
2304
2393
  ...(e.origin !== undefined ? { origin: e.origin } : {}),
2305
2394
  ...(e.originTaskId !== undefined ? { fromSubagent: true, originTaskId: e.originTaskId } : {}),
@@ -50,6 +50,7 @@ import { stripFormatCharacters } from "@sema-agent/core";
50
50
  * 现役消费方:`src/tool-approval.ts` 的 `durableRuleMaterial`(规则车道素材闸)与语料的 FP=0 格。
51
51
  */
52
52
  export declare const normalizeForRedaction: typeof stripFormatCharacters;
53
+ export declare const REDACTION_TOKEN_RE_SRC: string;
53
54
  export declare function armPoolBudgetForTest(): {
54
55
  spanCount: number;
55
56
  coveredPositions: number;
@@ -163,6 +164,11 @@ export declare function redactionSpansForTest(text: string, opts?: {
163
164
  * 让下游看到"这里有东西被省略了",而不是丢一个空对象或炸掉整条流。
164
165
  */
165
166
  export declare function redactDeep(value: unknown): unknown;
167
+ /** {@link redactDeep} 的**串叶处置可注入**形:同一只环 / 深度守卫的遍历,只有「一个串叶怎么脱」由调用方给。
168
+ * `redactDeep` 本身就是 `leaf = redactSecrets` 的那一档(行为逐字不变)。读面要的是另一档 —— 串叶走
169
+ * `redactModelText`(认出凭据形才改写字节,不含凭据形的串逐字节不变),遍历不该因此再写第二份。
170
+ * `leaf` 必须是纯的串→串函数且**自己不抛**(抛了就穿出本遍历)。 */
171
+ export declare function redactDeepWith(value: unknown, leaf: (s: string) => string): unknown;
166
172
  /**
167
173
  * A REDACTED, length-BOUNDED string preview of a structured value — for surfacing a pending tool call's `args`
168
174
  * in the assistant inbox ("what is this ask about") WITHOUT leaking secrets or dumping an unbounded payload (a
@@ -1,7 +1,7 @@
1
1
  import { displayUrlOf, stripFormatCharacters } from "@sema-agent/core";
2
2
  import { recordFailOpen } from "../observability/fail-open.js";
3
3
  export const normalizeForRedaction = stripFormatCharacters;
4
- const REDACTION_TOKEN_RE_SRC = String.raw `«redacted(?::[a-z-]{1,16}){0,2}»`;
4
+ export const REDACTION_TOKEN_RE_SRC = String.raw `«redacted(?::[a-z-]{1,16}){0,2}»`;
5
5
  const ARM_VALUE_CLASSES = [];
6
6
  const atLeast = (cls, min) => {
7
7
  ARM_VALUE_CLASSES.push(cls);
@@ -1797,11 +1797,14 @@ export function redactionSpansForTest(text, opts) {
1797
1797
  }
1798
1798
  const REDACT_MAX_DEPTH = 64;
1799
1799
  export function redactDeep(value) {
1800
- return redactDeepInner(value, 0, new WeakSet());
1800
+ return redactDeepWith(value, redactSecrets);
1801
1801
  }
1802
- function redactDeepInner(value, depth, seen) {
1802
+ export function redactDeepWith(value, leaf) {
1803
+ return redactDeepInner(value, 0, new WeakSet(), leaf);
1804
+ }
1805
+ function redactDeepInner(value, depth, seen, leaf) {
1803
1806
  if (typeof value === "string")
1804
- return redactSecrets(value);
1807
+ return leaf(value);
1805
1808
  if (!value || typeof value !== "object")
1806
1809
  return value;
1807
1810
  if (seen.has(value))
@@ -1811,11 +1814,8 @@ function redactDeepInner(value, depth, seen) {
1811
1814
  seen.add(value);
1812
1815
  try {
1813
1816
  if (Array.isArray(value))
1814
- return value.map((v) => redactDeepInner(v, depth + 1, seen));
1815
- const out = {};
1816
- for (const [k, v] of Object.entries(value))
1817
- out[k] = redactDeepInner(v, depth + 1, seen);
1818
- return out;
1817
+ return value.map((v) => redactDeepInner(v, depth + 1, seen, leaf));
1818
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, redactDeepInner(v, depth + 1, seen, leaf)]));
1819
1819
  }
1820
1820
  finally {
1821
1821
  seen.delete(value);
@@ -21,7 +21,7 @@ export declare const SELF_MINTED_WIRE: {
21
21
  /**
22
22
  * S-928(7.110.0):`engine_notice` `task.user_steer_undrained` 上**本仓铸**的那一臂。词表属主是 core(`USER_INPUT_UNDRAINED_CAUSES`,两族共用;
23
23
  * `leg_end` 由引擎铸、逐字过境,不在本表);本表登记的是「这个值自哪一版起由本服务出现在 wire 上」。`satisfies Record<CrashedCause, …>`:
24
- * core 删掉 `crashed` ⇒ `CrashedCause` 成 never ⇒ 本行编译红。码按受理时的车道选(`user-input-ledger.ts` `userInputLaneOf`):本行 = steer 车道。
24
+ * core 删掉 `crashed` ⇒ `CrashedCause` 成 never ⇒ 本行编译红。码按受理时的车道选(`user-input-ledger.ts` `acceptedInputKindOf`,车道 = core `injectionLaneOf`,码 = core `USER_INPUT_UNDRAINED_FAMILIES`):本行 = steer 车道。
25
25
  */
26
26
  readonly userSteerUndrainedServerCause: {
27
27
  readonly at: "engine_notice task.user_steer_undrained detail.cause(本服务铸的臂;下一条同会话 run 开通告口那一拍)";
@@ -23,8 +23,9 @@
23
23
  * 子代 resume(`…/subagents/:target/resume`)不往在飞的队列里投,起的是一条新的子代腿;follow-up 在本仓**没有**独立的受理口(`src/http/routes` 零
24
24
  * follow-up 入口)。steer 的 `later` 档在引擎里走 follow-up 车道(引擎在腿收场时按车道分账:没投出去的 `later` 记进 `task.user_followup_undrained`),
25
25
  * 但它仍是一次 `TaskStream.steer` 受理、消费帧同为 `human_input{delivery:"applied"}`(两车道同一只消费口)⇒ 同进本账,消费即结清。
26
- * **族 = 受理时它排进的引擎车道**({@link UserInputKind}:`steer` / `follow_up`,由档位经 {@link userInputLaneOf} 一只函数得出),崩溃告知的码按族选
27
- * (core 7.44.0 #1362:两族同一只成因词表 `USER_INPUT_UNDRAINED_CAUSES`,服务层的 `crashed` 臂按车道选码,与引擎自己的 `leg_end` 臂同一种分账)。
26
+ * **族 = 受理时它排进的引擎车道**(core `UserInputLane`,经引擎的 `injectionLaneOf(steerPriorityOf(…))` 读出 —— {@link acceptedInputKindOf};落盘 / wire 词 {@link UserInputKind}:
27
+ * `steer` / `follow_up`),崩溃告知的码与条数键按车道取 core `USER_INPUT_UNDRAINED_FAMILIES`(core 7.44.0 #1362:两族同一只成因词表 `USER_INPUT_UNDRAINED_CAUSES`,
28
+ * 服务层的 `crashed` 臂按车道选族,与引擎自己的 `leg_end` 臂同一种分账;core 7.45.0 #1408 起族表与车道读法在包根,本仓不再抄)。
28
29
  *
29
30
  * ## 五形行(append-only;行序 = 语义序;File 孪生的盘上形 = 下面的 {@link UserInputLedgerRecord},SQL 孪生 = 一行一受理 + CAS 列 + 删行,同义)
30
31
  * · `accepted` —— 受理点(`routes/runs.ts` 的 live 腿,**调 `TaskStream.steer` 之前**、回 200 之前)写;写不进 ⇒ 503
@@ -85,28 +86,30 @@
85
86
  * 不开旋钮(没有「关掉告知」的正当需求)· 不改 steer 的 200 体(回执仍是「已交付」,账本是它的影子)· 不重放丢失的输入(core 明写
86
87
  * NOT redelivered;本账只告知,不替人决定重发)· 不存输入正文(行里只有身份与时刻)。
87
88
  */
88
- import { type EngineNoticeCode, type SystemInjectionPriority, type TaskEvent, type UserInputUndrainedCause } from "@sema-agent/core";
89
+ import { type EngineNoticeCode, type SystemInjectionPriority, type TaskEvent, type UserInputLane, type UserInputUndrainedCause } from "@sema-agent/core";
89
90
  import { type RunRecord } from "./plugins/store-contracts.js";
90
91
  import { type EngineNoticeRouter } from "./trace/engine-notice-wire.js";
91
92
  /** local 数据根下的子目录(File 孪生;与 `approval-ask-audit/` 同层同形)。 */
92
93
  export declare const USER_INPUT_LEDGER_DIR = "user-input-ledger";
93
94
  /**
94
- * 账本收的输入族(闭集)= 受理时它排进的**引擎车道**:`steer`(`now` / `next` 档)/ `follow_up`(`later` 档)。崩溃告知的码按族选
95
- * ({@link UNDRAINED_NOTICE_OF}),读面行的 `kind` 就是它。`follow_up` 自 7.118.0 起写(之前的版本受理口一律写 `steer`,`later` 档的旧行照它当时的族读)。
95
+ * 账本收的输入族 = 受理时它排进的**引擎车道**(core `UserInputLane`:`steer` / `followUp`;车道怎么读、每条车道的告知码与条数键全归 core,见下)。
96
+ * 本表只做一件事:把车道写成本仓 7.118.0 起**已落盘、已上 wire**(`GET /v1/sessions/:id` 的 `userInputUndrained[].kind`)的那个词 —— `steer` / `follow_up`。
97
+ * `satisfies Record<UserInputLane, …>`:core 加一条车道 ⇒ 编译红,由人裁它落盘写什么。`follow_up` 自 7.118.0 起写(之前的版本受理口一律写 `steer`,`later` 档的旧行照它当时的族读)。
96
98
  */
97
- export type UserInputKind = "steer" | "follow_up";
98
- /** 读盘词表(`satisfies Record<…, true>` ⇒ 闭集加词而本表没跟 = 编译红;判定走 `Object.hasOwn`)。≤7.117.x 的读腿不认 `follow_up`(回滚须知在 MIGRATION)。 */
99
- export declare const USER_INPUT_KIND_WORDS: {
100
- readonly steer: true;
101
- readonly follow_up: true;
99
+ declare const LEDGER_KIND_OF_LANE: {
100
+ readonly steer: "steer";
101
+ readonly followUp: "follow_up";
102
102
  };
103
+ /** 落盘 / wire 的族词(闭集,派生自 {@link LEDGER_KIND_OF_LANE})。 */
104
+ export type UserInputKind = (typeof LEDGER_KIND_OF_LANE)[UserInputLane];
105
+ /** 读盘词表(判定走 `Object.hasOwn`;派生自 {@link LEDGER_KIND_OF_LANE})。≤7.117.x 的读腿不认 `follow_up`(回滚须知在 MIGRATION)。 */
106
+ export declare const USER_INPUT_KIND_WORDS: Readonly<Record<UserInputKind, true>>;
103
107
  /**
104
- * 一次受理排进哪条引擎车道 —— 三个受理口(run steer 的 live 腿 / interrupt 带 text 形 / 子代 steer)同调这一只。core 原句(`TaskStream.steer` 注):
105
- * 「a `"later"` steer rides the followUp lane, so its stranded account is `task.user_followup_undrained`」;`now` / `next` 走 steer 车道。
106
- * 档位型取 core 包根的 `SystemInjectionPriority`(`TaskStream.steer` 的 `priority`;缺席 = 引擎缺省 `next`),本仓不复述:core 加一档 ⇒ 下面的
107
- * 穷举 `never` 臂编译红,由人裁新档排进哪条车道。
108
+ * 一次受理的族词 —— 三个受理口(run steer 的 live 腿 / interrupt 带 text 形 / 子代 steer)同调这一只。车道是引擎的那一只读法
109
+ * `injectionLaneOf(steerPriorityOf(…))`(core 7.45.0 #1408:缺席档 = `next`;`later` 走 follow-up 车道,`now` / `next` 走 steer 车道;
110
+ * 档外值抛 `steering.invalid_content` —— 受理口在这之前已按 `isSystemInjectionPriority` 400 掉档外值,这一抛在本仓不可达)。
108
111
  */
109
- export declare function userInputLaneOf(tier: SystemInjectionPriority | undefined): UserInputKind;
112
+ export declare function acceptedInputKindOf(priority: SystemInjectionPriority | undefined): UserInputKind;
110
113
  /** `settled` 的两种收场(闭集,见顶注)。 */
111
114
  export type SettleHow = "leg_end" | "not_applied" | "consumed";
112
115
  /** 读盘词表(File 孪生的折叠守卫 / SQL 孪生的 `settle_how` 列读腿同读这一张;`consumed` 自 7.113.0 起,≤7.112.x 的读腿不认它 —— 回滚须知在 MIGRATION)。 */
@@ -380,4 +383,5 @@ export declare function createUserInputLedgerAtBoot(opts: Parameters<typeof crea
380
383
  * · `mysql` / `pg` —— 多副本共享一张表,「我启动之前写的」证明不了写者已死 ⇒ `run_store_verdict`(等 reaper 落款)。
381
384
  */
382
385
  export declare function writerGoneEvidenceFor(backendKind: "mysql" | "pg" | "local", runs: RunStoreReader): WriterGoneEvidence;
386
+ export {};
383
387
  //# sourceMappingURL=user-input-ledger.d.ts.map
@@ -1,22 +1,13 @@
1
- import { USER_INPUT_UNDRAINED_CAUSES } from "@sema-agent/core";
1
+ import { USER_INPUT_UNDRAINED_CAUSES, USER_INPUT_UNDRAINED_FAMILIES, injectionLaneOf, steerPriorityOf } from "@sema-agent/core";
2
2
  import { recordFailOpen } from "./observability/fail-open.js";
3
3
  import { isHolderDeathVerdict } from "./plugins/store-contracts.js";
4
4
  import { defaultEngineNoticeRouter } from "./trace/engine-notice-wire.js";
5
5
  export const USER_INPUT_LEDGER_DIR = "user-input-ledger";
6
- export const USER_INPUT_KIND_WORDS = { steer: true, follow_up: true };
7
- export function userInputLaneOf(tier) {
8
- switch (tier) {
9
- case "later":
10
- return "follow_up";
11
- case "now":
12
- case "next":
13
- case undefined:
14
- return "steer";
15
- default: {
16
- const unhandled = tier;
17
- throw new Error(`unhandled steer tier: ${String(unhandled)}`);
18
- }
19
- }
6
+ const LEDGER_KIND_OF_LANE = { steer: "steer", followUp: "follow_up" };
7
+ const LANE_OF_LEDGER_KIND = Object.fromEntries(Object.entries(LEDGER_KIND_OF_LANE).map(([lane, kind]) => [kind, lane]));
8
+ export const USER_INPUT_KIND_WORDS = Object.freeze(Object.fromEntries(Object.values(LEDGER_KIND_OF_LANE).map((kind) => [kind, true])));
9
+ export function acceptedInputKindOf(priority) {
10
+ return LEDGER_KIND_OF_LANE[injectionLaneOf(steerPriorityOf(priority === undefined ? {} : { priority }))];
20
11
  }
21
12
  export const SETTLE_HOW_WORDS = { leg_end: true, not_applied: true, consumed: true };
22
13
  const UNDRAINED_CAUSE_MINTER = { leg_end: "engine", crashed: "server" };
@@ -138,15 +129,16 @@ export function sortAcceptances(list) {
138
129
  export const USER_INPUT_OPEN_SCAN_MAX = 1000;
139
130
  export const USER_INPUT_OPEN_SCAN_PAGES = 100;
140
131
  export const USER_INPUT_SESSION_READ_MAX = 500;
141
- const UNDRAINED_NOTICE_OF = {
142
- steer: { code: "task.user_steer_undrained", countKey: "steer", what: "user steer(s)" },
143
- follow_up: { code: "task.user_followup_undrained", countKey: "followUp", what: "user steer(s) queued for the run's end (priority later)" },
132
+ const UNDRAINED_WHAT_OF_LANE = {
133
+ steer: "user steer(s)",
134
+ followUp: "user steer(s) queued for the run's end (priority later)",
144
135
  };
145
136
  export function buildCrashedUndrainedNotice(input) {
146
- const face = UNDRAINED_NOTICE_OF[input.family];
137
+ const lane = LANE_OF_LEDGER_KIND[input.family];
138
+ const face = USER_INPUT_UNDRAINED_FAMILIES[lane];
147
139
  return {
148
140
  code: face.code,
149
- message: `Check the earlier run's transcript and results before re-sending any of these steers to a live run. The server accepted ${input.count} ${face.what} for run ${input.taskId} (each answered 200 applied or 202 queued-immediate), but the process running that run ended before the engine confirmed consuming them. They are NOT redelivered.`,
141
+ message: `Check the earlier run's transcript and results before re-sending any of these steers to a live run. The server accepted ${input.count} ${UNDRAINED_WHAT_OF_LANE[lane]} for run ${input.taskId} (each answered 200 applied or 202 queued-immediate), but the process running that run ended before the engine confirmed consuming them. They are NOT redelivered.`,
150
142
  detail: { [face.countKey]: input.count, cause: CRASHED_CAUSE, taskId: input.taskId, sessionId: input.sessionId },
151
143
  sessionId: input.sessionId,
152
144
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/server",
3
- "version": "7.118.0",
3
+ "version": "7.119.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",
@@ -64,7 +64,7 @@
64
64
  "build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
65
65
  },
66
66
  "dependencies": {
67
- "@sema-agent/core": "7.44.0",
67
+ "@sema-agent/core": "7.45.0",
68
68
  "@sema-agent/settings-schema": "8.0.0",
69
69
  "e2b": "^2.28.0",
70
70
  "libsodium-wrappers": "^0.8.4",