antpath 0.6.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_shared/blueprint.d.ts +66 -7
- package/dist/_shared/blueprint.js +53 -8
- package/dist/_shared/operations.d.ts +52 -0
- package/dist/_shared/operations.js +83 -0
- package/dist/_shared/proxy-protocol.d.ts +10 -2
- package/dist/_shared/proxy-protocol.js +3 -2
- package/dist/_shared/runtime-types.d.ts +19 -0
- package/dist/_shared/submission.d.ts +21 -0
- package/dist/_shared/submission.js +102 -4
- package/dist/blueprint.d.ts +15 -6
- package/dist/blueprint.js.map +1 -1
- package/dist/bundle.d.ts +16 -1
- package/dist/bundle.js +32 -0
- package/dist/bundle.js.map +1 -1
- package/dist/cli.mjs +169 -38
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/client.d.ts +119 -24
- package/dist/client.js +198 -52
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +8 -6
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/proxy-endpoint.d.ts +131 -0
- package/dist/proxy-endpoint.js +147 -0
- package/dist/proxy-endpoint.js.map +1 -0
- package/dist/skill.d.ts +203 -31
- package/dist/skill.js +277 -30
- package/dist/skill.js.map +1 -1
- package/docs/credentials.md +34 -0
- package/docs/events.md +7 -0
- package/docs/mcp.md +28 -0
- package/docs/outputs.md +92 -12
- package/docs/quickstart.md +37 -0
- package/docs/skills.md +49 -0
- package/package.json +1 -1
|
@@ -4,11 +4,16 @@
|
|
|
4
4
|
* Concepts (mirrored in references/architecture-decisions.md):
|
|
5
5
|
*
|
|
6
6
|
* - `SkillRef` is the wire-level reference to a skill — either an
|
|
7
|
-
* `skl_*` id pointing at a workspace-uploaded bundle,
|
|
8
|
-
* `{vendor, skillId, version}` reference to a provider built-in
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* `skl_*` id pointing at a workspace-uploaded bundle, a
|
|
8
|
+
* `{vendor, skillId, version}` reference to a provider built-in, or
|
|
9
|
+
* a `{slot, name, contentHash}` reference to per-run bytes attached
|
|
10
|
+
* as a multipart part on the submitRun call (and torn down at run
|
|
11
|
+
* terminal). The three shapes are discriminated by `kind` so
|
|
12
|
+
* consumers branch mechanically and providers can never accidentally
|
|
13
|
+
* be looked up in `skill_bundles`. Transient refs do NOT round-trip
|
|
14
|
+
* through JSON (bytes can't be serialised back); `parseBlueprint`
|
|
15
|
+
* therefore rejects them while the BFF multipart submission parser
|
|
16
|
+
* accepts them.
|
|
12
17
|
*
|
|
13
18
|
* - `McpServerRef` is the non-secret part of an MCP server declaration:
|
|
14
19
|
* `name` and `url`. Bearer / cookie / per-request headers travel in
|
|
@@ -73,7 +78,7 @@ export declare const SKILL_BUNDLE_LIMITS: {
|
|
|
73
78
|
/** Stored directory mode. */
|
|
74
79
|
readonly defaultDirMode: 493;
|
|
75
80
|
};
|
|
76
|
-
export type SkillRef = WorkspaceSkillRef | ProviderSkillRef;
|
|
81
|
+
export type SkillRef = WorkspaceSkillRef | ProviderSkillRef | TransientSkillRef;
|
|
77
82
|
export interface WorkspaceSkillRef {
|
|
78
83
|
readonly kind: "workspace";
|
|
79
84
|
readonly id: string;
|
|
@@ -84,14 +89,68 @@ export interface ProviderSkillRef {
|
|
|
84
89
|
readonly skillId: string;
|
|
85
90
|
readonly version?: string;
|
|
86
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Per-run, non-persistent skill. The bytes ride alongside the JSON
|
|
94
|
+
* submission as a `name="skill:<slot>"` multipart body part and are
|
|
95
|
+
* uploaded by the BFF directly to the provider's per-run file storage.
|
|
96
|
+
* No `skill_bundles` row, no `run_skill_snapshots` entry, no platform
|
|
97
|
+
* storage stop — the BFF tears the file down at run terminal.
|
|
98
|
+
*
|
|
99
|
+
* `slot` is an SDK-assigned local id that pairs this ref with its
|
|
100
|
+
* multipart part within a single `submitRun` call. The BFF rejects any
|
|
101
|
+
* submission where the set of `kind:"transient"` refs doesn't have a
|
|
102
|
+
* strict 1:1 match against `name="skill:<slot>"` body parts.
|
|
103
|
+
*
|
|
104
|
+
* `contentHash` is a client-side advisory hash of the canonicalised zip
|
|
105
|
+
* (`sha256:<64 hex>`). The BFF recomputes server-side and rejects on
|
|
106
|
+
* mismatch; the value is also used by the janitor for orphan
|
|
107
|
+
* reconciliation. It MUST be deterministic across retries so the
|
|
108
|
+
* idempotency hash converges.
|
|
109
|
+
*
|
|
110
|
+
* Transient refs DO NOT round-trip safely through JSON — the bytes are
|
|
111
|
+
* not in the JSON and cannot be reconstructed. `parseBlueprint` rejects
|
|
112
|
+
* them (Blueprints are JSON-persistable and bytes are gone by the time
|
|
113
|
+
* `run.json` is loaded). `parseFlatSkills` accepts them (the BFF
|
|
114
|
+
* multipart parser is the only place that carries the matching bytes).
|
|
115
|
+
*/
|
|
116
|
+
export interface TransientSkillRef {
|
|
117
|
+
readonly kind: "transient";
|
|
118
|
+
readonly slot: string;
|
|
119
|
+
readonly name: string;
|
|
120
|
+
readonly contentHash: string;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Slot id format: `^[a-z][a-z0-9_-]{0,63}$`. The SDK generates these
|
|
124
|
+
* positionally as `transient-0`, `transient-1`, … but any client may
|
|
125
|
+
* supply an explicit slot as long as it matches the pattern and is
|
|
126
|
+
* unique within the submission.
|
|
127
|
+
*/
|
|
128
|
+
export declare const TRANSIENT_SLOT_PATTERN: RegExp;
|
|
129
|
+
/**
|
|
130
|
+
* Content-hash format: `^sha256:[0-9a-f]{64}$` (lowercase hex). The
|
|
131
|
+
* BFF accepts only this exact prefix; uppercase, base64, or any other
|
|
132
|
+
* digest is rejected so the wire form has one stable representation.
|
|
133
|
+
*/
|
|
134
|
+
export declare const TRANSIENT_CONTENT_HASH_PATTERN: RegExp;
|
|
87
135
|
export declare function isWorkspaceSkillRef(ref: SkillRef): ref is WorkspaceSkillRef;
|
|
88
136
|
export declare function isProviderSkillRef(ref: SkillRef): ref is ProviderSkillRef;
|
|
137
|
+
export declare function isTransientSkillRef(ref: SkillRef): ref is TransientSkillRef;
|
|
138
|
+
/**
|
|
139
|
+
* Options accepted by `parseSkillRef`. The default (`allowTransient: true`)
|
|
140
|
+
* is the BFF submission parser path. The Blueprint parser passes
|
|
141
|
+
* `allowTransient: false` because Blueprints can be persisted to disk
|
|
142
|
+
* (`antpath run --config run.json`) and the bytes carrying the
|
|
143
|
+
* transient slot would already be gone.
|
|
144
|
+
*/
|
|
145
|
+
export interface ParseSkillRefOptions {
|
|
146
|
+
readonly allowTransient?: boolean;
|
|
147
|
+
}
|
|
89
148
|
/**
|
|
90
149
|
* Parse a `SkillRef` from untrusted input. Used by the BFF run parser
|
|
91
150
|
* and by the operations module when deserialising API responses. Throws
|
|
92
151
|
* with a precise path so the caller can surface a usable error.
|
|
93
152
|
*/
|
|
94
|
-
export declare function parseSkillRef(input: unknown, path: string): SkillRef;
|
|
153
|
+
export declare function parseSkillRef(input: unknown, path: string, options?: ParseSkillRefOptions): SkillRef;
|
|
95
154
|
/**
|
|
96
155
|
* Manifest entry persisted in `skill_bundles.manifest` and
|
|
97
156
|
* `run_skill_snapshots.manifest`. `path` is forward-slash, relative,
|
|
@@ -4,11 +4,16 @@
|
|
|
4
4
|
* Concepts (mirrored in references/architecture-decisions.md):
|
|
5
5
|
*
|
|
6
6
|
* - `SkillRef` is the wire-level reference to a skill — either an
|
|
7
|
-
* `skl_*` id pointing at a workspace-uploaded bundle,
|
|
8
|
-
* `{vendor, skillId, version}` reference to a provider built-in
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* `skl_*` id pointing at a workspace-uploaded bundle, a
|
|
8
|
+
* `{vendor, skillId, version}` reference to a provider built-in, or
|
|
9
|
+
* a `{slot, name, contentHash}` reference to per-run bytes attached
|
|
10
|
+
* as a multipart part on the submitRun call (and torn down at run
|
|
11
|
+
* terminal). The three shapes are discriminated by `kind` so
|
|
12
|
+
* consumers branch mechanically and providers can never accidentally
|
|
13
|
+
* be looked up in `skill_bundles`. Transient refs do NOT round-trip
|
|
14
|
+
* through JSON (bytes can't be serialised back); `parseBlueprint`
|
|
15
|
+
* therefore rejects them while the BFF multipart submission parser
|
|
16
|
+
* accepts them.
|
|
12
17
|
*
|
|
13
18
|
* - `McpServerRef` is the non-secret part of an MCP server declaration:
|
|
14
19
|
* `name` and `url`. Bearer / cookie / per-request headers travel in
|
|
@@ -78,18 +83,34 @@ export const SKILL_BUNDLE_LIMITS = {
|
|
|
78
83
|
/** Stored directory mode. */
|
|
79
84
|
defaultDirMode: 0o755
|
|
80
85
|
};
|
|
86
|
+
/**
|
|
87
|
+
* Slot id format: `^[a-z][a-z0-9_-]{0,63}$`. The SDK generates these
|
|
88
|
+
* positionally as `transient-0`, `transient-1`, … but any client may
|
|
89
|
+
* supply an explicit slot as long as it matches the pattern and is
|
|
90
|
+
* unique within the submission.
|
|
91
|
+
*/
|
|
92
|
+
export const TRANSIENT_SLOT_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
93
|
+
/**
|
|
94
|
+
* Content-hash format: `^sha256:[0-9a-f]{64}$` (lowercase hex). The
|
|
95
|
+
* BFF accepts only this exact prefix; uppercase, base64, or any other
|
|
96
|
+
* digest is rejected so the wire form has one stable representation.
|
|
97
|
+
*/
|
|
98
|
+
export const TRANSIENT_CONTENT_HASH_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
81
99
|
export function isWorkspaceSkillRef(ref) {
|
|
82
100
|
return ref.kind === "workspace";
|
|
83
101
|
}
|
|
84
102
|
export function isProviderSkillRef(ref) {
|
|
85
103
|
return ref.kind === "provider";
|
|
86
104
|
}
|
|
105
|
+
export function isTransientSkillRef(ref) {
|
|
106
|
+
return ref.kind === "transient";
|
|
107
|
+
}
|
|
87
108
|
/**
|
|
88
109
|
* Parse a `SkillRef` from untrusted input. Used by the BFF run parser
|
|
89
110
|
* and by the operations module when deserialising API responses. Throws
|
|
90
111
|
* with a precise path so the caller can surface a usable error.
|
|
91
112
|
*/
|
|
92
|
-
export function parseSkillRef(input, path) {
|
|
113
|
+
export function parseSkillRef(input, path, options = {}) {
|
|
93
114
|
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
94
115
|
throw new Error(`${path} must be a SkillRef object`);
|
|
95
116
|
}
|
|
@@ -132,7 +153,31 @@ export function parseSkillRef(input, path) {
|
|
|
132
153
|
...(version !== undefined ? { version } : {})
|
|
133
154
|
};
|
|
134
155
|
}
|
|
135
|
-
|
|
156
|
+
if (kind === "transient") {
|
|
157
|
+
if (options.allowTransient === false) {
|
|
158
|
+
throw new Error(`${path} carries a transient SkillRef, which cannot round-trip through JSON — ` +
|
|
159
|
+
`transient skills must be supplied at submitRun time with their bytes attached`);
|
|
160
|
+
}
|
|
161
|
+
for (const key of Object.keys(record)) {
|
|
162
|
+
if (key !== "kind" && key !== "slot" && key !== "name" && key !== "contentHash") {
|
|
163
|
+
throw new Error(`${path} contains unexpected field for transient SkillRef: ${key}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const slot = record.slot;
|
|
167
|
+
if (typeof slot !== "string" || !TRANSIENT_SLOT_PATTERN.test(slot)) {
|
|
168
|
+
throw new Error(`${path}.slot must match ${TRANSIENT_SLOT_PATTERN.source}`);
|
|
169
|
+
}
|
|
170
|
+
const name = record.name;
|
|
171
|
+
if (typeof name !== "string" || !SKILL_NAME_PATTERN.test(name)) {
|
|
172
|
+
throw new Error(`${path}.name must match ${SKILL_NAME_PATTERN.source}`);
|
|
173
|
+
}
|
|
174
|
+
const contentHash = record.contentHash;
|
|
175
|
+
if (typeof contentHash !== "string" || !TRANSIENT_CONTENT_HASH_PATTERN.test(contentHash)) {
|
|
176
|
+
throw new Error(`${path}.contentHash must match ${TRANSIENT_CONTENT_HASH_PATTERN.source}`);
|
|
177
|
+
}
|
|
178
|
+
return { kind: "transient", slot, name, contentHash };
|
|
179
|
+
}
|
|
180
|
+
throw new Error(`${path}.kind must be 'workspace', 'provider', or 'transient'`);
|
|
136
181
|
}
|
|
137
182
|
export class SkillBundleValidationError extends Error {
|
|
138
183
|
constructor(message) {
|
|
@@ -466,7 +511,7 @@ function parseBlueprintSkills(value) {
|
|
|
466
511
|
if (!Array.isArray(value)) {
|
|
467
512
|
throw new Error("Blueprint.skills must be an array");
|
|
468
513
|
}
|
|
469
|
-
return value.map((item, index) => parseSkillRef(item, `Blueprint.skills[${index}]
|
|
514
|
+
return value.map((item, index) => parseSkillRef(item, `Blueprint.skills[${index}]`, { allowTransient: false }));
|
|
470
515
|
}
|
|
471
516
|
function parseBlueprintMcpServers(value) {
|
|
472
517
|
if (value === undefined) {
|
|
@@ -22,7 +22,39 @@ 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
|
+
/**
|
|
26
|
+
* Stream the per-run archive zip from the BFF. Returns the raw
|
|
27
|
+
* `Response` so callers can pipe the body to disk without buffering
|
|
28
|
+
* the whole archive in memory.
|
|
29
|
+
*
|
|
30
|
+
* The archive lifecycle contract lives in
|
|
31
|
+
* `apps/dashboard/src/server/run-archive.ts` and
|
|
32
|
+
* `packages/sdk/docs/outputs.md`. Pre-session runs reject with HTTP
|
|
33
|
+
* 409 `run_not_started`; mid-session and terminal both produce the
|
|
34
|
+
* same archive layout and differ only in `manifest.json`'s `source`
|
|
35
|
+
* + `partial` fields.
|
|
36
|
+
*/
|
|
37
|
+
export declare function downloadRunArchive(http: HttpClient, runId: string): Promise<Response>;
|
|
25
38
|
export declare function submitRunFlat(http: HttpClient, request: PlatformFlatRunSubmissionInput): Promise<Run>;
|
|
39
|
+
/**
|
|
40
|
+
* Multipart variant of `submitRunFlat` for runs that carry transient
|
|
41
|
+
* (per-run) skill bundles. The JSON submission travels as the
|
|
42
|
+
* `submission` part; each `TransientSkillRef.slot` in
|
|
43
|
+
* `request.submission.skills` MUST be mirrored by exactly one
|
|
44
|
+
* `skill:<slot>` part with the bundle bytes.
|
|
45
|
+
*
|
|
46
|
+
* The BFF re-canonicalises and re-hashes each bundle; a `contentHash`
|
|
47
|
+
* mismatch between the JSON ref and the recomputed hash of the bytes
|
|
48
|
+
* is rejected with a deterministic error.
|
|
49
|
+
*
|
|
50
|
+
* Bytes never persist on antpath storage; the dashboard BFF uploads
|
|
51
|
+
* them straight to Anthropic Files for the lifetime of the run.
|
|
52
|
+
*/
|
|
53
|
+
export declare function submitRunFlatMultipart(http: HttpClient, request: PlatformFlatRunSubmissionInput, bundles: ReadonlyArray<{
|
|
54
|
+
readonly slot: string;
|
|
55
|
+
readonly bytes: Uint8Array;
|
|
56
|
+
readonly filename: string;
|
|
57
|
+
}>): Promise<Run>;
|
|
26
58
|
/**
|
|
27
59
|
* Upload a workspace skill bundle as a zip blob. The dashboard BFF runs
|
|
28
60
|
* the two-phase flow internally (insert pending row, stream bytes into
|
|
@@ -40,3 +72,23 @@ export declare function createSkillBundle(http: HttpClient, args: {
|
|
|
40
72
|
export declare function listSkills(http: HttpClient): Promise<readonly Skill[]>;
|
|
41
73
|
export declare function getSkill(http: HttpClient, skillId: string): Promise<Skill>;
|
|
42
74
|
export declare function deleteSkill(http: HttpClient, skillId: string): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Lookup a live workspace skill by `(name, contentHash)`. Returns the
|
|
77
|
+
* matching `Skill` record or null when no live row carries that hash.
|
|
78
|
+
*
|
|
79
|
+
* `contentHash` is the wire format `sha256:<hex>` as returned by
|
|
80
|
+
* `hashSkillBundle`. This powers `Skill.uploadIfChanged` — the SDK
|
|
81
|
+
* computes the hash locally and calls this function to skip the upload
|
|
82
|
+
* when the bytes already exist.
|
|
83
|
+
*/
|
|
84
|
+
export declare function findSkillByHash(http: HttpClient, args: {
|
|
85
|
+
readonly name: string;
|
|
86
|
+
readonly contentHash: string;
|
|
87
|
+
}): Promise<Skill | null>;
|
|
88
|
+
/**
|
|
89
|
+
* Lookup a live workspace skill by `name`. Returns the matching `Skill`
|
|
90
|
+
* record or null when no live row carries that name. Implemented as a
|
|
91
|
+
* list-and-filter on the existing `/api/skills` endpoint — the
|
|
92
|
+
* indexed by-hash route is reserved for `uploadIfChanged`.
|
|
93
|
+
*/
|
|
94
|
+
export declare function findSkillByName(http: HttpClient, name: string): Promise<Skill | null>;
|
|
@@ -41,6 +41,22 @@ export async function deleteRun(http, runId) {
|
|
|
41
41
|
export async function whoami(http) {
|
|
42
42
|
return http.request("/api/whoami");
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Stream the per-run archive zip from the BFF. Returns the raw
|
|
46
|
+
* `Response` so callers can pipe the body to disk without buffering
|
|
47
|
+
* the whole archive in memory.
|
|
48
|
+
*
|
|
49
|
+
* The archive lifecycle contract lives in
|
|
50
|
+
* `apps/dashboard/src/server/run-archive.ts` and
|
|
51
|
+
* `packages/sdk/docs/outputs.md`. Pre-session runs reject with HTTP
|
|
52
|
+
* 409 `run_not_started`; mid-session and terminal both produce the
|
|
53
|
+
* same archive layout and differ only in `manifest.json`'s `source`
|
|
54
|
+
* + `partial` fields.
|
|
55
|
+
*/
|
|
56
|
+
export async function downloadRunArchive(http, runId) {
|
|
57
|
+
const { response } = await http.download(`/api/runs/${encodeURIComponent(runId)}/download`);
|
|
58
|
+
return response;
|
|
59
|
+
}
|
|
44
60
|
// ===========================================================================
|
|
45
61
|
// Flat (Skill / McpServer / Blueprint) operations
|
|
46
62
|
// ===========================================================================
|
|
@@ -50,6 +66,46 @@ export async function submitRunFlat(http, request) {
|
|
|
50
66
|
body: JSON.stringify(request)
|
|
51
67
|
});
|
|
52
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Multipart variant of `submitRunFlat` for runs that carry transient
|
|
71
|
+
* (per-run) skill bundles. The JSON submission travels as the
|
|
72
|
+
* `submission` part; each `TransientSkillRef.slot` in
|
|
73
|
+
* `request.submission.skills` MUST be mirrored by exactly one
|
|
74
|
+
* `skill:<slot>` part with the bundle bytes.
|
|
75
|
+
*
|
|
76
|
+
* The BFF re-canonicalises and re-hashes each bundle; a `contentHash`
|
|
77
|
+
* mismatch between the JSON ref and the recomputed hash of the bytes
|
|
78
|
+
* is rejected with a deterministic error.
|
|
79
|
+
*
|
|
80
|
+
* Bytes never persist on antpath storage; the dashboard BFF uploads
|
|
81
|
+
* them straight to Anthropic Files for the lifetime of the run.
|
|
82
|
+
*/
|
|
83
|
+
export async function submitRunFlatMultipart(http, request, bundles) {
|
|
84
|
+
if (!Array.isArray(bundles) || bundles.length === 0) {
|
|
85
|
+
throw new Error("submitRunFlatMultipart: bundles must be a non-empty array");
|
|
86
|
+
}
|
|
87
|
+
const form = new FormData();
|
|
88
|
+
// Submission rides as a typed JSON Blob so the BFF reads
|
|
89
|
+
// `multipart["submission"]` with the right content-type and never
|
|
90
|
+
// has to re-detect the body shape.
|
|
91
|
+
form.append("submission", new Blob([JSON.stringify(request)], { type: "application/json" }), "submission.json");
|
|
92
|
+
const seen = new Set();
|
|
93
|
+
for (const bundle of bundles) {
|
|
94
|
+
if (typeof bundle.slot !== "string" || !bundle.slot) {
|
|
95
|
+
throw new Error("submitRunFlatMultipart: each bundle must have a non-empty slot id");
|
|
96
|
+
}
|
|
97
|
+
if (seen.has(bundle.slot)) {
|
|
98
|
+
throw new Error(`submitRunFlatMultipart: duplicate transient skill slot "${bundle.slot}"`);
|
|
99
|
+
}
|
|
100
|
+
seen.add(bundle.slot);
|
|
101
|
+
const blob = toBlob(bundle.bytes, "application/zip");
|
|
102
|
+
form.append(`skill:${bundle.slot}`, blob, bundle.filename);
|
|
103
|
+
}
|
|
104
|
+
return http.request("/api/runs", {
|
|
105
|
+
method: "POST",
|
|
106
|
+
body: form
|
|
107
|
+
});
|
|
108
|
+
}
|
|
53
109
|
/**
|
|
54
110
|
* Upload a workspace skill bundle as a zip blob. The dashboard BFF runs
|
|
55
111
|
* the two-phase flow internally (insert pending row, stream bytes into
|
|
@@ -85,6 +141,33 @@ export async function deleteSkill(http, skillId) {
|
|
|
85
141
|
method: "DELETE"
|
|
86
142
|
});
|
|
87
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Lookup a live workspace skill by `(name, contentHash)`. Returns the
|
|
146
|
+
* matching `Skill` record or null when no live row carries that hash.
|
|
147
|
+
*
|
|
148
|
+
* `contentHash` is the wire format `sha256:<hex>` as returned by
|
|
149
|
+
* `hashSkillBundle`. This powers `Skill.uploadIfChanged` — the SDK
|
|
150
|
+
* computes the hash locally and calls this function to skip the upload
|
|
151
|
+
* when the bytes already exist.
|
|
152
|
+
*/
|
|
153
|
+
export async function findSkillByHash(http, args) {
|
|
154
|
+
const params = new URLSearchParams({
|
|
155
|
+
name: args.name,
|
|
156
|
+
content_hash: args.contentHash
|
|
157
|
+
});
|
|
158
|
+
const result = await http.request(`/api/skills/by-hash?${params.toString()}`);
|
|
159
|
+
return result.skill ?? null;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Lookup a live workspace skill by `name`. Returns the matching `Skill`
|
|
163
|
+
* record or null when no live row carries that name. Implemented as a
|
|
164
|
+
* list-and-filter on the existing `/api/skills` endpoint — the
|
|
165
|
+
* indexed by-hash route is reserved for `uploadIfChanged`.
|
|
166
|
+
*/
|
|
167
|
+
export async function findSkillByName(http, name) {
|
|
168
|
+
const skills = await listSkills(http);
|
|
169
|
+
return skills.find((skill) => skill.name === name) ?? null;
|
|
170
|
+
}
|
|
88
171
|
function unwrapSkill(result) {
|
|
89
172
|
if (result && typeof result === "object" && "skill" in result) {
|
|
90
173
|
return result.skill;
|
|
@@ -67,8 +67,16 @@ export interface ProxyIndexEntry {
|
|
|
67
67
|
* The actual auth value lives in the run's Vault bundle under
|
|
68
68
|
* `secrets.proxyEndpointAuth[i].value` and is never reflected back
|
|
69
69
|
* into the container or index file.
|
|
70
|
+
*
|
|
71
|
+
* The `none` variant declares an upstream that takes no auth (public
|
|
72
|
+
* APIs like Wikimedia Commons or NASA Images). It still routes through
|
|
73
|
+
* the proxy for unified egress, audit, and budget enforcement, but
|
|
74
|
+
* carries no `proxyEndpointAuth[]` entry and the BFF injects no
|
|
75
|
+
* header or query value.
|
|
70
76
|
*/
|
|
71
77
|
export type ProxyAuthShape = {
|
|
78
|
+
readonly type: "none";
|
|
79
|
+
} | {
|
|
72
80
|
readonly type: "bearer";
|
|
73
81
|
} | {
|
|
74
82
|
readonly type: "basic";
|
|
@@ -82,7 +90,7 @@ export type ProxyAuthShape = {
|
|
|
82
90
|
export type ProxyAuthType = ProxyAuthShape["type"];
|
|
83
91
|
/**
|
|
84
92
|
* Header name (lowercase) that an upstream auth shape uses as its
|
|
85
|
-
* carrier. Returns `undefined` for query-based auth.
|
|
93
|
+
* carrier. Returns `undefined` for query-based and keyless auth.
|
|
86
94
|
*
|
|
87
95
|
* Used by the submission parser to forbid `allowHeaders` from listing
|
|
88
96
|
* the auth header (avoids leaks via caller-supplied headers), and by
|
|
@@ -92,7 +100,7 @@ export type ProxyAuthType = ProxyAuthShape["type"];
|
|
|
92
100
|
export declare function authShapeHeaderName(shape: ProxyAuthShape): string | undefined;
|
|
93
101
|
/**
|
|
94
102
|
* Query-string key that an upstream query-based auth shape uses as its
|
|
95
|
-
* carrier. Returns `undefined` for non-query shapes.
|
|
103
|
+
* carrier. Returns `undefined` for non-query shapes (including "none").
|
|
96
104
|
*/
|
|
97
105
|
export declare function authShapeQueryName(shape: ProxyAuthShape): string | undefined;
|
|
98
106
|
/**
|
|
@@ -66,7 +66,7 @@ export const PROXY_ERROR_CODES = [
|
|
|
66
66
|
];
|
|
67
67
|
/**
|
|
68
68
|
* Header name (lowercase) that an upstream auth shape uses as its
|
|
69
|
-
* carrier. Returns `undefined` for query-based auth.
|
|
69
|
+
* carrier. Returns `undefined` for query-based and keyless auth.
|
|
70
70
|
*
|
|
71
71
|
* Used by the submission parser to forbid `allowHeaders` from listing
|
|
72
72
|
* the auth header (avoids leaks via caller-supplied headers), and by
|
|
@@ -81,12 +81,13 @@ export function authShapeHeaderName(shape) {
|
|
|
81
81
|
case "header":
|
|
82
82
|
return shape.name.toLowerCase();
|
|
83
83
|
case "query":
|
|
84
|
+
case "none":
|
|
84
85
|
return undefined;
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
88
|
/**
|
|
88
89
|
* Query-string key that an upstream query-based auth shape uses as its
|
|
89
|
-
* carrier. Returns `undefined` for non-query shapes.
|
|
90
|
+
* carrier. Returns `undefined` for non-query shapes (including "none").
|
|
90
91
|
*/
|
|
91
92
|
export function authShapeQueryName(shape) {
|
|
92
93
|
return shape.type === "query" ? shape.name : undefined;
|
|
@@ -71,6 +71,25 @@ export interface WhoAmI {
|
|
|
71
71
|
readonly tokenId?: string;
|
|
72
72
|
readonly tokenName?: string | null;
|
|
73
73
|
readonly scopes?: readonly string[];
|
|
74
|
+
/**
|
|
75
|
+
* Workspace-level caps the BFF will enforce on subsequent calls.
|
|
76
|
+
* Surfaced so consumers (e.g. broll's app-side admission gate) can
|
|
77
|
+
* decide whether to keep their own gate or rely on platform headers.
|
|
78
|
+
* All fields optional — older BFFs may omit. Numbers are concrete
|
|
79
|
+
* snapshots at the time of the `whoami` call.
|
|
80
|
+
*/
|
|
81
|
+
readonly caps?: {
|
|
82
|
+
/** Token-bucket cap on POST /api/runs per minute, per workspace. */
|
|
83
|
+
readonly runSubmitPerMinute?: number;
|
|
84
|
+
/** Hard cap on concurrent non-terminal runs the workspace may hold. */
|
|
85
|
+
readonly maxConcurrentRuns?: number;
|
|
86
|
+
/** Storage cap (bytes) on captured output objects, workspace-wide. */
|
|
87
|
+
readonly storageCapBytes?: number;
|
|
88
|
+
/** Current captured-output usage in bytes. */
|
|
89
|
+
readonly storageUsedBytes?: number;
|
|
90
|
+
/** Wall-clock ceiling on a single run before forced termination. */
|
|
91
|
+
readonly maxRunDurationMs?: number;
|
|
92
|
+
};
|
|
74
93
|
readonly [key: string]: unknown;
|
|
75
94
|
}
|
|
76
95
|
/**
|
|
@@ -175,6 +175,27 @@ export interface PlatformFlatSubmission {
|
|
|
175
175
|
readonly mcpServers: readonly McpServerRef[];
|
|
176
176
|
readonly environment?: PlatformTemplateEnvironment;
|
|
177
177
|
readonly metadata?: Record<string, JsonValue>;
|
|
178
|
+
/**
|
|
179
|
+
* Opt-in container paths to capture as `output_objects` at session
|
|
180
|
+
* terminal. When omitted, the worker still persists run metadata
|
|
181
|
+
* (status, events, snapshots, cleanup state) but does not capture
|
|
182
|
+
* any container file bytes. When present, the worker drives a
|
|
183
|
+
* synthetic agent turn at session terminal that instructs the agent
|
|
184
|
+
* to register every file under these paths via the Anthropic Files
|
|
185
|
+
* API, then walks the resulting list and copies bytes into private
|
|
186
|
+
* Supabase Storage.
|
|
187
|
+
*
|
|
188
|
+
* Validation:
|
|
189
|
+
* - Absolute UNIX paths only (starts with `/`).
|
|
190
|
+
* - No `..` segments, no NUL bytes, no embedded newlines.
|
|
191
|
+
* - Max 32 entries.
|
|
192
|
+
* - Max 512 bytes per entry.
|
|
193
|
+
*
|
|
194
|
+
* Entries are normalised (collapse `/+`, drop trailing `/` except
|
|
195
|
+
* for `/`) and deduplicated. The normalised list is what travels in
|
|
196
|
+
* the idempotency hash and the run snapshot.
|
|
197
|
+
*/
|
|
198
|
+
readonly outputDirs?: readonly string[];
|
|
178
199
|
}
|
|
179
200
|
export interface PlatformFlatRunSubmissionRequest {
|
|
180
201
|
readonly workspaceId: string;
|
|
@@ -272,6 +272,9 @@ function parseProxyAuthShape(input, field) {
|
|
|
272
272
|
const value = requireRecord(input, field);
|
|
273
273
|
const type = requireString(value.type, `${field}.type`);
|
|
274
274
|
switch (type) {
|
|
275
|
+
case "none":
|
|
276
|
+
assertOnlyKeys(value, field, ["type"]);
|
|
277
|
+
return { type: "none" };
|
|
275
278
|
case "bearer":
|
|
276
279
|
assertOnlyKeys(value, field, ["type"]);
|
|
277
280
|
return { type: "bearer" };
|
|
@@ -293,7 +296,7 @@ function parseProxyAuthShape(input, field) {
|
|
|
293
296
|
return { type: "query", name };
|
|
294
297
|
}
|
|
295
298
|
default:
|
|
296
|
-
throw new Error(`${field}.type must be one of: bearer, basic, header, query`);
|
|
299
|
+
throw new Error(`${field}.type must be one of: none, bearer, basic, header, query`);
|
|
297
300
|
}
|
|
298
301
|
}
|
|
299
302
|
function parseProxyMethods(input, field) {
|
|
@@ -382,6 +385,16 @@ function crossValidateProxyEndpointsAndAuth(endpoints, auth) {
|
|
|
382
385
|
const authByName = new Map(authList.map((a) => [a.name, a]));
|
|
383
386
|
for (const endpoint of endpointsList) {
|
|
384
387
|
const authEntry = authByName.get(endpoint.name);
|
|
388
|
+
if (endpoint.authShape.type === "none") {
|
|
389
|
+
// Keyless endpoints carry no auth value. Reject any matching
|
|
390
|
+
// auth entry so callers don't accidentally ship a secret bound
|
|
391
|
+
// to a "none" endpoint (which would be silently ignored at
|
|
392
|
+
// request time — confusing and a leak risk).
|
|
393
|
+
if (authEntry) {
|
|
394
|
+
throw new Error(`proxyEndpoints[${endpoint.name}] has authShape "none" but a matching secrets.proxyEndpointAuth entry was supplied; remove the auth entry`);
|
|
395
|
+
}
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
385
398
|
if (!authEntry) {
|
|
386
399
|
throw new Error(`proxyEndpoints[${endpoint.name}] has no matching secrets.proxyEndpointAuth entry`);
|
|
387
400
|
}
|
|
@@ -742,7 +755,8 @@ function parseFlatSubmission(input) {
|
|
|
742
755
|
"skills",
|
|
743
756
|
"mcpServers",
|
|
744
757
|
"environment",
|
|
745
|
-
"metadata"
|
|
758
|
+
"metadata",
|
|
759
|
+
"outputDirs"
|
|
746
760
|
]);
|
|
747
761
|
for (const key of Object.keys(value)) {
|
|
748
762
|
if (!allowed.has(key)) {
|
|
@@ -756,6 +770,7 @@ function parseFlatSubmission(input) {
|
|
|
756
770
|
const mcpServers = parseFlatMcpServers(value.mcpServers);
|
|
757
771
|
const environment = parseTemplateEnvironment(value.environment);
|
|
758
772
|
const metadata = optionalJsonRecord(value.metadata, "submission.metadata");
|
|
773
|
+
const outputDirs = parseOutputDirs(value.outputDirs);
|
|
759
774
|
return {
|
|
760
775
|
model,
|
|
761
776
|
...(system ? { system } : {}),
|
|
@@ -763,9 +778,84 @@ function parseFlatSubmission(input) {
|
|
|
763
778
|
skills,
|
|
764
779
|
mcpServers,
|
|
765
780
|
...(environment ? { environment } : {}),
|
|
766
|
-
...(metadata ? { metadata } : {})
|
|
781
|
+
...(metadata ? { metadata } : {}),
|
|
782
|
+
...(outputDirs ? { outputDirs } : {})
|
|
767
783
|
};
|
|
768
784
|
}
|
|
785
|
+
/**
|
|
786
|
+
* Maximum number of `outputDirs` entries accepted per submission.
|
|
787
|
+
*
|
|
788
|
+
* 32 is enough room for the typical "one or two capture roots" pattern
|
|
789
|
+
* plus a generous margin for legitimate multi-root use cases (per-tool
|
|
790
|
+
* output directory + scratch state + logs, repeated across a few
|
|
791
|
+
* subdirectories), without inviting abuse of the synthetic-turn path
|
|
792
|
+
* the worker drives at session terminal.
|
|
793
|
+
*/
|
|
794
|
+
const MAX_OUTPUT_DIRS = 32;
|
|
795
|
+
/**
|
|
796
|
+
* Maximum byte length of a single `outputDirs` entry (after UTF-8
|
|
797
|
+
* encoding). 512 bytes comfortably covers `/very/long/nested/path`
|
|
798
|
+
* style entries without letting a misuse smuggle large blobs through
|
|
799
|
+
* the field.
|
|
800
|
+
*/
|
|
801
|
+
const MAX_OUTPUT_DIR_BYTES = 512;
|
|
802
|
+
function parseOutputDirs(input) {
|
|
803
|
+
if (input === undefined) {
|
|
804
|
+
return undefined;
|
|
805
|
+
}
|
|
806
|
+
if (!Array.isArray(input)) {
|
|
807
|
+
throw new Error("submission.outputDirs must be an array of absolute UNIX paths");
|
|
808
|
+
}
|
|
809
|
+
if (input.length === 0) {
|
|
810
|
+
// Treat an empty array as omission so the idempotency hash matches
|
|
811
|
+
// the "no outputDirs" case.
|
|
812
|
+
return undefined;
|
|
813
|
+
}
|
|
814
|
+
if (input.length > MAX_OUTPUT_DIRS) {
|
|
815
|
+
throw new Error(`submission.outputDirs has ${input.length} entries; max is ${MAX_OUTPUT_DIRS}`);
|
|
816
|
+
}
|
|
817
|
+
const seen = new Set();
|
|
818
|
+
const normalised = [];
|
|
819
|
+
for (let i = 0; i < input.length; i++) {
|
|
820
|
+
const item = input[i];
|
|
821
|
+
if (typeof item !== "string") {
|
|
822
|
+
throw new Error(`submission.outputDirs[${i}] must be a string`);
|
|
823
|
+
}
|
|
824
|
+
if (item.length === 0) {
|
|
825
|
+
throw new Error(`submission.outputDirs[${i}] must be a non-empty absolute UNIX path`);
|
|
826
|
+
}
|
|
827
|
+
const bytes = new TextEncoder().encode(item).length;
|
|
828
|
+
if (bytes > MAX_OUTPUT_DIR_BYTES) {
|
|
829
|
+
throw new Error(`submission.outputDirs[${i}] exceeds ${MAX_OUTPUT_DIR_BYTES} bytes (got ${bytes})`);
|
|
830
|
+
}
|
|
831
|
+
if (!item.startsWith("/")) {
|
|
832
|
+
throw new Error(`submission.outputDirs[${i}] must be an absolute UNIX path (start with '/')`);
|
|
833
|
+
}
|
|
834
|
+
if (item.includes("\0")) {
|
|
835
|
+
throw new Error(`submission.outputDirs[${i}] must not contain NUL bytes`);
|
|
836
|
+
}
|
|
837
|
+
if (item.includes("\n") || item.includes("\r")) {
|
|
838
|
+
throw new Error(`submission.outputDirs[${i}] must not contain newline characters`);
|
|
839
|
+
}
|
|
840
|
+
const segments = item.split("/");
|
|
841
|
+
if (segments.includes("..")) {
|
|
842
|
+
throw new Error(`submission.outputDirs[${i}] must not contain '..' segments`);
|
|
843
|
+
}
|
|
844
|
+
const collapsed = segments
|
|
845
|
+
.filter((seg, idx) => seg.length > 0 || idx === 0)
|
|
846
|
+
.join("/");
|
|
847
|
+
const stripped = collapsed.length > 1 && collapsed.endsWith("/")
|
|
848
|
+
? collapsed.slice(0, -1)
|
|
849
|
+
: collapsed;
|
|
850
|
+
const canonical = stripped.length === 0 ? "/" : stripped;
|
|
851
|
+
if (seen.has(canonical)) {
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
seen.add(canonical);
|
|
855
|
+
normalised.push(canonical);
|
|
856
|
+
}
|
|
857
|
+
return normalised;
|
|
858
|
+
}
|
|
769
859
|
function parseFlatPrompt(input) {
|
|
770
860
|
if (typeof input === "string") {
|
|
771
861
|
if (input.length === 0) {
|
|
@@ -795,6 +885,7 @@ function parseFlatSkills(input) {
|
|
|
795
885
|
}
|
|
796
886
|
const seenWorkspace = new Set();
|
|
797
887
|
const seenProvider = new Set();
|
|
888
|
+
const seenTransientSlot = new Set();
|
|
798
889
|
return input.map((item, index) => {
|
|
799
890
|
const ref = parseSkillRef(item, `submission.skills[${index}]`);
|
|
800
891
|
if (ref.kind === "workspace") {
|
|
@@ -803,13 +894,20 @@ function parseFlatSkills(input) {
|
|
|
803
894
|
}
|
|
804
895
|
seenWorkspace.add(ref.id);
|
|
805
896
|
}
|
|
806
|
-
else {
|
|
897
|
+
else if (ref.kind === "provider") {
|
|
807
898
|
const key = `${ref.vendor}:${ref.skillId}:${ref.version ?? ""}`;
|
|
808
899
|
if (seenProvider.has(key)) {
|
|
809
900
|
throw new Error(`submission.skills duplicate provider skill: ${ref.vendor}:${ref.skillId}${ref.version ? `:${ref.version}` : ""}`);
|
|
810
901
|
}
|
|
811
902
|
seenProvider.add(key);
|
|
812
903
|
}
|
|
904
|
+
else {
|
|
905
|
+
// transient
|
|
906
|
+
if (seenTransientSlot.has(ref.slot)) {
|
|
907
|
+
throw new Error(`submission.skills duplicate transient slot: ${ref.slot}`);
|
|
908
|
+
}
|
|
909
|
+
seenTransientSlot.add(ref.slot);
|
|
910
|
+
}
|
|
813
911
|
return ref;
|
|
814
912
|
});
|
|
815
913
|
}
|