antpath 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,7 @@ export * from "./proxy-protocol.js";
6
6
  export * from "./secrets.js";
7
7
  export * from "./status.js";
8
8
  export * from "./submission.js";
9
+ export * from "./run-unit.js";
9
10
  export * from "./stable.js";
10
11
  export * from "./sdk-secrets.js";
11
12
  export * from "./sdk-errors.js";
@@ -6,6 +6,7 @@ export * from "./proxy-protocol.js";
6
6
  export * from "./secrets.js";
7
7
  export * from "./status.js";
8
8
  export * from "./submission.js";
9
+ export * from "./run-unit.js";
9
10
  // SDK + CLI shared surface — moved here so the published `antpath` SDK
10
11
  // and the in-container `antpath` CLI consume the SAME implementation.
11
12
  export * from "./stable.js";
@@ -1,4 +1,5 @@
1
1
  import type { HttpClient } from "./http.js";
2
+ import type { RunUnit } from "./run-unit.js";
2
3
  import type { Output, Run, RunEvent, SignedOutputLink, Skill, WhoAmI } from "./runtime-types.js";
3
4
  import type { PlatformFlatRunSubmissionInput, PlatformRunSubmissionInput } from "./submission.js";
4
5
  /**
@@ -16,12 +17,38 @@ import type { PlatformFlatRunSubmissionInput, PlatformRunSubmissionInput } from
16
17
  */
17
18
  export declare function submitRun(http: HttpClient, request: PlatformRunSubmissionInput): Promise<Run>;
18
19
  export declare function getRun(http: HttpClient, runId: string): Promise<Run>;
20
+ /**
21
+ * Strongly-typed accessor for the full self-contained run unit:
22
+ * parsed submission inputs, attempts, indexed events (with
23
+ * pagination cursor for large runs), raw-event Storage manifest,
24
+ * outputs, capture failures, proxy-call audit, pinned skills,
25
+ * provider skills, transient skills.
26
+ *
27
+ * Backed by the same `GET /api/runs/:runId` endpoint that
28
+ * `getRun` calls; this variant just narrows the return type to
29
+ * the documented wire shape. Prefer this for new code; `getRun`
30
+ * stays for callers that only need the loose record.
31
+ */
32
+ export declare function getRunUnit(http: HttpClient, runId: string): Promise<RunUnit>;
19
33
  export declare function listRunEvents(http: HttpClient, runId: string): Promise<readonly RunEvent[]>;
20
34
  export declare function listOutputs(http: HttpClient, runId: string): Promise<readonly Output[]>;
21
35
  export declare function createOutputLink(http: HttpClient, runId: string, outputId: string): Promise<SignedOutputLink>;
22
36
  export declare function cancelRun(http: HttpClient, runId: string): Promise<void>;
23
37
  export declare function deleteRun(http: HttpClient, runId: string): Promise<void>;
24
38
  export declare function whoami(http: HttpClient): Promise<WhoAmI>;
39
+ /**
40
+ * Stream the per-run archive zip from the BFF. Returns the raw
41
+ * `Response` so callers can pipe the body to disk without buffering
42
+ * the whole archive in memory.
43
+ *
44
+ * The archive lifecycle contract lives in
45
+ * `apps/dashboard/src/server/run-archive.ts` and
46
+ * `packages/sdk/docs/outputs.md`. Pre-session runs reject with HTTP
47
+ * 409 `run_not_started`; mid-session and terminal both produce the
48
+ * same archive layout and differ only in `manifest.json`'s `source`
49
+ * + `partial` fields.
50
+ */
51
+ export declare function downloadRunArchive(http: HttpClient, runId: string): Promise<Response>;
25
52
  export declare function submitRunFlat(http: HttpClient, request: PlatformFlatRunSubmissionInput): Promise<Run>;
26
53
  /**
27
54
  * Multipart variant of `submitRunFlat` for runs that carry transient
@@ -59,3 +86,23 @@ export declare function createSkillBundle(http: HttpClient, args: {
59
86
  export declare function listSkills(http: HttpClient): Promise<readonly Skill[]>;
60
87
  export declare function getSkill(http: HttpClient, skillId: string): Promise<Skill>;
61
88
  export declare function deleteSkill(http: HttpClient, skillId: string): Promise<void>;
89
+ /**
90
+ * Lookup a live workspace skill by `(name, contentHash)`. Returns the
91
+ * matching `Skill` record or null when no live row carries that hash.
92
+ *
93
+ * `contentHash` is the wire format `sha256:<hex>` as returned by
94
+ * `hashSkillBundle`. This powers `Skill.uploadIfChanged` — the SDK
95
+ * computes the hash locally and calls this function to skip the upload
96
+ * when the bytes already exist.
97
+ */
98
+ export declare function findSkillByHash(http: HttpClient, args: {
99
+ readonly name: string;
100
+ readonly contentHash: string;
101
+ }): Promise<Skill | null>;
102
+ /**
103
+ * Lookup a live workspace skill by `name`. Returns the matching `Skill`
104
+ * record or null when no live row carries that name. Implemented as a
105
+ * list-and-filter on the existing `/api/skills` endpoint — the
106
+ * indexed by-hash route is reserved for `uploadIfChanged`.
107
+ */
108
+ export declare function findSkillByName(http: HttpClient, name: string): Promise<Skill | null>;
@@ -21,6 +21,21 @@ export async function getRun(http, runId) {
21
21
  const result = await http.request(`/api/runs/${encodeURIComponent(runId)}`);
22
22
  return hasRun(result) ? result.run : result;
23
23
  }
24
+ /**
25
+ * Strongly-typed accessor for the full self-contained run unit:
26
+ * parsed submission inputs, attempts, indexed events (with
27
+ * pagination cursor for large runs), raw-event Storage manifest,
28
+ * outputs, capture failures, proxy-call audit, pinned skills,
29
+ * provider skills, transient skills.
30
+ *
31
+ * Backed by the same `GET /api/runs/:runId` endpoint that
32
+ * `getRun` calls; this variant just narrows the return type to
33
+ * the documented wire shape. Prefer this for new code; `getRun`
34
+ * stays for callers that only need the loose record.
35
+ */
36
+ export async function getRunUnit(http, runId) {
37
+ return http.request(`/api/runs/${encodeURIComponent(runId)}`);
38
+ }
24
39
  export async function listRunEvents(http, runId) {
25
40
  const result = await http.request(`/api/runs/${encodeURIComponent(runId)}/events`);
26
41
  return result.events;
@@ -41,6 +56,22 @@ export async function deleteRun(http, runId) {
41
56
  export async function whoami(http) {
42
57
  return http.request("/api/whoami");
43
58
  }
59
+ /**
60
+ * Stream the per-run archive zip from the BFF. Returns the raw
61
+ * `Response` so callers can pipe the body to disk without buffering
62
+ * the whole archive in memory.
63
+ *
64
+ * The archive lifecycle contract lives in
65
+ * `apps/dashboard/src/server/run-archive.ts` and
66
+ * `packages/sdk/docs/outputs.md`. Pre-session runs reject with HTTP
67
+ * 409 `run_not_started`; mid-session and terminal both produce the
68
+ * same archive layout and differ only in `manifest.json`'s `source`
69
+ * + `partial` fields.
70
+ */
71
+ export async function downloadRunArchive(http, runId) {
72
+ const { response } = await http.download(`/api/runs/${encodeURIComponent(runId)}/download`);
73
+ return response;
74
+ }
44
75
  // ===========================================================================
45
76
  // Flat (Skill / McpServer / Blueprint) operations
46
77
  // ===========================================================================
@@ -125,6 +156,33 @@ export async function deleteSkill(http, skillId) {
125
156
  method: "DELETE"
126
157
  });
127
158
  }
159
+ /**
160
+ * Lookup a live workspace skill by `(name, contentHash)`. Returns the
161
+ * matching `Skill` record or null when no live row carries that hash.
162
+ *
163
+ * `contentHash` is the wire format `sha256:<hex>` as returned by
164
+ * `hashSkillBundle`. This powers `Skill.uploadIfChanged` — the SDK
165
+ * computes the hash locally and calls this function to skip the upload
166
+ * when the bytes already exist.
167
+ */
168
+ export async function findSkillByHash(http, args) {
169
+ const params = new URLSearchParams({
170
+ name: args.name,
171
+ content_hash: args.contentHash
172
+ });
173
+ const result = await http.request(`/api/skills/by-hash?${params.toString()}`);
174
+ return result.skill ?? null;
175
+ }
176
+ /**
177
+ * Lookup a live workspace skill by `name`. Returns the matching `Skill`
178
+ * record or null when no live row carries that name. Implemented as a
179
+ * list-and-filter on the existing `/api/skills` endpoint — the
180
+ * indexed by-hash route is reserved for `uploadIfChanged`.
181
+ */
182
+ export async function findSkillByName(http, name) {
183
+ const skills = await listSkills(http);
184
+ return skills.find((skill) => skill.name === name) ?? null;
185
+ }
128
186
  function unwrapSkill(result) {
129
187
  if (result && typeof result === "object" && "skill" in result) {
130
188
  return result.skill;
@@ -67,8 +67,16 @@ export interface ProxyIndexEntry {
67
67
  * The actual auth value lives in the run's Vault bundle under
68
68
  * `secrets.proxyEndpointAuth[i].value` and is never reflected back
69
69
  * into the container or index file.
70
+ *
71
+ * The `none` variant declares an upstream that takes no auth (public
72
+ * APIs like Wikimedia Commons or NASA Images). It still routes through
73
+ * the proxy for unified egress, audit, and budget enforcement, but
74
+ * carries no `proxyEndpointAuth[]` entry and the BFF injects no
75
+ * header or query value.
70
76
  */
71
77
  export type ProxyAuthShape = {
78
+ readonly type: "none";
79
+ } | {
72
80
  readonly type: "bearer";
73
81
  } | {
74
82
  readonly type: "basic";
@@ -82,7 +90,7 @@ export type ProxyAuthShape = {
82
90
  export type ProxyAuthType = ProxyAuthShape["type"];
83
91
  /**
84
92
  * Header name (lowercase) that an upstream auth shape uses as its
85
- * carrier. Returns `undefined` for query-based auth.
93
+ * carrier. Returns `undefined` for query-based and keyless auth.
86
94
  *
87
95
  * Used by the submission parser to forbid `allowHeaders` from listing
88
96
  * the auth header (avoids leaks via caller-supplied headers), and by
@@ -92,7 +100,7 @@ export type ProxyAuthType = ProxyAuthShape["type"];
92
100
  export declare function authShapeHeaderName(shape: ProxyAuthShape): string | undefined;
93
101
  /**
94
102
  * Query-string key that an upstream query-based auth shape uses as its
95
- * carrier. Returns `undefined` for non-query shapes.
103
+ * carrier. Returns `undefined` for non-query shapes (including "none").
96
104
  */
97
105
  export declare function authShapeQueryName(shape: ProxyAuthShape): string | undefined;
98
106
  /**
@@ -66,7 +66,7 @@ export const PROXY_ERROR_CODES = [
66
66
  ];
67
67
  /**
68
68
  * Header name (lowercase) that an upstream auth shape uses as its
69
- * carrier. Returns `undefined` for query-based auth.
69
+ * carrier. Returns `undefined` for query-based and keyless auth.
70
70
  *
71
71
  * Used by the submission parser to forbid `allowHeaders` from listing
72
72
  * the auth header (avoids leaks via caller-supplied headers), and by
@@ -81,12 +81,13 @@ export function authShapeHeaderName(shape) {
81
81
  case "header":
82
82
  return shape.name.toLowerCase();
83
83
  case "query":
84
+ case "none":
84
85
  return undefined;
85
86
  }
86
87
  }
87
88
  /**
88
89
  * Query-string key that an upstream query-based auth shape uses as its
89
- * carrier. Returns `undefined` for non-query shapes.
90
+ * carrier. Returns `undefined` for non-query shapes (including "none").
90
91
  */
91
92
  export function authShapeQueryName(shape) {
92
93
  return shape.type === "query" ? shape.name : undefined;
@@ -0,0 +1,206 @@
1
+ /**
2
+ * RunUnit — the self-contained read shape of a run.
3
+ *
4
+ * One canonical struct that captures every non-secret artifact persisted
5
+ * for a single run: parsed submission inputs, status/lifecycle, attempts,
6
+ * indexed events, raw-event Storage manifest, outputs (+ capture
7
+ * failures), proxy-call audit log, pinned workspace skills, provider
8
+ * built-in skills, and transient (Anthropic Files) skill records.
9
+ *
10
+ * Wire contract for `GET /api/runs/:runId`, the per-run archive's
11
+ * `run.json`/`submission.json`/`caps.json`, and the SDK/CLI
12
+ * `client.runs.get(runId)` return type.
13
+ *
14
+ * Immutability: every field here is read-only. Edit endpoints do not
15
+ * exist by design — see `references/architecture-decisions.md` (Runs).
16
+ *
17
+ * Raw event payloads are not embedded in this struct. They live in
18
+ * private Supabase Storage as gzipped JSONL pages and are listed via
19
+ * `rawEventPages` (manifest only; bytes downloaded out-of-band so the
20
+ * detail response stays bounded). The archive zip carries the bytes.
21
+ */
22
+ import type { JsonValue, PlatformCleanupPolicy, PlatformFlatSubmission, PlatformProxyEndpoint, PlatformTemplateSubmission } from "./submission.js";
23
+ /**
24
+ * Parsed view of `runs.template_snapshot` jsonb. Two historical shapes
25
+ * coexist in storage:
26
+ *
27
+ * - `kind: "flat"` (current): {kind:"flat", submission, cleanup?}
28
+ * written by `insertRunWithSkillSnapshots` for all new runs.
29
+ * - legacy template-shaped: {template, executionPayloadSecretId,
30
+ * variables?, cleanup?} written by `insertRun` before the flat
31
+ * pivot.
32
+ *
33
+ * The parser tolerates both and returns a discriminated union so
34
+ * consumers branch mechanically. New work targets `kind: "flat"`.
35
+ *
36
+ * Note on legacy redaction: historic template-shaped snapshots have
37
+ * `system` and `messages` replaced with `"[redacted]"` by
38
+ * `redactTemplateForMetadata`. That redaction predates the raw-event /
39
+ * raw-input policy update and we do not retroactively un-redact data we
40
+ * never stored. Flat-shape snapshots are verbatim.
41
+ */
42
+ export type RunUnitSubmission = RunUnitFlatSubmission | RunUnitTemplateSubmission;
43
+ export interface RunUnitFlatSubmission {
44
+ readonly kind: "flat";
45
+ readonly submission: PlatformFlatSubmission;
46
+ readonly cleanup?: PlatformCleanupPolicy;
47
+ }
48
+ export interface RunUnitTemplateSubmission {
49
+ readonly kind: "template";
50
+ readonly template: PlatformTemplateSubmission;
51
+ readonly variables?: Record<string, JsonValue>;
52
+ readonly cleanup?: PlatformCleanupPolicy;
53
+ }
54
+ export interface RunUnitAttempt {
55
+ readonly id: string;
56
+ readonly attemptNumber: number;
57
+ readonly status: string;
58
+ readonly providerSessionId?: string;
59
+ readonly errorClass?: string;
60
+ readonly errorCode?: string;
61
+ readonly errorMessage?: string;
62
+ readonly startedAt?: string;
63
+ readonly terminalAt?: string;
64
+ readonly createdAt: string;
65
+ }
66
+ /**
67
+ * Indexed event metadata (dedupe-key + summary). Raw payload bytes for
68
+ * each event are NOT here — they ride in `rawEventPages`. This struct
69
+ * stays small so the detail response is bounded.
70
+ */
71
+ export interface RunUnitEvent {
72
+ readonly id: string;
73
+ readonly attemptId?: string;
74
+ readonly providerEventId?: string;
75
+ readonly type: string;
76
+ readonly summary?: string;
77
+ readonly occurredAt?: string;
78
+ readonly processedAt: string;
79
+ readonly usageDelta?: Record<string, JsonValue>;
80
+ }
81
+ /**
82
+ * Inline slice of events plus an optional cursor for the tail. Most
83
+ * runs fit entirely inline; long-running ones overflow and the
84
+ * consumer paginates via `GET /api/runs/:runId/events?cursor=...`.
85
+ */
86
+ export interface RunUnitEventPage {
87
+ readonly entries: readonly RunUnitEvent[];
88
+ readonly totalCount: number;
89
+ readonly truncated: boolean;
90
+ readonly nextCursor?: string;
91
+ }
92
+ /**
93
+ * One gzipped JSONL page of raw provider events captured to Storage.
94
+ * Bytes are downloaded via signed URL or surfaced inside the per-run
95
+ * archive zip. `storagePath` is bucket-relative; the BFF turns it
96
+ * into a signed URL for clients.
97
+ */
98
+ export interface RunUnitRawEventPage {
99
+ readonly attempt: number;
100
+ readonly page: number;
101
+ readonly byteSize: number;
102
+ readonly eventCount: number;
103
+ readonly storagePath: string;
104
+ readonly contentEncoding: "gzip";
105
+ readonly createdAt: string;
106
+ }
107
+ export interface RunUnitOutput {
108
+ readonly id: string;
109
+ readonly fileName: string;
110
+ readonly byteSize: number;
111
+ readonly contentType?: string;
112
+ }
113
+ export interface RunUnitOutputCaptureFailure {
114
+ readonly id: string;
115
+ readonly providerFileId?: string;
116
+ readonly filename?: string;
117
+ readonly byteSize?: number;
118
+ readonly reason: string;
119
+ readonly errorMessage?: string;
120
+ readonly createdAt: string;
121
+ }
122
+ export interface RunUnitProxyCall {
123
+ readonly id: string;
124
+ readonly endpointName: string;
125
+ readonly method: string;
126
+ readonly requestPathRedacted: string | null;
127
+ readonly requestByteSize: number;
128
+ readonly responseStatus: number | null;
129
+ readonly responseByteSize: number;
130
+ readonly outcome: string;
131
+ readonly errorClass: string | null;
132
+ readonly startedAt: string;
133
+ readonly finishedAt: string | null;
134
+ readonly durationMs: number | null;
135
+ }
136
+ export interface RunUnitProxyCallPage {
137
+ readonly entries: readonly RunUnitProxyCall[];
138
+ readonly totalCount: number;
139
+ readonly truncated: boolean;
140
+ readonly nextCursor?: string;
141
+ }
142
+ /**
143
+ * Workspace skill bundle pinned at submission. `liveSkillId` is `null`
144
+ * when the corresponding `skill_bundles` row has been soft-deleted —
145
+ * the UI uses that to render a tombstoned link.
146
+ */
147
+ export interface RunUnitSkillSnapshot {
148
+ readonly skillId: string;
149
+ readonly name: string;
150
+ readonly hash: string;
151
+ readonly sizeBytes: number;
152
+ readonly fileCount: number;
153
+ readonly liveSkillId: string | null;
154
+ }
155
+ export interface RunUnitProviderSkill {
156
+ readonly vendor: string;
157
+ readonly skillId: string;
158
+ readonly version?: string;
159
+ }
160
+ export interface RunUnitTransientSkill {
161
+ readonly id: string;
162
+ readonly slotId: string;
163
+ readonly skillName: string;
164
+ readonly contentHash: string;
165
+ readonly anthropicFileId: string | null;
166
+ readonly status: string;
167
+ readonly createdAt: string;
168
+ readonly updatedAt: string;
169
+ }
170
+ export interface RunUnit {
171
+ readonly id: string;
172
+ readonly workspaceId: string;
173
+ readonly status: string;
174
+ readonly lifecyclePhase?: string;
175
+ readonly cleanupStatus: string;
176
+ readonly createdAt: string;
177
+ readonly updatedAt: string;
178
+ readonly startedAt?: string;
179
+ readonly terminalAt?: string;
180
+ readonly deletedAt?: string;
181
+ readonly attemptCount: number;
182
+ readonly submission: RunUnitSubmission;
183
+ readonly capsSnapshot?: Record<string, JsonValue>;
184
+ readonly proxyEndpointsSnapshot?: readonly PlatformProxyEndpoint[];
185
+ readonly attempts: readonly RunUnitAttempt[];
186
+ readonly events: RunUnitEventPage;
187
+ readonly rawEventPages: readonly RunUnitRawEventPage[];
188
+ readonly outputs: readonly RunUnitOutput[];
189
+ readonly outputCaptureFailures: readonly RunUnitOutputCaptureFailure[];
190
+ readonly proxyCalls: RunUnitProxyCallPage;
191
+ readonly skillSnapshots: readonly RunUnitSkillSnapshot[];
192
+ readonly providerSkills: readonly RunUnitProviderSkill[];
193
+ readonly transientSkills: readonly RunUnitTransientSkill[];
194
+ }
195
+ /**
196
+ * Parse a `runs.template_snapshot` jsonb payload into the typed
197
+ * discriminated union. Tolerates both flat and legacy template shapes;
198
+ * never throws on minor unknown keys so we can forward-compat with
199
+ * worker-side enrichment.
200
+ *
201
+ * Returns a typed shape even for malformed snapshots — the worst case
202
+ * is `{kind: "flat", submission: {model: "", ...}}` with empty defaults
203
+ * — because the dashboard must still render *something* for a buggy
204
+ * historical row rather than 500ing the whole detail page.
205
+ */
206
+ export declare function parseRunUnitSubmission(input: unknown): RunUnitSubmission;
@@ -0,0 +1,213 @@
1
+ /**
2
+ * RunUnit — the self-contained read shape of a run.
3
+ *
4
+ * One canonical struct that captures every non-secret artifact persisted
5
+ * for a single run: parsed submission inputs, status/lifecycle, attempts,
6
+ * indexed events, raw-event Storage manifest, outputs (+ capture
7
+ * failures), proxy-call audit log, pinned workspace skills, provider
8
+ * built-in skills, and transient (Anthropic Files) skill records.
9
+ *
10
+ * Wire contract for `GET /api/runs/:runId`, the per-run archive's
11
+ * `run.json`/`submission.json`/`caps.json`, and the SDK/CLI
12
+ * `client.runs.get(runId)` return type.
13
+ *
14
+ * Immutability: every field here is read-only. Edit endpoints do not
15
+ * exist by design — see `references/architecture-decisions.md` (Runs).
16
+ *
17
+ * Raw event payloads are not embedded in this struct. They live in
18
+ * private Supabase Storage as gzipped JSONL pages and are listed via
19
+ * `rawEventPages` (manifest only; bytes downloaded out-of-band so the
20
+ * detail response stays bounded). The archive zip carries the bytes.
21
+ */
22
+ import { parseMcpServerRef, parseSkillRef } from "./blueprint.js";
23
+ // ---------------------------------------------------------------------------
24
+ // Submission parser
25
+ // ---------------------------------------------------------------------------
26
+ /**
27
+ * Parse a `runs.template_snapshot` jsonb payload into the typed
28
+ * discriminated union. Tolerates both flat and legacy template shapes;
29
+ * never throws on minor unknown keys so we can forward-compat with
30
+ * worker-side enrichment.
31
+ *
32
+ * Returns a typed shape even for malformed snapshots — the worst case
33
+ * is `{kind: "flat", submission: {model: "", ...}}` with empty defaults
34
+ * — because the dashboard must still render *something* for a buggy
35
+ * historical row rather than 500ing the whole detail page.
36
+ */
37
+ export function parseRunUnitSubmission(input) {
38
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
39
+ return fallbackFlat();
40
+ }
41
+ const value = input;
42
+ if (value.kind === "flat") {
43
+ return parseFlatProjection(value);
44
+ }
45
+ if (isRecord(value.template)) {
46
+ return parseTemplateProjection(value);
47
+ }
48
+ // Snapshot exists but matches neither shape — surface as an empty
49
+ // flat submission so consumers can still render lifecycle bits.
50
+ return fallbackFlat();
51
+ }
52
+ function parseFlatProjection(value) {
53
+ const submissionRaw = isRecord(value.submission) ? value.submission : {};
54
+ const cleanup = parseCleanup(value.cleanup);
55
+ const submission = {
56
+ model: typeof submissionRaw.model === "string" ? submissionRaw.model : "",
57
+ ...(typeof submissionRaw.system === "string" ? { system: submissionRaw.system } : {}),
58
+ prompt: toStringArray(submissionRaw.prompt),
59
+ skills: toSkillRefArray(submissionRaw.skills),
60
+ mcpServers: toMcpServerRefArray(submissionRaw.mcpServers),
61
+ ...(parseEnvironment(submissionRaw.environment)
62
+ ? { environment: parseEnvironment(submissionRaw.environment) }
63
+ : {}),
64
+ ...(isJsonRecord(submissionRaw.metadata) ? { metadata: submissionRaw.metadata } : {}),
65
+ ...(toOptionalStringArray(submissionRaw.outputDirs)
66
+ ? { outputDirs: toOptionalStringArray(submissionRaw.outputDirs) }
67
+ : {})
68
+ };
69
+ return {
70
+ kind: "flat",
71
+ submission,
72
+ ...(cleanup ? { cleanup } : {})
73
+ };
74
+ }
75
+ function parseTemplateProjection(value) {
76
+ const templateRaw = value.template;
77
+ const template = {
78
+ name: typeof templateRaw.name === "string" ? templateRaw.name : "",
79
+ model: typeof templateRaw.model === "string" ? templateRaw.model : "",
80
+ templateHash: typeof templateRaw.templateHash === "string" ? templateRaw.templateHash : "",
81
+ messages: toStringArray(templateRaw.messages),
82
+ ...(typeof templateRaw.system === "string" ? { system: templateRaw.system } : {}),
83
+ ...(isJsonRecord(templateRaw.metadata) ? { metadata: templateRaw.metadata } : {}),
84
+ ...(parseEnvironment(templateRaw.environment)
85
+ ? { environment: parseEnvironment(templateRaw.environment) }
86
+ : {})
87
+ };
88
+ const variables = isJsonRecord(value.variables) ? value.variables : undefined;
89
+ const cleanup = parseCleanup(value.cleanup);
90
+ return {
91
+ kind: "template",
92
+ template,
93
+ ...(variables ? { variables } : {}),
94
+ ...(cleanup ? { cleanup } : {})
95
+ };
96
+ }
97
+ function fallbackFlat() {
98
+ return {
99
+ kind: "flat",
100
+ submission: {
101
+ model: "",
102
+ prompt: [],
103
+ skills: [],
104
+ mcpServers: []
105
+ }
106
+ };
107
+ }
108
+ // ---------------------------------------------------------------------------
109
+ // Coercion helpers — deliberately lenient. We never throw on a single
110
+ // malformed sub-field; we collapse it to a safe default and keep going
111
+ // so a dashboard read of a malformed snapshot still surfaces the rest.
112
+ // ---------------------------------------------------------------------------
113
+ function isRecord(value) {
114
+ return typeof value === "object" && value !== null && !Array.isArray(value);
115
+ }
116
+ function isJsonRecord(value) {
117
+ return isRecord(value);
118
+ }
119
+ function toStringArray(value) {
120
+ if (!Array.isArray(value)) {
121
+ return [];
122
+ }
123
+ return value.filter((item) => typeof item === "string");
124
+ }
125
+ function toOptionalStringArray(value) {
126
+ if (!Array.isArray(value) || value.length === 0) {
127
+ return undefined;
128
+ }
129
+ const filtered = value.filter((item) => typeof item === "string");
130
+ return filtered.length === 0 ? undefined : filtered;
131
+ }
132
+ function toSkillRefArray(value) {
133
+ if (!Array.isArray(value)) {
134
+ return [];
135
+ }
136
+ const out = [];
137
+ for (let i = 0; i < value.length; i++) {
138
+ try {
139
+ out.push(parseSkillRef(value[i], `submission.skills[${i}]`, { allowTransient: true }));
140
+ }
141
+ catch {
142
+ // Skip malformed entries rather than failing the whole detail
143
+ // read. Worker-side enrichment may add fields we don't recognise.
144
+ }
145
+ }
146
+ return out;
147
+ }
148
+ function toMcpServerRefArray(value) {
149
+ if (!Array.isArray(value)) {
150
+ return [];
151
+ }
152
+ const out = [];
153
+ for (let i = 0; i < value.length; i++) {
154
+ try {
155
+ out.push(parseMcpServerRef(value[i], `submission.mcpServers[${i}]`));
156
+ }
157
+ catch {
158
+ // ignore malformed
159
+ }
160
+ }
161
+ return out;
162
+ }
163
+ function parseEnvironment(value) {
164
+ if (!isRecord(value)) {
165
+ return undefined;
166
+ }
167
+ const env = {};
168
+ if (isRecord(value.networking)) {
169
+ const mode = value.networking.mode;
170
+ const allowedHosts = value.networking.allowedHosts;
171
+ if (mode === "limited" || mode === "open") {
172
+ env.networking = {
173
+ mode,
174
+ ...(Array.isArray(allowedHosts)
175
+ ? { allowedHosts: toStringArray(allowedHosts) }
176
+ : {})
177
+ };
178
+ }
179
+ }
180
+ if (Array.isArray(value.packages)) {
181
+ const pkgs = value.packages
182
+ .filter(isRecord)
183
+ .map((p) => {
184
+ const r = p;
185
+ return typeof r.name === "string"
186
+ ? { name: r.name, ...(typeof r.version === "string" ? { version: r.version } : {}) }
187
+ : null;
188
+ })
189
+ .filter((p) => p !== null);
190
+ if (pkgs.length > 0) {
191
+ env.packages = pkgs;
192
+ }
193
+ }
194
+ return env.networking || env.packages
195
+ ? env
196
+ : undefined;
197
+ }
198
+ function parseCleanup(value) {
199
+ if (!isRecord(value)) {
200
+ return undefined;
201
+ }
202
+ const session = value.session;
203
+ const claudeSession = value.claudeSession;
204
+ const out = {};
205
+ if (session === "retain" || session === "delete") {
206
+ out.session = session;
207
+ }
208
+ if (claudeSession === "retain" || claudeSession === "delete") {
209
+ out.claudeSession = claudeSession;
210
+ }
211
+ return out.session || out.claudeSession ? out : undefined;
212
+ }
213
+ //# sourceMappingURL=run-unit.js.map
@@ -71,6 +71,25 @@ export interface WhoAmI {
71
71
  readonly tokenId?: string;
72
72
  readonly tokenName?: string | null;
73
73
  readonly scopes?: readonly string[];
74
+ /**
75
+ * Workspace-level caps the BFF will enforce on subsequent calls.
76
+ * Surfaced so consumers (e.g. broll's app-side admission gate) can
77
+ * decide whether to keep their own gate or rely on platform headers.
78
+ * All fields optional — older BFFs may omit. Numbers are concrete
79
+ * snapshots at the time of the `whoami` call.
80
+ */
81
+ readonly caps?: {
82
+ /** Token-bucket cap on POST /api/runs per minute, per workspace. */
83
+ readonly runSubmitPerMinute?: number;
84
+ /** Hard cap on concurrent non-terminal runs the workspace may hold. */
85
+ readonly maxConcurrentRuns?: number;
86
+ /** Storage cap (bytes) on captured output objects, workspace-wide. */
87
+ readonly storageCapBytes?: number;
88
+ /** Current captured-output usage in bytes. */
89
+ readonly storageUsedBytes?: number;
90
+ /** Wall-clock ceiling on a single run before forced termination. */
91
+ readonly maxRunDurationMs?: number;
92
+ };
74
93
  readonly [key: string]: unknown;
75
94
  }
76
95
  /**