@sema-agent/core 7.52.0 → 7.53.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 (191) hide show
  1. package/CHANGELOG.md +194 -0
  2. package/README.md +3 -1
  3. package/dist/agents/cross-session-envelope.d.ts +8 -0
  4. package/dist/agents/cross-session-envelope.js +6 -0
  5. package/dist/agents/cross-session-judge.d.ts +21 -1
  6. package/dist/agents/cross-session-judge.js +8 -0
  7. package/dist/agents/cross-session-ref.d.ts +5 -0
  8. package/dist/agents/cross-session-ref.js +1 -0
  9. package/dist/agents/org-delivery-gate.d.ts +122 -0
  10. package/dist/agents/org-delivery-gate.js +69 -0
  11. package/dist/agents/peer-held-queue.d.ts +9 -4
  12. package/dist/agents/peer-held-queue.js +6 -5
  13. package/dist/agents/peer-session-drain.d.ts +35 -5
  14. package/dist/agents/peer-session-drain.js +271 -180
  15. package/dist/agents/read-inbox-tool.d.ts +7 -0
  16. package/dist/agents/read-inbox-tool.js +5 -1
  17. package/dist/agents/subagent.d.ts +3 -0
  18. package/dist/agents/subagent.js +99 -72
  19. package/dist/agents/tool-filter.d.ts +18 -0
  20. package/dist/agents/tool-filter.js +6 -0
  21. package/dist/brain/anthropic.d.ts +6 -1
  22. package/dist/brain/anthropic.js +4 -4
  23. package/dist/brain/model-presets.js +2 -1
  24. package/dist/brain/route-adjudicator.d.ts +6 -0
  25. package/dist/brain/routing.d.ts +24 -1
  26. package/dist/brain/routing.js +54 -19
  27. package/dist/brain/timeout.js +5 -4
  28. package/dist/core/a2a.d.ts +5 -2
  29. package/dist/core/a2a.js +30 -13
  30. package/dist/core/allow-entry.d.ts +2 -1
  31. package/dist/core/allow-entry.js +2 -2
  32. package/dist/core/auto-compaction.d.ts +57 -0
  33. package/dist/core/auto-compaction.js +28 -2
  34. package/dist/core/checkpoint-store.d.ts +29 -20
  35. package/dist/core/checkpoint-store.js +3 -3
  36. package/dist/core/compliance.js +3 -1
  37. package/dist/core/effective-path-target.d.ts +25 -6
  38. package/dist/core/effective-path-target.js +24 -2
  39. package/dist/core/engine-notice.d.ts +13 -2
  40. package/dist/core/gate-fold.js +3 -2
  41. package/dist/core/gate-lanes.js +11 -13
  42. package/dist/core/gate-outcome.d.ts +11 -6
  43. package/dist/core/governance-codes.d.ts +3 -1
  44. package/dist/core/governance-codes.js +3 -0
  45. package/dist/core/hands-band-screen.d.ts +7 -0
  46. package/dist/core/hands-band-screen.js +32 -0
  47. package/dist/core/hooks.d.ts +7 -5
  48. package/dist/core/hooks.js +5 -3
  49. package/dist/core/locked-config.d.ts +84 -32
  50. package/dist/core/locked-config.js +128 -42
  51. package/dist/core/mcp-probe.js +1 -1
  52. package/dist/core/mcp-server-spec.d.ts +88 -10
  53. package/dist/core/mcp-server-spec.js +183 -33
  54. package/dist/core/mcp.d.ts +21 -14
  55. package/dist/core/mcp.js +55 -51
  56. package/dist/core/memory-engine/engine.d.ts +14 -19
  57. package/dist/core/memory-engine/engine.js +7 -5
  58. package/dist/core/mode-policy.d.ts +3 -18
  59. package/dist/core/mode-policy.js +2 -16
  60. package/dist/core/mods/kernel/dispatch.js +13 -15
  61. package/dist/core/mods/kernel/events.d.ts +9 -6
  62. package/dist/core/mods/kernel/events.js +2 -2
  63. package/dist/core/mods/kernel/receive.d.ts +39 -10
  64. package/dist/core/mods/kernel/receive.js +119 -54
  65. package/dist/core/mods/kernel/site-rules.d.ts +80 -15
  66. package/dist/core/mods/kernel/site-rules.js +232 -40
  67. package/dist/core/mods/stations.d.ts +0 -6
  68. package/dist/core/mods/stations.js +0 -7
  69. package/dist/core/mounted-mcp.d.ts +57 -0
  70. package/dist/core/mounted-mcp.js +71 -2
  71. package/dist/core/own-property-receipt.d.ts +13 -0
  72. package/dist/core/own-property-receipt.js +3 -1
  73. package/dist/core/parent-spec-seat.d.ts +2 -2
  74. package/dist/core/permission-rule-model.d.ts +58 -9
  75. package/dist/core/permission-rule-model.js +81 -7
  76. package/dist/core/permission-rule-org.d.ts +163 -33
  77. package/dist/core/permission-rule-org.js +205 -71
  78. package/dist/core/permission-rules.d.ts +7 -1
  79. package/dist/core/permission-rules.js +37 -8
  80. package/dist/core/retention.js +7 -2
  81. package/dist/core/retired-keys.d.ts +8 -2
  82. package/dist/core/retired-keys.js +12 -1
  83. package/dist/core/roles.js +60 -10
  84. package/dist/core/rule-lane-reads.js +1 -1
  85. package/dist/core/runner/contracts.d.ts +1 -0
  86. package/dist/core/runner/gate-exit.js +4 -3
  87. package/dist/core/runner/mcp-redial.d.ts +52 -10
  88. package/dist/core/runner/mcp-redial.js +38 -9
  89. package/dist/core/runner/permission-rule-lanes.d.ts +5 -2
  90. package/dist/core/runner/permission-rule-lanes.js +22 -10
  91. package/dist/core/runner/person-path-rows.d.ts +3 -1
  92. package/dist/core/runner/person-path-rows.js +2 -2
  93. package/dist/core/runner/prepare-config-doors.d.ts +48 -2
  94. package/dist/core/runner/prepare-config-doors.js +161 -145
  95. package/dist/core/runner/prepare-defer-classify.js +4 -7
  96. package/dist/core/runner/prepare-delegation-surface.d.ts +2 -2
  97. package/dist/core/runner/prepare-delegation-surface.js +7 -5
  98. package/dist/core/runner/prepare-hands-readface.d.ts +1 -1
  99. package/dist/core/runner/prepare-hands-readface.js +2 -1
  100. package/dist/core/runner/prepare-inherited-gate.js +2 -2
  101. package/dist/core/runner/prepare-listings.d.ts +1 -1
  102. package/dist/core/runner/prepare-listings.js +4 -2
  103. package/dist/core/runner/prepare-memory-engine-session.d.ts +0 -6
  104. package/dist/core/runner/prepare-memory-engine-session.js +19 -6
  105. package/dist/core/runner/prepare-memory.d.ts +3 -2
  106. package/dist/core/runner/prepare-memory.js +6 -2
  107. package/dist/core/runner/prepare-offload-wrappers.d.ts +4 -4
  108. package/dist/core/runner/prepare-offload-wrappers.js +2 -1
  109. package/dist/core/runner/prepare-policy-chain.d.ts +1 -1
  110. package/dist/core/runner/prepare-policy-chain.js +12 -12
  111. package/dist/core/runner/prepare-project-context.d.ts +1 -1
  112. package/dist/core/runner/prepare-project-context.js +6 -4
  113. package/dist/core/runner/prepare-prompt-assembly.d.ts +1 -1
  114. package/dist/core/runner/prepare-prompt-assembly.js +20 -16
  115. package/dist/core/runner/prepare-prompt-inputs.d.ts +12 -6
  116. package/dist/core/runner/prepare-prompt-inputs.js +6 -3
  117. package/dist/core/runner/prepare-protocol-tools.d.ts +7 -5
  118. package/dist/core/runner/prepare-protocol-tools.js +19 -12
  119. package/dist/core/runner/prepare-question-face.d.ts +4 -2
  120. package/dist/core/runner/prepare-question-face.js +4 -3
  121. package/dist/core/runner/prepare-task.js +10 -10
  122. package/dist/core/runner/prepare-wiring-manifest.d.ts +8 -3
  123. package/dist/core/runner/prepare-wiring-manifest.js +8 -5
  124. package/dist/core/runner/resume-admission.d.ts +4 -1
  125. package/dist/core/runner/resume-admission.js +7 -5
  126. package/dist/core/runner/resume-internals-and-config.d.ts +3 -3
  127. package/dist/core/runner/resume-internals-and-config.js +3 -8
  128. package/dist/core/runner/retired-runner-deps.d.ts +5 -6
  129. package/dist/core/runner/retired-runner-deps.js +1 -4
  130. package/dist/core/runner/run-leg.js +42 -12
  131. package/dist/core/runner/run-recovery-lanes.js +5 -3
  132. package/dist/core/runner/run-terminal-adoption.js +6 -5
  133. package/dist/core/runner/run-turn-boundary.js +9 -3
  134. package/dist/core/runner/runtask.js +1 -1
  135. package/dist/core/runner/skills-seat.d.ts +15 -0
  136. package/dist/core/runner/skills-seat.js +57 -0
  137. package/dist/core/runner/sub-agent-deny-lists.d.ts +13 -11
  138. package/dist/core/runner/sub-agent-deny-lists.js +7 -5
  139. package/dist/core/runner/synthetic-tools.d.ts +5 -2
  140. package/dist/core/runner/synthetic-tools.js +2 -12
  141. package/dist/core/runner-deps.d.ts +29 -14
  142. package/dist/core/session-policy-store.d.ts +19 -0
  143. package/dist/core/session-policy-store.js +22 -2
  144. package/dist/core/store-contracts/mailbox-store-contract.js +13 -0
  145. package/dist/core/task-event.d.ts +38 -2
  146. package/dist/core/task-notification.js +2 -2
  147. package/dist/core/task-spec.d.ts +1 -1
  148. package/dist/core/tool-policy.d.ts +3 -0
  149. package/dist/core/tool-policy.js +10 -4
  150. package/dist/core/types.d.ts +2 -2
  151. package/dist/core/types.js +1 -1
  152. package/dist/core/untrusted-text.d.ts +16 -0
  153. package/dist/core/untrusted-text.js +7 -2
  154. package/dist/core/user-attachment.d.ts +13 -2
  155. package/dist/core/user-attachment.js +47 -41
  156. package/dist/core/wiring-manifest.d.ts +4 -2
  157. package/dist/engine/harness/agent-harness.js +2 -1
  158. package/dist/engine/harness/types.d.ts +3 -0
  159. package/dist/engine/loop/agent-loop.d.ts +12 -1
  160. package/dist/engine/loop/agent-loop.js +17 -5
  161. package/dist/engine/loop/types.d.ts +7 -1
  162. package/dist/index.d.ts +15 -9
  163. package/dist/index.js +12 -6
  164. package/dist/internal/harness.d.ts +1 -0
  165. package/dist/internal/harness.js +1 -0
  166. package/dist/orchestration/goal.d.ts +24 -1
  167. package/dist/orchestration/goal.js +3 -2
  168. package/dist/prompt-assembly/artifact-store.d.ts +44 -9
  169. package/dist/prompt-assembly/artifact-store.js +38 -12
  170. package/dist/prompt-assembly/artifact.d.ts +12 -11
  171. package/dist/prompt-assembly/artifact.js +52 -21
  172. package/dist/prompt-assembly/assemble.d.ts +2 -0
  173. package/dist/prompt-assembly/assemble.js +2 -1
  174. package/dist/prompt-assembly/catalog/index.js +2 -1
  175. package/dist/prompt-assembly/catalog/memory.d.ts +13 -0
  176. package/dist/prompt-assembly/catalog/memory.js +6 -0
  177. package/dist/prompt-assembly/catalog/schema.d.ts +16 -2
  178. package/dist/prompt-assembly/composer.d.ts +2 -0
  179. package/dist/prompt-assembly/composer.js +1 -1
  180. package/dist/prompt-assembly/overlay.d.ts +21 -11
  181. package/dist/prompt-assembly/overlay.js +55 -17
  182. package/dist/prompt-assembly/select.d.ts +21 -6
  183. package/dist/prompt-assembly/select.js +39 -2
  184. package/dist/prompts/default.d.ts +0 -11
  185. package/dist/prompts/default.js +0 -3
  186. package/dist/stores/file/permission-rule-store.d.ts +1 -1
  187. package/dist/stores/file/permission-rule-store.js +2 -1
  188. package/dist/tools/artifact/artifact-tool.js +4 -3
  189. package/package.json +1 -1
  190. package/test/export-surface.snapshot.json +65 -5
  191. package/wire-changes/core@7.53.0.json +1946 -0
@@ -1,5 +1,11 @@
1
- import { escapeForDisclosure, parseRuleText, programRunReachOf, ruleReachesProgramRun } from "./permission-rule-model.js";
1
+ import { COMMAND_RULE_TOOL, PATH_RULE_FACES, escapeForDisclosure, parseRuleText, programRunReachOf, ruleReachesProgramRun, ruleToolGrammarOf } from "./permission-rule-model.js";
2
+ import { isNameableToolName, ruleSpellingOf } from "./permission-rule-syntax.js";
3
+ import { isShellFaceToolName, pathTargetOf } from "./tool-registry.js";
2
4
  import { writerOf } from "./permission-rule-store.js";
5
+ import { canonicalize } from "./canonical-json.js";
6
+ import { types } from "node:util";
7
+ import { describeFault, receive } from "./mods/kernel/receive.js";
8
+ import { DEFAULT_CAPS } from "./permission-rules.js";
3
9
  export const ORG_UNAVAILABLE_DECISION_REASON = "org_unavailable";
4
10
  export const ORG_ADJUDICATION_TIMEOUT_MS = 15_000;
5
11
  export function settleOrgVerdictWithin(p, fallback, opts) {
@@ -48,7 +54,7 @@ export function createOrgRulePartition(cfg) {
48
54
  let memoryState;
49
55
  const readState = async () => {
50
56
  const state = cfg.persistence !== undefined ? await cfg.persistence.readOrgState() : memoryState;
51
- return state === undefined ? undefined : structuredClone(state);
57
+ return state === undefined ? undefined : orgStateCopyOf(state);
52
58
  };
53
59
  const writeState = async (state) => {
54
60
  if (cfg.persistence !== undefined)
@@ -75,7 +81,7 @@ export function createOrgRulePartition(cfg) {
75
81
  throw new Error(`the org snapshot source did not answer within ${ORG_ADJUDICATION_TIMEOUT_MS}ms`);
76
82
  if ("e" in settled)
77
83
  throw settled.e;
78
- fetched = structuredClone(settled.v);
84
+ fetched = orgSnapshotCopyOf(settled.v);
79
85
  }
80
86
  catch (err) {
81
87
  fetchFailed = true;
@@ -152,6 +158,9 @@ export function createOrgRulePartition(cfg) {
152
158
  if (!freshlyInstalled && !fetchFailed && fetched === undefined) {
153
159
  disclosures.push("org snapshot source returned no snapshot — continuing on the last-known-good");
154
160
  }
161
+ const degraded = describeUnreadableOrgRows(readListedOrgRules(installed.snapshot.rules).unreadable, installed.snapshot.revision);
162
+ if (degraded !== undefined)
163
+ disclosures.push(degraded);
155
164
  return {
156
165
  source: "org",
157
166
  status: freshlyInstalled ? "fresh" : "last-known-good",
@@ -188,34 +197,187 @@ function persistedMarkIsCoherent(state) {
188
197
  Number.isFinite(state.snapshot.revision) &&
189
198
  state.revisionHighWater >= state.snapshot.revision);
190
199
  }
191
- function sameOrgPolicyContent(a, b) {
200
+ export function sameOrgPolicyContent(a, b) {
192
201
  if (a.rules.length !== b.rules.length)
193
202
  return false;
194
- return a.rules.every((r, i) => b.rules[i]?.rule === r.rule && b.rules[i]?.behavior === r.behavior);
203
+ const policy = (r) => canonicalize(orgRowPolicyOf(r));
204
+ return a.rules.every((r, i) => policy(b.rules[i]) === policy(r));
195
205
  }
196
- const DISCLOSED_UNENFORCEABLE_RULES = 20;
197
- export function unenforceableOrgRules(rules) {
198
- const out = [];
199
- for (const r of rules) {
200
- if (typeof r?.rule !== "string" || r.rule === "")
201
- continue;
202
- const parsed = parseRuleText(r.rule, r.behavior);
203
- if ("reject" in parsed) {
204
- out.push({ rule: r.rule, code: parsed.reject.code });
205
- continue;
206
- }
207
- if (parsed.rule.tool !== "Bash")
208
- out.push({ rule: r.rule, code: "unsupported.tool" });
206
+ const DISCLOSED_UNREADABLE_ROWS = 20;
207
+ function reachOfRowHead(text, behavior) {
208
+ const head = typeof text === "string" ? ruleSpellingOf(text)?.toolName : undefined;
209
+ if (head === undefined || !isNameableToolName(head))
210
+ return "every";
211
+ if (head === COMMAND_RULE_TOOL || isShellFaceToolName(head))
212
+ return { words: [COMMAND_RULE_TOOL], faces: [] };
213
+ if (ruleToolGrammarOf(head) === undefined)
214
+ return "every";
215
+ const column = Object.hasOwn(PATH_RULE_FACES, head) ? PATH_RULE_FACES[head]?.[behavior] : undefined;
216
+ return { words: [head], faces: column ?? [] };
217
+ }
218
+ function rowReaches(reach, words, face) {
219
+ if (reach === "every")
220
+ return true;
221
+ return reach.words.some((w) => words.includes(w)) || (face !== undefined && reach.faces.includes(face));
222
+ }
223
+ function listRefusalOf(rules) {
224
+ if (typeof rules === "object" && rules !== null) {
225
+ if (types.isProxy(rules))
226
+ return "rules is a Proxy, not an array";
227
+ const held = HELD_LIST_REFUSALS.get(rules);
228
+ if (held !== undefined)
229
+ return held;
209
230
  }
210
- return out;
231
+ if (!Array.isArray(rules))
232
+ return "rules is not an array";
233
+ const cap = DEFAULT_CAPS.maxRules;
234
+ return rules.length > cap ? `rules list has ${rules.length} entries; the cap is ${cap} — refused whole (not truncated)` : undefined;
235
+ }
236
+ export const ORG_ROW_POLICY_MEMBERS = Object.freeze(["rule", "behavior"]);
237
+ const ORG_SNAPSHOT_ENVELOPE_MEMBERS = Object.freeze(["revision", "fetchedAtMs", "signature"]);
238
+ const ORG_STATE_MEMBERS = Object.freeze(["revisionHighWater", "installedAtMs"]);
239
+ function scalarMembersOf(value, members) {
240
+ const held = {};
241
+ if (typeof value !== "object" || value === null)
242
+ return held;
243
+ for (const k of members) {
244
+ const v = Object.hasOwn(value, k) ? value[k] : undefined;
245
+ if (typeof v === "string" || typeof v === "boolean" || v === null || (typeof v === "number" && Number.isFinite(v)))
246
+ held[k] = v;
247
+ }
248
+ return held;
249
+ }
250
+ function orgRowPolicyOf(row) {
251
+ return scalarMembersOf(row, ORG_ROW_POLICY_MEMBERS);
252
+ }
253
+ const ORG_RULES_DEPTH = 2;
254
+ const ORG_RULES_RECEIPT = Object.freeze({ accessors: "call-once", depth: ORG_RULES_DEPTH, others: "copy", contain: true, isProxy: (v) => types.isProxy(v) });
255
+ export function orgRuleListOf(rules) {
256
+ const refused = listRefusalOf(rules);
257
+ if (refused !== undefined)
258
+ return { refused };
259
+ const received = receive(rules, ORG_RULES_RECEIPT);
260
+ if (!received.ok)
261
+ return { refused: `rules could not be received (${describeFault(received.fault)})` };
262
+ const copy = received.value;
263
+ return { rows: Array.from({ length: copy.length }, (_, i) => orgRowPolicyOf(Object.hasOwn(copy, i) ? copy[i] : undefined)) };
264
+ }
265
+ const HELD_LIST_REFUSALS = new WeakMap();
266
+ function heldRefusalOf(words) {
267
+ const held = Object.freeze({});
268
+ HELD_LIST_REFUSALS.set(held, words);
269
+ return held;
270
+ }
271
+ function orgSnapshotCopyOf(v) {
272
+ if (v === undefined)
273
+ return undefined;
274
+ if (typeof v === "object" && v !== null && types.isProxy(v))
275
+ throw new Error("the snapshot is a Proxy, not data");
276
+ const envelope = scalarMembersOf(v, ORG_SNAPSHOT_ENVELOPE_MEMBERS);
277
+ const rules = typeof v === "object" && v !== null ? v.rules : undefined;
278
+ const refusal = listRefusalOf(rules);
279
+ if (refusal !== undefined)
280
+ return { ...envelope, rules: heldRefusalOf(refusal) };
281
+ const list = orgRuleListOf(rules);
282
+ if ("refused" in list)
283
+ throw new Error(`the snapshot's ${list.refused}`);
284
+ return { ...envelope, rules: list.rows };
285
+ }
286
+ function orgStateCopyOf(state) {
287
+ if (typeof state === "object" && state !== null && types.isProxy(state))
288
+ throw new Error("the persisted org state is a Proxy, not data");
289
+ const snapshot = typeof state === "object" && state !== null ? state.snapshot : undefined;
290
+ return { ...scalarMembersOf(state, ORG_STATE_MEMBERS), snapshot: orgSnapshotCopyOf(snapshot ?? null) };
291
+ }
292
+ function readOrgRow(row, index) {
293
+ const { rule: text, behavior: said } = orgRowPolicyOf(row);
294
+ const behavior = said === "ask" ? "ask" : "deny";
295
+ const unreadable = (code, detail) => ({
296
+ kind: "unreadable",
297
+ index,
298
+ rule: typeof text === "string" ? text : text === undefined ? "<unreadable>" : escapeForDisclosure(text),
299
+ code,
300
+ ...(detail !== undefined ? { detail } : {}),
301
+ behavior,
302
+ reach: reachOfRowHead(text, behavior),
303
+ });
304
+ if (typeof text !== "string" || text === "")
305
+ return unreadable("invalid.grammar", "no rule text");
306
+ if (said !== "deny" && said !== "ask")
307
+ return unreadable("invalid.grammar", said === undefined ? "no behavior" : `behavior "${escapeForDisclosure(said)}" is not deny or ask`);
308
+ const parsed = parseRuleText(text, said);
309
+ if ("reject" in parsed)
310
+ return unreadable(parsed.reject.code);
311
+ if (parsed.rule.tool !== COMMAND_RULE_TOOL)
312
+ return unreadable("unsupported.tool");
313
+ return { kind: "read", index, rule: text, behavior: said, parsed: parsed.rule };
314
+ }
315
+ export function readOrgRules(rules) {
316
+ const list = orgRuleListOf(rules);
317
+ if ("refused" in list)
318
+ return list;
319
+ const rows = list.rows.map((row, i) => readOrgRow(row, i + 1));
320
+ const unreadable = rows.filter((r) => r.kind === "unreadable");
321
+ return {
322
+ rows,
323
+ unreadable,
324
+ verdictFor(call) {
325
+ const words = typeof call.tool === "string" ? [call.tool] : call.tool;
326
+ const command = words.includes(COMMAND_RULE_TOOL) && typeof call.command === "string" ? call.command : undefined;
327
+ let ask;
328
+ let unjudged;
329
+ for (const row of rows) {
330
+ if (row.kind === "unreadable") {
331
+ if (!rowReaches(row.reach, words, call.face))
332
+ continue;
333
+ const verdict = { behavior: row.behavior, rule: row.rule, unreadable: unreadableOrgRowReason(row) };
334
+ if (row.behavior === "deny")
335
+ return verdict;
336
+ ask ??= verdict;
337
+ continue;
338
+ }
339
+ if (command === undefined)
340
+ continue;
341
+ const outcome = programRunReachOf(row.parsed, command);
342
+ if (outcome.reach === "not_reached")
343
+ continue;
344
+ if (outcome.reach === "unreadable") {
345
+ unjudged ??= { behavior: "ask", rule: row.rule, unreadable: outcome.reason };
346
+ continue;
347
+ }
348
+ if (row.behavior === "deny")
349
+ return { behavior: "deny", rule: row.rule };
350
+ ask ??= { behavior: "ask", rule: row.rule };
351
+ }
352
+ return unjudged ?? ask;
353
+ },
354
+ };
355
+ }
356
+ export function unenforceableOrgRules(rules) {
357
+ return readListedOrgRules(rules).unreadable.map((u) => ({ rule: u.rule, code: u.code }));
358
+ }
359
+ function readListedOrgRules(rules) {
360
+ const reading = readOrgRules(rules);
361
+ if ("refused" in reading)
362
+ throw new TypeError(`the org rules cannot be read: ${reading.refused}`);
363
+ return reading;
364
+ }
365
+ export function unreadableOrgRowReason(row) {
366
+ return `row ${row.index} of the organization snapshot is not a rule this layer reads (${row.code}${row.detail !== undefined ? `: ${row.detail}` : ""}); republish it in the shell tool's command grammar`;
367
+ }
368
+ export function orgUnreadableClause(reason) {
369
+ return `which could not be read against it (${reason})`;
211
370
  }
212
- function describeUnenforceable(unenforceable) {
213
- const named = unenforceable
214
- .slice(0, DISCLOSED_UNENFORCEABLE_RULES)
215
- .map((u) => `"${escapeForDisclosure(u.rule)}" (${u.code})`)
371
+ export function describeUnreadableOrgRows(unreadable, revision) {
372
+ if (unreadable.length === 0)
373
+ return undefined;
374
+ const named = unreadable
375
+ .slice(0, DISCLOSED_UNREADABLE_ROWS)
376
+ .map((u) => `row ${u.index} "${escapeForDisclosure(u.rule)}" (${u.code}${u.detail !== undefined ? `: ${u.detail}` : ""})`)
216
377
  .join(", ");
217
- const more = unenforceable.length - DISCLOSED_UNENFORCEABLE_RULES;
218
- return more > 0 ? `${named} and ${more} more` : named;
378
+ const more = unreadable.length - DISCLOSED_UNREADABLE_ROWS;
379
+ return (`the org snapshot${revision !== undefined ? ` (revision ${escapeForDisclosure(revision)})` : ""} is served DEGRADED: ${unreadable.length} row(s) this layer cannot read — ${named}${more > 0 ? ` and ${more} more` : ""}; ` +
380
+ `each stands at its strictest reading over the calls it may reach (a deny row, or one whose behavior cannot be read, refuses them; an ask row asks a person), and every readable row stands as published — republish the named rows in the shell tool's command grammar`);
219
381
  }
220
382
  function validateOrgSnapshot(s, nowMs) {
221
383
  if (typeof s.revision !== "number" || !Number.isFinite(s.revision))
@@ -225,61 +387,33 @@ function validateOrgSnapshot(s, nowMs) {
225
387
  if (s.fetchedAtMs > nowMs + ORG_FETCHED_AT_SKEW_ALLOWANCE_MS) {
226
388
  return `fetchedAtMs is ${s.fetchedAtMs - nowMs}ms in the future (allowance ${ORG_FETCHED_AT_SKEW_ALLOWANCE_MS}ms) — a future-dated observation would satisfy the staleness bound indefinitely`;
227
389
  }
228
- if (!Array.isArray(s.rules))
229
- return "rules is not an array";
230
- for (const r of s.rules) {
231
- if (typeof r?.rule !== "string" || r.rule === "")
232
- return "a rule entry carries no rule text";
233
- if (r.behavior !== "deny" && r.behavior !== "ask") {
234
- return `rule "${escapeForDisclosure(r.rule)}" carries behavior "${escapeForDisclosure(r.behavior)}" — the org layer has no allow bucket`;
235
- }
236
- }
237
- const unenforceable = unenforceableOrgRules(s.rules);
238
- const first = unenforceable[0];
239
- if (first !== undefined) {
240
- const rest = unenforceable.length > 1 ? ` (${unenforceable.length} in all — ${describeUnenforceable(unenforceable)})` : "";
241
- return `rule "${escapeForDisclosure(first.rule)}" does not parse (${first.code}) — an unenforceable deny/ask must not stand as policy${rest}`;
242
- }
243
- return undefined;
390
+ return listRefusalOf(s.rules);
244
391
  }
245
392
  export function orgRuleVerdictFor(rules, call) {
246
- let ask;
247
- let unreadable;
248
- for (const r of rules) {
249
- const parsed = parseRuleText(r.rule, r.behavior);
250
- if ("reject" in parsed)
251
- continue;
252
- if (parsed.rule.tool !== call.tool)
253
- continue;
254
- const outcome = programRunReachOf(parsed.rule, call.command);
255
- if (outcome.reach === "not_reached")
256
- continue;
257
- if (outcome.reach === "unreadable") {
258
- unreadable ??= { behavior: "ask", rule: r.rule, unreadable: outcome.reason };
259
- continue;
260
- }
261
- if (r.behavior === "deny")
262
- return { behavior: "deny", rule: r.rule };
263
- ask ??= { behavior: "ask", rule: r.rule };
264
- }
265
- return unreadable ?? ask;
393
+ return readListedOrgRules(rules).verdictFor(call);
266
394
  }
267
395
  export function orgRuleShadows(orgRules, rule) {
268
396
  return compileOrgShadowPredicate(orgRules)(rule);
269
397
  }
270
398
  export function compileOrgShadowPredicate(orgRules) {
271
- const denies = [];
272
- for (const d of orgRules) {
273
- if (d.behavior !== "deny")
274
- continue;
275
- const parsed = parseRuleText(d.rule, "deny");
276
- if (!("reject" in parsed))
277
- denies.push(parsed.rule);
278
- }
399
+ const { rows } = readListedOrgRules(orgRules);
400
+ const denies = rows.flatMap((r) => (r.kind === "read" && r.behavior === "deny" ? [r.parsed] : []));
401
+ const standing = rows.flatMap((r) => (r.kind === "unreadable" && r.behavior === "deny" ? [r.reach] : []));
279
402
  return (rule) => {
280
- if (rule.tool !== "Bash" || denies.length === 0)
403
+ if (standing.length > 0) {
404
+ const word = isShellFaceToolName(rule.tool) ? COMMAND_RULE_TOOL : rule.tool;
405
+ const target = pathTargetOf(rule.tool);
406
+ const faces = Object.hasOwn(PATH_RULE_FACES, rule.tool)
407
+ ? (PATH_RULE_FACES[rule.tool]?.allow ?? [])
408
+ : target !== undefined
409
+ ? [target.access === "read" ? "read" : "write"]
410
+ : [undefined];
411
+ if (standing.some((reach) => faces.some((face) => rowReaches(reach, [word], face))))
412
+ return true;
413
+ }
414
+ if (rule.tool !== COMMAND_RULE_TOOL || denies.length === 0)
281
415
  return false;
282
- return denies.some((d) => d.tool === rule.tool && ruleReachesProgramRun(d, rule.command));
416
+ return denies.some((d) => ruleReachesProgramRun(d, rule.command));
283
417
  };
284
418
  }
285
419
  export function isWritablePermissionRuleStore(store) {
@@ -47,7 +47,10 @@
47
47
  * The NAME lane is the same shape for the tools of `NAME_RULE_FACES` (the `Skill` tool): a `Skill(<name>)` / `Skill(<prefix>:*)`
48
48
  * deny or ask — the reference's own spelling — compiles through the same validator (the model's name grammar) and is judged on the
49
49
  * one string the call names (`nameRuleSubjectOf`) with `nameRuleAdmits`, the reader the persisted rule store uses. The parameter
50
- * spelling `Skill(skill:<name>)` those tools used to take is retired: refused loudly, its sentence naming the CC form.
50
+ * spelling `Skill(skill:<name>)` those tools used to take is retired: refused loudly, its sentence naming the CC form. A KEYED
51
+ * value tool (`Artifact(action:<word>)`) takes the same lane for content keyed by its own parameter (`nameGrammarReads`); its
52
+ * other arguments stay on the generic `param:pattern` lane. A call of it that names no value for the key as the tool reads it
53
+ * is refused by the key's deny or ask rule (`keyedValueAbsentReason`, the persisted lane's reading).
51
54
  *
52
55
  * 🔴 与 CC 的有意差(design/127 r2):
53
56
  * - CC 参数级 generic 道无 allow lane(gnn 只有 deny/ask)—— 我们同样,且整个 policy 无 allow 臂。
@@ -126,6 +129,9 @@ export interface PermissionRuleCaps {
126
129
  * 的热路径成本上界。 */
127
130
  maxScalarValueChars: number;
128
131
  }
132
+ /** The caps a rule set is held to unless the caller names its own — `maxRules` is the ONE bound on how many rules a set
133
+ * carries, read by the compiler here and by the org layer's list judgment (which refuses a longer list before it is copied). */
134
+ export declare const DEFAULT_CAPS: Readonly<PermissionRuleCaps>;
129
135
  export interface PermissionRulePolicyOptions {
130
136
  /** Invalid/unsupported rules: `"throw"`(default — 部署作者写错要立刻知道)aggregates ALL issues
131
137
  * into one error; `"skip"` drops them (pair with {@link validatePermissionRules} for a report, and/or
@@ -2,9 +2,10 @@ import { attest } from "./tool-policy.js";
2
2
  import { MCP_NAMESPACE, protocolOf } from "./protocol-table.js";
3
3
  import { catalogRuleFaceOf, isShellFaceToolName, pathTargetOf } from "./tool-registry.js";
4
4
  import { indexOfUnescaped, lastIndexOfUnescaped, parsePermissionRule, ruleSpellingOf } from "./permission-rule-syntax.js";
5
- import { COMMAND_RULE_TOOL, NAME_RULE_FACES, PATH_RULE_BASE_LABEL, PATH_RULE_FACES, callPathFaceOf, isUsablePathBase, laneToolNamesOf, nameRuleAdmits, nameRuleSubjectOf, parseRuleText, programRunReachOf, ruleBasesNeeded, ruleToolGrammarOf } from "./permission-rule-model.js";
5
+ import { COMMAND_RULE_TOOL, NAME_RULE_FACES, PATH_RULE_BASE_LABEL, PATH_RULE_FACES, callPathFaceOf, isUsablePathBase, keyedValueAbsentReason, laneToolNamesOf, nameGrammarReads, nameRuleAdmits, nameRuleSubjectOf, parseRuleText, programRunReachOf, ruleBasesNeeded } from "./permission-rule-model.js";
6
6
  import { pathFamilyOf } from "../tools/fs/safety.js";
7
- import { callTargetReadingOf, callTargetViewsOf } from "./effective-path-target.js";
7
+ import { argsAsTheToolReads, callTargetReadingOf, callTargetViewsOf } from "./effective-path-target.js";
8
+ import { canonicalize, UNCANONICALIZABLE } from "./canonical-json.js";
8
9
  import { bindPathRules, judgeRuleRows, policyAnchors, rowsWhereTheyLead } from "./path-rule-binding.js";
9
10
  import { ruleDenialSentence } from "./rule-denial-sentence.js";
10
11
  import { heldValues } from "./tool-name-readings.js";
@@ -24,7 +25,7 @@ function describeBehavior(v) {
24
25
  default: return v === null ? "null" : "an object";
25
26
  }
26
27
  }
27
- const DEFAULT_CAPS = {
28
+ export const DEFAULT_CAPS = {
28
29
  maxRules: 256,
29
30
  maxRuleLength: 512,
30
31
  maxPatternLength: 256,
@@ -164,7 +165,7 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
164
165
  continue;
165
166
  }
166
167
  const face = resolved.face;
167
- if (ruleToolGrammarOf(toolName) === "name") {
168
+ if (nameGrammarReads(toolName, content)) {
168
169
  const parsedName = parseRuleText(text, verb);
169
170
  if ("reject" in parsedName) {
170
171
  bad(text, "invalid.command_rule", `${parsedName.reject.code}: ${parsedName.reject.message}`);
@@ -242,12 +243,16 @@ export function validatePermissionRules(rules, opts) {
242
243
  function isPlainObjectArgs(args) {
243
244
  return typeof args === "object" && args !== null && !Array.isArray(args);
244
245
  }
245
- function matchParamRules(rules, args, maxScalarValueChars) {
246
+ function matchParamRules(rules, req, maxScalarValueChars) {
247
+ const args = req.args;
246
248
  if (!isPlainObjectArgs(args))
247
249
  return undefined;
248
250
  for (const r of rules) {
249
- if (!Object.hasOwn(args, r.param))
251
+ if (!Object.hasOwn(args, r.param)) {
252
+ if (readingAbsorbs(req, r.param, r.pattern))
253
+ return r;
250
254
  continue;
255
+ }
251
256
  const raw = args[r.param];
252
257
  if (raw === undefined || raw === null)
253
258
  continue;
@@ -262,6 +267,17 @@ function matchParamRules(rules, args, maxScalarValueChars) {
262
267
  }
263
268
  return undefined;
264
269
  }
270
+ function readingAbsorbs(req, param, pattern) {
271
+ if (pattern.includes("*"))
272
+ return false;
273
+ try {
274
+ const read = canonicalize(argsAsTheToolReads(req, { ...req.args, [param]: pattern }));
275
+ return read !== UNCANONICALIZABLE && read === canonicalize(req.args);
276
+ }
277
+ catch {
278
+ return false;
279
+ }
280
+ }
265
281
  function ruleDenial(req, ruleText, source) {
266
282
  return ruleDenialSentence({ source: "permission", tool: req.toolName, rule: ruleText, ...(source ? { origin: source } : {}) });
267
283
  }
@@ -387,6 +403,16 @@ export function createPermissionRulePolicy(rules, opts) {
387
403
  }
388
404
  return unreadable;
389
405
  };
406
+ const keylessRefusal = (entry, req) => {
407
+ for (const r of [...entry.content.deny, ...entry.content.ask]) {
408
+ if (r.parsed.match !== "name" || nameRuleSubjectOf(r.parsed.tool, req.args) !== undefined)
409
+ continue;
410
+ const absent = keyedValueAbsentReason(r.parsed.tool);
411
+ if (absent !== undefined)
412
+ return ruleDenialSentence({ source: "permission", tool: req.toolName, rule: r.ruleText, ...(r.source ? { origin: r.source } : {}), detail: absent });
413
+ }
414
+ return undefined;
415
+ };
390
416
  const unreadableAsk = (hit, lane, req) => ({
391
417
  action: "ask",
392
418
  message: `tool "${req.toolName}" needs approval: the call could not be read against the permission rule ${hit.rule.ruleText} (${hit.unreadable}) — a person must decide`,
@@ -433,11 +459,14 @@ export function createPermissionRulePolicy(rules, opts) {
433
459
  return face !== undefined ? attest(denied, req, { pathDenial: { rule: contentDeny.rule.ruleText, view: contentDeny.view, face } }) : denied;
434
460
  }
435
461
  if (entry) {
436
- const paramDeny = matchParamRules(entry.param.deny, req.args, caps.maxScalarValueChars);
462
+ const paramDeny = matchParamRules(entry.param.deny, req, caps.maxScalarValueChars);
437
463
  if (paramDeny) {
438
464
  return { action: "deny", message: ruleDenial(req, paramDeny.ruleText, paramDeny.source) };
439
465
  }
440
466
  }
467
+ const keyless = entry !== undefined ? keylessRefusal(entry, req) : undefined;
468
+ if (keyless !== undefined)
469
+ return { action: "deny", message: keyless };
441
470
  if (contentDeny?.unreadable !== undefined)
442
471
  return unreadableAsk({ rule: contentDeny.rule, unreadable: contentDeny.unreadable }, "content_deny", req);
443
472
  if (entry) {
@@ -458,7 +487,7 @@ export function createPermissionRulePolicy(rules, opts) {
458
487
  return { action: "ask", message: ruleAskMessage(contentAsk.rule.ruleText, contentAsk.rule.source), matchedAskRule: contentAsk.rule.ruleText };
459
488
  }
460
489
  if (entry) {
461
- const paramAsk = matchParamRules(entry.param.ask, req.args, caps.maxScalarValueChars);
490
+ const paramAsk = matchParamRules(entry.param.ask, req, caps.maxScalarValueChars);
462
491
  if (paramAsk) {
463
492
  return { action: "ask", message: ruleAskMessage(paramAsk.ruleText, paramAsk.source), matchedAskRule: paramAsk.ruleText };
464
493
  }
@@ -1,3 +1,4 @@
1
+ import { describeRejectedValue } from "./tool-search-setting.js";
1
2
  export const RETENTION_CASCADE_DOMAINS = Object.freeze(["checkpoints", "session-trees", "orphan-tool-results"]);
2
3
  export const RETENTION_CASCADE_EXEMPT = Object.freeze(["session-pollution", "session-capture-optout", "lineage.json"]);
3
4
  function codedError(code, message) {
@@ -7,8 +8,12 @@ function codedError(code, message) {
7
8
  }
8
9
  export function assertRetentionCapability(input) {
9
10
  const { policy, locked, stores } = input;
11
+ const declared = policy;
12
+ if (declared !== undefined && (typeof declared !== "object" || declared === null || Array.isArray(declared))) {
13
+ throw codedError("config.retention_policy", `retentionPolicy must be an object carrying maxAgeDays (got ${declared === null ? "null" : Array.isArray(declared) ? "an array" : typeof declared}).`);
14
+ }
10
15
  if (policy !== undefined && (!Number.isFinite(policy.maxAgeDays) || policy.maxAgeDays < 0)) {
11
- throw codedError("config.retention_policy", `retentionPolicy.maxAgeDays must be a finite non-negative number of days (got ${String(policy.maxAgeDays)}).`);
16
+ throw codedError("config.retention_policy", `retentionPolicy.maxAgeDays must be a finite non-negative number of days (got ${describeRejectedValue(policy.maxAgeDays)}).`);
12
17
  }
13
18
  const declaredRetention = (name, store) => {
14
19
  const r = store.retention;
@@ -19,7 +24,7 @@ export function assertRetentionCapability(input) {
19
24
  throw codedError("config.retention_capability", `store "${name}" declares an unparseable retention value ${JSON.stringify(r)} — expected "managed" or "none".`);
20
25
  };
21
26
  const declarations = stores
22
- .filter((s) => s.store !== undefined)
27
+ .filter((s) => s.store !== undefined && s.store !== null)
23
28
  .map(({ name, store }) => ({ name, retention: declaredRetention(name, store) }));
24
29
  if (!locked || policy === undefined)
25
30
  return;
@@ -46,7 +46,13 @@ export declare const RETIRED_SHELL_READ_BOUNDARY_KEYS: Readonly<Record<string, s
46
46
  * decision that carries one is denied in the engine's own words, never read without it: the type retires a member only where
47
47
  * a decision is a literal in an annotated position, and a `check` whose return type is inferred still compiles with it. */
48
48
  export declare const RETIRED_DECISION_MEMBERS: Readonly<Record<string, string>>;
49
- /** The `ToolPolicy` door ({@link RETIRED_TOOL_POLICY_KEYS}); `undefined` is no policy. */
50
- export declare function assertNoRetiredToolPolicyKeys(policy: object | undefined): void;
49
+ /**
50
+ * The `ToolPolicy` door — every policy the engine installs passes it (a task's or deployment's seat, a combined member, an inherited
51
+ * constraint): `undefined` (or `null`, the seat's other absence spelling) is no policy; a value that is not an object whose `check`
52
+ * is a function is no policy either, and refuses by name (`config.invalid_argument`, `detail.field: "ToolPolicy"`) — a policy the
53
+ * engine cannot call is never installed as if it were absent, which would run the leg with nothing the deployment meant to apply;
54
+ * a retired member refuses by name ({@link RETIRED_TOOL_POLICY_KEYS}).
55
+ */
56
+ export declare function assertNoRetiredToolPolicyKeys(policy: unknown): void;
51
57
  /** The `ShellReadBoundary` door ({@link RETIRED_SHELL_READ_BOUNDARY_KEYS}). */
52
58
  export declare function assertNoRetiredShellReadBoundaryKeys(boundary: object): void;
@@ -45,8 +45,19 @@ export const RETIRED_DECISION_MEMBERS = Object.freeze({
45
45
  guard: 'write `guards: ["<word>"]` instead (the same five words, as a list: one ask may stand on several integrity guards)',
46
46
  });
47
47
  export function assertNoRetiredToolPolicyKeys(policy) {
48
- if (policy !== undefined)
48
+ if (policy === undefined || policy === null)
49
+ return;
50
+ const isRecord = typeof policy === "object" && !Array.isArray(policy);
51
+ if (isRecord)
49
52
  assertNoRetiredKeys(policy, RETIRED_TOOL_POLICY_KEYS, "ToolPolicy");
53
+ const check = isRecord ? policy.check : undefined;
54
+ if (typeof check !== "function") {
55
+ const got = typeof policy !== "object" ? typeof policy : Array.isArray(policy) ? "an array" : `an object whose check is ${check === null ? "null" : typeof check}`;
56
+ const e = new Error(`ToolPolicy must be an object whose \`check\` is a function (got ${got}) — a policy the engine cannot call is refused rather than run as no policy; omit the seat for none.`);
57
+ e.code = "config.invalid_argument";
58
+ e.detail = { field: "ToolPolicy" };
59
+ throw e;
60
+ }
50
61
  }
51
62
  export function assertNoRetiredShellReadBoundaryKeys(boundary) {
52
63
  assertNoRetiredKeys(boundary, RETIRED_SHELL_READ_BOUNDARY_KEYS, "ShellReadBoundary");