@sema-agent/core 7.56.0 → 7.57.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 (173) hide show
  1. package/CHANGELOG.md +541 -0
  2. package/LICENSE +1 -1
  3. package/dist/agents/org-delivery-gate.js +3 -4
  4. package/dist/agents/peer-held-queue.d.ts +69 -49
  5. package/dist/agents/peer-held-queue.js +255 -106
  6. package/dist/agents/peer-session-drain.js +63 -43
  7. package/dist/agents/send-message-tool.js +3 -2
  8. package/dist/agents/subagent.js +8 -8
  9. package/dist/brain/anthropic.js +16 -8
  10. package/dist/brain/open-responses.js +10 -8
  11. package/dist/brain/openai.js +11 -9
  12. package/dist/brain/provider-dialect.d.ts +3 -2
  13. package/dist/brain/provider-dialect.js +2 -2
  14. package/dist/brain/request-params.d.ts +6 -0
  15. package/dist/brain/request-params.js +14 -1
  16. package/dist/brain/stream-engine.d.ts +9 -0
  17. package/dist/brain/stream-engine.js +24 -10
  18. package/dist/brain/stream-shared.d.ts +5 -4
  19. package/dist/brain/stream-shared.js +4 -4
  20. package/dist/core/allow-entry.d.ts +3 -10
  21. package/dist/core/allow-entry.js +17 -25
  22. package/dist/core/approval-object.d.ts +21 -5
  23. package/dist/core/approval-object.js +44 -17
  24. package/dist/core/ask-origin.d.ts +2 -2
  25. package/dist/core/auto-compaction.d.ts +71 -6
  26. package/dist/core/auto-compaction.js +42 -26
  27. package/dist/core/background-agent-store.d.ts +6 -2
  28. package/dist/core/background-agent-store.js +1 -1
  29. package/dist/core/checkpoint-requirements.d.ts +7 -1
  30. package/dist/core/checkpoint-requirements.js +2 -0
  31. package/dist/core/checkpoint-store.d.ts +17 -29
  32. package/dist/core/checkpoint-store.js +19 -11
  33. package/dist/core/cross-boundary-taint.js +4 -2
  34. package/dist/core/engine-notice.d.ts +3 -2
  35. package/dist/core/engine-notice.js +6 -0
  36. package/dist/core/file-history-store.d.ts +14 -8
  37. package/dist/core/file-history-store.js +14 -4
  38. package/dist/core/gate-lanes.js +2 -3
  39. package/dist/core/hooks.d.ts +2 -2
  40. package/dist/core/mailbox-store.d.ts +62 -1
  41. package/dist/core/mailbox-store.js +59 -0
  42. package/dist/core/mcp-name-judge.d.ts +46 -0
  43. package/dist/core/mcp-name-judge.js +61 -0
  44. package/dist/core/mcp-probe.d.ts +1 -1
  45. package/dist/core/mcp-probe.js +2 -5
  46. package/dist/core/mcp.js +10 -30
  47. package/dist/core/memory-engine/consolidation-driver.js +4 -3
  48. package/dist/core/memory-engine/distiller.js +26 -17
  49. package/dist/core/memory-engine/layout.js +5 -2
  50. package/dist/core/park-selfcheck.js +2 -0
  51. package/dist/core/permission-rule-consent.d.ts +5 -3
  52. package/dist/core/permission-rule-consent.js +20 -15
  53. package/dist/core/permission-rule-model.d.ts +43 -2
  54. package/dist/core/permission-rule-model.js +52 -8
  55. package/dist/core/permission-rule-org.js +1 -1
  56. package/dist/core/protocol-http-auth.d.ts +16 -2
  57. package/dist/core/protocol-http-auth.js +42 -11
  58. package/dist/core/protocol-naming.d.ts +20 -0
  59. package/dist/core/protocol-naming.js +20 -3
  60. package/dist/core/read-only-shell.d.ts +20 -5
  61. package/dist/core/read-only-shell.js +29 -14
  62. package/dist/core/remote-env.d.ts +37 -1
  63. package/dist/core/remote-env.js +11 -0
  64. package/dist/core/resume-startup-cause.d.ts +24 -2
  65. package/dist/core/resume-startup-cause.js +20 -1
  66. package/dist/core/roles.js +50 -7
  67. package/dist/core/runner/compact-session.d.ts +10 -0
  68. package/dist/core/runner/compact-session.js +202 -0
  69. package/dist/core/runner/compaction-call-options.d.ts +2 -2
  70. package/dist/core/runner/compaction-seams.d.ts +21 -1
  71. package/dist/core/runner/compaction-seams.js +11 -8
  72. package/dist/core/runner/contracts.d.ts +23 -16
  73. package/dist/core/runner/derived-route-fallback.d.ts +13 -0
  74. package/dist/core/runner/derived-route-fallback.js +6 -0
  75. package/dist/core/runner/env-capability-census.d.ts +3 -2
  76. package/dist/core/runner/env-capability-census.js +9 -3
  77. package/dist/core/runner/permission-rule-lanes.d.ts +7 -7
  78. package/dist/core/runner/permission-rule-lanes.js +17 -11
  79. package/dist/core/runner/prepare-acquire-reconcile.d.ts +3 -0
  80. package/dist/core/runner/prepare-acquire-reconcile.js +2 -1
  81. package/dist/core/runner/prepare-config-doors.d.ts +6 -0
  82. package/dist/core/runner/prepare-config-doors.js +4 -3
  83. package/dist/core/runner/prepare-hands-readface.d.ts +3 -1
  84. package/dist/core/runner/prepare-hands-readface.js +2 -0
  85. package/dist/core/runner/prepare-park-ask.d.ts +2 -0
  86. package/dist/core/runner/prepare-park-ask.js +2 -2
  87. package/dist/core/runner/prepare-policy-chain.js +2 -4
  88. package/dist/core/runner/prepare-task.d.ts +4 -1
  89. package/dist/core/runner/prepare-task.js +23 -27
  90. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +13 -0
  91. package/dist/core/runner/prepare-tool-disclosure-mount.js +13 -7
  92. package/dist/core/runner/rewind-files.d.ts +2 -5
  93. package/dist/core/runner/rewind-files.js +9 -51
  94. package/dist/core/runner/run-leg.js +10 -21
  95. package/dist/core/runner/run-notification-lane.d.ts +2 -0
  96. package/dist/core/runner/run-notification-lane.js +2 -2
  97. package/dist/core/runner/run-recovery-lanes.js +3 -4
  98. package/dist/core/runner/run-settle-and-teardown.js +5 -11
  99. package/dist/core/runner/run-terminal-adoption.js +3 -4
  100. package/dist/core/runner/run-turn-boundary.js +3 -5
  101. package/dist/core/runner/runtask.d.ts +8 -1
  102. package/dist/core/runner/runtask.js +28 -34
  103. package/dist/core/runner/send-message-continuation.js +1 -1
  104. package/dist/core/runner/session-verb-doors.d.ts +28 -0
  105. package/dist/core/runner/session-verb-doors.js +48 -0
  106. package/dist/core/runner/session-write-ledger.d.ts +30 -0
  107. package/dist/core/runner/session-write-ledger.js +101 -0
  108. package/dist/core/runner/stream-settle-backstop.js +11 -14
  109. package/dist/core/runner/sub-agent-deny-lists.d.ts +25 -3
  110. package/dist/core/runner/sub-agent-deny-lists.js +21 -10
  111. package/dist/core/runner/teardown-bounded.d.ts +1 -0
  112. package/dist/core/runner/teardown-bounded.js +1 -0
  113. package/dist/core/runner-deps.d.ts +2 -2
  114. package/dist/core/secret-env.d.ts +4 -0
  115. package/dist/core/secret-env.js +3 -0
  116. package/dist/core/session-policy-store.d.ts +7 -2
  117. package/dist/core/session-policy-store.js +1 -1
  118. package/dist/core/shell-wrapper-projection.d.ts +1 -1
  119. package/dist/core/store-contracts/background-agent-store-contract.js +1 -0
  120. package/dist/core/store-contracts/checkpoint-store-contract.js +2 -1
  121. package/dist/core/store-contracts/file-history-store-contract.js +9 -0
  122. package/dist/core/store-contracts/mailbox-store-contract.js +49 -1
  123. package/dist/core/task-registry-agent.d.ts +8 -4
  124. package/dist/core/task-registry-agent.js +14 -8
  125. package/dist/core/task-registry-shared.d.ts +35 -6
  126. package/dist/core/task-registry-shared.js +11 -0
  127. package/dist/core/task-registry.d.ts +1 -1
  128. package/dist/core/task-registry.js +1 -1
  129. package/dist/core/tool-catalog-entries.d.ts +6 -0
  130. package/dist/core/tool-catalog-entries.js +1 -0
  131. package/dist/core/tool-name-aliases.d.ts +32 -2
  132. package/dist/core/tool-name-aliases.js +23 -0
  133. package/dist/core/tool-name-readings.d.ts +4 -0
  134. package/dist/core/tool-name-readings.js +1 -0
  135. package/dist/core/tool-policy.d.ts +10 -6
  136. package/dist/core/tool-policy.js +4 -7
  137. package/dist/core/trace.d.ts +4 -1
  138. package/dist/core/trace.js +2 -1
  139. package/dist/core/untrusted-egress.d.ts +20 -0
  140. package/dist/core/untrusted-egress.js +77 -3
  141. package/dist/engine/harness/agent-harness.d.ts +7 -5
  142. package/dist/engine/harness/agent-harness.js +11 -2
  143. package/dist/engine/loop/agent-loop.d.ts +1 -4
  144. package/dist/engine/loop/agent-loop.js +26 -4
  145. package/dist/index.d.ts +12 -7
  146. package/dist/index.js +7 -4
  147. package/dist/internal/harness.d.ts +2 -1
  148. package/dist/internal/harness.js +1 -0
  149. package/dist/primitives/credential-ref.d.ts +89 -0
  150. package/dist/primitives/credential-ref.js +125 -0
  151. package/dist/primitives/index.d.ts +2 -0
  152. package/dist/primitives/index.js +1 -0
  153. package/dist/stores/file/checkpoint-store.d.ts +0 -4
  154. package/dist/stores/file/checkpoint-store.js +0 -1
  155. package/dist/stores/file/file-history-store.js +12 -4
  156. package/dist/stores/file/mailbox-store.d.ts +2 -1
  157. package/dist/stores/file/mailbox-store.js +26 -1
  158. package/dist/tools/fs/bash-readonly-classifier.d.ts +15 -19
  159. package/dist/tools/fs/bash-readonly-classifier.js +5 -10
  160. package/dist/tools/fs/fs-bash.js +10 -3
  161. package/dist/tools/fs/pattern-kill-guard.d.ts +3 -2
  162. package/dist/tools/fs/pattern-kill-guard.js +68 -36
  163. package/dist/tools/fs/shell-read-check.d.ts +10 -0
  164. package/dist/tools/fs/shell-read-check.js +7 -1
  165. package/dist/tools/fs/shell-read-identity.d.ts +12 -9
  166. package/dist/tools/fs/shell-read-identity.js +9 -4
  167. package/dist/tools/web.js +9 -7
  168. package/package.json +1 -1
  169. package/test/export-surface.snapshot.json +79 -3
  170. package/wire-changes/core-stores-sql@7.57.0.json +29 -0
  171. package/wire-changes/core@7.57.0.json +1487 -0
  172. package/wire-changes/mods-kernel@7.57.0.json +29 -0
  173. package/wire-changes/primitives@7.57.0.json +245 -0
@@ -0,0 +1,125 @@
1
+ export const CREDENTIAL_REF_KINDS = Object.freeze(["env", "vault", "handle", "assertion"]);
2
+ export const CREDENTIAL_REF_TRUST = trustTable();
3
+ function trustTable() {
4
+ const rows = Object.create(null);
5
+ rows.env = Object.freeze({ request: "refused" });
6
+ rows.vault = Object.freeze({ request: "own_entry" });
7
+ rows.handle = Object.freeze({ request: "refused" });
8
+ rows.assertion = Object.freeze({ request: "refused" });
9
+ return Object.freeze(rows);
10
+ }
11
+ export const CREDENTIAL_REF_TEXT_MAX = 2048;
12
+ export const CREDENTIAL_REF_MAP_MAX = 64;
13
+ export const CREDENTIAL_REF_PROBLEM_REASONS = Object.freeze(["not_a_record", "unknown_kind", "missing_field", "extra_field", "bad_value"]);
14
+ const MEMBERS = {
15
+ env: ["headers", "env"],
16
+ vault: ["entry"],
17
+ handle: ["id"],
18
+ assertion: ["audience"],
19
+ };
20
+ const TEXT_MEMBER = { vault: "entry", handle: "id", assertion: "audience" };
21
+ const HEADER_NAME = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
22
+ const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
23
+ const ECHO_MAX = 64;
24
+ const UNSAFE = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\p{DI}\p{NChar}\p{Co}\ud800-\udfff]/u;
25
+ const UNSAFE_ALL = new RegExp(UNSAFE.source, "gu");
26
+ const ABSENT = Object.freeze({ found: "absent" });
27
+ const ACCESSOR = Object.freeze({ found: "accessor" });
28
+ const NOT_A_RECORD = Object.freeze({ ok: false, problem: Object.freeze({ reason: "not_a_record" }) });
29
+ function isRecord(value) {
30
+ if (typeof value !== "object" || value === null || Array.isArray(value))
31
+ return false;
32
+ const proto = Object.getPrototypeOf(value);
33
+ return proto === null || Object.getPrototypeOf(proto) === null;
34
+ }
35
+ function member(record, key) {
36
+ const descriptor = Object.getOwnPropertyDescriptor(record, key);
37
+ if (descriptor === undefined || descriptor.enumerable !== true)
38
+ return ABSENT;
39
+ return Object.hasOwn(descriptor, "value") ? { found: "value", value: descriptor.value } : ACCESSOR;
40
+ }
41
+ const echo = (text) => text.slice(0, ECHO_MAX).replace(UNSAFE_ALL, "?");
42
+ const refused = (problem) => Object.freeze({ ok: false, problem: Object.freeze(problem) });
43
+ const accepted = (ref) => Object.freeze({ ok: true, ref });
44
+ const isText = (value) => typeof value === "string" && value.length > 0 && value.length <= CREDENTIAL_REF_TEXT_MAX && !UNSAFE.test(value) && value === value.trim();
45
+ const isName = (value, form) => value.length <= CREDENTIAL_REF_TEXT_MAX && form.test(value);
46
+ function nameMap(value, keyForm, foldCase) {
47
+ try {
48
+ if (!isRecord(value))
49
+ return undefined;
50
+ const names = Object.keys(value);
51
+ if (names.length === 0 || names.length > CREDENTIAL_REF_MAP_MAX)
52
+ return undefined;
53
+ const out = Object.create(null);
54
+ const folded = new Set();
55
+ for (const name of names) {
56
+ const found = member(value, name);
57
+ if (found.found !== "value" || typeof found.value !== "string")
58
+ return undefined;
59
+ if (!isName(name, keyForm) || !isName(found.value, ENV_NAME))
60
+ return undefined;
61
+ if (foldCase) {
62
+ const key = name.toLowerCase();
63
+ if (folded.has(key))
64
+ return undefined;
65
+ folded.add(key);
66
+ }
67
+ out[name] = found.value;
68
+ }
69
+ return Object.freeze(out);
70
+ }
71
+ catch {
72
+ return undefined;
73
+ }
74
+ }
75
+ const mapOf = (found, keyForm, foldCase) => (found.found === "value" ? nameMap(found.value, keyForm, foldCase) : undefined);
76
+ function envRef(record) {
77
+ const given = { headers: member(record, "headers"), env: member(record, "env") };
78
+ if (given.headers.found === "absent" && given.env.found === "absent")
79
+ return refused({ reason: "missing_field", kind: "env", field: "headers|env" });
80
+ const headers = mapOf(given.headers, HEADER_NAME, true);
81
+ if (given.headers.found !== "absent" && headers === undefined)
82
+ return refused({ reason: "bad_value", kind: "env", field: "headers" });
83
+ const env = mapOf(given.env, ENV_NAME, false);
84
+ if (given.env.found !== "absent" && env === undefined)
85
+ return refused({ reason: "bad_value", kind: "env", field: "env" });
86
+ return accepted(Object.freeze({ kind: "env", ...(headers === undefined ? {} : { headers }), ...(env === undefined ? {} : { env }) }));
87
+ }
88
+ function read(value) {
89
+ if (!isRecord(value))
90
+ return NOT_A_RECORD;
91
+ const named = member(value, "kind");
92
+ if (named.found === "absent")
93
+ return refused({ reason: "missing_field", field: "kind" });
94
+ if (named.found !== "value" || typeof named.value !== "string")
95
+ return refused({ reason: "bad_value", field: "kind" });
96
+ const text = named.value;
97
+ const kind = CREDENTIAL_REF_KINDS.find((k) => k === text);
98
+ if (kind === undefined)
99
+ return refused({ reason: "unknown_kind", kind: echo(text) });
100
+ for (const key of Object.keys(value)) {
101
+ if (key !== "kind" && !MEMBERS[kind].includes(key))
102
+ return refused({ reason: "extra_field", kind, field: echo(key) });
103
+ }
104
+ if (kind === "env")
105
+ return envRef(value);
106
+ const field = TEXT_MEMBER[kind];
107
+ const found = member(value, field);
108
+ if (found.found === "absent")
109
+ return refused({ reason: "missing_field", kind, field });
110
+ if (found.found !== "value" || !isText(found.value))
111
+ return refused({ reason: "bad_value", kind, field });
112
+ if (kind === "vault")
113
+ return accepted(Object.freeze({ kind, entry: found.value }));
114
+ if (kind === "handle")
115
+ return accepted(Object.freeze({ kind, id: found.value }));
116
+ return accepted(Object.freeze({ kind, audience: found.value }));
117
+ }
118
+ export function readCredentialRef(value) {
119
+ try {
120
+ return read(value);
121
+ }
122
+ catch {
123
+ return NOT_A_RECORD;
124
+ }
125
+ }
@@ -0,0 +1,2 @@
1
+ /** The package entry of `@sema-agent/primitives`; core reads the same modules directly. */
2
+ export { CREDENTIAL_REF_KINDS, CREDENTIAL_REF_TRUST, CREDENTIAL_REF_TEXT_MAX, CREDENTIAL_REF_MAP_MAX, CREDENTIAL_REF_PROBLEM_REASONS, readCredentialRef, type CredentialRef, type CredentialRefKind, type CredentialRefRequestAdmission, type CredentialRefProblem, type CredentialRefProblemReason, type CredentialRefReading, } from "./credential-ref.js";
@@ -0,0 +1 @@
1
+ export { CREDENTIAL_REF_KINDS, CREDENTIAL_REF_TRUST, CREDENTIAL_REF_TEXT_MAX, CREDENTIAL_REF_MAP_MAX, CREDENTIAL_REF_PROBLEM_REASONS, readCredentialRef, } from "./credential-ref.js";
@@ -35,10 +35,6 @@ export declare class FileCheckpointStore implements CheckpointStore {
35
35
  * same JSON reading as the ledger line); the declaration is what makes the loss visible to the mint BEFORE the
36
36
  * row is filed. */
37
37
  readonly fidelity: "json";
38
- /** F-012 L2: `reopen` here is a real resolved→pending CAS journaled to the ledger — declared, not sniffed. */
39
- readonly redecision: {
40
- readonly reopen: true;
41
- };
42
38
  /** The execution-outcome record verb below is the contract's CAS (journaled), and `reopen` honours the record. */
43
39
  readonly execution: {
44
40
  readonly outcome: true;
@@ -88,7 +88,6 @@ const checkpointLedgers = new SharedLedgerTable({
88
88
  export class FileCheckpointStore {
89
89
  durability = "durable";
90
90
  fidelity = "json";
91
- redecision = { reopen: true };
92
91
  execution = { outcome: true };
93
92
  holderIsCurrent;
94
93
  fsyncEnabled;
@@ -1147,6 +1147,14 @@ export class FileFileHistoryStore {
1147
1147
  const shaped = validateFileHistoryExport(data);
1148
1148
  if (!shaped.ok)
1149
1149
  return shaped;
1150
+ for (const b of data.boundaries) {
1151
+ try {
1152
+ historyPathComponent(b.entryId);
1153
+ }
1154
+ catch (err) {
1155
+ return { ok: false, error: { code: "unsupported_format", message: `importHistory refused a boundary entry id this backend cannot hold as a record name: ${thrownText(err)} — nothing was fetched or published` } };
1156
+ }
1157
+ }
1150
1158
  const target = await this.importTargetOf(scope, data, "before");
1151
1159
  if (typeof target === "object")
1152
1160
  return target;
@@ -1164,7 +1172,7 @@ export class FileFileHistoryStore {
1164
1172
  return await this.runImport(scope, data, srcGetBlob, importJournal, identicalRepublish);
1165
1173
  }
1166
1174
  catch (err) {
1167
- return { ok: false, error: { code: "unsupported_format", message: `importHistory failed: ${thrownText(err)}`, ...caughtFault(err) } };
1175
+ return { ok: false, error: { code: "adopt_failed", message: `importHistory failed: ${thrownText(err)}`, ...caughtFault(err) } };
1168
1176
  }
1169
1177
  }
1170
1178
  async runImport(scope, data, srcGetBlob, importJournal, identicalRepublish) {
@@ -1191,12 +1199,12 @@ export class FileFileHistoryStore {
1191
1199
  bytes = await srcGetBlob(v.blobHash);
1192
1200
  }
1193
1201
  catch (err) {
1194
- return { ok: false, error: { code: "unsupported_format", message: `source blob ${v.blobHash} fetch failed: ${thrownText(err)}`, ...caughtFault(err) } };
1202
+ return { ok: false, error: { code: "adopt_failed", message: `source blob ${v.blobHash} fetch failed: ${thrownText(err)}`, ...caughtFault(err) } };
1195
1203
  }
1196
1204
  if (bytes === undefined)
1197
1205
  return { ok: false, error: { code: "adopt_failed", message: `source blob ${v.blobHash} is missing — the envelope's graph cannot be completed, so nothing is published` } };
1198
1206
  if (createHash("sha256").update(bytes).digest("hex") !== v.blobHash) {
1199
- return { ok: false, error: { code: "unsupported_format", message: `source blob ${v.blobHash} fails content-address integrity — refused, nothing published` } };
1207
+ return { ok: false, error: { code: "adopt_failed", message: `source blob ${v.blobHash} fails content-address integrity — refused, nothing published` } };
1200
1208
  }
1201
1209
  this.putBlob(v.blobHash, bytes);
1202
1210
  }
@@ -1250,7 +1258,7 @@ export class FileFileHistoryStore {
1250
1258
  return { ok: true };
1251
1259
  }
1252
1260
  catch (err) {
1253
- return { ok: false, error: { code: "unsupported_format", message: `importHistory failed: ${thrownText(err)}`, ...caughtFault(err) } };
1261
+ return { ok: false, error: { code: "adopt_failed", message: `importHistory failed: ${thrownText(err)}`, ...caughtFault(err) } };
1254
1262
  }
1255
1263
  finally {
1256
1264
  safeRm(importJournal);
@@ -1,4 +1,4 @@
1
- import { type MailboxAckResult, type MailboxAppendMessage, type MailboxLease, type MailboxStore } from "../../core/mailbox-store.js";
1
+ import { type MailboxAckResult, type MailboxAppendMessage, type MailboxLease, type MailboxDecisionOutcome, type MailboxDecision, type MailboxStore } from "../../core/mailbox-store.js";
2
2
  import { type CorruptReadInfo } from "./corrupt-read-sink.js";
3
3
  /** The waiter's refusal when a box lock stays held by a LIVE process past `lockWaitMs`. Carried as
4
4
  * `.code` on a plain Error (consumers branch on the string); the message names the lock and the holder. */
@@ -113,6 +113,7 @@ export declare class FileMailboxStore implements MailboxStore {
113
113
  append(scope: string, handle: string, msg: MailboxAppendMessage): Promise<number>;
114
114
  claimLease(scope: string, handle: string, owner: string, ttlMs: number, now?: number): Promise<MailboxLease | null>;
115
115
  ack(scope: string, handle: string, owner: string, upToSeq: number): Promise<MailboxAckResult>;
116
+ moveDecision(scope: string, handle: string, seq: number, expect: MailboxDecision | undefined, next: MailboxDecision): Promise<MailboxDecisionOutcome>;
116
117
  releaseLease(scope: string, handle: string, owner: string): Promise<void>;
117
118
  peekCount(scope: string, handle: string): Promise<number>;
118
119
  drop(scope: string, handle: string): Promise<void>;
@@ -2,7 +2,7 @@ import { join, resolve, sep } from "node:path";
2
2
  import { createHash, randomBytes } from "node:crypto";
3
3
  import { assertRetentionPolicy } from "../../core/retention-policy.js";
4
4
  import { existsSync, linkSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
5
- import { detachMailboxMessage, newestSentAt, readMailboxPeerMeta, } from "../../core/mailbox-store.js";
5
+ import { assertMailboxDecisionMove, detachMailboxMessage, newestSentAt, readStoredMailboxDecision, readMailboxPeerMeta, } from "../../core/mailbox-store.js";
6
6
  import { AppendLog, atomicWriteFile, canonicalStoreKey, ensureDir, readJsonlRecords, sanitizeScope, sanitizePathComponent } from "./fs-atomic.js";
7
7
  import { guardCorruptSink } from "./corrupt-read-sink.js";
8
8
  import { physicalPathOf } from "../../core/physical-path.js";
@@ -478,6 +478,19 @@ export class FileMailboxStore {
478
478
  return { acked: true };
479
479
  });
480
480
  }
481
+ async moveDecision(scope, handle, seq, expect, next) {
482
+ assertMailboxDecisionMove(seq, expect, next);
483
+ return this.withBox(this.lockKey(scope, handle), this.boxPath(scope, handle), () => {
484
+ const b = this.load(scope, handle);
485
+ const m = b.messages.find((x) => x.seq === seq);
486
+ if (m === undefined)
487
+ return { ok: false, current: "gone" };
488
+ if (m.decision !== expect)
489
+ return { ok: false, current: m.decision ?? "unset" };
490
+ this.commit(b, this.boxPath(scope, handle), { t: "decision", seq, state: next });
491
+ return { ok: true };
492
+ });
493
+ }
481
494
  async releaseLease(scope, handle, owner) {
482
495
  return this.withBox(this.lockKey(scope, handle), this.boxPath(scope, handle), () => {
483
496
  const b = this.load(scope, handle);
@@ -592,12 +605,19 @@ const MAILBOX_EVENT_JUDGE = {
592
605
  return typeof ev.owner === "string" && num(ev.maxSeq) && num(ev.expiresAt);
593
606
  if (ev.t === "ack")
594
607
  return num(ev.upToSeq);
608
+ if (ev.t === "decision")
609
+ return num(r.seq) && typeof r.state === "string";
595
610
  return ev.t === "release" && typeof ev.owner === "string";
596
611
  },
597
612
  what: "a mailbox record is not one of this box's events — an unknown tag, or a known tag whose payload is missing",
598
613
  };
599
614
  function applyEvent(b, ev) {
600
615
  if (ev.t === "append") {
616
+ const decision = readStoredMailboxDecision(ev.m.decision);
617
+ if (decision === undefined)
618
+ delete ev.m.decision;
619
+ else
620
+ ev.m.decision = decision;
601
621
  b.messages.push(ev.m);
602
622
  if (ev.m.seq >= b.nextSeq)
603
623
  b.nextSeq = ev.m.seq + 1;
@@ -613,6 +633,11 @@ function applyEvent(b, ev) {
613
633
  if (b.lease !== undefined && b.lease.maxSeq <= ev.upToSeq)
614
634
  b.lease = undefined;
615
635
  }
636
+ else if (ev.t === "decision") {
637
+ const m = b.messages.find((x) => x.seq === ev.seq);
638
+ if (m !== undefined)
639
+ m.decision = readStoredMailboxDecision(ev.state) ?? "held";
640
+ }
616
641
  else if (b.lease?.owner === ev.owner) {
617
642
  b.lease = undefined;
618
643
  }
@@ -2,15 +2,15 @@ import { type LexedCommand, type LexedSegment, type LexedWord, type ShellLex, ty
2
2
  import { type ProgramRow } from "./program-read-model.js";
3
3
  /**
4
4
  * Bare command names `bash_readonly` permits out of the box, ALSO the default reversible set for the
5
- * The {@link import("./fs-bash.js").bashReversibilityProbe} classifier. **Coarse first filter, NOT a security
6
- * boundary**: {@link coarseReadonlyCheck} matches only the command NAME, so a listed command with a
7
- * writing flag (e.g. `find -delete`, `sort -o`, `tee`) would still write. Such commands are therefore
8
- * kept OFF this default — the list is curated to commands with NO write/mutation mode under ANY args, so
9
- * the `effect:"read"` declaration (relied on by wake/resume reconcile + the verifier read-only boundary)
10
- * AND the classifier's "reversible" promise both stay truthful. The deployment's tool-policy gate is the
11
- * authoritative control (council blocker #1); anything that can mutate state or run arbitrary
12
- * code belongs on the full `bash` (effect:write, gated), not here. A deployment may widen this list,
13
- * accepting that responsibility.
5
+ * {@link import("./fs-bash.js").bashReversibilityProbe} classifier. On the read-only face and in the transcript gate the
6
+ * list only NARROWS the read-only reader ({@link import("../../core/read-only-shell.js").readOnlyShellVerdict}'s
7
+ * `readers`): a listed program runs there only in a call the reader reads as read-only, so a deployment that widens the
8
+ * list with `sort` still has `sort -o out f` refused. The reversibility classifier reads the list as its allowlist: a
9
+ * listed program with a program-model row has its writing options refused by the row; one without a row is admitted
10
+ * by its name. The default is curated to commands with NO write/mutation mode under ANY args, so the `effect:"read"`
11
+ * declaration (relied on by wake/resume reconcile + the verifier read-only boundary) AND the classifier's
12
+ * "reversible" promise both stay truthful. The deployment's tool-policy gate is the authoritative control;
13
+ * anything that can mutate state or run arbitrary code belongs on the full `bash` (effect:write, gated), not here.
14
14
  *
15
15
  * Deliberately ABSENT: `date` (`-s`/`--set` → CLOCK_SETTIME), `hostname` (`<name>` → kernel
16
16
  * hostname), and `file` (`-C -m` → compiles/writes a magic file) were REMOVED — each is read-only by NAME
@@ -112,7 +112,7 @@ export interface LeadingCommandNameOptions {
112
112
  pathPrefixedNameIsText?: boolean;
113
113
  }
114
114
  /**
115
- * The SINGLE fail-closed simple-command parser shared by `bash_readonly` ({@link coarseReadonlyCheck}), the
115
+ * The SINGLE fail-closed simple-command parser shared by `bash_readonly`'s single-command admission, the
116
116
  * `bash` reversibility classifier ({@link import("./fs-bash.js").bashReversibilityProbe}), and the coarse command-name policy
117
117
  * ({@link import("../../core/tool-policy.js").createCoarseCommandNamePolicy}). It extracts the leading
118
118
  * `argv[0]` command NAME of a SINGLE simple command, rejecting anything that could chain past or escape an
@@ -131,13 +131,9 @@ export declare function parseLeadingCommandName(command: string, options?: Leadi
131
131
  } | {
132
132
  reject: string;
133
133
  };
134
- /** Coarse pre-execution check for `bash_readonly`: the command must be a single bare allowlisted command
135
- * with no shell operators. It validates the NAME only (+ blocks operators/path-prefix/env-assignment); it
136
- * does NOT inspect arguments for write flags — keeping the allowlist free of write-capable commands and
137
- * the policy gate are what make `effect:"read"` truthful. Returns a reason when rejected, else undefined.
138
- * This `effect:"read"` DECLARATION face deliberately stays strict-single-command; only the
139
- * classify face ({@link classifyCompoundReadonly} via {@link import("./fs-bash.js").bashReversibilityProbe}) segments compounds. */
140
- export declare function coarseReadonlyCheck(command: string, allow: ReadonlySet<string>, options?: LeadingCommandNameOptions): string | undefined;
134
+ /** The one sentence for a program a face's list of programs does not name — the compound classifier's list check and
135
+ * the read-only reader's `readers` narrowing both mint it here. */
136
+ export declare function unlistedReaderSentence(program: string): string;
141
137
  /**
142
138
  * Does this text carry a bare redirection operator — the ONE metacharacter family
143
139
  * {@link splitShellCompoundSegments} can be asked to tolerate at its whole-string gate
@@ -226,7 +222,7 @@ export declare function splitShellCompoundSegments(source: string, options?: {
226
222
  /**
227
223
  * Compound read-only classification for the `bash` full-shell face (the shape tier).
228
224
  * Splits a command on the connector set `;` `&&` `||` `|` and passes every segment through
229
- * {@link coarseReadonlyCheck} (the single {@link parseLeadingCommandName} parser + bare-name allowlist).
225
+ * the single {@link parseLeadingCommandName} parser and the bare-name allowlist.
230
226
  * All segments green ⇒ read-only. Returns a reason when rejected, else undefined.
231
227
  *
232
228
  * G2 — the split CARRIES QUOTE STATE (the one lexer): a connector character inside a quoted region is
@@ -548,7 +544,7 @@ export declare const POLL_LOOP_MAX_SLEEP_SECONDS = 600;
548
544
  * -f`, unbounded devices all reject), and `sleep` must be a literal ≤
549
545
  * {@link POLL_LOOP_MAX_SLEEP_SECONDS}. Bounded beats × terminating body ⇒ the loop halts.
550
546
  * G2 NO WRITE OR EGRESS CHANNEL. Every non-`sleep` body segment must pass the SAME
551
- * {@link coarseReadonlyCheck} parser + allowlist as the plain face (one parser, no drift);
547
+ * {@link parseLeadingCommandName} parser + allowlist the compound classifier reads (one parser, no drift);
552
548
  * `sleep` touches neither filesystem nor network. No redirection (`>` `<`), no `$`/backtick
553
549
  * substitution, no subshell exists anywhere: the body bans them wholesale
554
550
  * ({@link POLL_BODY_HARD_REJECT}), and the head's single `$(seq <int> <int>)` exemption is an
@@ -71,13 +71,8 @@ export function parseLeadingCommandName(command, options) {
71
71
  return { reject: "the command name must be a bare token (no quotes, braces, globs, or ~)" };
72
72
  return { name: first };
73
73
  }
74
- export function coarseReadonlyCheck(command, allow, options) {
75
- const parsed = parseLeadingCommandName(command, options);
76
- if ("reject" in parsed)
77
- return parsed.reject;
78
- if (!allow.has(parsed.name))
79
- return `command "${parsed.name}" is not in the read-only allowlist`;
80
- return undefined;
74
+ export function unlistedReaderSentence(program) {
75
+ return `command "${program}" is not in the read-only allowlist`;
81
76
  }
82
77
  const SHELL_SEGMENT_QUOTE_BLIND_REJECT = /[<>()`\n\r\\]/;
83
78
  const SHELL_EXPANSION_CHARS = /[$]/;
@@ -761,7 +756,7 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
761
756
  return { reason: "`coproc` backgrounds a command — a backgrounded process outlives the command" };
762
757
  if (isBareControlWord(words, from, BLOCK_WORD_NAMES)) {
763
758
  unlistedSegments.add(si);
764
- nameRefusal ??= `command "${written}" is not in the read-only allowlist`;
759
+ nameRefusal ??= unlistedReaderSentence(written);
765
760
  continue;
766
761
  }
767
762
  const program = pos.program;
@@ -783,10 +778,10 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
783
778
  if (pathPrefixed || from > 0)
784
779
  reheaded.set(si, { from, head: name });
785
780
  if (from > 0)
786
- nameRefusal ??= `command "${folded[0]}" is not in the read-only allowlist`;
781
+ nameRefusal ??= unlistedReaderSentence(folded[0]);
787
782
  continue;
788
783
  }
789
- nameRefusal ??= `command "${parsed.name}" is not in the read-only allowlist`;
784
+ nameRefusal ??= unlistedReaderSentence(parsed.name);
790
785
  if (runsShellText(name))
791
786
  return { reason: unjudgedProgramTextRefusal(parsed.name, "runs its argument as shell text") };
792
787
  if (pos.launchers.length > 0) {
@@ -16,7 +16,8 @@ import { boundaryIsTheSandbox, hasDestroy, isIsolated } from "../../core/remote-
16
16
  import { ghRateLimitHint } from "./gh-rate-limit.js";
17
17
  import { backgroundLifetimeNote } from "./fs-bash-background-text.js";
18
18
  import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashTimeoutArgRefusal, bashTimeoutParamDescription, envErrorDetail, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, shellRecoveryHint, CWD_SENTINEL, stripCwdSentinel, } from "./fs-shared.js";
19
- import { bashClassifyDefaultAllow, BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyBoundedReadonlyPollLoopDetailed, classifyCompoundReadonlyDetailed } from "./bash-readonly-classifier.js";
19
+ import { bashClassifyDefaultAllow, BASH_READONLY_DEFAULT_ALLOW, classifyBoundedReadonlyPollLoopDetailed, classifyCompoundReadonlyDetailed, parseLeadingCommandName } from "./bash-readonly-classifier.js";
20
+ import { readOnlyShellVerdict } from "../../core/read-only-shell.js";
20
21
  import { mountRuleFace, toolFace } from "../../core/tool-catalog-entries.js";
21
22
  import { argvOf, compoundValueHidesRun, expansionHidesRun, expansionsInHeredocBody, lexShell } from "../../shell/lex.js";
22
23
  import { arithmeticAssignedNames } from "./shell-read-bindings.js";
@@ -26,6 +27,12 @@ function shellCommandOf(args) {
26
27
  const command = args?.command;
27
28
  return typeof command === "string" ? command : undefined;
28
29
  }
30
+ const CONCURRENT_READERS = new Set(BASH_READONLY_DEFAULT_ALLOW);
31
+ function readOnlyFaceRefusal(command, allow) {
32
+ const parsed = parseLeadingCommandName(command);
33
+ const verdict = "reject" in parsed ? { readOnly: false, reason: parsed.reject } : readOnlyShellVerdict(command, { readers: allow });
34
+ return verdict.readOnly ? undefined : verdict.reason;
35
+ }
29
36
  function isBackgroundShellCall(args) {
30
37
  return args?.run_in_background === true;
31
38
  }
@@ -578,7 +585,7 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
578
585
  const command = shellCommandOf(args);
579
586
  if (command === undefined || isBackgroundShellCall(args))
580
587
  return false;
581
- return coarseReadonlyCheck(command, new Set(BASH_READONLY_DEFAULT_ALLOW)) === undefined;
588
+ return readOnlyShellVerdict(command, { readers: CONCURRENT_READERS }).readOnly;
582
589
  },
583
590
  execute: async (args, ctx) => {
584
591
  const { command, timeout, run_in_background, description } = args;
@@ -797,7 +804,7 @@ export function createBashReadonlyTool(env, rootCanonical, allow, opts) {
797
804
  const timeoutRefusal = bashTimeoutArgRefusal(timeout, timeoutCaps);
798
805
  if (timeoutRefusal !== undefined)
799
806
  return errorResult(timeoutRefusal, { type: "bash_invalid_timeout", code: "bash_invalid_timeout" });
800
- const reason = coarseReadonlyCheck(command, allow);
807
+ const reason = readOnlyFaceRefusal(command, allow);
801
808
  if (reason)
802
809
  return errorResult(`Error (Bash): ${reason}`);
803
810
  const refused = await opts?.shellReadAdmission?.("Bash", ctx.toolCallId, command, ctx.signal);
@@ -12,8 +12,9 @@ export interface PatternKillReading {
12
12
  export declare function patternKillReadings(command: string, cwd: string): PatternKillReading[];
13
13
  /**
14
14
  * The refusal for a command whose pattern-kill would select this agent's own process or one above it — or undefined (run it).
15
- * `cwd`: where the command stands, for the plan's read of its words (nothing is read from disk). Remote environments and
16
- * Windows are not asked: their process table is not this one.
15
+ * `cwd`: where the command stands, for the plan's read of its words (nothing is read from disk). Where the question is put is
16
+ * the env's own declaration ({@link processTableOf}): this process's table is asked, another table is not, and an env that does
17
+ * not say is refused. Windows is not asked: this process's table is not read there.
17
18
  */
18
19
  export declare function patternKillRefusal(command: string, env: ExecutionEnv, cwd: string, signal?: AbortSignal): Promise<{
19
20
  content: string;