@warlock.js/ai 4.15.0 → 4.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +182 -163
- package/cjs/index.cjs +637 -104
- package/cjs/index.cjs.map +1 -1
- package/esm/contracts/index.d.mts +2 -2
- package/esm/contracts/memory/index.d.mts +1 -1
- package/esm/contracts/memory/memory-config.type.d.mts +29 -3
- package/esm/contracts/memory/memory-config.type.d.mts.map +1 -1
- package/esm/contracts/memory/memory-item.type.d.mts +15 -1
- package/esm/contracts/memory/memory-item.type.d.mts.map +1 -1
- package/esm/contracts/memory/memory.contract.d.mts +15 -2
- package/esm/contracts/memory/memory.contract.d.mts.map +1 -1
- package/esm/contracts/memory/recall-options.type.d.mts +12 -0
- package/esm/contracts/memory/recall-options.type.d.mts.map +1 -1
- package/esm/contracts/orchestrator/index.d.mts +1 -1
- package/esm/contracts/orchestrator/orchestrator-config.type.d.mts +38 -1
- package/esm/contracts/orchestrator/orchestrator-config.type.d.mts.map +1 -1
- package/esm/contracts/orchestrator/orchestrator.contract.d.mts +67 -3
- package/esm/contracts/orchestrator/orchestrator.contract.d.mts.map +1 -1
- package/esm/contracts/supervisor/supervisor-config.type.d.mts +23 -0
- package/esm/contracts/supervisor/supervisor-config.type.d.mts.map +1 -1
- package/esm/index.d.mts +6 -5
- package/esm/index.mjs +3 -2
- package/esm/memory/episodic-memory.mjs +14 -6
- package/esm/memory/episodic-memory.mjs.map +1 -1
- package/esm/memory/index.d.mts +1 -1
- package/esm/memory/memory.d.mts +13 -1
- package/esm/memory/memory.d.mts.map +1 -1
- package/esm/memory/memory.mjs +41 -7
- package/esm/memory/memory.mjs.map +1 -1
- package/esm/memory/procedural-memory.mjs +20 -7
- package/esm/memory/procedural-memory.mjs.map +1 -1
- package/esm/memory/semantic-memory.mjs +27 -10
- package/esm/memory/semantic-memory.mjs.map +1 -1
- package/esm/memory/working-memory.mjs +70 -13
- package/esm/memory/working-memory.mjs.map +1 -1
- package/esm/middleware/builtins/semantic-cache.d.mts +46 -1
- package/esm/middleware/builtins/semantic-cache.d.mts.map +1 -1
- package/esm/middleware/builtins/semantic-cache.mjs +60 -15
- package/esm/middleware/builtins/semantic-cache.mjs.map +1 -1
- package/esm/middleware/index.d.mts +1 -1
- package/esm/orchestrator/as-tool.d.mts +35 -9
- package/esm/orchestrator/as-tool.d.mts.map +1 -1
- package/esm/orchestrator/as-tool.mjs +67 -19
- package/esm/orchestrator/as-tool.mjs.map +1 -1
- package/esm/orchestrator/execution.d.mts.map +1 -1
- package/esm/orchestrator/execution.mjs +2 -2
- package/esm/orchestrator/execution.mjs.map +1 -1
- package/esm/orchestrator/index.d.mts +1 -1
- package/esm/orchestrator/index.mjs +1 -1
- package/esm/orchestrator/memory.d.mts +41 -5
- package/esm/orchestrator/memory.d.mts.map +1 -1
- package/esm/orchestrator/memory.mjs +53 -5
- package/esm/orchestrator/memory.mjs.map +1 -1
- package/esm/planner/plan-schema.d.mts +3 -3
- package/esm/planner/plan-schema.d.mts.map +1 -1
- package/esm/planner/plan-schema.mjs +30 -0
- package/esm/planner/plan-schema.mjs.map +1 -1
- package/esm/security/index.mjs +1 -0
- package/esm/security/outbound-policy.d.mts +9 -0
- package/esm/security/outbound-policy.d.mts.map +1 -1
- package/esm/security/outbound-policy.mjs +79 -5
- package/esm/security/outbound-policy.mjs.map +1 -1
- package/esm/security/outbound-policy.type.d.mts +8 -0
- package/esm/security/outbound-policy.type.d.mts.map +1 -1
- package/esm/security/safe-merge.d.mts +52 -0
- package/esm/security/safe-merge.d.mts.map +1 -0
- package/esm/security/safe-merge.mjs +68 -0
- package/esm/security/safe-merge.mjs.map +1 -0
- package/esm/supervisor/decide.mjs +52 -5
- package/esm/supervisor/decide.mjs.map +1 -1
- package/esm/supervisor/execution.d.mts +22 -0
- package/esm/supervisor/execution.d.mts.map +1 -1
- package/esm/supervisor/execution.mjs +46 -9
- package/esm/supervisor/execution.mjs.map +1 -1
- package/esm/supervisor/supervisor.mjs +4 -0
- package/esm/supervisor/supervisor.mjs.map +1 -1
- package/llms-full.txt +174 -10
- package/llms.txt +4 -3
- package/package.json +4 -4
- package/skills/README.md +5 -1
- package/skills/attach-ai-middleware/SKILL.md +17 -1
- package/skills/rag-loaders-and-stores/SKILL.md +3 -0
- package/skills/run-ai-agent/SKILL.md +3 -0
- package/skills/run-orchestrator/SKILL.md +6 -1
- package/skills/run-planner/SKILL.md +7 -3
- package/skills/run-supervisor/SKILL.md +11 -1
- package/skills/secure-outbound-requests/SKILL.md +85 -0
- package/skills/use-ai-memory/SKILL.md +36 -3
- package/skills/use-runtime-skills/SKILL.md +2 -1
|
@@ -9,6 +9,22 @@ import { isIP } from "node:net";
|
|
|
9
9
|
const DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
|
|
10
10
|
/** 10s — default per-request timeout. */
|
|
11
11
|
const DEFAULT_TIMEOUT_MS = 1e4;
|
|
12
|
+
/** Default cap on the number of policy-validated redirect hops. */
|
|
13
|
+
const DEFAULT_MAX_REDIRECTS = 5;
|
|
14
|
+
/** 3xx statuses whose `Location` a follow re-issues. */
|
|
15
|
+
const REDIRECT_STATUSES = new Set([
|
|
16
|
+
301,
|
|
17
|
+
302,
|
|
18
|
+
303,
|
|
19
|
+
307,
|
|
20
|
+
308
|
|
21
|
+
]);
|
|
22
|
+
/** Credential headers that must not survive a cross-origin redirect. */
|
|
23
|
+
const CROSS_ORIGIN_STRIP_HEADERS = [
|
|
24
|
+
"authorization",
|
|
25
|
+
"cookie",
|
|
26
|
+
"proxy-authorization"
|
|
27
|
+
];
|
|
12
28
|
/**
|
|
13
29
|
* Fill an {@link OutboundPolicy} with strict defaults: https-only,
|
|
14
30
|
* private-IP deny on, 10s timeout, 5 MiB cap, global `fetch`. Idempotent
|
|
@@ -21,6 +37,7 @@ function resolveOutboundPolicy(policy = {}) {
|
|
|
21
37
|
denyPrivateIPsAfterDNS: policy.denyPrivateIPsAfterDNS ?? true,
|
|
22
38
|
maxBytes: policy.maxBytes ?? DEFAULT_MAX_BYTES,
|
|
23
39
|
timeoutMs: policy.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
40
|
+
maxRedirects: policy.maxRedirects ?? DEFAULT_MAX_REDIRECTS,
|
|
24
41
|
signal: policy.signal,
|
|
25
42
|
fetch: policy.fetch ?? globalThis.fetch
|
|
26
43
|
};
|
|
@@ -107,16 +124,33 @@ function mergeSignals(timeout, external) {
|
|
|
107
124
|
else external.addEventListener("abort", () => abort(external), { once: true });
|
|
108
125
|
return controller.signal;
|
|
109
126
|
}
|
|
127
|
+
/** Flatten a headers init into a mutable lower-cased-key record. */
|
|
128
|
+
function headersToRecord(headersInit) {
|
|
129
|
+
const record = {};
|
|
130
|
+
new Headers(headersInit).forEach((value, key) => {
|
|
131
|
+
record[key] = value;
|
|
132
|
+
});
|
|
133
|
+
return record;
|
|
134
|
+
}
|
|
110
135
|
/**
|
|
111
136
|
* Policy-guarded `fetch`: validates the URL ({@link assertUrlAllowed}),
|
|
112
137
|
* then performs the request with the policy's timeout and (optional)
|
|
113
138
|
* caller signal merged. Returns the raw `Response` — read its body via
|
|
114
139
|
* {@link readTextCapped} to enforce `maxBytes`. Throws
|
|
115
140
|
* {@link OutboundPolicyError} on a policy violation or timeout.
|
|
141
|
+
*
|
|
142
|
+
* Redirects are NEVER delegated to the platform: every hop is issued
|
|
143
|
+
* with `redirect: "manual"` and its `Location` is re-run through
|
|
144
|
+
* {@link assertUrlAllowed} before being followed (capped at
|
|
145
|
+
* `maxRedirects`), so a 3xx from an allowed host cannot smuggle the
|
|
146
|
+
* request to a private / metadata / off-allowlist target. Credential
|
|
147
|
+
* headers are stripped when a hop crosses an origin boundary. Pass
|
|
148
|
+
* `init.redirect: "manual"` to receive the raw 3xx, or `"error"` to
|
|
149
|
+
* reject on any redirect.
|
|
116
150
|
*/
|
|
117
151
|
async function guardedFetch(rawUrl, policyInput, init) {
|
|
118
152
|
const policy = resolveOutboundPolicy(policyInput);
|
|
119
|
-
|
|
153
|
+
let url = await assertUrlAllowed(rawUrl, policy);
|
|
120
154
|
const timeoutController = new AbortController();
|
|
121
155
|
const timer = setTimeout(() => {
|
|
122
156
|
timeoutController.abort(new OutboundPolicyError(`outbound request timed out after ${policy.timeoutMs}ms`, { context: {
|
|
@@ -124,11 +158,51 @@ async function guardedFetch(rawUrl, policyInput, init) {
|
|
|
124
158
|
timeoutMs: policy.timeoutMs
|
|
125
159
|
} }));
|
|
126
160
|
}, policy.timeoutMs);
|
|
161
|
+
const signal = mergeSignals(timeoutController.signal, policy.signal);
|
|
162
|
+
const redirectMode = init?.redirect ?? "follow";
|
|
163
|
+
const headers = headersToRecord(init?.headers);
|
|
164
|
+
let method = init?.method ?? "GET";
|
|
165
|
+
let body = init?.body ?? void 0;
|
|
127
166
|
try {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
167
|
+
for (let hop = 0;; hop++) {
|
|
168
|
+
const response = await policy.fetch(url, {
|
|
169
|
+
...init,
|
|
170
|
+
method,
|
|
171
|
+
headers: { ...headers },
|
|
172
|
+
body,
|
|
173
|
+
redirect: "manual",
|
|
174
|
+
signal
|
|
175
|
+
});
|
|
176
|
+
const location = response.headers.get("location");
|
|
177
|
+
if (!REDIRECT_STATUSES.has(response.status) || location === null) return response;
|
|
178
|
+
if (redirectMode === "manual") return response;
|
|
179
|
+
if (redirectMode === "error") throw new OutboundPolicyError(`outbound request blocked — redirect received with redirect: "error" (${response.status} → ${location})`, { context: {
|
|
180
|
+
url: url.toString(),
|
|
181
|
+
location,
|
|
182
|
+
status: response.status
|
|
183
|
+
} });
|
|
184
|
+
if (hop >= policy.maxRedirects) throw new OutboundPolicyError(`outbound request blocked — more than ${policy.maxRedirects} redirects`, { context: {
|
|
185
|
+
url: rawUrl,
|
|
186
|
+
maxRedirects: policy.maxRedirects
|
|
187
|
+
} });
|
|
188
|
+
let target;
|
|
189
|
+
try {
|
|
190
|
+
target = new URL(location, url);
|
|
191
|
+
} catch {
|
|
192
|
+
throw new OutboundPolicyError(`outbound request blocked — invalid redirect Location: ${location}`, { context: {
|
|
193
|
+
url: url.toString(),
|
|
194
|
+
location
|
|
195
|
+
} });
|
|
196
|
+
}
|
|
197
|
+
const next = await assertUrlAllowed(target.toString(), policy);
|
|
198
|
+
if (response.body) await response.body.cancel().catch(() => void 0);
|
|
199
|
+
if (next.origin !== url.origin) for (const name of CROSS_ORIGIN_STRIP_HEADERS) delete headers[name];
|
|
200
|
+
if (response.status === 303 || (response.status === 301 || response.status === 302) && method !== "GET" && method !== "HEAD") {
|
|
201
|
+
method = "GET";
|
|
202
|
+
body = void 0;
|
|
203
|
+
}
|
|
204
|
+
url = next;
|
|
205
|
+
}
|
|
132
206
|
} finally {
|
|
133
207
|
clearTimeout(timer);
|
|
134
208
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"outbound-policy.mjs","names":[],"sources":["../../../../../../../ai/src/security/outbound-policy.ts"],"sourcesContent":["import { lookup } from \"node:dns/promises\";\nimport { isIP } from \"node:net\";\nimport { OutboundPolicyError } from \"../errors\";\nimport { isPrivateOrReservedIp } from \"./private-ip\";\nimport type {\n OutboundPolicy,\n ResolvedOutboundPolicy,\n} from \"./outbound-policy.type\";\n\n/** 5 MiB — default cap on an outbound response body. */\nconst DEFAULT_MAX_BYTES = 5 * 1024 * 1024;\n/** 10s — default per-request timeout. */\nconst DEFAULT_TIMEOUT_MS = 10_000;\n\n/**\n * Fill an {@link OutboundPolicy} with strict defaults: https-only,\n * private-IP deny on, 10s timeout, 5 MiB cap, global `fetch`. Idempotent\n * — resolving an already-resolved policy yields the same shape.\n */\nexport function resolveOutboundPolicy(\n policy: OutboundPolicy = {},\n): ResolvedOutboundPolicy {\n return {\n allowedSchemes: policy.allowedSchemes ?? [\"https\"],\n hostAllowlist: policy.hostAllowlist,\n denyPrivateIPsAfterDNS: policy.denyPrivateIPsAfterDNS ?? true,\n maxBytes: policy.maxBytes ?? DEFAULT_MAX_BYTES,\n timeoutMs: policy.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n signal: policy.signal,\n fetch: policy.fetch ?? globalThis.fetch,\n };\n}\n\n/** Strip the `[ ]` IPv6 brackets `URL.hostname` keeps. */\nfunction stripBrackets(host: string): string {\n return host.startsWith(\"[\") && host.endsWith(\"]\") ? host.slice(1, -1) : host;\n}\n\n/** Whether `host` equals or is a subdomain of any allowlist entry. */\nfunction hostAllowed(host: string, allowlist: string[]): boolean {\n const lower = host.toLowerCase();\n return allowlist.some(entry => {\n const e = entry.toLowerCase();\n return lower === e || lower.endsWith(`.${e}`);\n });\n}\n\n/**\n * Validate a URL against the policy BEFORE any network call: scheme\n * allowlist, host allowlist, and (when enabled) a DNS resolution that\n * rejects private / loopback / link-local / metadata addresses — the SSRF\n * guard. Returns the parsed `URL` on success; throws\n * {@link OutboundPolicyError} otherwise.\n */\nexport async function assertUrlAllowed(\n rawUrl: string,\n policy: ResolvedOutboundPolicy,\n): Promise<URL> {\n let url: URL;\n try {\n url = new URL(rawUrl);\n } catch {\n throw new OutboundPolicyError(`outbound request blocked — invalid URL: ${rawUrl}`, {\n context: { url: rawUrl },\n });\n }\n\n const scheme = url.protocol.replace(/:$/, \"\").toLowerCase();\n if (!policy.allowedSchemes.some(s => s.toLowerCase() === scheme)) {\n throw new OutboundPolicyError(\n `outbound request blocked — scheme \"${scheme}\" is not allowed (allowed: ${policy.allowedSchemes.join(\", \")})`,\n { context: { url: rawUrl, scheme } },\n );\n }\n\n const host = stripBrackets(url.hostname);\n\n if (policy.hostAllowlist && !hostAllowed(host, policy.hostAllowlist)) {\n throw new OutboundPolicyError(\n `outbound request blocked — host \"${host}\" is not in the allowlist`,\n { context: { url: rawUrl, host } },\n );\n }\n\n if (policy.denyPrivateIPsAfterDNS) {\n await assertHostNotPrivate(host, rawUrl);\n }\n\n return url;\n}\n\n/**\n * Reject when `host` is — or resolves to — a private / reserved address.\n * IP literals are checked directly; hostnames are resolved via DNS and\n * every returned address is checked (a public name pointing inward is\n * caught). A resolution failure fails closed.\n */\nasync function assertHostNotPrivate(host: string, rawUrl: string): Promise<void> {\n if (isIP(host) !== 0) {\n if (isPrivateOrReservedIp(host)) {\n throw new OutboundPolicyError(\n `outbound request blocked — \"${host}\" is a private/reserved address`,\n { context: { url: rawUrl, address: host } },\n );\n }\n return;\n }\n\n let addresses: Array<{ address: string }>;\n try {\n addresses = await lookup(host, { all: true });\n } catch (cause) {\n throw new OutboundPolicyError(\n `outbound request blocked — could not resolve host \"${host}\" to verify it is public`,\n { cause, context: { url: rawUrl, host } },\n );\n }\n\n for (const { address } of addresses) {\n if (isPrivateOrReservedIp(address)) {\n throw new OutboundPolicyError(\n `outbound request blocked — host \"${host}\" resolves to a private/reserved address (${address})`,\n { context: { url: rawUrl, host, address } },\n );\n }\n }\n}\n\n/** Merge the internal timeout signal with an optional caller signal. */\nfunction mergeSignals(\n timeout: AbortSignal,\n external?: AbortSignal,\n): AbortSignal {\n if (!external) return timeout;\n\n const controller = new AbortController();\n const abort = (from: AbortSignal) => controller.abort(from.reason);\n\n if (timeout.aborted) abort(timeout);\n else timeout.addEventListener(\"abort\", () => abort(timeout), { once: true });\n\n if (external.aborted) abort(external);\n else external.addEventListener(\"abort\", () => abort(external), { once: true });\n\n return controller.signal;\n}\n\n/**\n * Policy-guarded `fetch`: validates the URL ({@link assertUrlAllowed}),\n * then performs the request with the policy's timeout and (optional)\n * caller signal merged. Returns the raw `Response` — read its body via\n * {@link readTextCapped} to enforce `maxBytes`. Throws\n * {@link OutboundPolicyError} on a policy violation or timeout.\n */\nexport async function guardedFetch(\n rawUrl: string,\n policyInput: OutboundPolicy,\n init?: RequestInit,\n): Promise<Response> {\n const policy = resolveOutboundPolicy(policyInput);\n const url = await assertUrlAllowed(rawUrl, policy);\n\n const timeoutController = new AbortController();\n const timer = setTimeout(() => {\n timeoutController.abort(\n new OutboundPolicyError(\n `outbound request timed out after ${policy.timeoutMs}ms`,\n { context: { url: rawUrl, timeoutMs: policy.timeoutMs } },\n ),\n );\n }, policy.timeoutMs);\n\n try {\n return await policy.fetch(url, {\n ...init,\n signal: mergeSignals(timeoutController.signal, policy.signal),\n });\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Read a response body as UTF-8 text with a hard byte cap. A declared\n * `content-length` over the cap fails fast; otherwise the stream is read\n * chunk-by-chunk and aborted the moment the running total exceeds\n * `maxBytes`. Throws {@link OutboundPolicyError} on overflow.\n */\nexport async function readTextCapped(\n response: Response,\n maxBytes: number,\n): Promise<string> {\n const declared = Number(response.headers.get(\"content-length\"));\n if (Number.isFinite(declared) && declared > maxBytes) {\n throw new OutboundPolicyError(\n `outbound response body too large — declared ${declared} bytes exceeds the ${maxBytes}-byte cap`,\n { context: { declared, maxBytes } },\n );\n }\n\n if (!response.body) {\n const text = await response.text();\n if (Buffer.byteLength(text) > maxBytes) {\n throw new OutboundPolicyError(\n `outbound response body exceeded the ${maxBytes}-byte cap`,\n { context: { maxBytes } },\n );\n }\n return text;\n }\n\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n if (!value) continue;\n\n total += value.byteLength;\n if (total > maxBytes) {\n await reader.cancel();\n throw new OutboundPolicyError(\n `outbound response body exceeded the ${maxBytes}-byte cap`,\n { context: { maxBytes } },\n );\n }\n chunks.push(value);\n }\n\n return Buffer.concat(chunks).toString(\"utf8\");\n}\n\n/**\n * Convenience: {@link guardedFetch} + {@link readTextCapped}. Returns the\n * response status alongside the (capped) body text so callers can shape\n * their own not-OK error. The body is only read when the response is OK.\n */\nexport async function fetchTextWithPolicy(\n rawUrl: string,\n policyInput: OutboundPolicy,\n init?: RequestInit,\n): Promise<{ ok: boolean; status: number; statusText: string; text: string }> {\n const policy = resolveOutboundPolicy(policyInput);\n const response = await guardedFetch(rawUrl, policy, init);\n\n return {\n ok: response.ok,\n status: response.status,\n statusText: response.statusText,\n text: response.ok ? await readTextCapped(response, policy.maxBytes) : \"\",\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"outbound-policy.mjs","names":[],"sources":["../../../../../../../ai/src/security/outbound-policy.ts"],"sourcesContent":["import { lookup } from \"node:dns/promises\";\nimport { isIP } from \"node:net\";\nimport { OutboundPolicyError } from \"../errors\";\nimport { isPrivateOrReservedIp } from \"./private-ip\";\nimport type {\n OutboundPolicy,\n ResolvedOutboundPolicy,\n} from \"./outbound-policy.type\";\n\n/** 5 MiB — default cap on an outbound response body. */\nconst DEFAULT_MAX_BYTES = 5 * 1024 * 1024;\n/** 10s — default per-request timeout. */\nconst DEFAULT_TIMEOUT_MS = 10_000;\n/** Default cap on the number of policy-validated redirect hops. */\nconst DEFAULT_MAX_REDIRECTS = 5;\n\n/** 3xx statuses whose `Location` a follow re-issues. */\nconst REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);\n\n/** Credential headers that must not survive a cross-origin redirect. */\nconst CROSS_ORIGIN_STRIP_HEADERS = [\n \"authorization\",\n \"cookie\",\n \"proxy-authorization\",\n];\n\n/**\n * Fill an {@link OutboundPolicy} with strict defaults: https-only,\n * private-IP deny on, 10s timeout, 5 MiB cap, global `fetch`. Idempotent\n * — resolving an already-resolved policy yields the same shape.\n */\nexport function resolveOutboundPolicy(\n policy: OutboundPolicy = {},\n): ResolvedOutboundPolicy {\n return {\n allowedSchemes: policy.allowedSchemes ?? [\"https\"],\n hostAllowlist: policy.hostAllowlist,\n denyPrivateIPsAfterDNS: policy.denyPrivateIPsAfterDNS ?? true,\n maxBytes: policy.maxBytes ?? DEFAULT_MAX_BYTES,\n timeoutMs: policy.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n maxRedirects: policy.maxRedirects ?? DEFAULT_MAX_REDIRECTS,\n signal: policy.signal,\n fetch: policy.fetch ?? globalThis.fetch,\n };\n}\n\n/** Strip the `[ ]` IPv6 brackets `URL.hostname` keeps. */\nfunction stripBrackets(host: string): string {\n return host.startsWith(\"[\") && host.endsWith(\"]\") ? host.slice(1, -1) : host;\n}\n\n/** Whether `host` equals or is a subdomain of any allowlist entry. */\nfunction hostAllowed(host: string, allowlist: string[]): boolean {\n const lower = host.toLowerCase();\n return allowlist.some(entry => {\n const e = entry.toLowerCase();\n return lower === e || lower.endsWith(`.${e}`);\n });\n}\n\n/**\n * Validate a URL against the policy BEFORE any network call: scheme\n * allowlist, host allowlist, and (when enabled) a DNS resolution that\n * rejects private / loopback / link-local / metadata addresses — the SSRF\n * guard. Returns the parsed `URL` on success; throws\n * {@link OutboundPolicyError} otherwise.\n */\nexport async function assertUrlAllowed(\n rawUrl: string,\n policy: ResolvedOutboundPolicy,\n): Promise<URL> {\n let url: URL;\n try {\n url = new URL(rawUrl);\n } catch {\n throw new OutboundPolicyError(`outbound request blocked — invalid URL: ${rawUrl}`, {\n context: { url: rawUrl },\n });\n }\n\n const scheme = url.protocol.replace(/:$/, \"\").toLowerCase();\n if (!policy.allowedSchemes.some(s => s.toLowerCase() === scheme)) {\n throw new OutboundPolicyError(\n `outbound request blocked — scheme \"${scheme}\" is not allowed (allowed: ${policy.allowedSchemes.join(\", \")})`,\n { context: { url: rawUrl, scheme } },\n );\n }\n\n const host = stripBrackets(url.hostname);\n\n if (policy.hostAllowlist && !hostAllowed(host, policy.hostAllowlist)) {\n throw new OutboundPolicyError(\n `outbound request blocked — host \"${host}\" is not in the allowlist`,\n { context: { url: rawUrl, host } },\n );\n }\n\n if (policy.denyPrivateIPsAfterDNS) {\n await assertHostNotPrivate(host, rawUrl);\n }\n\n return url;\n}\n\n/**\n * Reject when `host` is — or resolves to — a private / reserved address.\n * IP literals are checked directly; hostnames are resolved via DNS and\n * every returned address is checked (a public name pointing inward is\n * caught). A resolution failure fails closed.\n */\nasync function assertHostNotPrivate(host: string, rawUrl: string): Promise<void> {\n if (isIP(host) !== 0) {\n if (isPrivateOrReservedIp(host)) {\n throw new OutboundPolicyError(\n `outbound request blocked — \"${host}\" is a private/reserved address`,\n { context: { url: rawUrl, address: host } },\n );\n }\n return;\n }\n\n let addresses: Array<{ address: string }>;\n try {\n addresses = await lookup(host, { all: true });\n } catch (cause) {\n throw new OutboundPolicyError(\n `outbound request blocked — could not resolve host \"${host}\" to verify it is public`,\n { cause, context: { url: rawUrl, host } },\n );\n }\n\n for (const { address } of addresses) {\n if (isPrivateOrReservedIp(address)) {\n throw new OutboundPolicyError(\n `outbound request blocked — host \"${host}\" resolves to a private/reserved address (${address})`,\n { context: { url: rawUrl, host, address } },\n );\n }\n }\n}\n\n/** Merge the internal timeout signal with an optional caller signal. */\nfunction mergeSignals(\n timeout: AbortSignal,\n external?: AbortSignal,\n): AbortSignal {\n if (!external) return timeout;\n\n const controller = new AbortController();\n const abort = (from: AbortSignal) => controller.abort(from.reason);\n\n if (timeout.aborted) abort(timeout);\n else timeout.addEventListener(\"abort\", () => abort(timeout), { once: true });\n\n if (external.aborted) abort(external);\n else external.addEventListener(\"abort\", () => abort(external), { once: true });\n\n return controller.signal;\n}\n\n/** Flatten a headers init into a mutable lower-cased-key record. */\nfunction headersToRecord(\n headersInit?: RequestInit[\"headers\"],\n): Record<string, string> {\n const record: Record<string, string> = {};\n new Headers(headersInit).forEach((value, key) => {\n record[key] = value;\n });\n return record;\n}\n\n/**\n * Policy-guarded `fetch`: validates the URL ({@link assertUrlAllowed}),\n * then performs the request with the policy's timeout and (optional)\n * caller signal merged. Returns the raw `Response` — read its body via\n * {@link readTextCapped} to enforce `maxBytes`. Throws\n * {@link OutboundPolicyError} on a policy violation or timeout.\n *\n * Redirects are NEVER delegated to the platform: every hop is issued\n * with `redirect: \"manual\"` and its `Location` is re-run through\n * {@link assertUrlAllowed} before being followed (capped at\n * `maxRedirects`), so a 3xx from an allowed host cannot smuggle the\n * request to a private / metadata / off-allowlist target. Credential\n * headers are stripped when a hop crosses an origin boundary. Pass\n * `init.redirect: \"manual\"` to receive the raw 3xx, or `\"error\"` to\n * reject on any redirect.\n */\nexport async function guardedFetch(\n rawUrl: string,\n policyInput: OutboundPolicy,\n init?: RequestInit,\n): Promise<Response> {\n const policy = resolveOutboundPolicy(policyInput);\n let url = await assertUrlAllowed(rawUrl, policy);\n\n const timeoutController = new AbortController();\n const timer = setTimeout(() => {\n timeoutController.abort(\n new OutboundPolicyError(\n `outbound request timed out after ${policy.timeoutMs}ms`,\n { context: { url: rawUrl, timeoutMs: policy.timeoutMs } },\n ),\n );\n }, policy.timeoutMs);\n\n const signal = mergeSignals(timeoutController.signal, policy.signal);\n const redirectMode = init?.redirect ?? \"follow\";\n const headers = headersToRecord(init?.headers);\n let method = init?.method ?? \"GET\";\n let body = init?.body ?? undefined;\n\n try {\n for (let hop = 0; ; hop++) {\n const response = await policy.fetch(url, {\n ...init,\n method,\n headers: { ...headers },\n body,\n redirect: \"manual\",\n signal,\n });\n\n const location = response.headers.get(\"location\");\n if (!REDIRECT_STATUSES.has(response.status) || location === null) {\n return response;\n }\n\n if (redirectMode === \"manual\") {\n return response;\n }\n\n if (redirectMode === \"error\") {\n throw new OutboundPolicyError(\n `outbound request blocked — redirect received with redirect: \"error\" (${response.status} → ${location})`,\n { context: { url: url.toString(), location, status: response.status } },\n );\n }\n\n if (hop >= policy.maxRedirects) {\n throw new OutboundPolicyError(\n `outbound request blocked — more than ${policy.maxRedirects} redirects`,\n { context: { url: rawUrl, maxRedirects: policy.maxRedirects } },\n );\n }\n\n let target: URL;\n try {\n target = new URL(location, url);\n } catch {\n throw new OutboundPolicyError(\n `outbound request blocked — invalid redirect Location: ${location}`,\n { context: { url: url.toString(), location } },\n );\n }\n\n // The redirect target gets the SAME scheme / allowlist / private-IP\n // validation as the original URL.\n const next = await assertUrlAllowed(target.toString(), policy);\n\n // Discard the interim body so the connection can be reused.\n if (response.body) {\n await response.body.cancel().catch(() => undefined);\n }\n\n if (next.origin !== url.origin) {\n for (const name of CROSS_ORIGIN_STRIP_HEADERS) {\n delete headers[name];\n }\n }\n\n // 303 — and the legacy 301/302-on-a-non-GET convention — re-issue\n // as a bodyless GET, matching platform follow semantics.\n if (\n response.status === 303 ||\n ((response.status === 301 || response.status === 302) &&\n method !== \"GET\" &&\n method !== \"HEAD\")\n ) {\n method = \"GET\";\n body = undefined;\n }\n\n url = next;\n }\n } finally {\n clearTimeout(timer);\n }\n}\n\n/**\n * Read a response body as UTF-8 text with a hard byte cap. A declared\n * `content-length` over the cap fails fast; otherwise the stream is read\n * chunk-by-chunk and aborted the moment the running total exceeds\n * `maxBytes`. Throws {@link OutboundPolicyError} on overflow.\n */\nexport async function readTextCapped(\n response: Response,\n maxBytes: number,\n): Promise<string> {\n const declared = Number(response.headers.get(\"content-length\"));\n if (Number.isFinite(declared) && declared > maxBytes) {\n throw new OutboundPolicyError(\n `outbound response body too large — declared ${declared} bytes exceeds the ${maxBytes}-byte cap`,\n { context: { declared, maxBytes } },\n );\n }\n\n if (!response.body) {\n const text = await response.text();\n if (Buffer.byteLength(text) > maxBytes) {\n throw new OutboundPolicyError(\n `outbound response body exceeded the ${maxBytes}-byte cap`,\n { context: { maxBytes } },\n );\n }\n return text;\n }\n\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n if (!value) continue;\n\n total += value.byteLength;\n if (total > maxBytes) {\n await reader.cancel();\n throw new OutboundPolicyError(\n `outbound response body exceeded the ${maxBytes}-byte cap`,\n { context: { maxBytes } },\n );\n }\n chunks.push(value);\n }\n\n return Buffer.concat(chunks).toString(\"utf8\");\n}\n\n/**\n * Convenience: {@link guardedFetch} + {@link readTextCapped}. Returns the\n * response status alongside the (capped) body text so callers can shape\n * their own not-OK error. The body is only read when the response is OK.\n */\nexport async function fetchTextWithPolicy(\n rawUrl: string,\n policyInput: OutboundPolicy,\n init?: RequestInit,\n): Promise<{ ok: boolean; status: number; statusText: string; text: string }> {\n const policy = resolveOutboundPolicy(policyInput);\n const response = await guardedFetch(rawUrl, policy, init);\n\n return {\n ok: response.ok,\n status: response.status,\n statusText: response.statusText,\n text: response.ok ? await readTextCapped(response, policy.maxBytes) : \"\",\n };\n}\n"],"mappings":"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"}
|
|
@@ -47,6 +47,13 @@ type OutboundPolicy = {
|
|
|
47
47
|
*/
|
|
48
48
|
maxBytes?: number; /** Per-request timeout in milliseconds. Default `10_000`. */
|
|
49
49
|
timeoutMs?: number;
|
|
50
|
+
/**
|
|
51
|
+
* Maximum number of redirect hops `guardedFetch` will follow. Every
|
|
52
|
+
* hop's `Location` is re-validated against this policy (scheme, host
|
|
53
|
+
* allowlist, private-IP guard) before it is followed — a redirect can
|
|
54
|
+
* never reach a URL the original request could not have. Default `5`.
|
|
55
|
+
*/
|
|
56
|
+
maxRedirects?: number;
|
|
50
57
|
/**
|
|
51
58
|
* Caller `AbortSignal`, merged with the internal timeout — whichever
|
|
52
59
|
* fires first aborts the request.
|
|
@@ -66,6 +73,7 @@ type ResolvedOutboundPolicy = {
|
|
|
66
73
|
denyPrivateIPsAfterDNS: boolean;
|
|
67
74
|
maxBytes: number;
|
|
68
75
|
timeoutMs: number;
|
|
76
|
+
maxRedirects: number;
|
|
69
77
|
signal?: AbortSignal;
|
|
70
78
|
fetch: typeof fetch;
|
|
71
79
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"outbound-policy.type.d.mts","names":[],"sources":["../../../../../../../ai/src/security/outbound-policy.type.ts"],"mappings":";;AAoBA
|
|
1
|
+
{"version":3,"file":"outbound-policy.type.d.mts","names":[],"sources":["../../../../../../../ai/src/security/outbound-policy.type.ts"],"mappings":";;AAoBA;;;;;;;;;;;;;;;AA8CsB;AAItB;;;KAlDY,cAAA;EAmDV;;;;;EA7CA,cAAA;EAmDA;;;;;AACmB;EA7CnB,aAAA;;;;;;;EAOA,sBAAA;;;;;;EAMA,QAAA;EAEA,SAAA;;;;;;;EAOA,YAAA;;;;;EAKA,MAAA,GAAS,WAAA;;;;;;EAMT,KAAA,UAAe,KAAK;AAAA;;KAIV,sBAAA;EACV,cAAA;EACA,aAAA;EACA,sBAAA;EACA,QAAA;EACA,SAAA;EACA,YAAA;EACA,MAAA,GAAS,WAAA;EACT,KAAA,SAAc,KAAK;AAAA"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//#region ../ai/src/security/safe-merge.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Prototype-key guard for merges of model-influenced data into plain
|
|
4
|
+
* objects (supervisor `state`, artifact bags, refine slices, …).
|
|
5
|
+
*
|
|
6
|
+
* Any `target[key] = value` where `key` came from an LLM, a tool
|
|
7
|
+
* result, or a permissively-schema'd agent output is a prototype-
|
|
8
|
+
* tampering primitive: `state["__proto__"] = {...}` repoints that
|
|
9
|
+
* object's prototype, and `state["constructor"]` shadows its
|
|
10
|
+
* constructor. On a plain object literal the blast radius is contained
|
|
11
|
+
* (the write lands on the one object, not on `Object.prototype`), but
|
|
12
|
+
* it becomes real prototype pollution the moment anything downstream
|
|
13
|
+
* uses `in`, `hasOwnProperty`, or a recursive deep-merge on the
|
|
14
|
+
* tainted object — which is exactly the kind of change that gets added
|
|
15
|
+
* later without re-auditing the merge sites.
|
|
16
|
+
*
|
|
17
|
+
* So: one shared guard, applied at every merge boundary, dropping the
|
|
18
|
+
* dangerous keys instead of assigning them. Dropping (not throwing) is
|
|
19
|
+
* deliberate — these keys are never legitimate state fields, and a
|
|
20
|
+
* merge boundary in the middle of a settled iteration is the wrong
|
|
21
|
+
* place to fail a run. Callers get the dropped keys back so they can
|
|
22
|
+
* log the anomaly.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Keys that must never be written through a dynamic-key assignment.
|
|
26
|
+
* `__proto__` repoints the prototype; `constructor` / `prototype`
|
|
27
|
+
* are the standard escalation path from there.
|
|
28
|
+
*/
|
|
29
|
+
declare const UNSAFE_MERGE_KEYS: ReadonlyArray<string>;
|
|
30
|
+
/**
|
|
31
|
+
* True when `key` must not be assigned onto an object built from
|
|
32
|
+
* untrusted (model/tool-influenced) data.
|
|
33
|
+
*/
|
|
34
|
+
declare function isUnsafeMergeKey(key: string): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Assign one key onto `target`, skipping prototype-tampering keys.
|
|
37
|
+
* Returns `true` when the value was written, `false` when the key was
|
|
38
|
+
* refused.
|
|
39
|
+
*/
|
|
40
|
+
declare function assignSafeKey(target: Record<string, unknown>, key: string, value: unknown): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Shallow-merge every own enumerable key of `source` into `target`,
|
|
43
|
+
* skipping prototype-tampering keys. Mutates `target` in place (call
|
|
44
|
+
* sites rely on external references to the merged object staying
|
|
45
|
+
* coherent) and returns the list of refused keys — empty in the
|
|
46
|
+
* overwhelmingly common case, non-empty only when something upstream
|
|
47
|
+
* tried to smuggle `__proto__`/`constructor`/`prototype` through.
|
|
48
|
+
*/
|
|
49
|
+
declare function mergeSafely(target: Record<string, unknown>, source: Record<string, unknown>): string[];
|
|
50
|
+
//#endregion
|
|
51
|
+
export { UNSAFE_MERGE_KEYS, assignSafeKey, isUnsafeMergeKey, mergeSafely };
|
|
52
|
+
//# sourceMappingURL=safe-merge.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-merge.d.mts","names":[],"sources":["../../../../../../../ai/src/security/safe-merge.ts"],"mappings":";;AA4BA;;;;AAA6C;AAQ7C;;;;AAA4C;AAS5C;;;;;;;;;AAGgB;AAmBhB;;;;;;cAvCa,iBAAA,EAAmB,aAAa;;;AAyCZ;;iBAjCjB,gBAAA,CAAiB,GAAW;;;;;;iBAS5B,aAAA,CACd,MAAA,EAAQ,MAAM,mBACd,GAAA,UACA,KAAA;;;;;;;;;iBAmBc,WAAA,CACd,MAAA,EAAQ,MAAA,mBACR,MAAA,EAAQ,MAAM"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
//#region ../ai/src/security/safe-merge.ts
|
|
2
|
+
/**
|
|
3
|
+
* Prototype-key guard for merges of model-influenced data into plain
|
|
4
|
+
* objects (supervisor `state`, artifact bags, refine slices, …).
|
|
5
|
+
*
|
|
6
|
+
* Any `target[key] = value` where `key` came from an LLM, a tool
|
|
7
|
+
* result, or a permissively-schema'd agent output is a prototype-
|
|
8
|
+
* tampering primitive: `state["__proto__"] = {...}` repoints that
|
|
9
|
+
* object's prototype, and `state["constructor"]` shadows its
|
|
10
|
+
* constructor. On a plain object literal the blast radius is contained
|
|
11
|
+
* (the write lands on the one object, not on `Object.prototype`), but
|
|
12
|
+
* it becomes real prototype pollution the moment anything downstream
|
|
13
|
+
* uses `in`, `hasOwnProperty`, or a recursive deep-merge on the
|
|
14
|
+
* tainted object — which is exactly the kind of change that gets added
|
|
15
|
+
* later without re-auditing the merge sites.
|
|
16
|
+
*
|
|
17
|
+
* So: one shared guard, applied at every merge boundary, dropping the
|
|
18
|
+
* dangerous keys instead of assigning them. Dropping (not throwing) is
|
|
19
|
+
* deliberate — these keys are never legitimate state fields, and a
|
|
20
|
+
* merge boundary in the middle of a settled iteration is the wrong
|
|
21
|
+
* place to fail a run. Callers get the dropped keys back so they can
|
|
22
|
+
* log the anomaly.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Keys that must never be written through a dynamic-key assignment.
|
|
26
|
+
* `__proto__` repoints the prototype; `constructor` / `prototype`
|
|
27
|
+
* are the standard escalation path from there.
|
|
28
|
+
*/
|
|
29
|
+
const UNSAFE_MERGE_KEYS = [
|
|
30
|
+
"__proto__",
|
|
31
|
+
"constructor",
|
|
32
|
+
"prototype"
|
|
33
|
+
];
|
|
34
|
+
const UNSAFE_MERGE_KEY_SET = new Set(UNSAFE_MERGE_KEYS);
|
|
35
|
+
/**
|
|
36
|
+
* True when `key` must not be assigned onto an object built from
|
|
37
|
+
* untrusted (model/tool-influenced) data.
|
|
38
|
+
*/
|
|
39
|
+
function isUnsafeMergeKey(key) {
|
|
40
|
+
return UNSAFE_MERGE_KEY_SET.has(key);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Assign one key onto `target`, skipping prototype-tampering keys.
|
|
44
|
+
* Returns `true` when the value was written, `false` when the key was
|
|
45
|
+
* refused.
|
|
46
|
+
*/
|
|
47
|
+
function assignSafeKey(target, key, value) {
|
|
48
|
+
if (isUnsafeMergeKey(key)) return false;
|
|
49
|
+
target[key] = value;
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Shallow-merge every own enumerable key of `source` into `target`,
|
|
54
|
+
* skipping prototype-tampering keys. Mutates `target` in place (call
|
|
55
|
+
* sites rely on external references to the merged object staying
|
|
56
|
+
* coherent) and returns the list of refused keys — empty in the
|
|
57
|
+
* overwhelmingly common case, non-empty only when something upstream
|
|
58
|
+
* tried to smuggle `__proto__`/`constructor`/`prototype` through.
|
|
59
|
+
*/
|
|
60
|
+
function mergeSafely(target, source) {
|
|
61
|
+
const skipped = [];
|
|
62
|
+
for (const [key, value] of Object.entries(source)) if (!assignSafeKey(target, key, value)) skipped.push(key);
|
|
63
|
+
return skipped;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
//#endregion
|
|
67
|
+
export { UNSAFE_MERGE_KEYS, assignSafeKey, isUnsafeMergeKey, mergeSafely };
|
|
68
|
+
//# sourceMappingURL=safe-merge.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-merge.mjs","names":[],"sources":["../../../../../../../ai/src/security/safe-merge.ts"],"sourcesContent":["/**\n * Prototype-key guard for merges of model-influenced data into plain\n * objects (supervisor `state`, artifact bags, refine slices, …).\n *\n * Any `target[key] = value` where `key` came from an LLM, a tool\n * result, or a permissively-schema'd agent output is a prototype-\n * tampering primitive: `state[\"__proto__\"] = {...}` repoints that\n * object's prototype, and `state[\"constructor\"]` shadows its\n * constructor. On a plain object literal the blast radius is contained\n * (the write lands on the one object, not on `Object.prototype`), but\n * it becomes real prototype pollution the moment anything downstream\n * uses `in`, `hasOwnProperty`, or a recursive deep-merge on the\n * tainted object — which is exactly the kind of change that gets added\n * later without re-auditing the merge sites.\n *\n * So: one shared guard, applied at every merge boundary, dropping the\n * dangerous keys instead of assigning them. Dropping (not throwing) is\n * deliberate — these keys are never legitimate state fields, and a\n * merge boundary in the middle of a settled iteration is the wrong\n * place to fail a run. Callers get the dropped keys back so they can\n * log the anomaly.\n */\n\n/**\n * Keys that must never be written through a dynamic-key assignment.\n * `__proto__` repoints the prototype; `constructor` / `prototype`\n * are the standard escalation path from there.\n */\nexport const UNSAFE_MERGE_KEYS: ReadonlyArray<string> = [\"__proto__\", \"constructor\", \"prototype\"];\n\nconst UNSAFE_MERGE_KEY_SET = new Set(UNSAFE_MERGE_KEYS);\n\n/**\n * True when `key` must not be assigned onto an object built from\n * untrusted (model/tool-influenced) data.\n */\nexport function isUnsafeMergeKey(key: string): boolean {\n return UNSAFE_MERGE_KEY_SET.has(key);\n}\n\n/**\n * Assign one key onto `target`, skipping prototype-tampering keys.\n * Returns `true` when the value was written, `false` when the key was\n * refused.\n */\nexport function assignSafeKey(\n target: Record<string, unknown>,\n key: string,\n value: unknown,\n): boolean {\n if (isUnsafeMergeKey(key)) {\n return false;\n }\n\n target[key] = value;\n\n return true;\n}\n\n/**\n * Shallow-merge every own enumerable key of `source` into `target`,\n * skipping prototype-tampering keys. Mutates `target` in place (call\n * sites rely on external references to the merged object staying\n * coherent) and returns the list of refused keys — empty in the\n * overwhelmingly common case, non-empty only when something upstream\n * tried to smuggle `__proto__`/`constructor`/`prototype` through.\n */\nexport function mergeSafely(\n target: Record<string, unknown>,\n source: Record<string, unknown>,\n): string[] {\n const skipped: string[] = [];\n\n for (const [key, value] of Object.entries(source)) {\n if (!assignSafeKey(target, key, value)) {\n skipped.push(key);\n }\n }\n\n return skipped;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,MAAa,oBAA2C;CAAC;CAAa;CAAe;AAAW;AAEhG,MAAM,uBAAuB,IAAI,IAAI,iBAAiB;;;;;AAMtD,SAAgB,iBAAiB,KAAsB;CACrD,OAAO,qBAAqB,IAAI,GAAG;AACrC;;;;;;AAOA,SAAgB,cACd,QACA,KACA,OACS;CACT,IAAI,iBAAiB,GAAG,GACtB,OAAO;CAGT,OAAO,OAAO;CAEd,OAAO;AACT;;;;;;;;;AAUA,SAAgB,YACd,QACA,QACU;CACV,MAAM,UAAoB,CAAC;CAE3B,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAC9C,IAAI,CAAC,cAAc,QAAQ,KAAK,KAAK,GACnC,QAAQ,KAAK,GAAG;CAIpB,OAAO;AACT"}
|
|
@@ -50,7 +50,7 @@ async function decideViaCallback(params) {
|
|
|
50
50
|
throw wrapRouteError(params.config.name, thrown);
|
|
51
51
|
}
|
|
52
52
|
const durationMs = performance.now() - started;
|
|
53
|
-
return normalize(raw, params.entries, "route", durationMs);
|
|
53
|
+
return normalize(raw, params.entries, "route", durationMs, resolveMaxFanOut(params.config));
|
|
54
54
|
}
|
|
55
55
|
async function decideViaRouter(params) {
|
|
56
56
|
const { agent, placeholders, inputOverride, historySlicer } = resolveRouterEntry(params.config.router);
|
|
@@ -99,7 +99,7 @@ async function decideViaRouter(params) {
|
|
|
99
99
|
availableKeys: [...params.entries.keys()]
|
|
100
100
|
});
|
|
101
101
|
return {
|
|
102
|
-
...normalize(rawNext, params.entries, "router", durationMs),
|
|
102
|
+
...normalize(rawNext, params.entries, "router", durationMs, resolveMaxFanOut(params.config)),
|
|
103
103
|
reasoning: typeof reasoning === "string" ? reasoning : void 0,
|
|
104
104
|
usage: routerResult.usage,
|
|
105
105
|
routerReport: routerResult.report
|
|
@@ -139,7 +139,7 @@ function resolveSupervisorPromptText(config) {
|
|
|
139
139
|
* `next` field) into a canonical `DispatchDecision`, validating every
|
|
140
140
|
* named intent against the supervisor's `intents` map.
|
|
141
141
|
*/
|
|
142
|
-
function normalize(raw, entries, source, durationMs) {
|
|
142
|
+
function normalize(raw, entries, source, durationMs, maxFanOut) {
|
|
143
143
|
if (isEnd(raw)) return {
|
|
144
144
|
kind: "end",
|
|
145
145
|
source,
|
|
@@ -170,7 +170,7 @@ function normalize(raw, entries, source, durationMs) {
|
|
|
170
170
|
}
|
|
171
171
|
return {
|
|
172
172
|
kind: "dispatch",
|
|
173
|
-
intents: raw,
|
|
173
|
+
intents: capFanOut(raw, entries, maxFanOut),
|
|
174
174
|
source,
|
|
175
175
|
raw,
|
|
176
176
|
durationMs
|
|
@@ -181,6 +181,53 @@ function normalize(raw, entries, source, durationMs) {
|
|
|
181
181
|
availableKeys: [...entries.keys()]
|
|
182
182
|
});
|
|
183
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* Default fan-out WIDTH ceiling — how many intents one dispatch
|
|
186
|
+
* decision may run in parallel. `maxIterations` bounds depth; this
|
|
187
|
+
* bounds width, so total work per run is bounded by the product
|
|
188
|
+
* instead of by iterations alone.
|
|
189
|
+
*/
|
|
190
|
+
const DEFAULT_MAX_FAN_OUT = 10;
|
|
191
|
+
/**
|
|
192
|
+
* Resolve the configured width ceiling. Factory validation
|
|
193
|
+
* (`supervisor.ts`) rejects non-integer / `< 1` values at authoring
|
|
194
|
+
* time, so this only has to apply the default.
|
|
195
|
+
*/
|
|
196
|
+
function resolveMaxFanOut(config) {
|
|
197
|
+
return config.maxFanOut ?? 10;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Dedupe + width-cap a fan-out intent list before it reaches
|
|
201
|
+
* `Promise.all(...dispatchOne)`.
|
|
202
|
+
*
|
|
203
|
+
* Duplicates are collapsed silently: running the same intent twice in
|
|
204
|
+
* one decision is pure wasted spend (branch results are indexed by
|
|
205
|
+
* intent downstream, so the extras can't change the outcome), and a
|
|
206
|
+
* router that repeats itself is sloppy rather than hostile.
|
|
207
|
+
*
|
|
208
|
+
* Exceeding the cap *after* dedupe THROWS rather than truncating.
|
|
209
|
+
* Truncation would silently hand an attacker-chosen subset of the
|
|
210
|
+
* decision to the executor and hide the anomaly from the operator;
|
|
211
|
+
* every other routing violation in this file (unknown key, empty
|
|
212
|
+
* array, non-string element) already fails loudly as
|
|
213
|
+
* `SupervisorRoutingError`, so a width violation surfaces in the same
|
|
214
|
+
* place, with the same code, carrying the offending array.
|
|
215
|
+
*
|
|
216
|
+
* Threat model: the router's prompt embeds supervisor `state` and
|
|
217
|
+
* prior branch outputs, both of which can carry attacker-controlled
|
|
218
|
+
* text from tool results. Without a width bound, one injected
|
|
219
|
+
* "always return this 200-element `next` array" turns a single
|
|
220
|
+
* iteration into 200 real agent/workflow executions — no unknown
|
|
221
|
+
* intent name required, so the existing allowlist check never fires.
|
|
222
|
+
*/
|
|
223
|
+
function capFanOut(intents, entries, maxFanOut) {
|
|
224
|
+
const unique = [...new Set(intents)];
|
|
225
|
+
if (unique.length > maxFanOut) throw new SupervisorRoutingError(`routing decision fanned out to ${unique.length} intents — exceeds maxFanOut=${maxFanOut}. Raise \`maxFanOut\` if this width is intended.`, {
|
|
226
|
+
returned: intents,
|
|
227
|
+
availableKeys: [...entries.keys()]
|
|
228
|
+
});
|
|
229
|
+
return unique;
|
|
230
|
+
}
|
|
184
231
|
function validateKey(intent, entries) {
|
|
185
232
|
if (!entries.has(intent)) throw new SupervisorRoutingError(`router returned unknown agent key "${intent}"`, {
|
|
186
233
|
returned: intent,
|
|
@@ -268,5 +315,5 @@ function wrapRouteError(supervisorName, thrown) {
|
|
|
268
315
|
}
|
|
269
316
|
|
|
270
317
|
//#endregion
|
|
271
|
-
export { decide };
|
|
318
|
+
export { capFanOut, decide, resolveMaxFanOut };
|
|
272
319
|
//# sourceMappingURL=decide.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decide.mjs","names":[],"sources":["../../../../../../../ai/src/supervisor/decide.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport type { AgentContract } from \"../contracts/agent/agent.contract\";\nimport type { Message } from \"../contracts/conversation-message.type\";\nimport { END, type EndSentinel } from \"../contracts/end.type\";\nimport type { BaseReport } from \"../contracts/result/base-report.type\";\nimport type { IterationSnapshot } from \"../contracts/supervisor/iteration-snapshot.type\";\nimport type { Next } from \"../contracts/supervisor/next.type\";\nimport type { RouteContext } from \"../contracts/supervisor/route-context.type\";\nimport type { RouterEntry } from \"../contracts/supervisor/router-entry.type\";\nimport type { SupervisorConfig } from \"../contracts/supervisor/supervisor-config.type\";\nimport type { SupervisorInput } from \"../contracts/supervisor/supervisor-input.type\";\nimport { AIError, SupervisorFailedError, SupervisorRoutingError } from \"../errors\";\nimport type { ResolvedIntentEntry } from \"./entries\";\nimport { buildRouterContextMessage } from \"./router-prompt\";\n\n/**\n * Outcome of one dispatch decision — what the iteration loop needs to\n * act on. `kind: \"end\"` signals termination; `kind: \"dispatch\"` carries\n * the resolved intents (always an array; single-agent dispatch has\n * length 1). `source` records which path made the call so the\n * iteration snapshot can surface it to debuggers.\n */\nexport type DispatchDecision =\n | {\n kind: \"end\";\n source: \"route\" | \"router\" | \"initialAgent\" | \"classifier\";\n raw: Next;\n reasoning?: string;\n durationMs: number;\n usage?: { input: number; output: number; total: number };\n /** Full router-agent report when this decision came from a router. */\n routerReport?: BaseReport;\n }\n | {\n kind: \"dispatch\";\n intents: string[];\n source: \"route\" | \"router\" | \"initialAgent\" | \"classifier\";\n raw: Next;\n reasoning?: string;\n durationMs: number;\n usage?: { input: number; output: number; total: number };\n /** Full router-agent report when this decision came from a router. */\n routerReport?: BaseReport;\n };\n\nexport type DecideParams = {\n config: SupervisorConfig<unknown>;\n entries: Map<string, ResolvedIntentEntry>;\n iteration: number;\n maxIterations: number;\n iterations: IterationSnapshot[];\n input: SupervisorInput;\n /**\n * Per-execute state accumulator at the start of this iteration.\n * Threaded into `RouteContext` for the route callback and\n * rendered into the router prompt so routing decisions can be\n * state-aware (Q14).\n */\n state: Record<string, unknown>;\n /**\n * Frozen request-scoped bag from the `execute({ context })` call —\n * surfaced on `RouteContext.context` for both `route` callbacks\n * and `RouterEntry.placeholders` / `RouterEntry.input` resolvers.\n */\n context: Readonly<Record<string, unknown>>;\n /**\n * Frozen prior-conversation history from `execute({ history })` —\n * surfaced on `RouteContext.history` and forwarded to the router\n * agent as `agent.execute(input, { history })` so router decisions\n * are conversation-aware.\n */\n history: ReadonlyArray<Message>;\n /**\n * Resolved natural-language objective from `SupervisorConfig.goal`\n * (materialized to plain text at supervisor construction). Surfaced\n * on `RouteContext.goal` for `route` / `RouterEntry` resolvers, and\n * injected into the router agent's per-turn user message via\n * `buildRouterContextMessage`. `undefined` when no goal was set.\n */\n goal: string | undefined;\n evaluateFeedback?: RouteContext[\"evaluateFeedback\"];\n /**\n * Forensic record of the iter-0 classifier (Phase 7). Threaded into\n * `RouteContext.classifier` so route callbacks and router-agent\n * input composers can read the classification trail without\n * re-parsing state.\n */\n classifier?: RouteContext[\"classifier\"];\n signal?: AbortSignal;\n /**\n * Override for the very first iteration — when `initialAgent` is\n * set, the first turn skips `route`/`router` and dispatches the\n * named intent directly. `runIteration` passes `true` only on turn\n * 0 when the config has `initialAgent`.\n */\n useInitialAgent?: boolean;\n};\n\n/**\n * Unified dispatch decision entry — calls either the `route` callback\n * or the `router` agent based on the supervisor's configured mode and\n * normalizes the result into a `DispatchDecision`. Runtime validates\n * every routing value against the configured agent keys; unknown keys\n * surface as `SupervisorRoutingError`.\n */\nexport async function decide(params: DecideParams): Promise<DispatchDecision> {\n if (params.useInitialAgent && params.config.initialAgent) {\n const intent = params.config.initialAgent;\n validateKey(intent, params.entries);\n\n return {\n kind: \"dispatch\",\n intents: [intent],\n source: \"initialAgent\",\n raw: intent,\n durationMs: 0,\n };\n }\n\n if (params.config.route) {\n return decideViaCallback(params);\n }\n\n if (params.config.router) {\n return decideViaRouter(params);\n }\n\n throw new SupervisorFailedError(\n `ai.supervisor(\"${params.config.name}\"): neither \\`route\\` nor \\`router\\` is configured — factory validation should have prevented this`,\n { context: { authoring: true } },\n );\n}\n\nasync function decideViaCallback(params: DecideParams): Promise<DispatchDecision> {\n const started = performance.now();\n const ctx: RouteContext = {\n iteration: params.iteration,\n input: params.input,\n state: params.state,\n iterations: params.iterations,\n feedback:\n typeof params.evaluateFeedback?.feedback === \"string\"\n ? params.evaluateFeedback.feedback\n : undefined,\n evaluateFeedback: params.evaluateFeedback,\n context: params.context,\n history: params.history,\n goal: params.goal,\n classifier: params.classifier,\n };\n\n let raw: Next;\n\n try {\n raw = await params.config.route!(ctx);\n } catch (thrown) {\n throw wrapRouteError(params.config.name, thrown);\n }\n\n const durationMs = performance.now() - started;\n\n return normalize(raw, params.entries, \"route\", durationMs);\n}\n\nasync function decideViaRouter(params: DecideParams): Promise<DispatchDecision> {\n const { agent, placeholders, inputOverride, historySlicer } = resolveRouterEntry(\n params.config.router!,\n );\n const started = performance.now();\n\n const routeCtx: RouteContext = {\n iteration: params.iteration,\n input: params.input,\n state: params.state,\n iterations: params.iterations,\n feedback:\n typeof params.evaluateFeedback?.feedback === \"string\"\n ? params.evaluateFeedback.feedback\n : undefined,\n evaluateFeedback: params.evaluateFeedback,\n context: params.context,\n history: params.history,\n goal: params.goal,\n };\n\n const userMessage =\n inputOverride?.(routeCtx) ??\n buildRouterContextMessage({\n entries: params.entries,\n iteration: params.iteration,\n maxIterations: params.maxIterations,\n iterations: params.iterations,\n input: params.input,\n state: params.state,\n feedback: routeCtx.feedback,\n supervisorPrompt: resolveSupervisorPromptText(params.config),\n goal: params.goal,\n });\n\n const resolvedPlaceholders = placeholders?.(routeCtx);\n\n // Inject the canonical router output schema so the supervisor gets\n // a predictable `{ next, reasoning? }` shape regardless of what the\n // user scripted on the router agent. Lets the router stay a plain\n // agent — no supervisor-specific config needed at construction.\n const routerHistory = resolveRouterHistory(\n historySlicer,\n routeCtx,\n params.history,\n params.config.historyWindow?.router,\n );\n\n const routerResult = await agent.execute(userMessage, {\n signal: params.signal,\n output: ROUTER_OUTPUT_SCHEMA as unknown as StandardSchemaV1<{\n next: Next;\n reasoning?: string;\n }>,\n ...(resolvedPlaceholders ? { placeholders: resolvedPlaceholders } : {}),\n ...(routerHistory.length > 0 ? { history: routerHistory } : {}),\n });\n\n const durationMs = performance.now() - started;\n\n if (routerResult.error) {\n throw routerResult.error instanceof AIError\n ? routerResult.error\n : new SupervisorFailedError(`router agent failed`, {\n cause: routerResult.error,\n });\n }\n\n const data = routerResult.data;\n\n if (!data || typeof data !== \"object\") {\n throw new SupervisorRoutingError(\n `router agent returned no structured \\`next\\` — did its output schema include { next, reasoning? }?`,\n { returned: data, availableKeys: [...params.entries.keys()] },\n );\n }\n\n const rawNext = (data as { next?: unknown }).next;\n const reasoning = (data as { reasoning?: unknown }).reasoning;\n\n if (rawNext === undefined) {\n throw new SupervisorRoutingError(`router agent output missing \\`next\\` field`, {\n returned: data,\n availableKeys: [...params.entries.keys()],\n });\n }\n\n const decision = normalize(rawNext as Next, params.entries, \"router\", durationMs);\n\n return {\n ...decision,\n reasoning: typeof reasoning === \"string\" ? reasoning : undefined,\n usage: routerResult.usage,\n routerReport: routerResult.report,\n };\n}\n\n/**\n * Normalize the `router` config field — accepts either a bare\n * `AgentContract` (shorthand) or a full `RouterEntry` — into a\n * uniform `{ agent, placeholders?, inputOverride? }` triple. Centralized\n * so the dispatch path doesn't branch on shape.\n */\nfunction resolveRouterEntry(router: AgentContract<unknown> | RouterEntry): {\n agent: AgentContract<unknown>;\n placeholders?: RouterEntry[\"placeholders\"];\n inputOverride?: RouterEntry[\"input\"];\n historySlicer?: RouterEntry[\"history\"];\n} {\n if (typeof (router as { execute?: unknown }).execute === \"function\") {\n return { agent: router as AgentContract<unknown> };\n }\n\n const entry = router as RouterEntry;\n\n return {\n agent: entry.agent,\n placeholders: entry.placeholders,\n inputOverride: entry.input,\n historySlicer: entry.history,\n };\n}\n\n/**\n * Resolve the supervisor's own `systemPrompt` (string or contract)\n * into plain text. Returns `undefined` when the supervisor didn't\n * configure one. The resolved text is surfaced in the per-turn\n * router user message so the router sees team/domain context without\n * disturbing the router agent's own factory-level system prompt —\n * functionally equivalent to prepending, without requiring an API\n * expansion on `AgentContract` to read the router's system prompt.\n */\nfunction resolveSupervisorPromptText(config: SupervisorConfig<unknown>): string | undefined {\n if (!config.systemPrompt) {\n return undefined;\n }\n\n return typeof config.systemPrompt === \"string\"\n ? config.systemPrompt\n : config.systemPrompt.resolve();\n}\n\n/**\n * Convert the raw routing value (callback return OR router agent\n * `next` field) into a canonical `DispatchDecision`, validating every\n * named intent against the supervisor's `intents` map.\n */\nfunction normalize(\n raw: Next,\n entries: Map<string, ResolvedIntentEntry>,\n source: \"route\" | \"router\",\n durationMs: number,\n): DispatchDecision {\n if (isEnd(raw)) {\n return { kind: \"end\", source, raw, durationMs };\n }\n\n if (typeof raw === \"string\") {\n validateKey(raw, entries);\n\n return {\n kind: \"dispatch\",\n intents: [raw],\n source,\n raw,\n durationMs,\n };\n }\n\n if (Array.isArray(raw)) {\n if (raw.length === 0) {\n throw new SupervisorRoutingError(\n `router returned an empty array — must be a non-empty list of agent intents`,\n { returned: raw, availableKeys: [...entries.keys()] },\n );\n }\n\n for (const intent of raw) {\n if (typeof intent !== \"string\") {\n throw new SupervisorRoutingError(`router returned a non-string inside its fan-out array`, {\n returned: raw,\n availableKeys: [...entries.keys()],\n });\n }\n\n validateKey(intent, entries);\n }\n\n return {\n kind: \"dispatch\",\n intents: raw,\n source,\n raw,\n durationMs,\n };\n }\n\n throw new SupervisorRoutingError(\n `router returned an unsupported value — expected a string, string[], or END`,\n { returned: raw, availableKeys: [...entries.keys()] },\n );\n}\n\nfunction validateKey(intent: string, entries: Map<string, ResolvedIntentEntry>): void {\n if (!entries.has(intent)) {\n throw new SupervisorRoutingError(`router returned unknown agent key \"${intent}\"`, {\n returned: intent,\n availableKeys: [...entries.keys()],\n });\n }\n}\n\nfunction isEnd(value: unknown): value is EndSentinel {\n return value === END;\n}\n\n/**\n * Resolve the history slice forwarded to the router agent. Mirrors\n * `SupervisorExecution.resolveHistoryFor(\"router\", ...)` — duplicated\n * here so the standalone `decide()` function stays callable without\n * threading the execution instance through. Precedence is identical:\n * entry slicer > `historyWindow.router` > full history.\n */\nfunction resolveRouterHistory(\n slicer: RouterEntry[\"history\"] | undefined,\n routeCtx: RouteContext,\n full: ReadonlyArray<Message>,\n window: number | undefined,\n): Message[] {\n if (slicer) {\n const sliced = slicer(routeCtx);\n return sliced ? [...sliced] : [];\n }\n\n if (window === undefined || window < 0) {\n return [...full];\n }\n\n if (window === 0) {\n return [];\n }\n\n return full.slice(-window);\n}\n\n/**\n * JSON Schema form of the canonical router output shape. Surfaced via\n * the Standard JSON Schema V1 extension path (`[\"~standard\"].jsonSchema.input`)\n * so `extractJsonSchema()` can pull it for native structured-output\n * enforcement on capable providers (OpenAI strict json_schema mode,\n * Anthropic tool-use shape, etc.). Without this, the model is told to\n * emit JSON only via soft system-prompt instruction — fragile, and\n * skipped entirely when the model advertises `structuredOutput: true`.\n *\n * `next` is intentionally `string` (not a union with arrays) because\n * OpenAI strict mode rejects polymorphic root types — fan-out via\n * `string[]` is still validated at the framework layer; the model\n * just emits a single intent name (or the END sentinel) and the\n * supervisor's own normalizer handles the rest.\n */\nconst ROUTER_OUTPUT_JSON_SCHEMA = {\n type: \"object\",\n properties: {\n next: {\n type: \"string\",\n description: \"Name of the agent to dispatch next, or the END sentinel to terminate the run.\",\n },\n reasoning: {\n type: \"string\",\n description: \"One-sentence justification for the routing choice.\",\n },\n },\n required: [\"next\", \"reasoning\"],\n additionalProperties: false,\n};\n\n/**\n * Canonical Standard Schema the supervisor injects when calling the\n * router agent. Pragmatic — accepts any `next` shape the router can\n * plausibly emit (`string`, `string[]`, or the `END` literal) plus an\n * optional `reasoning` field. Rejects anything else so a broken\n * router output surfaces cleanly via the agent's own validation path.\n *\n * Exposes `[\"~standard\"].jsonSchema.input()` (Standard JSON Schema V1)\n * so capable providers enforce the shape natively rather than relying\n * on prompt-side coaching.\n */\nconst ROUTER_OUTPUT_SCHEMA: StandardSchemaV1<{\n next: Next;\n reasoning?: string;\n}> = {\n \"~standard\": {\n version: 1,\n vendor: \"warlock-supervisor\",\n jsonSchema: {\n input: () => ROUTER_OUTPUT_JSON_SCHEMA,\n },\n validate(value: unknown): StandardSchemaV1.Result<{ next: Next; reasoning?: string }> {\n if (!value || typeof value !== \"object\") {\n return { issues: [{ message: \"router output must be an object\" }] };\n }\n\n const record = value as { next?: unknown; reasoning?: unknown };\n const rawNext = record.next;\n\n const nextIsValid =\n typeof rawNext === \"string\" ||\n (Array.isArray(rawNext) && rawNext.every((element) => typeof element === \"string\"));\n\n if (!nextIsValid) {\n return {\n issues: [\n {\n message: \"router output `next` must be a string, string[], or the END sentinel\",\n },\n ],\n };\n }\n\n const reasoning = typeof record.reasoning === \"string\" ? record.reasoning : undefined;\n\n return {\n value: { next: rawNext as Next, reasoning },\n };\n },\n } as StandardSchemaV1<{ next: Next; reasoning?: string }>[\"~standard\"] & {\n jsonSchema: { input: () => Record<string, unknown> };\n },\n};\n\nfunction wrapRouteError(supervisorName: string, thrown: unknown): AIError {\n if (thrown instanceof AIError) {\n return thrown;\n }\n\n const message = thrown instanceof Error ? thrown.message : String(thrown);\n\n return new SupervisorFailedError(\n `\\`route\\` callback threw in supervisor \"${supervisorName}\": ${message}`,\n { cause: thrown },\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"decide.mjs","names":[],"sources":["../../../../../../../ai/src/supervisor/decide.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport type { AgentContract } from \"../contracts/agent/agent.contract\";\nimport type { Message } from \"../contracts/conversation-message.type\";\nimport { END, type EndSentinel } from \"../contracts/end.type\";\nimport type { BaseReport } from \"../contracts/result/base-report.type\";\nimport type { IterationSnapshot } from \"../contracts/supervisor/iteration-snapshot.type\";\nimport type { Next } from \"../contracts/supervisor/next.type\";\nimport type { RouteContext } from \"../contracts/supervisor/route-context.type\";\nimport type { RouterEntry } from \"../contracts/supervisor/router-entry.type\";\nimport type { SupervisorConfig } from \"../contracts/supervisor/supervisor-config.type\";\nimport type { SupervisorInput } from \"../contracts/supervisor/supervisor-input.type\";\nimport { AIError, SupervisorFailedError, SupervisorRoutingError } from \"../errors\";\nimport type { ResolvedIntentEntry } from \"./entries\";\nimport { buildRouterContextMessage } from \"./router-prompt\";\n\n/**\n * Outcome of one dispatch decision — what the iteration loop needs to\n * act on. `kind: \"end\"` signals termination; `kind: \"dispatch\"` carries\n * the resolved intents (always an array; single-agent dispatch has\n * length 1). `source` records which path made the call so the\n * iteration snapshot can surface it to debuggers.\n */\nexport type DispatchDecision =\n | {\n kind: \"end\";\n source: \"route\" | \"router\" | \"initialAgent\" | \"classifier\";\n raw: Next;\n reasoning?: string;\n durationMs: number;\n usage?: { input: number; output: number; total: number };\n /** Full router-agent report when this decision came from a router. */\n routerReport?: BaseReport;\n }\n | {\n kind: \"dispatch\";\n intents: string[];\n source: \"route\" | \"router\" | \"initialAgent\" | \"classifier\";\n raw: Next;\n reasoning?: string;\n durationMs: number;\n usage?: { input: number; output: number; total: number };\n /** Full router-agent report when this decision came from a router. */\n routerReport?: BaseReport;\n };\n\nexport type DecideParams = {\n config: SupervisorConfig<unknown>;\n entries: Map<string, ResolvedIntentEntry>;\n iteration: number;\n maxIterations: number;\n iterations: IterationSnapshot[];\n input: SupervisorInput;\n /**\n * Per-execute state accumulator at the start of this iteration.\n * Threaded into `RouteContext` for the route callback and\n * rendered into the router prompt so routing decisions can be\n * state-aware (Q14).\n */\n state: Record<string, unknown>;\n /**\n * Frozen request-scoped bag from the `execute({ context })` call —\n * surfaced on `RouteContext.context` for both `route` callbacks\n * and `RouterEntry.placeholders` / `RouterEntry.input` resolvers.\n */\n context: Readonly<Record<string, unknown>>;\n /**\n * Frozen prior-conversation history from `execute({ history })` —\n * surfaced on `RouteContext.history` and forwarded to the router\n * agent as `agent.execute(input, { history })` so router decisions\n * are conversation-aware.\n */\n history: ReadonlyArray<Message>;\n /**\n * Resolved natural-language objective from `SupervisorConfig.goal`\n * (materialized to plain text at supervisor construction). Surfaced\n * on `RouteContext.goal` for `route` / `RouterEntry` resolvers, and\n * injected into the router agent's per-turn user message via\n * `buildRouterContextMessage`. `undefined` when no goal was set.\n */\n goal: string | undefined;\n evaluateFeedback?: RouteContext[\"evaluateFeedback\"];\n /**\n * Forensic record of the iter-0 classifier (Phase 7). Threaded into\n * `RouteContext.classifier` so route callbacks and router-agent\n * input composers can read the classification trail without\n * re-parsing state.\n */\n classifier?: RouteContext[\"classifier\"];\n signal?: AbortSignal;\n /**\n * Override for the very first iteration — when `initialAgent` is\n * set, the first turn skips `route`/`router` and dispatches the\n * named intent directly. `runIteration` passes `true` only on turn\n * 0 when the config has `initialAgent`.\n */\n useInitialAgent?: boolean;\n};\n\n/**\n * Unified dispatch decision entry — calls either the `route` callback\n * or the `router` agent based on the supervisor's configured mode and\n * normalizes the result into a `DispatchDecision`. Runtime validates\n * every routing value against the configured agent keys; unknown keys\n * surface as `SupervisorRoutingError`.\n */\nexport async function decide(params: DecideParams): Promise<DispatchDecision> {\n if (params.useInitialAgent && params.config.initialAgent) {\n const intent = params.config.initialAgent;\n validateKey(intent, params.entries);\n\n return {\n kind: \"dispatch\",\n intents: [intent],\n source: \"initialAgent\",\n raw: intent,\n durationMs: 0,\n };\n }\n\n if (params.config.route) {\n return decideViaCallback(params);\n }\n\n if (params.config.router) {\n return decideViaRouter(params);\n }\n\n throw new SupervisorFailedError(\n `ai.supervisor(\"${params.config.name}\"): neither \\`route\\` nor \\`router\\` is configured — factory validation should have prevented this`,\n { context: { authoring: true } },\n );\n}\n\nasync function decideViaCallback(params: DecideParams): Promise<DispatchDecision> {\n const started = performance.now();\n const ctx: RouteContext = {\n iteration: params.iteration,\n input: params.input,\n state: params.state,\n iterations: params.iterations,\n feedback:\n typeof params.evaluateFeedback?.feedback === \"string\"\n ? params.evaluateFeedback.feedback\n : undefined,\n evaluateFeedback: params.evaluateFeedback,\n context: params.context,\n history: params.history,\n goal: params.goal,\n classifier: params.classifier,\n };\n\n let raw: Next;\n\n try {\n raw = await params.config.route!(ctx);\n } catch (thrown) {\n throw wrapRouteError(params.config.name, thrown);\n }\n\n const durationMs = performance.now() - started;\n\n return normalize(raw, params.entries, \"route\", durationMs, resolveMaxFanOut(params.config));\n}\n\nasync function decideViaRouter(params: DecideParams): Promise<DispatchDecision> {\n const { agent, placeholders, inputOverride, historySlicer } = resolveRouterEntry(\n params.config.router!,\n );\n const started = performance.now();\n\n const routeCtx: RouteContext = {\n iteration: params.iteration,\n input: params.input,\n state: params.state,\n iterations: params.iterations,\n feedback:\n typeof params.evaluateFeedback?.feedback === \"string\"\n ? params.evaluateFeedback.feedback\n : undefined,\n evaluateFeedback: params.evaluateFeedback,\n context: params.context,\n history: params.history,\n goal: params.goal,\n };\n\n const userMessage =\n inputOverride?.(routeCtx) ??\n buildRouterContextMessage({\n entries: params.entries,\n iteration: params.iteration,\n maxIterations: params.maxIterations,\n iterations: params.iterations,\n input: params.input,\n state: params.state,\n feedback: routeCtx.feedback,\n supervisorPrompt: resolveSupervisorPromptText(params.config),\n goal: params.goal,\n });\n\n const resolvedPlaceholders = placeholders?.(routeCtx);\n\n // Inject the canonical router output schema so the supervisor gets\n // a predictable `{ next, reasoning? }` shape regardless of what the\n // user scripted on the router agent. Lets the router stay a plain\n // agent — no supervisor-specific config needed at construction.\n const routerHistory = resolveRouterHistory(\n historySlicer,\n routeCtx,\n params.history,\n params.config.historyWindow?.router,\n );\n\n const routerResult = await agent.execute(userMessage, {\n signal: params.signal,\n output: ROUTER_OUTPUT_SCHEMA as unknown as StandardSchemaV1<{\n next: Next;\n reasoning?: string;\n }>,\n ...(resolvedPlaceholders ? { placeholders: resolvedPlaceholders } : {}),\n ...(routerHistory.length > 0 ? { history: routerHistory } : {}),\n });\n\n const durationMs = performance.now() - started;\n\n if (routerResult.error) {\n throw routerResult.error instanceof AIError\n ? routerResult.error\n : new SupervisorFailedError(`router agent failed`, {\n cause: routerResult.error,\n });\n }\n\n const data = routerResult.data;\n\n if (!data || typeof data !== \"object\") {\n throw new SupervisorRoutingError(\n `router agent returned no structured \\`next\\` — did its output schema include { next, reasoning? }?`,\n { returned: data, availableKeys: [...params.entries.keys()] },\n );\n }\n\n const rawNext = (data as { next?: unknown }).next;\n const reasoning = (data as { reasoning?: unknown }).reasoning;\n\n if (rawNext === undefined) {\n throw new SupervisorRoutingError(`router agent output missing \\`next\\` field`, {\n returned: data,\n availableKeys: [...params.entries.keys()],\n });\n }\n\n const decision = normalize(\n rawNext as Next,\n params.entries,\n \"router\",\n durationMs,\n resolveMaxFanOut(params.config),\n );\n\n return {\n ...decision,\n reasoning: typeof reasoning === \"string\" ? reasoning : undefined,\n usage: routerResult.usage,\n routerReport: routerResult.report,\n };\n}\n\n/**\n * Normalize the `router` config field — accepts either a bare\n * `AgentContract` (shorthand) or a full `RouterEntry` — into a\n * uniform `{ agent, placeholders?, inputOverride? }` triple. Centralized\n * so the dispatch path doesn't branch on shape.\n */\nfunction resolveRouterEntry(router: AgentContract<unknown> | RouterEntry): {\n agent: AgentContract<unknown>;\n placeholders?: RouterEntry[\"placeholders\"];\n inputOverride?: RouterEntry[\"input\"];\n historySlicer?: RouterEntry[\"history\"];\n} {\n if (typeof (router as { execute?: unknown }).execute === \"function\") {\n return { agent: router as AgentContract<unknown> };\n }\n\n const entry = router as RouterEntry;\n\n return {\n agent: entry.agent,\n placeholders: entry.placeholders,\n inputOverride: entry.input,\n historySlicer: entry.history,\n };\n}\n\n/**\n * Resolve the supervisor's own `systemPrompt` (string or contract)\n * into plain text. Returns `undefined` when the supervisor didn't\n * configure one. The resolved text is surfaced in the per-turn\n * router user message so the router sees team/domain context without\n * disturbing the router agent's own factory-level system prompt —\n * functionally equivalent to prepending, without requiring an API\n * expansion on `AgentContract` to read the router's system prompt.\n */\nfunction resolveSupervisorPromptText(config: SupervisorConfig<unknown>): string | undefined {\n if (!config.systemPrompt) {\n return undefined;\n }\n\n return typeof config.systemPrompt === \"string\"\n ? config.systemPrompt\n : config.systemPrompt.resolve();\n}\n\n/**\n * Convert the raw routing value (callback return OR router agent\n * `next` field) into a canonical `DispatchDecision`, validating every\n * named intent against the supervisor's `intents` map.\n */\nfunction normalize(\n raw: Next,\n entries: Map<string, ResolvedIntentEntry>,\n source: \"route\" | \"router\",\n durationMs: number,\n maxFanOut: number,\n): DispatchDecision {\n if (isEnd(raw)) {\n return { kind: \"end\", source, raw, durationMs };\n }\n\n if (typeof raw === \"string\") {\n validateKey(raw, entries);\n\n return {\n kind: \"dispatch\",\n intents: [raw],\n source,\n raw,\n durationMs,\n };\n }\n\n if (Array.isArray(raw)) {\n if (raw.length === 0) {\n throw new SupervisorRoutingError(\n `router returned an empty array — must be a non-empty list of agent intents`,\n { returned: raw, availableKeys: [...entries.keys()] },\n );\n }\n\n for (const intent of raw) {\n if (typeof intent !== \"string\") {\n throw new SupervisorRoutingError(`router returned a non-string inside its fan-out array`, {\n returned: raw,\n availableKeys: [...entries.keys()],\n });\n }\n\n validateKey(intent, entries);\n }\n\n return {\n kind: \"dispatch\",\n intents: capFanOut(raw as string[], entries, maxFanOut),\n source,\n raw,\n durationMs,\n };\n }\n\n throw new SupervisorRoutingError(\n `router returned an unsupported value — expected a string, string[], or END`,\n { returned: raw, availableKeys: [...entries.keys()] },\n );\n}\n\n/**\n * Default fan-out WIDTH ceiling — how many intents one dispatch\n * decision may run in parallel. `maxIterations` bounds depth; this\n * bounds width, so total work per run is bounded by the product\n * instead of by iterations alone.\n */\nexport const DEFAULT_MAX_FAN_OUT = 10;\n\n/**\n * Resolve the configured width ceiling. Factory validation\n * (`supervisor.ts`) rejects non-integer / `< 1` values at authoring\n * time, so this only has to apply the default.\n */\nexport function resolveMaxFanOut(config: Pick<SupervisorConfig<never>, \"maxFanOut\">): number {\n return config.maxFanOut ?? DEFAULT_MAX_FAN_OUT;\n}\n\n/**\n * Dedupe + width-cap a fan-out intent list before it reaches\n * `Promise.all(...dispatchOne)`.\n *\n * Duplicates are collapsed silently: running the same intent twice in\n * one decision is pure wasted spend (branch results are indexed by\n * intent downstream, so the extras can't change the outcome), and a\n * router that repeats itself is sloppy rather than hostile.\n *\n * Exceeding the cap *after* dedupe THROWS rather than truncating.\n * Truncation would silently hand an attacker-chosen subset of the\n * decision to the executor and hide the anomaly from the operator;\n * every other routing violation in this file (unknown key, empty\n * array, non-string element) already fails loudly as\n * `SupervisorRoutingError`, so a width violation surfaces in the same\n * place, with the same code, carrying the offending array.\n *\n * Threat model: the router's prompt embeds supervisor `state` and\n * prior branch outputs, both of which can carry attacker-controlled\n * text from tool results. Without a width bound, one injected\n * \"always return this 200-element `next` array\" turns a single\n * iteration into 200 real agent/workflow executions — no unknown\n * intent name required, so the existing allowlist check never fires.\n */\nexport function capFanOut(\n intents: string[],\n entries: Map<string, ResolvedIntentEntry>,\n maxFanOut: number,\n): string[] {\n const unique = [...new Set(intents)];\n\n if (unique.length > maxFanOut) {\n throw new SupervisorRoutingError(\n `routing decision fanned out to ${unique.length} intents — exceeds maxFanOut=${maxFanOut}. Raise \\`maxFanOut\\` if this width is intended.`,\n { returned: intents, availableKeys: [...entries.keys()] },\n );\n }\n\n return unique;\n}\n\nfunction validateKey(intent: string, entries: Map<string, ResolvedIntentEntry>): void {\n if (!entries.has(intent)) {\n throw new SupervisorRoutingError(`router returned unknown agent key \"${intent}\"`, {\n returned: intent,\n availableKeys: [...entries.keys()],\n });\n }\n}\n\nfunction isEnd(value: unknown): value is EndSentinel {\n return value === END;\n}\n\n/**\n * Resolve the history slice forwarded to the router agent. Mirrors\n * `SupervisorExecution.resolveHistoryFor(\"router\", ...)` — duplicated\n * here so the standalone `decide()` function stays callable without\n * threading the execution instance through. Precedence is identical:\n * entry slicer > `historyWindow.router` > full history.\n */\nfunction resolveRouterHistory(\n slicer: RouterEntry[\"history\"] | undefined,\n routeCtx: RouteContext,\n full: ReadonlyArray<Message>,\n window: number | undefined,\n): Message[] {\n if (slicer) {\n const sliced = slicer(routeCtx);\n return sliced ? [...sliced] : [];\n }\n\n if (window === undefined || window < 0) {\n return [...full];\n }\n\n if (window === 0) {\n return [];\n }\n\n return full.slice(-window);\n}\n\n/**\n * JSON Schema form of the canonical router output shape. Surfaced via\n * the Standard JSON Schema V1 extension path (`[\"~standard\"].jsonSchema.input`)\n * so `extractJsonSchema()` can pull it for native structured-output\n * enforcement on capable providers (OpenAI strict json_schema mode,\n * Anthropic tool-use shape, etc.). Without this, the model is told to\n * emit JSON only via soft system-prompt instruction — fragile, and\n * skipped entirely when the model advertises `structuredOutput: true`.\n *\n * `next` is intentionally `string` (not a union with arrays) because\n * OpenAI strict mode rejects polymorphic root types — fan-out via\n * `string[]` is still validated at the framework layer; the model\n * just emits a single intent name (or the END sentinel) and the\n * supervisor's own normalizer handles the rest.\n */\nconst ROUTER_OUTPUT_JSON_SCHEMA = {\n type: \"object\",\n properties: {\n next: {\n type: \"string\",\n description: \"Name of the agent to dispatch next, or the END sentinel to terminate the run.\",\n },\n reasoning: {\n type: \"string\",\n description: \"One-sentence justification for the routing choice.\",\n },\n },\n required: [\"next\", \"reasoning\"],\n additionalProperties: false,\n};\n\n/**\n * Canonical Standard Schema the supervisor injects when calling the\n * router agent. Pragmatic — accepts any `next` shape the router can\n * plausibly emit (`string`, `string[]`, or the `END` literal) plus an\n * optional `reasoning` field. Rejects anything else so a broken\n * router output surfaces cleanly via the agent's own validation path.\n *\n * Exposes `[\"~standard\"].jsonSchema.input()` (Standard JSON Schema V1)\n * so capable providers enforce the shape natively rather than relying\n * on prompt-side coaching.\n */\nconst ROUTER_OUTPUT_SCHEMA: StandardSchemaV1<{\n next: Next;\n reasoning?: string;\n}> = {\n \"~standard\": {\n version: 1,\n vendor: \"warlock-supervisor\",\n jsonSchema: {\n input: () => ROUTER_OUTPUT_JSON_SCHEMA,\n },\n validate(value: unknown): StandardSchemaV1.Result<{ next: Next; reasoning?: string }> {\n if (!value || typeof value !== \"object\") {\n return { issues: [{ message: \"router output must be an object\" }] };\n }\n\n const record = value as { next?: unknown; reasoning?: unknown };\n const rawNext = record.next;\n\n const nextIsValid =\n typeof rawNext === \"string\" ||\n (Array.isArray(rawNext) && rawNext.every((element) => typeof element === \"string\"));\n\n if (!nextIsValid) {\n return {\n issues: [\n {\n message: \"router output `next` must be a string, string[], or the END sentinel\",\n },\n ],\n };\n }\n\n const reasoning = typeof record.reasoning === \"string\" ? record.reasoning : undefined;\n\n return {\n value: { next: rawNext as Next, reasoning },\n };\n },\n } as StandardSchemaV1<{ next: Next; reasoning?: string }>[\"~standard\"] & {\n jsonSchema: { input: () => Record<string, unknown> };\n },\n};\n\nfunction wrapRouteError(supervisorName: string, thrown: unknown): AIError {\n if (thrown instanceof AIError) {\n return thrown;\n }\n\n const message = thrown instanceof Error ? thrown.message : String(thrown);\n\n return new SupervisorFailedError(\n `\\`route\\` callback threw in supervisor \"${supervisorName}\": ${message}`,\n { cause: thrown },\n );\n}\n"],"mappings":"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"}
|
|
@@ -251,6 +251,13 @@ declare class SupervisorExecution<TOutput> {
|
|
|
251
251
|
* branch errors don't abort siblings — they're recorded on the
|
|
252
252
|
* branch snapshot and let evaluate (or default termination logic)
|
|
253
253
|
* decide the response.
|
|
254
|
+
*
|
|
255
|
+
* `capFanOut` runs here as well as in `decide.ts` — this is the one
|
|
256
|
+
* chokepoint every dispatch source funnels through (router/route
|
|
257
|
+
* decisions, `evaluate.reassignTo`, classifier picks, per-intent
|
|
258
|
+
* `next` unions), so the width bound holds even for the paths that
|
|
259
|
+
* build a `DispatchDecision` without going through `normalize()`.
|
|
260
|
+
* Idempotent for already-normalized decisions.
|
|
254
261
|
*/
|
|
255
262
|
private dispatchBranches;
|
|
256
263
|
/**
|
|
@@ -461,6 +468,21 @@ declare class SupervisorExecution<TOutput> {
|
|
|
461
468
|
* specialist commits.
|
|
462
469
|
*/
|
|
463
470
|
private mergeAckIntoState;
|
|
471
|
+
/**
|
|
472
|
+
* Single funnel for "shallow-merge a model-influenced slice into
|
|
473
|
+
* `this.state`". Wraps the shared {@link mergeSafely} guard so no
|
|
474
|
+
* merge site can assign `__proto__` / `constructor` / `prototype`
|
|
475
|
+
* onto the run's state object, and logs when something tried.
|
|
476
|
+
*
|
|
477
|
+
* Every slice reaching state is model- or tool-influenced (agent
|
|
478
|
+
* outputs validated against a DEVELOPER-supplied schema, which may
|
|
479
|
+
* legitimately be permissive: `z.record()`, `.passthrough()`,
|
|
480
|
+
* `z.any()`), so the key names are untrusted input even when the
|
|
481
|
+
* values are shaped.
|
|
482
|
+
*/
|
|
483
|
+
private mergeIntoState;
|
|
484
|
+
/** Shared logging for refused prototype-tampering keys. */
|
|
485
|
+
private warnOnUnsafeKeys;
|
|
464
486
|
/**
|
|
465
487
|
* Run the iter-0 classifier prelude (Phase 7 / decisions §37).
|
|
466
488
|
* Resolves the configured classifier (agent / callback / entry
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"execution.d.mts","names":[],"sources":["../../../../../../../ai/src/supervisor/execution.ts"],"mappings":";;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"execution.d.mts","names":[],"sources":["../../../../../../../ai/src/supervisor/execution.ts"],"mappings":"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"}
|