@sema-agent/core 7.2.0 → 7.3.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 (64) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/agents/cross-session-envelope.d.ts +7 -0
  3. package/dist/agents/cross-session-envelope.js +4 -0
  4. package/dist/agents/list-agents-tool.d.ts +55 -0
  5. package/dist/agents/list-agents-tool.js +94 -0
  6. package/dist/agents/peer-admission.d.ts +17 -1
  7. package/dist/agents/peer-admission.js +19 -2
  8. package/dist/agents/peer-directory.d.ts +208 -0
  9. package/dist/agents/peer-directory.js +272 -0
  10. package/dist/agents/peer-session-drain.d.ts +159 -0
  11. package/dist/agents/peer-session-drain.js +245 -0
  12. package/dist/agents/send-message-tool.d.ts +31 -0
  13. package/dist/agents/send-message-tool.js +145 -4
  14. package/dist/agents/subagent-steps.d.ts +11 -0
  15. package/dist/agents/subagent-steps.js +27 -4
  16. package/dist/brain/status-sink.d.ts +10 -0
  17. package/dist/brain/status-sink.js +13 -4
  18. package/dist/brain/stream-engine.d.ts +11 -0
  19. package/dist/brain/stream-engine.js +39 -3
  20. package/dist/core/arg-summary.d.ts +13 -3
  21. package/dist/core/arg-summary.js +138 -7
  22. package/dist/core/auto-mode-arming.d.ts +11 -0
  23. package/dist/core/auto-mode-arming.js +7 -1
  24. package/dist/core/auto-mode-prompt.d.ts +5 -0
  25. package/dist/core/auto-mode-prompt.js +2 -1
  26. package/dist/core/auto-mode-rebuild.d.ts +2 -1
  27. package/dist/core/auto-mode-rebuild.js +2 -0
  28. package/dist/core/checkpoint-store.d.ts +14 -0
  29. package/dist/core/checkpoint-store.js +4 -3
  30. package/dist/core/governance-codes.d.ts +1 -1
  31. package/dist/core/governance-codes.js +6 -0
  32. package/dist/core/mailbox-store.d.ts +89 -2
  33. package/dist/core/mailbox-store.js +77 -2
  34. package/dist/core/permission-rule-model.d.ts +9 -0
  35. package/dist/core/permission-rule-model.js +4 -1
  36. package/dist/core/runner/prepare-task.d.ts +20 -0
  37. package/dist/core/runner/prepare-task.js +152 -37
  38. package/dist/core/runner/runtask.js +5 -2
  39. package/dist/core/runner/tool-output-projection.js +1 -0
  40. package/dist/core/store-contracts/mailbox-store-contract.d.ts +23 -0
  41. package/dist/core/store-contracts/mailbox-store-contract.js +157 -1
  42. package/dist/core/task-notification.d.ts +38 -9
  43. package/dist/core/task-notification.js +8 -2
  44. package/dist/core/tools.js +1 -0
  45. package/dist/core/types.d.ts +176 -26
  46. package/dist/core/wiring-manifest.d.ts +62 -5
  47. package/dist/core/wiring-manifest.js +9 -0
  48. package/dist/engine/harness/agent-harness.d.ts +1 -0
  49. package/dist/engine/harness/agent-harness.js +3 -0
  50. package/dist/engine/harness/types.d.ts +3 -0
  51. package/dist/engine/loop/agent-loop.d.ts +7 -0
  52. package/dist/engine/loop/agent-loop.js +79 -0
  53. package/dist/engine/loop/types.d.ts +42 -0
  54. package/dist/index.d.ts +12 -5
  55. package/dist/index.js +11 -4
  56. package/dist/internal/harness-types.d.ts +1 -1
  57. package/dist/stores/cc/mailbox-store.d.ts +1 -1
  58. package/dist/stores/cc/mailbox-store.js +13 -0
  59. package/dist/stores/file/adoption/marker.d.ts +1 -1
  60. package/dist/stores/file/mailbox-store.d.ts +57 -0
  61. package/dist/stores/file/mailbox-store.js +369 -18
  62. package/dist/tools/fs/fs-write.js +69 -3
  63. package/package.json +1 -1
  64. package/test/export-surface.snapshot.json +121 -1
@@ -1,5 +1,5 @@
1
1
  import { strict as assert } from "node:assert";
2
- import { MAILBOX_TOMBSTONED_RECIPIENT_CODE } from "../mailbox-store.js";
2
+ import { MAILBOX_INVALID_PEER_META_CODE, MAILBOX_TOMBSTONED_RECIPIENT_CODE } from "../mailbox-store.js";
3
3
  import { beginContract } from "./contract-harness.js";
4
4
  export const MAILBOX_CONTRACT_SCOPE = "default";
5
5
  const msg = (content, sentAt = 1000) => ({ content, sentAt });
@@ -112,6 +112,75 @@ export async function mailboxStoreContract(mk, runAssertion) {
112
112
  const after = await s.claimLease(S, "a1", "rival", 60_000, 12_000);
113
113
  assert.deepEqual(after?.messages.map((m) => m.seq), [1], "归还后同一条按原 seq 回可见");
114
114
  });
115
+ run("peerMeta 回环:典型 typed 记录逐字节回读;缺席保持缺席(pre-385/foreign 记录语义);空对象也是「在场」", async (make) => {
116
+ const s = make();
117
+ const full = {
118
+ fromSession: "sess_1",
119
+ fromMode: "prompting",
120
+ senderKey: "k-1",
121
+ kind: "peer_message",
122
+ fromScope: "team/a b",
123
+ gateReceiptId: "gr-9",
124
+ };
125
+ await s.append(S, "a1", { content: "typed", sentAt: 1_000, peerMeta: full });
126
+ await s.append(S, "a1", { content: "plain", sentAt: 1_001 });
127
+ await s.append(S, "a1", { content: "empty-meta", sentAt: 1_002, peerMeta: {} });
128
+ const lease = await s.claimLease(S, "a1", "w1", 60_000, 10_000);
129
+ assert.deepEqual(lease.messages.map((m) => m.content), ["typed", "plain", "empty-meta"]);
130
+ assert.deepEqual(lease.messages[0].peerMeta, full, "六字段逐字节回读");
131
+ assert.equal("peerMeta" in lease.messages[1], false, "未带 peerMeta 的记录回读时该键必须缺席(不是 undefined 值)");
132
+ assert.deepEqual(lease.messages[2].peerMeta, {}, "空对象=在场但无字段,与缺席区分");
133
+ });
134
+ run("peerMeta 坏值响亮:garbage 以具名码拒 append,且零副作用(不入箱、不耗 seq、不动租约)", async (make) => {
135
+ const s = make();
136
+ const seqBefore = await s.append(S, "a1", { content: "anchor", sentAt: 1_000 });
137
+ const held = await s.claimLease(S, "a1", "holder", 60_000, 10_000);
138
+ assert.equal(held?.messages.length, 1);
139
+ const codeOf = (e) => e?.code;
140
+ class Meta {
141
+ fromSession = "s";
142
+ }
143
+ const hiddenKey = { fromSession: "s" };
144
+ Object.defineProperty(hiddenKey, "bogus", { value: 1, enumerable: false });
145
+ for (const [label, bad] of [
146
+ ["fromMode 集外值", { fromMode: "root" }],
147
+ ["kind 集外值", { kind: "gossip" }],
148
+ ["未知键", { fromSession: "s", bogus: 1 }],
149
+ ["空字符串", { senderKey: "" }],
150
+ ["非字符串", { gateReceiptId: 7 }],
151
+ ["数组", ["fromSession"]],
152
+ ["null", null],
153
+ ["字符串", "fromSession=x"],
154
+ ["Date", new Date(0)],
155
+ ["Map", new Map()],
156
+ ["class 实例", new Meta()],
157
+ ["symbol 键", { fromSession: "s", [Symbol("hidden")]: 1 }],
158
+ ["不可枚举的未知键", hiddenKey],
159
+ ]) {
160
+ let thrown;
161
+ try {
162
+ await s.append(S, "a1", { content: `bad:${label}`, sentAt: 2_000, peerMeta: bad });
163
+ }
164
+ catch (e) {
165
+ thrown = e;
166
+ }
167
+ assert.notEqual(thrown, undefined, `${label}:必须拒`);
168
+ assert.equal(codeOf(thrown), MAILBOX_INVALID_PEER_META_CODE, `${label}:拒必须带码`);
169
+ }
170
+ assert.equal(await s.peekCount(S, "a1"), 1, "被拒的 append 不得入箱");
171
+ assert.equal(await s.claimLease(S, "a1", "rival", 60_000, 11_000), null, "被拒的 append 不得动活租约");
172
+ await s.ack(S, "a1", "holder", held.maxSeq);
173
+ assert.equal(await s.append(S, "a1", { content: "next", sentAt: 3_000 }), seqBefore + 1, "被拒的 append 不得消耗 seq");
174
+ });
175
+ run("peerMeta 不串引用:消费者改写租约批里的 peerMeta,不得改到下次投递的记录", async (make) => {
176
+ const s = make();
177
+ await s.append(S, "a1", { content: "m1", sentAt: 1_000, peerMeta: { fromMode: "bypass", fromSession: "sess_1" } });
178
+ const first = await s.claimLease(S, "a1", "w1", 1_000, 10_000);
179
+ first.messages[0].peerMeta.fromSession = "forged";
180
+ await s.releaseLease(S, "a1", "w1");
181
+ const again = await s.claimLease(S, "a1", "w2", 1_000, 20_000);
182
+ assert.deepEqual(again.messages[0].peerMeta, { fromMode: "bypass", fromSession: "sess_1" }, "重投的记录必须是原样");
183
+ });
115
184
  await settle();
116
185
  }
117
186
  export async function mailboxAckOwnershipContract(mk, runAssertion) {
@@ -192,6 +261,93 @@ export async function mailboxBundledOnlyContract(mk, runAssertion) {
192
261
  });
193
262
  await settle();
194
263
  }
264
+ export async function mailboxCrossProcessContract(hooks) {
265
+ const { run: runRaw, settle } = beginContract(hooks.runAssertion);
266
+ const run = (name, fn) => runRaw(name, async () => {
267
+ const pair = await hooks.openPair();
268
+ try {
269
+ await fn(pair.local, pair.peer);
270
+ }
271
+ finally {
272
+ await pair.dispose();
273
+ }
274
+ });
275
+ const S = MAILBOX_CONTRACT_SCOPE;
276
+ const H = "session.shared";
277
+ const range = (n) => Array.from({ length: n }, (_, i) => i + 1);
278
+ run("跨进程 seq 单铸:两个进程交错 append N+N ⇒ seq 恰为 1..2N(无重复无空洞),每条消息在任一方视角都可见", async (local, peer) => {
279
+ const N = 40;
280
+ const seqs = [];
281
+ for (let i = 0; i < N; i++) {
282
+ const [a, b] = await Promise.all([local.append(S, H, msg(`local-${i}`, 1_000 + i)), peer.append(S, H, msg(`peer-${i}`, 2_000 + i))]);
283
+ seqs.push(a, b);
284
+ }
285
+ assert.deepEqual([...seqs].sort((x, y) => x - y), range(2 * N), "两方拿到的回执合起来必须恰是 1..2N");
286
+ assert.equal(await peer.peekCount(S, H), 2 * N, "对方进程的观察面必须看到本方的 append");
287
+ assert.equal(await local.peekCount(S, H), 2 * N);
288
+ const lease = await local.claimLease(S, H, "cycle-1", 60_000, 10_000);
289
+ assert.deepEqual(lease.messages.map((m) => m.seq), range(2 * N), "本方 claim 必须按 seq 序取到全部 2N 条");
290
+ assert.equal(lease.maxSeq, 2 * N);
291
+ const contents = new Set(lease.messages.map((m) => m.content));
292
+ for (let i = 0; i < N; i++) {
293
+ assert.ok(contents.has(`local-${i}`), `local-${i} 不得丢`);
294
+ assert.ok(contents.has(`peer-${i}`), `peer-${i} 不得丢`);
295
+ }
296
+ });
297
+ run("lease/ack 不跨进程交叉:一方的活租约 fence 住另一方的 claim;非持有方的 ack 是 no-op;持有方的 ack 对另一方立即可见;高水位跨对方的 ack 存活", async (local, peer) => {
298
+ await local.append(S, H, msg("m1", 1));
299
+ await peer.append(S, H, msg("m2", 2));
300
+ const held = await peer.claimLease(S, H, "cycle-peer", 60_000, 10_000);
301
+ assert.deepEqual(held.messages.map((m) => m.seq), [1, 2]);
302
+ assert.equal(await local.claimLease(S, H, "cycle-local", 60_000, 11_000), null, "对方进程的活租约必须 fence 住本方");
303
+ await local.ack(S, H, "cycle-local", held.maxSeq);
304
+ assert.equal(await peer.peekCount(S, H), 2, "非持有方的 ack 不得动持有方手上的批");
305
+ await peer.ack(S, H, "cycle-peer", held.maxSeq);
306
+ assert.equal(await local.peekCount(S, H), 0, "持有方的 ack 必须被本方看见");
307
+ assert.equal(await local.claimLease(S, H, "cycle-local", 60_000, 12_000), null);
308
+ assert.equal(await local.append(S, H, msg("m3", 3)), 3, "seq 高水位必须跨对方进程的 ack 存活(永不复用)");
309
+ await peer.append(S, H, msg("m4", 4));
310
+ const held2 = await peer.claimLease(S, H, "cycle-peer-2", 1_000, 20_000);
311
+ assert.deepEqual(held2.messages.map((m) => m.seq), [3, 4]);
312
+ const takeover = await local.claimLease(S, H, "cycle-local-2", 60_000, 30_000);
313
+ assert.deepEqual(takeover.messages.map((m) => m.seq), [3, 4], "过期租约的接管跨进程成立,seq 原样");
314
+ });
315
+ run("一方的 housekeeping 不丢另一方的 append:两进程各自反复压缩/重写期间交错写入 120 条,任一方视角都完整", async (local, peer) => {
316
+ const N = 60;
317
+ for (let i = 0; i < N; i++) {
318
+ await Promise.all([local.append(S, H, msg(`L${i}`, 1_000 + i)), peer.append(S, H, msg(`P${i}`, 2_000 + i))]);
319
+ }
320
+ const fromPeer = await peer.claimLease(S, H, "cycle-peer", 60_000, 10_000);
321
+ assert.deepEqual(fromPeer.messages.map((m) => m.seq), range(2 * N), "对方视角:seq 1..2N 全在");
322
+ await peer.releaseLease(S, H, "cycle-peer");
323
+ const fromLocal = await local.claimLease(S, H, "cycle-local", 60_000, 11_000);
324
+ assert.deepEqual(fromLocal.messages.map((m) => m.seq), range(2 * N), "本方视角:seq 1..2N 全在");
325
+ const contents = fromLocal.messages.map((m) => m.content).sort();
326
+ assert.deepEqual(contents, [...range(N).map((i) => `L${i - 1}`), ...range(N).map((i) => `P${i - 1}`)].sort());
327
+ await local.ack(S, H, "cycle-local", fromLocal.maxSeq);
328
+ assert.equal(await peer.peekCount(S, H), 0, "本方的 ack 对方可见");
329
+ });
330
+ run("reap 看得见对方进程的旧消息,哪怕本方缓存里这个箱是空的(已加载的空缓存不得替对方的旧消息说「没有」)", async (local, peer) => {
331
+ await local.append(S, H, msg("mine", 1_000));
332
+ const held = await local.claimLease(S, H, "cycle-local", 60_000, 1_000);
333
+ await local.ack(S, H, "cycle-local", held.maxSeq);
334
+ await peer.append(S, H, msg("old-from-peer", 2_000));
335
+ assert.equal(await local.reap(S, 100_000, { maxAgeMs: 50_000 }), 1, "对方写的过期消息必须被本方 reap 看见并老化");
336
+ assert.equal(await peer.peekCount(S, H), 0);
337
+ assert.equal(await peer.append(S, H, msg("after", 200_000)), 3, "高水位跨 reap 存活(对方视角)");
338
+ });
339
+ run("reap 与 drop 的 seq 语义跨进程:一方 reap 后另一方继续高水位;一方 drop 后另一方从 1 重铸", async (local, peer) => {
340
+ await peer.append(S, H, msg("old", 1_000));
341
+ assert.equal(await local.reap(S, 100_000, { maxAgeMs: 50_000 }), 1, "本方 reap 必须看见对方写的旧消息");
342
+ assert.equal(await peer.peekCount(S, H), 0);
343
+ assert.equal(await peer.append(S, H, msg("after-reap", 200_000)), 2, "reap 清箱但保高水位(RB-90 跨进程)");
344
+ await local.drop(S, H);
345
+ assert.equal(await peer.peekCount(S, H), 0, "本方 drop 对方可见");
346
+ assert.equal(await peer.append(S, H, msg("after-drop", 300_000)), 1, "drop 结束箱的一生:对方从 1 重铸");
347
+ assert.equal(await local.peekCount(S, H), 1);
348
+ });
349
+ await settle();
350
+ }
195
351
  export async function mailboxTombstonedRecipientContract(mk, hooks) {
196
352
  const { run: runRaw, settle } = beginContract(hooks.runAssertion);
197
353
  const run = (name, fn) => runRaw(name, () => withStores(mk, fn));
@@ -155,23 +155,45 @@ export interface TaskNotificationPayload {
155
155
  body: string;
156
156
  };
157
157
  /**
158
- * design/385 §1.4 d1 — the engine-minted PROVENANCE side record of an agent-message frame, a typed
159
- * key (never model text) so a host can attribute and correlate the injection on the wire:
160
- * `kind` names the lane, `from` is the sender label the frame's attribute spells, `taskId` the
161
- * sender's run/agent id, `seq` the producer's per-frame counter (= this payload's `seq`),
162
- * `agentType` the sender's resolved agent type when the producer knows it. Present exactly when
163
- * {@link agentMessage} is.
158
+ * design/385 §4.1 — the CROSS-SESSION carrier (a peer session's message drained from this session's
159
+ * own box). Its PRESENCE is the render discriminator: the model reads a top-level
160
+ * `<cross-session-message from="…" …>` envelope (design/385 §4.2 model-face codec from-session /
161
+ * from-name / from-mode / from-scope as the typed record carried them; the hop chain never appears)
162
+ * followed by the cross-session discipline block, never a `<task-notification>` shell. The
163
+ * attributes are a PROJECTION of the store record's typed `peerMeta` — the text is presentation,
164
+ * the typed side record ({@link _sema_provenance}) is the authority. Minted ONLY by the engine's
165
+ * session-box drain; an external `notify()` cannot wear it.
166
+ */
167
+ crossSessionMessage?: import("../agents/cross-session-envelope.js").CrossSessionEnvelopeFields & {
168
+ body: string;
169
+ };
170
+ /**
171
+ * design/385 §1.4 d1 / §4.1 — the engine-minted PROVENANCE side record of an engine-injected peer
172
+ * frame, a typed key (never model text) so a host can attribute and correlate the injection on the
173
+ * wire. Present exactly when {@link agentMessage} OR {@link crossSessionMessage} is — one record
174
+ * per carrier, `kind` naming which. Its fields are read per `kind` (see {@link SemaProvenance}):
175
+ * on the agent-message lane `from` is the sender label the frame's attribute spells, `taskId` the
176
+ * sender's run/agent id, `seq` the producer's per-frame counter (= this payload's `seq`) and
177
+ * `agentType` the sender's resolved agent type when the producer knows it; on the cross-session
178
+ * lane `from` is the sender's ADDRESS, `taskId` the recipient's own box handle, `seq` the box seq,
179
+ * and the typed peer record rides in `peerMeta`.
164
180
  */
165
181
  _sema_provenance?: SemaProvenance;
166
182
  }
167
- /** design/385 §1.4 d1 — see {@link TaskNotificationPayload._sema_provenance}. `kind` is a closed set
168
- * with one member today; a future lane adds a member, never a second key. */
183
+ /** design/385 §1.4 d1 / §4.1 — see {@link TaskNotificationPayload._sema_provenance}. `kind` is a
184
+ * closed set of the two engine lanes that mint the record; a future lane adds a member, never a
185
+ * second key. */
169
186
  export interface SemaProvenance {
170
- kind: "agent_message";
187
+ /** `agent_message` = the same-process uplink (§1.4 d1); `cross_session_message` = the session-box
188
+ * drain (§4.1) — `from` is then the sender's ADDRESS, `taskId` the recipient's own box handle,
189
+ * `seq` the box seq, and the typed peer record rides in `peerMeta`. */
190
+ kind: "agent_message" | "cross_session_message";
171
191
  from: string;
172
192
  taskId: string;
173
193
  seq: number;
174
194
  agentType?: string;
195
+ /** cross_session_message only — the store record's typed peer metadata, verbatim (authority). */
196
+ peerMeta?: import("./mailbox-store.js").MailboxPeerMeta;
175
197
  }
176
198
  /**
177
199
  * design/144 §2 — the caller-facing input of `TaskStream.notify()`: a STRUCTURED external event to inject
@@ -278,6 +300,13 @@ export declare function renderAgentMessageFrame(frame: {
278
300
  from: string;
279
301
  body: string;
280
302
  }): string;
303
+ /**
304
+ * design/385 §4.1 — render a drained peer-session message as the model-face envelope (the §4.2
305
+ * codec: canonical attribute order, neutralized body) followed by the cross-session discipline block
306
+ * OUTSIDE the envelope. Distinct from {@link renderAgentMessageFrame}: another lane, another trust
307
+ * level, another block — sharing either would blur the classifier's lane judgment.
308
+ */
309
+ export declare function renderCrossSessionMessageFrame(frame: NonNullable<TaskNotificationPayload["crossSessionMessage"]>): string;
281
310
  export declare function renderTaskNotificationXml(n: TaskNotificationPayload): string;
282
311
  /**
283
312
  * The BETWEEN-TURNS pending lane. A task notification born while NO turn is
@@ -1,6 +1,6 @@
1
1
  import { escapeEnvelopeTag, inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
2
2
  import { PEER_MESSAGE_NOTICE } from "../agents/peer-admission.js";
3
- import { neutralizePeerBody } from "../agents/cross-session-envelope.js";
3
+ import { buildCrossSessionEnvelope, CROSS_SESSION_MESSAGE_NOTICE, neutralizePeerBody } from "../agents/cross-session-envelope.js";
4
4
  export const SYSTEM_INJECTION_PRIORITIES = ["now", "next", "later"];
5
5
  export function isSystemInjectionPriority(value) {
6
6
  return typeof value === "string" && SYSTEM_INJECTION_PRIORITIES.includes(value);
@@ -53,9 +53,15 @@ export function renderAgentMessageFrame(frame) {
53
53
  const body = escapeEnvelopeTag(AGENT_MESSAGE_TAG, neutralizePeerBody(frame.body));
54
54
  return `<${AGENT_MESSAGE_TAG} from="${attrEscape(frame.from)}">\n${body}\n</${AGENT_MESSAGE_TAG}>\n\n${PEER_MESSAGE_NOTICE}`;
55
55
  }
56
+ export function renderCrossSessionMessageFrame(frame) {
57
+ const { body, ...fields } = frame;
58
+ return `${buildCrossSessionEnvelope(fields, body)}\n\n${CROSS_SESSION_MESSAGE_NOTICE}`;
59
+ }
56
60
  export function renderTaskNotificationXml(n) {
57
61
  if (n.agentMessage !== undefined)
58
62
  return renderAgentMessageFrame(n.agentMessage);
63
+ if (n.crossSessionMessage !== undefined)
64
+ return renderCrossSessionMessageFrame(n.crossSessionMessage);
59
65
  const usage = n.usage === undefined
60
66
  ? undefined
61
67
  : (() => {
@@ -211,7 +217,7 @@ export function discloseDroppedPending(drained) {
211
217
  if (dropped === undefined || disclosed.has(lane))
212
218
  return [n];
213
219
  disclosed.add(lane);
214
- if (n.agentMessage !== undefined) {
220
+ if (n.agentMessage !== undefined || n.crossSessionMessage !== undefined) {
215
221
  const line = {
216
222
  task_id: n.task_id,
217
223
  task_type: n.task_type,
@@ -55,6 +55,7 @@ export function defineTool(spec, options) {
55
55
  ...(spec.alwaysLoad !== undefined ? { alwaysLoad: spec.alwaysLoad } : {}),
56
56
  ...(spec.prepareArguments ? { prepareArguments: spec.prepareArguments } : {}),
57
57
  ...(spec.approvalPreview ? { approvalPreview: spec.approvalPreview } : {}),
58
+ ...(spec.validateInput ? { validateInput: spec.validateInput } : {}),
58
59
  execute: async (toolCallId, rawParams, signal) => {
59
60
  const params = spec.prepareArguments ? spec.prepareArguments(rawParams) : rawParams;
60
61
  if (!Value.Check(spec.parameters, params)) {
@@ -1,5 +1,5 @@
1
1
  import type { TSchema } from "typebox";
2
- import type { AgentTool, ThinkingLevel } from "../internal/harness.js";
2
+ import type { AgentTool, ThinkingLevel, ToolInputValidationContext, ToolInputVerdict } from "../internal/harness.js";
3
3
  import type { ActorAssertion, CompleteSimpleFn, DocumentContent, ImageContent, Model, ResilienceOptions, StreamFn, TextContent } from "../internal/llm.js";
4
4
  import type { TaskNotificationPayload } from "./task-notification.js";
5
5
  /** A model reference: either a fully-formed Model, or a `"provider/id"` ref resolved via a catalog. */
@@ -241,6 +241,24 @@ export interface ToolSpec<TParams extends TSchema = TSchema> {
241
241
  /** approval display projection (see AgentTool.approvalPreview): pure args→display value
242
242
  * for human approval surfaces; clamped + throw-swallowed by the gate; never adjudication input. */
243
243
  approvalPreview?: (args: unknown) => unknown;
244
+ /**
245
+ * OPTIONAL input pre-validation — runs BEFORE the permission ask (and before every other tool-call
246
+ * gate station: PreToolUse hooks, policy, the auto-mode classifier), AFTER schema validation. A
247
+ * refusal (`{ ok: false, message }`) is returned to the model as a typed error result and NO
248
+ * approval card is shown for the call; `{ ok: true }` / `undefined` lets the call proceed to the
249
+ * gate unchanged. Upstream form (CC 2.1.250 `validateInput` → `{result:false, message, errorCode}`,
250
+ * consulted ahead of the permission prompt): a call the tool would refuse on its own precondition —
251
+ * an Edit/Write/NotebookEdit whose target file was never Read this session — used to reach the
252
+ * operator as a card that could only ever fail; the model then retried the same failing call under
253
+ * auto mode until the turn cap. A validator MUST be pure (read tracking state, path grammar; no
254
+ * side effects, no writes, nothing the run observes) and MUST refuse with the SAME text its
255
+ * execution would have produced for that input, so the model sees one refusal either way. A
256
+ * validator that THROWS is read as "no verdict": the call proceeds to the gate and the fault is
257
+ * disclosed via `RunnerDeps.onError` (phase `"hook"`) — a broken validator must not refuse tools.
258
+ * The execution-time check stays in place (validate and execute are two reads; a direct
259
+ * `AgentTool.execute` caller — durable replay — never passes this seat).
260
+ */
261
+ validateInput?: (args: unknown, ctx: ToolInputValidationContext) => Promise<ToolInputVerdict | undefined> | ToolInputVerdict | undefined;
244
262
  /**
245
263
  * [c209-C] Q4 — the model catalog names (`SubagentToolOptions.models` keys), filled by
246
264
  * `createSubagentTool` alongside {@link agentListing}. Rendered as the agent_listing INITIAL frame's
@@ -1032,7 +1050,8 @@ export interface ToolExecuteContext {
1032
1050
  } | undefined;
1033
1051
  /**
1034
1052
  * RB-201 FO-3 (form-one audit, CC 220 `Ipd`/`ein` parity) — the auto-mode classifier decider ARMED
1035
- * for THIS task (`RuntimeCaps.autoMode === true` AND `RunnerDeps.autoMode` both present; the same
1053
+ * for THIS task (auto-mode intent `RunnerDeps.autoMode` present `RuntimeCaps.autoMode !== false`
1054
+ * — see {@link TaskSpec.autoModeRequested}; the same
1036
1055
  * instance `runToolGate`'s per-call ask review already consults, carrying its own live breaker
1037
1056
  * state — not a fresh one built from raw config). Runner-filled, trusted, undefined when auto-mode
1038
1057
  * is not armed or the tool runs outside a Runner task.
@@ -2608,6 +2627,35 @@ export interface TaskSpec {
2608
2627
  * pending call itself runs once, having been adjudicated). Re-pass the same value the original task used.
2609
2628
  */
2610
2629
  shellGate?: "off" | "always" | "classify";
2630
+ /**
2631
+ * The caller's AUTO-MODE INTENT for this task — the "user turned auto on" half of the classifier
2632
+ * arming (CC 2.1.250 polarity: auto mode is something the USER enables at the permission-mode
2633
+ * seat, and something an ORGANIZATION may only DENY; it is never something an organization grants
2634
+ * on a user's behalf). The engine arms its per-run classifier only when ALL THREE hold:
2635
+ * - this seat is `true` (intent),
2636
+ * - {@link RunnerDeps.autoMode} is present (the deployment is classifier-capable — the trust gate;
2637
+ * classifier RULES still enter ONLY through that face, never through this seat), and
2638
+ * - {@link RuntimeCaps.autoMode} is not `false` (the per-principal DENY bit; ABSENT is NOT a denial).
2639
+ *
2640
+ * **Ownership** — same split as {@link shellGate}: the service layer TRANSLATES the effective
2641
+ * permission mode into this seat (at the same point it translates the mode into `shellGate`);
2642
+ * core only CONSUMES it. Absent ⇒ not an auto-mode task (byte-identical to the pre-seat shape:
2643
+ * asks flow the original chain). Only the literal `true` is a value here: any other present value
2644
+ * (`false`, `"true"`, `1`) is refused at the door (`config.auto_mode_requested_invalid`) rather than
2645
+ * read as either polarity — a marshalled string must never silently arm, or silently disarm, a
2646
+ * classifier.
2647
+ *
2648
+ * **Inheritance**: the intent is session-wide, like a permission mode — an engine-spawned child of
2649
+ * an auto-mode task inherits it through the trusted constraint chain
2650
+ * (`InheritedGate.autoModeRequested`), never through a model-authored argument; the child's own
2651
+ * deny bit and deployment face are still evaluated for the child. **On resume:** the intent the
2652
+ * suspend leg resolved (this seat, or the bit its chain carried) is recorded on the checkpoint's
2653
+ * data half (`CheckpointState.inheritedGate.autoModeRequested`) and read back as one more intent
2654
+ * source, so a redemption in another process arms as the suspend leg did without re-passing the
2655
+ * seat; re-passing it is still honoured (the sources fold by OR). Intent only — the resuming
2656
+ * deployment's face and the resuming principal's deny bit are judged afresh on every leg.
2657
+ */
2658
+ autoModeRequested?: true;
2611
2659
  /** Task-scoped MCP servers, materialized into tools then disposed. */
2612
2660
  mcp?: McpServerSpec[];
2613
2661
  /** Task-scoped A2A peers (remote agents), whose advertised skills mount as `a2a__<peer>__<skill>`
@@ -3964,11 +4012,25 @@ export type BrainStatusPhase = "rate_limited" | "retrying" | "reconnecting" | "c
3964
4012
  * RB-420-c — TERMINAL frames closing a retry sequence. Without them a consumer that rendered a
3965
4013
  * `rate_limited`/`retrying`/`reconnecting` frame has nothing telling it the wait is over, so the
3966
4014
  * warning row stays pinned until the next unrelated event. Exactly one terminal frame is emitted,
3967
- * and only for a call that actually waited: `recovered` = a later attempt served the call,
3968
- * `gave_up` = the retry budget ran out (or the call was cancelled) and a terminal error follows.
4015
+ * and only for a call that actually waited (a retry wait, or a `waiting_first_token` trail
4016
+ * #530): `recovered` = the call was served after the wait (a later attempt, or the first token
4017
+ * finally arriving), `gave_up` = the retry budget ran out (or the call was cancelled) and a
4018
+ * terminal error follows.
3969
4019
  * Both mean the same thing to a renderer — stop showing the retry state.
3970
4020
  */
3971
- | "recovered" | "gave_up";
4021
+ | "recovered" | "gave_up"
4022
+ /**
4023
+ * #530 — the first-token WAIT, before any failure: a first-token watchdog is armed
4024
+ * (`firstTokenTimeoutMs > 0`) and nothing has arrived yet. Emitted once the wait has lasted
4025
+ * `WAITING_FIRST_TOKEN_AFTER_MS` and re-emitted every `WAITING_FIRST_TOKEN_EVERY_MS` while it goes
4026
+ * on, carrying {@link BrainStatus.elapsedMs} and {@link BrainStatus.timeoutMs}; never carries a
4027
+ * retry countdown (`retryInMs`/`retryAtMs` — no wait is being announced, a wait is being
4028
+ * OBSERVED). CLOSED like a retry trail: once a wait frame went out, the call's terminal frame
4029
+ * follows — `recovered` when the call went on to be served (the first token arrived, on this
4030
+ * attempt or a later one), `gave_up` when it failed for good — so a renderer that pinned the row
4031
+ * has its "stop" signal. No frame at all when no watchdog is armed.
4032
+ */
4033
+ | "waiting_first_token";
3972
4034
  /**
3973
4035
  * WHY a retry wait is happening, as a closed, provider-NEUTRAL bucket — the companion to
3974
4036
  * {@link BrainStatusPhase}, which says what the brain is doing about it. A consumer rendering an
@@ -4023,6 +4085,13 @@ export interface BrainStatus {
4023
4085
  * the authoritative RELATIVE quantity, and this seat is the absolute convenience derived from it.
4024
4086
  */
4025
4087
  retryAtMs?: number;
4088
+ /** #530 — on a `waiting_first_token` frame: how long the first token has been awaited, ms, at the
4089
+ * moment the frame is emitted (the emitting process's clock; approximate across a hop). Absent on
4090
+ * every other phase. */
4091
+ elapsedMs?: number;
4092
+ /** #530 — on a `waiting_first_token` frame: the armed first-token watchdog, ms — the wait's ceiling,
4093
+ * so a consumer can render "45s of 120s". Absent on every other phase. */
4094
+ timeoutMs?: number;
4026
4095
  /** RB-420-c — 1-based index of the attempt that just failed (the wait precedes attempt `attempt + 1`);
4027
4096
  * same numbering as the `brain.retry` telemetry frame. Absent on frames that are not a retry wait. */
4028
4097
  attempt?: number;
@@ -5248,9 +5317,16 @@ export interface WorkflowGovernanceBaseline {
5248
5317
  /**
5249
5318
  * design/99 §K — the per-principal runtime ENTITLEMENTS the engine enforces server-side, resolved by a
5250
5319
  * deployment via {@link RunnerDeps.runtimeCapsResolver} from center's `GET /api/config/effective?principal=`.
5251
- * Every field is TIGHTEN-ONLY (a cap only ever DENIES or FORCES; `undefined` = no per-principal restriction).
5252
- * Only the two caps the ENGINE can enforce live here; `allowUltracode`/`allowBypassPermissions` are
5253
- * shell-UX / service-settings-layer concerns (see {@link RunnerDeps.runtimeCapsResolver}).
5320
+ * Every field is TIGHTEN-ONLY (a cap only ever DENIES or FORCES) but the family carries THREE
5321
+ * polarities, and "absent" does not read the same on each:
5322
+ * · DENY-shaped, `false` denies / absent is no restriction: {@link allowWorkflows}, {@link allowFork},
5323
+ * {@link autoMode} (the per-principal deny bit of a user-enabled mode), {@link allowMemoryOptOut}
5324
+ * (the one member whose resolver-FAULT degrade is allow, on the privacy axis);
5325
+ * · GRANT-shaped, `=== true` opts in / absent is OFF: {@link allowObservers};
5326
+ * · FORCE-shaped, `true` mandates / absent is the caller's opt-in default: {@link forceDurableGate}
5327
+ * (binds only where a checkpoint store is wired; announced `config.durable_gate_unavailable` where none is).
5328
+ * These six are the caps the ENGINE enforces; `allowUltracode`/`allowBypassPermissions` are shell-UX /
5329
+ * service-settings-layer concerns (see {@link RunnerDeps.runtimeCapsResolver}).
5254
5330
  */
5255
5331
  export interface RuntimeCaps {
5256
5332
  /** `false` DENIES workflow self-orchestration for this principal server-side — even on a deployment that is
@@ -5279,18 +5355,29 @@ export interface RuntimeCaps {
5279
5355
  /** `true` FORCES this principal's run onto the durable-approval path: the engine synthesizes a
5280
5356
  * `durableApproval{scope: principal}` so a policy `ask` suspends to the wire (a per-tool CC-faithful gate)
5281
5357
  * even when the caller did not opt in — center's fleet-wide "interactive runs gate" mandate. A
5282
- * caller-supplied `TaskSpec.durableApproval` always wins (it may carry a tighter scope/ttl). */
5358
+ * caller-supplied `TaskSpec.durableApproval` always wins (it may carry a tighter scope/ttl).
5359
+ * The mandate binds only where a {@link RunnerDeps.checkpointStore} is wired — the park facility it
5360
+ * forces the run onto. On a store-less deployment it has no facility: the leg announces
5361
+ * `config.durable_gate_unavailable` once (before its first ask), every ask resolves on the live chain
5362
+ * (a live approver / question face in-stream, or the fail-closed deny with none), no durable record
5363
+ * is written, and a live question face is consulted rather than vetoed (a mandate that cannot park
5364
+ * must not demote a reachable person into the model answering for them). */
5283
5365
  forceDurableGate?: boolean;
5284
5366
  /**
5285
- * design/143 批2 (CC 2.1.207 auto mode): route a surviving permission `ask` to the
5286
- * small-model security CLASSIFIER before any human/durable resolution (allow run; block deny
5287
- * `source:"classifier"`; classifier failure FAIL-CLOSED back to the original ask chain).
5288
- * **POLARITY: `=== true` EXPLICIT opt-in, default OFF** (same as {@link allowObservers} — auto mode
5289
- * hands a model the ask-resolution power for this principal's session; that is never implied).
5290
- * Arming ALSO requires the deployment to be classifier-capable ({@link RunnerDeps.autoMode} the
5291
- * trust gate lives there: classifier RULES enter only through the deployment assembly face, never a
5292
- * repo-controlled plane; the CC 2.1.207 three-source invariant). `true` on an incapable
5293
- * deployment resolves to a no-op (asks flow the original chain; no warn the gate, not a mistake).
5367
+ * The per-principal auto-mode DENY bit (CC 2.1.250 polarity the mirror of the settings-plane
5368
+ * `permissions.disableAutoMode` ratchet: an organization may only take auto mode AWAY; it is the
5369
+ * USER who turns it on, at the task's intent seat {@link TaskSpec.autoModeRequested}).
5370
+ *
5371
+ * **POLARITY: `=== false` DENIES; ABSENT IS NOT A DENIAL.** This is the family's deny-shaped
5372
+ * member (like {@link allowWorkflows} / {@link allowFork}), NOT a grant-shaped one like
5373
+ * {@link allowObservers}: a deployment with no entitlement source at all resolves `undefined` here
5374
+ * and still arms on intent + capability. `true` is accepted and means the same as absent (no
5375
+ * per-principal restriction) it is not a grant, and it cannot arm a task whose intent seat is
5376
+ * unset. The arming itself: intent ∧ {@link RunnerDeps.autoMode} present ∧ this bit not `false`
5377
+ * (allow → run; block → deny `source:"classifier"`; classifier failure → FAIL-CLOSED back to the
5378
+ * original ask chain). The trust gate is unchanged: classifier RULES enter only through the
5379
+ * deployment assembly face, never a repo-controlled plane (the CC 2.1.207 three-source invariant).
5380
+ * A denial resolves to a no-op (asks flow the original chain; no warn — the gate, not a mistake).
5294
5381
  */
5295
5382
  autoMode?: boolean;
5296
5383
  /**
@@ -5770,6 +5857,23 @@ export interface EngineNotice {
5770
5857
  * `onNotice` sink per value (console arm once per process) instead of lying in wait; where the
5771
5858
  * value IS in force the prepare refuses with the same code as `TaskResult.errorCode` (one
5772
5859
  * fact, one code, two loudness dialects); `detail: { raw }`.
5860
+ * - `"config.durable_gate_unavailable"` — the per-principal `forceDurableGate` entitlement is in
5861
+ * force on a leg whose deployment wired NO checkpoint store. The entitlement is a mandate
5862
+ * ("every interactive ask of this principal gates durably"), and a mandate with no park
5863
+ * facility cannot be honored: no ask of the leg can suspend, so each one resolves on the LIVE
5864
+ * chain — a live approver / question face answers in-stream, and with none the ask is denied
5865
+ * fail-closed — and NO durable approval record is written. The ask routing itself is not
5866
+ * changed by the notice (a store-less leg never could park); the notice is the loud half of a
5867
+ * posture that used to degrade silently, disclosed before the first ask of the leg. Audience
5868
+ * `"user"` (the person whose asks will not be recorded is entitled to that; the operator hears
5869
+ * it through the sink as always). Once per prepared task leg — a resume or a delegated child
5870
+ * is its own leg and says so again; `detail: { sessionId, runId, principal?, cause, liveApprover,
5871
+ * liveQuestionFace }` — `cause` is `"no_deployment_store"` (the deployment wired none) or
5872
+ * `"task_store_null"` (`TaskSpec.checkpointStore: null`, the per-run off switch, on a leg
5873
+ * whose deployment may well have a store: the fix is the task's); `liveApprover` /
5874
+ * `liveQuestionFace`: whether a live seat will EFFECTIVELY answer the leg's permission asks /
5875
+ * questions in-stream (a delegated child under an inherited content mandate reads `false`
5876
+ * even with a face wired — the marker withholds the question), or the fail-closed deny will.
5773
5877
  *
5774
5878
  * - `"task.user_steer_undrained"` / `"task.user_followup_undrained"` (#259) — user steers /
5775
5879
  * follow-ups whose receipts said "queued" were still in their queue at agent_end: the run
@@ -6021,6 +6125,24 @@ export interface EngineNotice {
6021
6125
  * disposition table with the error dispositions. The loser MUST NOT retry into the winner's
6022
6126
  * account. A consumer diffing its own table against the catalog must not add a row for it.
6023
6127
  *
6128
+ * - `"config.peer_lane_unmounted"` (design/385 §1.2⑥) — `RunnerDeps.peerDirectory` is wired but
6129
+ * the cross-session lane could not mount on this leg: no `mailboxStore`, or one that does not
6130
+ * declare `crossProcessSafe: true` (several terminals would share one session box on luck). The
6131
+ * seat is inert for the run (no ListAgents, peer addresses refuse with the same reason). Once per
6132
+ * prepared leg; `detail: { reason, code, mailboxWired, sessionId, runId }`. Audience `"operator"`
6133
+ * (a wiring fact; the fix is the deployment's).
6134
+ * - `"peer.inbound_disposition"` (design/385 §1.2④ / §4.6) — a message parked in THIS session's
6135
+ * own box was settled at the drain WITHOUT reaching the model. `detail.disposition` is the arm:
6136
+ * `"refused"` (this session's `crossSessionInbound` setting, or a record whose typed fields
6137
+ * cannot be rendered canonically), `"admission_refused"` (the drain-stage admission re-check:
6138
+ * duplicate / hop loop / runaway — `cause` names it), `"notice_unrouted"` (a notice-kind record
6139
+ * — idle/delivery notice — met a build with no notice face: settled without delivery, never
6140
+ * ridden through the peer-message envelope), or `"held"` (mode parity held it; the
6141
+ * held-message review face is not mounted in this build, so the message STAYS PARKED and the
6142
+ * drain stops at it — disclosed once per seq per leg). `detail: { disposition, cause, seq, box,
6143
+ * fromSession?, sessionId, runId }`. Audience `"user"`: the session's user is the one who did
6144
+ * not receive it.
6145
+ *
6024
6146
  * - `"delegation.transcript_integrity"` (subagent transcript persistence) — a durable agent row
6025
6147
  * with a BOUND transcript sessionId met a session store that attests `not_found` for it: the
6026
6148
  * deployment's declared transcript durability is being contradicted by reality. Announced at
@@ -6661,11 +6783,15 @@ export interface RunnerDeps {
6661
6783
  * resolver, or an unset cap) = NO per-principal restriction (TIGHTEN-ONLY: a cap can only ever DENY or
6662
6784
  * FORCE; absence falls back to the deployment-level default). Symmetric with {@link sessionPolicyStore}.
6663
6785
  *
6664
- * core enforces only the caps it CAN enforce in the engine: `allowWorkflows` (the third stage of the
6665
- * workflows gate — task opt-in ∧ deployment capability ∧ this) and `forceDurableGate` (forces a run onto
6666
- * the durable-approval path so a policy `ask` suspends to the wire). `allowUltracode` is a shell/UX concern
6667
- * (reasoning-tier, not a core primitive); `allowBypassPermissions` is enforced at the service settings-
6668
- * resolution layer (the engine sees only a resolved policy, never a "this allow came from bypass" signal).
6786
+ * core enforces the six caps it CAN enforce in the engine (polarity per member on {@link RuntimeCaps}):
6787
+ * `allowWorkflows` (the third stage of the workflows gate — task opt-in ∧ deployment capability ∧ this),
6788
+ * `allowFork` (the Agent-fork route), `allowObservers` (explicit opt-in for observer auto-spawn),
6789
+ * `autoMode` (the per-principal deny bit of the user-enabled auto mode), `allowMemoryOptOut` (may this
6790
+ * principal declare a capture opt-out), and `forceDurableGate` (forces a run onto the durable-approval
6791
+ * path so a policy `ask` suspends to the wire — where a checkpoint store is wired; a store-less leg
6792
+ * announces `config.durable_gate_unavailable`). `allowUltracode` is a shell/UX concern (reasoning-tier,
6793
+ * not a core primitive); `allowBypassPermissions` is enforced at the service settings-resolution layer
6794
+ * (the engine sees only a resolved policy, never a "this allow came from bypass" signal).
6669
6795
  */
6670
6796
  runtimeCapsResolver?: (principal: string | undefined) => RuntimeCaps | undefined | Promise<RuntimeCaps | undefined>;
6671
6797
  /**
@@ -6734,8 +6860,10 @@ export interface RunnerDeps {
6734
6860
  retentionPolicy?: import("./retention.js").RetentionPolicy;
6735
6861
  /**
6736
6862
  * design/143 批2b — the auto-mode classifier's DEPLOYMENT assembly face. This is the
6737
- * capability half of the arming AND-gate: a run arms auto mode only when the per-principal
6738
- * entitlement grants it ({@link RuntimeCaps.autoMode}` === true`) AND this face is present.
6863
+ * CAPABILITY arm of the three-arm arming: a run arms auto mode only when the task carries the
6864
+ * auto-mode INTENT ({@link TaskSpec.autoModeRequested}, or the same bit inherited on the chain by an
6865
+ * engine-spawned child) AND this face is present AND the per-principal DENY bit is not set
6866
+ * ({@link RuntimeCaps.autoMode}` !== false` — absent is not a denial).
6739
6867
  *
6740
6868
  * 🔐 Trust gate (the CC 2.1.207 three-source invariant): classifier RULES enter EXCLUSIVELY
6741
6869
  * here — a deployment-constructed object, never a `TaskSpec` field, never a repo-file plane. The
@@ -6901,6 +7029,28 @@ export interface RunnerDeps {
6901
7029
  * absent means the default table, not "guard off". Threaded into the auto-mounted SendMessage as
6902
7030
  * `SendMessageToolOptions.admission`; read per call (a value change governs the next message). */
6903
7031
  peerAdmission?: Partial<import("../agents/peer-admission.js").PeerAdmissionConfig>;
7032
+ /**
7033
+ * design/385 §2.1 / §5.1 — the peer-session DIRECTORY: the discovery truth of the cross-session
7034
+ * lane (other sessions of this engine for the same user). A HOST implements the contract (cli: the
7035
+ * pid-keyed registration files; server: its session table) and writes the rows; the engine only
7036
+ * reads them. Seat semantics = the mailbox seat's: NO seat, NO lane — every face stays byte-identical
7037
+ * to a pre-385 build. With the seat wired the engine (a) mounts `ListAgents` (alias `ListPeers`),
7038
+ * (b) opens SendMessage's `session.<id>` address arm and its last `name [ref]` rung, (c) drains this
7039
+ * session's own `session.<sessionId>` box at every turn boundary (the mailbox contract's third
7040
+ * consumption chain), and (d) splices the cross-session rule into the auto-mode classifier slot.
7041
+ * HARD PRECONDITION (design/385 §1.2⑥): `mailboxStore` must be wired AND declare
7042
+ * `crossProcessSafe: true`; otherwise the lane is REFUSED at prepare with a named
7043
+ * `config.peer_lane_unmounted` notice (never mounted on luck), and the seat is inert for that run.
7044
+ */
7045
+ peerDirectory?: import("../agents/peer-directory.js").PeerDirectory;
7046
+ /**
7047
+ * design/385 §4.4 — this deployment's `crossSessionInbound` setting LAYERS for the drain-point
7048
+ * judgment (managed / user / repo, resolved by `resolveCrossSessionInboundSetting` — explicit value
7049
+ * wins, unset ⇒ mode parity, an unrecognized value forces `hold` loudly). Read at every drain, so a
7050
+ * host that re-reads its settings files hands the current layers through a getter. Absent = unset
7051
+ * everywhere (mode parity, the CC default). Inert without {@link peerDirectory}.
7052
+ */
7053
+ crossSessionInbound?: import("../agents/cross-session-judge.js").CrossSessionInboundSettingLayers | (() => import("../agents/cross-session-judge.js").CrossSessionInboundSettingLayers);
6904
7054
  /**
6905
7055
  * design/164 件五 — DEPLOYMENT-level usage governance: allowances that span TASKS, evaluated per
6906
7056
  * principal (or once for the whole deployment when a task declares none). A different axis from
@@ -7431,4 +7581,4 @@ export interface RunnerDeps {
7431
7581
  * 3, CC parity). The Runner tracks the consecutive-reuse counter per task across both compaction sites. */
7432
7582
  maxConsecutiveProviderReuse?: number;
7433
7583
  }
7434
- export type { AgentTool, Model, StreamFn, ThinkingLevel };
7584
+ export type { AgentTool, Model, StreamFn, ThinkingLevel, ToolInputValidationContext, ToolInputVerdict };