@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
@@ -99,6 +99,23 @@ class AutoModeTimeout extends Error {
99
99
  super("auto-mode classify timeout");
100
100
  }
101
101
  }
102
+ export function readDenialLimitFallback(v) {
103
+ if (typeof v !== "object" || v === null)
104
+ return undefined;
105
+ const o = v;
106
+ const count = (x) => typeof x === "number" && Number.isFinite(x) && x >= 0;
107
+ if (!count(o["consecutive"]) || !count(o["total"]) || !count(o["autoDenyAfterMs"]))
108
+ return undefined;
109
+ if (o["limit"] !== "consecutive" && o["limit"] !== "total")
110
+ return undefined;
111
+ return { consecutive: o["consecutive"], total: o["total"], limit: o["limit"], autoDenyAfterMs: o["autoDenyAfterMs"] };
112
+ }
113
+ export function unarmedWindow(fallback) {
114
+ return { ...countsOf(fallback), autoDenyAfterMs: 0 };
115
+ }
116
+ function countsOf(fallback) {
117
+ return { consecutive: fallback.consecutive, total: fallback.total, limit: fallback.limit };
118
+ }
102
119
  const MAX_TIMER_DELAY_MS = 2_147_483_647;
103
120
  export function createAutoModeDenialTracker(opts = {}) {
104
121
  const bound = (name, v, dflt) => {
@@ -121,6 +138,8 @@ export function createAutoModeDenialTracker(opts = {}) {
121
138
  let consecutive = 0;
122
139
  let total = 0;
123
140
  let timedFallbackShown = false;
141
+ let windowArmedAt;
142
+ let minted = new WeakSet();
124
143
  return {
125
144
  recordBlock() {
126
145
  consecutive += 1;
@@ -128,23 +147,32 @@ export function createAutoModeDenialTracker(opts = {}) {
128
147
  if (consecutive < maxConsecutive && total < maxTotal)
129
148
  return { limitReached: false };
130
149
  const totalTripped = total >= maxTotal;
131
- const windowMs = timedFallbackShown || totalTripped ? 0 : autoDenyAfterMs;
132
- const fallback = { consecutive, total, limit: totalTripped ? "total" : "consecutive", autoDenyAfterMs: windowMs };
150
+ const fallback = { consecutive, total, limit: totalTripped ? "total" : "consecutive" };
151
+ minted.add(fallback);
133
152
  if (totalTripped) {
134
153
  consecutive = 0;
135
154
  total = 0;
136
155
  timedFallbackShown = false;
137
- }
138
- else if (windowMs > 0) {
139
- timedFallbackShown = true;
156
+ windowArmedAt = undefined;
157
+ minted = new WeakSet();
158
+ minted.add(fallback);
140
159
  }
141
160
  return { limitReached: true, fallback };
142
161
  },
162
+ armTimedWindow(fallback) {
163
+ if (!minted.delete(fallback) || fallback.limit === "total" || timedFallbackShown || autoDenyAfterMs === 0)
164
+ return unarmedWindow(fallback);
165
+ timedFallbackShown = true;
166
+ windowArmedAt = Date.now();
167
+ return { ...countsOf(fallback), autoDenyAfterMs };
168
+ },
143
169
  recordAllow() {
144
170
  consecutive = 0;
145
171
  timedFallbackShown = false;
172
+ windowArmedAt = undefined;
173
+ minted = new WeakSet();
146
174
  },
147
- snapshot: () => ({ consecutive, total, timedFallbackShown }),
175
+ snapshot: () => ({ consecutive, total, timedFallbackShown, ...(windowArmedAt !== undefined ? { windowArmedAt } : {}) }),
148
176
  };
149
177
  }
150
178
  export function denialLimitFallbackMessage(fallback, latestBlockedAction) {
@@ -439,7 +439,9 @@ export declare function buildRiskDescriptor(input: {
439
439
  * refuses to redeem such a row (the governed boundary must not vanish with a deployment's wiring).
440
440
  */
441
441
  export interface RealApprovalGateBit {
442
- origin: "org_rule" | "org_unavailable" | "policy";
442
+ /** The three members of the closed ask-origin set that can mint the bit — derived FROM that set, so
443
+ * the two vocabularies cannot drift into two spellings of one word. */
444
+ origin: Extract<import("./ask-origin.js").AskOrigin, "org_rule" | "org_unavailable" | "policy">;
443
445
  }
444
446
  export type CheckpointGate =
445
447
  /** F4: a human (or any external authority) must allow/deny a pending tool call. design/80 §D-E:
@@ -803,6 +805,28 @@ export type PendingAction = {
803
805
  * consent record threads it as `prepareCardApproval`'s `execCwd`. Present only beside
804
806
  * {@link ruleOffers} when the run had a tracked cwd; echo-only, never a control input. */
805
807
  execCwd?: string;
808
+ /** #557 (additive; no checkpoint-version bump — the `previewWithheld` precedent: an optional
809
+ * field an older reader ignores and the resume path never reads) — the PARK twin of
810
+ * `AskRequest.requiresRealApproval`: present (`true`) ⇔ this parked ask demanded real human
811
+ * judgment (the gate's `realApproval` bit was minted — the SAME fact, restated where a card
812
+ * reader looks for it, so an inbox needs no gate-kind decoding to say "only a person can clear
813
+ * this"). Absent otherwise (never `false`). Display metadata: the resume belts keep reading the
814
+ * gate's own bit. */
815
+ requiresRealApproval?: true;
816
+ /** #557 (additive, same precedent) — the PARK twin of `AskRequest.denialLimitFallback`: present
817
+ * ⇔ this parked ask is the classifier DENIAL-LIMIT fallback, carrying the counts that tripped
818
+ * the bound. Its `autoDenyAfterMs` is ALWAYS `0` on a row: the window is a fact of the ask's
819
+ * route, armed only at a hand-out to a live approver, and nothing counts down on the parked lane
820
+ * — a park's expiry is the row's own `deadline`/ttl, the engine arms NO timer off this row, and
821
+ * its settlement never reads as the fallback's auto-deny (see `tool_end.autoDenied`). A durable
822
+ * card renders the counts and the limit; it has no countdown to render. Echo-only. */
823
+ denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
824
+ /** (additive, same precedent) — the PARK twin of `AskRequest.origin`: WHICH AUTHORITY raised the
825
+ * parked ask, one word from the closed {@link import("./ask-origin.js").AskOrigin} set, the same
826
+ * word the synchronous card is rendered by (engine-stamped at the gate, never a policy's claim).
827
+ * Written only when it is a member of the set; a pre-existing row reads absent (unreported).
828
+ * Display metadata — the resume belts keep reading the gate's own bit. */
829
+ origin?: import("./ask-origin.js").AskOrigin;
806
830
  /**
807
831
  * design/80 D-1 §2 (slice 1a.2): the server-minted **opaque** boundInputHash of {@link args} — a
808
832
  * SHA-256 (hex) via {@link import("./canonical-json.js").boundInputHashOf}, computed ONCE here at
@@ -1157,11 +1181,7 @@ export interface CheckpointState {
1157
1181
  * catalog left the tool description with Q4). Absent (older checkpoint / never announced) ⇒ drift
1158
1182
  * undetectable, silent (conservative, same B-5 direction).
1159
1183
  */
1160
- announcedListings?: {
1161
- agents?: string[];
1162
- skills?: string[];
1163
- models?: string[];
1164
- };
1184
+ announcedListings?: import("../internal/harness.js").AnnouncedListingSnapshot;
1165
1185
  /**
1166
1186
  * The git-status frame's announced `(kind, hash)` state at suspend (env-tail migration: the
1167
1187
  * run-loop-maintained mirror, advanced at every frame receipt). The LOWEST rung of the resume
@@ -1842,6 +1862,21 @@ export interface CheckpointSummary {
1842
1862
  * projection's own materials (a pre-#457 row therefore reads absent, which is honest: unreported).
1843
1863
  * ECHO-ONLY display metadata; nothing gates on it. */
1844
1864
  previewWithheld?: "oversize" | "unavailable";
1865
+ /** #557 — the row's `PendingAction.tool_approval.requiresRealApproval`, projected so the ONE-CALL
1866
+ * inbox can label "only a person can clear this" without an N+1 `get`. `true` or OMITTED (the
1867
+ * {@link toolCallId} absence-shape contract); pure echo of the persisted bit — a pre-#557 row reads
1868
+ * absent, which is honest (unreported), never a claim that budget may clear it. */
1869
+ requiresRealApproval?: true;
1870
+ /** #557 — the row's `PendingAction.tool_approval.denialLimitFallback`, projected BESIDE the preview
1871
+ * for the same one-call reason: a card that shows the fallback's counts and limit must not re-open
1872
+ * the row to learn them (`autoDenyAfterMs` is `0` on every row — see the row member). Pure echo;
1873
+ * OMITTED when the row carries none. */
1874
+ denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
1875
+ /** The row's `PendingAction.tool_approval.origin`, projected for the same one-call reason: a card
1876
+ * rendered by the ask's origin word must not re-open the row to learn it. Echoed ONLY when the stored
1877
+ * value is a member of the closed set ({@link import("./ask-origin.js").isAskOrigin}) — a bad value
1878
+ * reads as absent, never as a word. OMITTED when the row carries none (a pre-existing row). */
1879
+ origin?: import("./ask-origin.js").AskOrigin;
1845
1880
  /**
1846
1881
  * #438: PRESENCE ONLY — this pending `tool_approval` row's payload carries at least one DIRECTIONAL
1847
1882
  * format control (the {@link import("./tool-policy.js").AskRequest.hasBidiControls} character class;
@@ -2,6 +2,8 @@ import { randomBytes, randomUUID } from "node:crypto";
2
2
  import { uuidv7 } from "../internal/harness.js";
3
3
  import { PROBE_CAUSE_PATH_MAX, inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
4
4
  import { carriesBidiControls } from "./tool-policy.js";
5
+ import { readDenialLimitFallback } from "./auto-mode.js";
6
+ import { isAskOrigin } from "./ask-origin.js";
5
7
  import { renderUntrustedCommandText, stripFormatCharacters } from "./permission-rule-model.js";
6
8
  import { redactSecrets } from "./untrusted-egress.js";
7
9
  import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
@@ -262,6 +264,12 @@ export function summarizeCheckpoint(cp) {
262
264
  : {}),
263
265
  ...(tool?.preview !== undefined ? { preview: tool.preview } : {}),
264
266
  ...(tool?.previewWithheld === "oversize" || tool?.previewWithheld === "unavailable" ? { previewWithheld: tool.previewWithheld } : {}),
267
+ ...(tool?.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
268
+ ...(() => {
269
+ const fb = readDenialLimitFallback(tool?.denialLimitFallback);
270
+ return fb !== undefined ? { denialLimitFallback: fb } : {};
271
+ })(),
272
+ ...(tool !== undefined && isAskOrigin(tool.origin) ? { origin: tool.origin } : {}),
265
273
  ...(hasBidi ? { hasBidiControls: true } : {}),
266
274
  };
267
275
  }
@@ -962,6 +962,19 @@ export interface ToolGateResult {
962
962
  * `tool_end` frame — the machine-readable "why was this refused" a consumer classifies on.
963
963
  */
964
964
  resolution?: import("./tool-policy.js").AskDenyResolution;
965
+ /**
966
+ * `true` ⇔ this BLOCK is the classifier denial-limit fallback's own auto-deny: the ask's window
967
+ * (`AskRequest.denialLimitFallback.autoDenyAfterMs`) elapsed with nobody answering and the resolver
968
+ * refused it itself — the same fact the `permissionDenied` observer payload's `autoDenied` carries,
969
+ * read from the same two places (this gate's own resolution, or the module-private attestation an
970
+ * inherited arm bound to THIS call). Rides to the call's `tool_end` frame so a consumer can tell
971
+ * "the classifier fallback's own 120 s window auto-denied" from "the approval factory's own window
972
+ * (`approvalTimeoutMs`) elapsed" — both settle as `settledBy:"timeout"` / `resolution:"window_expired"`,
973
+ * and the factory's deadline NEVER carries this. A durable park's expiry settles on the decide lane
974
+ * with no `resolution` word, so it is told apart by that absence, not by this bit.
975
+ * Absent everywhere else (a person's no, a policy deny, an executing call).
976
+ */
977
+ autoDenied?: true;
965
978
  /**
966
979
  * design/252 G-7 — WHOSE settlement that was: the identifier the approval channel reported, carried
967
980
  * out verbatim beside {@link settledBy}. This layer authenticates nothing and compares nothing; the
@@ -1226,13 +1239,14 @@ export interface ToolGateInput {
1226
1239
  * design/384 slice 1 — the PER-CALL abort signal, carried in from the `tool_call` hook event's
1227
1240
  * own `signal` seat (the loop's turn-scoped controller; the run abort is composed into it). The
1228
1241
  * gate itself never awaits on it — it hands the signal verbatim to the four consuming seats
1229
- * below (`adjudicate`, `resolveAsk`, `resolveContentAsk`, and slice 2 `suspendAsk`, whose
1230
- * park closure composes it into the halt×park fence) as their optional trailing argument, and
1231
- * the Runner's closures compose it with the run signal so a turn interrupt (bare halt /
1232
- * steer-now) releases the wait or, on the durable leg, makes the park concede. Absent (a host
1233
- * driving the gate directly, or an unthreaded emit) every seat is invoked exactly as before
1234
- * same arity, no trailing `undefined` (the threading is a conditional invocation) and waits
1235
- * stay bound to the run signal alone: the historical shape, byte-identical.
1242
+ * below: `adjudicate`, `resolveAsk` and `resolveContentAsk` take it as their optional trailing
1243
+ * argument; `suspendAsk` (slice 2) reads it as `carry.signal` inside its structural seat
1244
+ * ({@link AskCarry}), and its park closure composes it into the halt×park fence. The Runner's
1245
+ * closures compose it with the run signal so a turn interrupt (bare halt / steer-now) releases
1246
+ * the wait or, on the durable leg, makes the park concede. Absent (a host driving the gate
1247
+ * directly, or an unthreaded emit) ⇒ the three synchronous seats are invoked exactly as before —
1248
+ * same arity, no trailing `undefined` (the threading is a conditional invocation) — the carry has
1249
+ * no `signal` member, and waits stay bound to the run signal alone: the historical shape.
1236
1250
  */
1237
1251
  callSignal?: AbortSignal;
1238
1252
  /** The combined tool-policy check (abort-bound), or undefined when no policy is wired (→ allow).
@@ -1321,12 +1335,13 @@ export interface ToolGateInput {
1321
1335
  * states — every parameter here is positional — and typed `boolean` so a mis-bind against the two
1322
1336
  * trailing `string` seats is a compile error rather than a silent re-binding. */
1323
1337
  probeMandated?: boolean,
1324
- /** design/384 slice 2{@link ToolGateInput.callSignal}, threaded VERBATIM as a CONDITIONAL
1325
- * trailing argument (the slice-1 arity discipline: absent means the historical arity, never a
1326
- * trailing `undefined`). The park closure composes it with the run signal into the halt×park
1327
- * fence's cut signal; an implementation that ignores it keeps the run-level binding. APPENDED at
1328
- * the tail like its neighbours, and typed `AbortSignal` so a mis-bind is a compile error. */
1329
- callSignal?: AbortSignal) => Promise<ToolGateResult["suspend"] | ParkAttemptFailed | undefined>;
1338
+ /** The ONE structural seat after the twelve positional ones everything else the park closure needs
1339
+ * travels inside it ({@link AskCarry}: the per-call signal, the surviving ask's fallback member, its
1340
+ * origin word), so the tuple does not grow a seat per member. ALWAYS passed by the gate (thirteen
1341
+ * arguments, every call) optional on the TYPE only because the ten seats before it are optional and
1342
+ * a required parameter cannot follow one. The twelve seats above are the historical shape; a host-built
1343
+ * closure that read the signal as the 13th positional argument now reads `carry.signal`. */
1344
+ carry?: AskCarry) => Promise<ToolGateResult["suspend"] | ParkAttemptFailed | undefined>;
1330
1345
  /**
1331
1346
  * design/174 — route a policy `ask` on the reserved question tool to this run's CONTENT-ask channel
1332
1347
  * before it can become a park or a refusal. Called in the `ask` branch with the FINAL post-hook,
@@ -1454,12 +1469,12 @@ export interface ToolGateInput {
1454
1469
  * resolution for safety asks simply does not arm auto mode. The classifier can never AUTO-ALLOW by
1455
1470
  * failing — only an affirmative `<block>no</block>` allows.
1456
1471
  * EXCLUDED from that scope (the ask flows the ORIGINAL chain untouched, exactly as if this field
1457
- * were absent for that one call): an org-constrained ask (design/182 §7.4, either org arm), the
1458
- * reserved question tool, a call MARKED unresolvable (design/153), a hook-raised or hook-promoted
1459
- * ask (`decisionReason:"hook"`), and an ask carrying `matchedAskRule` (an explicit `ask` permission
1460
- * rule matched #213: a person's standing "ask me each time" is not classifier hesitation). The
1461
- * last two are the same pair the persisted-rule lane refuses, so the gate's two ask→allow seams
1462
- * answer provenance questions identically.
1472
+ * were absent for that one call): every ask whose ORIGIN is not the plain `policy` family — the
1473
+ * closed set and the classifier's per-origin table are in `ask-origin.ts` (an org-constrained ask on
1474
+ * either org arm, the reserved question tool, a call MARKED unresolvable, a hook-raised or
1475
+ * hook-promoted ask, an explicit matched `ask` rule, and the classifier's own denial-limit fallback).
1476
+ * The hook and ask-rule rows are the same pair the persisted-rule lane refuses, so the gate's two
1477
+ * ask→allow seams answer provenance questions identically.
1463
1478
  */
1464
1479
  autoMode?: {
1465
1480
  decider: import("./auto-mode.js").AutoModeDecider;
@@ -1723,6 +1738,35 @@ export declare function persistedRuleMandateOf(marks: {
1723
1738
  * {@link import("./types.js").ReversibilityVerdict.mandated}. */
1724
1739
  probeMandated?: boolean;
1725
1740
  }): "operator_always" | "tool_marks" | "probe_mandate" | undefined;
1741
+ /**
1742
+ * The park closure's ONE structural seat — what the gate hands the durable park beyond the twelve
1743
+ * positional seats. Every member is optional and read by name, so a new thing the park must know
1744
+ * becomes a member here, never a fourteenth positional argument (each of the last four seats was
1745
+ * appended "at the tail so nothing re-binds", which is the growth this seat ends).
1746
+ */
1747
+ export interface AskCarry {
1748
+ /** design/384 slice 2 — {@link ToolGateInput.callSignal}, verbatim: the park closure composes it
1749
+ * with the run signal into the halt×park fence's cut signal. Absent ⇒ the run-level binding alone. */
1750
+ readonly signal?: AbortSignal;
1751
+ /** The surviving ask's `denialLimitFallback` (the classifier denial-limit counts) run through the
1752
+ * window-less route station ({@link import("./auto-mode.js").unarmedWindow}) when the gate builds the
1753
+ * carry: a park is not a hand-out to a live approver, so nothing on this lane counts down. The
1754
+ * decision's member cannot state a window (its type has no such member), and this is the one place
1755
+ * the `0` a park row shows is written. Persisted on the row so a durable card shows the counts the
1756
+ * synchronous card shows. Absent then means: not a fallback ask. */
1757
+ readonly denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
1758
+ /** The surviving ask's engine-stamped origin word ({@link import("./ask-origin.js").AskOrigin}), persisted
1759
+ * on the row so a durable card is rendered by the same word the synchronous card is. */
1760
+ readonly origin?: import("./ask-origin.js").AskOrigin;
1761
+ }
1762
+ /** What a durable row persists of an {@link AskCarry}: the fallback member COPIED (the row never aliases
1763
+ * the gate's object; its window is already `0` — the gate's carry mint is the one writer of that
1764
+ * number) and the origin word only when it is a member of the closed set (the carry is typed, but the
1765
+ * row is the boundary a host reads back through — the same screen the summary applies). */
1766
+ export declare function askCarryRowMembers(carry: AskCarry | undefined): {
1767
+ denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
1768
+ origin?: import("./ask-origin.js").AskOrigin;
1769
+ };
1726
1770
  /**
1727
1771
  * The design/37 **two-phase tool gate** — the single chokepoint that makes the load-bearing invariant
1728
1772
  * structural ("a hook's `allow` cannot bypass the policy's `deny`/`ask`"):
@@ -1,6 +1,7 @@
1
1
  import { coreMintedAutoDeniedOf, coreMintedResolutionOf, decisionText, describeThrown, isAskDenyResolution, refuseOutOfContractDecision } from "./tool-policy.js";
2
2
  import { brandPolicyAskClass } from "./ask-class.js";
3
- import { denialLimitFallbackMessage } from "./auto-mode.js";
3
+ import { askOriginOf, classifierMayAnswer, isAskOrigin } from "./ask-origin.js";
4
+ import { denialLimitFallbackMessage, unarmedWindow } from "./auto-mode.js";
4
5
  import { inlineUntrusted } from "./untrusted-text.js";
5
6
  import { mintSystemReminder } from "./reminder-mint.js";
6
7
  import { PROBE_REASON_MAX, normalizeProbeCause } from "./checkpoint-store.js";
@@ -424,6 +425,17 @@ export function persistedRuleMandateOf(marks) {
424
425
  ? "tool_marks"
425
426
  : undefined;
426
427
  }
428
+ export function askCarryRowMembers(carry) {
429
+ if (carry === undefined)
430
+ return {};
431
+ return {
432
+ ...(carry.denialLimitFallback !== undefined ? { denialLimitFallback: { ...carry.denialLimitFallback } } : {}),
433
+ ...(isAskOrigin(carry.origin) ? { origin: carry.origin } : {}),
434
+ };
435
+ }
436
+ function parkWith(suspendAsk, parkArgs, carry) {
437
+ return suspendAsk(...parkArgs, carry);
438
+ }
427
439
  export async function runToolGate(input) {
428
440
  const { event, preToolUse, adjudicate, resolveAsk, suspendAsk } = input;
429
441
  const { toolCallId, toolName } = event;
@@ -632,6 +644,11 @@ export async function runToolGate(input) {
632
644
  }
633
645
  let orgRealApprovalRequired = false;
634
646
  let orgAskOrigin;
647
+ const askOriginFacts = () => ({
648
+ contentQuestion: toolName === ASK_USER_QUESTION_TOOL_NAME,
649
+ markedUnresolvable: input.isMarkedUnresolvable?.(input.event.toolCallId) === true,
650
+ org: orgAskOrigin,
651
+ });
635
652
  let orgTightenCount = 0;
636
653
  let resolvedApprover;
637
654
  let orgEvidence = (() => {
@@ -798,17 +815,12 @@ export async function runToolGate(input) {
798
815
  };
799
816
  }
800
817
  }
818
+ const originFacts = askOriginFacts();
801
819
  if (decision.action === "ask") {
802
- decision = { ...decision, ruleEvidence: mintRuleEvidence(personalEvidence), ...(laneCoverage !== undefined ? { segmentCoverage: laneCoverage } : {}) };
820
+ const stamped = { ...decision, ruleEvidence: mintRuleEvidence(personalEvidence), ...(laneCoverage !== undefined ? { segmentCoverage: laneCoverage } : {}) };
821
+ decision = { ...stamped, origin: askOriginOf(stamped, originFacts) };
803
822
  }
804
- if (input.autoMode &&
805
- !orgRealApprovalRequired &&
806
- decision.action === "ask" &&
807
- decision.decisionReason !== "hook" &&
808
- decision.matchedAskRule === undefined &&
809
- decision.denialLimitFallback === undefined &&
810
- req.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
811
- input.isMarkedUnresolvable?.(input.event.toolCallId) !== true) {
823
+ if (input.autoMode && decision.action === "ask" && decision.origin !== undefined && classifierMayAnswer(decision.origin)) {
812
824
  const verdict = await input.autoMode.decider
813
825
  .decide({ req, askMessage: decisionText(decision) }, input.abortSignal)
814
826
  .catch(() => ({ kind: "unavailable", cause: "error" }));
@@ -826,13 +838,14 @@ export async function runToolGate(input) {
826
838
  const category = verdict.category ? inlineUntrusted(verdict.category) : "";
827
839
  const tracked = input.autoMode.denialTracking?.recordBlock();
828
840
  if (tracked?.limitReached === true) {
829
- decision = {
841
+ const fallbackAsk = {
830
842
  ...decision,
831
843
  message: denialLimitFallbackMessage(tracked.fallback, reason || category || toolName),
832
844
  decisionReason: "classifier",
833
845
  requiresRealApproval: true,
834
846
  denialLimitFallback: tracked.fallback,
835
847
  };
848
+ decision = { ...fallbackAsk, origin: askOriginOf(fallbackAsk, originFacts) };
836
849
  denySource = "classifier";
837
850
  }
838
851
  else {
@@ -875,9 +888,14 @@ export async function runToolGate(input) {
875
888
  const realApprovalOf = (d) => d.action === "ask" && d.requiresRealApproval === true
876
889
  ? { origin: orgAskOrigin !== undefined ? `org_${orgAskOrigin}` : "policy" }
877
890
  : undefined;
891
+ const carryOf = (d) => ({
892
+ ...(callSignal !== undefined ? { signal: callSignal } : {}),
893
+ ...(d.action === "ask" && d.denialLimitFallback !== undefined ? { denialLimitFallback: unarmedWindow(d.denialLimitFallback) } : {}),
894
+ ...(d.action === "ask" && d.origin !== undefined ? { origin: d.origin } : {}),
895
+ });
878
896
  if (suspendAsk && decision.action === "ask") {
879
897
  const parkArgs = [req, currentInput, safety, undefined, realApprovalOf(decision), decision.action === "ask" ? decision.persistedRuleShadowed : undefined, decision.action === "ask" ? decision.decisionReason : undefined, decision.action === "ask" ? decision.probeReason : undefined, decision.action === "ask" ? decision.probeCause : undefined, decision.action === "ask" ? decision.segmentCoverage : undefined, decision.action === "ask" ? decision.matchedAskRule : undefined, decision.action === "ask" ? decision.probeMandated : undefined];
880
- const suspended = await (callSignal !== undefined ? suspendAsk(...parkArgs, callSignal) : suspendAsk(...parkArgs));
898
+ const suspended = await parkWith(suspendAsk, parkArgs, carryOf(decision));
881
899
  if (suspended) {
882
900
  if ("parkFailed" in suspended)
883
901
  parkFailed = suspended.parkFailed;
@@ -897,7 +915,7 @@ export async function runToolGate(input) {
897
915
  }
898
916
  if (suspendAsk && outcome.parkDeclined && parkFailed === undefined) {
899
917
  const parkArgs = [req, currentInput, safety, true, realApprovalOf(decision), decision.action === "ask" ? decision.persistedRuleShadowed : undefined, decision.action === "ask" ? decision.decisionReason : undefined, decision.action === "ask" ? decision.probeReason : undefined, decision.action === "ask" ? decision.probeCause : undefined, decision.action === "ask" ? decision.segmentCoverage : undefined, decision.action === "ask" ? decision.matchedAskRule : undefined, decision.action === "ask" ? decision.probeMandated : undefined];
900
- const suspended = await (callSignal !== undefined ? suspendAsk(...parkArgs, callSignal) : suspendAsk(...parkArgs));
918
+ const suspended = await parkWith(suspendAsk, parkArgs, carryOf(decision));
901
919
  if (suspended) {
902
920
  if ("parkFailed" in suspended)
903
921
  parkFailed = suspended.parkFailed;
@@ -932,7 +950,7 @@ export async function runToolGate(input) {
932
950
  decision = resolved;
933
951
  if (resolved.action === "deny" && resolved.approverUnavailable === true && suspendAsk && parkFailed === undefined) {
934
952
  const parkArgs = [req, currentInput, safety, true, realApprovalOf(askBeforeResolve), askBeforeResolve.action === "ask" ? askBeforeResolve.persistedRuleShadowed : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.decisionReason : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeReason : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeCause : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.segmentCoverage : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.matchedAskRule : undefined, askBeforeResolve.action === "ask" ? askBeforeResolve.probeMandated : undefined];
935
- const suspended = await (callSignal !== undefined ? suspendAsk(...parkArgs, callSignal) : suspendAsk(...parkArgs));
953
+ const suspended = await parkWith(suspendAsk, parkArgs, carryOf(askBeforeResolve));
936
954
  if (suspended) {
937
955
  if ("parkFailed" in suspended)
938
956
  parkFailed = suspended.parkFailed;
@@ -949,7 +967,7 @@ export async function runToolGate(input) {
949
967
  askDenyResolution === "approver_unavailable" ||
950
968
  resolved.approverUnavailable === true) {
951
969
  try {
952
- input.onHeadlessDenialLimit?.({ toolName, toolCallId, fallback: askBeforeResolve.denialLimitFallback });
970
+ input.onHeadlessDenialLimit?.({ toolName, toolCallId, fallback: unarmedWindow(askBeforeResolve.denialLimitFallback) });
953
971
  }
954
972
  catch {
955
973
  }
@@ -1035,6 +1053,7 @@ export async function runToolGate(input) {
1035
1053
  editRewrittenSinceHuman = true;
1036
1054
  }
1037
1055
  const orgTightensBefore = orgTightenCount;
1056
+ orgAskOrigin = undefined;
1038
1057
  recheck = await applyOrgLayer(recheck, editArgs);
1039
1058
  const orgRaisedThisRound = orgTightenCount > orgTightensBefore;
1040
1059
  if (recheck.action === "deny") {
@@ -1052,7 +1071,8 @@ export async function runToolGate(input) {
1052
1071
  editArgs = recheck.updatedInput;
1053
1072
  editRewrittenSinceHuman = true;
1054
1073
  }
1055
- const editAskDecision = { ...recheck, ruleEvidence: mintRuleEvidence({ dotsAbsent: "not_adjudicated" }) };
1074
+ const editAskSnapshot = { ...recheck, ruleEvidence: mintRuleEvidence({ dotsAbsent: "not_adjudicated" }) };
1075
+ const editAskDecision = { ...editAskSnapshot, origin: askOriginOf(editAskSnapshot, askOriginFacts()) };
1056
1076
  const editAskReq = { toolName, args: editArgs, toolCallId };
1057
1077
  const rr = await (callSignal !== undefined ? resolveAsk(editAskDecision, editAskReq, callSignal) : resolveAsk(editAskDecision, editAskReq));
1058
1078
  resolvedApprover = rr.action !== "ask" ? rr.approver : undefined;
@@ -1105,6 +1125,7 @@ export async function runToolGate(input) {
1105
1125
  reason: formatHookFeedback(denyReason, input.reminderMark),
1106
1126
  ...(denySettledBy !== undefined ? { settledBy: denySettledBy } : {}),
1107
1127
  ...(denyResolution !== undefined ? { resolution: denyResolution } : {}),
1128
+ ...(denyAutoDenied ? { autoDenied: true } : {}),
1108
1129
  ...(denyApprover !== undefined ? { approver: denyApprover } : {}),
1109
1130
  preToolContext,
1110
1131
  };
@@ -1,58 +1,3 @@
1
- /**
2
- * MCP client integration. Protocol revision `2025-11-25`, supplied by `@modelcontextprotocol/sdk` 1.30.0
3
- * (core pins the SDK EXACTLY and does not negotiate the revision itself). Client side only — core never
4
- * implements an MCP server.
5
- *
6
- * ── DEPRECATION-IMMUNITY REGISTRY (MCP revision `2026-07-28`) ────────────────────────────────────────
7
- * The `2026-07-28` revision deprecates four surfaces this module has NEVER implemented. That is not an
8
- * accident and not an unfinished edge: each one was declined on its own merits, and the revision has now
9
- * put all four on a deprecation clock. So there is no migration debt here — and, symmetrically, none of
10
- * the four may be "completed" later on the grounds that the client looks incomplete next to the older
11
- * revision's feature list:
12
- *
13
- * 1. **roots** (`roots/list`, `notifications/roots/list_changed`) — never implemented. The replacement the
14
- * revision itself names is what this module already does: directory/file scope travels as ordinary tool
15
- * arguments or resource URIs, never as a client-hosted root list a server can enumerate.
16
- * 2. **sampling** (`sampling/createMessage`) — never implemented. A server that wants model inference uses
17
- * its own provider; core never lends the caller's model channel (or budget) to a connected server.
18
- * 3. **logging** (`logging/setLevel`, `notifications/message`) — never implemented. Server diagnostics
19
- * arrive as stdio stderr, or as ordinary tool/resource content, or not at all.
20
- * 4. **HTTP+SSE transport** — never implemented. `buildTransport` builds stdio and Streamable HTTP only;
21
- * the SDK's `SSEClientTransport` is not imported anywhere. The revision's SSE off-ramp is therefore a
22
- * no-op here.
23
- *
24
- * Consequence for future edits: adding any of the four is a REGRESSION, not a gap being closed. The bar for
25
- * reopening one is a LATER protocol revision that revives the surface, or a concrete consumer requirement
26
- * recorded under its own ticket — not the absence of the feature.
27
- *
28
- * THE `2026-07-28` DELIVERY FORM, and why it does not narrow the four (design/384 §1, §2-J). That revision
29
- * does not carry `roots/list`, `sampling/createMessage` or `elicitation/create` as standalone JSON-RPC
30
- * methods at all. Its request-method table (as a 2026-era client dispatches it — CC 2.1.250's bundled era
31
- * descriptor) is exactly: `tools/call`, `tools/list`, `prompts/get`, `prompts/list`, `resources/list`,
32
- * `resources/templates/list`, `resources/read`, `completion/complete`, `server/discover`,
33
- * `subscriptions/listen`. The three arrive instead through a DIFFERENT door — as embedded `inputRequests`
34
- * inside an `input_required` result of `tools/call` / `prompts/get` / `resources/read`, answered by
35
- * RETRYING the original request with `inputResponses`.
36
- *
37
- * That is a change of DELIVERY FORM ONLY, and it buys no exemption. Items 1 and 2 are reasoned about what
38
- * the capability MEANS, not about how it is framed on the wire: an embedded `roots/list` enumerates the
39
- * very root list item 1 refuses to host, and an embedded `sampling/createMessage` spends the very model
40
- * channel and budget item 2 refuses to lend. So embedded roots/sampling are banned ON THE SAME TERMS and
41
- * must clear the SAME reopening bar — a new door is not a reason, and "the registry only named the legacy
42
- * method" is not a loophole. What this paragraph exists to prevent is the OPPOSITE misreading: items 1-4
43
- * were written against the `2025-11-25` surface and are not themselves a ruling on the 2026 mechanics.
44
- * `elicitation/create` — which core DOES implement (opt-in, form mode) and which is NOT one of the four —
45
- * comes through that same door, so an era connection would have to decide how it is driven; that is
46
- * design/384 P3's question, not this registry's. Nothing here is reachable today: core speaks `2025-11-25`,
47
- * where all three are standalone methods, and core's `initialize` declares neither `roots` nor `sampling`
48
- * (pinned).
49
- *
50
- * Two more revision-relevant absences, same reasoning, different cause:
51
- * - `resources/subscribe` / `resources/unsubscribe` — the revision REMOVES the methods. Tool-list freshness
52
- * here is served by the explicit `RefreshMcpTools` tool over the existing connection, which is also the
53
- * only thing this module's connect-per-task connection lifetime can honestly support.
54
- * - `ping` — likewise removed by the revision. Liveness here is the idle watchdog plus per-call timeouts.
55
- */
56
1
  import { type ProtocolId } from "./protocol-table.js";
57
2
  import type { AgentTool } from "../internal/harness-types.js";
58
3
  import type { ImageContent, TextContent } from "../internal/llm.js";
@@ -313,6 +258,11 @@ export interface McpServerStatus {
313
258
  };
314
259
  toolNames?: string[];
315
260
  error?: string;
261
+ /** Present iff `status === "failed"` — the failure's CLASS, judged from the thrown error object at
262
+ * connect time ({@link mcpFailureCodeOf}; the closed set is {@link MCP_FAILURE_CODES} plus the
263
+ * `http_<status>` form). A consumer keys an action off THIS (a `http_401` is a re-authorization
264
+ * door, a `spawn_failed` is a missing binary) instead of parsing the neutralized `error` text. */
265
+ errorCode?: McpFailureCode;
316
266
  /**
317
267
  * RB-437-a — set to `true` once core OBSERVES this server's transport close during the task; absent
318
268
  * otherwise (never `false`: absence means "no close was observed", which is not the same as "alive").
@@ -460,6 +410,48 @@ export interface HttpTransportFailure {
460
410
  * reached them" means.
461
411
  */
462
412
  export declare function describeHttpTransportFailure(err: unknown): HttpTransportFailure | undefined;
413
+ /**
414
+ * The closed vocabulary of MCP connect-failure classes a per-server status record may carry
415
+ * ({@link McpServerStatus.errorCode}, relayed onto the wiring manifest's `mcp[]` read face). The
416
+ * transport family is named by the failure's SHAPE wherever the SDK exposes one (error class, JSON-RPC
417
+ * code, errno on the cause chain, HTTP status), so a consumer can map a class to an action without
418
+ * parsing the neutralized error string. Two words are the exception and are text-derived, because
419
+ * undici and the SDK surface those two conditions as bare messages with no code: `network` (a
420
+ * `TypeError` whose message matches `fetch failed|terminated|network` and carries no errno) and the
421
+ * non-`McpError` arm of `connection_closed` (a message matching `not connected|connection closed`).
422
+ * A wording change in those libraries moves such a failure to `unknown`, never to a wrong word; the
423
+ * classifier table pins both regexes. An HTTP endpoint that answered with a status is
424
+ * reported in the `http_<status>` form (`http_401` = the endpoint wants (re-)authorization, `http_5xx` =
425
+ * the endpoint is down) — a template member of the type beside these fixed words. Additive-only: a new
426
+ * word here is a new arm for consumers, never a re-meaning of an existing one.
427
+ * · `connect_refused` — a connect-phase errno (ECONNREFUSED / ENOTFOUND / …): the request provably never left;
428
+ * · `connection_failed` — a network errno past the connect phase (reset / timed out mid-exchange);
429
+ * · `network` — the HTTP request failed at the network layer with no errno on the cause chain;
430
+ * · `not_mcp_response` — the HTTP endpoint answered something that is not an MCP payload (no status to name);
431
+ * · `spawn_failed` — a stdio server process could not be started (a missing/unexecutable command);
432
+ * · `connection_closed` — the transport closed during the handshake (the server process exited, the stream ended);
433
+ * · `timeout` — the handshake's request timed out;
434
+ * · `protocol` — the server answered the handshake with an MCP/JSON-RPC error (a version mismatch, a
435
+ * rejected capability, …) — `error` carries the named condition;
436
+ * · `invalid_config` — the declaration itself could not be dialed (a malformed `url`): a configuration
437
+ * fault to fix in the spec, never an outage to wait out;
438
+ * · `unknown` — none of the above could be established from the thrown value.
439
+ */
440
+ export declare const MCP_FAILURE_CODES: readonly ["connect_refused", "connection_failed", "network", "not_mcp_response", "spawn_failed", "connection_closed", "timeout", "protocol", "invalid_config", "unknown"];
441
+ export type McpFailureCode = (typeof MCP_FAILURE_CODES)[number] | `http_${number}`;
442
+ /**
443
+ * Classify one connect-time failure into {@link McpFailureCode}. Reads the thrown value's SHAPE
444
+ * (error class, JSON-RPC code, errno on the cause chain, HTTP status) first; only the two
445
+ * message-shaped conditions named on {@link MCP_FAILURE_CODES} (`network`, the non-`McpError`
446
+ * `connection_closed`) are decided by a fixed regex over the message, and no other word can be chosen
447
+ * by message text — a remote's own wording can at most leave a failure at `unknown`.
448
+ * `transport` disambiguates the errno family: on a stdio server an errno is the child
449
+ * process failing to spawn, on an HTTP server it is the socket; a caller that could not establish the
450
+ * kind passes `undefined` and the errno family reads as the transport-neutral `connection_failed`.
451
+ * Total: every input maps to a word, and `unknown` is the honest word for a value this table cannot
452
+ * read (a caller must not receive a guess).
453
+ */
454
+ export declare function mcpFailureCodeOf(err: unknown, transport: "stdio" | "http" | undefined): McpFailureCode;
463
455
  export { normalizeNameSegment as normalizeMcpName, clampNameSegment } from "./protocol-naming.js";
464
456
  export * from "./image-downsample.js";
465
457
  /**