antpath 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -14
- package/dist/_shared/blueprint.d.ts +263 -0
- package/dist/_shared/blueprint.js +505 -0
- package/dist/_shared/http.d.ts +6 -1
- package/dist/_shared/http.js +10 -5
- package/dist/_shared/index.d.ts +1 -0
- package/dist/_shared/index.js +1 -0
- package/dist/_shared/operations.d.ts +32 -9
- package/dist/_shared/operations.js +73 -12
- package/dist/_shared/runtime-types.d.ts +30 -0
- package/dist/_shared/stable.d.ts +14 -0
- package/dist/_shared/stable.js +14 -0
- package/dist/_shared/submission.d.ts +55 -0
- package/dist/_shared/submission.js +135 -1
- package/dist/cli.mjs +114 -58
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/client.d.ts +13 -6
- package/dist/client.js +17 -16
- package/dist/client.js.map +1 -1
- package/docs/credentials.md +1 -3
- package/docs/quickstart.md +4 -7
- package/docs/release.md +57 -12
- package/examples/mcp-static-bearer.ts +1 -3
- package/examples/quickstart.ts +1 -3
- package/package.json +2 -3
- package/references/architecture-decisions.md +0 -473
- package/references/implementation-plan.md +0 -452
- package/references/research-sources.md +0 -41
- package/references/testing-strategy.md +0 -29
|
@@ -0,0 +1,505 @@
|
|
|
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
|
+
}
|
|
295
|
+
catch (cause) {
|
|
296
|
+
if (cause instanceof Error && cause.message.startsWith(path)) {
|
|
297
|
+
throw cause;
|
|
298
|
+
}
|
|
299
|
+
throw new Error(`${path}.url is not a valid URL: ${url}`);
|
|
300
|
+
}
|
|
301
|
+
return { name, url };
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Strict parser for Blueprint-level MCP server entries. Allows only the
|
|
305
|
+
* `{name, url, headers?}` shape — used by `parseBlueprintMcpServers` so
|
|
306
|
+
* a Blueprint that came from `--config run.json` cannot smuggle
|
|
307
|
+
* unrelated fields past the parser.
|
|
308
|
+
*/
|
|
309
|
+
function parseBlueprintMcpServerRef(input, path) {
|
|
310
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
311
|
+
throw new Error(`${path} must be an object`);
|
|
312
|
+
}
|
|
313
|
+
const record = input;
|
|
314
|
+
for (const key of Object.keys(record)) {
|
|
315
|
+
if (key !== "name" && key !== "url" && key !== "headers") {
|
|
316
|
+
throw new Error(`${path}.${key} is not an allowed field for BlueprintMcpServer; permitted: name, url, headers`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// Reuse the {name,url} validator by passing the stripped object.
|
|
320
|
+
const stripped = { name: record.name, url: record.url };
|
|
321
|
+
const ref = parseMcpServerRef(stripped, path);
|
|
322
|
+
const rawHeaders = record.headers;
|
|
323
|
+
if (rawHeaders === undefined) {
|
|
324
|
+
return ref;
|
|
325
|
+
}
|
|
326
|
+
if (rawHeaders === null || typeof rawHeaders !== "object" || Array.isArray(rawHeaders)) {
|
|
327
|
+
throw new Error(`${path}.headers, when provided, must be a string-keyed object`);
|
|
328
|
+
}
|
|
329
|
+
const headers = {};
|
|
330
|
+
for (const [hk, hv] of Object.entries(rawHeaders)) {
|
|
331
|
+
if (typeof hv !== "string") {
|
|
332
|
+
throw new Error(`${path}.headers.${hk} must be a string`);
|
|
333
|
+
}
|
|
334
|
+
headers[hk] = hv;
|
|
335
|
+
}
|
|
336
|
+
return { ...ref, headers };
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Currier for parameterised Blueprints.
|
|
340
|
+
*
|
|
341
|
+
* ```ts
|
|
342
|
+
* const investigate = defineRun((p: { repo: string; issue: number }) => ({
|
|
343
|
+
* model: "claude-sonnet-4-5-20250929",
|
|
344
|
+
* system: `You work on ${p.repo}.`,
|
|
345
|
+
* prompt: `Investigate issue #${p.issue}.`,
|
|
346
|
+
* skills: [rules],
|
|
347
|
+
* }));
|
|
348
|
+
* await client.submitRun({
|
|
349
|
+
* ...investigate({ repo: "antpath", issue: 123 }),
|
|
350
|
+
* secrets: { anthropic: { apiKey } },
|
|
351
|
+
* });
|
|
352
|
+
* ```
|
|
353
|
+
*
|
|
354
|
+
* The returned function is referentially transparent — it just calls
|
|
355
|
+
* the provided producer. The wrapper exists for two reasons: (a) a
|
|
356
|
+
* single named entry point makes IDEs surface the type of the inner
|
|
357
|
+
* blueprint at the call site, and (b) it pins the "no late binding"
|
|
358
|
+
* contract — strings inside the Blueprint are resolved by the TS call
|
|
359
|
+
* site, not by a server-side template engine.
|
|
360
|
+
*/
|
|
361
|
+
export function defineRun(producer) {
|
|
362
|
+
if (typeof producer !== "function") {
|
|
363
|
+
throw new TypeError("defineRun expects a function");
|
|
364
|
+
}
|
|
365
|
+
return (params) => producer(params);
|
|
366
|
+
}
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
// Blueprint parser (used by CLI to load `run.json`)
|
|
369
|
+
// ---------------------------------------------------------------------------
|
|
370
|
+
/**
|
|
371
|
+
* Parse a Blueprint from JSON. Defensive — used by the host CLI to
|
|
372
|
+
* load `--config run.json`. Throws with the JSON path that failed so
|
|
373
|
+
* a user can fix their file. Headers are preserved here and split out
|
|
374
|
+
* later by the SDK normalisation step.
|
|
375
|
+
*/
|
|
376
|
+
export function parseBlueprint(input) {
|
|
377
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
378
|
+
throw new Error("Blueprint must be an object");
|
|
379
|
+
}
|
|
380
|
+
const record = input;
|
|
381
|
+
const allowed = new Set([
|
|
382
|
+
"model",
|
|
383
|
+
"system",
|
|
384
|
+
"prompt",
|
|
385
|
+
"skills",
|
|
386
|
+
"mcpServers",
|
|
387
|
+
"environment",
|
|
388
|
+
"cleanup",
|
|
389
|
+
"proxyEndpoints",
|
|
390
|
+
"metadata"
|
|
391
|
+
]);
|
|
392
|
+
for (const key of Object.keys(record)) {
|
|
393
|
+
if (!allowed.has(key)) {
|
|
394
|
+
throw new Error(`Blueprint contains unexpected field: ${key}`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const model = record.model;
|
|
398
|
+
if (typeof model !== "string" || model.length === 0) {
|
|
399
|
+
throw new Error("Blueprint.model must be a non-empty string");
|
|
400
|
+
}
|
|
401
|
+
const system = record.system;
|
|
402
|
+
if (system !== undefined && typeof system !== "string") {
|
|
403
|
+
throw new Error("Blueprint.system, when provided, must be a string");
|
|
404
|
+
}
|
|
405
|
+
const prompt = parseBlueprintPrompt(record.prompt);
|
|
406
|
+
const skills = parseBlueprintSkills(record.skills);
|
|
407
|
+
const mcpServers = parseBlueprintMcpServers(record.mcpServers);
|
|
408
|
+
return {
|
|
409
|
+
model,
|
|
410
|
+
...(system !== undefined ? { system } : {}),
|
|
411
|
+
prompt,
|
|
412
|
+
...(skills !== undefined ? { skills } : {}),
|
|
413
|
+
...(mcpServers !== undefined ? { mcpServers } : {}),
|
|
414
|
+
// environment / cleanup / proxyEndpoints / metadata: passed through
|
|
415
|
+
// as-is — the BFF revalidates them via `parseFlatRunSubmissionRequest`,
|
|
416
|
+
// so duplicating the heavyweight parsers here would mean two sources
|
|
417
|
+
// of truth. The CLI surfaces structural errors at submission time.
|
|
418
|
+
...(record.environment !== undefined
|
|
419
|
+
? { environment: record.environment }
|
|
420
|
+
: {}),
|
|
421
|
+
...(record.cleanup !== undefined
|
|
422
|
+
? { cleanup: record.cleanup }
|
|
423
|
+
: {}),
|
|
424
|
+
...(record.proxyEndpoints !== undefined
|
|
425
|
+
? { proxyEndpoints: record.proxyEndpoints }
|
|
426
|
+
: {}),
|
|
427
|
+
...(record.metadata !== undefined
|
|
428
|
+
? { metadata: record.metadata }
|
|
429
|
+
: {})
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
function parseBlueprintPrompt(value) {
|
|
433
|
+
if (typeof value === "string") {
|
|
434
|
+
if (value.length === 0) {
|
|
435
|
+
throw new Error("Blueprint.prompt must be a non-empty string");
|
|
436
|
+
}
|
|
437
|
+
return value;
|
|
438
|
+
}
|
|
439
|
+
if (Array.isArray(value)) {
|
|
440
|
+
const arr = [];
|
|
441
|
+
for (let i = 0; i < value.length; i++) {
|
|
442
|
+
const item = value[i];
|
|
443
|
+
if (typeof item !== "string" || item.length === 0) {
|
|
444
|
+
throw new Error(`Blueprint.prompt[${i}] must be a non-empty string`);
|
|
445
|
+
}
|
|
446
|
+
arr.push(item);
|
|
447
|
+
}
|
|
448
|
+
if (arr.length === 0) {
|
|
449
|
+
throw new Error("Blueprint.prompt must be a non-empty string or array of strings");
|
|
450
|
+
}
|
|
451
|
+
return arr;
|
|
452
|
+
}
|
|
453
|
+
throw new Error("Blueprint.prompt must be a string or array of strings");
|
|
454
|
+
}
|
|
455
|
+
function parseBlueprintSkills(value) {
|
|
456
|
+
if (value === undefined) {
|
|
457
|
+
return undefined;
|
|
458
|
+
}
|
|
459
|
+
if (!Array.isArray(value)) {
|
|
460
|
+
throw new Error("Blueprint.skills must be an array");
|
|
461
|
+
}
|
|
462
|
+
return value.map((item, index) => parseSkillRef(item, `Blueprint.skills[${index}]`));
|
|
463
|
+
}
|
|
464
|
+
function parseBlueprintMcpServers(value) {
|
|
465
|
+
if (value === undefined) {
|
|
466
|
+
return undefined;
|
|
467
|
+
}
|
|
468
|
+
if (!Array.isArray(value)) {
|
|
469
|
+
throw new Error("Blueprint.mcpServers must be an array");
|
|
470
|
+
}
|
|
471
|
+
const seen = new Set();
|
|
472
|
+
return value.map((item, index) => {
|
|
473
|
+
const entry = parseBlueprintMcpServerRef(item, `Blueprint.mcpServers[${index}]`);
|
|
474
|
+
if (seen.has(entry.name)) {
|
|
475
|
+
throw new Error(`Blueprint.mcpServers duplicate name: ${entry.name}`);
|
|
476
|
+
}
|
|
477
|
+
seen.add(entry.name);
|
|
478
|
+
return entry;
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
export function normaliseBlueprint(blueprint) {
|
|
482
|
+
const prompt = typeof blueprint.prompt === "string" ? [blueprint.prompt] : blueprint.prompt;
|
|
483
|
+
const skills = blueprint.skills ?? [];
|
|
484
|
+
const mcpServers = [];
|
|
485
|
+
const mcpServerSecrets = [];
|
|
486
|
+
for (const entry of blueprint.mcpServers ?? []) {
|
|
487
|
+
mcpServers.push({ name: entry.name, url: entry.url });
|
|
488
|
+
if (entry.headers !== undefined) {
|
|
489
|
+
mcpServerSecrets.push({ name: entry.name, url: entry.url, headers: entry.headers });
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return {
|
|
493
|
+
model: blueprint.model,
|
|
494
|
+
...(blueprint.system !== undefined ? { system: blueprint.system } : {}),
|
|
495
|
+
prompt,
|
|
496
|
+
skills,
|
|
497
|
+
mcpServers,
|
|
498
|
+
...(blueprint.environment !== undefined ? { environment: blueprint.environment } : {}),
|
|
499
|
+
...(blueprint.cleanup !== undefined ? { cleanup: blueprint.cleanup } : {}),
|
|
500
|
+
...(blueprint.proxyEndpoints !== undefined ? { proxyEndpoints: blueprint.proxyEndpoints } : {}),
|
|
501
|
+
...(blueprint.metadata !== undefined ? { metadata: blueprint.metadata } : {}),
|
|
502
|
+
mcpServerSecrets
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
//# sourceMappingURL=blueprint.js.map
|
package/dist/_shared/http.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
2
2
|
export interface HttpClientOptions {
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Dashboard BFF root. Optional — defaults to `ANTPATH_DEFAULT_BASE_URL`
|
|
5
|
+
* (`https://antpath.ai`). Self-hosted deployments override with their
|
|
6
|
+
* own URL; no env var consults this value.
|
|
7
|
+
*/
|
|
8
|
+
readonly baseUrl?: string;
|
|
4
9
|
readonly apiToken: string;
|
|
5
10
|
readonly fetch?: FetchLike;
|
|
6
11
|
}
|
package/dist/_shared/http.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AntpathApiError } from "./sdk-errors.js";
|
|
2
|
+
import { ANTPATH_DEFAULT_BASE_URL } from "./stable.js";
|
|
2
3
|
/**
|
|
3
4
|
* Thin transport used by every BFF-bound operation. The SDK class and
|
|
4
5
|
* the CLI subcommands BOTH build an `HttpClient` and pass it to the
|
|
@@ -10,13 +11,11 @@ export class HttpClient {
|
|
|
10
11
|
#apiToken;
|
|
11
12
|
#fetch;
|
|
12
13
|
constructor(options) {
|
|
13
|
-
if (!options.baseUrl) {
|
|
14
|
-
throw new Error("HttpClient: baseUrl is required");
|
|
15
|
-
}
|
|
16
14
|
if (!options.apiToken) {
|
|
17
15
|
throw new Error("HttpClient: apiToken is required");
|
|
18
16
|
}
|
|
19
|
-
const
|
|
17
|
+
const raw = options.baseUrl ?? ANTPATH_DEFAULT_BASE_URL;
|
|
18
|
+
const normalized = raw.endsWith("/") ? raw : `${raw}/`;
|
|
20
19
|
this.#baseUrl = new URL(normalized);
|
|
21
20
|
this.#apiToken = options.apiToken;
|
|
22
21
|
this.#fetch = options.fetch ?? fetch;
|
|
@@ -32,7 +31,13 @@ export class HttpClient {
|
|
|
32
31
|
...normalizeHeaders(init.headers)
|
|
33
32
|
};
|
|
34
33
|
if (init.body !== undefined && init.body !== null && !headers["content-type"]) {
|
|
35
|
-
|
|
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
|
+
}
|
|
36
41
|
}
|
|
37
42
|
const response = await this.#fetch(url, { ...init, headers });
|
|
38
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 {
|
|
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
|
|
@@ -8,12 +8,35 @@ import type { PlatformRunSubmissionRequest } from "./submission.js";
|
|
|
8
8
|
*
|
|
9
9
|
* Every function takes an HttpClient (so callers control auth + fetch
|
|
10
10
|
* injection) and returns parsed responses.
|
|
11
|
+
*
|
|
12
|
+
* Workspace identity is derived server-side from the API token on
|
|
13
|
+
* every request — callers do not pass `workspaceId`. See
|
|
14
|
+
* `references/development-principles.md` (Agent-first surface design,
|
|
15
|
+
* Concrete rule 3).
|
|
11
16
|
*/
|
|
12
|
-
export declare function submitRun(http: HttpClient, request:
|
|
13
|
-
export declare function getRun(http: HttpClient,
|
|
14
|
-
export declare function listRunEvents(http: HttpClient,
|
|
15
|
-
export declare function listOutputs(http: HttpClient,
|
|
16
|
-
export declare function createOutputLink(http: HttpClient,
|
|
17
|
-
export declare function cancelRun(http: HttpClient,
|
|
18
|
-
export declare function deleteRun(http: HttpClient,
|
|
17
|
+
export declare function submitRun(http: HttpClient, request: PlatformRunSubmissionInput): Promise<Run>;
|
|
18
|
+
export declare function getRun(http: HttpClient, runId: string): Promise<Run>;
|
|
19
|
+
export declare function listRunEvents(http: HttpClient, runId: string): Promise<readonly RunEvent[]>;
|
|
20
|
+
export declare function listOutputs(http: HttpClient, runId: string): Promise<readonly Output[]>;
|
|
21
|
+
export declare function createOutputLink(http: HttpClient, runId: string, outputId: string): Promise<SignedOutputLink>;
|
|
22
|
+
export declare function cancelRun(http: HttpClient, runId: string): Promise<void>;
|
|
23
|
+
export declare function deleteRun(http: HttpClient, runId: string): Promise<void>;
|
|
19
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>;
|