pilotswarm-sdk 0.5.30 → 0.5.32
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/api/index.d.ts +40 -0
- package/api/index.js +8 -0
- package/api/src/session-authz.d.ts +76 -0
- package/api/src/session-authz.js +240 -0
- package/dist/agent-fqn.d.ts +72 -0
- package/dist/agent-fqn.d.ts.map +1 -0
- package/dist/agent-fqn.js +137 -0
- package/dist/agent-fqn.js.map +1 -0
- package/dist/agent-loader.d.ts.map +1 -1
- package/dist/agent-loader.js +22 -6
- package/dist/agent-loader.js.map +1 -1
- package/dist/agent-manager-tools.d.ts +135 -0
- package/dist/agent-manager-tools.d.ts.map +1 -0
- package/dist/agent-manager-tools.js +813 -0
- package/dist/agent-manager-tools.js.map +1 -0
- package/dist/agent-package-diff.d.ts +97 -0
- package/dist/agent-package-diff.d.ts.map +1 -0
- package/dist/agent-package-diff.js +203 -0
- package/dist/agent-package-diff.js.map +1 -0
- package/dist/agent-package-format.d.ts +2 -0
- package/dist/agent-package-format.d.ts.map +1 -1
- package/dist/agent-package-format.js +15 -0
- package/dist/agent-package-format.js.map +1 -1
- package/dist/agent-package-import-fetch.d.ts +68 -0
- package/dist/agent-package-import-fetch.d.ts.map +1 -0
- package/dist/agent-package-import-fetch.js +156 -0
- package/dist/agent-package-import-fetch.js.map +1 -0
- package/dist/agent-package-import-policy.d.ts +133 -0
- package/dist/agent-package-import-policy.d.ts.map +1 -0
- package/dist/agent-package-import-policy.js +347 -0
- package/dist/agent-package-import-policy.js.map +1 -0
- package/dist/agent-package-installer.d.ts.map +1 -1
- package/dist/agent-package-installer.js +30 -4
- package/dist/agent-package-installer.js.map +1 -1
- package/dist/cms-migrations.d.ts.map +1 -1
- package/dist/cms-migrations.js +653 -1
- package/dist/cms-migrations.js.map +1 -1
- package/dist/cms.d.ts +103 -10
- package/dist/cms.d.ts.map +1 -1
- package/dist/cms.js +85 -10
- package/dist/cms.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/inspect-tools.d.ts +68 -8
- package/dist/inspect-tools.d.ts.map +1 -1
- package/dist/inspect-tools.js +282 -21
- package/dist/inspect-tools.js.map +1 -1
- package/dist/managed-session.d.ts.map +1 -1
- package/dist/managed-session.js +87 -0
- package/dist/managed-session.js.map +1 -1
- package/dist/management-client.d.ts +20 -1
- package/dist/management-client.d.ts.map +1 -1
- package/dist/management-client.js +25 -0
- package/dist/management-client.js.map +1 -1
- package/dist/session-manager.d.ts +61 -0
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +143 -1
- package/dist/session-manager.js.map +1 -1
- package/dist/session-proxy.d.ts +9 -1
- package/dist/session-proxy.d.ts.map +1 -1
- package/dist/session-proxy.js +345 -12
- package/dist/session-proxy.js.map +1 -1
- package/dist/types.d.ts +31 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/web/web-management-client.d.ts +2 -0
- package/dist/web/web-management-client.d.ts.map +1 -1
- package/dist/web/web-management-client.js +6 -0
- package/dist/web/web-management-client.js.map +1 -1
- package/dist/worker.d.ts +5 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +28 -0
- package/dist/worker.js.map +1 -1
- package/package.json +3 -3
- package/plugins/mgmt/agents/pilotswarm.agent.md +2 -2
- package/plugins/mgmt/skills/cost-latency-analysis/SKILL.md +1 -1
- package/plugins/mgmt/skills/graph-debug/SKILL.md +4 -4
- package/plugins/mgmt/skills/orchestration-session-lifecycle/SKILL.md +1 -1
- package/plugins/mgmt/agents/agent-tuner.agent.md +0 -316
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The guarded fetch behind `import_agent_package`.
|
|
3
|
+
*
|
|
4
|
+
* Separated from the policy so the DECISIONS stay unit-testable without a
|
|
5
|
+
* network, and the I/O stays small enough to audit by eye.
|
|
6
|
+
*
|
|
7
|
+
* Three properties this file exists to guarantee, all from §15 A1:
|
|
8
|
+
*
|
|
9
|
+
* 1. **Every hop is re-checked.** An allowlisted origin that redirects to a
|
|
10
|
+
* disallowed one is the obvious bypass. `fetch()` follows redirects
|
|
11
|
+
* silently by default, so redirect following is done MANUALLY here with
|
|
12
|
+
* `redirect: "manual"` — automatic following would check only the first
|
|
13
|
+
* URL and fetch the last.
|
|
14
|
+
*
|
|
15
|
+
* 2. **DNS is not trusted.** The resolved address is checked against the
|
|
16
|
+
* private/metadata denylist on every hop, because an allowlisted NAME may
|
|
17
|
+
* still answer with `169.254.169.254`.
|
|
18
|
+
*
|
|
19
|
+
* 3. **Response bodies never reach an error, a log line or the transcript.**
|
|
20
|
+
* The agent reading this is the same agent an attacker may be talking to;
|
|
21
|
+
* echoing a body back is the exfiltration channel the allowlist exists to
|
|
22
|
+
* close.
|
|
23
|
+
*
|
|
24
|
+
* @module
|
|
25
|
+
*/
|
|
26
|
+
import { IMPORT_FETCH_LIMITS, type ImportPolicy } from "./agent-package-import-policy.js";
|
|
27
|
+
export interface ImportFetchResult {
|
|
28
|
+
bytes: Buffer;
|
|
29
|
+
/** The final URL after any permitted redirects. */
|
|
30
|
+
finalUrl: string;
|
|
31
|
+
/** Each URL visited, in order — useful in an audit record. */
|
|
32
|
+
hops: string[];
|
|
33
|
+
}
|
|
34
|
+
export declare class ImportRefusedError extends Error {
|
|
35
|
+
readonly code = "AGENT_PACKAGE_IMPORT_REFUSED";
|
|
36
|
+
/** The URL that was refused, credentials already stripped. */
|
|
37
|
+
readonly url: string | undefined;
|
|
38
|
+
constructor(message: string, url?: string);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a hostname and refuse if ANY answer is in blocked space.
|
|
42
|
+
*
|
|
43
|
+
* "Any" rather than "the one we will use" on purpose: a name that resolves to
|
|
44
|
+
* both a public and a private address is a DNS-rebinding setup, and we have no
|
|
45
|
+
* way to pin which record the socket will actually take.
|
|
46
|
+
*/
|
|
47
|
+
export declare function assertHostResolvesPublicly(hostname: string, resolver?: {
|
|
48
|
+
lookup: (h: string, opts: any) => Promise<Array<{
|
|
49
|
+
address: string;
|
|
50
|
+
}>>;
|
|
51
|
+
}): Promise<string[]>;
|
|
52
|
+
/**
|
|
53
|
+
* Fetch a package from an allowlisted origin, or refuse.
|
|
54
|
+
*
|
|
55
|
+
* `fetchImpl` and `resolver` are injectable so the guard logic can be tested
|
|
56
|
+
* against redirect chains and hostile DNS without a network.
|
|
57
|
+
*/
|
|
58
|
+
export declare function guardedImportFetch(rawUrl: string, policy: ImportPolicy, opts?: {
|
|
59
|
+
fetchImpl?: typeof fetch;
|
|
60
|
+
resolver?: {
|
|
61
|
+
lookup: (h: string, o: any) => Promise<Array<{
|
|
62
|
+
address: string;
|
|
63
|
+
}>>;
|
|
64
|
+
};
|
|
65
|
+
limits?: typeof IMPORT_FETCH_LIMITS;
|
|
66
|
+
signal?: AbortSignal;
|
|
67
|
+
}): Promise<ImportFetchResult>;
|
|
68
|
+
//# sourceMappingURL=agent-package-import-fetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-package-import-fetch.d.ts","sourceRoot":"","sources":["../src/agent-package-import-fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,OAAO,EACH,mBAAmB,EAGnB,KAAK,YAAY,EACpB,MAAM,kCAAkC,CAAC;AAE1C,MAAM,WAAW,iBAAiB;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,8DAA8D;IAC9D,IAAI,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,qBAAa,kBAAmB,SAAQ,KAAK;IACzC,QAAQ,CAAC,IAAI,kCAAkC;IAC/C,8DAA8D;IAC9D,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;gBACrB,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM;CAK5C;AAED;;;;;;GAMG;AACH,wBAAsB,0BAA0B,CAC5C,QAAQ,EAAE,MAAM,EAChB,QAAQ,GAAE;IAAE,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAA;CAAe,GACjG,OAAO,CAAC,MAAM,EAAE,CAAC,CAqBnB;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACpC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,YAAY,EACpB,IAAI,GAAE;IACF,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,QAAQ,CAAC,EAAE;QAAE,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,GAAG,KAAK,OAAO,CAAC,KAAK,CAAC;YAAE,OAAO,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC,CAAA;KAAE,CAAC;IAClF,MAAM,CAAC,EAAE,OAAO,mBAAmB,CAAC;IACpC,MAAM,CAAC,EAAE,WAAW,CAAC;CACnB,GACP,OAAO,CAAC,iBAAiB,CAAC,CAiE5B"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The guarded fetch behind `import_agent_package`.
|
|
3
|
+
*
|
|
4
|
+
* Separated from the policy so the DECISIONS stay unit-testable without a
|
|
5
|
+
* network, and the I/O stays small enough to audit by eye.
|
|
6
|
+
*
|
|
7
|
+
* Three properties this file exists to guarantee, all from §15 A1:
|
|
8
|
+
*
|
|
9
|
+
* 1. **Every hop is re-checked.** An allowlisted origin that redirects to a
|
|
10
|
+
* disallowed one is the obvious bypass. `fetch()` follows redirects
|
|
11
|
+
* silently by default, so redirect following is done MANUALLY here with
|
|
12
|
+
* `redirect: "manual"` — automatic following would check only the first
|
|
13
|
+
* URL and fetch the last.
|
|
14
|
+
*
|
|
15
|
+
* 2. **DNS is not trusted.** The resolved address is checked against the
|
|
16
|
+
* private/metadata denylist on every hop, because an allowlisted NAME may
|
|
17
|
+
* still answer with `169.254.169.254`.
|
|
18
|
+
*
|
|
19
|
+
* 3. **Response bodies never reach an error, a log line or the transcript.**
|
|
20
|
+
* The agent reading this is the same agent an attacker may be talking to;
|
|
21
|
+
* echoing a body back is the exfiltration channel the allowlist exists to
|
|
22
|
+
* close.
|
|
23
|
+
*
|
|
24
|
+
* @module
|
|
25
|
+
*/
|
|
26
|
+
import * as dns from "node:dns/promises";
|
|
27
|
+
import { IMPORT_FETCH_LIMITS, checkImportUrl, isBlockedAddress, } from "./agent-package-import-policy.js";
|
|
28
|
+
export class ImportRefusedError extends Error {
|
|
29
|
+
code = "AGENT_PACKAGE_IMPORT_REFUSED";
|
|
30
|
+
/** The URL that was refused, credentials already stripped. */
|
|
31
|
+
url;
|
|
32
|
+
constructor(message, url) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = "ImportRefusedError";
|
|
35
|
+
this.url = url;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Resolve a hostname and refuse if ANY answer is in blocked space.
|
|
40
|
+
*
|
|
41
|
+
* "Any" rather than "the one we will use" on purpose: a name that resolves to
|
|
42
|
+
* both a public and a private address is a DNS-rebinding setup, and we have no
|
|
43
|
+
* way to pin which record the socket will actually take.
|
|
44
|
+
*/
|
|
45
|
+
export async function assertHostResolvesPublicly(hostname, resolver = dns) {
|
|
46
|
+
let answers;
|
|
47
|
+
try {
|
|
48
|
+
answers = await resolver.lookup(hostname, { all: true, verbatim: true });
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
// A name that will not resolve cannot be fetched anyway; refusing here
|
|
52
|
+
// keeps the failure legible instead of surfacing a socket error later.
|
|
53
|
+
throw new ImportRefusedError(`could not resolve host "${hostname}": ${error?.code ?? "lookup failed"}`);
|
|
54
|
+
}
|
|
55
|
+
if (!answers?.length) {
|
|
56
|
+
throw new ImportRefusedError(`host "${hostname}" resolved to no addresses`);
|
|
57
|
+
}
|
|
58
|
+
const addresses = answers.map((a) => a.address);
|
|
59
|
+
for (const address of addresses) {
|
|
60
|
+
if (isBlockedAddress(address)) {
|
|
61
|
+
throw new ImportRefusedError(`host "${hostname}" resolves to a non-public address (${address}); refusing to connect`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return addresses;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Fetch a package from an allowlisted origin, or refuse.
|
|
68
|
+
*
|
|
69
|
+
* `fetchImpl` and `resolver` are injectable so the guard logic can be tested
|
|
70
|
+
* against redirect chains and hostile DNS without a network.
|
|
71
|
+
*/
|
|
72
|
+
export async function guardedImportFetch(rawUrl, policy, opts = {}) {
|
|
73
|
+
const limits = opts.limits ?? IMPORT_FETCH_LIMITS;
|
|
74
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
75
|
+
const hops = [];
|
|
76
|
+
let current = rawUrl;
|
|
77
|
+
for (let hop = 0; hop <= limits.maxRedirects; hop += 1) {
|
|
78
|
+
// The SAME check for the initial URL and every redirect. Sharing one
|
|
79
|
+
// function is the point: a redirect path with its own weaker check is
|
|
80
|
+
// how this gets broken.
|
|
81
|
+
const decision = checkImportUrl(current, policy);
|
|
82
|
+
if (!decision.allowed) {
|
|
83
|
+
throw new ImportRefusedError(hop === 0
|
|
84
|
+
? `import refused: ${decision.reason}`
|
|
85
|
+
: `import refused after ${hop} redirect(s): ${decision.reason}`, decision.url);
|
|
86
|
+
}
|
|
87
|
+
const url = new URL(decision.url);
|
|
88
|
+
await assertHostResolvesPublicly(url.hostname, opts.resolver);
|
|
89
|
+
hops.push(url.href);
|
|
90
|
+
const timeout = AbortSignal.timeout(limits.timeoutMs);
|
|
91
|
+
const signal = opts.signal
|
|
92
|
+
? AbortSignal.any?.([timeout, opts.signal]) ?? timeout
|
|
93
|
+
: timeout;
|
|
94
|
+
const response = await doFetch(url.href, {
|
|
95
|
+
// MANUAL: automatic following would validate only the first URL
|
|
96
|
+
// and fetch whatever the last one turned out to be.
|
|
97
|
+
redirect: "manual",
|
|
98
|
+
signal,
|
|
99
|
+
headers: { accept: "application/octet-stream, application/gzip, */*" },
|
|
100
|
+
});
|
|
101
|
+
if (response.status >= 300 && response.status < 400) {
|
|
102
|
+
const location = response.headers.get("location");
|
|
103
|
+
if (!location) {
|
|
104
|
+
throw new ImportRefusedError(`redirect with no Location header (HTTP ${response.status})`, url.href);
|
|
105
|
+
}
|
|
106
|
+
// Relative Locations are legal; resolve against the current URL so
|
|
107
|
+
// the next iteration re-checks a fully-qualified target.
|
|
108
|
+
current = new URL(location, url.href).href;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
// Status only. The body may be attacker-authored, and this string
|
|
113
|
+
// reaches an agent transcript.
|
|
114
|
+
throw new ImportRefusedError(`fetch failed with HTTP ${response.status}`, url.href);
|
|
115
|
+
}
|
|
116
|
+
const declared = Number(response.headers.get("content-length") ?? "");
|
|
117
|
+
if (Number.isFinite(declared) && declared > limits.maxBytes) {
|
|
118
|
+
throw new ImportRefusedError(`package is ${declared} bytes, over the ${limits.maxBytes}-byte import cap`, url.href);
|
|
119
|
+
}
|
|
120
|
+
const bytes = await readCapped(response, limits.maxBytes, url.href);
|
|
121
|
+
return { bytes, finalUrl: url.href, hops };
|
|
122
|
+
}
|
|
123
|
+
throw new ImportRefusedError(`too many redirects (limit ${limits.maxRedirects})`, hops[hops.length - 1]);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Read a body, stopping the moment it exceeds the cap.
|
|
127
|
+
*
|
|
128
|
+
* Streamed rather than buffered-then-measured: a declared Content-Length is
|
|
129
|
+
* the server's claim, and an unbounded `arrayBuffer()` on a lying server is a
|
|
130
|
+
* worker OOM.
|
|
131
|
+
*/
|
|
132
|
+
async function readCapped(response, maxBytes, url) {
|
|
133
|
+
const body = response.body;
|
|
134
|
+
if (!body)
|
|
135
|
+
return Buffer.alloc(0);
|
|
136
|
+
const chunks = [];
|
|
137
|
+
let total = 0;
|
|
138
|
+
const reader = body.getReader();
|
|
139
|
+
for (;;) {
|
|
140
|
+
const { done, value } = await reader.read();
|
|
141
|
+
if (done)
|
|
142
|
+
break;
|
|
143
|
+
const chunk = Buffer.from(value);
|
|
144
|
+
total += chunk.length;
|
|
145
|
+
if (total > maxBytes) {
|
|
146
|
+
try {
|
|
147
|
+
await reader.cancel();
|
|
148
|
+
}
|
|
149
|
+
catch { /* already closing */ }
|
|
150
|
+
throw new ImportRefusedError(`package exceeds the ${maxBytes}-byte import cap`, url);
|
|
151
|
+
}
|
|
152
|
+
chunks.push(chunk);
|
|
153
|
+
}
|
|
154
|
+
return Buffer.concat(chunks);
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=agent-package-import-fetch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-package-import-fetch.js","sourceRoot":"","sources":["../src/agent-package-import-fetch.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import policy — the allowlist and network guards for `import_agent_package`.
|
|
3
|
+
*
|
|
4
|
+
* ── Why this file is defensive out of proportion to its size ────────────
|
|
5
|
+
*
|
|
6
|
+
* `import_agent_package` is the worker's FIRST outbound-fetch primitive
|
|
7
|
+
* (§15 A1). Before it, there was no `fetch(` anywhere in the worker core. It
|
|
8
|
+
* is reachable with a model-chosen URL, inside an agent whose entire job is
|
|
9
|
+
* reading untrusted material — session transcripts, archives, other people's
|
|
10
|
+
* packages. That is the lethal trifecta in one tool: private data, untrusted
|
|
11
|
+
* content, and an outbound channel.
|
|
12
|
+
*
|
|
13
|
+
* The primary control is an ALLOWLIST rather than a blocklist, because there
|
|
14
|
+
* is then no hostile URL to filter — anything off the list is simply
|
|
15
|
+
* unreachable. A prompt injection saying "also import from
|
|
16
|
+
* http://169.254.169.254/metadata/identity/oauth2/token" cannot be honoured,
|
|
17
|
+
* not because we recognized it, but because that origin was never permitted.
|
|
18
|
+
*
|
|
19
|
+
* Layered underneath, because an allowlist alone is not enough: DNS is not
|
|
20
|
+
* ours to trust, so an allowlisted NAME that resolves into private address
|
|
21
|
+
* space must still be refused — and re-checked on every redirect hop.
|
|
22
|
+
*
|
|
23
|
+
* ── Everything here is a pure decision ──────────────────────────────────
|
|
24
|
+
*
|
|
25
|
+
* No sockets are opened in this module. It decides; the caller connects. That
|
|
26
|
+
* keeps the whole bypass surface unit-testable without a network.
|
|
27
|
+
*
|
|
28
|
+
* @module
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* The base allowlist is a CONSTANT IN SOURCE, not a config default.
|
|
32
|
+
*
|
|
33
|
+
* Three entries because GitHub serves one repository from three hosts. Keeping
|
|
34
|
+
* them here rather than in the shipped config file means changing them shows
|
|
35
|
+
* up in a PR diff, and a deployment cannot silently drop them by overwriting a
|
|
36
|
+
* file it mounts.
|
|
37
|
+
*/
|
|
38
|
+
export declare const BASE_IMPORT_ALLOWLIST: readonly string[];
|
|
39
|
+
export declare const IMPORT_CONFIG_FILENAME = ".agent_packages.json";
|
|
40
|
+
export interface ImportAllowlistEntry {
|
|
41
|
+
protocol: "https:";
|
|
42
|
+
/** Lower-cased, trailing dot stripped. */
|
|
43
|
+
host: string;
|
|
44
|
+
/** Explicit port, or null meaning "default only". */
|
|
45
|
+
port: string | null;
|
|
46
|
+
/** Normalized path prefix, always starting with `/`. */
|
|
47
|
+
pathPrefix: string;
|
|
48
|
+
/** The entry as written, for error messages. */
|
|
49
|
+
raw: string;
|
|
50
|
+
}
|
|
51
|
+
export interface ImportPolicy {
|
|
52
|
+
entries: ImportAllowlistEntry[];
|
|
53
|
+
mode: "append" | "replace";
|
|
54
|
+
/** Where the deployment config came from, for diagnostics. */
|
|
55
|
+
configPath: string | null;
|
|
56
|
+
}
|
|
57
|
+
export interface UrlDecision {
|
|
58
|
+
allowed: boolean;
|
|
59
|
+
/** Populated only when refused. Never contains a response body. */
|
|
60
|
+
reason?: string;
|
|
61
|
+
/** The normalized URL that was checked. */
|
|
62
|
+
url?: string;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Normalize a path for prefix comparison.
|
|
66
|
+
*
|
|
67
|
+
* Percent-decoding happens BEFORE `..` resolution, because otherwise
|
|
68
|
+
* `%2e%2e%2f` survives as literal text and slips past a traversal check that
|
|
69
|
+
* only looked for `../`.
|
|
70
|
+
*/
|
|
71
|
+
export declare function normalizePathForMatch(rawPath: string): string;
|
|
72
|
+
/** Strip a single trailing dot and lower-case. `GitHub.COM.` → `github.com`. */
|
|
73
|
+
export declare function normalizeHost(host: string): string;
|
|
74
|
+
/**
|
|
75
|
+
* Parse one allowlist entry. Returns null (with no exception) for anything
|
|
76
|
+
* unusable — a malformed entry must never widen the list.
|
|
77
|
+
*/
|
|
78
|
+
export declare function parseAllowlistEntry(raw: string): ImportAllowlistEntry | null;
|
|
79
|
+
/**
|
|
80
|
+
* Load `.agent_packages.json` and compose it with the base list.
|
|
81
|
+
*
|
|
82
|
+
* A MALFORMED FILE IS FATAL rather than ignored. A security control that
|
|
83
|
+
* silently falls back to a default when its config is broken is how
|
|
84
|
+
* deployments end up unknowingly running the permissive version — the operator
|
|
85
|
+
* edited the file, fat-fingered a comma, and never learned the edit did
|
|
86
|
+
* nothing.
|
|
87
|
+
*/
|
|
88
|
+
export declare function loadImportPolicy(opts?: {
|
|
89
|
+
configDir?: string;
|
|
90
|
+
configPath?: string;
|
|
91
|
+
}): ImportPolicy;
|
|
92
|
+
/**
|
|
93
|
+
* Is this resolved address inside a range we must never fetch from?
|
|
94
|
+
*
|
|
95
|
+
* Applied to the RESOLVED ADDRESS, not the hostname, and on every redirect
|
|
96
|
+
* hop — an allowlisted name whose DNS answers `169.254.169.254` is exactly
|
|
97
|
+
* the attack this stops.
|
|
98
|
+
*/
|
|
99
|
+
export declare function isBlockedAddress(address: string): boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Reject host forms that are not plain names — decimal, octal and hex IPv4
|
|
102
|
+
* literals resolve to addresses without ever looking like one.
|
|
103
|
+
* `http://2130706433/` is `127.0.0.1`.
|
|
104
|
+
*/
|
|
105
|
+
export declare function isSuspiciousHostLiteral(host: string): boolean;
|
|
106
|
+
/**
|
|
107
|
+
* May we fetch this URL?
|
|
108
|
+
*
|
|
109
|
+
* Call for the initial URL AND for every redirect hop — an allowed origin
|
|
110
|
+
* redirecting to a disallowed one is the obvious bypass, and the only thing
|
|
111
|
+
* that stops it is re-asking the same question about the new location.
|
|
112
|
+
*/
|
|
113
|
+
export declare function checkImportUrl(rawUrl: string, policy: ImportPolicy): UrlDecision;
|
|
114
|
+
/**
|
|
115
|
+
* Prefix match on SEGMENT BOUNDARIES.
|
|
116
|
+
*
|
|
117
|
+
* `/affandar/pilotswarm-evil` must not match the prefix
|
|
118
|
+
* `/affandar/pilotswarm/`. A naive `startsWith` accepts it, which is how
|
|
119
|
+
* allowlists usually get broken.
|
|
120
|
+
*/
|
|
121
|
+
export declare function pathPrefixMatches(requestPath: string, prefix: string): boolean;
|
|
122
|
+
/**
|
|
123
|
+
* Caps applied to every import fetch. Values are conservative on purpose:
|
|
124
|
+
* a package is a small tarball, and anything wanting more headroom than this
|
|
125
|
+
* is not the thing this tool exists to fetch.
|
|
126
|
+
*/
|
|
127
|
+
export declare const IMPORT_FETCH_LIMITS: Readonly<{
|
|
128
|
+
/** Matches the documented upload cap so both paths refuse at the same size. */
|
|
129
|
+
maxBytes: number;
|
|
130
|
+
maxRedirects: 3;
|
|
131
|
+
timeoutMs: 20000;
|
|
132
|
+
}>;
|
|
133
|
+
//# sourceMappingURL=agent-package-import-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-package-import-policy.d.ts","sourceRoot":"","sources":["../src/agent-package-import-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAKH;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,EAAE,SAAS,MAAM,EAIjD,CAAC;AAEH,eAAO,MAAM,sBAAsB,yBAAyB,CAAC;AAE7D,MAAM,WAAW,oBAAoB;IACjC,QAAQ,EAAE,QAAQ,CAAC;IACnB,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,wDAAwD;IACxD,UAAU,EAAE,MAAM,CAAC;IACnB,gDAAgD;IAChD,GAAG,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IACzB,OAAO,EAAE,oBAAoB,EAAE,CAAC;IAChC,IAAI,EAAE,QAAQ,GAAG,SAAS,CAAC;IAC3B,8DAA8D;IAC9D,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,WAAW;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2CAA2C;IAC3C,GAAG,CAAC,EAAE,MAAM,CAAC;CAChB;AAID;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA2B7D;AAED,gFAAgF;AAChF,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,oBAAoB,GAAG,IAAI,CAmB5E;AAID;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,YAAY,CAmDrG;AAID;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAgCzD;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAgB7D;AAID;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,WAAW,CA+ChF;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAK9E;AAgBD;;;;GAIG;AACH,eAAO,MAAM,mBAAmB;IAC5B,+EAA+E;;;;EAIjF,CAAC"}
|