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.
@@ -5,6 +5,11 @@
5
5
  *
6
6
  * Every function takes an HttpClient (so callers control auth + fetch
7
7
  * injection) and returns parsed responses.
8
+ *
9
+ * Workspace identity is derived server-side from the API token on
10
+ * every request — callers do not pass `workspaceId`. See
11
+ * `references/development-principles.md` (Agent-first surface design,
12
+ * Concrete rule 3).
8
13
  */
9
14
  export async function submitRun(http, request) {
10
15
  return http.request("/api/runs", {
@@ -12,30 +17,86 @@ export async function submitRun(http, request) {
12
17
  body: JSON.stringify(request)
13
18
  });
14
19
  }
15
- export async function getRun(http, workspaceId, runId) {
16
- const result = await http.request(`/api/runs/${encodeURIComponent(runId)}`, {}, { workspaceId });
20
+ export async function getRun(http, runId) {
21
+ const result = await http.request(`/api/runs/${encodeURIComponent(runId)}`);
17
22
  return hasRun(result) ? result.run : result;
18
23
  }
19
- export async function listRunEvents(http, workspaceId, runId) {
20
- const result = await http.request(`/api/runs/${encodeURIComponent(runId)}/events`, {}, { workspaceId });
24
+ export async function listRunEvents(http, runId) {
25
+ const result = await http.request(`/api/runs/${encodeURIComponent(runId)}/events`);
21
26
  return result.events;
22
27
  }
23
- export async function listOutputs(http, workspaceId, runId) {
24
- const result = await http.request(`/api/runs/${encodeURIComponent(runId)}/outputs`, {}, { workspaceId });
28
+ export async function listOutputs(http, runId) {
29
+ const result = await http.request(`/api/runs/${encodeURIComponent(runId)}/outputs`);
25
30
  return result.outputs;
26
31
  }
27
- export async function createOutputLink(http, workspaceId, runId, outputId) {
28
- return http.request(`/api/runs/${encodeURIComponent(runId)}/outputs/${encodeURIComponent(outputId)}/link`, { method: "POST" }, { workspaceId });
32
+ export async function createOutputLink(http, runId, outputId) {
33
+ return http.request(`/api/runs/${encodeURIComponent(runId)}/outputs/${encodeURIComponent(outputId)}/link`, { method: "POST" });
29
34
  }
30
- export async function cancelRun(http, workspaceId, runId) {
31
- await http.request(`/api/runs/${encodeURIComponent(runId)}/cancel`, { method: "POST" }, { workspaceId });
35
+ export async function cancelRun(http, runId) {
36
+ await http.request(`/api/runs/${encodeURIComponent(runId)}/cancel`, { method: "POST" });
32
37
  }
33
- export async function deleteRun(http, workspaceId, runId) {
34
- await http.request(`/api/runs/${encodeURIComponent(runId)}`, { method: "DELETE" }, { workspaceId });
38
+ export async function deleteRun(http, runId) {
39
+ await http.request(`/api/runs/${encodeURIComponent(runId)}`, { method: "DELETE" });
35
40
  }
36
41
  export async function whoami(http) {
37
42
  return http.request("/api/whoami");
38
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
+ }
39
100
  function hasRun(value) {
40
101
  return Boolean(value && typeof value === "object" && "run" in value);
41
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,2 +1,16 @@
1
+ /**
2
+ * Canonical hosted antpath URL. Used as the default `baseUrl` for the
3
+ * SDK `AntpathClient` and the host-side CLI `--dashboard-url` flag.
4
+ *
5
+ * A single canonical default is not a "footnote mode" — it is the
6
+ * canonical product. Self-hosted deployments override via the
7
+ * explicit `baseUrl` / `--dashboard-url` parameter. The value lives in
8
+ * source (no env-var override) so the agent reading the SDK call site
9
+ * can see exactly where the call goes.
10
+ *
11
+ * See `references/development-principles.md` (Agent-first surface
12
+ * design, Concrete rule 3).
13
+ */
14
+ export declare const ANTPATH_DEFAULT_BASE_URL = "https://antpath.ai";
1
15
  export declare function stableStringify(value: unknown): string;
2
16
  export declare function sha256(value: unknown): string;
@@ -1,4 +1,18 @@
1
1
  import { createHash } from "node:crypto";
2
+ /**
3
+ * Canonical hosted antpath URL. Used as the default `baseUrl` for the
4
+ * SDK `AntpathClient` and the host-side CLI `--dashboard-url` flag.
5
+ *
6
+ * A single canonical default is not a "footnote mode" — it is the
7
+ * canonical product. Self-hosted deployments override via the
8
+ * explicit `baseUrl` / `--dashboard-url` parameter. The value lives in
9
+ * source (no env-var override) so the agent reading the SDK call site
10
+ * can see exactly where the call goes.
11
+ *
12
+ * See `references/development-principles.md` (Agent-first surface
13
+ * design, Concrete rule 3).
14
+ */
15
+ export const ANTPATH_DEFAULT_BASE_URL = "https://antpath.ai";
2
16
  export function stableStringify(value) {
3
17
  return JSON.stringify(sortValue(value));
4
18
  }
@@ -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;
@@ -125,6 +126,21 @@ export interface PlatformRunSubmissionRequest {
125
126
  */
126
127
  readonly proxyEndpoints?: readonly PlatformProxyEndpoint[];
127
128
  }
129
+ /**
130
+ * Wire shape posted by the SDK and CLI. `workspaceId` is **omitted by
131
+ * design** — token-authenticated clients never name the workspace
132
+ * because it is derived from their API token on the server. The BFF
133
+ * route resolves the workspace from the token and injects it before
134
+ * calling `parseRunSubmissionRequest`. The dashboard UI (Auth.js user
135
+ * principal, multi-workspace) is the only caller that supplies
136
+ * `workspaceId` itself.
137
+ *
138
+ * See `references/development-principles.md` (Agent-first surface
139
+ * design, Concrete rule 3).
140
+ */
141
+ export type PlatformRunSubmissionInput = Omit<PlatformRunSubmissionRequest, "workspaceId"> & {
142
+ readonly workspaceId?: string;
143
+ };
128
144
  /**
129
145
  * Default caps for a proxy endpoint when the submission doesn't specify
130
146
  * one. Conservative on purpose. Operators can override the platform-
@@ -140,3 +156,42 @@ export declare const PROXY_ENDPOINT_DEFAULTS: {
140
156
  readonly responseByteBudget: number;
141
157
  };
142
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
- return input.map((entry, index) => parseMcpServer(entry, `secrets.mcpServers[${index}]`));
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,129 @@ 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
+ return input.map((item, index) => parseSkillRef(item, `submission.skills[${index}]`));
797
+ }
798
+ function parseFlatMcpServers(input) {
799
+ if (input === undefined) {
800
+ return [];
801
+ }
802
+ if (!Array.isArray(input)) {
803
+ throw new Error("submission.mcpServers must be an array of {name, url} objects");
804
+ }
805
+ const seen = new Set();
806
+ return input.map((item, index) => {
807
+ const ref = parseMcpServerRef(item, `submission.mcpServers[${index}]`);
808
+ if (seen.has(ref.name)) {
809
+ throw new Error(`submission.mcpServers duplicate name: ${ref.name}`);
810
+ }
811
+ seen.add(ref.name);
812
+ return ref;
813
+ });
814
+ }
681
815
  //# sourceMappingURL=submission.js.map