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.
@@ -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
@@ -1,6 +1,11 @@
1
1
  export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
2
2
  export interface HttpClientOptions {
3
- readonly baseUrl: string;
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
  }
@@ -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 normalized = options.baseUrl.endsWith("/") ? options.baseUrl : `${options.baseUrl}/`;
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
- headers["content-type"] = "application/json";
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);
@@ -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";
@@ -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 { PlatformRunSubmissionRequest } 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
@@ -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: PlatformRunSubmissionRequest): Promise<Run>;
13
- export declare function getRun(http: HttpClient, workspaceId: string, runId: string): Promise<Run>;
14
- export declare function listRunEvents(http: HttpClient, workspaceId: string, runId: string): Promise<readonly RunEvent[]>;
15
- export declare function listOutputs(http: HttpClient, workspaceId: string, runId: string): Promise<readonly Output[]>;
16
- export declare function createOutputLink(http: HttpClient, workspaceId: string, runId: string, outputId: string): Promise<SignedOutputLink>;
17
- export declare function cancelRun(http: HttpClient, workspaceId: string, runId: string): Promise<void>;
18
- export declare function deleteRun(http: HttpClient, workspaceId: string, runId: string): Promise<void>;
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>;