@aexhq/sdk 0.13.7 → 0.13.8
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/README.md +14 -14
- package/dist/_contracts/connection-ticket.d.ts +8 -7
- package/dist/_contracts/connection-ticket.js +20 -14
- package/dist/_contracts/event-envelope.d.ts +17 -18
- package/dist/_contracts/event-envelope.js +10 -11
- package/dist/_contracts/managed-key.d.ts +27 -1
- package/dist/_contracts/managed-key.js +75 -4
- package/dist/_contracts/operations.d.ts +9 -20
- package/dist/_contracts/operations.js +33 -82
- package/dist/_contracts/proxy-protocol.d.ts +35 -2
- package/dist/_contracts/proxy-protocol.js +34 -1
- package/dist/_contracts/run-artifacts.d.ts +12 -10
- package/dist/_contracts/run-artifacts.js +13 -11
- package/dist/_contracts/run-config.d.ts +7 -0
- package/dist/_contracts/run-config.js +93 -24
- package/dist/_contracts/run-custody.d.ts +3 -3
- package/dist/_contracts/run-custody.js +5 -5
- package/dist/_contracts/run-record.d.ts +5 -17
- package/dist/_contracts/run-record.js +4 -15
- package/dist/_contracts/run-retention.d.ts +2 -2
- package/dist/_contracts/run-retention.js +3 -3
- package/dist/_contracts/run-unit.d.ts +4 -5
- package/dist/_contracts/runner-event.d.ts +7 -8
- package/dist/_contracts/runner-event.js +7 -8
- package/dist/_contracts/side-effect-audit.d.ts +2 -2
- package/dist/_contracts/side-effect-audit.js +3 -3
- package/dist/_contracts/stable.d.ts +1 -1
- package/dist/_contracts/stable.js +1 -1
- package/dist/_contracts/submission.d.ts +5 -6
- package/dist/_contracts/submission.js +1 -1
- package/dist/cli.mjs +127 -127
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/client.d.ts +7 -57
- package/dist/client.js +302 -167
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/cleanup.md +4 -4
- package/docs/credentials.md +5 -5
- package/docs/events.md +5 -5
- package/docs/outputs.md +23 -25
- package/docs/product-boundaries.md +5 -5
- package/docs/provider-runtime-capabilities.md +1 -1
- package/docs/quickstart.md +12 -12
- package/docs/run-config.md +1 -1
- package/docs/run-record.md +6 -9
- package/docs/skills.md +23 -25
- package/package.json +2 -2
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { strToU8, zipSync } from "fflate";
|
|
2
2
|
import { RunStateError } from "./sdk-errors.js";
|
|
3
3
|
import { assertRunRecordArchivePublicSafeV1, buildRunRecordDownloadManifestV1 } from "./run-record.js";
|
|
4
|
-
import { runArtifactRel } from "./run-artifacts.js";
|
|
5
4
|
/**
|
|
6
5
|
* The single source of truth for SDK<->BFF transport. The SDK class
|
|
7
6
|
* AND the CLI subcommands both call these functions; neither
|
|
@@ -32,12 +31,29 @@ export async function getRun(http, runId) {
|
|
|
32
31
|
export async function getRunUnit(http, runId) {
|
|
33
32
|
return http.request(`/api/runs/${encodeURIComponent(runId)}`);
|
|
34
33
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
// Bound the transparent pager: the read route caps each page at 1000, so this
|
|
35
|
+
// admits up to ~1e6 events before bailing — past any real run, but bounded so a
|
|
36
|
+
// server that never clears `nextCursor` can't loop forever.
|
|
37
|
+
const LIST_EVENTS_PAGE_BUDGET = 1000;
|
|
38
|
+
/**
|
|
39
|
+
* List a run's events. The read endpoint is PAGED (bounded per response so a
|
|
40
|
+
* long run can't return an unbounded body); this follows `nextCursor` across
|
|
41
|
+
* pages and returns the FULL accumulated list, preserving the prior single-call
|
|
42
|
+
* contract for callers (download/*, CLI, streamEvents polling).
|
|
43
|
+
*/
|
|
44
|
+
export async function listRunEvents(http, runId) {
|
|
45
|
+
const path = `/api/runs/${encodeURIComponent(runId)}/events`;
|
|
46
|
+
const all = [];
|
|
47
|
+
let cursor;
|
|
48
|
+
for (let page = 0; page < LIST_EVENTS_PAGE_BUDGET; page++) {
|
|
49
|
+
const query = cursor !== undefined ? { cursor: String(cursor) } : {};
|
|
50
|
+
const result = await http.request(path, {}, query);
|
|
51
|
+
all.push(...result.events);
|
|
52
|
+
if (typeof result.nextCursor !== "number")
|
|
53
|
+
break;
|
|
54
|
+
cursor = result.nextCursor;
|
|
55
|
+
}
|
|
56
|
+
return all;
|
|
41
57
|
}
|
|
42
58
|
/**
|
|
43
59
|
* Mint a short-lived coordinator WS ticket via the workspace-token-gated
|
|
@@ -52,14 +68,6 @@ export async function listOutputs(http, runId) {
|
|
|
52
68
|
const result = await http.request(`/api/runs/${encodeURIComponent(runId)}/outputs`);
|
|
53
69
|
return result.outputs;
|
|
54
70
|
}
|
|
55
|
-
/**
|
|
56
|
-
* List the run's platform diagnostics (the `logs` namespace). Legacy stored
|
|
57
|
-
* filenames are normalized to canonical public namespaces.
|
|
58
|
-
*/
|
|
59
|
-
export async function listLogs(http, runId) {
|
|
60
|
-
const result = await http.request(`/api/runs/${encodeURIComponent(runId)}/logs`);
|
|
61
|
-
return result.logs.map((log) => typeof log.filename === "string" ? { ...log, filename: runArtifactRel(log.filename) } : log);
|
|
62
|
-
}
|
|
63
71
|
export async function createOutputLink(http, runId, outputId) {
|
|
64
72
|
return http.request(`/api/runs/${encodeURIComponent(runId)}/outputs/${encodeURIComponent(outputId)}/link`, { method: "POST" });
|
|
65
73
|
}
|
|
@@ -123,7 +131,7 @@ export async function whoami(http) {
|
|
|
123
131
|
}
|
|
124
132
|
/**
|
|
125
133
|
* Download each artifact's bytes into a zip-file map keyed by
|
|
126
|
-
* `<zipPrefix><relative-path>`, fetched from the `outputs`
|
|
134
|
+
* `<zipPrefix><relative-path>`, fetched from the `outputs`
|
|
127
135
|
* download route. Best-effort: a per-artifact fetch failure records an
|
|
128
136
|
* `errors[]` entry rather than aborting the rest, so the failure is
|
|
129
137
|
* surfaced (never silent) while a partially-available run still yields a
|
|
@@ -141,7 +149,7 @@ async function collectArtifactBytes(http, runId, items, zipPrefix, namespace) {
|
|
|
141
149
|
path: `${zipPrefix}${rel}`,
|
|
142
150
|
bytes: new Uint8Array(await response.arrayBuffer()),
|
|
143
151
|
...(item.contentType !== undefined ? { contentType: item.contentType } : {}),
|
|
144
|
-
|
|
152
|
+
customerContent: true
|
|
145
153
|
});
|
|
146
154
|
captured.push({
|
|
147
155
|
id: item.id,
|
|
@@ -159,28 +167,6 @@ async function collectArtifactBytes(http, runId, items, zipPrefix, namespace) {
|
|
|
159
167
|
function eventsJsonl(events) {
|
|
160
168
|
return strToU8(events.map((event) => JSON.stringify(event)).join("\n"));
|
|
161
169
|
}
|
|
162
|
-
async function tryListOptionalRunEvents(http, runId, channel) {
|
|
163
|
-
try {
|
|
164
|
-
const events = await listRunEvents(http, runId, { channel });
|
|
165
|
-
if (channel === "log") {
|
|
166
|
-
return events.length > 0 && events.every(isLogChannelEvent)
|
|
167
|
-
? { status: "present", events }
|
|
168
|
-
: { status: "unavailable", events: [] };
|
|
169
|
-
}
|
|
170
|
-
return hasUnifiedStreamEvidence(events)
|
|
171
|
-
? { status: "present", events }
|
|
172
|
-
: { status: "unavailable", events: [] };
|
|
173
|
-
}
|
|
174
|
-
catch {
|
|
175
|
-
return { status: "unavailable", events: [] };
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
function isLogChannelEvent(event) {
|
|
179
|
-
return event.channel === "log";
|
|
180
|
-
}
|
|
181
|
-
function hasUnifiedStreamEvidence(events) {
|
|
182
|
-
return events.some((event) => event.channel === "log" || event.channel === "event");
|
|
183
|
-
}
|
|
184
170
|
function isPathSelector(selector) {
|
|
185
171
|
return Boolean(selector && typeof selector === "object" && "path" in selector);
|
|
186
172
|
}
|
|
@@ -188,50 +174,37 @@ function normalizeOutputLookupPath(path) {
|
|
|
188
174
|
return path.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
189
175
|
}
|
|
190
176
|
/**
|
|
191
|
-
* Download EVERYTHING about a run as one zip, organised into the
|
|
177
|
+
* Download EVERYTHING public about a run as one zip, organised into the three
|
|
192
178
|
* namespace folders:
|
|
193
179
|
*
|
|
194
180
|
* metadata/run.json — the run record.
|
|
195
181
|
* events/events.jsonl — typed event-channel records.
|
|
196
|
-
* events/logs.jsonl — log-channel records, when the API serves them.
|
|
197
|
-
* events/all.jsonl — full unified stream, when the API serves it.
|
|
198
182
|
* outputs/<rel> — the run's deliverables.
|
|
199
|
-
* logs/<rel> — platform diagnostics.
|
|
200
183
|
* manifest.json — `RunRecordManifestV1`.
|
|
201
184
|
*/
|
|
202
185
|
export async function download(http, runId) {
|
|
203
|
-
const [run, events,
|
|
186
|
+
const [run, events, outputs] = await Promise.all([
|
|
204
187
|
getRun(http, runId),
|
|
205
188
|
listRunEvents(http, runId),
|
|
206
|
-
|
|
207
|
-
tryListOptionalRunEvents(http, runId, "all"),
|
|
208
|
-
listOutputs(http, runId),
|
|
209
|
-
listLogs(http, runId)
|
|
189
|
+
listOutputs(http, runId)
|
|
210
190
|
]);
|
|
211
191
|
const out = await collectArtifactBytes(http, runId, outputs, "outputs/", "outputs");
|
|
212
|
-
const logs = await collectArtifactBytes(http, runId, logItems, "logs/", "logs");
|
|
213
192
|
const submissionSnapshot = extractSubmissionSnapshot(run);
|
|
214
193
|
const costTelemetry = extractCostTelemetry(run);
|
|
215
194
|
const manifest = buildRunRecordDownloadManifestV1({
|
|
216
195
|
runId,
|
|
217
196
|
outputs: out.captured,
|
|
218
|
-
|
|
219
|
-
errors: [...out.errors, ...logs.errors],
|
|
197
|
+
errors: out.errors,
|
|
220
198
|
typedEventCount: events.length,
|
|
221
199
|
...(submissionSnapshot ? { submission: { status: "present" } } : {}),
|
|
222
|
-
...(costTelemetry ? { cost: { status: "present" } } : {})
|
|
223
|
-
logEvents: { status: logEvents.status, recordCount: logEvents.events.length },
|
|
224
|
-
allEvents: { status: allEvents.status, recordCount: allEvents.events.length }
|
|
200
|
+
...(costTelemetry ? { cost: { status: "present" } } : {})
|
|
225
201
|
});
|
|
226
202
|
return zipEntries([
|
|
227
203
|
jsonEntry("metadata/run.json", run),
|
|
228
204
|
...(submissionSnapshot ? [jsonEntry("metadata/submission.json", submissionSnapshot)] : []),
|
|
229
205
|
...(costTelemetry ? [jsonEntry("metadata/cost.json", costTelemetry)] : []),
|
|
230
206
|
jsonlEntry("events/events.jsonl", events),
|
|
231
|
-
...(logEvents.status === "present" ? [jsonlEntry("events/logs.jsonl", logEvents.events)] : []),
|
|
232
|
-
...(allEvents.status === "present" ? [jsonlEntry("events/all.jsonl", allEvents.events)] : []),
|
|
233
207
|
...out.entries,
|
|
234
|
-
...logs.entries,
|
|
235
208
|
jsonEntry("manifest.json", manifest)
|
|
236
209
|
]);
|
|
237
210
|
}
|
|
@@ -248,35 +221,13 @@ export async function downloadOutputs(http, runId) {
|
|
|
248
221
|
jsonEntry("manifest.json", { runId, namespace: "outputs", outputs: captured, errors })
|
|
249
222
|
]);
|
|
250
223
|
}
|
|
251
|
-
/**
|
|
252
|
-
* Download only the platform diagnostics (the `logs` namespace). Zip
|
|
253
|
-
* layout: `<rel>` per file plus a `manifest.json`
|
|
254
|
-
* (`{ runId, namespace: "logs", logs[], errors[] }`).
|
|
255
|
-
*/
|
|
256
|
-
export async function downloadLogs(http, runId) {
|
|
257
|
-
const logItems = await listLogs(http, runId);
|
|
258
|
-
const { entries, captured, errors } = await collectArtifactBytes(http, runId, logItems, "", "logs");
|
|
259
|
-
return zipEntries([
|
|
260
|
-
...entries,
|
|
261
|
-
jsonEntry("manifest.json", { runId, namespace: "logs", logs: captured, errors })
|
|
262
|
-
]);
|
|
263
|
-
}
|
|
264
224
|
/**
|
|
265
225
|
* Download only the event archive (the `events` namespace). Always includes
|
|
266
|
-
* typed `events.jsonl
|
|
267
|
-
* event API proves those channel exports are available.
|
|
226
|
+
* typed `events.jsonl`.
|
|
268
227
|
*/
|
|
269
228
|
export async function downloadEvents(http, runId) {
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
tryListOptionalRunEvents(http, runId, "log"),
|
|
273
|
-
tryListOptionalRunEvents(http, runId, "all")
|
|
274
|
-
]);
|
|
275
|
-
return zipEntries([
|
|
276
|
-
jsonlEntry("events.jsonl", events),
|
|
277
|
-
...(logEvents.status === "present" ? [jsonlEntry("logs.jsonl", logEvents.events)] : []),
|
|
278
|
-
...(allEvents.status === "present" ? [jsonlEntry("all.jsonl", allEvents.events)] : [])
|
|
279
|
-
]);
|
|
229
|
+
const events = await listRunEvents(http, runId);
|
|
230
|
+
return zipEntries([jsonlEntry("events.jsonl", events)]);
|
|
280
231
|
}
|
|
281
232
|
/**
|
|
282
233
|
* Download only the run record (the `metadata` namespace) as a zip
|
|
@@ -9,7 +9,40 @@
|
|
|
9
9
|
* the new version.
|
|
10
10
|
*/
|
|
11
11
|
export declare const PROXY_PROTOCOL_VERSION: "1";
|
|
12
|
+
/**
|
|
13
|
+
* Streaming named-proxy protocol. A client that sends `"2"` in
|
|
14
|
+
* {@link PROXY_PROTOCOL_HEADER} opts into the streamed response path: the
|
|
15
|
+
* Worker pipes the upstream body back unbuffered (no base64, no
|
|
16
|
+
* `arrayBuffer()` in the ~128MB isolate) and carries the envelope metadata
|
|
17
|
+
* in the `x-aex-proxy-*` response headers below instead of a JSON envelope.
|
|
18
|
+
*
|
|
19
|
+
* Additive: `"1"` stays valid and keeps the buffered
|
|
20
|
+
* {@link ProxyResponseEnvelope}. Old runners keep working; new runners
|
|
21
|
+
* stream. The version is on the request header so the Worker can serve
|
|
22
|
+
* both shapes without a coordinated CLI/BFF release.
|
|
23
|
+
*/
|
|
24
|
+
export declare const PROXY_PROTOCOL_VERSION_V2: "2";
|
|
12
25
|
export declare const PROXY_PROTOCOL_HEADER = "x-aex-proxy-protocol";
|
|
26
|
+
/**
|
|
27
|
+
* Response headers for the streamed (v2) named-proxy path. The Worker sets
|
|
28
|
+
* these BEFORE it starts streaming the upstream body, so the client can
|
|
29
|
+
* reconstruct the same fields the v1 {@link ProxyResponseEnvelope} carried
|
|
30
|
+
* without buffering. All values are plain strings; numeric fields are
|
|
31
|
+
* decimal, booleans are `"true"`/`"false"`.
|
|
32
|
+
*/
|
|
33
|
+
export declare const PROXY_RESP_STATUS_HEADER = "x-aex-proxy-status";
|
|
34
|
+
export declare const PROXY_RESP_MODE_HEADER = "x-aex-proxy-effective-mode";
|
|
35
|
+
/**
|
|
36
|
+
* `"true"` when the cap forced truncation, `"false"` when the full body
|
|
37
|
+
* fit, `"unknown"` when the upstream omitted `content-length` and the body
|
|
38
|
+
* happened to reach the cap (can't distinguish exact-fit from over-cap
|
|
39
|
+
* without buffering). See the streaming byte-cap note in proxy-routes.ts.
|
|
40
|
+
*/
|
|
41
|
+
export declare const PROXY_RESP_TRUNCATED_HEADER = "x-aex-proxy-truncated";
|
|
42
|
+
export declare const PROXY_RESP_REMAINING_CALLS_HEADER = "x-aex-proxy-remaining-calls";
|
|
43
|
+
export declare const PROXY_RESP_REMAINING_BYTES_HEADER = "x-aex-proxy-remaining-bytes";
|
|
44
|
+
/** JSON object of lowercase upstream header names → values (mode-filtered). */
|
|
45
|
+
export declare const PROXY_RESP_UPSTREAM_HEADERS_HEADER = "x-aex-proxy-upstream-headers";
|
|
13
46
|
/**
|
|
14
47
|
* Default `User-Agent` the proxy attaches to every outbound request when
|
|
15
48
|
* the caller did not supply one via `allowHeaders`. Some upstreams reject
|
|
@@ -115,8 +148,8 @@ export interface ProxyEndpointPolicy {
|
|
|
115
148
|
export interface BuildProxyIndexFileInput {
|
|
116
149
|
readonly runId: string;
|
|
117
150
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
151
|
+
* Worker host that serves `/api/runs/:runId/proxy/:endpointName`.
|
|
152
|
+
* When unset
|
|
120
153
|
* (or empty) the run has no reachable proxy plane and `proxyBaseUrl`
|
|
121
154
|
* resolves to `null`.
|
|
122
155
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Wire format between the in-container runtime bridge (mounted at
|
|
2
2
|
// `/mnt/session/uploads/aex/aex`, invoked through `node`) and
|
|
3
|
-
// the
|
|
3
|
+
// the Worker-owned named proxy route
|
|
4
4
|
// (`POST /api/runs/:runId/proxy/:endpointName`).
|
|
5
5
|
//
|
|
6
6
|
// This module is the single source of truth for the request shape, the
|
|
@@ -18,7 +18,40 @@
|
|
|
18
18
|
* the new version.
|
|
19
19
|
*/
|
|
20
20
|
export const PROXY_PROTOCOL_VERSION = "1";
|
|
21
|
+
/**
|
|
22
|
+
* Streaming named-proxy protocol. A client that sends `"2"` in
|
|
23
|
+
* {@link PROXY_PROTOCOL_HEADER} opts into the streamed response path: the
|
|
24
|
+
* Worker pipes the upstream body back unbuffered (no base64, no
|
|
25
|
+
* `arrayBuffer()` in the ~128MB isolate) and carries the envelope metadata
|
|
26
|
+
* in the `x-aex-proxy-*` response headers below instead of a JSON envelope.
|
|
27
|
+
*
|
|
28
|
+
* Additive: `"1"` stays valid and keeps the buffered
|
|
29
|
+
* {@link ProxyResponseEnvelope}. Old runners keep working; new runners
|
|
30
|
+
* stream. The version is on the request header so the Worker can serve
|
|
31
|
+
* both shapes without a coordinated CLI/BFF release.
|
|
32
|
+
*/
|
|
33
|
+
export const PROXY_PROTOCOL_VERSION_V2 = "2";
|
|
21
34
|
export const PROXY_PROTOCOL_HEADER = "x-aex-proxy-protocol";
|
|
35
|
+
/**
|
|
36
|
+
* Response headers for the streamed (v2) named-proxy path. The Worker sets
|
|
37
|
+
* these BEFORE it starts streaming the upstream body, so the client can
|
|
38
|
+
* reconstruct the same fields the v1 {@link ProxyResponseEnvelope} carried
|
|
39
|
+
* without buffering. All values are plain strings; numeric fields are
|
|
40
|
+
* decimal, booleans are `"true"`/`"false"`.
|
|
41
|
+
*/
|
|
42
|
+
export const PROXY_RESP_STATUS_HEADER = "x-aex-proxy-status";
|
|
43
|
+
export const PROXY_RESP_MODE_HEADER = "x-aex-proxy-effective-mode";
|
|
44
|
+
/**
|
|
45
|
+
* `"true"` when the cap forced truncation, `"false"` when the full body
|
|
46
|
+
* fit, `"unknown"` when the upstream omitted `content-length` and the body
|
|
47
|
+
* happened to reach the cap (can't distinguish exact-fit from over-cap
|
|
48
|
+
* without buffering). See the streaming byte-cap note in proxy-routes.ts.
|
|
49
|
+
*/
|
|
50
|
+
export const PROXY_RESP_TRUNCATED_HEADER = "x-aex-proxy-truncated";
|
|
51
|
+
export const PROXY_RESP_REMAINING_CALLS_HEADER = "x-aex-proxy-remaining-calls";
|
|
52
|
+
export const PROXY_RESP_REMAINING_BYTES_HEADER = "x-aex-proxy-remaining-bytes";
|
|
53
|
+
/** JSON object of lowercase upstream header names → values (mode-filtered). */
|
|
54
|
+
export const PROXY_RESP_UPSTREAM_HEADERS_HEADER = "x-aex-proxy-upstream-headers";
|
|
22
55
|
/**
|
|
23
56
|
* Default `User-Agent` the proxy attaches to every outbound request when
|
|
24
57
|
* the caller did not supply one via `allowHeaders`. Some upstreams reject
|
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Single source of truth for a run's artifact namespaces.
|
|
3
3
|
*
|
|
4
|
-
* Every run stores
|
|
4
|
+
* Every run stores public outputs and internal diagnostics under separate
|
|
5
|
+
* prefixes:
|
|
5
6
|
*
|
|
6
|
-
* runs/<runId>/outputs/<rel>
|
|
7
|
-
* runs/<runId>/logs/<rel>
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* runs/<runId>/outputs/<rel> — the run's real deliverables.
|
|
8
|
+
* runs/<runId>/internal/logs/<rel> — platform diagnostics
|
|
9
|
+
* (`runtime/`, `host/`,
|
|
10
|
+
* `provider-proxy/`,
|
|
11
|
+
* `control-plane/`).
|
|
10
12
|
*
|
|
11
13
|
* The runner uploads every file with a workspace-relative path and the
|
|
12
14
|
* server decides the namespace from that path's prefix, so the split is
|
|
13
15
|
* owned here — the runner does not need to know about it. Diagnostics
|
|
14
16
|
* reach the upload route as workspace dotdirs (`.runtime-logs/...`,
|
|
15
|
-
* `.host-logs/...`, etc.)
|
|
16
|
-
*
|
|
17
|
+
* `.host-logs/...`, etc.) or runtime-managed home-directory state; the stored
|
|
18
|
+
* path uses canonical log namespaces.
|
|
17
19
|
*/
|
|
18
20
|
export declare const RUN_OUTPUTS_PREFIX = "outputs";
|
|
19
|
-
export declare const
|
|
21
|
+
export declare const RUN_INTERNAL_LOGS_PREFIX = "internal/logs";
|
|
20
22
|
/**
|
|
21
23
|
* Relative-path prefixes that mark a stored artifact as a platform diagnostic
|
|
22
24
|
* rather than a run deliverable. Dotted forms are upload-time paths; legacy
|
|
23
25
|
* forms are accepted so old records normalize to the canonical namespace.
|
|
24
26
|
*/
|
|
25
27
|
export declare const RUN_LOG_REL_PREFIXES: readonly [".runtime-logs/", ".host-logs/", ".provider-proxy/", ".control-plane/", ".anthropic-debug/", ".goose-logs/", ".fly-logs/", "runtime/", "host/", "provider-proxy/", "control-plane/", "anthropic-debug/", "goose-logs/", "fly-logs/"];
|
|
26
|
-
/** True when a workspace-relative artifact path belongs to the
|
|
28
|
+
/** True when a workspace-relative artifact path belongs to the internal logs namespace. */
|
|
27
29
|
export declare function isRunLogRelPath(rel: string): boolean;
|
|
28
30
|
/**
|
|
29
31
|
* The artifact's path relative to its namespace prefix as stored. Diagnostics
|
|
@@ -33,7 +35,7 @@ export declare function isRunLogRelPath(rel: string): boolean;
|
|
|
33
35
|
export declare function runArtifactRel(rel: string): string;
|
|
34
36
|
/**
|
|
35
37
|
* Storage key for a run artifact uploaded with relative path `rel`, routing
|
|
36
|
-
* diagnostics into `logs/` and everything else into `outputs/`.
|
|
38
|
+
* diagnostics into `internal/logs/` and everything else into `outputs/`.
|
|
37
39
|
*/
|
|
38
40
|
export declare function runArtifactKey(runId: string, rel: string): string;
|
|
39
41
|
/** The `assets` namespace under a run's prefix. */
|
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Single source of truth for a run's artifact namespaces.
|
|
3
3
|
*
|
|
4
|
-
* Every run stores
|
|
4
|
+
* Every run stores public outputs and internal diagnostics under separate
|
|
5
|
+
* prefixes:
|
|
5
6
|
*
|
|
6
|
-
* runs/<runId>/outputs/<rel>
|
|
7
|
-
* runs/<runId>/logs/<rel>
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* runs/<runId>/outputs/<rel> — the run's real deliverables.
|
|
8
|
+
* runs/<runId>/internal/logs/<rel> — platform diagnostics
|
|
9
|
+
* (`runtime/`, `host/`,
|
|
10
|
+
* `provider-proxy/`,
|
|
11
|
+
* `control-plane/`).
|
|
10
12
|
*
|
|
11
13
|
* The runner uploads every file with a workspace-relative path and the
|
|
12
14
|
* server decides the namespace from that path's prefix, so the split is
|
|
13
15
|
* owned here — the runner does not need to know about it. Diagnostics
|
|
14
16
|
* reach the upload route as workspace dotdirs (`.runtime-logs/...`,
|
|
15
|
-
* `.host-logs/...`, etc.)
|
|
16
|
-
*
|
|
17
|
+
* `.host-logs/...`, etc.) or runtime-managed home-directory state; the stored
|
|
18
|
+
* path uses canonical log namespaces.
|
|
17
19
|
*/
|
|
18
20
|
export const RUN_OUTPUTS_PREFIX = "outputs";
|
|
19
|
-
export const
|
|
21
|
+
export const RUN_INTERNAL_LOGS_PREFIX = "internal/logs";
|
|
20
22
|
/**
|
|
21
23
|
* Relative-path prefixes that mark a stored artifact as a platform diagnostic
|
|
22
24
|
* rather than a run deliverable. Dotted forms are upload-time paths; legacy
|
|
@@ -38,7 +40,7 @@ export const RUN_LOG_REL_PREFIXES = [
|
|
|
38
40
|
"goose-logs/",
|
|
39
41
|
"fly-logs/"
|
|
40
42
|
];
|
|
41
|
-
/** True when a workspace-relative artifact path belongs to the
|
|
43
|
+
/** True when a workspace-relative artifact path belongs to the internal logs namespace. */
|
|
42
44
|
export function isRunLogRelPath(rel) {
|
|
43
45
|
return RUN_LOG_REL_PREFIXES.some((prefix) => rel.startsWith(prefix));
|
|
44
46
|
}
|
|
@@ -80,10 +82,10 @@ export function runArtifactRel(rel) {
|
|
|
80
82
|
}
|
|
81
83
|
/**
|
|
82
84
|
* Storage key for a run artifact uploaded with relative path `rel`, routing
|
|
83
|
-
* diagnostics into `logs/` and everything else into `outputs/`.
|
|
85
|
+
* diagnostics into `internal/logs/` and everything else into `outputs/`.
|
|
84
86
|
*/
|
|
85
87
|
export function runArtifactKey(runId, rel) {
|
|
86
|
-
const namespace = isRunLogRelPath(rel) ?
|
|
88
|
+
const namespace = isRunLogRelPath(rel) ? RUN_INTERNAL_LOGS_PREFIX : RUN_OUTPUTS_PREFIX;
|
|
87
89
|
return `runs/${runId}/${namespace}/${runArtifactRel(rel)}`;
|
|
88
90
|
}
|
|
89
91
|
/** The `assets` namespace under a run's prefix. */
|
|
@@ -61,6 +61,13 @@ export declare const SKILL_NAME_PATTERN: RegExp;
|
|
|
61
61
|
export declare const SKILL_BUNDLE_LIMITS: {
|
|
62
62
|
/** Compressed (.zip) ceiling. */
|
|
63
63
|
readonly maxCompressedBytes: number;
|
|
64
|
+
/**
|
|
65
|
+
* Hard ceiling for the direct-to-storage (presigned PUT) upload path, where
|
|
66
|
+
* bytes never transit the hosted API so its memory/request-payload limits no
|
|
67
|
+
* longer cap the bundle. Kept well under the object store's 5 GiB single-PUT
|
|
68
|
+
* limit; objects above this would need S3 multipart, which is out of scope.
|
|
69
|
+
*/
|
|
70
|
+
readonly maxBytes: number;
|
|
64
71
|
/** Sum of uncompressed file sizes. */
|
|
65
72
|
readonly maxDecompressedBytes: number;
|
|
66
73
|
/** Number of regular file entries (directories don't count). */
|
|
@@ -65,6 +65,13 @@ export const SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{0,127}$/;
|
|
|
65
65
|
export const SKILL_BUNDLE_LIMITS = {
|
|
66
66
|
/** Compressed (.zip) ceiling. */
|
|
67
67
|
maxCompressedBytes: 10 * 1024 * 1024,
|
|
68
|
+
/**
|
|
69
|
+
* Hard ceiling for the direct-to-storage (presigned PUT) upload path, where
|
|
70
|
+
* bytes never transit the hosted API so its memory/request-payload limits no
|
|
71
|
+
* longer cap the bundle. Kept well under the object store's 5 GiB single-PUT
|
|
72
|
+
* limit; objects above this would need S3 multipart, which is out of scope.
|
|
73
|
+
*/
|
|
74
|
+
maxBytes: 2 * 1024 * 1024 * 1024,
|
|
68
75
|
/** Sum of uncompressed file sizes. */
|
|
69
76
|
maxDecompressedBytes: 50 * 1024 * 1024,
|
|
70
77
|
/** Number of regular file entries (directories don't count). */
|
|
@@ -419,26 +426,41 @@ export function rejectStdioMcpShape(record) {
|
|
|
419
426
|
}
|
|
420
427
|
}
|
|
421
428
|
/**
|
|
422
|
-
* Reasons an
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
429
|
+
* Reasons an IP-literal host should be refused. Returns null when the
|
|
430
|
+
* literal is a routable public address (or not an IP literal at all — name
|
|
431
|
+
* resolution is the caller's concern). Single source of truth for the
|
|
432
|
+
* numeric-range deny-list so the shared MCP parser, the Worker BYOK proxy
|
|
433
|
+
* handlers, and `submission.parseProxyBaseUrl` classify the same bytes.
|
|
426
434
|
*
|
|
427
|
-
*
|
|
435
|
+
* `host` is the already-bracket-stripped, lowercased hostname.
|
|
436
|
+
*
|
|
437
|
+
* NOTE — residual DNS-rebind gap: this denies IP *literals* only. A name
|
|
438
|
+
* that resolves to a private/metadata IP is NOT caught here (we don't
|
|
439
|
+
* resolve at parse time). Closing that requires resolve-then-pin at egress;
|
|
440
|
+
* that pinning is deferred. The host's outbound fetch still refuses RFC1918
|
|
441
|
+
* at connect, but not loopback/169.254/CGNAT/ULA — which is exactly why the
|
|
442
|
+
* literal checks below exist as defense in depth.
|
|
428
443
|
*/
|
|
429
|
-
function
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
//
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
444
|
+
function denyReasonForHostIp(host) {
|
|
445
|
+
// IPv4-mapped / IPv4-compatible IPv6 literals decode to an embedded IPv4 —
|
|
446
|
+
// classify that IPv4 so a mapped form can't smuggle a private target. Two
|
|
447
|
+
// shapes reach us: the dotted-quad form a caller may type (`::ffff:127.0.0.1`)
|
|
448
|
+
// and the hex form `new URL().hostname` normalises it to (`::ffff:7f00:1`),
|
|
449
|
+
// where the two trailing hextets ARE the four IPv4 octets.
|
|
450
|
+
const mappedDotted = /^::(?:ffff:)?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.exec(host);
|
|
451
|
+
if (mappedDotted) {
|
|
452
|
+
return denyReasonForV4(mappedDotted[1]);
|
|
453
|
+
}
|
|
454
|
+
const mappedHex = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(host);
|
|
455
|
+
if (mappedHex) {
|
|
456
|
+
const hi = Number.parseInt(mappedHex[1], 16);
|
|
457
|
+
const lo = Number.parseInt(mappedHex[2], 16);
|
|
458
|
+
const dotted = `${hi >> 8}.${hi & 0xff}.${lo >> 8}.${lo & 0xff}`;
|
|
459
|
+
return denyReasonForV4(dotted);
|
|
460
|
+
}
|
|
461
|
+
const v4 = denyReasonForV4(host);
|
|
462
|
+
if (v4)
|
|
463
|
+
return v4;
|
|
442
464
|
// Loopback IPv6 (::1 in any acceptable form)
|
|
443
465
|
if (host === "::1" || host === "0:0:0:0:0:0:0:1") {
|
|
444
466
|
return "must not target loopback IPv6 (::1)";
|
|
@@ -447,20 +469,67 @@ function denyReasonForMcpHost(parsed) {
|
|
|
447
469
|
if (/^fe[89ab][0-9a-f]?:/.test(host)) {
|
|
448
470
|
return "must not target link-local IPv6 (fe80::/10)";
|
|
449
471
|
}
|
|
472
|
+
// Unique-local IPv6 (fc00::/7 — fc00:: through fdff::), the IPv6
|
|
473
|
+
// equivalent of RFC1918 private space.
|
|
474
|
+
if (/^f[cd][0-9a-f]{0,2}:/.test(host)) {
|
|
475
|
+
return "must not target unique-local IPv6 (fc00::/7)";
|
|
476
|
+
}
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* IPv4-literal deny-list. Returns null when `host` is not a dotted-quad or
|
|
481
|
+
* is a routable public IPv4. Split out of {@link denyReasonForHostIp} so the
|
|
482
|
+
* IPv4-mapped IPv6 branch reuses the exact same ranges.
|
|
483
|
+
*/
|
|
484
|
+
function denyReasonForV4(host) {
|
|
485
|
+
if (!/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host))
|
|
486
|
+
return null;
|
|
487
|
+
const octets = host.split(".").map((o) => Number.parseInt(o, 10));
|
|
488
|
+
if (octets.some((o) => o > 255))
|
|
489
|
+
return null;
|
|
490
|
+
const [a, b] = octets;
|
|
491
|
+
// Loopback IPv4 (127.0.0.0/8)
|
|
492
|
+
if (a === 127)
|
|
493
|
+
return "must not target loopback IPv4 (127.0.0.0/8)";
|
|
450
494
|
// Link-local / metadata IPv4 (169.254.0.0/16 — includes 169.254.169.254)
|
|
451
|
-
if (
|
|
495
|
+
if (a === 169 && b === 254) {
|
|
452
496
|
return "must not target link-local IPv4 (169.254.0.0/16) — cloud metadata range";
|
|
453
497
|
}
|
|
498
|
+
// CGNAT shared address space (100.64.0.0/10) — runners NAT through it, so
|
|
499
|
+
// an upstream there can reach sibling tenants / the runner host.
|
|
500
|
+
if (a === 100 && b >= 64 && b <= 127) {
|
|
501
|
+
return "must not target CGNAT IPv4 (100.64.0.0/10)";
|
|
502
|
+
}
|
|
454
503
|
// RFC1918 private ranges (10/8, 172.16/12, 192.168/16) — defense in depth.
|
|
455
|
-
if (
|
|
504
|
+
if (a === 10)
|
|
456
505
|
return "must not target RFC1918 IPv4 (10.0.0.0/8)";
|
|
457
|
-
|
|
458
|
-
if (/^172\.(1[6-9]|2[0-9]|3[01])\.[0-9]+\.[0-9]+$/.test(host)) {
|
|
506
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
459
507
|
return "must not target RFC1918 IPv4 (172.16.0.0/12)";
|
|
460
|
-
|
|
461
|
-
if (/^192\.168\.[0-9]+\.[0-9]+$/.test(host)) {
|
|
508
|
+
if (a === 192 && b === 168)
|
|
462
509
|
return "must not target RFC1918 IPv4 (192.168.0.0/16)";
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Reasons an MCP server URL should be refused at parse time. Returns null
|
|
514
|
+
* when the URL is acceptable. Hostnames are lowercased; numeric ranges
|
|
515
|
+
* are checked literally so the catch covers both names ("localhost") and
|
|
516
|
+
* IP literals ("127.0.0.1") symmetrically. The numeric-range checks
|
|
517
|
+
* delegate to {@link denyReasonForHostIp} (shared with the Worker proxy).
|
|
518
|
+
*
|
|
519
|
+
* Surface tracked by server-side SSRF regression coverage.
|
|
520
|
+
*/
|
|
521
|
+
function denyReasonForMcpHost(parsed) {
|
|
522
|
+
// `new URL("https://[fe80::1]/").hostname` returns `[fe80::1]` WITH
|
|
523
|
+
// the brackets on Node 22; strip them so the IPv6 checks match either
|
|
524
|
+
// shape symmetrically.
|
|
525
|
+
const host = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
526
|
+
// Loopback name (covers `localhost` + `localhost.localdomain` etc.)
|
|
527
|
+
if (host === "localhost" || host.endsWith(".localhost")) {
|
|
528
|
+
return "must not target a loopback hostname";
|
|
463
529
|
}
|
|
530
|
+
const ipDenial = denyReasonForHostIp(host);
|
|
531
|
+
if (ipDenial)
|
|
532
|
+
return ipDenial;
|
|
464
533
|
// Port constraint: https must be on 443 (defense in depth — non-standard
|
|
465
534
|
// https ports often indicate internal services). http allowance keeps
|
|
466
535
|
// the existing local-dev pattern (e.g. host.docker.internal:8787) usable.
|
|
@@ -12,7 +12,7 @@ export declare const CUSTODY_SECRET_CLASSES: readonly ["provider_api_key", "mcp_
|
|
|
12
12
|
export type CustodySecretClass = (typeof CUSTODY_SECRET_CLASSES)[number];
|
|
13
13
|
export declare const CUSTODY_RESOURCE_CLASSES: readonly ["runtime_machine", "native_provider_session", "native_provider_resource", "provider_asset", "proxy_token", "execution_secret", "event_archive", "run_output", "run_log", "run_asset"];
|
|
14
14
|
export type CustodyResourceClass = (typeof CUSTODY_RESOURCE_CLASSES)[number];
|
|
15
|
-
export declare const CUSTODY_EXPOSURE_SURFACES: readonly ["aex_vault", "aex_kv", "
|
|
15
|
+
export declare const CUSTODY_EXPOSURE_SURFACES: readonly ["aex_vault", "aex_kv", "host_env", "host_file", "provider_vault", "provider_session", "dashboard_proxy", "api_provider_proxy", "api_mcp_proxy", "run_artifact_store", "coordinator_event_archive"];
|
|
16
16
|
export type CustodyExposureSurface = (typeof CUSTODY_EXPOSURE_SURFACES)[number];
|
|
17
17
|
export declare const CUSTODY_EXPOSURE_ACCESS_KINDS: readonly ["stored", "injected", "proxied", "replicated", "observed", "unknown"];
|
|
18
18
|
export type CustodyExposureAccessKind = (typeof CUSTODY_EXPOSURE_ACCESS_KINDS)[number];
|
|
@@ -24,7 +24,7 @@ export declare const CUSTODY_CLEANUP_STATUSES: readonly ["succeeded", "partial",
|
|
|
24
24
|
export type CustodyCleanupStatus = (typeof CUSTODY_CLEANUP_STATUSES)[number];
|
|
25
25
|
export declare const CUSTODY_EVIDENCE_SOURCES: readonly ["run_row", "runtime_manifest", "terminal_event", "cleanup_step", "output_capture", "proxy_audit", "provider_cleanup_summary"];
|
|
26
26
|
export type CustodyEvidenceSource = (typeof CUSTODY_EVIDENCE_SOURCES)[number];
|
|
27
|
-
export declare const CUSTODY_MANIFEST_EXCLUDED_VALUE_CLASSES: readonly ["raw_secret_values", "bearer_hashes", "provider_response_bodies", "signed_urls", "
|
|
27
|
+
export declare const CUSTODY_MANIFEST_EXCLUDED_VALUE_CLASSES: readonly ["raw_secret_values", "bearer_hashes", "provider_response_bodies", "signed_urls", "object_store_keys", "vault_ids", "private_resource_handles"];
|
|
28
28
|
export type CustodyManifestExcludedValueClass = (typeof CUSTODY_MANIFEST_EXCLUDED_VALUE_CLASSES)[number];
|
|
29
29
|
export declare const CUSTODY_TOMBSTONE_MANIFEST_STATUSES: readonly ["written", "not_written", "write_failed", "purged"];
|
|
30
30
|
export type CustodyTombstoneManifestStatus = (typeof CUSTODY_TOMBSTONE_MANIFEST_STATUSES)[number];
|
|
@@ -198,7 +198,7 @@ export interface CustodyManifestWriteResult {
|
|
|
198
198
|
export interface CustodyManifestWriter {
|
|
199
199
|
writeCustodyManifest(input: CustodyManifestInput): Promise<CustodyManifestWriteResult>;
|
|
200
200
|
}
|
|
201
|
-
export type CustodyRedactionReason = "forbidden_field_name" | "bearer_token" | "provider_key" | "signed_url" | "
|
|
201
|
+
export type CustodyRedactionReason = "forbidden_field_name" | "bearer_token" | "provider_key" | "signed_url" | "object_store_key" | "vault_id" | "private_resource_handle" | "high_entropy_token";
|
|
202
202
|
export interface CustodyRedactionFinding {
|
|
203
203
|
readonly path: string;
|
|
204
204
|
readonly reason: CustodyRedactionReason;
|
|
@@ -27,8 +27,8 @@ export const CUSTODY_RESOURCE_CLASSES = [
|
|
|
27
27
|
export const CUSTODY_EXPOSURE_SURFACES = [
|
|
28
28
|
"aex_vault",
|
|
29
29
|
"aex_kv",
|
|
30
|
-
"
|
|
31
|
-
"
|
|
30
|
+
"host_env",
|
|
31
|
+
"host_file",
|
|
32
32
|
"provider_vault",
|
|
33
33
|
"provider_session",
|
|
34
34
|
"dashboard_proxy",
|
|
@@ -84,7 +84,7 @@ export const CUSTODY_MANIFEST_EXCLUDED_VALUE_CLASSES = [
|
|
|
84
84
|
"bearer_hashes",
|
|
85
85
|
"provider_response_bodies",
|
|
86
86
|
"signed_urls",
|
|
87
|
-
"
|
|
87
|
+
"object_store_keys",
|
|
88
88
|
"vault_ids",
|
|
89
89
|
"private_resource_handles"
|
|
90
90
|
];
|
|
@@ -413,7 +413,7 @@ const forbiddenStringPatterns = Object.freeze([
|
|
|
413
413
|
regex: /\b(?:sk-(?:ant|proj|live|test|deepseek|openai)|xox[baprs]-|AIza)[A-Za-z0-9_-]{8,}/i
|
|
414
414
|
},
|
|
415
415
|
{ reason: "signed_url", regex: /[?&](?:X-Amz-Signature|X-Amz-Credential|X-Amz-Algorithm|AWSAccessKeyId)=/i },
|
|
416
|
-
{ reason: "
|
|
416
|
+
{ reason: "object_store_key", regex: /(^|[\s"'`])(?:runs|assets)\/[^?<#\s"'`]+/i },
|
|
417
417
|
{ reason: "vault_id", regex: /\b(?:vault|vlt|secret)[_:-][A-Za-z0-9][A-Za-z0-9_-]{7,}\b/i },
|
|
418
418
|
{
|
|
419
419
|
reason: "private_resource_handle",
|
|
@@ -422,7 +422,7 @@ const forbiddenStringPatterns = Object.freeze([
|
|
|
422
422
|
{ reason: "high_entropy_token", regex: /\b(?=[A-Za-z0-9_-]{40,}\b)(?=.*[A-Za-z])(?=.*\d)[A-Za-z0-9_-]{40,}\b/ }
|
|
423
423
|
]);
|
|
424
424
|
function isForbiddenCustodyFieldName(key) {
|
|
425
|
-
return /^(apiKey|secretValue|bearerHash|signedUrl|
|
|
425
|
+
return /^(apiKey|secretValue|bearerHash|signedUrl|objectStoreKey|objectKey|vaultId|providerResponseBody|responseBody|privateResourceHandle|resourceHandle|rawBody)$/i.test(key);
|
|
426
426
|
}
|
|
427
427
|
function assertSafeIdentifier(value, field) {
|
|
428
428
|
assertNonEmptyString(value, field);
|