@rarebit-one/harness-kernel 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +286 -0
- package/dist/agent.d.ts +71 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +258 -0
- package/dist/agent.js.map +1 -0
- package/dist/connectors/mcpClient.d.ts +22 -0
- package/dist/connectors/mcpClient.d.ts.map +1 -0
- package/dist/connectors/mcpClient.js +63 -0
- package/dist/connectors/mcpClient.js.map +1 -0
- package/dist/context/types.d.ts +68 -0
- package/dist/context/types.d.ts.map +1 -0
- package/dist/context/types.js +43 -0
- package/dist/context/types.js.map +1 -0
- package/dist/engines/capability.d.ts +67 -0
- package/dist/engines/capability.d.ts.map +1 -0
- package/dist/engines/capability.js +73 -0
- package/dist/engines/capability.js.map +1 -0
- package/dist/engines/capabilityEmissions.d.ts +27 -0
- package/dist/engines/capabilityEmissions.d.ts.map +1 -0
- package/dist/engines/capabilityEmissions.js +37 -0
- package/dist/engines/capabilityEmissions.js.map +1 -0
- package/dist/engines/capabilityMcp.d.ts +23 -0
- package/dist/engines/capabilityMcp.d.ts.map +1 -0
- package/dist/engines/capabilityMcp.js +36 -0
- package/dist/engines/capabilityMcp.js.map +1 -0
- package/dist/engines/capabilityStdio.d.ts +46 -0
- package/dist/engines/capabilityStdio.d.ts.map +1 -0
- package/dist/engines/capabilityStdio.js +39 -0
- package/dist/engines/capabilityStdio.js.map +1 -0
- package/dist/engines/claudeCode.d.ts +73 -0
- package/dist/engines/claudeCode.d.ts.map +1 -0
- package/dist/engines/claudeCode.js +100 -0
- package/dist/engines/claudeCode.js.map +1 -0
- package/dist/engines/claudeCodeDriver.d.ts +14 -0
- package/dist/engines/claudeCodeDriver.d.ts.map +1 -0
- package/dist/engines/claudeCodeDriver.js +90 -0
- package/dist/engines/claudeCodeDriver.js.map +1 -0
- package/dist/engines/codex.d.ts +72 -0
- package/dist/engines/codex.d.ts.map +1 -0
- package/dist/engines/codex.js +157 -0
- package/dist/engines/codex.js.map +1 -0
- package/dist/engines/codexDriver.d.ts +43 -0
- package/dist/engines/codexDriver.d.ts.map +1 -0
- package/dist/engines/codexDriver.js +110 -0
- package/dist/engines/codexDriver.js.map +1 -0
- package/dist/engines/connectorMcp.d.ts +60 -0
- package/dist/engines/connectorMcp.d.ts.map +1 -0
- package/dist/engines/connectorMcp.js +137 -0
- package/dist/engines/connectorMcp.js.map +1 -0
- package/dist/engines/index.d.ts +33 -0
- package/dist/engines/index.d.ts.map +1 -0
- package/dist/engines/index.js +29 -0
- package/dist/engines/index.js.map +1 -0
- package/dist/engines/native.d.ts +55 -0
- package/dist/engines/native.d.ts.map +1 -0
- package/dist/engines/native.js +117 -0
- package/dist/engines/native.js.map +1 -0
- package/dist/engines/types.d.ts +107 -0
- package/dist/engines/types.d.ts.map +1 -0
- package/dist/engines/types.js +2 -0
- package/dist/engines/types.js.map +1 -0
- package/dist/events.d.ts +211 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +69 -0
- package/dist/events.js.map +1 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +82 -0
- package/dist/index.js.map +1 -0
- package/dist/loop.d.ts +132 -0
- package/dist/loop.d.ts.map +1 -0
- package/dist/loop.js +77 -0
- package/dist/loop.js.map +1 -0
- package/dist/models/chat.d.ts +65 -0
- package/dist/models/chat.d.ts.map +1 -0
- package/dist/models/chat.js +66 -0
- package/dist/models/chat.js.map +1 -0
- package/dist/models/middleware.d.ts +63 -0
- package/dist/models/middleware.d.ts.map +1 -0
- package/dist/models/middleware.js +139 -0
- package/dist/models/middleware.js.map +1 -0
- package/dist/models/registry.d.ts +78 -0
- package/dist/models/registry.d.ts.map +1 -0
- package/dist/models/registry.js +0 -0
- package/dist/models/registry.js.map +1 -0
- package/dist/models/types.d.ts +149 -0
- package/dist/models/types.d.ts.map +1 -0
- package/dist/models/types.js +64 -0
- package/dist/models/types.js.map +1 -0
- package/dist/primitives/codeExec.d.ts +25 -0
- package/dist/primitives/codeExec.d.ts.map +1 -0
- package/dist/primitives/codeExec.js +69 -0
- package/dist/primitives/codeExec.js.map +1 -0
- package/dist/primitives/download.d.ts +19 -0
- package/dist/primitives/download.d.ts.map +1 -0
- package/dist/primitives/download.js +113 -0
- package/dist/primitives/download.js.map +1 -0
- package/dist/primitives/fs.d.ts +5 -0
- package/dist/primitives/fs.d.ts.map +1 -0
- package/dist/primitives/fs.js +59 -0
- package/dist/primitives/fs.js.map +1 -0
- package/dist/primitives/http.d.ts +100 -0
- package/dist/primitives/http.d.ts.map +1 -0
- package/dist/primitives/http.js +257 -0
- package/dist/primitives/http.js.map +1 -0
- package/dist/providers/anthropic.d.ts +15 -0
- package/dist/providers/anthropic.d.ts.map +1 -0
- package/dist/providers/anthropic.js +90 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/clientOptions.d.ts +14 -0
- package/dist/providers/clientOptions.d.ts.map +1 -0
- package/dist/providers/clientOptions.js +26 -0
- package/dist/providers/clientOptions.js.map +1 -0
- package/dist/providers/index.d.ts +24 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +51 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/mock.d.ts +25 -0
- package/dist/providers/mock.d.ts.map +1 -0
- package/dist/providers/mock.js +85 -0
- package/dist/providers/mock.js.map +1 -0
- package/dist/providers/openai.d.ts +32 -0
- package/dist/providers/openai.d.ts.map +1 -0
- package/dist/providers/openai.js +118 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers/openrouter.d.ts +29 -0
- package/dist/providers/openrouter.d.ts.map +1 -0
- package/dist/providers/openrouter.js +35 -0
- package/dist/providers/openrouter.js.map +1 -0
- package/dist/providers/types.d.ts +60 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +2 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/routing/staticResolver.d.ts +41 -0
- package/dist/routing/staticResolver.d.ts.map +1 -0
- package/dist/routing/staticResolver.js +67 -0
- package/dist/routing/staticResolver.js.map +1 -0
- package/dist/routing/types.d.ts +72 -0
- package/dist/routing/types.d.ts.map +1 -0
- package/dist/routing/types.js +11 -0
- package/dist/routing/types.js.map +1 -0
- package/dist/secrets.d.ts +8 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +14 -0
- package/dist/secrets.js.map +1 -0
- package/dist/tools/metadata.d.ts +93 -0
- package/dist/tools/metadata.d.ts.map +1 -0
- package/dist/tools/metadata.js +49 -0
- package/dist/tools/metadata.js.map +1 -0
- package/dist/tools/modelTool.d.ts +35 -0
- package/dist/tools/modelTool.d.ts.map +1 -0
- package/dist/tools/modelTool.js +37 -0
- package/dist/tools/modelTool.js.map +1 -0
- package/dist/tools/registry.d.ts +65 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +166 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/types.d.ts +71 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Agent, fetch as undiciFetch } from "undici";
|
|
2
|
+
/** Subset of `dns.lookup({ all: true })` we depend on — injectable for tests. */
|
|
3
|
+
export type Lookup = (host: string) => Promise<Array<{
|
|
4
|
+
address: string;
|
|
5
|
+
family: number;
|
|
6
|
+
}>>;
|
|
7
|
+
/**
|
|
8
|
+
* The `fetch` implementation, injectable for tests. We use undici's OWN `fetch`
|
|
9
|
+
* (not the global) because the global is Node's *bundled* undici, whose internal
|
|
10
|
+
* handler interface is incompatible with a `dispatcher` built from the npm
|
|
11
|
+
* `undici` package — mixing them throws `invalid onRequestStart method`. Pinning
|
|
12
|
+
* the connection (see `pinnedAgent`) requires our dispatcher, so the matching
|
|
13
|
+
* `fetch` must come from the same package.
|
|
14
|
+
*/
|
|
15
|
+
export type FetchImpl = typeof undiciFetch;
|
|
16
|
+
export interface HttpFetchOptions {
|
|
17
|
+
url: string;
|
|
18
|
+
method?: string;
|
|
19
|
+
headers?: Record<string, string>;
|
|
20
|
+
body?: string;
|
|
21
|
+
/** When provided, EVERY request hop (including redirects) must be to a host in this list. */
|
|
22
|
+
allowHosts?: string[];
|
|
23
|
+
/** Cap on the response body; a larger response is rejected (memory-DoS guard). */
|
|
24
|
+
maxBytes?: number;
|
|
25
|
+
/** Max redirects to follow. */
|
|
26
|
+
maxRedirects?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Per-request timeout in milliseconds. Aborts the fetch (and applies afresh to
|
|
29
|
+
* each redirect hop) so a hung endpoint can't stall the whole run. Defaults to
|
|
30
|
+
* `RUNNER_HTTP_TIMEOUT_MS` if set, else 30s.
|
|
31
|
+
*/
|
|
32
|
+
timeoutMs?: number;
|
|
33
|
+
/** DNS resolver (defaults to `dns.lookup`); injectable to test rebinding guards. */
|
|
34
|
+
lookup?: Lookup;
|
|
35
|
+
/** `fetch` implementation (defaults to undici's `fetch`); injectable for tests. */
|
|
36
|
+
fetchImpl?: FetchImpl;
|
|
37
|
+
}
|
|
38
|
+
export interface HttpResponse {
|
|
39
|
+
status: number;
|
|
40
|
+
headers: Record<string, string>;
|
|
41
|
+
body: string;
|
|
42
|
+
}
|
|
43
|
+
/** The validated IP records a hop may connect to, pinned so `fetch` can't re-resolve. */
|
|
44
|
+
type ValidatedRecords = Array<{
|
|
45
|
+
address: string;
|
|
46
|
+
family: number;
|
|
47
|
+
}>;
|
|
48
|
+
/**
|
|
49
|
+
* SSRF guard for a single URL: refuse internal/loopback/link-local hosts (literal
|
|
50
|
+
* or DNS-resolved), and — when `allowHosts` is set — require the host be on it.
|
|
51
|
+
* Shared by httpFetch and the bundle downloader so both enforce the same policy.
|
|
52
|
+
*
|
|
53
|
+
* Returns the exact, already-validated address record(s) the host may connect to:
|
|
54
|
+
* the resolved records for a DNS hostname, or the single literal for an IP host.
|
|
55
|
+
* httpFetch PINS the connection to these (see `pinnedAgent`) so the socket reaches
|
|
56
|
+
* the same IP the guard approved — closing the DNS-rebinding TOCTOU where `fetch`
|
|
57
|
+
* would otherwise re-resolve to a different (internal) address. Callers that only
|
|
58
|
+
* need the boolean verdict (e.g. the bundle downloader) can ignore the return.
|
|
59
|
+
*/
|
|
60
|
+
export declare function assertSafeUrl(target: string, opts?: {
|
|
61
|
+
allowHosts?: string[];
|
|
62
|
+
lookup?: Lookup;
|
|
63
|
+
}): Promise<ValidatedRecords>;
|
|
64
|
+
/**
|
|
65
|
+
* An undici dispatcher that PINS the connection to the already-validated IP(s).
|
|
66
|
+
* Its `connect.lookup` ignores the hostname and returns ONLY the records the SSRF
|
|
67
|
+
* guard approved, so the socket connects to the exact IP that was checked — a
|
|
68
|
+
* hostile low-TTL resolver can't hand a different (internal) address to `fetch`.
|
|
69
|
+
*
|
|
70
|
+
* TLS correctness: we pin the *IP* via `lookup`, but undici still presents the
|
|
71
|
+
* original hostname for SNI and certificate verification (its connector derives
|
|
72
|
+
* `servername` from the request host, and `net/tls.connect` keeps `host` = the
|
|
73
|
+
* hostname while using our lookup result only to choose the socket address). So
|
|
74
|
+
* cert hostname checks are preserved and verification is NEVER disabled. We do
|
|
75
|
+
* not set `rejectUnauthorized` — the secure default stands.
|
|
76
|
+
*
|
|
77
|
+
* Exported for tests: the pin + TLS-servername behavior is the security-critical
|
|
78
|
+
* unit and is exercised directly against loopback servers.
|
|
79
|
+
*/
|
|
80
|
+
export declare function pinnedAgent(records: ValidatedRecords): Agent;
|
|
81
|
+
/**
|
|
82
|
+
* Thin wrapper over undici's `fetch`. A general-purpose primitive: domain
|
|
83
|
+
* specific API logic belongs in user-repo code that calls this.
|
|
84
|
+
*
|
|
85
|
+
* SSRF guards: requests to internal/loopback/link-local addresses are ALWAYS
|
|
86
|
+
* refused (e.g. cloud metadata at 169.254.169.254), and when `allowHosts` is set
|
|
87
|
+
* the host must be on it. Redirects are followed manually so EACH hop is
|
|
88
|
+
* re-validated (an allowed/public host could 30x to an internal one) AND pinned
|
|
89
|
+
* (see below). The response body is always size-capped.
|
|
90
|
+
*
|
|
91
|
+
* DNS-rebinding TOCTOU is closed: each hop is validated, then the connection is
|
|
92
|
+
* PINNED to the exact validated IP via a per-hop undici dispatcher (`pinnedAgent`).
|
|
93
|
+
* `fetch` therefore cannot re-resolve the hostname to a different address between
|
|
94
|
+
* the guard's check and the socket connect. TLS SNI/cert verification still uses
|
|
95
|
+
* the original hostname (see `pinnedAgent`). Prefer an explicit `allowHosts`
|
|
96
|
+
* allowlist for untrusted callers regardless.
|
|
97
|
+
*/
|
|
98
|
+
export declare function httpFetch(options: HttpFetchOptions): Promise<HttpResponse>;
|
|
99
|
+
export {};
|
|
100
|
+
//# sourceMappingURL=http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/primitives/http.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,EAAE,KAAK,IAAI,WAAW,EAAE,MAAM,QAAQ,CAAA;AAEpD,iFAAiF;AACjF,MAAM,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,CAAA;AAI1F;;;;;;;GAOG;AACH,MAAM,MAAM,SAAS,GAAG,OAAO,WAAW,CAAA;AAE1C,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,6FAA6F;IAC7F,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IACrB,kFAAkF;IAClF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,+BAA+B;IAC/B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,oFAAoF;IACpF,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,mFAAmF;IACnF,SAAS,CAAC,EAAE,SAAS,CAAA;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,IAAI,EAAE,MAAM,CAAA;CACb;AAYD,yFAAyF;AACzF,KAAK,gBAAgB,GAAG,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA;AAElE;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,IAAI,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GACpD,OAAO,CAAC,gBAAgB,CAAC,CAyB3B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,gBAAgB,GAAG,KAAK,CAW5D;AA8DD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,YAAY,CAAC,CA4DhF"}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import dns from "node:dns/promises";
|
|
3
|
+
import { Agent, fetch as undiciFetch } from "undici";
|
|
4
|
+
const defaultLookup = (host) => dns.lookup(host, { all: true });
|
|
5
|
+
const DEFAULT_MAX_BYTES = 8 * 1024 * 1024;
|
|
6
|
+
const DEFAULT_MAX_REDIRECTS = 5;
|
|
7
|
+
const FALLBACK_TIMEOUT_MS = 30_000;
|
|
8
|
+
/** Resolve the default per-request timeout from env, falling back to 30s. */
|
|
9
|
+
function defaultTimeoutMs() {
|
|
10
|
+
const raw = Number(process.env.RUNNER_HTTP_TIMEOUT_MS);
|
|
11
|
+
return Number.isFinite(raw) && raw > 0 ? raw : FALLBACK_TIMEOUT_MS;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* SSRF guard for a single URL: refuse internal/loopback/link-local hosts (literal
|
|
15
|
+
* or DNS-resolved), and — when `allowHosts` is set — require the host be on it.
|
|
16
|
+
* Shared by httpFetch and the bundle downloader so both enforce the same policy.
|
|
17
|
+
*
|
|
18
|
+
* Returns the exact, already-validated address record(s) the host may connect to:
|
|
19
|
+
* the resolved records for a DNS hostname, or the single literal for an IP host.
|
|
20
|
+
* httpFetch PINS the connection to these (see `pinnedAgent`) so the socket reaches
|
|
21
|
+
* the same IP the guard approved — closing the DNS-rebinding TOCTOU where `fetch`
|
|
22
|
+
* would otherwise re-resolve to a different (internal) address. Callers that only
|
|
23
|
+
* need the boolean verdict (e.g. the bundle downloader) can ignore the return.
|
|
24
|
+
*/
|
|
25
|
+
export async function assertSafeUrl(target, opts = {}) {
|
|
26
|
+
const { allowHosts, lookup = defaultLookup } = opts;
|
|
27
|
+
const u = new URL(target);
|
|
28
|
+
if (isInternalHost(u.hostname))
|
|
29
|
+
throw new Error(`host not allowed (internal): ${u.hostname}`);
|
|
30
|
+
if (allowHosts && !allowHosts.includes(u.host))
|
|
31
|
+
throw new Error(`host not allowed: ${u.host}`);
|
|
32
|
+
const literal = u.hostname.replace(/^\[|\]$/g, ""); // URL.hostname keeps IPv6 brackets
|
|
33
|
+
const literalVersion = net.isIP(literal);
|
|
34
|
+
if (literalVersion !== 0) {
|
|
35
|
+
// An IP literal: already classified as public above; pin to it directly.
|
|
36
|
+
return [{ address: literal, family: literalVersion }];
|
|
37
|
+
}
|
|
38
|
+
const records = await lookup(u.hostname);
|
|
39
|
+
if (records.length === 0) {
|
|
40
|
+
// No records means there is nothing to pin; surface a clear guard error
|
|
41
|
+
// rather than letting the pinned dispatcher fail later with a cryptic socket error.
|
|
42
|
+
throw new Error(`host not allowed (no DNS records): ${u.hostname}`);
|
|
43
|
+
}
|
|
44
|
+
for (const { address } of records) {
|
|
45
|
+
if (isInternalAddress(address)) {
|
|
46
|
+
throw new Error(`host not allowed (resolves to internal): ${u.hostname} -> ${address}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return records;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* An undici dispatcher that PINS the connection to the already-validated IP(s).
|
|
53
|
+
* Its `connect.lookup` ignores the hostname and returns ONLY the records the SSRF
|
|
54
|
+
* guard approved, so the socket connects to the exact IP that was checked — a
|
|
55
|
+
* hostile low-TTL resolver can't hand a different (internal) address to `fetch`.
|
|
56
|
+
*
|
|
57
|
+
* TLS correctness: we pin the *IP* via `lookup`, but undici still presents the
|
|
58
|
+
* original hostname for SNI and certificate verification (its connector derives
|
|
59
|
+
* `servername` from the request host, and `net/tls.connect` keeps `host` = the
|
|
60
|
+
* hostname while using our lookup result only to choose the socket address). So
|
|
61
|
+
* cert hostname checks are preserved and verification is NEVER disabled. We do
|
|
62
|
+
* not set `rejectUnauthorized` — the secure default stands.
|
|
63
|
+
*
|
|
64
|
+
* Exported for tests: the pin + TLS-servername behavior is the security-critical
|
|
65
|
+
* unit and is exercised directly against loopback servers.
|
|
66
|
+
*/
|
|
67
|
+
export function pinnedAgent(records) {
|
|
68
|
+
return new Agent({
|
|
69
|
+
connect: {
|
|
70
|
+
// Node's `net.connect` calls lookup as `(hostname, { all: true }, cb)` and
|
|
71
|
+
// expects the array form `cb(null, [{ address, family }])`. Return the pinned
|
|
72
|
+
// records verbatim, ignoring `hostname` so no fresh DNS resolution happens.
|
|
73
|
+
lookup: (_hostname, _options, callback) => {
|
|
74
|
+
callback(null, records);
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Start a single fetch hop under an AbortController-backed timeout, PINNED to the
|
|
81
|
+
* already-validated IP(s) via a per-hop undici dispatcher. The SAME
|
|
82
|
+
* controller/timer must remain armed while the body is streamed (see
|
|
83
|
+
* `readBodyCapped`), because `fetch` resolves once headers arrive — a server that
|
|
84
|
+
* then drip-feeds the body would otherwise stall forever. On timeout the abort
|
|
85
|
+
* surfaces as a clear `http timeout` error rather than the raw AbortError.
|
|
86
|
+
*
|
|
87
|
+
* A fresh dispatcher is built per hop (each hop is validated + pinned to its own
|
|
88
|
+
* address) and closed in `done()` so connections aren't reused across hosts.
|
|
89
|
+
*/
|
|
90
|
+
async function fetchWithTimeout(fetchImpl, target, init, records, timeoutMs) {
|
|
91
|
+
const controller = new AbortController();
|
|
92
|
+
const agent = pinnedAgent(records);
|
|
93
|
+
let timedOut = false;
|
|
94
|
+
const timer = setTimeout(() => {
|
|
95
|
+
timedOut = true;
|
|
96
|
+
controller.abort();
|
|
97
|
+
}, timeoutMs);
|
|
98
|
+
const closeAgent = () => {
|
|
99
|
+
void agent.close().catch(() => { });
|
|
100
|
+
};
|
|
101
|
+
try {
|
|
102
|
+
const res = await fetchImpl(target, { ...init, signal: controller.signal, dispatcher: agent });
|
|
103
|
+
return {
|
|
104
|
+
res,
|
|
105
|
+
controller,
|
|
106
|
+
timedOut: () => timedOut,
|
|
107
|
+
done: () => {
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
closeAgent();
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
closeAgent();
|
|
116
|
+
if (timedOut) {
|
|
117
|
+
throw new Error(`http timeout after ${timeoutMs}ms: ${target}`, { cause: err });
|
|
118
|
+
}
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Thin wrapper over undici's `fetch`. A general-purpose primitive: domain
|
|
124
|
+
* specific API logic belongs in user-repo code that calls this.
|
|
125
|
+
*
|
|
126
|
+
* SSRF guards: requests to internal/loopback/link-local addresses are ALWAYS
|
|
127
|
+
* refused (e.g. cloud metadata at 169.254.169.254), and when `allowHosts` is set
|
|
128
|
+
* the host must be on it. Redirects are followed manually so EACH hop is
|
|
129
|
+
* re-validated (an allowed/public host could 30x to an internal one) AND pinned
|
|
130
|
+
* (see below). The response body is always size-capped.
|
|
131
|
+
*
|
|
132
|
+
* DNS-rebinding TOCTOU is closed: each hop is validated, then the connection is
|
|
133
|
+
* PINNED to the exact validated IP via a per-hop undici dispatcher (`pinnedAgent`).
|
|
134
|
+
* `fetch` therefore cannot re-resolve the hostname to a different address between
|
|
135
|
+
* the guard's check and the socket connect. TLS SNI/cert verification still uses
|
|
136
|
+
* the original hostname (see `pinnedAgent`). Prefer an explicit `allowHosts`
|
|
137
|
+
* allowlist for untrusted callers regardless.
|
|
138
|
+
*/
|
|
139
|
+
export async function httpFetch(options) {
|
|
140
|
+
const { url, method = "GET", headers, body, allowHosts, maxBytes = DEFAULT_MAX_BYTES, maxRedirects = DEFAULT_MAX_REDIRECTS, timeoutMs = defaultTimeoutMs(), lookup = defaultLookup, fetchImpl = undiciFetch, } = options;
|
|
141
|
+
const assertSafe = (target) => assertSafeUrl(target, { allowHosts, lookup });
|
|
142
|
+
let currentUrl = url;
|
|
143
|
+
let records = await assertSafe(currentUrl);
|
|
144
|
+
let hop = await fetchWithTimeout(fetchImpl, currentUrl, { method, headers, body, redirect: "manual" }, records, timeoutMs);
|
|
145
|
+
let hops = 0;
|
|
146
|
+
while (hop.res.status >= 300 && hop.res.status < 400 && hop.res.headers.has("location")) {
|
|
147
|
+
// Cancel the unread redirect body so the socket isn't left half-open before
|
|
148
|
+
// we close this hop's dispatcher and follow the Location.
|
|
149
|
+
await hop.res.body?.cancel().catch(() => { });
|
|
150
|
+
hop.done(); // disarm this hop's timer + close its pinned dispatcher before following the redirect
|
|
151
|
+
if (hops >= maxRedirects)
|
|
152
|
+
throw new Error("too many redirects");
|
|
153
|
+
const next = new URL(hop.res.headers.get("location"), currentUrl).toString();
|
|
154
|
+
records = await assertSafe(next); // validate AND re-pin the new hop's address
|
|
155
|
+
currentUrl = next;
|
|
156
|
+
hop = await fetchWithTimeout(fetchImpl, currentUrl, { method, headers, redirect: "manual" }, records, timeoutMs);
|
|
157
|
+
hops += 1;
|
|
158
|
+
}
|
|
159
|
+
const res = hop.res;
|
|
160
|
+
const responseHeaders = {};
|
|
161
|
+
res.headers.forEach((value, key) => {
|
|
162
|
+
responseHeaders[key] = value;
|
|
163
|
+
});
|
|
164
|
+
// The hop's timer is still armed, so the body read below is bounded by the same
|
|
165
|
+
// per-request timeout: a stalled body trips `hop.timedOut()` → `http timeout`.
|
|
166
|
+
return {
|
|
167
|
+
status: res.status,
|
|
168
|
+
headers: responseHeaders,
|
|
169
|
+
body: await readBodyCapped(hop, currentUrl, maxBytes),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Read the body, aborting if it exceeds `maxBytes` OR if the hop's timeout fires
|
|
174
|
+
* mid-stream. Bounding the body (not just time-to-headers) means a server that
|
|
175
|
+
* sends headers then drip-feeds the body still can't stall the run.
|
|
176
|
+
*/
|
|
177
|
+
async function readBodyCapped(hop, target, maxBytes) {
|
|
178
|
+
const { res, controller, timedOut, done } = hop;
|
|
179
|
+
try {
|
|
180
|
+
if (!res.body)
|
|
181
|
+
return await res.text();
|
|
182
|
+
// undici's ReadableStream types are non-generic, so the reader yields `any`;
|
|
183
|
+
// fetch response bodies are always byte streams.
|
|
184
|
+
const reader = res.body.getReader();
|
|
185
|
+
const chunks = [];
|
|
186
|
+
let total = 0;
|
|
187
|
+
try {
|
|
188
|
+
for (;;) {
|
|
189
|
+
const { done: streamDone, value } = await reader.read();
|
|
190
|
+
if (streamDone)
|
|
191
|
+
break;
|
|
192
|
+
total += value.byteLength;
|
|
193
|
+
if (total > maxBytes) {
|
|
194
|
+
await reader.cancel();
|
|
195
|
+
throw new Error(`response exceeds ${maxBytes} bytes`);
|
|
196
|
+
}
|
|
197
|
+
chunks.push(value);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
// The timer aborts the controller, which surfaces here as an abort/read
|
|
202
|
+
// error — translate it to the same clear timeout message as the header phase.
|
|
203
|
+
if (timedOut() || controller.signal.aborted) {
|
|
204
|
+
throw new Error(`http timeout: ${target}`, { cause: err });
|
|
205
|
+
}
|
|
206
|
+
throw err;
|
|
207
|
+
}
|
|
208
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
done();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/** True for hostnames/IP-literals that point at the host or a private network. */
|
|
215
|
+
function isInternalHost(host) {
|
|
216
|
+
if (host === "localhost")
|
|
217
|
+
return true;
|
|
218
|
+
const bare = host.replace(/^\[|\]$/g, ""); // URL.hostname keeps IPv6 brackets
|
|
219
|
+
const version = net.isIP(bare);
|
|
220
|
+
if (version === 4)
|
|
221
|
+
return isInternalV4(bare);
|
|
222
|
+
if (version === 6)
|
|
223
|
+
return isInternalV6(bare);
|
|
224
|
+
return false; // a regular hostname — not an IP literal we can classify here
|
|
225
|
+
}
|
|
226
|
+
/** Classify a resolved IP address string (v4 or v6) as internal. */
|
|
227
|
+
function isInternalAddress(ip) {
|
|
228
|
+
const version = net.isIP(ip);
|
|
229
|
+
if (version === 4)
|
|
230
|
+
return isInternalV4(ip);
|
|
231
|
+
if (version === 6)
|
|
232
|
+
return isInternalV6(ip);
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
function isInternalV4(ip) {
|
|
236
|
+
const [a = -1, b = -1] = ip.split(".").map(Number);
|
|
237
|
+
if (a === 0 || a === 127 || a === 10)
|
|
238
|
+
return true; // unspecified, loopback, private
|
|
239
|
+
if (a === 169 && b === 254)
|
|
240
|
+
return true; // link-local (incl. cloud metadata)
|
|
241
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
242
|
+
return true; // private
|
|
243
|
+
if (a === 192 && b === 168)
|
|
244
|
+
return true; // private
|
|
245
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
246
|
+
return true; // CGNAT (RFC 6598)
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
function isInternalV6(ip) {
|
|
250
|
+
const x = ip.toLowerCase().replace(/^\[|\]$/g, "");
|
|
251
|
+
// IPv4-mapped (::ffff:a.b.c.d) maps onto a v4 address — classify by the embedded v4.
|
|
252
|
+
const mapped = x.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
|
|
253
|
+
if (mapped?.[1])
|
|
254
|
+
return isInternalV4(mapped[1]);
|
|
255
|
+
return (x === "::1" || x === "::" || x.startsWith("fc") || x.startsWith("fd") || x.startsWith("fe80"));
|
|
256
|
+
}
|
|
257
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/primitives/http.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
import type { AgentMessage, CompletionRequest, ConverseRequest, ConverseResult, Provider } from "./types.js";
|
|
3
|
+
/** Map provider-neutral messages to Anthropic's message params (pure; tested). */
|
|
4
|
+
export declare function toAnthropicMessages(messages: AgentMessage[]): Anthropic.MessageParam[];
|
|
5
|
+
/** Reduce an Anthropic response's content blocks to text + tool calls (pure; tested). */
|
|
6
|
+
export declare function parseAnthropicContent(content: Anthropic.ContentBlock[]): ConverseResult;
|
|
7
|
+
export declare class AnthropicProvider implements Provider {
|
|
8
|
+
readonly name = "anthropic";
|
|
9
|
+
private client;
|
|
10
|
+
private model;
|
|
11
|
+
constructor(apiKey: string, model?: string);
|
|
12
|
+
complete(req: CompletionRequest): Promise<string>;
|
|
13
|
+
converse(req: ConverseRequest): Promise<ConverseResult>;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=anthropic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anthropic.d.ts","sourceRoot":"","sources":["../../src/providers/anthropic.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,mBAAmB,CAAA;AAEzC,OAAO,KAAK,EACV,YAAY,EACZ,iBAAiB,EACjB,eAAe,EACf,cAAc,EACd,QAAQ,EAET,MAAM,YAAY,CAAA;AAEnB,kFAAkF;AAClF,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,SAAS,CAAC,YAAY,EAAE,CAwBtF;AAED,yFAAyF;AACzF,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,SAAS,CAAC,YAAY,EAAE,GAAG,cAAc,CAevF;AAED,qBAAa,iBAAkB,YAAW,QAAQ;IAChD,QAAQ,CAAC,IAAI,eAAc;IAC3B,OAAO,CAAC,MAAM,CAAW;IACzB,OAAO,CAAC,KAAK,CAAQ;gBAET,MAAM,EAAE,MAAM,EAAE,KAAK,SAAqD;IAYhF,QAAQ,CAAC,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC;IAcjD,QAAQ,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC;CAe9D"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
+
import { maxTokens, providerMaxRetries, providerTimeoutMs } from "./clientOptions.js";
|
|
3
|
+
/** Map provider-neutral messages to Anthropic's message params (pure; tested). */
|
|
4
|
+
export function toAnthropicMessages(messages) {
|
|
5
|
+
return messages.map((m) => {
|
|
6
|
+
if (m.role === "user") {
|
|
7
|
+
return { role: "user", content: m.text };
|
|
8
|
+
}
|
|
9
|
+
if (m.role === "assistant") {
|
|
10
|
+
const blocks = [];
|
|
11
|
+
if (m.text)
|
|
12
|
+
blocks.push({ type: "text", text: m.text });
|
|
13
|
+
for (const tc of m.toolCalls) {
|
|
14
|
+
blocks.push({ type: "tool_use", id: tc.id, name: tc.name, input: tc.input });
|
|
15
|
+
}
|
|
16
|
+
return { role: "assistant", content: blocks };
|
|
17
|
+
}
|
|
18
|
+
// Tool results are sent back as a user turn of tool_result blocks.
|
|
19
|
+
return {
|
|
20
|
+
role: "user",
|
|
21
|
+
content: m.results.map((r) => ({
|
|
22
|
+
type: "tool_result",
|
|
23
|
+
tool_use_id: r.toolCallId,
|
|
24
|
+
content: r.content,
|
|
25
|
+
is_error: r.isError ?? false,
|
|
26
|
+
})),
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/** Reduce an Anthropic response's content blocks to text + tool calls (pure; tested). */
|
|
31
|
+
export function parseAnthropicContent(content) {
|
|
32
|
+
let text = "";
|
|
33
|
+
const toolCalls = [];
|
|
34
|
+
for (const block of content) {
|
|
35
|
+
if (block.type === "text") {
|
|
36
|
+
text += text ? `\n${block.text}` : block.text;
|
|
37
|
+
}
|
|
38
|
+
else if (block.type === "tool_use") {
|
|
39
|
+
toolCalls.push({
|
|
40
|
+
id: block.id,
|
|
41
|
+
name: block.name,
|
|
42
|
+
input: (block.input ?? {}),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { text, toolCalls };
|
|
47
|
+
}
|
|
48
|
+
export class AnthropicProvider {
|
|
49
|
+
name = "anthropic";
|
|
50
|
+
client;
|
|
51
|
+
model;
|
|
52
|
+
constructor(apiKey, model = process.env.ANTHROPIC_MODEL ?? "claude-sonnet-4-6") {
|
|
53
|
+
// The SDK retries transient 429/5xx/network errors with exponential backoff
|
|
54
|
+
// internally, so a blip doesn't fail the whole run; `timeout` bounds each
|
|
55
|
+
// request. Both are env-overridable.
|
|
56
|
+
this.client = new Anthropic({
|
|
57
|
+
apiKey,
|
|
58
|
+
timeout: providerTimeoutMs(),
|
|
59
|
+
maxRetries: providerMaxRetries(),
|
|
60
|
+
});
|
|
61
|
+
this.model = model;
|
|
62
|
+
}
|
|
63
|
+
async complete(req) {
|
|
64
|
+
const message = await this.client.messages.create({
|
|
65
|
+
model: this.model,
|
|
66
|
+
max_tokens: maxTokens(),
|
|
67
|
+
system: req.system,
|
|
68
|
+
messages: [{ role: "user", content: req.prompt }],
|
|
69
|
+
});
|
|
70
|
+
return message.content
|
|
71
|
+
.filter((block) => block.type === "text")
|
|
72
|
+
.map((block) => block.text)
|
|
73
|
+
.join("\n");
|
|
74
|
+
}
|
|
75
|
+
async converse(req) {
|
|
76
|
+
const message = await this.client.messages.create({
|
|
77
|
+
model: this.model,
|
|
78
|
+
max_tokens: maxTokens(),
|
|
79
|
+
system: req.system,
|
|
80
|
+
tools: req.tools.map((t) => ({
|
|
81
|
+
name: t.name,
|
|
82
|
+
description: t.description,
|
|
83
|
+
input_schema: t.inputSchema,
|
|
84
|
+
})),
|
|
85
|
+
messages: toAnthropicMessages(req.messages),
|
|
86
|
+
});
|
|
87
|
+
return parseAnthropicContent(message.content);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=anthropic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anthropic.js","sourceRoot":"","sources":["../../src/providers/anthropic.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,mBAAmB,CAAA;AACzC,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAUrF,kFAAkF;AAClF,MAAM,UAAU,mBAAmB,CAAC,QAAwB;IAC1D,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAA0B,EAAE;QAChD,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YACtB,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;QAC1C,CAAC;QACD,IAAI,CAAC,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;YAC3B,MAAM,MAAM,GAAkC,EAAE,CAAA;YAChD,IAAI,CAAC,CAAC,IAAI;gBAAE,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAA;YACvD,KAAK,MAAM,EAAE,IAAI,CAAC,CAAC,SAAS,EAAE,CAAC;gBAC7B,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,CAAA;YAC9E,CAAC;YACD,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,CAAA;QAC/C,CAAC;QACD,mEAAmE;QACnE,OAAO;YACL,IAAI,EAAE,MAAM;YACZ,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBAC7B,IAAI,EAAE,aAAa;gBACnB,WAAW,EAAE,CAAC,CAAC,UAAU;gBACzB,OAAO,EAAE,CAAC,CAAC,OAAO;gBAClB,QAAQ,EAAE,CAAC,CAAC,OAAO,IAAI,KAAK;aAC7B,CAAC,CAAC;SACJ,CAAA;IACH,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,qBAAqB,CAAC,OAAiC;IACrE,IAAI,IAAI,GAAG,EAAE,CAAA;IACb,MAAM,SAAS,GAAe,EAAE,CAAA;IAChC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YAC1B,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAA;QAC/C,CAAC;aAAM,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YACrC,SAAS,CAAC,IAAI,CAAC;gBACb,EAAE,EAAE,KAAK,CAAC,EAAE;gBACZ,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,KAAK,EAAE,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE,CAA4B;aACtD,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAA;AAC5B,CAAC;AAED,MAAM,OAAO,iBAAiB;IACnB,IAAI,GAAG,WAAW,CAAA;IACnB,MAAM,CAAW;IACjB,KAAK,CAAQ;IAErB,YAAY,MAAc,EAAE,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,mBAAmB;QACpF,4EAA4E;QAC5E,0EAA0E;QAC1E,qCAAqC;QACrC,IAAI,CAAC,MAAM,GAAG,IAAI,SAAS,CAAC;YAC1B,MAAM;YACN,OAAO,EAAE,iBAAiB,EAAE;YAC5B,UAAU,EAAE,kBAAkB,EAAE;SACjC,CAAC,CAAA;QACF,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;IACpB,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,GAAsB;QACnC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;YAChD,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,UAAU,EAAE,SAAS,EAAE;YACvB,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,QAAQ,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC;SAClD,CAAC,CAAA;QAEF,OAAO,OAAO,CAAC,OAAO;aACnB,MAAM,CAAC,CAAC,KAAK,EAAgC,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,MAAM,CAAC;aACtE,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC;aAC1B,IAAI,CAAC,IAAI,CAAC,CAAA;IACf,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,GAAoB;QACjC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;YAChD,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,UAAU,EAAE,SAAS,EAAE;YACvB,MAAM,EAAE,GAAG,CAAC,MAAM;YAClB,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBAC3B,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,WAAW,EAAE,CAAC,CAAC,WAAW;gBAC1B,YAAY,EAAE,CAAC,CAAC,WAAyC;aAC1D,CAAC,CAAC;YACH,QAAQ,EAAE,mBAAmB,CAAC,GAAG,CAAC,QAAQ,CAAC;SAC5C,CAAC,CAAA;QAEF,OAAO,qBAAqB,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;IAC/C,CAAC;CACF"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared resolution of provider-SDK robustness knobs. Both the Anthropic and
|
|
3
|
+
* OpenAI SDKs accept `timeout` (per-request, ms) and `maxRetries` in their
|
|
4
|
+
* constructor and handle transient 429/5xx/network retries with backoff
|
|
5
|
+
* themselves — we just supply sensible, env-overridable defaults. `maxTokens`
|
|
6
|
+
* is the per-request completion cap both providers send with each call.
|
|
7
|
+
*/
|
|
8
|
+
/** Per-request provider timeout in ms (`RUNNER_PROVIDER_TIMEOUT_MS`, default 60s). */
|
|
9
|
+
export declare function providerTimeoutMs(): number;
|
|
10
|
+
/** SDK auto-retry count for transient failures (`RUNNER_PROVIDER_MAX_RETRIES`, default 2). */
|
|
11
|
+
export declare function providerMaxRetries(): number;
|
|
12
|
+
/** Per-request completion token cap (`RUNNER_MAX_TOKENS`, default 4096). */
|
|
13
|
+
export declare function maxTokens(): number;
|
|
14
|
+
//# sourceMappingURL=clientOptions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clientOptions.d.ts","sourceRoot":"","sources":["../../src/providers/clientOptions.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,sFAAsF;AACtF,wBAAgB,iBAAiB,IAAI,MAAM,CAG1C;AAED,8FAA8F;AAC9F,wBAAgB,kBAAkB,IAAI,MAAM,CAG3C;AAED,4EAA4E;AAC5E,wBAAgB,SAAS,IAAI,MAAM,CAGlC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared resolution of provider-SDK robustness knobs. Both the Anthropic and
|
|
3
|
+
* OpenAI SDKs accept `timeout` (per-request, ms) and `maxRetries` in their
|
|
4
|
+
* constructor and handle transient 429/5xx/network retries with backoff
|
|
5
|
+
* themselves — we just supply sensible, env-overridable defaults. `maxTokens`
|
|
6
|
+
* is the per-request completion cap both providers send with each call.
|
|
7
|
+
*/
|
|
8
|
+
const FALLBACK_TIMEOUT_MS = 60_000;
|
|
9
|
+
const FALLBACK_MAX_RETRIES = 2;
|
|
10
|
+
const FALLBACK_MAX_TOKENS = 4096;
|
|
11
|
+
/** Per-request provider timeout in ms (`RUNNER_PROVIDER_TIMEOUT_MS`, default 60s). */
|
|
12
|
+
export function providerTimeoutMs() {
|
|
13
|
+
const raw = Number(process.env.RUNNER_PROVIDER_TIMEOUT_MS);
|
|
14
|
+
return Number.isFinite(raw) && raw > 0 ? raw : FALLBACK_TIMEOUT_MS;
|
|
15
|
+
}
|
|
16
|
+
/** SDK auto-retry count for transient failures (`RUNNER_PROVIDER_MAX_RETRIES`, default 2). */
|
|
17
|
+
export function providerMaxRetries() {
|
|
18
|
+
const raw = Number(process.env.RUNNER_PROVIDER_MAX_RETRIES);
|
|
19
|
+
return Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : FALLBACK_MAX_RETRIES;
|
|
20
|
+
}
|
|
21
|
+
/** Per-request completion token cap (`RUNNER_MAX_TOKENS`, default 4096). */
|
|
22
|
+
export function maxTokens() {
|
|
23
|
+
const raw = Number(process.env.RUNNER_MAX_TOKENS);
|
|
24
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : FALLBACK_MAX_TOKENS;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=clientOptions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clientOptions.js","sourceRoot":"","sources":["../../src/providers/clientOptions.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,mBAAmB,GAAG,MAAM,CAAA;AAClC,MAAM,oBAAoB,GAAG,CAAC,CAAA;AAC9B,MAAM,mBAAmB,GAAG,IAAI,CAAA;AAEhC,sFAAsF;AACtF,MAAM,UAAU,iBAAiB;IAC/B,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAA;IAC1D,OAAO,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,mBAAmB,CAAA;AACpE,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,kBAAkB;IAChC,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,2BAA2B,CAAC,CAAA;IAC3D,OAAO,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,oBAAoB,CAAA;AAClF,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,SAAS;IACvB,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAA;IACjD,OAAO,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,mBAAmB,CAAA;AAChF,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Provider } from "./types.js";
|
|
2
|
+
export type { Provider, CompletionRequest } from "./types.js";
|
|
3
|
+
/** Per-run model + bring-your-own keys, resolved by the caller. */
|
|
4
|
+
export interface ProviderSelection {
|
|
5
|
+
/** Model id for this run; falls back to the provider's env model when absent. */
|
|
6
|
+
model?: string | null;
|
|
7
|
+
/** Per-org provider API keys; each falls back to the runner's env key when absent. */
|
|
8
|
+
credentials?: {
|
|
9
|
+
anthropic?: string;
|
|
10
|
+
openai?: string;
|
|
11
|
+
openrouter?: string;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Resolves a provider from the workflow's preference and the host's
|
|
16
|
+
* environment. The kernel is provider-agnostic; when the preferred provider has
|
|
17
|
+
* no credentials we degrade to the offline mock rather than failing the run,
|
|
18
|
+
* which keeps a harness runnable end-to-end without keys.
|
|
19
|
+
*
|
|
20
|
+
* `opts` carries the per-run model and per-org BYO keys; both fall back to the
|
|
21
|
+
* host's env (model via the provider constructor's env default, keys below).
|
|
22
|
+
*/
|
|
23
|
+
export declare function selectProvider(preferred?: string | null, opts?: ProviderSelection): Provider;
|
|
24
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAE1C,YAAY,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAE7D,mEAAmE;AACnE,MAAM,WAAW,iBAAiB;IAChC,iFAAiF;IACjF,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,sFAAsF;IACtF,WAAW,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAC3E;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,GAAE,iBAAsB,GAAG,QAAQ,CAgChG"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { AnthropicProvider } from "./anthropic.js";
|
|
2
|
+
import { OpenAIProvider } from "./openai.js";
|
|
3
|
+
import { OpenRouterProvider } from "./openrouter.js";
|
|
4
|
+
import { MockProvider } from "./mock.js";
|
|
5
|
+
/**
|
|
6
|
+
* Resolves a provider from the workflow's preference and the host's
|
|
7
|
+
* environment. The kernel is provider-agnostic; when the preferred provider has
|
|
8
|
+
* no credentials we degrade to the offline mock rather than failing the run,
|
|
9
|
+
* which keeps a harness runnable end-to-end without keys.
|
|
10
|
+
*
|
|
11
|
+
* `opts` carries the per-run model and per-org BYO keys; both fall back to the
|
|
12
|
+
* host's env (model via the provider constructor's env default, keys below).
|
|
13
|
+
*/
|
|
14
|
+
export function selectProvider(preferred, opts = {}) {
|
|
15
|
+
const anthropicKey = opts.credentials?.anthropic ?? process.env.ANTHROPIC_API_KEY;
|
|
16
|
+
const openaiKey = opts.credentials?.openai ?? process.env.OPENAI_API_KEY;
|
|
17
|
+
const openrouterKey = opts.credentials?.openrouter ?? process.env.OPENROUTER_API_KEY;
|
|
18
|
+
// undefined → the provider constructor's env-default model kicks in.
|
|
19
|
+
const model = opts.model ?? undefined;
|
|
20
|
+
switch (preferred) {
|
|
21
|
+
case "anthropic":
|
|
22
|
+
if (anthropicKey)
|
|
23
|
+
return new AnthropicProvider(anthropicKey, model);
|
|
24
|
+
break;
|
|
25
|
+
case "openai":
|
|
26
|
+
if (openaiKey)
|
|
27
|
+
return new OpenAIProvider(openaiKey, model);
|
|
28
|
+
break;
|
|
29
|
+
case "openrouter":
|
|
30
|
+
if (openrouterKey)
|
|
31
|
+
return new OpenRouterProvider(openrouterKey, model);
|
|
32
|
+
break;
|
|
33
|
+
case "mock":
|
|
34
|
+
// An explicit `preferred: mock` always gets the mock, even when a key is
|
|
35
|
+
// configured — it's the offline dry-run lens, so honour it deterministically
|
|
36
|
+
// rather than silently upgrading to a real provider. Hand it the full
|
|
37
|
+
// selection so it can self-document the model + BYO key it WOULD have used.
|
|
38
|
+
return new MockProvider(opts);
|
|
39
|
+
}
|
|
40
|
+
// No explicit (or usable) preference: pick whatever is configured.
|
|
41
|
+
if (anthropicKey)
|
|
42
|
+
return new AnthropicProvider(anthropicKey, model);
|
|
43
|
+
if (openaiKey)
|
|
44
|
+
return new OpenAIProvider(openaiKey, model);
|
|
45
|
+
if (openrouterKey)
|
|
46
|
+
return new OpenRouterProvider(openrouterKey, model);
|
|
47
|
+
// The offline fallback still carries the selection, so a keyless run's output
|
|
48
|
+
// artifact reports the model (and any BYO key) the caller resolved.
|
|
49
|
+
return new MockProvider(opts);
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAC5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAaxC;;;;;;;;GAQG;AACH,MAAM,UAAU,cAAc,CAAC,SAAyB,EAAE,OAA0B,EAAE;IACpF,MAAM,YAAY,GAAG,IAAI,CAAC,WAAW,EAAE,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAA;IACjF,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,EAAE,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,CAAA;IACxE,MAAM,aAAa,GAAG,IAAI,CAAC,WAAW,EAAE,UAAU,IAAI,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAA;IACpF,qEAAqE;IACrE,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,SAAS,CAAA;IAErC,QAAQ,SAAS,EAAE,CAAC;QAClB,KAAK,WAAW;YACd,IAAI,YAAY;gBAAE,OAAO,IAAI,iBAAiB,CAAC,YAAY,EAAE,KAAK,CAAC,CAAA;YACnE,MAAK;QACP,KAAK,QAAQ;YACX,IAAI,SAAS;gBAAE,OAAO,IAAI,cAAc,CAAC,SAAS,EAAE,KAAK,CAAC,CAAA;YAC1D,MAAK;QACP,KAAK,YAAY;YACf,IAAI,aAAa;gBAAE,OAAO,IAAI,kBAAkB,CAAC,aAAa,EAAE,KAAK,CAAC,CAAA;YACtE,MAAK;QACP,KAAK,MAAM;YACT,yEAAyE;YACzE,6EAA6E;YAC7E,sEAAsE;YACtE,4EAA4E;YAC5E,OAAO,IAAI,YAAY,CAAC,IAAI,CAAC,CAAA;IACjC,CAAC;IAED,mEAAmE;IACnE,IAAI,YAAY;QAAE,OAAO,IAAI,iBAAiB,CAAC,YAAY,EAAE,KAAK,CAAC,CAAA;IACnE,IAAI,SAAS;QAAE,OAAO,IAAI,cAAc,CAAC,SAAS,EAAE,KAAK,CAAC,CAAA;IAC1D,IAAI,aAAa;QAAE,OAAO,IAAI,kBAAkB,CAAC,aAAa,EAAE,KAAK,CAAC,CAAA;IACtE,8EAA8E;IAC9E,oEAAoE;IACpE,OAAO,IAAI,YAAY,CAAC,IAAI,CAAC,CAAA;AAC/B,CAAC"}
|