@aexhq/sdk 0.13.7 → 0.13.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +14 -14
  2. package/dist/_contracts/connection-ticket.d.ts +8 -7
  3. package/dist/_contracts/connection-ticket.js +20 -14
  4. package/dist/_contracts/event-envelope.d.ts +17 -18
  5. package/dist/_contracts/event-envelope.js +10 -11
  6. package/dist/_contracts/managed-key.d.ts +27 -1
  7. package/dist/_contracts/managed-key.js +75 -4
  8. package/dist/_contracts/operations.d.ts +9 -20
  9. package/dist/_contracts/operations.js +33 -82
  10. package/dist/_contracts/proxy-protocol.d.ts +35 -2
  11. package/dist/_contracts/proxy-protocol.js +34 -1
  12. package/dist/_contracts/run-artifacts.d.ts +12 -10
  13. package/dist/_contracts/run-artifacts.js +13 -11
  14. package/dist/_contracts/run-config.d.ts +7 -0
  15. package/dist/_contracts/run-config.js +93 -24
  16. package/dist/_contracts/run-custody.d.ts +3 -3
  17. package/dist/_contracts/run-custody.js +5 -5
  18. package/dist/_contracts/run-record.d.ts +5 -17
  19. package/dist/_contracts/run-record.js +4 -15
  20. package/dist/_contracts/run-retention.d.ts +2 -2
  21. package/dist/_contracts/run-retention.js +3 -3
  22. package/dist/_contracts/run-unit.d.ts +4 -5
  23. package/dist/_contracts/runner-event.d.ts +7 -8
  24. package/dist/_contracts/runner-event.js +7 -8
  25. package/dist/_contracts/side-effect-audit.d.ts +2 -2
  26. package/dist/_contracts/side-effect-audit.js +3 -3
  27. package/dist/_contracts/stable.d.ts +1 -1
  28. package/dist/_contracts/stable.js +1 -1
  29. package/dist/_contracts/submission.d.ts +5 -6
  30. package/dist/_contracts/submission.js +1 -1
  31. package/dist/cli.mjs +127 -127
  32. package/dist/cli.mjs.sha256 +1 -1
  33. package/dist/client.d.ts +7 -57
  34. package/dist/client.js +624 -167
  35. package/dist/client.js.map +1 -1
  36. package/dist/index.d.ts +3 -3
  37. package/dist/index.js +2 -2
  38. package/dist/index.js.map +1 -1
  39. package/dist/version.d.ts +1 -1
  40. package/dist/version.js +1 -1
  41. package/docs/cleanup.md +4 -4
  42. package/docs/credentials.md +5 -5
  43. package/docs/events.md +5 -5
  44. package/docs/outputs.md +23 -25
  45. package/docs/product-boundaries.md +5 -5
  46. package/docs/provider-runtime-capabilities.md +1 -1
  47. package/docs/quickstart.md +12 -12
  48. package/docs/run-config.md +1 -1
  49. package/docs/run-record.md +6 -9
  50. package/docs/skills.md +23 -25
  51. 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
- export async function listRunEvents(http, runId, options = {}) {
36
- const query = options.channel && options.channel !== "event"
37
- ? { channel: options.channel }
38
- : {};
39
- const result = await http.request(`/api/runs/${encodeURIComponent(runId)}/events`, {}, query);
40
- return result.events;
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` or `logs`
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
- ...(namespace === "outputs" ? { customerContent: true } : {})
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 four
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, logEvents, allEvents, outputs, logItems] = await Promise.all([
186
+ const [run, events, outputs] = await Promise.all([
204
187
  getRun(http, runId),
205
188
  listRunEvents(http, runId),
206
- tryListOptionalRunEvents(http, runId, "log"),
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
- logs: logs.captured,
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`; includes `logs.jsonl` / `all.jsonl` when the deployed
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 [events, logEvents, allEvents] = await Promise.all([
271
- listRunEvents(http, runId),
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
- * Dashboard host that serves `/api/runs/:runId/proxy/:endpointName`
119
- * (the BFF proxy route). Distinct from the api Worker host. When unset
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 dashboard BFF proxy route
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 its artifacts under two sibling prefixes:
4
+ * Every run stores public outputs and internal diagnostics under separate
5
+ * prefixes:
5
6
  *
6
- * runs/<runId>/outputs/<rel> — the run's real deliverables.
7
- * runs/<runId>/logs/<rel> — platform diagnostics (`runtime/`,
8
- * `host/`, `provider-proxy/`,
9
- * `control-plane/`).
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.); the stored path uses canonical log namespaces.
16
- * Legacy diagnostic prefixes are normalized on read/write compatibility paths.
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 RUN_LOGS_PREFIX = "logs";
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 `logs` namespace. */
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 its artifacts under two sibling prefixes:
4
+ * Every run stores public outputs and internal diagnostics under separate
5
+ * prefixes:
5
6
  *
6
- * runs/<runId>/outputs/<rel> — the run's real deliverables.
7
- * runs/<runId>/logs/<rel> — platform diagnostics (`runtime/`,
8
- * `host/`, `provider-proxy/`,
9
- * `control-plane/`).
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.); the stored path uses canonical log namespaces.
16
- * Legacy diagnostic prefixes are normalized on read/write compatibility paths.
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 RUN_LOGS_PREFIX = "logs";
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 `logs` namespace. */
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) ? RUN_LOGS_PREFIX : RUN_OUTPUTS_PREFIX;
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 MCP server URL should be refused at parse time. Returns null
423
- * when the URL is acceptable. Hostnames are lowercased; numeric ranges
424
- * are checked literally so the catch covers both names ("localhost") and
425
- * IP literals ("127.0.0.1") symmetrically.
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
- * Surface tracked by server-side SSRF regression coverage.
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 denyReasonForMcpHost(parsed) {
430
- // `new URL("https://[fe80::1]/").hostname` returns `[fe80::1]` WITH
431
- // the brackets on Node 22; strip them so the IPv6 checks match either
432
- // shape symmetrically.
433
- const host = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, "");
434
- // Loopback name (covers `localhost` + `localhost.localdomain` etc.)
435
- if (host === "localhost" || host.endsWith(".localhost")) {
436
- return "must not target a loopback hostname";
437
- }
438
- // Loopback IPv4 (127.0.0.0/8)
439
- if (/^127(?:\.[0-9]+){3}$/.test(host)) {
440
- return "must not target loopback IPv4 (127.0.0.0/8)";
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 (/^169\.254\.[0-9]+\.[0-9]+$/.test(host)) {
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 (/^10\.[0-9]+\.[0-9]+\.[0-9]+$/.test(host)) {
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", "fly_machine_env", "fly_machine_file", "provider_vault", "provider_session", "dashboard_proxy", "api_provider_proxy", "api_mcp_proxy", "run_artifact_store", "coordinator_event_archive"];
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", "r2_object_keys", "vault_ids", "private_resource_handles"];
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" | "r2_object_key" | "vault_id" | "private_resource_handle" | "high_entropy_token";
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
- "fly_machine_env",
31
- "fly_machine_file",
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
- "r2_object_keys",
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: "r2_object_key", regex: /(^|[\s"'`])(?:runs|assets)\/[^?<#\s"'`]+/i },
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|r2Key|objectKey|vaultId|providerResponseBody|responseBody|privateResourceHandle|resourceHandle|rawBody)$/i.test(key);
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);