antpath 0.4.0 → 0.4.2
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/dist/_shared/blueprint.d.ts +263 -0
- package/dist/_shared/blueprint.js +512 -0
- package/dist/_shared/http.js +7 -1
- package/dist/_shared/index.d.ts +1 -0
- package/dist/_shared/index.js +1 -0
- package/dist/_shared/operations.d.ts +20 -2
- package/dist/_shared/operations.js +56 -0
- package/dist/_shared/runtime-types.d.ts +30 -0
- package/dist/_shared/submission.d.ts +40 -0
- package/dist/_shared/submission.js +153 -1
- package/dist/cli.mjs +68 -1
- package/dist/cli.mjs.sha256 +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The flat agent-first composition surface that replaces `Template`.
|
|
3
|
+
*
|
|
4
|
+
* Concepts (mirrored in references/architecture-decisions.md):
|
|
5
|
+
*
|
|
6
|
+
* - `SkillRef` is the wire-level reference to a skill — either an
|
|
7
|
+
* `skl_*` id pointing at a workspace-uploaded bundle, or a
|
|
8
|
+
* `{vendor, skillId, version}` reference to a provider built-in.
|
|
9
|
+
* The two shapes are discriminated by `kind` so consumers branch
|
|
10
|
+
* mechanically and providers can never accidentally be looked up
|
|
11
|
+
* in `skill_bundles`.
|
|
12
|
+
*
|
|
13
|
+
* - `McpServerRef` is the non-secret part of an MCP server declaration:
|
|
14
|
+
* `name` and `url`. Bearer / cookie / per-request headers travel in
|
|
15
|
+
* the run's vaulted `secrets.mcpServers` block keyed by the same
|
|
16
|
+
* `name`, and never enter the hashed submission payload or the
|
|
17
|
+
* run snapshot.
|
|
18
|
+
*
|
|
19
|
+
* - `Blueprint` is what the user authors. It excludes
|
|
20
|
+
* `secrets`/`idempotencyKey`/`signal` so it can be safely persisted
|
|
21
|
+
* to disk (e.g. `antpath run --config run.json`), shared between
|
|
22
|
+
* teams, or curried via `defineRun` without leaking credentials.
|
|
23
|
+
* Strings inside a Blueprint are **already resolved** — there are
|
|
24
|
+
* no `{{variable}}` placeholders, no template language, no late
|
|
25
|
+
* binding. The whole point of `defineRun` is to make the resolution
|
|
26
|
+
* happen at the TS call site where the IDE can type-check it.
|
|
27
|
+
*
|
|
28
|
+
* - Skill bundle validation lives here so the SDK (zipping locally),
|
|
29
|
+
* the BFF (server-side unzip + manifest extraction), and the worker
|
|
30
|
+
* (sanity-check before mounting) share a single source of truth for
|
|
31
|
+
* the limits, the path normaliser, and the manifest invariants. The
|
|
32
|
+
* DB CHECK constraints on `skill_bundles.manifest` mirror these.
|
|
33
|
+
*
|
|
34
|
+
* See `references/architecture-decisions.md` (Composition primitives,
|
|
35
|
+
* Skill custody) and `references/development-principles.md` (Agent-first
|
|
36
|
+
* surface design) for the rationale.
|
|
37
|
+
*/
|
|
38
|
+
import type { JsonValue, PlatformCleanupPolicy, PlatformProxyEndpoint, PlatformTemplateEnvironment } from "./submission.js";
|
|
39
|
+
/**
|
|
40
|
+
* Mirrors the CHECK constraint
|
|
41
|
+
* `skill_bundles_id_format_chk = check (id ~ '^skl_[A-Za-z0-9_-]{8,128}$')`
|
|
42
|
+
* defined in supabase/migrations/20260512000000_skill_bundles.sql. Keep
|
|
43
|
+
* the two in lockstep — the DB is the ultimate authority.
|
|
44
|
+
*/
|
|
45
|
+
export declare const SKILL_ID_PATTERN: RegExp;
|
|
46
|
+
/**
|
|
47
|
+
* Human-readable, workspace-scoped name. Lowercase, kebab-friendly,
|
|
48
|
+
* 1..128 chars. The DB enforces the length bound via
|
|
49
|
+
* `skill_bundles_name_len_chk`; this regex tightens the SDK/CLI input
|
|
50
|
+
* surface so callers fail at the boundary rather than in the BFF.
|
|
51
|
+
*/
|
|
52
|
+
export declare const SKILL_NAME_PATTERN: RegExp;
|
|
53
|
+
/**
|
|
54
|
+
* Hard caps applied at upload time. The SDK enforces these before
|
|
55
|
+
* computing the zip hash so a clearly-too-big bundle never wastes
|
|
56
|
+
* bytes-on-the-wire; the BFF re-enforces server-side because the SDK
|
|
57
|
+
* is untrusted. Numbers are deliberately conservative for the MVP and
|
|
58
|
+
* can be tuned later; keep this object as the single tuning point.
|
|
59
|
+
*/
|
|
60
|
+
export declare const SKILL_BUNDLE_LIMITS: {
|
|
61
|
+
/** Compressed (.zip) ceiling. */
|
|
62
|
+
readonly maxCompressedBytes: number;
|
|
63
|
+
/** Sum of uncompressed file sizes. */
|
|
64
|
+
readonly maxDecompressedBytes: number;
|
|
65
|
+
/** Number of regular file entries (directories don't count). */
|
|
66
|
+
readonly maxFiles: 1000;
|
|
67
|
+
/** Maximum directory nesting depth — `a/b/c/d` has depth 4. */
|
|
68
|
+
readonly maxDepth: 16;
|
|
69
|
+
/** Single-entry path length cap. */
|
|
70
|
+
readonly maxPathLength: 512;
|
|
71
|
+
/** Stored file mode for ordinary files. */
|
|
72
|
+
readonly defaultFileMode: 420;
|
|
73
|
+
/** Stored directory mode. */
|
|
74
|
+
readonly defaultDirMode: 493;
|
|
75
|
+
};
|
|
76
|
+
export type SkillRef = WorkspaceSkillRef | ProviderSkillRef;
|
|
77
|
+
export interface WorkspaceSkillRef {
|
|
78
|
+
readonly kind: "workspace";
|
|
79
|
+
readonly id: string;
|
|
80
|
+
}
|
|
81
|
+
export interface ProviderSkillRef {
|
|
82
|
+
readonly kind: "provider";
|
|
83
|
+
readonly vendor: "anthropic" | "custom";
|
|
84
|
+
readonly skillId: string;
|
|
85
|
+
readonly version?: string;
|
|
86
|
+
}
|
|
87
|
+
export declare function isWorkspaceSkillRef(ref: SkillRef): ref is WorkspaceSkillRef;
|
|
88
|
+
export declare function isProviderSkillRef(ref: SkillRef): ref is ProviderSkillRef;
|
|
89
|
+
/**
|
|
90
|
+
* Parse a `SkillRef` from untrusted input. Used by the BFF run parser
|
|
91
|
+
* and by the operations module when deserialising API responses. Throws
|
|
92
|
+
* with a precise path so the caller can surface a usable error.
|
|
93
|
+
*/
|
|
94
|
+
export declare function parseSkillRef(input: unknown, path: string): SkillRef;
|
|
95
|
+
/**
|
|
96
|
+
* Manifest entry persisted in `skill_bundles.manifest` and
|
|
97
|
+
* `run_skill_snapshots.manifest`. `path` is forward-slash, relative,
|
|
98
|
+
* normalised. `mode` is the stored POSIX mode (sanitised, NOT the user's
|
|
99
|
+
* filesystem mode) — see `SKILL_BUNDLE_LIMITS.defaultFileMode`.
|
|
100
|
+
*/
|
|
101
|
+
export interface SkillBundleEntry {
|
|
102
|
+
readonly path: string;
|
|
103
|
+
readonly size: number;
|
|
104
|
+
readonly mode: number;
|
|
105
|
+
}
|
|
106
|
+
export interface SkillBundleManifest {
|
|
107
|
+
readonly entries: readonly SkillBundleEntry[];
|
|
108
|
+
/** Total uncompressed bytes (sum of `entries[i].size`). */
|
|
109
|
+
readonly totalSize: number;
|
|
110
|
+
/** Number of file entries. Equals `entries.length` by construction. */
|
|
111
|
+
readonly fileCount: number;
|
|
112
|
+
}
|
|
113
|
+
export declare class SkillBundleValidationError extends Error {
|
|
114
|
+
constructor(message: string);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Reject input paths that try to escape the bundle root or smuggle
|
|
118
|
+
* platform-specific syntax. Returns the canonical forward-slash
|
|
119
|
+
* relative path; never returns paths starting or ending with `/`.
|
|
120
|
+
*
|
|
121
|
+
* Rejects:
|
|
122
|
+
* - empty strings and pure whitespace
|
|
123
|
+
* - absolute paths (`/foo`, `C:\foo`, `\\server\share`)
|
|
124
|
+
* - backslash separators (Windows)
|
|
125
|
+
* - `..` segments anywhere in the path
|
|
126
|
+
* - `.` segments anywhere except a leading bare `.`
|
|
127
|
+
* - paths whose length exceeds `SKILL_BUNDLE_LIMITS.maxPathLength`
|
|
128
|
+
* - paths whose depth exceeds `SKILL_BUNDLE_LIMITS.maxDepth`
|
|
129
|
+
* - NUL bytes
|
|
130
|
+
*/
|
|
131
|
+
export declare function normaliseSkillBundlePath(input: string): string;
|
|
132
|
+
/**
|
|
133
|
+
* Validate one manifest entry: normalises the path, bounds the size,
|
|
134
|
+
* and sanitises the mode to one of {defaultFileMode, defaultDirMode}.
|
|
135
|
+
* The bundle is files-only, so any non-regular-file entry is rejected
|
|
136
|
+
* upstream by the caller (zip parser must skip symlinks, device files,
|
|
137
|
+
* etc. before reaching this function).
|
|
138
|
+
*/
|
|
139
|
+
export declare function validateSkillBundleEntry(input: {
|
|
140
|
+
readonly path: string;
|
|
141
|
+
readonly size: number;
|
|
142
|
+
readonly mode?: number;
|
|
143
|
+
}): SkillBundleEntry;
|
|
144
|
+
/**
|
|
145
|
+
* Validate a full manifest. Enforces:
|
|
146
|
+
* - entries is a non-empty array
|
|
147
|
+
* - `SKILL.md` exists at the bundle root (Claude's auto-discovery key)
|
|
148
|
+
* - file count <= maxFiles
|
|
149
|
+
* - total uncompressed size <= maxDecompressedBytes
|
|
150
|
+
* - per-entry validation (see `validateSkillBundleEntry`)
|
|
151
|
+
* - no duplicate paths
|
|
152
|
+
*
|
|
153
|
+
* Returns a canonical manifest with totals computed.
|
|
154
|
+
*/
|
|
155
|
+
export declare function validateSkillBundleManifest(input: ReadonlyArray<{
|
|
156
|
+
readonly path: string;
|
|
157
|
+
readonly size: number;
|
|
158
|
+
readonly mode?: number;
|
|
159
|
+
}>): SkillBundleManifest;
|
|
160
|
+
/**
|
|
161
|
+
* The non-secret half of an MCP server declaration. This is what enters
|
|
162
|
+
* the hashed submission, the run snapshot, and any audit log. `name`
|
|
163
|
+
* keys into `secrets.mcpServers` for the per-request headers.
|
|
164
|
+
*/
|
|
165
|
+
export interface McpServerRef {
|
|
166
|
+
readonly name: string;
|
|
167
|
+
readonly url: string;
|
|
168
|
+
}
|
|
169
|
+
export declare const MCP_SERVER_NAME_PATTERN: RegExp;
|
|
170
|
+
/**
|
|
171
|
+
* A Blueprint-level MCP entry. The user is free to supply headers
|
|
172
|
+
* inline; the SDK splits the call site cleanly at submission time so
|
|
173
|
+
* the Authorization (or other auth-bearing) header never enters the
|
|
174
|
+
* non-secret wire payload.
|
|
175
|
+
*/
|
|
176
|
+
export interface BlueprintMcpServer extends McpServerRef {
|
|
177
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
178
|
+
}
|
|
179
|
+
export declare function parseMcpServerRef(input: unknown, path: string): McpServerRef;
|
|
180
|
+
/**
|
|
181
|
+
* What the user authors and passes to `client.submitRun({...blueprint, secrets})`.
|
|
182
|
+
*
|
|
183
|
+
* Blueprint deliberately EXCLUDES `secrets`, `idempotencyKey`, and
|
|
184
|
+
* `signal` so:
|
|
185
|
+
* - `defineRun((p) => Blueprint)` can be reused across calls without
|
|
186
|
+
* re-injecting an API key,
|
|
187
|
+
* - a Blueprint can be JSON-serialised to disk (`antpath run --config
|
|
188
|
+
* run.json`) without ever encoding a credential,
|
|
189
|
+
* - audit / replay tooling can persist a Blueprint as-is.
|
|
190
|
+
*/
|
|
191
|
+
export interface Blueprint {
|
|
192
|
+
readonly model: string;
|
|
193
|
+
readonly system?: string;
|
|
194
|
+
readonly prompt: string | readonly string[];
|
|
195
|
+
readonly skills?: readonly SkillRef[];
|
|
196
|
+
readonly mcpServers?: readonly BlueprintMcpServer[];
|
|
197
|
+
readonly environment?: PlatformTemplateEnvironment;
|
|
198
|
+
readonly cleanup?: PlatformCleanupPolicy;
|
|
199
|
+
readonly proxyEndpoints?: readonly PlatformProxyEndpoint[];
|
|
200
|
+
readonly metadata?: Readonly<Record<string, JsonValue>>;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Currier for parameterised Blueprints.
|
|
204
|
+
*
|
|
205
|
+
* ```ts
|
|
206
|
+
* const investigate = defineRun((p: { repo: string; issue: number }) => ({
|
|
207
|
+
* model: "claude-sonnet-4-5-20250929",
|
|
208
|
+
* system: `You work on ${p.repo}.`,
|
|
209
|
+
* prompt: `Investigate issue #${p.issue}.`,
|
|
210
|
+
* skills: [rules],
|
|
211
|
+
* }));
|
|
212
|
+
* await client.submitRun({
|
|
213
|
+
* ...investigate({ repo: "antpath", issue: 123 }),
|
|
214
|
+
* secrets: { anthropic: { apiKey } },
|
|
215
|
+
* });
|
|
216
|
+
* ```
|
|
217
|
+
*
|
|
218
|
+
* The returned function is referentially transparent — it just calls
|
|
219
|
+
* the provided producer. The wrapper exists for two reasons: (a) a
|
|
220
|
+
* single named entry point makes IDEs surface the type of the inner
|
|
221
|
+
* blueprint at the call site, and (b) it pins the "no late binding"
|
|
222
|
+
* contract — strings inside the Blueprint are resolved by the TS call
|
|
223
|
+
* site, not by a server-side template engine.
|
|
224
|
+
*/
|
|
225
|
+
export declare function defineRun<TParams>(producer: (params: TParams) => Blueprint): (params: TParams) => Blueprint;
|
|
226
|
+
/**
|
|
227
|
+
* Parse a Blueprint from JSON. Defensive — used by the host CLI to
|
|
228
|
+
* load `--config run.json`. Throws with the JSON path that failed so
|
|
229
|
+
* a user can fix their file. Headers are preserved here and split out
|
|
230
|
+
* later by the SDK normalisation step.
|
|
231
|
+
*/
|
|
232
|
+
export declare function parseBlueprint(input: unknown): Blueprint;
|
|
233
|
+
/**
|
|
234
|
+
* Result of splitting a `Blueprint` into the non-secret submission and
|
|
235
|
+
* the secret MCP-headers bundle. The SDK calls this just before posting
|
|
236
|
+
* to /api/runs: the `submission` half is what the BFF hashes for
|
|
237
|
+
* idempotency, the `mcpServerSecrets` half is what enters the Vault.
|
|
238
|
+
*
|
|
239
|
+
* `prompt` is normalised to `readonly string[]` (single-string callers
|
|
240
|
+
* get wrapped in a length-1 array) so the wire payload, the worker, and
|
|
241
|
+
* the audit log don't have to re-handle two shapes.
|
|
242
|
+
*/
|
|
243
|
+
export interface NormalisedBlueprint {
|
|
244
|
+
readonly model: string;
|
|
245
|
+
readonly system?: string;
|
|
246
|
+
readonly prompt: readonly string[];
|
|
247
|
+
readonly skills: readonly SkillRef[];
|
|
248
|
+
readonly mcpServers: readonly McpServerRef[];
|
|
249
|
+
readonly environment?: PlatformTemplateEnvironment;
|
|
250
|
+
readonly cleanup?: PlatformCleanupPolicy;
|
|
251
|
+
readonly proxyEndpoints?: readonly PlatformProxyEndpoint[];
|
|
252
|
+
readonly metadata?: Readonly<Record<string, JsonValue>>;
|
|
253
|
+
/**
|
|
254
|
+
* MCP servers whose Blueprint entry carried `headers`. Keyed by the
|
|
255
|
+
* `name` that appears in `mcpServers` so the BFF can pair them up.
|
|
256
|
+
*/
|
|
257
|
+
readonly mcpServerSecrets: ReadonlyArray<{
|
|
258
|
+
readonly name: string;
|
|
259
|
+
readonly url: string;
|
|
260
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
261
|
+
}>;
|
|
262
|
+
}
|
|
263
|
+
export declare function normaliseBlueprint(blueprint: Blueprint): NormalisedBlueprint;
|
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The flat agent-first composition surface that replaces `Template`.
|
|
3
|
+
*
|
|
4
|
+
* Concepts (mirrored in references/architecture-decisions.md):
|
|
5
|
+
*
|
|
6
|
+
* - `SkillRef` is the wire-level reference to a skill — either an
|
|
7
|
+
* `skl_*` id pointing at a workspace-uploaded bundle, or a
|
|
8
|
+
* `{vendor, skillId, version}` reference to a provider built-in.
|
|
9
|
+
* The two shapes are discriminated by `kind` so consumers branch
|
|
10
|
+
* mechanically and providers can never accidentally be looked up
|
|
11
|
+
* in `skill_bundles`.
|
|
12
|
+
*
|
|
13
|
+
* - `McpServerRef` is the non-secret part of an MCP server declaration:
|
|
14
|
+
* `name` and `url`. Bearer / cookie / per-request headers travel in
|
|
15
|
+
* the run's vaulted `secrets.mcpServers` block keyed by the same
|
|
16
|
+
* `name`, and never enter the hashed submission payload or the
|
|
17
|
+
* run snapshot.
|
|
18
|
+
*
|
|
19
|
+
* - `Blueprint` is what the user authors. It excludes
|
|
20
|
+
* `secrets`/`idempotencyKey`/`signal` so it can be safely persisted
|
|
21
|
+
* to disk (e.g. `antpath run --config run.json`), shared between
|
|
22
|
+
* teams, or curried via `defineRun` without leaking credentials.
|
|
23
|
+
* Strings inside a Blueprint are **already resolved** — there are
|
|
24
|
+
* no `{{variable}}` placeholders, no template language, no late
|
|
25
|
+
* binding. The whole point of `defineRun` is to make the resolution
|
|
26
|
+
* happen at the TS call site where the IDE can type-check it.
|
|
27
|
+
*
|
|
28
|
+
* - Skill bundle validation lives here so the SDK (zipping locally),
|
|
29
|
+
* the BFF (server-side unzip + manifest extraction), and the worker
|
|
30
|
+
* (sanity-check before mounting) share a single source of truth for
|
|
31
|
+
* the limits, the path normaliser, and the manifest invariants. The
|
|
32
|
+
* DB CHECK constraints on `skill_bundles.manifest` mirror these.
|
|
33
|
+
*
|
|
34
|
+
* See `references/architecture-decisions.md` (Composition primitives,
|
|
35
|
+
* Skill custody) and `references/development-principles.md` (Agent-first
|
|
36
|
+
* surface design) for the rationale.
|
|
37
|
+
*/
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Skill ID + name format
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
/**
|
|
42
|
+
* Mirrors the CHECK constraint
|
|
43
|
+
* `skill_bundles_id_format_chk = check (id ~ '^skl_[A-Za-z0-9_-]{8,128}$')`
|
|
44
|
+
* defined in supabase/migrations/20260512000000_skill_bundles.sql. Keep
|
|
45
|
+
* the two in lockstep — the DB is the ultimate authority.
|
|
46
|
+
*/
|
|
47
|
+
export const SKILL_ID_PATTERN = /^skl_[A-Za-z0-9_-]{8,128}$/;
|
|
48
|
+
/**
|
|
49
|
+
* Human-readable, workspace-scoped name. Lowercase, kebab-friendly,
|
|
50
|
+
* 1..128 chars. The DB enforces the length bound via
|
|
51
|
+
* `skill_bundles_name_len_chk`; this regex tightens the SDK/CLI input
|
|
52
|
+
* surface so callers fail at the boundary rather than in the BFF.
|
|
53
|
+
*/
|
|
54
|
+
export const SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{0,127}$/;
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Skill bundle limits (uploaded bundles)
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
/**
|
|
59
|
+
* Hard caps applied at upload time. The SDK enforces these before
|
|
60
|
+
* computing the zip hash so a clearly-too-big bundle never wastes
|
|
61
|
+
* bytes-on-the-wire; the BFF re-enforces server-side because the SDK
|
|
62
|
+
* is untrusted. Numbers are deliberately conservative for the MVP and
|
|
63
|
+
* can be tuned later; keep this object as the single tuning point.
|
|
64
|
+
*/
|
|
65
|
+
export const SKILL_BUNDLE_LIMITS = {
|
|
66
|
+
/** Compressed (.zip) ceiling. */
|
|
67
|
+
maxCompressedBytes: 10 * 1024 * 1024,
|
|
68
|
+
/** Sum of uncompressed file sizes. */
|
|
69
|
+
maxDecompressedBytes: 50 * 1024 * 1024,
|
|
70
|
+
/** Number of regular file entries (directories don't count). */
|
|
71
|
+
maxFiles: 1000,
|
|
72
|
+
/** Maximum directory nesting depth — `a/b/c/d` has depth 4. */
|
|
73
|
+
maxDepth: 16,
|
|
74
|
+
/** Single-entry path length cap. */
|
|
75
|
+
maxPathLength: 512,
|
|
76
|
+
/** Stored file mode for ordinary files. */
|
|
77
|
+
defaultFileMode: 0o644,
|
|
78
|
+
/** Stored directory mode. */
|
|
79
|
+
defaultDirMode: 0o755
|
|
80
|
+
};
|
|
81
|
+
export function isWorkspaceSkillRef(ref) {
|
|
82
|
+
return ref.kind === "workspace";
|
|
83
|
+
}
|
|
84
|
+
export function isProviderSkillRef(ref) {
|
|
85
|
+
return ref.kind === "provider";
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Parse a `SkillRef` from untrusted input. Used by the BFF run parser
|
|
89
|
+
* and by the operations module when deserialising API responses. Throws
|
|
90
|
+
* with a precise path so the caller can surface a usable error.
|
|
91
|
+
*/
|
|
92
|
+
export function parseSkillRef(input, path) {
|
|
93
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
94
|
+
throw new Error(`${path} must be a SkillRef object`);
|
|
95
|
+
}
|
|
96
|
+
const record = input;
|
|
97
|
+
const kind = record.kind;
|
|
98
|
+
if (kind === "workspace") {
|
|
99
|
+
for (const key of Object.keys(record)) {
|
|
100
|
+
if (key !== "kind" && key !== "id") {
|
|
101
|
+
throw new Error(`${path} contains unexpected field for workspace SkillRef: ${key}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const id = record.id;
|
|
105
|
+
if (typeof id !== "string" || !SKILL_ID_PATTERN.test(id)) {
|
|
106
|
+
throw new Error(`${path}.id must match ${SKILL_ID_PATTERN.source}`);
|
|
107
|
+
}
|
|
108
|
+
return { kind: "workspace", id };
|
|
109
|
+
}
|
|
110
|
+
if (kind === "provider") {
|
|
111
|
+
for (const key of Object.keys(record)) {
|
|
112
|
+
if (key !== "kind" && key !== "vendor" && key !== "skillId" && key !== "version") {
|
|
113
|
+
throw new Error(`${path} contains unexpected field for provider SkillRef: ${key}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const vendor = record.vendor;
|
|
117
|
+
if (vendor !== "anthropic" && vendor !== "custom") {
|
|
118
|
+
throw new Error(`${path}.vendor must be 'anthropic' or 'custom'`);
|
|
119
|
+
}
|
|
120
|
+
const skillId = record.skillId;
|
|
121
|
+
if (typeof skillId !== "string" || skillId.length === 0 || skillId.length > 256) {
|
|
122
|
+
throw new Error(`${path}.skillId must be a non-empty string (<= 256 chars)`);
|
|
123
|
+
}
|
|
124
|
+
const version = record.version;
|
|
125
|
+
if (version !== undefined && (typeof version !== "string" || version.length === 0 || version.length > 64)) {
|
|
126
|
+
throw new Error(`${path}.version, when provided, must be a non-empty string (<= 64 chars)`);
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
kind: "provider",
|
|
130
|
+
vendor,
|
|
131
|
+
skillId,
|
|
132
|
+
...(version !== undefined ? { version } : {})
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
throw new Error(`${path}.kind must be 'workspace' or 'provider'`);
|
|
136
|
+
}
|
|
137
|
+
export class SkillBundleValidationError extends Error {
|
|
138
|
+
constructor(message) {
|
|
139
|
+
super(message);
|
|
140
|
+
this.name = "SkillBundleValidationError";
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Reject input paths that try to escape the bundle root or smuggle
|
|
145
|
+
* platform-specific syntax. Returns the canonical forward-slash
|
|
146
|
+
* relative path; never returns paths starting or ending with `/`.
|
|
147
|
+
*
|
|
148
|
+
* Rejects:
|
|
149
|
+
* - empty strings and pure whitespace
|
|
150
|
+
* - absolute paths (`/foo`, `C:\foo`, `\\server\share`)
|
|
151
|
+
* - backslash separators (Windows)
|
|
152
|
+
* - `..` segments anywhere in the path
|
|
153
|
+
* - `.` segments anywhere except a leading bare `.`
|
|
154
|
+
* - paths whose length exceeds `SKILL_BUNDLE_LIMITS.maxPathLength`
|
|
155
|
+
* - paths whose depth exceeds `SKILL_BUNDLE_LIMITS.maxDepth`
|
|
156
|
+
* - NUL bytes
|
|
157
|
+
*/
|
|
158
|
+
export function normaliseSkillBundlePath(input) {
|
|
159
|
+
if (typeof input !== "string") {
|
|
160
|
+
throw new SkillBundleValidationError("bundle entry path must be a string");
|
|
161
|
+
}
|
|
162
|
+
if (input.length === 0 || input.trim().length === 0) {
|
|
163
|
+
throw new SkillBundleValidationError("bundle entry path must be non-empty");
|
|
164
|
+
}
|
|
165
|
+
if (input.length > SKILL_BUNDLE_LIMITS.maxPathLength) {
|
|
166
|
+
throw new SkillBundleValidationError(`bundle entry path exceeds maxPathLength (${SKILL_BUNDLE_LIMITS.maxPathLength}): ${input}`);
|
|
167
|
+
}
|
|
168
|
+
if (input.includes("\0")) {
|
|
169
|
+
throw new SkillBundleValidationError(`bundle entry path contains NUL byte: ${JSON.stringify(input)}`);
|
|
170
|
+
}
|
|
171
|
+
if (input.includes("\\")) {
|
|
172
|
+
throw new SkillBundleValidationError(`bundle entry path uses backslash separator: ${input}`);
|
|
173
|
+
}
|
|
174
|
+
if (/^[A-Za-z]:[\\/]/.test(input)) {
|
|
175
|
+
throw new SkillBundleValidationError(`bundle entry path uses a drive letter: ${input}`);
|
|
176
|
+
}
|
|
177
|
+
if (input.startsWith("/")) {
|
|
178
|
+
throw new SkillBundleValidationError(`bundle entry path must be relative: ${input}`);
|
|
179
|
+
}
|
|
180
|
+
// Reject trailing slash so callers cannot disguise directory entries
|
|
181
|
+
// as files. The manifest is files-only.
|
|
182
|
+
if (input.endsWith("/")) {
|
|
183
|
+
throw new SkillBundleValidationError(`bundle entry path must not end with '/': ${input}`);
|
|
184
|
+
}
|
|
185
|
+
const segments = input.split("/");
|
|
186
|
+
for (const segment of segments) {
|
|
187
|
+
if (segment === "..") {
|
|
188
|
+
throw new SkillBundleValidationError(`bundle entry path contains '..' segment: ${input}`);
|
|
189
|
+
}
|
|
190
|
+
if (segment === "." || segment === "") {
|
|
191
|
+
throw new SkillBundleValidationError(`bundle entry path contains empty or '.' segment: ${input}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (segments.length > SKILL_BUNDLE_LIMITS.maxDepth) {
|
|
195
|
+
throw new SkillBundleValidationError(`bundle entry path exceeds maxDepth (${SKILL_BUNDLE_LIMITS.maxDepth}): ${input}`);
|
|
196
|
+
}
|
|
197
|
+
return input;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Validate one manifest entry: normalises the path, bounds the size,
|
|
201
|
+
* and sanitises the mode to one of {defaultFileMode, defaultDirMode}.
|
|
202
|
+
* The bundle is files-only, so any non-regular-file entry is rejected
|
|
203
|
+
* upstream by the caller (zip parser must skip symlinks, device files,
|
|
204
|
+
* etc. before reaching this function).
|
|
205
|
+
*/
|
|
206
|
+
export function validateSkillBundleEntry(input) {
|
|
207
|
+
const path = normaliseSkillBundlePath(input.path);
|
|
208
|
+
if (!Number.isFinite(input.size) || !Number.isInteger(input.size) || input.size < 0) {
|
|
209
|
+
throw new SkillBundleValidationError(`bundle entry size must be a non-negative integer (${path})`);
|
|
210
|
+
}
|
|
211
|
+
if (input.size > SKILL_BUNDLE_LIMITS.maxDecompressedBytes) {
|
|
212
|
+
throw new SkillBundleValidationError(`bundle entry size exceeds maxDecompressedBytes (${SKILL_BUNDLE_LIMITS.maxDecompressedBytes}): ${path}`);
|
|
213
|
+
}
|
|
214
|
+
// Sanitise the stored mode. Executable bit is implied by runtime
|
|
215
|
+
// convention; we never persist arbitrary chmod from the user's FS.
|
|
216
|
+
const mode = (input.mode ?? SKILL_BUNDLE_LIMITS.defaultFileMode) & 0o777;
|
|
217
|
+
if (mode !== SKILL_BUNDLE_LIMITS.defaultFileMode && mode !== SKILL_BUNDLE_LIMITS.defaultDirMode) {
|
|
218
|
+
return { path, size: input.size, mode: SKILL_BUNDLE_LIMITS.defaultFileMode };
|
|
219
|
+
}
|
|
220
|
+
return { path, size: input.size, mode };
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Validate a full manifest. Enforces:
|
|
224
|
+
* - entries is a non-empty array
|
|
225
|
+
* - `SKILL.md` exists at the bundle root (Claude's auto-discovery key)
|
|
226
|
+
* - file count <= maxFiles
|
|
227
|
+
* - total uncompressed size <= maxDecompressedBytes
|
|
228
|
+
* - per-entry validation (see `validateSkillBundleEntry`)
|
|
229
|
+
* - no duplicate paths
|
|
230
|
+
*
|
|
231
|
+
* Returns a canonical manifest with totals computed.
|
|
232
|
+
*/
|
|
233
|
+
export function validateSkillBundleManifest(input) {
|
|
234
|
+
if (!Array.isArray(input) || input.length === 0) {
|
|
235
|
+
throw new SkillBundleValidationError("bundle manifest must be a non-empty array of entries");
|
|
236
|
+
}
|
|
237
|
+
if (input.length > SKILL_BUNDLE_LIMITS.maxFiles) {
|
|
238
|
+
throw new SkillBundleValidationError(`bundle exceeds maxFiles (${SKILL_BUNDLE_LIMITS.maxFiles}): got ${input.length}`);
|
|
239
|
+
}
|
|
240
|
+
const seen = new Set();
|
|
241
|
+
const entries = [];
|
|
242
|
+
let totalSize = 0;
|
|
243
|
+
let hasSkillMd = false;
|
|
244
|
+
for (const raw of input) {
|
|
245
|
+
const entry = validateSkillBundleEntry(raw);
|
|
246
|
+
if (seen.has(entry.path)) {
|
|
247
|
+
throw new SkillBundleValidationError(`bundle manifest contains duplicate path: ${entry.path}`);
|
|
248
|
+
}
|
|
249
|
+
seen.add(entry.path);
|
|
250
|
+
if (entry.path === "SKILL.md") {
|
|
251
|
+
hasSkillMd = true;
|
|
252
|
+
}
|
|
253
|
+
totalSize += entry.size;
|
|
254
|
+
if (totalSize > SKILL_BUNDLE_LIMITS.maxDecompressedBytes) {
|
|
255
|
+
throw new SkillBundleValidationError(`bundle total size exceeds maxDecompressedBytes (${SKILL_BUNDLE_LIMITS.maxDecompressedBytes})`);
|
|
256
|
+
}
|
|
257
|
+
entries.push(entry);
|
|
258
|
+
}
|
|
259
|
+
if (!hasSkillMd) {
|
|
260
|
+
throw new SkillBundleValidationError("bundle manifest must contain a 'SKILL.md' entry at the bundle root");
|
|
261
|
+
}
|
|
262
|
+
return { entries, totalSize, fileCount: entries.length };
|
|
263
|
+
}
|
|
264
|
+
export const MCP_SERVER_NAME_PATTERN = /^[a-z][a-z0-9_-]{0,62}$/;
|
|
265
|
+
export function parseMcpServerRef(input, path) {
|
|
266
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
267
|
+
throw new Error(`${path} must be an object`);
|
|
268
|
+
}
|
|
269
|
+
const record = input;
|
|
270
|
+
// Headers belong on `BlueprintMcpServer`, not the non-secret wire ref;
|
|
271
|
+
// and the wire `submission.mcpServers` must NEVER contain headers. So
|
|
272
|
+
// reject any field other than {name,url} explicitly to make a caller
|
|
273
|
+
// accidentally inlining `headers` into the non-secret half fail loudly
|
|
274
|
+
// instead of silently dropping the field. `parseBlueprintMcpServer`
|
|
275
|
+
// handles the headers case separately for Blueprint-level entries.
|
|
276
|
+
for (const key of Object.keys(record)) {
|
|
277
|
+
if (key !== "name" && key !== "url") {
|
|
278
|
+
throw new Error(`${path}.${key} is not an allowed field for McpServerRef; permitted: name, url`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const name = record.name;
|
|
282
|
+
if (typeof name !== "string" || !MCP_SERVER_NAME_PATTERN.test(name)) {
|
|
283
|
+
throw new Error(`${path}.name must match ${MCP_SERVER_NAME_PATTERN.source}`);
|
|
284
|
+
}
|
|
285
|
+
const url = record.url;
|
|
286
|
+
if (typeof url !== "string" || url.length === 0) {
|
|
287
|
+
throw new Error(`${path}.url must be a non-empty string`);
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
const parsed = new URL(url);
|
|
291
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
292
|
+
throw new Error(`${path}.url must use http or https (got ${parsed.protocol})`);
|
|
293
|
+
}
|
|
294
|
+
// Auth belongs in `secrets.mcpServers[i].headers`, never in the URL
|
|
295
|
+
// itself. A `https://user:pass@host` style URL would be persisted in
|
|
296
|
+
// the non-secret run snapshot and hashed into the idempotency key —
|
|
297
|
+
// both unacceptable for credential material.
|
|
298
|
+
if (parsed.username !== "" || parsed.password !== "") {
|
|
299
|
+
throw new Error(`${path}.url must not contain userinfo (username/password); use secrets.mcpServers[].headers for auth`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch (cause) {
|
|
303
|
+
if (cause instanceof Error && cause.message.startsWith(path)) {
|
|
304
|
+
throw cause;
|
|
305
|
+
}
|
|
306
|
+
throw new Error(`${path}.url is not a valid URL: ${url}`);
|
|
307
|
+
}
|
|
308
|
+
return { name, url };
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Strict parser for Blueprint-level MCP server entries. Allows only the
|
|
312
|
+
* `{name, url, headers?}` shape — used by `parseBlueprintMcpServers` so
|
|
313
|
+
* a Blueprint that came from `--config run.json` cannot smuggle
|
|
314
|
+
* unrelated fields past the parser.
|
|
315
|
+
*/
|
|
316
|
+
function parseBlueprintMcpServerRef(input, path) {
|
|
317
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
318
|
+
throw new Error(`${path} must be an object`);
|
|
319
|
+
}
|
|
320
|
+
const record = input;
|
|
321
|
+
for (const key of Object.keys(record)) {
|
|
322
|
+
if (key !== "name" && key !== "url" && key !== "headers") {
|
|
323
|
+
throw new Error(`${path}.${key} is not an allowed field for BlueprintMcpServer; permitted: name, url, headers`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// Reuse the {name,url} validator by passing the stripped object.
|
|
327
|
+
const stripped = { name: record.name, url: record.url };
|
|
328
|
+
const ref = parseMcpServerRef(stripped, path);
|
|
329
|
+
const rawHeaders = record.headers;
|
|
330
|
+
if (rawHeaders === undefined) {
|
|
331
|
+
return ref;
|
|
332
|
+
}
|
|
333
|
+
if (rawHeaders === null || typeof rawHeaders !== "object" || Array.isArray(rawHeaders)) {
|
|
334
|
+
throw new Error(`${path}.headers, when provided, must be a string-keyed object`);
|
|
335
|
+
}
|
|
336
|
+
const headers = {};
|
|
337
|
+
for (const [hk, hv] of Object.entries(rawHeaders)) {
|
|
338
|
+
if (typeof hv !== "string") {
|
|
339
|
+
throw new Error(`${path}.headers.${hk} must be a string`);
|
|
340
|
+
}
|
|
341
|
+
headers[hk] = hv;
|
|
342
|
+
}
|
|
343
|
+
return { ...ref, headers };
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Currier for parameterised Blueprints.
|
|
347
|
+
*
|
|
348
|
+
* ```ts
|
|
349
|
+
* const investigate = defineRun((p: { repo: string; issue: number }) => ({
|
|
350
|
+
* model: "claude-sonnet-4-5-20250929",
|
|
351
|
+
* system: `You work on ${p.repo}.`,
|
|
352
|
+
* prompt: `Investigate issue #${p.issue}.`,
|
|
353
|
+
* skills: [rules],
|
|
354
|
+
* }));
|
|
355
|
+
* await client.submitRun({
|
|
356
|
+
* ...investigate({ repo: "antpath", issue: 123 }),
|
|
357
|
+
* secrets: { anthropic: { apiKey } },
|
|
358
|
+
* });
|
|
359
|
+
* ```
|
|
360
|
+
*
|
|
361
|
+
* The returned function is referentially transparent — it just calls
|
|
362
|
+
* the provided producer. The wrapper exists for two reasons: (a) a
|
|
363
|
+
* single named entry point makes IDEs surface the type of the inner
|
|
364
|
+
* blueprint at the call site, and (b) it pins the "no late binding"
|
|
365
|
+
* contract — strings inside the Blueprint are resolved by the TS call
|
|
366
|
+
* site, not by a server-side template engine.
|
|
367
|
+
*/
|
|
368
|
+
export function defineRun(producer) {
|
|
369
|
+
if (typeof producer !== "function") {
|
|
370
|
+
throw new TypeError("defineRun expects a function");
|
|
371
|
+
}
|
|
372
|
+
return (params) => producer(params);
|
|
373
|
+
}
|
|
374
|
+
// ---------------------------------------------------------------------------
|
|
375
|
+
// Blueprint parser (used by CLI to load `run.json`)
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
/**
|
|
378
|
+
* Parse a Blueprint from JSON. Defensive — used by the host CLI to
|
|
379
|
+
* load `--config run.json`. Throws with the JSON path that failed so
|
|
380
|
+
* a user can fix their file. Headers are preserved here and split out
|
|
381
|
+
* later by the SDK normalisation step.
|
|
382
|
+
*/
|
|
383
|
+
export function parseBlueprint(input) {
|
|
384
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
385
|
+
throw new Error("Blueprint must be an object");
|
|
386
|
+
}
|
|
387
|
+
const record = input;
|
|
388
|
+
const allowed = new Set([
|
|
389
|
+
"model",
|
|
390
|
+
"system",
|
|
391
|
+
"prompt",
|
|
392
|
+
"skills",
|
|
393
|
+
"mcpServers",
|
|
394
|
+
"environment",
|
|
395
|
+
"cleanup",
|
|
396
|
+
"proxyEndpoints",
|
|
397
|
+
"metadata"
|
|
398
|
+
]);
|
|
399
|
+
for (const key of Object.keys(record)) {
|
|
400
|
+
if (!allowed.has(key)) {
|
|
401
|
+
throw new Error(`Blueprint contains unexpected field: ${key}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const model = record.model;
|
|
405
|
+
if (typeof model !== "string" || model.length === 0) {
|
|
406
|
+
throw new Error("Blueprint.model must be a non-empty string");
|
|
407
|
+
}
|
|
408
|
+
const system = record.system;
|
|
409
|
+
if (system !== undefined && typeof system !== "string") {
|
|
410
|
+
throw new Error("Blueprint.system, when provided, must be a string");
|
|
411
|
+
}
|
|
412
|
+
const prompt = parseBlueprintPrompt(record.prompt);
|
|
413
|
+
const skills = parseBlueprintSkills(record.skills);
|
|
414
|
+
const mcpServers = parseBlueprintMcpServers(record.mcpServers);
|
|
415
|
+
return {
|
|
416
|
+
model,
|
|
417
|
+
...(system !== undefined ? { system } : {}),
|
|
418
|
+
prompt,
|
|
419
|
+
...(skills !== undefined ? { skills } : {}),
|
|
420
|
+
...(mcpServers !== undefined ? { mcpServers } : {}),
|
|
421
|
+
// environment / cleanup / proxyEndpoints / metadata: passed through
|
|
422
|
+
// as-is — the BFF revalidates them via `parseFlatRunSubmissionRequest`,
|
|
423
|
+
// so duplicating the heavyweight parsers here would mean two sources
|
|
424
|
+
// of truth. The CLI surfaces structural errors at submission time.
|
|
425
|
+
...(record.environment !== undefined
|
|
426
|
+
? { environment: record.environment }
|
|
427
|
+
: {}),
|
|
428
|
+
...(record.cleanup !== undefined
|
|
429
|
+
? { cleanup: record.cleanup }
|
|
430
|
+
: {}),
|
|
431
|
+
...(record.proxyEndpoints !== undefined
|
|
432
|
+
? { proxyEndpoints: record.proxyEndpoints }
|
|
433
|
+
: {}),
|
|
434
|
+
...(record.metadata !== undefined
|
|
435
|
+
? { metadata: record.metadata }
|
|
436
|
+
: {})
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
function parseBlueprintPrompt(value) {
|
|
440
|
+
if (typeof value === "string") {
|
|
441
|
+
if (value.length === 0) {
|
|
442
|
+
throw new Error("Blueprint.prompt must be a non-empty string");
|
|
443
|
+
}
|
|
444
|
+
return value;
|
|
445
|
+
}
|
|
446
|
+
if (Array.isArray(value)) {
|
|
447
|
+
const arr = [];
|
|
448
|
+
for (let i = 0; i < value.length; i++) {
|
|
449
|
+
const item = value[i];
|
|
450
|
+
if (typeof item !== "string" || item.length === 0) {
|
|
451
|
+
throw new Error(`Blueprint.prompt[${i}] must be a non-empty string`);
|
|
452
|
+
}
|
|
453
|
+
arr.push(item);
|
|
454
|
+
}
|
|
455
|
+
if (arr.length === 0) {
|
|
456
|
+
throw new Error("Blueprint.prompt must be a non-empty string or array of strings");
|
|
457
|
+
}
|
|
458
|
+
return arr;
|
|
459
|
+
}
|
|
460
|
+
throw new Error("Blueprint.prompt must be a string or array of strings");
|
|
461
|
+
}
|
|
462
|
+
function parseBlueprintSkills(value) {
|
|
463
|
+
if (value === undefined) {
|
|
464
|
+
return undefined;
|
|
465
|
+
}
|
|
466
|
+
if (!Array.isArray(value)) {
|
|
467
|
+
throw new Error("Blueprint.skills must be an array");
|
|
468
|
+
}
|
|
469
|
+
return value.map((item, index) => parseSkillRef(item, `Blueprint.skills[${index}]`));
|
|
470
|
+
}
|
|
471
|
+
function parseBlueprintMcpServers(value) {
|
|
472
|
+
if (value === undefined) {
|
|
473
|
+
return undefined;
|
|
474
|
+
}
|
|
475
|
+
if (!Array.isArray(value)) {
|
|
476
|
+
throw new Error("Blueprint.mcpServers must be an array");
|
|
477
|
+
}
|
|
478
|
+
const seen = new Set();
|
|
479
|
+
return value.map((item, index) => {
|
|
480
|
+
const entry = parseBlueprintMcpServerRef(item, `Blueprint.mcpServers[${index}]`);
|
|
481
|
+
if (seen.has(entry.name)) {
|
|
482
|
+
throw new Error(`Blueprint.mcpServers duplicate name: ${entry.name}`);
|
|
483
|
+
}
|
|
484
|
+
seen.add(entry.name);
|
|
485
|
+
return entry;
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
export function normaliseBlueprint(blueprint) {
|
|
489
|
+
const prompt = typeof blueprint.prompt === "string" ? [blueprint.prompt] : blueprint.prompt;
|
|
490
|
+
const skills = blueprint.skills ?? [];
|
|
491
|
+
const mcpServers = [];
|
|
492
|
+
const mcpServerSecrets = [];
|
|
493
|
+
for (const entry of blueprint.mcpServers ?? []) {
|
|
494
|
+
mcpServers.push({ name: entry.name, url: entry.url });
|
|
495
|
+
if (entry.headers !== undefined) {
|
|
496
|
+
mcpServerSecrets.push({ name: entry.name, url: entry.url, headers: entry.headers });
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return {
|
|
500
|
+
model: blueprint.model,
|
|
501
|
+
...(blueprint.system !== undefined ? { system: blueprint.system } : {}),
|
|
502
|
+
prompt,
|
|
503
|
+
skills,
|
|
504
|
+
mcpServers,
|
|
505
|
+
...(blueprint.environment !== undefined ? { environment: blueprint.environment } : {}),
|
|
506
|
+
...(blueprint.cleanup !== undefined ? { cleanup: blueprint.cleanup } : {}),
|
|
507
|
+
...(blueprint.proxyEndpoints !== undefined ? { proxyEndpoints: blueprint.proxyEndpoints } : {}),
|
|
508
|
+
...(blueprint.metadata !== undefined ? { metadata: blueprint.metadata } : {}),
|
|
509
|
+
mcpServerSecrets
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
//# sourceMappingURL=blueprint.js.map
|
package/dist/_shared/http.js
CHANGED
|
@@ -31,7 +31,13 @@ export class HttpClient {
|
|
|
31
31
|
...normalizeHeaders(init.headers)
|
|
32
32
|
};
|
|
33
33
|
if (init.body !== undefined && init.body !== null && !headers["content-type"]) {
|
|
34
|
-
|
|
34
|
+
// Default to JSON only for string-shaped bodies. FormData / Blob /
|
|
35
|
+
// ArrayBuffer / streams set their own content-type (and FormData
|
|
36
|
+
// specifically needs fetch to compute the multipart boundary), so
|
|
37
|
+
// we leave content-type untouched for non-string bodies.
|
|
38
|
+
if (typeof init.body === "string") {
|
|
39
|
+
headers["content-type"] = "application/json";
|
|
40
|
+
}
|
|
35
41
|
}
|
|
36
42
|
const response = await this.#fetch(url, { ...init, headers });
|
|
37
43
|
const body = await readJson(response);
|
package/dist/_shared/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export * from "./stable.js";
|
|
|
10
10
|
export * from "./sdk-secrets.js";
|
|
11
11
|
export * from "./sdk-errors.js";
|
|
12
12
|
export * from "./template/index.js";
|
|
13
|
+
export * from "./blueprint.js";
|
|
13
14
|
export * from "./runtime-types.js";
|
|
14
15
|
export * from "./known-events.js";
|
|
15
16
|
export * from "./http.js";
|
package/dist/_shared/index.js
CHANGED
|
@@ -12,6 +12,7 @@ export * from "./stable.js";
|
|
|
12
12
|
export * from "./sdk-secrets.js";
|
|
13
13
|
export * from "./sdk-errors.js";
|
|
14
14
|
export * from "./template/index.js";
|
|
15
|
+
export * from "./blueprint.js";
|
|
15
16
|
export * from "./runtime-types.js";
|
|
16
17
|
export * from "./known-events.js";
|
|
17
18
|
export * from "./http.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { HttpClient } from "./http.js";
|
|
2
|
-
import type { Output, Run, RunEvent, SignedOutputLink, WhoAmI } from "./runtime-types.js";
|
|
3
|
-
import type { PlatformRunSubmissionInput } from "./submission.js";
|
|
2
|
+
import type { Output, Run, RunEvent, SignedOutputLink, Skill, WhoAmI } from "./runtime-types.js";
|
|
3
|
+
import type { PlatformFlatRunSubmissionInput, PlatformRunSubmissionInput } from "./submission.js";
|
|
4
4
|
/**
|
|
5
5
|
* The single source of truth for SDK<->BFF transport. The SDK class
|
|
6
6
|
* AND the CLI subcommands both call these functions; neither
|
|
@@ -22,3 +22,21 @@ export declare function createOutputLink(http: HttpClient, runId: string, output
|
|
|
22
22
|
export declare function cancelRun(http: HttpClient, runId: string): Promise<void>;
|
|
23
23
|
export declare function deleteRun(http: HttpClient, runId: string): Promise<void>;
|
|
24
24
|
export declare function whoami(http: HttpClient): Promise<WhoAmI>;
|
|
25
|
+
export declare function submitRunFlat(http: HttpClient, request: PlatformFlatRunSubmissionInput): Promise<Run>;
|
|
26
|
+
/**
|
|
27
|
+
* Upload a workspace skill bundle as a zip blob. The dashboard BFF runs
|
|
28
|
+
* the two-phase flow internally (insert pending row, stream bytes into
|
|
29
|
+
* Supabase Storage, validate manifest, transition to ready) and returns
|
|
30
|
+
* the finalized `Skill`. Use `Skill.fromPath` / `Skill.upload` in the
|
|
31
|
+
* SDK to build the body; this transport function only knows about
|
|
32
|
+
* bytes.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createSkillBundle(http: HttpClient, args: {
|
|
35
|
+
readonly name: string;
|
|
36
|
+
readonly body: Blob | ArrayBuffer | Uint8Array;
|
|
37
|
+
readonly contentType?: string;
|
|
38
|
+
readonly filename?: string;
|
|
39
|
+
}): Promise<Skill>;
|
|
40
|
+
export declare function listSkills(http: HttpClient): Promise<readonly Skill[]>;
|
|
41
|
+
export declare function getSkill(http: HttpClient, skillId: string): Promise<Skill>;
|
|
42
|
+
export declare function deleteSkill(http: HttpClient, skillId: string): Promise<void>;
|
|
@@ -41,6 +41,62 @@ export async function deleteRun(http, runId) {
|
|
|
41
41
|
export async function whoami(http) {
|
|
42
42
|
return http.request("/api/whoami");
|
|
43
43
|
}
|
|
44
|
+
// ===========================================================================
|
|
45
|
+
// Flat (Skill / McpServer / Blueprint) operations
|
|
46
|
+
// ===========================================================================
|
|
47
|
+
export async function submitRunFlat(http, request) {
|
|
48
|
+
return http.request("/api/runs", {
|
|
49
|
+
method: "POST",
|
|
50
|
+
body: JSON.stringify(request)
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Upload a workspace skill bundle as a zip blob. The dashboard BFF runs
|
|
55
|
+
* the two-phase flow internally (insert pending row, stream bytes into
|
|
56
|
+
* Supabase Storage, validate manifest, transition to ready) and returns
|
|
57
|
+
* the finalized `Skill`. Use `Skill.fromPath` / `Skill.upload` in the
|
|
58
|
+
* SDK to build the body; this transport function only knows about
|
|
59
|
+
* bytes.
|
|
60
|
+
*/
|
|
61
|
+
export async function createSkillBundle(http, args) {
|
|
62
|
+
const form = new FormData();
|
|
63
|
+
form.append("name", args.name);
|
|
64
|
+
const blobBody = toBlob(args.body, args.contentType ?? "application/zip");
|
|
65
|
+
form.append("bundle", blobBody, args.filename ?? `${args.name}.zip`);
|
|
66
|
+
return http.request("/api/skills", {
|
|
67
|
+
method: "POST",
|
|
68
|
+
body: form
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
export async function listSkills(http) {
|
|
72
|
+
const result = await http.request("/api/skills");
|
|
73
|
+
if (Array.isArray(result)) {
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
return result.skills;
|
|
77
|
+
}
|
|
78
|
+
export async function getSkill(http, skillId) {
|
|
79
|
+
return http.request(`/api/skills/${encodeURIComponent(skillId)}`);
|
|
80
|
+
}
|
|
81
|
+
export async function deleteSkill(http, skillId) {
|
|
82
|
+
await http.request(`/api/skills/${encodeURIComponent(skillId)}`, {
|
|
83
|
+
method: "DELETE"
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
function toBlob(input, contentType) {
|
|
87
|
+
if (input instanceof Blob) {
|
|
88
|
+
return input;
|
|
89
|
+
}
|
|
90
|
+
if (input instanceof Uint8Array) {
|
|
91
|
+
// BlobPart accepts ArrayBufferView, but lib.dom's overload set
|
|
92
|
+
// narrows on the underlying buffer kind. Slice into a fresh
|
|
93
|
+
// ArrayBuffer so a SharedArrayBuffer-backed Uint8Array works.
|
|
94
|
+
const copy = new Uint8Array(input.byteLength);
|
|
95
|
+
copy.set(input);
|
|
96
|
+
return new Blob([copy.buffer], { type: contentType });
|
|
97
|
+
}
|
|
98
|
+
return new Blob([input], { type: contentType });
|
|
99
|
+
}
|
|
44
100
|
function hasRun(value) {
|
|
45
101
|
return Boolean(value && typeof value === "object" && "run" in value);
|
|
46
102
|
}
|
|
@@ -73,6 +73,36 @@ export interface WhoAmI {
|
|
|
73
73
|
readonly scopes?: readonly string[];
|
|
74
74
|
readonly [key: string]: unknown;
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Workspace skill bundle as the dashboard BFF returns it. Mirrors a row
|
|
78
|
+
* of `skill_bundles` joined with its computed manifest. `state` is the
|
|
79
|
+
* upload lifecycle (`pending` -> `ready`); only `ready` rows are
|
|
80
|
+
* referenceable from a run. `deletedAt` is the soft-delete tombstone
|
|
81
|
+
* (`null` for live bundles).
|
|
82
|
+
*
|
|
83
|
+
* See `references/repo-architecture.md` (Skill custody) and
|
|
84
|
+
* supabase/migrations/20260512000000_skill_bundles.sql for the
|
|
85
|
+
* authoritative shape.
|
|
86
|
+
*/
|
|
87
|
+
export interface Skill {
|
|
88
|
+
readonly id: string;
|
|
89
|
+
readonly workspaceId?: string;
|
|
90
|
+
readonly name: string;
|
|
91
|
+
readonly state: "pending" | "ready";
|
|
92
|
+
readonly hash?: string | null;
|
|
93
|
+
readonly sizeBytes?: number | null;
|
|
94
|
+
readonly fileCount?: number | null;
|
|
95
|
+
readonly manifest?: ReadonlyArray<{
|
|
96
|
+
readonly path: string;
|
|
97
|
+
readonly size: number;
|
|
98
|
+
readonly mode: number;
|
|
99
|
+
}>;
|
|
100
|
+
readonly createdAt?: string;
|
|
101
|
+
readonly updatedAt?: string;
|
|
102
|
+
readonly finalizedAt?: string | null;
|
|
103
|
+
readonly deletedAt?: string | null;
|
|
104
|
+
readonly [key: string]: unknown;
|
|
105
|
+
}
|
|
76
106
|
/**
|
|
77
107
|
* Full submission as the SDK and CLI assemble it before posting. The
|
|
78
108
|
* `template` is the SDK-level ResolvedTemplate (or a JSON-encoded one);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ProxyAuthShape, type ProxyMethod, type ProxyResponseMode } from "./proxy-protocol.js";
|
|
2
|
+
import type { McpServerRef, SkillRef } from "./blueprint.js";
|
|
2
3
|
export type JsonPrimitive = string | number | boolean | null;
|
|
3
4
|
export type JsonValue = JsonPrimitive | JsonValue[] | {
|
|
4
5
|
readonly [key: string]: JsonValue;
|
|
@@ -155,3 +156,42 @@ export declare const PROXY_ENDPOINT_DEFAULTS: {
|
|
|
155
156
|
readonly responseByteBudget: number;
|
|
156
157
|
};
|
|
157
158
|
export declare function parseRunSubmissionRequest(input: unknown): PlatformRunSubmissionRequest;
|
|
159
|
+
/**
|
|
160
|
+
* Wire-level submission posted to /api/runs in the flat surface. The
|
|
161
|
+
* `prompt` is always an array internally so the worker, the audit log,
|
|
162
|
+
* and the BFF idempotency hash all see one shape. `mcpServers` carries
|
|
163
|
+
* only the non-secret half; bearer headers travel in
|
|
164
|
+
* `secrets.mcpServers` keyed by `name`.
|
|
165
|
+
*
|
|
166
|
+
* `skills` is a list of `SkillRef`s — workspace refs point at
|
|
167
|
+
* `skill_bundles.id` (validated by the BFF before acceptance and pinned
|
|
168
|
+
* into `run_skill_snapshots`), provider refs pass through unchanged.
|
|
169
|
+
*/
|
|
170
|
+
export interface PlatformFlatSubmission {
|
|
171
|
+
readonly model: string;
|
|
172
|
+
readonly system?: string;
|
|
173
|
+
readonly prompt: readonly string[];
|
|
174
|
+
readonly skills: readonly SkillRef[];
|
|
175
|
+
readonly mcpServers: readonly McpServerRef[];
|
|
176
|
+
readonly environment?: PlatformTemplateEnvironment;
|
|
177
|
+
readonly metadata?: Record<string, JsonValue>;
|
|
178
|
+
}
|
|
179
|
+
export interface PlatformFlatRunSubmissionRequest {
|
|
180
|
+
readonly workspaceId: string;
|
|
181
|
+
readonly idempotencyKey: string;
|
|
182
|
+
readonly submission: PlatformFlatSubmission;
|
|
183
|
+
readonly cleanup?: PlatformCleanupPolicy;
|
|
184
|
+
readonly secrets: PlatformInlineSecrets;
|
|
185
|
+
readonly proxyEndpoints?: readonly PlatformProxyEndpoint[];
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Same `workspaceId is optional` rule as the template-path
|
|
189
|
+
* `PlatformRunSubmissionInput`: token-authenticated clients leave it
|
|
190
|
+
* out, the BFF route injects it from the token before invoking the
|
|
191
|
+
* parser. Dashboard UI callers (Auth.js user principal) pass it
|
|
192
|
+
* explicitly.
|
|
193
|
+
*/
|
|
194
|
+
export type PlatformFlatRunSubmissionInput = Omit<PlatformFlatRunSubmissionRequest, "workspaceId"> & {
|
|
195
|
+
readonly workspaceId?: string;
|
|
196
|
+
};
|
|
197
|
+
export declare function parseFlatRunSubmissionRequest(input: unknown): PlatformFlatRunSubmissionRequest;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { authShapeHeaderName, PROXY_ALLOWED_METHODS, PROXY_RESPONSE_MODES } from "./proxy-protocol.js";
|
|
2
|
+
import { parseMcpServerRef, parseSkillRef } from "./blueprint.js";
|
|
2
3
|
const SECRETS_KEY = "secrets";
|
|
3
4
|
/**
|
|
4
5
|
* Default caps for a proxy endpoint when the submission doesn't specify
|
|
@@ -459,7 +460,15 @@ function parseMcpServers(input) {
|
|
|
459
460
|
if (!Array.isArray(input)) {
|
|
460
461
|
throw new Error("secrets.mcpServers must be an array");
|
|
461
462
|
}
|
|
462
|
-
|
|
463
|
+
const seen = new Set();
|
|
464
|
+
return input.map((entry, index) => {
|
|
465
|
+
const parsed = parseMcpServer(entry, `secrets.mcpServers[${index}]`);
|
|
466
|
+
if (seen.has(parsed.name)) {
|
|
467
|
+
throw new Error(`secrets.mcpServers duplicate name: ${parsed.name}`);
|
|
468
|
+
}
|
|
469
|
+
seen.add(parsed.name);
|
|
470
|
+
return parsed;
|
|
471
|
+
});
|
|
463
472
|
}
|
|
464
473
|
function parseMcpServer(input, path) {
|
|
465
474
|
const value = requireRecord(input, path);
|
|
@@ -678,4 +687,147 @@ function isJsonValue(input) {
|
|
|
678
687
|
}
|
|
679
688
|
return false;
|
|
680
689
|
}
|
|
690
|
+
export function parseFlatRunSubmissionRequest(input) {
|
|
691
|
+
const value = requireRecord(input, "submission");
|
|
692
|
+
// Defence in depth: scan every non-secrets field for credential-named
|
|
693
|
+
// keys, exactly like `parseRunSubmissionRequest`. The `secrets` key is
|
|
694
|
+
// the only allow-listed home for credential material.
|
|
695
|
+
for (const [key, fieldValue] of Object.entries(value)) {
|
|
696
|
+
if (key === SECRETS_KEY) {
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
if (deniedSecretFields.has(key)) {
|
|
700
|
+
throw new Error(`Secret-bearing field is not allowed in platform submission: ${key}`);
|
|
701
|
+
}
|
|
702
|
+
assertNoSecretBearingFields(fieldValue, [key]);
|
|
703
|
+
}
|
|
704
|
+
const cleanup = parseCleanupPolicy(value.cleanup);
|
|
705
|
+
const proxyEndpoints = parseProxyEndpoints(value.proxyEndpoints);
|
|
706
|
+
const secrets = parseInlineSecrets(value.secrets);
|
|
707
|
+
crossValidateProxyEndpointsAndAuth(proxyEndpoints, secrets.proxyEndpointAuth);
|
|
708
|
+
const submission = parseFlatSubmission(value.submission);
|
|
709
|
+
// mcpServers names must agree across the submission half and the
|
|
710
|
+
// secrets half — every secrets.mcpServers[i].name MUST resolve to a
|
|
711
|
+
// submission.mcpServers entry (no orphan secrets) AND the URL must
|
|
712
|
+
// match exactly. The reverse is allowed (an MCP server with no auth
|
|
713
|
+
// headers is a valid public-MCP mode).
|
|
714
|
+
if (secrets.mcpServers !== undefined) {
|
|
715
|
+
const declared = new Map(submission.mcpServers.map((m) => [m.name, m.url]));
|
|
716
|
+
for (const secret of secrets.mcpServers) {
|
|
717
|
+
const declaredUrl = declared.get(secret.name);
|
|
718
|
+
if (declaredUrl === undefined) {
|
|
719
|
+
throw new Error(`secrets.mcpServers[name=${secret.name}] has no matching submission.mcpServers entry`);
|
|
720
|
+
}
|
|
721
|
+
if (declaredUrl !== secret.url) {
|
|
722
|
+
throw new Error(`secrets.mcpServers[name=${secret.name}].url must equal submission.mcpServers[name=${secret.name}].url ` +
|
|
723
|
+
`(got submission=${declaredUrl}, secrets=${secret.url})`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return {
|
|
728
|
+
workspaceId: requireString(value.workspaceId, "workspaceId"),
|
|
729
|
+
idempotencyKey: requireString(value.idempotencyKey, "idempotencyKey"),
|
|
730
|
+
submission,
|
|
731
|
+
...(cleanup ? { cleanup } : {}),
|
|
732
|
+
...(proxyEndpoints ? { proxyEndpoints } : {}),
|
|
733
|
+
secrets
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
function parseFlatSubmission(input) {
|
|
737
|
+
const value = requireRecord(input, "submission.submission");
|
|
738
|
+
const allowed = new Set([
|
|
739
|
+
"model",
|
|
740
|
+
"system",
|
|
741
|
+
"prompt",
|
|
742
|
+
"skills",
|
|
743
|
+
"mcpServers",
|
|
744
|
+
"environment",
|
|
745
|
+
"metadata"
|
|
746
|
+
]);
|
|
747
|
+
for (const key of Object.keys(value)) {
|
|
748
|
+
if (!allowed.has(key)) {
|
|
749
|
+
throw new Error(`submission.${key} is not an allowed field; permitted: ${[...allowed].join(", ")}`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
const model = requireString(value.model, "submission.model");
|
|
753
|
+
const system = optionalString(value.system, "submission.system");
|
|
754
|
+
const prompt = parseFlatPrompt(value.prompt);
|
|
755
|
+
const skills = parseFlatSkills(value.skills);
|
|
756
|
+
const mcpServers = parseFlatMcpServers(value.mcpServers);
|
|
757
|
+
const environment = parseTemplateEnvironment(value.environment);
|
|
758
|
+
const metadata = optionalJsonRecord(value.metadata, "submission.metadata");
|
|
759
|
+
return {
|
|
760
|
+
model,
|
|
761
|
+
...(system ? { system } : {}),
|
|
762
|
+
prompt,
|
|
763
|
+
skills,
|
|
764
|
+
mcpServers,
|
|
765
|
+
...(environment ? { environment } : {}),
|
|
766
|
+
...(metadata ? { metadata } : {})
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
function parseFlatPrompt(input) {
|
|
770
|
+
if (typeof input === "string") {
|
|
771
|
+
if (input.length === 0) {
|
|
772
|
+
throw new Error("submission.prompt must be non-empty");
|
|
773
|
+
}
|
|
774
|
+
return [input];
|
|
775
|
+
}
|
|
776
|
+
if (!Array.isArray(input)) {
|
|
777
|
+
throw new Error("submission.prompt must be a string or an array of strings");
|
|
778
|
+
}
|
|
779
|
+
if (input.length === 0) {
|
|
780
|
+
throw new Error("submission.prompt array must be non-empty");
|
|
781
|
+
}
|
|
782
|
+
return input.map((item, index) => {
|
|
783
|
+
if (typeof item !== "string" || item.length === 0) {
|
|
784
|
+
throw new Error(`submission.prompt[${index}] must be a non-empty string`);
|
|
785
|
+
}
|
|
786
|
+
return item;
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
function parseFlatSkills(input) {
|
|
790
|
+
if (input === undefined) {
|
|
791
|
+
return [];
|
|
792
|
+
}
|
|
793
|
+
if (!Array.isArray(input)) {
|
|
794
|
+
throw new Error("submission.skills must be an array of SkillRef objects");
|
|
795
|
+
}
|
|
796
|
+
const seenWorkspace = new Set();
|
|
797
|
+
const seenProvider = new Set();
|
|
798
|
+
return input.map((item, index) => {
|
|
799
|
+
const ref = parseSkillRef(item, `submission.skills[${index}]`);
|
|
800
|
+
if (ref.kind === "workspace") {
|
|
801
|
+
if (seenWorkspace.has(ref.id)) {
|
|
802
|
+
throw new Error(`submission.skills duplicate workspace skill id: ${ref.id}`);
|
|
803
|
+
}
|
|
804
|
+
seenWorkspace.add(ref.id);
|
|
805
|
+
}
|
|
806
|
+
else {
|
|
807
|
+
const key = `${ref.vendor}:${ref.skillId}:${ref.version ?? ""}`;
|
|
808
|
+
if (seenProvider.has(key)) {
|
|
809
|
+
throw new Error(`submission.skills duplicate provider skill: ${ref.vendor}:${ref.skillId}${ref.version ? `:${ref.version}` : ""}`);
|
|
810
|
+
}
|
|
811
|
+
seenProvider.add(key);
|
|
812
|
+
}
|
|
813
|
+
return ref;
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
function parseFlatMcpServers(input) {
|
|
817
|
+
if (input === undefined) {
|
|
818
|
+
return [];
|
|
819
|
+
}
|
|
820
|
+
if (!Array.isArray(input)) {
|
|
821
|
+
throw new Error("submission.mcpServers must be an array of {name, url} objects");
|
|
822
|
+
}
|
|
823
|
+
const seen = new Set();
|
|
824
|
+
return input.map((item, index) => {
|
|
825
|
+
const ref = parseMcpServerRef(item, `submission.mcpServers[${index}]`);
|
|
826
|
+
if (seen.has(ref.name)) {
|
|
827
|
+
throw new Error(`submission.mcpServers duplicate name: ${ref.name}`);
|
|
828
|
+
}
|
|
829
|
+
seen.add(ref.name);
|
|
830
|
+
return ref;
|
|
831
|
+
});
|
|
832
|
+
}
|
|
681
833
|
//# sourceMappingURL=submission.js.map
|
package/dist/cli.mjs
CHANGED
|
@@ -52,6 +52,24 @@ var TERMINAL_RUN_STATUSES = [
|
|
|
52
52
|
];
|
|
53
53
|
var terminalRunStatuses = new Set(TERMINAL_RUN_STATUSES);
|
|
54
54
|
|
|
55
|
+
// ../shared/dist/blueprint.js
|
|
56
|
+
var SKILL_BUNDLE_LIMITS = {
|
|
57
|
+
/** Compressed (.zip) ceiling. */
|
|
58
|
+
maxCompressedBytes: 10 * 1024 * 1024,
|
|
59
|
+
/** Sum of uncompressed file sizes. */
|
|
60
|
+
maxDecompressedBytes: 50 * 1024 * 1024,
|
|
61
|
+
/** Number of regular file entries (directories don't count). */
|
|
62
|
+
maxFiles: 1e3,
|
|
63
|
+
/** Maximum directory nesting depth — `a/b/c/d` has depth 4. */
|
|
64
|
+
maxDepth: 16,
|
|
65
|
+
/** Single-entry path length cap. */
|
|
66
|
+
maxPathLength: 512,
|
|
67
|
+
/** Stored file mode for ordinary files. */
|
|
68
|
+
defaultFileMode: 420,
|
|
69
|
+
/** Stored directory mode. */
|
|
70
|
+
defaultDirMode: 493
|
|
71
|
+
};
|
|
72
|
+
|
|
55
73
|
// ../shared/dist/submission.js
|
|
56
74
|
var PROXY_ENDPOINT_DEFAULTS = {
|
|
57
75
|
allowHeaders: [],
|
|
@@ -350,7 +368,9 @@ var HttpClient = class {
|
|
|
350
368
|
...normalizeHeaders(init.headers)
|
|
351
369
|
};
|
|
352
370
|
if (init.body !== void 0 && init.body !== null && !headers["content-type"]) {
|
|
353
|
-
|
|
371
|
+
if (typeof init.body === "string") {
|
|
372
|
+
headers["content-type"] = "application/json";
|
|
373
|
+
}
|
|
354
374
|
}
|
|
355
375
|
const response = await this.#fetch(url, { ...init, headers });
|
|
356
376
|
const body = await readJson(response);
|
|
@@ -414,11 +434,16 @@ var operations_exports = {};
|
|
|
414
434
|
__export(operations_exports, {
|
|
415
435
|
cancelRun: () => cancelRun,
|
|
416
436
|
createOutputLink: () => createOutputLink,
|
|
437
|
+
createSkillBundle: () => createSkillBundle,
|
|
417
438
|
deleteRun: () => deleteRun,
|
|
439
|
+
deleteSkill: () => deleteSkill,
|
|
418
440
|
getRun: () => getRun,
|
|
441
|
+
getSkill: () => getSkill,
|
|
419
442
|
listOutputs: () => listOutputs,
|
|
420
443
|
listRunEvents: () => listRunEvents,
|
|
444
|
+
listSkills: () => listSkills,
|
|
421
445
|
submitRun: () => submitRun,
|
|
446
|
+
submitRunFlat: () => submitRunFlat,
|
|
422
447
|
whoami: () => whoami
|
|
423
448
|
});
|
|
424
449
|
async function submitRun(http, request) {
|
|
@@ -451,6 +476,48 @@ async function deleteRun(http, runId) {
|
|
|
451
476
|
async function whoami(http) {
|
|
452
477
|
return http.request("/api/whoami");
|
|
453
478
|
}
|
|
479
|
+
async function submitRunFlat(http, request) {
|
|
480
|
+
return http.request("/api/runs", {
|
|
481
|
+
method: "POST",
|
|
482
|
+
body: JSON.stringify(request)
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
async function createSkillBundle(http, args) {
|
|
486
|
+
const form = new FormData();
|
|
487
|
+
form.append("name", args.name);
|
|
488
|
+
const blobBody = toBlob(args.body, args.contentType ?? "application/zip");
|
|
489
|
+
form.append("bundle", blobBody, args.filename ?? `${args.name}.zip`);
|
|
490
|
+
return http.request("/api/skills", {
|
|
491
|
+
method: "POST",
|
|
492
|
+
body: form
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
async function listSkills(http) {
|
|
496
|
+
const result = await http.request("/api/skills");
|
|
497
|
+
if (Array.isArray(result)) {
|
|
498
|
+
return result;
|
|
499
|
+
}
|
|
500
|
+
return result.skills;
|
|
501
|
+
}
|
|
502
|
+
async function getSkill(http, skillId) {
|
|
503
|
+
return http.request(`/api/skills/${encodeURIComponent(skillId)}`);
|
|
504
|
+
}
|
|
505
|
+
async function deleteSkill(http, skillId) {
|
|
506
|
+
await http.request(`/api/skills/${encodeURIComponent(skillId)}`, {
|
|
507
|
+
method: "DELETE"
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
function toBlob(input, contentType) {
|
|
511
|
+
if (input instanceof Blob) {
|
|
512
|
+
return input;
|
|
513
|
+
}
|
|
514
|
+
if (input instanceof Uint8Array) {
|
|
515
|
+
const copy = new Uint8Array(input.byteLength);
|
|
516
|
+
copy.set(input);
|
|
517
|
+
return new Blob([copy.buffer], { type: contentType });
|
|
518
|
+
}
|
|
519
|
+
return new Blob([input], { type: contentType });
|
|
520
|
+
}
|
|
454
521
|
function hasRun(value) {
|
|
455
522
|
return Boolean(value && typeof value === "object" && "run" in value);
|
|
456
523
|
}
|
package/dist/cli.mjs.sha256
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
2d4fdd070358ffea5d49c62d5cca10863da0fc6279a88fe78fa7f11eb6eb18d7 cli.mjs
|