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
package/dist/skill.js
CHANGED
|
@@ -1,41 +1,88 @@
|
|
|
1
|
+
import { SKILL_NAME_PATTERN } from "./_shared/index.js";
|
|
2
|
+
import { bundleSkillFiles, hashSkillBundle } from "./bundle.js";
|
|
3
|
+
import { readDirectoryAsFiles } from "./node-fs.js";
|
|
1
4
|
/**
|
|
2
|
-
*
|
|
3
|
-
* a discriminated wire shape under `.ref`. Keeping a single class means
|
|
4
|
-
* `submitRun({ skills: [...] })` accepts a uniform array; the SDK
|
|
5
|
-
* normalises every entry to its `ref` before sending the wire payload.
|
|
5
|
+
* One `Skill` class, one mental model, three usage modes:
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* - **Workspace** — uploaded to the antpath platform and reused across
|
|
8
|
+
* runs.
|
|
9
|
+
* ```ts
|
|
10
|
+
* const persisted = await Skill.fromFiles({ name, files }).upload(client);
|
|
11
|
+
* // or
|
|
12
|
+
* const ref = Skill.fromId("skl_abc123");
|
|
13
|
+
* ```
|
|
8
14
|
*
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* - `client.skills.fromPath("./dir", { name })` — local directory; SDK
|
|
15
|
-
* reads, zips, and uploads (Node only).
|
|
15
|
+
* - **Provider built-in** — references a provider-side skill (e.g.
|
|
16
|
+
* Anthropic web-search). Not uploaded to your workspace.
|
|
17
|
+
* ```ts
|
|
18
|
+
* const websearch = Skill.provider({ vendor: "anthropic", skillId: "web-search" });
|
|
19
|
+
* ```
|
|
16
20
|
*
|
|
17
|
-
*
|
|
21
|
+
* - **Transient (per-run)** — built from local files, never persisted.
|
|
22
|
+
* The bytes ride alongside `submitRun` as a multipart part and the
|
|
23
|
+
* BFF tears them down at run terminal.
|
|
24
|
+
* ```ts
|
|
25
|
+
* const rules = await Skill.fromFiles({ name: "rules", files: {...} });
|
|
26
|
+
* await client.submitRun({ skills: [rules], ... });
|
|
27
|
+
* ```
|
|
18
28
|
*
|
|
19
|
-
*
|
|
20
|
-
* provider built-in skill (e.g. Anthropic web-search). Provider
|
|
21
|
-
* skills are NOT uploaded to your workspace and have no
|
|
22
|
-
* soft-delete semantics.
|
|
29
|
+
* ## Lifecycle of an unstaged transient Skill
|
|
23
30
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
31
|
+
* After `Skill.fromFiles(...)` returns, the Skill is "unstaged transient":
|
|
32
|
+
*
|
|
33
|
+
* 1. Passing it to `submitRun` uploads the bytes for that one run only
|
|
34
|
+
* and assigns a positional slot id (`transient-0`, `transient-1`, …).
|
|
35
|
+
* The same instance may be passed to multiple `submitRun` calls
|
|
36
|
+
* until it is consumed by `.upload(...)`.
|
|
37
|
+
*
|
|
38
|
+
* 2. Calling `await skill.upload(client)` persists the bundle to the
|
|
39
|
+
* workspace skill store and returns a **new** `Skill` instance
|
|
40
|
+
* whose `.ref.kind === "workspace"`. The **original** is marked
|
|
41
|
+
* **consumed**:
|
|
42
|
+
* - any further use in `submitRun` throws a clear validation error,
|
|
43
|
+
* - `.upload(...)` on the consumed instance throws,
|
|
44
|
+
* - `JSON.stringify` / `toJSON()` on the consumed instance throws.
|
|
45
|
+
*
|
|
46
|
+
* Use the returned (workspace) Skill from then on. The consumed
|
|
47
|
+
* reference exists only to surface the mistake loudly the first
|
|
48
|
+
* time the user re-uses it; it never silently re-uploads as
|
|
49
|
+
* transient.
|
|
50
|
+
*
|
|
51
|
+
* Unstaged transient Skills do NOT round-trip through JSON (the bytes
|
|
52
|
+
* cannot be serialised back). `toJSON()` throws in that state too —
|
|
53
|
+
* persist via `.upload(client)` first, or pass the unstaged Skill
|
|
54
|
+
* directly to `submitRun`.
|
|
55
|
+
*
|
|
56
|
+
* The Skill class is the only public way to build a transient bundle;
|
|
57
|
+
* the SDK never accepts a raw `TransientSkillRef` object from user code.
|
|
26
58
|
*/
|
|
27
59
|
export class Skill {
|
|
28
60
|
/** Workspace skill record returned by the BFF (only set for workspace-uploaded skills). */
|
|
29
61
|
record;
|
|
30
|
-
ref;
|
|
62
|
+
#ref;
|
|
63
|
+
#transientBytes;
|
|
64
|
+
#consumed = false;
|
|
31
65
|
/**
|
|
32
|
-
* Internal constructor. Use `Skill.fromId`, `Skill.provider`,
|
|
33
|
-
* `
|
|
34
|
-
* instances.
|
|
66
|
+
* Internal constructor. Use `Skill.fromId`, `Skill.provider`,
|
|
67
|
+
* `Skill.fromFiles`, or `Skill.fromPath` to create instances.
|
|
35
68
|
*/
|
|
36
|
-
constructor(ref, record) {
|
|
37
|
-
this
|
|
69
|
+
constructor(ref, record, transientBytes) {
|
|
70
|
+
this.#ref = ref;
|
|
38
71
|
this.record = record;
|
|
72
|
+
this.#transientBytes = transientBytes;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The wire-level reference. For workspace and provider skills this is
|
|
76
|
+
* stable. For unstaged transient skills it carries the placeholder
|
|
77
|
+
* `slot: "(unstaged)"` — `submitRun` rewrites the slot to a real
|
|
78
|
+
* positional id (`transient-0`, …) before sending.
|
|
79
|
+
*
|
|
80
|
+
* For consumed Skills, the getter still returns the original
|
|
81
|
+
* transient ref so error messages can be precise — but using a
|
|
82
|
+
* consumed Skill in any submit/serialise path throws.
|
|
83
|
+
*/
|
|
84
|
+
get ref() {
|
|
85
|
+
return this.#ref;
|
|
39
86
|
}
|
|
40
87
|
/**
|
|
41
88
|
* Reference an existing workspace skill by its id (e.g. `skl_abc123`).
|
|
@@ -47,8 +94,7 @@ export class Skill {
|
|
|
47
94
|
if (typeof id !== "string" || !id) {
|
|
48
95
|
throw new Error("Skill.fromId: id is required");
|
|
49
96
|
}
|
|
50
|
-
|
|
51
|
-
return new Skill(ref);
|
|
97
|
+
return new Skill({ kind: "workspace", id });
|
|
52
98
|
}
|
|
53
99
|
/**
|
|
54
100
|
* Reference a provider built-in skill. The vendor decides what the
|
|
@@ -70,13 +116,214 @@ export class Skill {
|
|
|
70
116
|
: { kind: "provider", vendor: args.vendor, skillId: args.skillId };
|
|
71
117
|
return new Skill(ref);
|
|
72
118
|
}
|
|
73
|
-
/**
|
|
119
|
+
/**
|
|
120
|
+
* Build an unstaged transient Skill from an inline files map. The SDK
|
|
121
|
+
* validates basic safety (no path traversal, size caps, has
|
|
122
|
+
* `SKILL.md`), deterministically zips the bundle, and computes the
|
|
123
|
+
* `sha256:<hex>` content hash that travels in the wire ref.
|
|
124
|
+
*
|
|
125
|
+
* The returned Skill carries the bytes privately. Pass it to
|
|
126
|
+
* `submitRun` for transient (per-run) use, or call `.upload(client)`
|
|
127
|
+
* to persist it as a workspace skill.
|
|
128
|
+
*/
|
|
129
|
+
static async fromFiles(args) {
|
|
130
|
+
if (!args || typeof args !== "object") {
|
|
131
|
+
throw new Error("Skill.fromFiles: args is required");
|
|
132
|
+
}
|
|
133
|
+
if (typeof args.name !== "string" || !SKILL_NAME_PATTERN.test(args.name)) {
|
|
134
|
+
throw new Error(`Skill.fromFiles: name must match ${SKILL_NAME_PATTERN.source}`);
|
|
135
|
+
}
|
|
136
|
+
const bundled = bundleSkillFiles(args.files);
|
|
137
|
+
const contentHash = await hashSkillBundle(bundled.zip);
|
|
138
|
+
const ref = {
|
|
139
|
+
kind: "transient",
|
|
140
|
+
slot: UNSTAGED_SLOT,
|
|
141
|
+
name: args.name,
|
|
142
|
+
contentHash
|
|
143
|
+
};
|
|
144
|
+
return new Skill(ref, undefined, bundled.zip);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Read a local directory and build an unstaged transient Skill.
|
|
148
|
+
* Symlinks and non-regular files are skipped. Node-only.
|
|
149
|
+
*
|
|
150
|
+
* The returned Skill behaves identically to one built via
|
|
151
|
+
* `Skill.fromFiles` — pass it to `submitRun` for transient use, or
|
|
152
|
+
* call `.upload(client)` to persist as a workspace skill.
|
|
153
|
+
*/
|
|
154
|
+
static async fromPath(rootDir, args) {
|
|
155
|
+
const files = await readDirectoryAsFiles(rootDir);
|
|
156
|
+
return Skill.fromFiles({ name: args.name, files });
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Persist this unstaged transient Skill as a workspace skill and
|
|
160
|
+
* return a new `Skill` instance backed by the resulting `skl_*`
|
|
161
|
+
* record. After `upload` resolves successfully, the original
|
|
162
|
+
* instance is marked **consumed** — any further use throws a clear
|
|
163
|
+
* validation error.
|
|
164
|
+
*
|
|
165
|
+
* Only unstaged transient Skills can be uploaded. Calling this on a
|
|
166
|
+
* workspace-ref Skill, a provider-ref Skill, or a previously
|
|
167
|
+
* consumed Skill throws.
|
|
168
|
+
*
|
|
169
|
+
* Accepts either an `AntpathClient` (preferred) or its
|
|
170
|
+
* `SkillsClient` if the caller already has a handle to it.
|
|
171
|
+
*/
|
|
172
|
+
async upload(client) {
|
|
173
|
+
if (this.#consumed) {
|
|
174
|
+
throw new Error(consumedMessage());
|
|
175
|
+
}
|
|
176
|
+
if (this.#ref.kind !== "transient" || !this.#transientBytes) {
|
|
177
|
+
throw new Error("Skill.upload: only unstaged transient Skills (built via Skill.fromFiles / Skill.fromPath) can be uploaded; " +
|
|
178
|
+
"use Skill.fromId(record.id) to reference an already-persisted skill");
|
|
179
|
+
}
|
|
180
|
+
const uploader = resolveUploader(client);
|
|
181
|
+
const record = await uploader({ name: this.#ref.name, body: this.#transientBytes });
|
|
182
|
+
// Only mark consumed AFTER a successful upload — a failed upload
|
|
183
|
+
// leaves the original instance reusable so the caller can retry.
|
|
184
|
+
this.#consumed = true;
|
|
185
|
+
return new Skill({ kind: "workspace", id: record.id }, record);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Persist this unstaged transient Skill as a workspace skill **only
|
|
189
|
+
* if** an existing skill with the same `(name, contentHash)` is not
|
|
190
|
+
* already live.
|
|
191
|
+
*
|
|
192
|
+
* Flow:
|
|
193
|
+
*
|
|
194
|
+
* 1. Hash is already known from `Skill.fromFiles` / `fromPath`.
|
|
195
|
+
* 2. Call `client.skills.findByHash({ name, contentHash })`. If a
|
|
196
|
+
* live row exists, mark this instance consumed and return a
|
|
197
|
+
* new `Skill` backed by that record.
|
|
198
|
+
* 3. Otherwise call `upload(client)`.
|
|
199
|
+
*
|
|
200
|
+
* Returns either the existing or newly-created workspace Skill;
|
|
201
|
+
* callers do not need to branch.
|
|
202
|
+
*
|
|
203
|
+
* Only unstaged transient Skills can be `uploadIfChanged`. Calling
|
|
204
|
+
* this on a workspace-ref Skill, a provider-ref Skill, or a
|
|
205
|
+
* previously consumed Skill throws.
|
|
206
|
+
*/
|
|
207
|
+
async uploadIfChanged(client) {
|
|
208
|
+
if (this.#consumed) {
|
|
209
|
+
throw new Error(consumedMessage());
|
|
210
|
+
}
|
|
211
|
+
if (this.#ref.kind !== "transient" || !this.#transientBytes) {
|
|
212
|
+
throw new Error("Skill.uploadIfChanged: only unstaged transient Skills (built via Skill.fromFiles / Skill.fromPath) can be checked; " +
|
|
213
|
+
"use Skill.fromId(record.id) to reference an already-persisted skill");
|
|
214
|
+
}
|
|
215
|
+
const lookup = resolveLookup(client);
|
|
216
|
+
if (lookup) {
|
|
217
|
+
const existing = await lookup({
|
|
218
|
+
name: this.#ref.name,
|
|
219
|
+
contentHash: this.#ref.contentHash
|
|
220
|
+
});
|
|
221
|
+
if (existing) {
|
|
222
|
+
this.#consumed = true;
|
|
223
|
+
return new Skill({ kind: "workspace", id: existing.id }, existing);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return this.upload(client);
|
|
227
|
+
}
|
|
228
|
+
/** True for `Skill.fromId(...)` and skills returned by `.upload(...)`. */
|
|
74
229
|
get isWorkspace() {
|
|
75
|
-
return this
|
|
230
|
+
return this.#ref.kind === "workspace";
|
|
76
231
|
}
|
|
77
232
|
/** True for `Skill.provider(...)`. */
|
|
78
233
|
get isProvider() {
|
|
79
|
-
return this
|
|
234
|
+
return this.#ref.kind === "provider";
|
|
235
|
+
}
|
|
236
|
+
/** True for any transient Skill (unstaged or consumed). */
|
|
237
|
+
get isTransient() {
|
|
238
|
+
return this.#ref.kind === "transient";
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* True only while the transient Skill still carries bytes and has
|
|
242
|
+
* not been consumed by `.upload(...)`. Unstaged Skills can be passed
|
|
243
|
+
* to `submitRun` or `.upload(client)`; consumed ones cannot.
|
|
244
|
+
*/
|
|
245
|
+
get isUnstaged() {
|
|
246
|
+
return (this.#ref.kind === "transient" &&
|
|
247
|
+
this.#ref.slot === UNSTAGED_SLOT &&
|
|
248
|
+
!this.#consumed &&
|
|
249
|
+
this.#transientBytes !== undefined);
|
|
250
|
+
}
|
|
251
|
+
/** True after a successful `.upload(...)` — using this instance further throws. */
|
|
252
|
+
get isConsumed() {
|
|
253
|
+
return this.#consumed;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Internal: yield the unstaged transient bundle's payload so the
|
|
257
|
+
* client's `submitRun` can build a multipart part. Returns undefined
|
|
258
|
+
* for non-transient or consumed skills. Throws if the instance is
|
|
259
|
+
* marked consumed (a stronger signal than "no bytes").
|
|
260
|
+
*
|
|
261
|
+
* NOT part of the public API — the `_` prefix is a "do not call from
|
|
262
|
+
* user code" marker. Callers within the SDK pass the returned bytes
|
|
263
|
+
* into the multipart body once per `submitRun` invocation.
|
|
264
|
+
*/
|
|
265
|
+
_takeUnstagedBundle() {
|
|
266
|
+
if (this.#consumed) {
|
|
267
|
+
throw new Error(consumedMessage());
|
|
268
|
+
}
|
|
269
|
+
if (!this.isUnstaged) {
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
if (this.#ref.kind !== "transient" || !this.#transientBytes) {
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
name: this.#ref.name,
|
|
277
|
+
bytes: this.#transientBytes,
|
|
278
|
+
contentHash: this.#ref.contentHash
|
|
279
|
+
};
|
|
80
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* JSON serialisation guard. Workspace and provider Skills serialise
|
|
283
|
+
* to their `ref`. Unstaged transient Skills throw — the bytes are not
|
|
284
|
+
* in the JSON, so a round-trip would silently drop them. Consumed
|
|
285
|
+
* Skills also throw, to surface the "you forgot to use the returned
|
|
286
|
+
* Skill from `.upload(...)`" mistake at the point it happens.
|
|
287
|
+
*/
|
|
288
|
+
toJSON() {
|
|
289
|
+
if (this.#consumed) {
|
|
290
|
+
throw new Error(consumedMessage());
|
|
291
|
+
}
|
|
292
|
+
if (this.isUnstaged) {
|
|
293
|
+
throw new Error("Cannot JSON-serialise an unstaged transient Skill — the bytes are not in the JSON. " +
|
|
294
|
+
"Persist via skill.upload(client) and serialise the returned (workspace) Skill, " +
|
|
295
|
+
"or pass the unstaged Skill directly to submitRun without serialising.");
|
|
296
|
+
}
|
|
297
|
+
return this.#ref;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/** Sentinel slot id used by unstaged transient Skills. */
|
|
301
|
+
const UNSTAGED_SLOT = "(unstaged)";
|
|
302
|
+
function resolveUploader(client) {
|
|
303
|
+
const direct = client._uploadSkillBundle;
|
|
304
|
+
if (typeof direct === "function") {
|
|
305
|
+
return direct.bind(client);
|
|
306
|
+
}
|
|
307
|
+
const nested = client.skills?._uploadSkillBundle;
|
|
308
|
+
if (typeof nested === "function") {
|
|
309
|
+
return nested.bind(client.skills);
|
|
310
|
+
}
|
|
311
|
+
throw new Error("Skill.upload: client argument does not expose an upload entry point — " +
|
|
312
|
+
"pass the AntpathClient instance or its `client.skills`");
|
|
313
|
+
}
|
|
314
|
+
function resolveLookup(client) {
|
|
315
|
+
const nested = client.skills?.findByHash;
|
|
316
|
+
if (typeof nested === "function") {
|
|
317
|
+
return nested.bind(client.skills);
|
|
318
|
+
}
|
|
319
|
+
const direct = client.findByHash;
|
|
320
|
+
if (typeof direct === "function") {
|
|
321
|
+
return direct.bind(client);
|
|
322
|
+
}
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
function consumedMessage() {
|
|
326
|
+
return ("this Skill was already uploaded via skill.upload(client); use the returned Skill or " +
|
|
327
|
+
"Skill.fromId(record.id) for subsequent runs");
|
|
81
328
|
}
|
|
82
329
|
//# sourceMappingURL=skill.js.map
|
package/dist/skill.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skill.js","sourceRoot":"","sources":["../src/skill.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"skill.js","sourceRoot":"","sources":["../src/skill.ts"],"names":[],"mappings":"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"}
|
package/docs/credentials.md
CHANGED
|
@@ -84,6 +84,40 @@ Inside the run container, every session has the platform CLI mounted at `/antpat
|
|
|
84
84
|
|
|
85
85
|
The CLI reads the per-run bearer from `/antpath/run-token`, attaches the `X-Antpath-Proxy-Protocol` header, and the BFF injects the bearer/header/query/basic credential before dispatching the outbound call. Only the response (subject to `responseMode` and `maxResponseBytes`) reaches the container. `--response-mode` can only narrow below the policy ceiling.
|
|
86
86
|
|
|
87
|
+
#### Keyless upstreams (`authShape: { type: "none" }`)
|
|
88
|
+
|
|
89
|
+
For public APIs that take no credential (Wikimedia Commons, Internet Archive, Library of Congress, NASA Images, NARA, GDELT, etc.), declare the endpoint with `authShape: { type: "none" }` and omit the matching `proxyEndpointAuth[]` entry entirely:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
const proxyEndpoints = [
|
|
93
|
+
{
|
|
94
|
+
name: "wikimedia",
|
|
95
|
+
baseUrl: "https://commons.wikimedia.org",
|
|
96
|
+
authShape: { type: "none" },
|
|
97
|
+
allowMethods: ["GET"],
|
|
98
|
+
allowPathPrefixes: ["/wiki/", "/w/api.php"]
|
|
99
|
+
}
|
|
100
|
+
] as const;
|
|
101
|
+
|
|
102
|
+
const ref = await client.submitRun(template, {
|
|
103
|
+
proxyEndpoints,
|
|
104
|
+
secrets: { anthropic: { apiKey: process.env.ANTHROPIC_API_KEY! } }
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The keyless endpoint still routes through the antpath managed proxy: every call is allow-listed, audited, redacted, and counted against per-run budgets. The BFF injects no `Authorization` header and no query-string credential. Shipping a `proxyEndpointAuth` entry for a `none`-shape endpoint is rejected at submission time. Equivalent class-based form:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { ProxyEndpoint } from "antpath";
|
|
112
|
+
|
|
113
|
+
ProxyEndpoint.none({
|
|
114
|
+
name: "wikimedia",
|
|
115
|
+
baseUrl: "https://commons.wikimedia.org",
|
|
116
|
+
allowMethods: ["GET"],
|
|
117
|
+
allowPathPrefixes: ["/wiki/", "/w/api.php"]
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
87
121
|
`/antpath/antpath --help` reads endpoint details from `/antpath/index.json`. Runs that do not declare any `proxyEndpoints` still have the CLI and an empty manifest mounted, so agents never need to introspect whether the surface exists.
|
|
88
122
|
|
|
89
123
|
### Networking
|
package/docs/events.md
CHANGED
|
@@ -23,6 +23,13 @@ for await (const event of ref.stream({ intervalMs: 1000 })) {
|
|
|
23
23
|
}
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
> **Transport.** `stream()` polls under the hood — it issues `GET
|
|
27
|
+
> /api/runs/:id/events?since=…` calls at `intervalMs` until the run
|
|
28
|
+
> reaches a terminal status. There is no SSE / WebSocket push from the
|
|
29
|
+
> BFF today. For latency-sensitive UIs this is acceptable down to about
|
|
30
|
+
> 500 ms; below that, every dashboard request is one round-trip. A push
|
|
31
|
+
> transport is on the public backlog — not committed to a release.
|
|
32
|
+
|
|
26
33
|
The CLI mirrors the same surface:
|
|
27
34
|
|
|
28
35
|
```bash
|
package/docs/mcp.md
CHANGED
|
@@ -16,3 +16,31 @@ Rules:
|
|
|
16
16
|
- Only provider-native bearer/OAuth auth is supported.
|
|
17
17
|
|
|
18
18
|
Use allowlists for sensitive servers whenever possible.
|
|
19
|
+
|
|
20
|
+
## Large-payload responses
|
|
21
|
+
|
|
22
|
+
antpath is a session dispatcher, not an MCP runtime. We intentionally do
|
|
23
|
+
**not** interpose on the transport between Claude and an upstream MCP
|
|
24
|
+
server, so we cannot elide MCP responses or write them to the session
|
|
25
|
+
filesystem on the user's behalf. Anything an MCP tool returns lands
|
|
26
|
+
directly in the model's context.
|
|
27
|
+
|
|
28
|
+
For ingestion-style tools that return large JSON blobs (search results,
|
|
29
|
+
catalogue dumps, bulk reads), use the **CLI-as-skill + managed proxy**
|
|
30
|
+
pattern instead of MCP:
|
|
31
|
+
|
|
32
|
+
1. Package the upstream as a `Skill` — a CLI binary the agent invokes
|
|
33
|
+
with its bash tool.
|
|
34
|
+
2. Route every upstream HTTPS call through a per-run `ProxyEndpoint`
|
|
35
|
+
(audit, byte caps, budget enforcement).
|
|
36
|
+
3. Have the CLI write the full payload under one of the directories you
|
|
37
|
+
passed to `outputDirs`. Return only a small handle (path, item count,
|
|
38
|
+
summary) to the model.
|
|
39
|
+
|
|
40
|
+
The agent sees the handle in context; the bytes ride out through
|
|
41
|
+
`download()` as a normal captured output.
|
|
42
|
+
|
|
43
|
+
If you genuinely want everything in context (small responses, code
|
|
44
|
+
search, etc.), use MCP. If the payload would blow your context budget,
|
|
45
|
+
the CLI-as-skill pattern is the supported answer — there is no platform
|
|
46
|
+
flag to elide MCP responses.
|
package/docs/outputs.md
CHANGED
|
@@ -4,24 +4,104 @@ title: Outputs
|
|
|
4
4
|
|
|
5
5
|
# Outputs
|
|
6
6
|
|
|
7
|
-
Every run
|
|
7
|
+
Every run produces durable metadata (status, events, snapshots, cleanup state). File bytes are **opt-in** via the submission's `outputDirs` field. There is a single method, `RunRef.download()`, that returns the whole run — metadata plus opt-in file bytes — as a streaming zip.
|
|
8
|
+
|
|
9
|
+
## Quickstart
|
|
8
10
|
|
|
9
11
|
```ts
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
+
const ref = await client.submitRun({
|
|
13
|
+
model: "claude-opus-4-7",
|
|
14
|
+
prompt: "Produce a report and stash working files",
|
|
15
|
+
outputDirs: ["/workspace/outputs", "/workspace/state"],
|
|
16
|
+
secrets: { anthropic: { apiKey } }
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
await ref.download({ to: "./run-archive.zip" });
|
|
12
20
|
```
|
|
13
21
|
|
|
14
22
|
```bash
|
|
15
|
-
antpath
|
|
16
|
-
|
|
17
|
-
|
|
23
|
+
antpath download <run-id> --out ./run-archive.zip --api-token …
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## What `download()` returns
|
|
27
|
+
|
|
28
|
+
A zip with this layout:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
run.json # run record (status, runId, timestamps, snapshot)
|
|
32
|
+
events.jsonl # one event per line, ordered
|
|
33
|
+
outputs/<name> # one file per captured output object
|
|
34
|
+
manifest.json # { source, partial, outputs, missing }
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`manifest.json` carries:
|
|
38
|
+
|
|
39
|
+
| Field | Meaning |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| `source: "live" \| "s3"` | Where file bytes came from. `live` = streamed from Anthropic Files API (mid-session); `s3` = streamed from Supabase Storage (after capture completed). |
|
|
42
|
+
| `partial: boolean` | `true` if the run had not finished capture yet — more bytes may exist on a later call. |
|
|
43
|
+
| `outputs[]` | `{ id, path, byteSize, contentType? }` — each row corresponds to a file under `outputs/` in the zip. |
|
|
44
|
+
| `missing[]` | `{ id?, path?, reason, message? }` — anything the platform tried to include but could not. Reasons documented below. |
|
|
45
|
+
|
|
46
|
+
## Lifecycle behaviour
|
|
47
|
+
|
|
48
|
+
`download()` is lifecycle-aware in one method. You never branch on state — check `manifest.partial` if you care about staleness.
|
|
49
|
+
|
|
50
|
+
| Run state | Behaviour |
|
|
51
|
+
| --- | --- |
|
|
52
|
+
| `pending` / `queued` / `provisioning` | HTTP 409 `run_not_started`. Wait for the run to start. |
|
|
53
|
+
| `provider_running`, mid-session | Live: stream-zip from Anthropic Files API + DB metadata/events. `source: "live"`, `partial: true`. |
|
|
54
|
+
| `cleaning_up` | Same as `provider_running` until S3 capture completes; then same as terminal. |
|
|
55
|
+
| `succeeded` / `failed` / `cancelled` / `terminated` | Terminal: stream-zip from S3 `output_objects` + DB. `source: "s3"`, `partial: false`. |
|
|
56
|
+
|
|
57
|
+
## `outputDirs` — opt-in file capture
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
client.submitRun({
|
|
61
|
+
/* ... */,
|
|
62
|
+
outputDirs: ["/workspace/outputs", "/workspace/state"]
|
|
63
|
+
});
|
|
18
64
|
```
|
|
19
65
|
|
|
20
|
-
|
|
66
|
+
Validation:
|
|
67
|
+
|
|
68
|
+
- absolute UNIX paths only (`/...`),
|
|
69
|
+
- no `..` segments, no NUL bytes,
|
|
70
|
+
- maximum 32 entries,
|
|
71
|
+
- maximum 512 bytes per entry.
|
|
72
|
+
|
|
73
|
+
Mechanism (no platform-magical paths — this is honest):
|
|
74
|
+
|
|
75
|
+
1. Worker submits the run, sends the user prompt, streams events.
|
|
76
|
+
2. At session-idle (the agent's primary task is done), the worker sends one synthetic `user.message` to the agent:
|
|
77
|
+
*"Run `/antpath/antpath outputs sync <dirs>` once."*
|
|
78
|
+
3. The agent runs that command via its bash tool. The in-container CLI walks the listed dirs, prints a JSON line per file, and the agent's tool output triggers Anthropic Managed Agents' file registration.
|
|
79
|
+
4. Worker walks the Files API, copies bytes into Supabase Storage, and tears down the session.
|
|
80
|
+
|
|
81
|
+
Cost: one extra agent turn (~hundreds of tokens, observable in `span.model_request_*` events). Document this against your token budget if you submit very high-volume runs.
|
|
82
|
+
|
|
83
|
+
Failure modes — anything that did not make it into the zip lands in `manifest.missing[]`:
|
|
84
|
+
|
|
85
|
+
| `reason` | What happened |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| `agent_did_not_sync` | The agent refused or skipped the synthetic instruction. Run still succeeded, just no file bytes. |
|
|
88
|
+
| `agent_reported_error` | The `/antpath/antpath outputs sync` invocation returned non-zero (e.g. dir did not exist). |
|
|
89
|
+
| `session_terminated_pre_sync` | Session was terminated (cancel / timeout) before the sync turn ran. |
|
|
90
|
+
| `storage_cap_exceeded` | Workspace storage quota would have been breached. |
|
|
91
|
+
| `download_failed` | Files API entry could not be fetched. |
|
|
92
|
+
| `pending_session_terminal` | Mid-session download — file may show up on a later `download()` once the session reaches terminal. |
|
|
93
|
+
|
|
94
|
+
## Runs without `outputDirs`
|
|
95
|
+
|
|
96
|
+
Metadata still gets the full treatment. `download()` returns a zip with `run.json`, `events.jsonl`, and an empty `outputs/` directory (manifest `outputs: []`). No file bytes are captured because no path was opted in. This is the default and is intentional — see `references/architecture-decisions.md` for the design rationale.
|
|
97
|
+
|
|
98
|
+
## Mid-session download semantics
|
|
99
|
+
|
|
100
|
+
Mid-session calls are **best-effort and side-effect-free**: the BFF reads whatever the agent has already registered with the Files API and streams that. It does **not** trigger a synthetic sync — that would interfere with the running agent's plan. If you need the full output set, wait for the run to reach terminal status and call `download()` again. The manifest's `partial: true` flag tells you when to retry.
|
|
21
101
|
|
|
22
|
-
Safety
|
|
102
|
+
## Safety
|
|
23
103
|
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
104
|
+
- Filenames are sanitized for cross-platform safety; collisions are disambiguated with a short id suffix before the extension.
|
|
105
|
+
- Downloads stay within the requested local directory.
|
|
106
|
+
- The archive endpoint is workspace-scoped (`outputs:read` scope) and rate-limited (`ANTPATH_RATE_LIMIT_RUN_ARCHIVE_PER_MINUTE`, default 30/min/workspace).
|
|
107
|
+
- `manifest.json` never contains file bytes — only ids, paths, sizes, content types.
|
package/docs/quickstart.md
CHANGED
|
@@ -45,3 +45,40 @@ antpath run ./template.json \
|
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
Both surfaces hit the same dashboard BFF and operate on the same durable run records — pick whichever is most convenient.
|
|
48
|
+
|
|
49
|
+
## Safe retries with `idempotencyKey`
|
|
50
|
+
|
|
51
|
+
Every `submitRun` call carries an `idempotencyKey`. When omitted the SDK auto-generates a UUID per call. Supplying your own key makes retries deterministic:
|
|
52
|
+
|
|
53
|
+
| Submit shape | Server response |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| New `idempotencyKey` | HTTP 201 — new run created. |
|
|
56
|
+
| Same key + identical request body hash | HTTP 200 — returns the original run. The SDK call resolves with the existing `RunRef`. |
|
|
57
|
+
| Same key + **different** request body hash | HTTP 409 — body `{ error: { message, code: "idempotency_conflict", details: { existingRunId } } }`. The SDK throws an `HttpError` carrying that body. Use `details.existingRunId` to adopt the pre-existing run, or pick a fresh key. |
|
|
58
|
+
| Omitted `idempotencyKey` | A new UUID is generated on every call — repeat submissions create new runs. |
|
|
59
|
+
|
|
60
|
+
The request hash is computed server-side over the canonical submission JSON (model, prompt, system, environment, skill refs, MCP server descriptors, proxy endpoints, `outputDirs`, etc.) so reordering JSON keys, adding whitespace, or rotating the inline secret bundle does **not** change the hash. Bumping a variable or changing the prompt does.
|
|
61
|
+
|
|
62
|
+
Pattern for safe retries:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
const idempotencyKey = crypto.randomUUID();
|
|
66
|
+
async function submitWithRetry() {
|
|
67
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
68
|
+
try {
|
|
69
|
+
return await client.submitRun({
|
|
70
|
+
model: "claude-haiku-4-5",
|
|
71
|
+
prompt: "...",
|
|
72
|
+
idempotencyKey,
|
|
73
|
+
secrets: { anthropic: { apiKey: process.env.ANTHROPIC_API_KEY! } }
|
|
74
|
+
});
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (err instanceof Error && err.message.includes("network")) continue;
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
throw new Error("submitRun failed after retries");
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The same `idempotencyKey` reused with the same body will deterministically resolve to the same run id regardless of how many times the network drops between attempts.
|
package/docs/skills.md
CHANGED
|
@@ -16,3 +16,52 @@ Local directories are packaged as zip files and mounted under `/antpath/skills/`
|
|
|
16
16
|
Inline skills are uploaded as markdown files and mounted under `/antpath/skills/` unless overridden.
|
|
17
17
|
|
|
18
18
|
The platform also mounts the `antpath` CLI at `/antpath/antpath` and a per-run manifest at `/antpath/index.json` on **every** run. Skills can invoke the managed HTTP proxy via `/antpath/antpath proxy …` — see `credentials.md` for the policy/auth model.
|
|
19
|
+
|
|
20
|
+
## Workspace skills: upload, find, reuse
|
|
21
|
+
|
|
22
|
+
Workspace skills persist across runs. Build a `Skill` from files, upload once, reference by `skl_*` id thereafter:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { AntpathClient, Skill } from "antpath";
|
|
26
|
+
|
|
27
|
+
const client = new AntpathClient({ apiToken });
|
|
28
|
+
|
|
29
|
+
// First publish.
|
|
30
|
+
const draft = await Skill.fromPath("./skills/broll-provider-ingest", {
|
|
31
|
+
name: "broll-provider-ingest"
|
|
32
|
+
});
|
|
33
|
+
const persisted = await draft.upload(client);
|
|
34
|
+
console.log(persisted.record!.id); // skl_…
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### `uploadIfChanged(client)`
|
|
38
|
+
|
|
39
|
+
Repeat publishes are common in CI. `uploadIfChanged` first checks whether a live skill with the same `(name, contentHash)` already exists, and only uploads when the bytes have actually changed:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
const skill = await Skill.fromPath("./skills/broll-provider-ingest", {
|
|
43
|
+
name: "broll-provider-ingest"
|
|
44
|
+
}).then((draft) => draft.uploadIfChanged(client));
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`contentHash` is `sha256:<hex>` of the canonical bundle zip (the SDK normalises file order, mtime, and permissions before hashing). Identical inputs always produce the same hash, so a no-op `uploadIfChanged` returns the existing record without re-uploading bytes.
|
|
48
|
+
|
|
49
|
+
After the call resolves, the returned `Skill` is workspace-backed regardless of whether bytes were uploaded — pass it straight to `submitRun({ skills: [...] })` or call `.record!.id` to persist the id in your manifest.
|
|
50
|
+
|
|
51
|
+
### `client.skills.findByHash(...)` / `findByName(...)`
|
|
52
|
+
|
|
53
|
+
You can also look up workspace skills directly:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
const byHash = await client.skills.findByHash({
|
|
57
|
+
name: "broll-provider-ingest",
|
|
58
|
+
contentHash: "sha256:..."
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const byName = await client.skills.findByName("broll-provider-ingest");
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Both return `null` when no live, undeleted skill matches. `findByHash` is an indexed lookup on `(name, hash)` — use it when you have a precomputed hash. `findByName` is a list-and-filter convenience for the common "what's the current id of this skill?" case.
|
|
65
|
+
|
|
66
|
+
Soft-deleted skills are never returned by either method. Runs that pinned a soft-deleted skill at submission time keep working via their snapshot — see `references/architecture-decisions.md` for the full snapshot model.
|
|
67
|
+
|