@sema-agent/core 5.21.1 → 5.23.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 (96) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/dist/agents/send-message-tool.js +6 -3
  3. package/dist/agents/subagent.d.ts +6 -0
  4. package/dist/agents/subagent.js +48 -6
  5. package/dist/brain/errors.d.ts +20 -0
  6. package/dist/brain/errors.js +40 -0
  7. package/dist/brain/retry.d.ts +16 -2
  8. package/dist/brain/retry.js +3 -2
  9. package/dist/brain/status-sink.d.ts +9 -2
  10. package/dist/brain/stream-engine.d.ts +22 -0
  11. package/dist/brain/stream-engine.js +41 -10
  12. package/dist/core/ask-class.d.ts +48 -0
  13. package/dist/core/ask-class.js +33 -0
  14. package/dist/core/checkpoint-store.d.ts +103 -10
  15. package/dist/core/checkpoint-store.js +3 -1
  16. package/dist/core/governance-codes.d.ts +38 -0
  17. package/dist/core/governance-codes.js +14 -0
  18. package/dist/core/hooks.d.ts +101 -1
  19. package/dist/core/hooks.js +116 -14
  20. package/dist/core/locked-config.d.ts +7 -1
  21. package/dist/core/locked-config.js +2 -1
  22. package/dist/core/memory-engine/delegation-provenance.d.ts +62 -0
  23. package/dist/core/memory-engine/delegation-provenance.js +26 -0
  24. package/dist/core/memory-engine/engine.d.ts +94 -1
  25. package/dist/core/memory-engine/engine.js +329 -12
  26. package/dist/core/memory-engine/header-hints.d.ts +30 -0
  27. package/dist/core/memory-engine/header-hints.js +41 -0
  28. package/dist/core/memory-engine/index.d.ts +3 -2
  29. package/dist/core/memory-engine/index.js +3 -2
  30. package/dist/core/memory-engine/layout.d.ts +232 -0
  31. package/dist/core/memory-engine/layout.js +469 -1
  32. package/dist/core/memory-engine/tools.d.ts +30 -0
  33. package/dist/core/memory-engine/tools.js +108 -17
  34. package/dist/core/permission-rule-consent.d.ts +25 -9
  35. package/dist/core/permission-rule-consent.js +93 -21
  36. package/dist/core/permission-rule-model.d.ts +9 -1
  37. package/dist/core/permission-rule-model.js +2 -2
  38. package/dist/core/permission-rule-org.d.ts +195 -0
  39. package/dist/core/permission-rule-org.js +234 -0
  40. package/dist/core/permission-rule-store.d.ts +249 -6
  41. package/dist/core/permission-rule-store.js +314 -3
  42. package/dist/core/permission-rule-sync.d.ts +139 -0
  43. package/dist/core/permission-rule-sync.js +343 -0
  44. package/dist/core/runner/prepare-memory.js +35 -8
  45. package/dist/core/runner/prepare-task.d.ts +64 -3
  46. package/dist/core/runner/prepare-task.js +357 -32
  47. package/dist/core/runner/runtask.js +166 -6
  48. package/dist/core/shared-memory/contract.js +19 -4
  49. package/dist/core/shared-memory/normalize.d.ts +3 -1
  50. package/dist/core/shared-memory/tools.js +73 -17
  51. package/dist/core/shared-memory/types.d.ts +27 -1
  52. package/dist/core/store-contracts/permission-rule-sync-contract.d.ts +33 -0
  53. package/dist/core/store-contracts/permission-rule-sync-contract.js +186 -0
  54. package/dist/core/task-notification.d.ts +5 -2
  55. package/dist/core/task-registry-agent.d.ts +1 -1
  56. package/dist/core/task-registry-agent.js +6 -2
  57. package/dist/core/task-registry-shared.d.ts +9 -2
  58. package/dist/core/task-registry.d.ts +9 -3
  59. package/dist/core/task-registry.js +2 -0
  60. package/dist/core/tool-policy.d.ts +155 -4
  61. package/dist/core/tool-policy.js +148 -10
  62. package/dist/core/tool-result-store.d.ts +9 -1
  63. package/dist/core/tool-result-store.js +2 -1
  64. package/dist/core/trace.d.ts +79 -1
  65. package/dist/core/types.d.ts +94 -3
  66. package/dist/core/wiring-manifest.d.ts +16 -1
  67. package/dist/core/wiring-manifest.js +7 -1
  68. package/dist/index.d.ts +15 -8
  69. package/dist/index.js +12 -5
  70. package/dist/orchestration/goal.d.ts +10 -0
  71. package/dist/orchestration/goal.js +6 -5
  72. package/dist/stores/file/adoption/adopt.d.ts +146 -0
  73. package/dist/stores/file/adoption/adopt.js +616 -0
  74. package/dist/stores/file/adoption/marker.d.ts +194 -0
  75. package/dist/stores/file/adoption/marker.js +198 -0
  76. package/dist/stores/file/background-agent-store.js +2 -0
  77. package/dist/stores/file/checkpoint-store.d.ts +4 -0
  78. package/dist/stores/file/checkpoint-store.js +3 -0
  79. package/dist/stores/file/file-snapshot-store.js +2 -0
  80. package/dist/stores/file/index.d.ts +2 -0
  81. package/dist/stores/file/index.js +4 -0
  82. package/dist/stores/file/mailbox-store.js +2 -0
  83. package/dist/stores/file/memory-store.js +2 -0
  84. package/dist/stores/file/permission-rule-adopt.d.ts +62 -0
  85. package/dist/stores/file/permission-rule-adopt.js +95 -0
  86. package/dist/stores/file/permission-rule-store.d.ts +80 -2
  87. package/dist/stores/file/permission-rule-store.js +189 -46
  88. package/dist/stores/file/session-policy-store.js +2 -0
  89. package/dist/stores/file/session-store.js +2 -0
  90. package/dist/stores/file/task-list-store.js +2 -0
  91. package/dist/stores/file/tool-result-store.js +2 -0
  92. package/dist/stores/file/usage-window-store.js +2 -0
  93. package/dist/stores/file/workflow-journal-store.js +2 -0
  94. package/dist/stores/file/workflow-run-store.js +2 -0
  95. package/dist/tools/fs/bash-readonly-classifier.js +59 -10
  96. package/package.json +3 -2
@@ -1,4 +1,6 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { homedir } from "node:os";
3
+ import { brandPolicyAskClass } from "./ask-class.js";
2
4
  import { join, normalize as normalizePath, posix as posixPath, sep, win32 as winPath } from "node:path";
3
5
  import { BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName } from "../tools/fs/index.js";
4
6
  import { boundInputHashOf } from "./canonical-json.js";
@@ -12,6 +14,67 @@ export function isApprovalSettledBy(v) {
12
14
  export function decisionText(d) {
13
15
  return d.message;
14
16
  }
17
+ export function checkToolPolicyProjection(projection, req) {
18
+ for (const c of projection.components) {
19
+ if (c.kind === "tool_deny") {
20
+ if (c.names.includes(req.toolName)) {
21
+ return { action: "deny", message: `tool "${req.toolName}" is denied by a frozen inherited policy projection` };
22
+ }
23
+ continue;
24
+ }
25
+ if (c.kind === "tool_allowlist") {
26
+ if (!c.names.includes(req.toolName)) {
27
+ return { action: "deny", message: `tool "${req.toolName}" is not in a frozen inherited policy projection's allowlist` };
28
+ }
29
+ continue;
30
+ }
31
+ if (!c.tools.includes(req.toolName))
32
+ continue;
33
+ const command = req.args?.command;
34
+ if (typeof command !== "string") {
35
+ if (c.unparseableAction === "deny") {
36
+ return { action: "deny", message: `tool "${req.toolName}" call has no parseable command string (frozen inherited policy projection)` };
37
+ }
38
+ continue;
39
+ }
40
+ const parsed = parseLeadingCommandName(command);
41
+ if ("reject" in parsed) {
42
+ if (c.unparseableAction === "deny") {
43
+ return { action: "deny", message: `command is not a single simple command (${parsed.reject}) (frozen inherited policy projection)` };
44
+ }
45
+ continue;
46
+ }
47
+ if (c.deny.includes(parsed.name)) {
48
+ return { action: "deny", message: `command "${parsed.name}" is denied by a frozen inherited policy projection` };
49
+ }
50
+ if (c.allow !== undefined && !c.allow.includes(parsed.name) && c.unmatchedAction === "deny") {
51
+ return { action: "deny", message: `command "${parsed.name}" is not in a frozen inherited policy projection's allowlist` };
52
+ }
53
+ }
54
+ return undefined;
55
+ }
56
+ export function constraintChainEntryOf(policy, meta) {
57
+ const m = {
58
+ ...(meta?.autoModeArmed ? { autoModeArmed: true } : {}),
59
+ ...(meta?.durableMandate ? { durableMandate: true } : {}),
60
+ ...(meta?.contentMandate ? { contentMandate: true } : {}),
61
+ };
62
+ const p = policy.projection;
63
+ if (p === undefined)
64
+ return { opaque: true, ...m };
65
+ return { components: p.components, requiresLiveRemainder: p.requiresLiveRemainder, ...m };
66
+ }
67
+ function stableJson(v) {
68
+ if (v === null || typeof v !== "object")
69
+ return JSON.stringify(v) ?? "null";
70
+ if (Array.isArray(v))
71
+ return `[${v.map(stableJson).join(",")}]`;
72
+ const keys = Object.keys(v).sort();
73
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${stableJson(v[k])}`).join(",")}}`;
74
+ }
75
+ export function constraintChainDigest(chain) {
76
+ return `cpv1:${createHash("sha256").update(stableJson(chain)).digest("hex")}`;
77
+ }
15
78
  const ALLOW = { action: "allow" };
16
79
  const RETIRED_TEXT_FIELD = "reason";
17
80
  const RETIRED_TEXT_FIELD_DENY_MESSAGE = `a permission decision carries the retired "${RETIRED_TEXT_FIELD}" field — rename it to "message" (the one text field ` +
@@ -100,6 +163,13 @@ export function createAllowDenyPolicy(opts) {
100
163
  const allow = opts.allow ? new Set(opts.allow) : undefined;
101
164
  const deny = new Set(opts.deny ?? []);
102
165
  return {
166
+ projection: {
167
+ components: [
168
+ ...(opts.deny && opts.deny.length > 0 ? [{ kind: "tool_deny", names: [...opts.deny] }] : []),
169
+ ...(opts.allow ? [{ kind: "tool_allowlist", names: [...opts.allow] }] : []),
170
+ ],
171
+ requiresLiveRemainder: false,
172
+ },
103
173
  nameSets: [{ ...(opts.allow ? { allow: [...opts.allow] } : {}), ...(opts.deny ? { deny: [...opts.deny] } : {}) }],
104
174
  check(req) {
105
175
  const toolName = req.toolName;
@@ -124,6 +194,13 @@ export function createApprovalPolicy(opts) {
124
194
  const deny = new Set(opts.deny ?? []);
125
195
  const auto = new Set(opts.autoAllow ?? []);
126
196
  return {
197
+ projection: {
198
+ components: [
199
+ ...(opts.deny && opts.deny.length > 0 ? [{ kind: "tool_deny", names: [...opts.deny] }] : []),
200
+ ...(opts.denyByDefault === true ? [{ kind: "tool_allowlist", names: [...opts.requireApproval, ...(opts.autoAllow ?? [])] }] : []),
201
+ ],
202
+ requiresLiveRemainder: opts.requireApproval.length > 0,
203
+ },
127
204
  nameSets: [
128
205
  {
129
206
  ask: [...opts.requireApproval],
@@ -179,7 +256,25 @@ export function createApprovalPolicy(opts) {
179
256
  }
180
257
  export function combinePolicies(...policies) {
181
258
  const nameSets = policies.flatMap(toolPolicyNameSets);
259
+ const childProjections = policies.map((p) => p.projection);
260
+ const projectedComponents = [];
261
+ let combinedRemainder = false;
262
+ for (const p of childProjections) {
263
+ if (p === undefined) {
264
+ combinedRemainder = true;
265
+ break;
266
+ }
267
+ projectedComponents.push(...p.components);
268
+ if (p.requiresLiveRemainder) {
269
+ combinedRemainder = true;
270
+ break;
271
+ }
272
+ }
273
+ const combinedProjection = childProjections.some((p) => p !== undefined)
274
+ ? { components: projectedComponents, requiresLiveRemainder: combinedRemainder }
275
+ : undefined;
182
276
  return {
277
+ ...(combinedProjection !== undefined ? { projection: combinedProjection } : {}),
183
278
  ...(nameSets.length > 0 ? { nameSets } : {}),
184
279
  async check(req, signal) {
185
280
  let asked;
@@ -225,7 +320,22 @@ export function createCoarseCommandNamePolicy(opts) {
225
320
  const fallback = (reason) => defaultAction === "deny"
226
321
  ? { action: "deny", message: reason, decisionReason: "rule" }
227
322
  : { action: "ask", message: reason, decisionReason: "rule" };
228
- return {
323
+ const fallbackIsDeny = defaultAction === "deny";
324
+ const projection = {
325
+ components: [
326
+ {
327
+ kind: "shell_command",
328
+ tools: [...shellTools],
329
+ deny: [...deny],
330
+ ...(allow !== undefined ? { allow: [...allow] } : {}),
331
+ unmatchedAction: fallbackIsDeny ? "deny" : "none",
332
+ unparseableAction: fallbackIsDeny ? "deny" : "none",
333
+ },
334
+ ],
335
+ requiresLiveRemainder: !fallbackIsDeny,
336
+ };
337
+ return brandPolicyAskClass({
338
+ projection,
229
339
  check(req) {
230
340
  if (!shellTools.has(req.toolName))
231
341
  return ALLOW;
@@ -245,7 +355,7 @@ export function createCoarseCommandNamePolicy(opts) {
245
355
  }
246
356
  return ALLOW;
247
357
  },
248
- };
358
+ }, "sandbox_local");
249
359
  }
250
360
  const UNVERIFIABLE_DELETE_SAFE_VARS = ["TMPDIR", "HOME", "PWD"];
251
361
  const DELETE_HEADS = new Set(["rm", "xargs", "Remove-Item"]);
@@ -473,7 +583,7 @@ function hasRecursiveForce(argvTail) {
473
583
  export function createUnverifiableDeletePolicy(opts) {
474
584
  const safeVars = new Set([...UNVERIFIABLE_DELETE_SAFE_VARS, ...(opts?.safeVars ?? [])]);
475
585
  const shellTools = canonicalToolNameSet(opts?.tools);
476
- return {
586
+ return brandPolicyAskClass({
477
587
  check(req) {
478
588
  if (!shellTools.has(req.toolName))
479
589
  return ALLOW;
@@ -492,7 +602,7 @@ export function createUnverifiableDeletePolicy(opts) {
492
602
  `(or assign the variable in the same command, e.g. \`DIR=/exact/path; rm -rf "$DIR"\`) so the target can be verified.`,
493
603
  };
494
604
  },
495
- };
605
+ }, "external_authority");
496
606
  }
497
607
  function pathSegments(p) {
498
608
  return p.split(/[\\/]/).filter(Boolean);
@@ -569,7 +679,7 @@ export function createTranscriptIntegrityPolicy(opts) {
569
679
  `files — modifying or deleting them tampers with the run's own audit trail. Reading them (ls/cat/grep ` +
570
680
  `as a single simple command) is fine.`,
571
681
  });
572
- return {
682
+ return brandPolicyAskClass({
573
683
  check(req) {
574
684
  const toolName = req.toolName;
575
685
  if (shellTools.has(toolName)) {
@@ -597,7 +707,7 @@ export function createTranscriptIntegrityPolicy(opts) {
597
707
  }
598
708
  return ALLOW;
599
709
  },
600
- };
710
+ }, "external_authority");
601
711
  }
602
712
  const delegatedApproverRoot = new WeakMap();
603
713
  export function withDelegationProvenance(onAsk, delegation) {
@@ -734,12 +844,40 @@ export async function resolveAsk(req, onAsk, signal) {
734
844
  };
735
845
  }
736
846
  if (typeof ok === "object" && ok !== null) {
737
- if (ok.allow !== true) {
738
- return { action: "deny", message: `approval denied for "${req.toolName}": ${req.message}`, decisionReason: "mode", settledBy: "human" };
847
+ const supplied = ok.settledBy;
848
+ const allowed = ok.allow;
849
+ const suppliedEdit = ok.updatedInput;
850
+ if (supplied !== undefined && supplied !== "human" && supplied !== "timeout") {
851
+ return {
852
+ action: "deny",
853
+ message: `the approver for "${req.toolName}" reported settledBy "${typeof supplied === "string" ? containThrownText(supplied) : supplied === null ? "null" : typeof supplied}", which is outside what a synchronous ` +
854
+ `approver may self-report — it is exactly "human" or "timeout" (or omitted); denied fail-closed`,
855
+ decisionReason: "mode",
856
+ settledBy: "aborted",
857
+ };
858
+ }
859
+ if (supplied === "timeout" && allowed === true) {
860
+ return {
861
+ action: "deny",
862
+ message: `the approver for "${req.toolName}" returned an allow settled by "timeout" — an elapsed approval window cannot be ` +
863
+ `what approved a call; denied fail-closed (report timeout with allow:false, or allow with settledBy "human"/omitted)`,
864
+ decisionReason: "mode",
865
+ settledBy: "timeout",
866
+ };
867
+ }
868
+ if (allowed !== true) {
869
+ return {
870
+ action: "deny",
871
+ message: supplied === "timeout"
872
+ ? `approval for "${req.toolName}" was not answered before the approver's own window elapsed: ${req.message}`
873
+ : `approval denied for "${req.toolName}": ${req.message}`,
874
+ decisionReason: "mode",
875
+ settledBy: supplied === "timeout" ? "timeout" : "human",
876
+ };
739
877
  }
740
- if (ok.updatedInput === undefined)
878
+ if (suppliedEdit === undefined)
741
879
  return { action: "allow", decisionReason: "mode", presentedInput: presented.value, settledBy: "human" };
742
- const edit = tryCloneArgs(ok.updatedInput);
880
+ const edit = tryCloneArgs(suppliedEdit);
743
881
  if (!edit.ok) {
744
882
  return {
745
883
  action: "deny",
@@ -213,7 +213,15 @@ export declare function firstPartyOffloadPolicy(toolName: string): {
213
213
  };
214
214
  /** Head+tail preview of an offloaded result, pointing at {@link OFFLOAD_TOOL_NAME} via `ref`. Exported so
215
215
  * the per-message aggregate budget (design/64 §17.2) produces the SAME preview format (deterministic →
216
- * byte-identical across queries → prompt-cache safe). */
216
+ * byte-identical across queries → prompt-cache safe).
217
+ *
218
+ * #115 (2026-08-09) — the truncation line states WHERE the rest is, not what anyone can do with it.
219
+ * "read more" alone reads as a promise that the full output is retrievable, and the model relays that
220
+ * promise to the person it is talking to; what is actually guaranteed is narrower and exactly two
221
+ * things: the text is retained in this run's tool-result store, and THIS chain can page it back. A
222
+ * host-side read face (an HTTP route that hands the user the whole output) is a separate deployment
223
+ * feature that may or may not exist — so the line names it as a deployment fact instead of implying
224
+ * it, and the model can answer "can I get the full log?" without guessing. */
217
225
  export declare function buildPreview(full: string, ref: string, sizes?: {
218
226
  head: number;
219
227
  tail: number;
@@ -154,7 +154,8 @@ export function buildPreview(full, ref, sizes, reachableTools) {
154
154
  const tail = full.slice(tailStart);
155
155
  return (`${PERSISTED_OUTPUT_PREFIX}"${ref}" chars="${total}">\n` +
156
156
  `${head}\n` +
157
- `…[truncated — ${total} chars total. ${offloadPagebackHint(ref, "preview", reachableTools)}]\n` +
157
+ `…[truncated — ${total} chars total, retained in this run's tool-result store; a user-facing read face is a deployment fact. ` +
158
+ `${offloadPagebackHint(ref, "preview", reachableTools)}]\n` +
158
159
  `${tail}\n` +
159
160
  `</persisted-output>`);
160
161
  }
@@ -16,7 +16,7 @@
16
16
  * core's own contract is only the field names here; it deliberately stops at supplying the FACTS a bridge
17
17
  * needs (ids, turn indices, token counts) rather than adopting the convention's vocabulary internally.
18
18
  */
19
- import type { TaskStatus, ToolEffect } from "./types.js";
19
+ import type { BrainRetryErrClass, TaskStatus, ToolEffect } from "./types.js";
20
20
  import type { ThinkingLevel } from "../internal/harness-types.js";
21
21
  import type { ToolManifestRow } from "../prompt-assembly/tool-catalog.js";
22
22
  /**
@@ -382,6 +382,76 @@ export type TraceEvent = {
382
382
  taskId: string;
383
383
  message: string;
384
384
  ts: number;
385
+ } | {
386
+ /**
387
+ * design/182 §4.6 — a cloud-sync round moved a (rule, scope) from REMOVED back to LIVE: another
388
+ * replica's add outlived the local tombstone (add-wins is the locked merge semantic, so the
389
+ * transition itself is not news — its VISIBILITY is). The person who deleted the rule has to be
390
+ * able to learn that it came back, and to delete it again. Metadata only: the canonical rule
391
+ * text and the scope KIND — a project scope's root path stays out of the trace.
392
+ */
393
+ kind: "permission.rule_sync_resurrected";
394
+ version: 1;
395
+ /** The bucket this round synced. A local-owner bucket cannot sync, so this is always a principal. */
396
+ principal: string;
397
+ rule: string;
398
+ scopeKind: "global" | "project";
399
+ ts: number;
400
+ } | {
401
+ /**
402
+ * design/182 §4.3/§5.2 — a cloud-sync round REFUSED an inbound row, or quarantined a local one.
403
+ * The reason is a closed code from the governance-codes family (never free text): the machine
404
+ * reads the table. One event per dropped row — a silently refused row is indistinguishable from
405
+ * a row that never existed, which is the failure this event exists to prevent.
406
+ */
407
+ kind: "permission.rule_sync_dropped";
408
+ version: 1;
409
+ principal: string;
410
+ rule: string;
411
+ scopeKind: "global" | "project";
412
+ /** Closed reason code ({@link import("./governance-codes.js").RuleSyncDropReason}). */
413
+ reason: string;
414
+ ts: number;
415
+ } | {
416
+ /**
417
+ * design/182 §7.4 — an org-governed deployment could NOT adjudicate against an org snapshot
418
+ * (never installed, provider failed past the staleness bound, or the persisted state was
419
+ * refused). The gate answered by failing the WHOLE decision boundary closed: every terminal
420
+ * allow became a real-approval ask. This is the operator-facing half of that tighten — the
421
+ * deployment is paying the "ask about everything" price and has to be able to see why.
422
+ */
423
+ kind: "permission.org_snapshot_unavailable";
424
+ version: 1;
425
+ taskId: string;
426
+ toolName: string;
427
+ toolCallId: string;
428
+ /** The overlay's own disclosure lines (deployment-authored/engine-authored, never model text),
429
+ * joined — provider failure, refused snapshot, rollback refusal, missing durable persistence. */
430
+ message: string;
431
+ ts: number;
432
+ } | {
433
+ /**
434
+ * F-012 L2 — a pending ask was AUTO-ADMITTED by the sandbox-admission leg: the deployment's
435
+ * execution env declares an isolated sandbox, every surviving ask on the call was
436
+ * engine-classified `sandbox_local`, and the call crosses no declared boundary. This record is
437
+ * the durable admission ledger's operator half (the transcript half rides the tool result's
438
+ * system-reminder context); a consumer anchors on THIS kind, never on wording.
439
+ */
440
+ kind: "permission.sandbox_admitted";
441
+ version: 1;
442
+ taskId: string;
443
+ toolName: string;
444
+ toolCallId: string;
445
+ /** The engine ask classes of the admitted call's surviving asks (all `sandbox_local` by the
446
+ * admission predicate; carried explicitly so the ledger states what was judged, not just that
447
+ * judgment passed). */
448
+ askClasses: readonly string[];
449
+ /** Which layers raised the admitted asks (`caller_policy` / `ancestor_constraint` /
450
+ * `gate_safety_tighten`). */
451
+ sourceLayers: readonly string[];
452
+ /** The boundary classification the admission asserted. */
453
+ boundary: "sandbox_internal";
454
+ ts: number;
385
455
  } | {
386
456
  /** C1 — the failover brain served this call from a FALLBACK entry (`createFailoverBrain`): the
387
457
  * primary (and possibly earlier hops) failed cleanly upfront. Without this, same-model gateway
@@ -418,6 +488,14 @@ export type TraceEvent = {
418
488
  /** 1-based attempt number that was ABANDONED (the retry that follows is attempt+1). */
419
489
  attempt: number;
420
490
  phase: "connect" | "midstream";
491
+ /** WHY the attempt is being retried, as the closed neutral bucket the user-facing status frame
492
+ * also carries ({@link BrainRetryErrClass}) — an operator reading this and a user reading a
493
+ * progress line then agree on the reason. Optional: absent on frames minted before it existed. */
494
+ errClass?: BrainRetryErrClass;
495
+ /** The backoff about to be slept, ms — 0 for an immediate re-send. Together with `attempt` this
496
+ * makes a whole retry chain reconstructible from the trace alone, which is what a caller that saw
497
+ * only silence needs to explain where the time went. */
498
+ nextDelayMs?: number;
421
499
  ts: number;
422
500
  } | {
423
501
  /** C2 — the agent loop drove one of its self-heal recoveries (malformed-tool retry / thinking-only
@@ -737,6 +737,17 @@ export interface ToolExecuteContext {
737
737
  * tool runs outside a Runner task.
738
738
  */
739
739
  inheritedGateForChildren?: () => import("./runner/prepare-task.js").InheritedGate;
740
+ /**
741
+ * design/180 half A — the delegation runtime-provenance ARMING face. Runner-filled; a delegation
742
+ * tool calls it at spawn time: a non-undefined return means this (parent) run is armed (it mounts
743
+ * a memory session, or is itself recording for its own parent) and carries the chain's FROZEN
744
+ * content-safety snapshot — the tool then mints the child's recorder ref and threads both into
745
+ * the child's trusted `RunInternals.delegationProvenance`. Undefined return / absent field ⇒ the
746
+ * child spawns without a recorder (its deliveries read `unknown` and every judgment stays on the
747
+ * static floor — v1 behavior byte-identical). Same trust posture as
748
+ * {@link inheritedGateForChildren}: never a model/tool argument, never a TaskSpec field.
749
+ */
750
+ delegationProvenanceForChildren?: () => import("./memory-engine/delegation-provenance.js").DelegationContentSafety | undefined;
740
751
  /**
741
752
  * RB-201 FO-3 (form-one audit, CC 220 `Ipd`/`ein` parity) — the auto-mode classifier decider ARMED
742
753
  * for THIS task (`RuntimeCaps.autoMode === true` AND `RunnerDeps.autoMode` both present; the same
@@ -1802,6 +1813,17 @@ export interface TaskSpec {
1802
1813
  agents?: AgentDefinition[];
1803
1814
  /** Gate tool calls before they run (allow/deny/approval). Overrides `RunnerDeps.toolPolicy`. */
1804
1815
  toolPolicy?: import("./tool-policy.js").ToolPolicy;
1816
+ /**
1817
+ * #93 (F-012 L3) — the OVERRIDE seat for the durable resume-edit re-adjudication policy: the policy
1818
+ * an approver's `updatedInput` EDIT is re-checked against before a resumed pending call executes
1819
+ * (and, at park-mint time, the policy a store-codec-moved projection is re-adjudicated by). ABSENT
1820
+ * falls back to the caller policy (`toolPolicy ?? RunnerDeps.toolPolicy`) — never to a silent skip:
1821
+ * the pre-#93 shape skipped the whole recheck for deployments with no caller policy, which let a
1822
+ * fidelity-projection-moved value land unadjudicated. Supply this only to make the resume-edit
1823
+ * boundary STRICTER/different from the live caller policy; it never widens (the frozen ancestor
1824
+ * projections and the deny-narrowing layers still apply regardless).
1825
+ */
1826
+ basePolicyForResumeEdit?: import("./tool-policy.js").ToolPolicy;
1805
1827
  /** How `ask` decisions resolve for this task (headless auto-deny by default). Overrides `RunnerDeps.onAsk`. */
1806
1828
  onAsk?: import("./tool-policy.js").OnAsk;
1807
1829
  /** Content-ask seam (design/64 §5): routes an AskUserQuestion tool call to a real human/UI. When set, the
@@ -2999,6 +3021,24 @@ export type BrainStatusPhase = "rate_limited" | "retrying" | "reconnecting" | "c
2999
3021
  * Both mean the same thing to a renderer — stop showing the retry state.
3000
3022
  */
3001
3023
  | "recovered" | "gave_up";
3024
+ /**
3025
+ * WHY a retry wait is happening, as a closed, provider-NEUTRAL bucket — the companion to
3026
+ * {@link BrainStatusPhase}, which says what the brain is doing about it. A consumer rendering an
3027
+ * unattended progress line ("no answer for three minutes") needs the reason, and until this existed the
3028
+ * only carriers of it were the HTTP status and the syscall code, neither of which may cross this
3029
+ * channel. Values are about the SHAPE of the failure, never its provider taxonomy:
3030
+ * - `connect_refused` — the attempt got a definite negative about the target itself (nothing accepts
3031
+ * at that address, or the name has no address). This is the class the SHORT retry lane serves.
3032
+ * - `transport` — any other transport-level failure: a connect timeout, a reset, a mid-stream
3033
+ * tear, a stalled stream. No verdict about the target; the full ladder applies.
3034
+ * - `rate_limit` — the provider asked the caller to slow down.
3035
+ * - `server` — the provider reported a failure on its own side.
3036
+ * - `http` — a response the status predicate calls terminal, retried anyway because the
3037
+ * provider's own explicit retry verdict said to.
3038
+ * - `output_cap` — not a failure of the connection at all: the request is being re-sent with a
3039
+ * lowered output cap after the provider reported the context limit exceeded (no backoff).
3040
+ */
3041
+ export type BrainRetryErrClass = "connect_refused" | "transport" | "rate_limit" | "server" | "http" | "output_cap";
3002
3042
  /** design/99 §E3/§E10 — the payload of a {@link TaskEvent} `status` event (and the brain→runner signal). */
3003
3043
  export interface BrainStatus {
3004
3044
  phase: BrainStatusPhase;
@@ -3013,8 +3053,14 @@ export interface BrainStatus {
3013
3053
  /** RB-420-c — 1-based index of the attempt that just failed (the wait precedes attempt `attempt + 1`);
3014
3054
  * same numbering as the `brain.retry` telemetry frame. Absent on frames that are not a retry wait. */
3015
3055
  attempt?: number;
3016
- /** RB-420-c — the retry budget of THIS lane, so a consumer can render "attempt 3 of 10". */
3056
+ /** RB-420-c — the retry budget of THIS lane, so a consumer can render "attempt 3 of 10". Lane, not
3057
+ * engine: a failure class served by a shorter ladder reports that ladder's budget, so the fraction a
3058
+ * consumer renders is the one actually in force rather than the engine-wide ceiling. */
3017
3059
  maxRetries?: number;
3060
+ /** Why this wait is happening ({@link BrainRetryErrClass}). Present on retry-wait frames whose cause
3061
+ * the engine classified; absent on frames that are not a retry wait (`recovered`/`gave_up`) and on a
3062
+ * `circuit_open` fast-fail, which is a local verdict rather than an observed failure. */
3063
+ errClass?: BrainRetryErrClass;
3018
3064
  }
3019
3065
  /**
3020
3066
  * design/97 CORE-8 (③): one lightweight TOOL-ACTIVITY beat surfaced from a running task, for a per-agent live
@@ -3955,8 +4001,11 @@ export interface BackgroundChildEvent {
3955
4001
  */
3956
4002
  editedFiles?: import("../agents/subagent-steps.js").SubagentEditedFile[];
3957
4003
  /**
3958
- * terminal (residual observability, lane E): `true` when the child can be revived with `SendMessage` (its session is
3959
- * retained and it was not stopped by the user); `false` for a user-stopped / non-resumable child.
4004
+ * terminal (residual observability, lane E): `true` when the child can be revived with `SendMessage`
4005
+ * either its session is retained live and the run was not killed, or it has a NAMED durable row that no
4006
+ * USER stop closed (a parent teardown, a reap or a host death is precisely what the durable revival
4007
+ * lane recovers from). `false` for a user-stopped child, an anonymous or store-less one, and for the
4008
+ * session-teardown reap frame, whose lane cannot know what survives the teardown.
3960
4009
  * Lets a parent's orchestration logic decide "continue it" vs "start fresh" without trial-and-error.
3961
4010
  */
3962
4011
  resumable?: boolean;
@@ -4151,6 +4200,44 @@ export interface RunnerDeps {
4151
4200
  * disclosure — a loosening face fails toward asking.
4152
4201
  */
4153
4202
  permissionRuleStore?: import("./permission-rule-store.js").PermissionRuleStoreProvider;
4203
+ /**
4204
+ * design/182 §4.5 (F-011) — declare that this deployment keeps its permission rules in the
4205
+ * IDENTITY-LESS local bucket: a task with no `principal` resolves rules through the provider's
4206
+ * `forLocalOwner()` face instead of resolving to zero rules.
4207
+ *
4208
+ * A DECLARATION, never an inference (same rule as {@link permissionRuleOrg}): the local bucket is
4209
+ * "this machine's owner", a fact only the deployment knows. Omitted/`false` ⇒ v1 exactly — an
4210
+ * unauthenticated task reads no rules. Declared `true` while the wired provider has no local-owner
4211
+ * face (or while no provider is wired at all) is a configuration contradiction and is refused loudly
4212
+ * at prepare, rather than silently degrading to "the rules this person approved stopped applying".
4213
+ *
4214
+ * A local-owner bucket cannot cloud-sync (syncing is an authenticated act); it is adopted into a
4215
+ * principal bucket by `adoptFilePermissionRuleStore`, after which the local-owner face resolves the
4216
+ * adopted principal's bucket forever.
4217
+ */
4218
+ localOwnerRules?: boolean;
4219
+ /**
4220
+ * design/182 §7 — the ORG rule overlay for an org-GOVERNED deployment. Constructed with
4221
+ * `createOrgRuleOverlay` (that constructor is the boot gate: a governed declaration with no snapshot
4222
+ * provider refuses to boot), so a value here IS the declaration — nothing is inferred from wiring.
4223
+ *
4224
+ * Present ⇒ every tool call is adjudicated against the org's published deny/ask snapshot BEFORE the
4225
+ * ask-resolution chain (org deny > org ask > personal allow rule > bare ask), and while the overlay
4226
+ * cannot adjudicate (never installed / past the staleness bound / a refused rollback) the WHOLE
4227
+ * decision boundary fails closed: every terminal allow becomes a real-approval ask and both
4228
+ * ask→allow seams are disarmed. Omitted ⇒ the org layer does not exist and the decision path is
4229
+ * byte-identical to a build without it.
4230
+ */
4231
+ permissionRuleOrg?: import("./permission-rule-org.js").OrgRuleOverlay;
4232
+ /**
4233
+ * design/182 §9 — DECLARE that this deployment drives cloud sync (`syncPermissionRules`) for the
4234
+ * wired rule store. Purely a disclosure input: it is reported as `permissionRules.syncWired` on the
4235
+ * wiring manifest and changes no decision. It exists because the sync loop is HOST-driven (core
4236
+ * bundles no fetch and owns no timer), so there is nothing for the engine to infer — and "are this
4237
+ * machine's standing approvals shared with other devices and a server?" is exactly the trust-domain
4238
+ * fact an operator must be able to read off the manifest instead of guessing.
4239
+ */
4240
+ permissionRuleSyncWired?: boolean;
4154
4241
  /**
4155
4242
  * design/99 §K — resolve the **per-principal runtime ENTITLEMENTS** the engine
4156
4243
  * ENFORCES server-side, keyed by `spec.principal`. A deployment (the SERVICE) implements it over center's
@@ -4543,6 +4630,10 @@ export interface RunnerDeps {
4543
4630
  mcpImageResizer?: import("./mcp.js").McpImageResizer;
4544
4631
  /** Default tool-call gate for all tasks (a task's own `toolPolicy` overrides this). */
4545
4632
  toolPolicy?: import("./tool-policy.js").ToolPolicy;
4633
+ /** Deployment default for {@link TaskSpec.basePolicyForResumeEdit} (#93 / F-012 L3): the resume-edit
4634
+ * re-adjudication override. Resolution: `spec.basePolicyForResumeEdit ?? THIS ?? (spec.toolPolicy ??
4635
+ * deps.toolPolicy)` — absence falls back to the caller policy, never to a silent skip. */
4636
+ basePolicyForResumeEdit?: import("./tool-policy.js").ToolPolicy;
4546
4637
  /**
4547
4638
  * How `ask` decisions resolve when a policy/hook requests human confirmation (design/37). Default
4548
4639
  * (omitted) = `"deny"`: **headless auto-deny** — no approver, so `ask` resolves deterministically to
@@ -136,6 +136,17 @@ export interface WiringManifest {
136
136
  * "this build has no such feature", and a present-and-false one says "it exists and is off here". */
137
137
  permissionRules: {
138
138
  storeWired: boolean;
139
+ /** design/182 §9 — this deployment DECLARED that it drives cloud sync for the wired rule store
140
+ * (`RunnerDeps.permissionRuleSyncWired`). Connecting a bucket to a sync endpoint merges every
141
+ * device, the transport and the server into ONE consent trust domain, which is a fact an operator
142
+ * reads off the manifest rather than infers. `false` on a deployment that does not sync — same
143
+ * present-and-false shape as `storeWired`, for the same reason. */
144
+ syncWired: boolean;
145
+ /** design/182 §7 — this deployment DECLARED org governance (`RunnerDeps.permissionRuleOrg`, whose
146
+ * constructor refuses to boot a governed declaration with no snapshot provider). `true` also means
147
+ * the fail-closed availability contract is armed: while no org snapshot can be adjudicated
148
+ * against, every terminal allow tightens to a real-approval ask. */
149
+ orgGoverned: boolean;
139
150
  };
140
151
  /**
141
152
  * Governance surfaces (presence facts only). `audience: "operator"` is the MACHINE-READABLE
@@ -192,6 +203,10 @@ export interface WiringFacts {
192
203
  backgroundAgentStoreWired: boolean;
193
204
  /** design/179 — a persisted allow-rule store provider is wired. */
194
205
  permissionRuleStoreWired: boolean;
206
+ /** design/182 §9 — the deployment declared that it drives cloud sync for that store. */
207
+ permissionRuleSyncWired: boolean;
208
+ /** design/182 §7 — the deployment declared org governance (an org rule overlay is wired). */
209
+ permissionRuleOrgGoverned: boolean;
195
210
  hostChildEventSinkWired: boolean;
196
211
  lockedConfigWired: boolean;
197
212
  complianceWired: boolean;
@@ -200,7 +215,7 @@ export interface WiringFacts {
200
215
  }
201
216
  /** Named view of the deps seats the static half reads (a `Pick` of the real {@link RunnerDeps} —
202
217
  * single-source shapes, no parallel hand-copied interface). */
203
- export type StaticWiringDeps = Pick<RunnerDeps, "onAsk" | "onQuestion" | "interactionPosture" | "onElicit" | "checkpointStore" | "sessionStore" | "backgroundAgentStore" | "onBackgroundChildEvent" | "lockedConfig" | "compliancePostureResolver" | "memoryScopeAdmission" | "retentionPolicy" | "permissionRuleStore">;
218
+ export type StaticWiringDeps = Pick<RunnerDeps, "onAsk" | "onQuestion" | "interactionPosture" | "onElicit" | "checkpointStore" | "sessionStore" | "backgroundAgentStore" | "onBackgroundChildEvent" | "lockedConfig" | "compliancePostureResolver" | "memoryScopeAdmission" | "retentionPolicy" | "permissionRuleStore" | "permissionRuleSyncWired" | "permissionRuleOrg">;
204
219
  /** Named view of the spec seats the static half reads (a `Pick` of the real {@link TaskSpec}). */
205
220
  export type StaticWiringSpec = Pick<TaskSpec, "onAsk" | "onQuestion" | "checkpointStore" | "durableApproval" | "mcp" | "interactiveTools" | "interactionPosture">;
206
221
  /**
@@ -86,7 +86,11 @@ export function deriveWiringManifest(facts) {
86
86
  parkLane,
87
87
  session: { store: manifestDurabilityOf(facts.sessionDurability) },
88
88
  fleet: { backgroundAgentStore: facts.backgroundAgentStoreWired, hostChildEventSink: facts.hostChildEventSinkWired },
89
- permissionRules: { storeWired: facts.permissionRuleStoreWired },
89
+ permissionRules: {
90
+ storeWired: facts.permissionRuleStoreWired,
91
+ syncWired: facts.permissionRuleSyncWired,
92
+ orgGoverned: facts.permissionRuleOrgGoverned,
93
+ },
90
94
  governance: {
91
95
  audience: "operator",
92
96
  lockedConfig: facts.lockedConfigWired,
@@ -179,6 +183,8 @@ export function describeStaticWiring(deps, spec = {}) {
179
183
  sessionDurability: resolveDeclaredDurability(deps.sessionStore, "sessionStore"),
180
184
  backgroundAgentStoreWired: deps.backgroundAgentStore !== undefined,
181
185
  permissionRuleStoreWired: deps.permissionRuleStore !== undefined,
186
+ permissionRuleSyncWired: deps.permissionRuleSyncWired === true,
187
+ permissionRuleOrgGoverned: deps.permissionRuleOrg !== undefined,
182
188
  hostChildEventSinkWired: deps.onBackgroundChildEvent !== undefined,
183
189
  lockedConfigWired: deps.lockedConfig !== undefined,
184
190
  complianceWired: deps.compliancePostureResolver !== undefined,