@sema-agent/core 7.4.0 → 7.5.1

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 (115) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/dist/agents/cascade.d.ts +2 -1
  3. package/dist/agents/peer-notices.d.ts +11 -1
  4. package/dist/agents/peer-session-drain.js +2 -0
  5. package/dist/agents/verify.d.ts +2 -1
  6. package/dist/core/ask-origin.d.ts +130 -0
  7. package/dist/core/ask-origin.js +35 -0
  8. package/dist/core/auto-mode-arming.d.ts +40 -1
  9. package/dist/core/auto-mode-arming.js +51 -3
  10. package/dist/core/auto-mode.d.ts +87 -10
  11. package/dist/core/auto-mode.js +34 -6
  12. package/dist/core/checkpoint-store.d.ts +41 -6
  13. package/dist/core/checkpoint-store.js +8 -0
  14. package/dist/core/hooks.d.ts +63 -19
  15. package/dist/core/hooks.js +37 -16
  16. package/dist/core/mcp.d.ts +47 -55
  17. package/dist/core/mcp.js +89 -31
  18. package/dist/core/park-selfcheck.js +3 -0
  19. package/dist/core/permission-rule-consent.d.ts +2 -11
  20. package/dist/core/permission-rule-consent.js +13 -62
  21. package/dist/core/permission-rule-org.d.ts +70 -54
  22. package/dist/core/permission-rule-org.js +47 -61
  23. package/dist/core/permission-rule-provider.d.ts +162 -0
  24. package/dist/core/permission-rule-provider.js +148 -0
  25. package/dist/core/permission-rule-session.d.ts +23 -19
  26. package/dist/core/permission-rule-session.js +5 -5
  27. package/dist/core/permission-rule-store.d.ts +46 -21
  28. package/dist/core/permission-rule-store.js +13 -6
  29. package/dist/core/permission-rule-sync.d.ts +2 -1
  30. package/dist/core/permission-rule-sync.js +11 -0
  31. package/dist/core/runner/assemble-result.d.ts +3 -2
  32. package/dist/core/runner/checkpoint-scope.d.ts +32 -0
  33. package/dist/core/runner/checkpoint-scope.js +4 -0
  34. package/dist/core/runner/contracts.d.ts +1878 -0
  35. package/dist/core/runner/contracts.js +1 -0
  36. package/dist/core/runner/denial-limit-arms.d.ts +57 -31
  37. package/dist/core/runner/denial-limit-arms.js +42 -17
  38. package/dist/core/runner/derived-route-fallback.d.ts +34 -0
  39. package/dist/core/runner/derived-route-fallback.js +16 -0
  40. package/dist/core/runner/prepare-acquire-reconcile.d.ts +1 -1
  41. package/dist/core/runner/prepare-announce-once.d.ts +83 -0
  42. package/dist/core/runner/prepare-announce-once.js +105 -0
  43. package/dist/core/runner/prepare-caps-and-workflow.d.ts +170 -0
  44. package/dist/core/runner/prepare-caps-and-workflow.js +255 -0
  45. package/dist/core/runner/prepare-config-doors.d.ts +2 -10
  46. package/dist/core/runner/prepare-defer-classify.d.ts +86 -0
  47. package/dist/core/runner/prepare-defer-classify.js +107 -0
  48. package/dist/core/runner/prepare-delegation-surface.d.ts +104 -0
  49. package/dist/core/runner/prepare-delegation-surface.js +144 -0
  50. package/dist/core/runner/prepare-execution-env.d.ts +54 -0
  51. package/dist/core/runner/prepare-execution-env.js +86 -0
  52. package/dist/core/runner/prepare-file-history.d.ts +95 -0
  53. package/dist/core/runner/prepare-file-history.js +383 -0
  54. package/dist/core/runner/prepare-hands-readface.d.ts +6 -8
  55. package/dist/core/runner/prepare-hands-readface.js +3 -3
  56. package/dist/core/runner/prepare-inherited-gate.d.ts +268 -0
  57. package/dist/core/runner/prepare-inherited-gate.js +266 -0
  58. package/dist/core/runner/prepare-listings.d.ts +77 -0
  59. package/dist/core/runner/prepare-listings.js +76 -0
  60. package/dist/core/runner/prepare-lsp.d.ts +55 -0
  61. package/dist/core/runner/prepare-lsp.js +27 -0
  62. package/dist/core/runner/prepare-memory.d.ts +1 -1
  63. package/dist/core/runner/prepare-offload-wrappers.d.ts +62 -0
  64. package/dist/core/runner/prepare-offload-wrappers.js +45 -0
  65. package/dist/core/runner/prepare-permission-rules.d.ts +132 -0
  66. package/dist/core/runner/prepare-permission-rules.js +140 -0
  67. package/dist/core/runner/prepare-project-context.d.ts +131 -0
  68. package/dist/core/runner/prepare-project-context.js +150 -0
  69. package/dist/core/runner/prepare-prompt-inputs.d.ts +138 -0
  70. package/dist/core/runner/prepare-prompt-inputs.js +141 -0
  71. package/dist/core/runner/prepare-protocol-tools.d.ts +91 -0
  72. package/dist/core/runner/prepare-protocol-tools.js +182 -0
  73. package/dist/core/runner/prepare-question-face.d.ts +119 -0
  74. package/dist/core/runner/prepare-question-face.js +83 -0
  75. package/dist/core/runner/prepare-run-refs.d.ts +89 -0
  76. package/dist/core/runner/prepare-run-refs.js +39 -0
  77. package/dist/core/runner/prepare-safety-scan.d.ts +3 -2
  78. package/dist/core/runner/prepare-task.d.ts +11 -1815
  79. package/dist/core/runner/prepare-task.js +138 -2542
  80. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +111 -0
  81. package/dist/core/runner/prepare-tool-disclosure-mount.js +219 -0
  82. package/dist/core/runner/prepare-wiring-manifest.d.ts +184 -0
  83. package/dist/core/runner/prepare-wiring-manifest.js +240 -0
  84. package/dist/core/runner/prepare-workspace-restore.d.ts +1 -27
  85. package/dist/core/runner/prepare-workspace-restore.js +1 -22
  86. package/dist/core/runner/rollback-stack.d.ts +32 -0
  87. package/dist/core/runner/rollback-stack.js +30 -0
  88. package/dist/core/runner/runtask.d.ts +11 -2
  89. package/dist/core/runner/runtask.js +27 -9
  90. package/dist/core/runner/workspace-path.d.ts +33 -0
  91. package/dist/core/runner/workspace-path.js +22 -0
  92. package/dist/core/sensitive-path-policy.d.ts +16 -0
  93. package/dist/core/sensitive-path-policy.js +1 -1
  94. package/dist/core/tool-policy.d.ts +57 -9
  95. package/dist/core/tool-policy.js +11 -0
  96. package/dist/core/types.d.ts +63 -51
  97. package/dist/core/wiring-manifest.d.ts +40 -3
  98. package/dist/core/wiring-manifest.js +4 -3
  99. package/dist/core/write-protect.d.ts +13 -2
  100. package/dist/core/write-protect.js +58 -29
  101. package/dist/engine/harness/types.d.ts +38 -16
  102. package/dist/engine/harness/types.js +25 -1
  103. package/dist/engine/session/session.d.ts +3 -11
  104. package/dist/index.d.ts +10 -6
  105. package/dist/index.js +9 -5
  106. package/dist/internal/harness.d.ts +1 -0
  107. package/dist/stores/file/adoption/adopt.d.ts +1 -1
  108. package/dist/stores/file/adoption/marker.d.ts +1 -1
  109. package/dist/stores/file/permission-rule-adopt.js +4 -3
  110. package/dist/stores/file/permission-rule-store.d.ts +65 -25
  111. package/dist/stores/file/permission-rule-store.js +215 -37
  112. package/dist/stores/file/task-list-store.d.ts +1 -1
  113. package/dist/tools/fs/read-face.d.ts +1 -1
  114. package/package.json +8 -2
  115. package/test/export-surface.snapshot.json +76 -28
@@ -0,0 +1 @@
1
+ export {};
@@ -1,7 +1,9 @@
1
- import { type AutoModeDecider, type AutoModeDenialLimitOptions, type AutoModeDenialTracker, type DenialLimitFallback } from "../auto-mode.js";
1
+ import { type AutoModeDecider, type AutoModeDenialLimitOptions, type AutoModeDenialTracker, type DenialLimitFallback, type DenialLimitFallbackFace, type UnarmedDenialLimitFallback } from "../auto-mode.js";
2
+ import { type AutoModeArmingRecipe } from "../auto-mode-arming.js";
2
3
  import type { PermissionResult, ResolvedAsk, ToolCallRequest } from "../tool-policy.js";
3
4
  import { type EngineNotice } from "../types.js";
4
- import type { Prepared } from "./prepare-task.js";
5
+ import { type AskOrigin } from "../ask-origin.js";
6
+ import type { Prepared } from "./contracts.js";
5
7
  /**
6
8
  * #548 — the classifier DENIAL-LIMIT arms of the tool gate's inherited (delegation) lane, extracted
7
9
  * from `prepareTask` as a phase module (design/238 D-7: extract, don't accrete). The gate's OWN
@@ -11,28 +13,43 @@ import type { Prepared } from "./prepare-task.js";
11
13
  * · the frozen classifier's judgment with the count-then-fallback step, shared by the two inherited
12
14
  * wrapper arms and the approved-edit re-check ({@link judgeInheritedClassifier});
13
15
  * · the post-resolution settlement of a fallback ask — a person's allow resets the streak, a deny
14
- * nobody made stops the run ({@link settleDenialLimitFallback}).
16
+ * nobody made stops the run ({@link settleDenialLimitFallback});
17
+ * · the request-side carry of the ask's origin word and its fallback member, with the auto-deny window
18
+ * armed at the hand-out to a live approver and nowhere else ({@link gateAskCarry} for the gate's own
19
+ * station, {@link inheritedAskCarry} for the three inherited-lane ones).
15
20
  * Each helper is pure over what prepare hands it; the fold/ask seams themselves stay in the runner.
16
21
  */
17
- /** An ancestor chain entry's classifier half as the arms read it: the decider and, beside it, the
18
- * per-run denial tracker (absent on an entry that counts nothing). */
22
+ /** An ancestor chain entry's classifier half as the arms read it: the decider, the per-run denial
23
+ * tracker beside it (absent on an entry that counts nothing), and — on a re-supplied entry — the
24
+ * ancestor's recorded arming recipe, whose denial-limit bounds the fresh tracker is built under. */
19
25
  export interface InheritedAutoMode {
20
26
  decider: AutoModeDecider;
21
27
  denialTracking?: AutoModeDenialTracker;
28
+ arming?: AutoModeArmingRecipe;
22
29
  }
23
30
  /**
24
31
  * A re-supplied chain entry (a cross-process redemption rebuilt its ancestor's classifier from the
25
32
  * recorded recipe — `rebuildAutoModeDecider` hands back a decider and its arming, nothing live)
26
33
  * carries no denial tracker, and an entry with a decider but no tracker would count nothing: the
27
34
  * rebuilt classifier could block without bound on the redeemed leg, the exact gap the limit closes.
28
- * Attach a FRESH tracker under this deployment's own bounds (a fresh count is the documented
29
- * cross-process semantics — the breaker restarts the same way). Entries that already carry one (the
30
- * in-process chain from a live ancestor) are kept BY IDENTITY; only a tracker-less armed entry is
31
- * re-wrapped (its `policy`, the identity the fold and the pass-through compare, is untouched).
32
- * ONE tracker per DECIDER identity, not per entry: a parent attaches its one decider to both its
33
- * hook entry and its caller-policy entry, and a count split across two trackers could alternate
34
- * forever without either reaching the bound. The tracker is minted lazily, so a bad bound refuses
35
- * only a prepare that actually has such an entry (the armed leg's own mint screens the knob first).
35
+ * Attach a FRESH tracker (a fresh COUNT is the documented cross-process semantics the breaker
36
+ * restarts the same way) under the ancestor's recorded BOUNDS tightened by this deployment's own
37
+ * (#556, `tightenDenialLimit` the same rule and the same call the arming fold makes, so the
38
+ * recipe's account of the criteria and the tracker's actual bounds cannot drift). Before #556 the
39
+ * bounds came from this deployment alone, so an ancestor that allowed three consecutive blocks
40
+ * before a person had to look could be redeemed into a leg that allowed five. An entry with no
41
+ * READABLE recipe leaves the deployment's own bounds in force there is no ancestor account to bind
42
+ * by, and manufacturing one would state criteria nobody chose.
43
+ *
44
+ * Entries that already carry a tracker (the in-process chain from a live ancestor) are kept BY
45
+ * IDENTITY; only a tracker-less armed entry is re-wrapped (its `policy`, the identity the fold and
46
+ * the pass-through compare, is untouched). ONE tracker per DECIDER identity, not per entry: a parent
47
+ * attaches its one decider to both its hook entry and its caller-policy entry, and a count split
48
+ * across two trackers could alternate forever without either reaching the bound. The bounds are
49
+ * therefore folded across EVERY entry naming that decider before any tracker is minted — two entries
50
+ * of one decider carrying different recipes get the strictest reading of the two rather than
51
+ * whichever the iteration happened to reach first. The mint stays lazy, so a bad bound refuses only a
52
+ * prepare that actually has such an entry (the armed leg's own mint screens the knob first).
36
53
  */
37
54
  export declare function attachRebuiltDenialTrackers<T extends {
38
55
  autoMode?: InheritedAutoMode;
@@ -41,7 +58,7 @@ export declare function attachRebuiltDenialTrackers<T extends {
41
58
  export type StopForDenialLimit = (info: {
42
59
  toolName: string;
43
60
  toolCallId: string;
44
- fallback: DenialLimitFallback;
61
+ fallback: DenialLimitFallbackFace;
45
62
  }) => void;
46
63
  /**
47
64
  * The gate's typed stop seat (`Prepared.gateStopRef`) and the one closure that fills it — the headless
@@ -65,20 +82,24 @@ export declare function createDenialLimitStop(opts: {
65
82
  export type InheritedAsk = Extract<PermissionResult, {
66
83
  action: "ask";
67
84
  }>;
68
- /**
69
- * The delegation half of the two classifier exclusions the LIVE gate enforces: an ask raised by a hook
70
- * (`decisionReason:"hook"`, engine-stamped, never self-supplied) and an ask minted by an explicit `ask`
71
- * permission rule (`matchedAskRule`) are exactly as out of the FROZEN classifier's reach as the live
72
- * one's — the person's / hook's standing decision travels with the frozen entry, and a frozen decider
73
- * resolving it would re-open the same hole one process over. An ask that already IS a denial-limit
74
- * fallback (a deeper ancestor's) is not re-judged either. Excluded asks fall through to the frozen
75
- * approver chain, the delegation lane's original chain.
76
- */
77
- export declare function frozenClassifierExcluded(d: {
78
- decisionReason?: string;
79
- matchedAskRule?: string;
80
- denialLimitFallback?: unknown;
81
- }): boolean;
85
+ /** The members of a surviving ask that ride onto the approval request with a COMPUTED value — what the
86
+ * four request mint stations (the gate's own and the three inherited-lane ones) spread after the seats
87
+ * they spell themselves: the ask's origin word and its denial-limit fallback. */
88
+ export interface AskRequestCarry {
89
+ origin?: AskOrigin;
90
+ denialLimitFallback?: DenialLimitFallback;
91
+ }
92
+ /** The GATE's own mint station: both members are read off the surviving decision, which is the gate's
93
+ * OWN snapshot (its stamp point spreads the policy's object and stamps the word onto the copy), so the
94
+ * origin is the engine-derived one and the member is the one the gate's classifier step minted. */
95
+ export declare function gateAskCarry(decision: InheritedAsk, liveApprover: boolean, tracker: AutoModeDenialTracker | undefined): AskRequestCarry;
96
+ /** An INHERITED-lane mint station (the two wrapper arms and the approved-edit re-check): the ask is an
97
+ * ancestor policy's own decision, never engine-stamped, so BOTH members come from the judgment
98
+ * {@link judgeInheritedClassifier} handed back — the word it derived under the station facts and the
99
+ * fallback member it captured — each read ONCE there and threaded here, never re-read off the
100
+ * caller-owned object (a stateful accessor could otherwise answer the card with one value and the
101
+ * settlement with another; a self-declared `origin` on the ancestor's decision is never read). */
102
+ export declare function inheritedAskCarry(origin: AskOrigin, fallback: UnarmedDenialLimitFallback | undefined, liveApprover: boolean, tracker: AutoModeDenialTracker | undefined): AskRequestCarry;
82
103
  /** What {@link judgeInheritedClassifier} hands back to the arm that called it. */
83
104
  export type InheritedClassifierJudgment<A extends InheritedAsk> =
84
105
  /** The frozen classifier allowed — the arm returns its own allow shape (with or without the ask's rewrite). */
@@ -92,12 +113,17 @@ export type InheritedClassifierJudgment<A extends InheritedAsk> =
92
113
  }
93
114
  /** Proceed to the frozen approver with `ask`: the incoming ask unchanged (classifier unavailable,
94
115
  * excluded, or not armed), or — at the bound — the fallback ask minted from it (`mintedHere`).
95
- * `fallback` is the member riding the ask either way (an incoming one is a deeper ancestor's). */
116
+ * `fallback` is the member riding the ask either way (an incoming one is a deeper ancestor's).
117
+ * `origin` is the ask's word under the station facts, threaded to the request mint: on the two
118
+ * unchanged-ask branches it is the SAME read the exclusion judged by (a caller-owned decision with a
119
+ * stateful accessor cannot answer the exclusion with one word and the card with another); on the
120
+ * bound branch it is derived from the re-spoken ask this station minted (its own object). */
96
121
  | {
97
122
  kind: "resolve";
98
123
  ask: A;
99
- fallback: DenialLimitFallback | undefined;
124
+ fallback: UnarmedDenialLimitFallback | undefined;
100
125
  mintedHere: boolean;
126
+ origin: AskOrigin;
101
127
  };
102
128
  /**
103
129
  * The ancestor's frozen classifier, run BEFORE its frozen approver in the ancestor's own gate order,
@@ -138,7 +164,7 @@ export declare function headlessDenyAtRecheck(r: FallbackResolution): boolean;
138
164
  * when the ask was not a fallback.
139
165
  */
140
166
  export declare function settleDenialLimitFallback(opts: {
141
- fallback: DenialLimitFallback | undefined;
167
+ fallback: UnarmedDenialLimitFallback | undefined;
142
168
  mintedHere: boolean;
143
169
  tracker: AutoModeDenialTracker | undefined;
144
170
  resolved: FallbackResolution;
@@ -1,19 +1,38 @@
1
- import { createAutoModeDenialTracker, denialLimitFallbackMessage, denialLimitSentence } from "../auto-mode.js";
1
+ import { createAutoModeDenialTracker, denialLimitFallbackMessage, denialLimitSentence, unarmedWindow } from "../auto-mode.js";
2
+ import { sanitizeAutoModeArmingRecipe, tightenDenialLimit } from "../auto-mode-arming.js";
2
3
  import { deliverEngineNotice } from "../types.js";
3
4
  import { inlineUntrusted } from "../untrusted-text.js";
5
+ import { askOriginOf, classifierMayAnswer } from "../ask-origin.js";
4
6
  export function attachRebuiltDenialTrackers(entries, denialLimit) {
5
- const rebuiltTrackers = new Map();
7
+ if (entries === undefined)
8
+ return undefined;
9
+ const needsTracker = (pc) => pc.autoMode !== undefined && pc.autoMode.denialTracking === undefined;
10
+ const boundsFor = new Map();
11
+ const liveTrackers = new Map();
12
+ for (const pc of entries) {
13
+ if (pc.autoMode?.denialTracking !== undefined)
14
+ liveTrackers.set(pc.autoMode.decider, pc.autoMode.denialTracking);
15
+ }
16
+ for (const pc of entries) {
17
+ if (!needsTracker(pc) || liveTrackers.has(pc.autoMode.decider))
18
+ continue;
19
+ const recorded = sanitizeAutoModeArmingRecipe(pc.autoMode.arming);
20
+ if (recorded === undefined)
21
+ continue;
22
+ const merged = tightenDenialLimit(recorded.denialLimit, denialLimit);
23
+ const seen = boundsFor.get(pc.autoMode.decider);
24
+ boundsFor.set(pc.autoMode.decider, seen === undefined ? merged : tightenDenialLimit(seen, merged));
25
+ }
26
+ const rebuiltTrackers = new Map(liveTrackers);
6
27
  const trackerForRebuilt = (decider) => {
7
28
  let t = rebuiltTrackers.get(decider);
8
29
  if (t === undefined) {
9
- t = createAutoModeDenialTracker(denialLimit);
30
+ t = createAutoModeDenialTracker(boundsFor.get(decider) ?? denialLimit);
10
31
  rebuiltTrackers.set(decider, t);
11
32
  }
12
33
  return t;
13
34
  };
14
- return entries?.map((pc) => pc.autoMode !== undefined && pc.autoMode.denialTracking === undefined
15
- ? { ...pc, autoMode: { ...pc.autoMode, denialTracking: trackerForRebuilt(pc.autoMode.decider) } }
16
- : pc);
35
+ return entries.map((pc) => (needsTracker(pc) ? { ...pc, autoMode: { ...pc.autoMode, denialTracking: trackerForRebuilt(pc.autoMode.decider) } } : pc));
17
36
  }
18
37
  export function createDenialLimitStop(opts) {
19
38
  const gateStopRef = {};
@@ -34,13 +53,23 @@ export function createDenialLimitStop(opts) {
34
53
  };
35
54
  return { gateStopRef, stopForDenialLimit };
36
55
  }
37
- export function frozenClassifierExcluded(d) {
38
- return d.decisionReason === "hook" || d.matchedAskRule !== undefined || d.denialLimitFallback !== undefined;
56
+ const INHERITED_STATION_FACTS = { contentQuestion: false, markedUnresolvable: false, org: undefined };
57
+ function fallbackCarry(fallback, liveApprover, tracker) {
58
+ if (fallback === undefined)
59
+ return {};
60
+ return { denialLimitFallback: liveApprover && tracker !== undefined ? tracker.armTimedWindow(fallback) : unarmedWindow(fallback) };
61
+ }
62
+ export function gateAskCarry(decision, liveApprover, tracker) {
63
+ return { ...(decision.origin !== undefined ? { origin: decision.origin } : {}), ...fallbackCarry(decision.denialLimitFallback, liveApprover, tracker) };
64
+ }
65
+ export function inheritedAskCarry(origin, fallback, liveApprover, tracker) {
66
+ return { origin, ...fallbackCarry(fallback, liveApprover, tracker) };
39
67
  }
40
68
  export async function judgeInheritedClassifier(opts) {
41
69
  const { autoMode, ask, req } = opts;
42
- if (autoMode === undefined || frozenClassifierExcluded(ask))
43
- return { kind: "resolve", ask, fallback: ask.denialLimitFallback, mintedHere: false };
70
+ const origin = askOriginOf(ask, INHERITED_STATION_FACTS);
71
+ if (autoMode === undefined || !classifierMayAnswer(origin))
72
+ return { kind: "resolve", ask, fallback: ask.denialLimitFallback, mintedHere: false, origin };
44
73
  const verdict = await autoMode.decider
45
74
  .decide({ req, ...(ask.message !== undefined ? { askMessage: ask.message } : {}) }, opts.signal)
46
75
  .catch(() => ({ kind: "unavailable", cause: "error" }));
@@ -49,7 +78,7 @@ export async function judgeInheritedClassifier(opts) {
49
78
  return { kind: "allow" };
50
79
  }
51
80
  if (verdict.kind !== "block")
52
- return { kind: "resolve", ask, fallback: ask.denialLimitFallback, mintedHere: false };
81
+ return { kind: "resolve", ask, fallback: ask.denialLimitFallback, mintedHere: false, origin };
53
82
  const reason = verdict.reason ? inlineUntrusted(verdict.reason) : "";
54
83
  const category = verdict.category ? inlineUntrusted(verdict.category) : "";
55
84
  const tracked = autoMode.denialTracking?.recordBlock();
@@ -64,12 +93,8 @@ export async function judgeInheritedClassifier(opts) {
64
93
  };
65
94
  }
66
95
  const fallback = tracked.fallback;
67
- return {
68
- kind: "resolve",
69
- ask: { ...ask, message: denialLimitFallbackMessage(fallback, reason || category || req.toolName), decisionReason: "classifier", requiresRealApproval: true, denialLimitFallback: fallback },
70
- fallback,
71
- mintedHere: true,
72
- };
96
+ const minted = { ...ask, message: denialLimitFallbackMessage(fallback, reason || category || req.toolName), decisionReason: "classifier", requiresRealApproval: true, denialLimitFallback: fallback };
97
+ return { kind: "resolve", ask: minted, fallback, mintedHere: true, origin: askOriginOf(minted, INHERITED_STATION_FACTS) };
73
98
  }
74
99
  export function headlessDenyAtFold(r) {
75
100
  return r.resolution === "no_approver" || r.resolution === "blanket_allow_refused";
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The derived-leg route pre-flight shared by prepareTask's two derived seats: the compaction-summary model (the
3
+ * driver's own seat, resolved before the offload prelude) and the auto-mode classifier model (the caps-and-workflow
4
+ * phase's arming). Moved whole out of the orchestrator so the phase reaches DOWN for it instead of the two callers
5
+ * spelling it twice. The judgment law itself lives in the brain's route adjudicator; this is the one guarded seat
6
+ * around it.
7
+ */
8
+ import type { Model } from "../../internal/llm.js";
9
+ import type { RunnerDeps, TaskSpec } from "../types.js";
10
+ /**
11
+ * The derived-leg pairing pre-flight as ONE guarded seat: judge the derived model where its route
12
+ * differs from the primary's, announce the fallback on a broken pairing, and ABSTAIN (keep the
13
+ * derived model) on ANY throw. The pre-flight is advisory — the brain's request gate re-runs the
14
+ * same law — so neither a throwing judge face nor a garbage catalog value (the route-identity
15
+ * normalization throws a TypeError on a non-string baseUrl) may widen into "task preparation
16
+ * failed": at the compaction seat the session is already acquired and the throw-cleanup contract
17
+ * is not yet armed, so a throw here would leak the acquired session view, and the garbage value
18
+ * still earns its loud refusal at the request gate the moment the derived leg is actually used.
19
+ * Returns whether the seat should fall back to the primary model.
20
+ */
21
+ export declare function derivedRouteFallsBack(args: {
22
+ seat: string;
23
+ derived: Model;
24
+ primary: Model;
25
+ brain: RunnerDeps["brain"];
26
+ getApiKeyAndHeaders: TaskSpec["getApiKeyAndHeaders"];
27
+ onNotice: RunnerDeps["onNotice"];
28
+ /** #433 — the run this seat resolution belongs to; carried onto the notice as its routing key
29
+ * (the audience stays operator: correlation, not entitlement). Absent ⇒ nothing is fabricated. */
30
+ sessionId?: string;
31
+ /** #499 — the INVOCATION this seat resolution belongs to. The session cannot stand in for it: two
32
+ * runs of one session that both fall back on the same seat mint otherwise byte-identical lines. */
33
+ runId?: string;
34
+ }): Promise<boolean>;
@@ -0,0 +1,16 @@
1
+ import { adjudicateDerivedRoute, fallbackToPrimaryNotice, sameRouteIdentity } from "../../brain/route-adjudicator.js";
2
+ import { deliverEngineNotice } from "../types.js";
3
+ export async function derivedRouteFallsBack(args) {
4
+ try {
5
+ if (sameRouteIdentity(args.derived, args.primary))
6
+ return false;
7
+ const verdict = await adjudicateDerivedRoute({ brain: args.brain, model: args.derived, getApiKeyAndHeaders: args.getApiKeyAndHeaders });
8
+ if (verdict === undefined || verdict.ok)
9
+ return false;
10
+ deliverEngineNotice(args.onNotice, fallbackToPrimaryNotice({ seat: args.seat, from: args.derived.id, to: args.primary.id, verdict, ...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}), ...(args.runId !== undefined ? { runId: args.runId } : {}) }));
11
+ return true;
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ }
@@ -22,7 +22,7 @@ import { StoredSession } from "../session.js";
22
22
  import type { AcquiredSession, SessionStore } from "../session.js";
23
23
  import { type RecoveredOrphan } from "../session-reconcile.js";
24
24
  import type { TaskSpec, ToolEffect } from "../types.js";
25
- import type { PrepareResume } from "./prepare-task.js";
25
+ import type { PrepareResume } from "./contracts.js";
26
26
  export interface PrepareAcquireReconcileInput {
27
27
  /** borrowed-mutable (service port) — the deployment's session store. This phase's verbs on it are
28
28
  * the acquire/forget half of the session resource protocol: `acquire` per attempt, `forget` to
@@ -0,0 +1,83 @@
1
+ import type { AnnouncedListingSnapshot, AnnouncedListingSnapshotInput, AnnouncedMcpFailure } from "../../internal/harness.js";
2
+ import type { McpDroppedTool, McpServerStatus } from "../mcp.js";
3
+ import type { RunnerDeps } from "../types.js";
4
+ /** Upper bound on the advisory-digest arm kept in the record (oldest dropped past it). A digest is
5
+ * 64 hex chars; a session accumulates a handful of distinct advisories, never hundreds — the bound
6
+ * keeps a pathological deployment (a warning whose text embeds a per-run value) from growing the
7
+ * snapshot without limit while still holding every realistic set. */
8
+ export declare const ANNOUNCE_ONCE_MAX_DIGESTS = 200;
9
+ /** Stable digest of one announcement: `sha256(lane \n text)` as hex. Opaque on the wire; only ever
10
+ * compared for equality. */
11
+ export declare function announceDigest(lane: string, text: string): string;
12
+ /** The digest of an operator-lane warning: its `code` (empty when uncoded) and message. The message is
13
+ * passed through the secret-tier redaction FIRST: an advisory that embeds a credential (a misconfigured
14
+ * header, a URL with userinfo) must be recognized as the same advisory when the credential rotates, and
15
+ * the record must never persist a secret-bearing text's exact digest either — the digest is opaque, but
16
+ * an equality oracle on a secret is still an oracle. The forwarded line itself is untouched (the sink
17
+ * owns its own redaction posture); only the identity is judged on the redacted form. */
18
+ export declare function warningDigest(err: unknown): string;
19
+ /** The digest of one model-facing dropped-tool entry (server, tool, reason — the three fields the
20
+ * frame prints). */
21
+ export declare function droppedToolDigest(d: McpDroppedTool): string;
22
+ /** `existing ∪ add`, insertion-ordered, bounded to {@link ANNOUNCE_ONCE_MAX_DIGESTS} (oldest first
23
+ * out). Returns a NEW array; the input arrays are never mutated. */
24
+ export declare function withAnnounceDigests(existing: readonly string[] | undefined, add: readonly string[]): string[];
25
+ /** The session face this ledger needs: the nearest announced snapshot and the append. */
26
+ export interface AnnounceOnceSession {
27
+ getAnnouncedListing(): Promise<AnnouncedListingSnapshot | undefined>;
28
+ appendAnnouncedListing(snapshot: AnnouncedListingSnapshotInput): Promise<string>;
29
+ }
30
+ /** The run-local ledger — see the module header for the two commit disciplines it serves. */
31
+ export interface AnnounceOnceLedger {
32
+ /** The nearest announced snapshot on the branch as read at prepare (undefined = pre-entry
33
+ * session). Exposed so the listing seed can reuse this ONE read instead of walking the branch again. */
34
+ readonly entry: AnnouncedListingSnapshot | undefined;
35
+ /** Whether the branch read SUCCEEDED (a pre-entry session reads as `undefined` and IS recovered; a
36
+ * throwing read is not). While false, no whole-snapshot write may happen on this leg — the nearest
37
+ * `announced_listing` has snapshot semantics, so a write seeded from a failed read would REPLACE
38
+ * the branch's real record with the few arms this leg happens to know, hiding every other arm.
39
+ * A skipped write degrades to a duplicate announcement next run, never a loss. The run loop
40
+ * reads the same flag (`Prepared.announcedSnapshotRecovered`) before its own once-arm flush. */
41
+ readonly recovered: boolean;
42
+ /** The operator-lane sink, deduplicated: a warning whose `(code, message)` digest is already in
43
+ * the record is dropped; a new one is forwarded to `deps.onError` and recorded. With no sink wired
44
+ * nothing is recorded either — "announced" means delivered. Same signature as `RunnerDeps.onError`. */
45
+ onError: NonNullable<RunnerDeps["onError"]>;
46
+ /** The MODEL-face pending set for this run: the currently failed servers (name/error neutralized +
47
+ * bounded at intake, the reminder's own bounds) MINUS the pairs already announced on this branch.
48
+ * As a side effect the record's `mcpFailed` arm is pruned to the pairs that are STILL failing —
49
+ * a recovered server leaves the record silently. Call once per prepare. */
50
+ mcpFailuresToAnnounce(statuses: ReadonlyArray<McpServerStatus>): AnnouncedMcpFailure[];
51
+ /** Remove, IN PLACE, the dropped-tool entries whose model-facing digest is already recorded (the
52
+ * frame was delivered on an earlier run of this session). The operator line about each entry is a
53
+ * separate digest — run that loop BEFORE pruning so both faces are judged on the full list. */
54
+ pruneDroppedTools(dropped: McpDroppedTool[]): void;
55
+ /** The whole announced snapshot as it stands — the seed for the run loop's `announcedListingsRef`
56
+ * mirror. Listing arms (agents/skills/models) are copied from ONE source, the checkpoint mirror
57
+ * when it carries any of them, else the entry (rung-pure: a roster and its catalog come from the
58
+ * same commit); the once-per-session arms come from their own rung selection. Present-only: an
59
+ * empty arm is omitted, so a session that never announced anything serializes no
60
+ * `announcedListings` at all. The listing arms are a state-only carry: a commit of any ONE arm
61
+ * rewrites the nearest snapshot whole, so the other arms must already be on the object or the
62
+ * commit erases them from the branch's record. */
63
+ arms(): AnnouncedListingSnapshot;
64
+ /** End-of-prepare settlement: fold the operator-lane digests recorded during prepare into `ref`
65
+ * (the mirror the run loop and the checkpoint serializer read) and, when the record advanced,
66
+ * persist the whole snapshot — the listing arms as they stand on `ref` (or as read at prepare),
67
+ * plus the two arms this module owns. Best-effort: an append failure degrades to a duplicate
68
+ * announcement on the next run, never a lost one. */
69
+ settle(session: AnnounceOnceSession, ref: AnnouncedListingSnapshotInput): Promise<void>;
70
+ }
71
+ /**
72
+ * Load the ledger for this prepare. Read ladder for the two arms: the resume checkpoint's mirror
73
+ * first (cut atomically with the suspend; the entry is a best-effort write that can lag it), then
74
+ * the nearest `announced_listing` carrier on the branch. A snapshot without the arms (an entry
75
+ * written before they existed) reads as the empty set — additive schema, same posture as the
76
+ * checkpoint's own listing arms. A session read failure also reads as empty: the worst case of
77
+ * an unseeded ledger is a duplicate announcement, never a lost one.
78
+ */
79
+ export declare function loadAnnounceOnceLedger(session: AnnounceOnceSession, checkpointMirror: AnnouncedListingSnapshot | undefined, onError: RunnerDeps["onError"]): Promise<AnnounceOnceLedger>;
80
+ /** The suspend-time projection of the run loop's announced mirror onto the checkpoint field:
81
+ * present-only arms, copied; `undefined` when nothing was announced on any face (the resume seed
82
+ * then falls to the lower rungs — unchanged contract). */
83
+ export declare function serializeAnnouncedListings(ref: AnnouncedListingSnapshotInput): AnnouncedListingSnapshot | undefined;
@@ -0,0 +1,105 @@
1
+ import { createHash } from "node:crypto";
2
+ import { redactSecrets } from "../untrusted-egress.js";
3
+ import { inlineUntrusted } from "../untrusted-text.js";
4
+ export const ANNOUNCE_ONCE_MAX_DIGESTS = 200;
5
+ const DROPPED_TOOL_MODEL_LANE = "mcp_dropped_tools";
6
+ export function announceDigest(lane, text) {
7
+ return createHash("sha256").update(lane).update("\n").update(text).digest("hex");
8
+ }
9
+ export function warningDigest(err) {
10
+ const e = err;
11
+ const code = typeof e?.code === "string" ? e.code : "";
12
+ const message = err instanceof Error ? err.message : String(err);
13
+ return announceDigest(code, redactSecrets(message));
14
+ }
15
+ export function droppedToolDigest(d) {
16
+ return announceDigest(DROPPED_TOOL_MODEL_LANE, `${d.server}\n${d.tool}\n${d.reason}`);
17
+ }
18
+ export function withAnnounceDigests(existing, add) {
19
+ const out = [...(existing ?? [])];
20
+ const seen = new Set(out);
21
+ for (const d of add) {
22
+ if (seen.has(d))
23
+ continue;
24
+ seen.add(d);
25
+ out.push(d);
26
+ }
27
+ return out.length > ANNOUNCE_ONCE_MAX_DIGESTS ? out.slice(out.length - ANNOUNCE_ONCE_MAX_DIGESTS) : out;
28
+ }
29
+ function sameFailure(a, b) {
30
+ return a.name === b.name && a.error === b.error;
31
+ }
32
+ export async function loadAnnounceOnceLedger(session, checkpointMirror, onError) {
33
+ let recovered = true;
34
+ const entry = await session.getAnnouncedListing().catch(() => {
35
+ recovered = false;
36
+ return undefined;
37
+ });
38
+ const seed = checkpointMirror?.mcpFailed !== undefined || checkpointMirror?.advisories !== undefined ? checkpointMirror : entry;
39
+ const listingSrc = checkpointMirror?.agents !== undefined || checkpointMirror?.skills !== undefined || checkpointMirror?.models !== undefined ? checkpointMirror : entry;
40
+ let mcpFailed = (seed?.mcpFailed ?? []).map((f) => ({ ...f }));
41
+ const advisories = new Set(seed?.advisories ?? []);
42
+ let dirty = false;
43
+ return {
44
+ entry,
45
+ recovered,
46
+ onError: (err, ctx) => {
47
+ if (onError === undefined)
48
+ return;
49
+ const digest = warningDigest(err);
50
+ if (advisories.has(digest))
51
+ return;
52
+ onError(err, ctx);
53
+ advisories.add(digest);
54
+ dirty = true;
55
+ },
56
+ mcpFailuresToAnnounce: (statuses) => {
57
+ const current = statuses
58
+ .filter((s) => s.status === "failed")
59
+ .map((s) => ({ name: inlineUntrusted(s.name, 160), ...(s.error !== undefined ? { error: inlineUntrusted(s.error, 240) } : {}) }));
60
+ const carried = mcpFailed.filter((r) => current.some((c) => sameFailure(c, r)));
61
+ if (carried.length !== mcpFailed.length)
62
+ dirty = true;
63
+ mcpFailed = carried;
64
+ return current.filter((c) => !carried.some((r) => sameFailure(c, r)));
65
+ },
66
+ pruneDroppedTools: (dropped) => {
67
+ for (let i = dropped.length - 1; i >= 0; i--) {
68
+ if (advisories.has(droppedToolDigest(dropped[i])))
69
+ dropped.splice(i, 1);
70
+ }
71
+ },
72
+ arms: () => ({
73
+ ...(listingSrc?.agents !== undefined ? { agents: [...listingSrc.agents] } : {}),
74
+ ...(listingSrc?.skills !== undefined ? { skills: [...listingSrc.skills] } : {}),
75
+ ...(listingSrc?.models !== undefined ? { models: [...listingSrc.models] } : {}),
76
+ ...(mcpFailed.length > 0 ? { mcpFailed: mcpFailed.map((f) => ({ ...f })) } : {}),
77
+ ...(advisories.size > 0 ? { advisories: withAnnounceDigests(undefined, [...advisories]) } : {}),
78
+ }),
79
+ settle: async (s, ref) => {
80
+ if (advisories.size > 0)
81
+ ref.advisories = withAnnounceDigests(undefined, [...advisories]);
82
+ if (!dirty || !recovered)
83
+ return;
84
+ await s
85
+ .appendAnnouncedListing({
86
+ ...(ref.agents !== undefined ? { agents: ref.agents } : {}),
87
+ ...(ref.skills !== undefined ? { skills: ref.skills } : {}),
88
+ ...(ref.models !== undefined ? { models: ref.models } : {}),
89
+ ...(ref.mcpFailed !== undefined && ref.mcpFailed.length > 0 ? { mcpFailed: ref.mcpFailed } : {}),
90
+ ...(ref.advisories !== undefined && ref.advisories.length > 0 ? { advisories: ref.advisories } : {}),
91
+ })
92
+ .catch(() => undefined);
93
+ },
94
+ };
95
+ }
96
+ export function serializeAnnouncedListings(ref) {
97
+ const out = {
98
+ ...(ref.agents !== undefined ? { agents: [...ref.agents] } : {}),
99
+ ...(ref.skills !== undefined ? { skills: [...ref.skills] } : {}),
100
+ ...(ref.models !== undefined ? { models: [...ref.models] } : {}),
101
+ ...(ref.mcpFailed !== undefined ? { mcpFailed: ref.mcpFailed.map((f) => ({ ...f })) } : {}),
102
+ ...(ref.advisories !== undefined ? { advisories: [...ref.advisories] } : {}),
103
+ };
104
+ return Object.keys(out).length > 0 ? out : undefined;
105
+ }