@aexhq/sdk 0.24.0 → 0.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -125
- package/dist/_contracts/index.d.ts +0 -1
- package/dist/_contracts/index.js +0 -1
- package/dist/_contracts/models.js +1 -2
- package/dist/_contracts/operations.d.ts +13 -5
- package/dist/_contracts/operations.js +220 -10
- package/dist/_contracts/provider-support.d.ts +18 -29
- package/dist/_contracts/provider-support.js +3 -22
- package/dist/_contracts/proxy-protocol.d.ts +4 -2
- package/dist/_contracts/proxy-protocol.js +10 -3
- package/dist/_contracts/run-config.d.ts +28 -4
- package/dist/_contracts/run-config.js +10 -5
- package/dist/_contracts/run-cost.d.ts +3 -11
- package/dist/_contracts/run-cost.js +2 -57
- package/dist/_contracts/run-custody.d.ts +1 -52
- package/dist/_contracts/run-custody.js +3 -87
- package/dist/_contracts/run-retention.d.ts +1 -5
- package/dist/_contracts/run-retention.js +2 -14
- package/dist/_contracts/run-unit.d.ts +2 -2
- package/dist/_contracts/run-unit.js +3 -1
- package/dist/_contracts/runtime-security-profile.js +1 -1
- package/dist/_contracts/runtime-types.d.ts +38 -12
- package/dist/_contracts/side-effect-audit.d.ts +4 -5
- package/dist/_contracts/side-effect-audit.js +1 -4
- package/dist/_contracts/status.d.ts +3 -4
- package/dist/_contracts/status.js +3 -8
- package/dist/_contracts/submission.d.ts +68 -42
- package/dist/_contracts/submission.js +144 -30
- package/dist/bundle.d.ts +13 -0
- package/dist/bundle.js +51 -0
- package/dist/bundle.js.map +1 -1
- package/dist/cli.mjs +232 -58
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/client.d.ts +25 -21
- package/dist/client.js +59 -15
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/tool.d.ts +41 -0
- package/dist/tool.js +138 -0
- package/dist/tool.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/concepts/agent-tools.md +53 -0
- package/docs/concepts/composition.md +43 -0
- package/docs/concepts/providers-and-runtimes.md +53 -0
- package/docs/concepts/runs.md +40 -0
- package/docs/credentials.md +8 -4
- package/docs/events.md +12 -0
- package/docs/limits.md +53 -0
- package/docs/outputs.md +58 -0
- package/docs/provider-runtime-capabilities.md +53 -55
- package/docs/public-surface.json +81 -0
- package/docs/quickstart.md +28 -105
- package/docs/release.md +1 -1
- package/docs/run-config.md +2 -2
- package/docs/secrets.md +123 -0
- package/docs/skills.md +3 -3
- package/docs/vision-skills.md +14 -19
- package/package.json +2 -2
- package/dist/_contracts/managed-key.d.ts +0 -101
- package/dist/_contracts/managed-key.js +0 -181
- package/docs/product-boundaries.md +0 -57
package/README.md
CHANGED
|
@@ -2,163 +2,74 @@
|
|
|
2
2
|
title: aex
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
#
|
|
5
|
+
# @aexhq/sdk
|
|
6
6
|
|
|
7
|
-
aex is
|
|
7
|
+
aex is an agent execution platform for launching autonomous agents from a simple TypeScript SDK and CLI.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
AgentExecutor, // the only client class — submits durable runs to aex
|
|
12
|
-
RunModels, // closed public model id constants
|
|
13
|
-
Skill, // local, URL, and catalog skill bundles normalized to assets
|
|
14
|
-
McpServer, // MCP server declarations (headers split into secrets server-side)
|
|
15
|
-
ProxyEndpoint, // per-run managed HTTP proxy endpoint
|
|
16
|
-
AgentsMd, // AGENTS.md / CLAUDE.md uploads
|
|
17
|
-
File, // arbitrary workspace files mounted into the session
|
|
18
|
-
validateProxyAuth // helper that fails fast on policy/auth mismatch
|
|
19
|
-
} from "@aexhq/sdk";
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
aex run --config ./run.json --api-token ant_… \
|
|
24
|
-
--anthropic-api-key sk-ant-… --follow
|
|
25
|
-
aex status <run-id> --api-token …
|
|
26
|
-
aex wait <run-id> [--timeout 8m] [--interval 2s] --api-token …
|
|
27
|
-
aex events <run-id> [--follow] [--timeout 8m] --api-token …
|
|
28
|
-
aex outputs <run-id> --api-token …
|
|
29
|
-
aex download <run-id> [--only outputs|events|metadata] [--out path] --api-token …
|
|
30
|
-
aex cancel <run-id> --api-token …
|
|
31
|
-
aex delete <run-id> --api-token …
|
|
32
|
-
aex whoami --api-token …
|
|
33
|
-
aex skills <upload|list|get|delete> [flags] --api-token …
|
|
34
|
-
aex delete-asset <assetId|hash> --api-token …
|
|
35
|
-
```
|
|
9
|
+
The package ships:
|
|
36
10
|
|
|
37
|
-
|
|
11
|
+
- `AgentExecutor` for submit, run, wait, stream, inspect, download, cancel, and delete.
|
|
12
|
+
- Typed run primitives: `Models`, `Providers`, `RuntimeSizes`, `Skill`, `AgentsMd`, `File`, `McpServer`, `ProxyEndpoint`, and `Secret`.
|
|
13
|
+
- A bundled `aex` CLI with the same run, status, events, outputs, download, cancel, delete, whoami, and skills operations.
|
|
38
14
|
|
|
39
|
-
|
|
15
|
+
## Install
|
|
40
16
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
regardless of provider:
|
|
45
|
-
- omit `runtime` or pass `runtime: "managed"`; every provider uses the
|
|
46
|
-
managed runtime and BYOK provider-proxy.
|
|
47
|
-
- `provider: "anthropic" | "deepseek" | "openai" | "gemini" | "mistral"`.
|
|
48
|
-
- See [provider/runtime capabilities](docs/provider-runtime-capabilities.md)
|
|
49
|
-
for the generated matrix.
|
|
50
|
-
- BYO provider key + MCP credentials + skill references — passed inline on every submission and held in run-scoped custody, with cleanup/revocation attempted at terminal. Cross-provider keys are rejected loudly at submission time.
|
|
51
|
-
- Workspace is the tenant boundary. Workspace identity is derived server-side from the API token (1:1 binding); the SDK / CLI never name it.
|
|
52
|
-
- No SDK-side storage of provider keys, MCP credentials, or output file contents.
|
|
53
|
-
- Cleanup runs by default for tracked runtime resources.
|
|
54
|
-
- Product boundaries are explicit: see [product capabilities and boundaries](docs/product-boundaries.md) for what aex owns, inherits, and does not support.
|
|
17
|
+
```bash
|
|
18
|
+
npm install @aexhq/sdk
|
|
19
|
+
```
|
|
55
20
|
|
|
56
|
-
##
|
|
21
|
+
## First Run
|
|
57
22
|
|
|
58
23
|
```ts
|
|
59
|
-
import { AgentExecutor,
|
|
24
|
+
import { AgentExecutor, Models, Providers } from "@aexhq/sdk";
|
|
60
25
|
|
|
61
26
|
const aex = new AgentExecutor({
|
|
62
|
-
apiToken: process.env.AEX_API_TOKEN
|
|
63
|
-
// baseUrl defaults to https://api.aex.dev - set it for local or staging planes.
|
|
27
|
+
apiToken: process.env.AEX_API_TOKEN!
|
|
64
28
|
});
|
|
65
29
|
|
|
66
30
|
const runId = await aex.submit({
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
prompt: "Write
|
|
31
|
+
provider: Providers.ANTHROPIC,
|
|
32
|
+
model: Models.CLAUDE_HAIKU_4_5,
|
|
33
|
+
prompt: "Write the report and save outputs.",
|
|
70
34
|
secrets: { apiKey: process.env.ANTHROPIC_API_KEY! }
|
|
71
35
|
});
|
|
72
36
|
|
|
73
|
-
const
|
|
74
|
-
console.log(
|
|
75
|
-
|
|
76
|
-
for (const output of await aex.outputs(runId)) {
|
|
77
|
-
console.log(output.id, output.filename);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const report = await aex.downloadOutput(runId, { path: "report.txt", match: "suffix" });
|
|
81
|
-
console.log(new TextDecoder().decode(report));
|
|
82
|
-
|
|
83
|
-
await aex.downloadOutputs(runId, { to: "./outputs.zip" });
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Reusable, credential-free configs can be ordinary functions:
|
|
87
|
-
|
|
88
|
-
```ts
|
|
89
|
-
function summarise(topic: string) {
|
|
90
|
-
return {
|
|
91
|
-
model: RunModels.CLAUDE_HAIKU_4_5,
|
|
92
|
-
system: "You are a concise automation agent.",
|
|
93
|
-
prompt: `Write a short answer about ${topic}.`
|
|
94
|
-
};
|
|
37
|
+
for await (const event of aex.stream(runId)) {
|
|
38
|
+
console.log(event.type);
|
|
95
39
|
}
|
|
96
40
|
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
secrets: { apiKey: process.env.ANTHROPIC_API_KEY! }
|
|
100
|
-
});
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
Stream events live with `aex.stream(runId)`:
|
|
41
|
+
const run = await aex.wait(runId);
|
|
42
|
+
console.log(run.status);
|
|
104
43
|
|
|
105
|
-
|
|
106
|
-
for await (const event of aex.stream(runId)) {
|
|
107
|
-
if (event.type === "TEXT_MESSAGE_CONTENT") {
|
|
108
|
-
// typed event helpers live under `aex`'s event guard exports.
|
|
109
|
-
}
|
|
110
|
-
}
|
|
44
|
+
await aex.download(runId, { to: "./run.zip" });
|
|
111
45
|
```
|
|
112
46
|
|
|
113
|
-
The same
|
|
47
|
+
The same request can run from the CLI:
|
|
114
48
|
|
|
115
49
|
```bash
|
|
116
50
|
aex run \
|
|
117
51
|
--api-token "$AEX_API_TOKEN" \
|
|
118
52
|
--anthropic-api-key "$ANTHROPIC_API_KEY" \
|
|
119
53
|
--model claude-haiku-4-5 \
|
|
120
|
-
--
|
|
121
|
-
--prompt "Write a short answer about agent-first SDK design." \
|
|
122
|
-
--follow
|
|
123
|
-
|
|
124
|
-
aex run \
|
|
125
|
-
--api-token "$AEX_API_TOKEN" \
|
|
126
|
-
--anthropic-api-key "$ANTHROPIC_API_KEY" \
|
|
127
|
-
--config ./run.json \
|
|
54
|
+
--prompt "Write the report and save outputs." \
|
|
128
55
|
--follow
|
|
129
56
|
```
|
|
130
57
|
|
|
131
|
-
|
|
58
|
+
## Feature Areas
|
|
132
59
|
|
|
133
|
-
|
|
60
|
+
- **Agent runtime:** managed autonomous runs with filesystem read/edit, grep/glob/head/tail, open web fetch/search defaults, optional notebook tools, and post-hook repair.
|
|
61
|
+
- **Durable infrastructure:** run records, status, wait/cancel/delete, idempotency, typed events, output capture, downloads, timeouts, and runtime sizes.
|
|
62
|
+
- **Agent composition:** skills, files, AGENTS.md, remote MCP servers, proxy endpoints, environment variables, packages, and networking controls.
|
|
63
|
+
- **Subagents:** typed parent/child lineage for async child runs, output handoff, and bounded agent delegation.
|
|
64
|
+
- **Models and providers:** Anthropic, DeepSeek, OpenAI, Gemini, Mistral, OpenRouter, Doubao, and Doubao China behind one submission shape.
|
|
65
|
+
- **Typed control surface:** strongly typed SDK inputs, CLI parity, BYOK secrets, scoped proxy auth, redaction, and output modes.
|
|
134
66
|
|
|
135
|
-
##
|
|
136
|
-
|
|
137
|
-
```text
|
|
138
|
-
pnpm test # unit (deterministic; uses fakes/snapshots)
|
|
139
|
-
pnpm test:user:offline # clean install of packed/published SDK, no live API
|
|
140
|
-
pnpm test:user # live hosted API user tests
|
|
141
|
-
pnpm test:user:heavy # explicit heavy live canary
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
User tests auto-pack the current SDK when no artifact env is set. CI can pin a
|
|
145
|
-
specific artifact with exactly one of `AEX_USER_TEST_TARBALL` or
|
|
146
|
-
`AEX_USER_TEST_VERSION`; live runs also need `AEX_API_URL`,
|
|
147
|
-
`AEX_API_TOKEN`, and `DEEPSEEK_API_KEY`.
|
|
148
|
-
|
|
149
|
-
## Guides
|
|
67
|
+
## Docs
|
|
150
68
|
|
|
151
69
|
- [Quickstart](docs/quickstart.md)
|
|
152
|
-
- [Run
|
|
153
|
-
- [
|
|
154
|
-
- [
|
|
70
|
+
- [Run configuration](docs/run-config.md)
|
|
71
|
+
- [Agent tools](docs/concepts/agent-tools.md)
|
|
72
|
+
- [Composition](docs/concepts/composition.md)
|
|
73
|
+
- [Secrets](docs/secrets.md)
|
|
74
|
+
- [Limits](docs/limits.md)
|
|
155
75
|
- [Provider/runtime capabilities](docs/provider-runtime-capabilities.md)
|
|
156
|
-
- [Credentials](docs/credentials.md)
|
|
157
|
-
- [MCP](docs/mcp.md)
|
|
158
|
-
- [Skills](docs/skills.md)
|
|
159
|
-
- [Outputs](docs/outputs.md)
|
|
160
|
-
- [Events](docs/events.md)
|
|
161
|
-
- [Cleanup](docs/cleanup.md)
|
|
162
|
-
- [Testing](docs/testing.md)
|
|
163
|
-
- [Release](docs/release.md)
|
|
164
|
-
|
|
@@ -18,7 +18,6 @@ export * from "./run-cost.js";
|
|
|
18
18
|
export * from "./run-custody.js";
|
|
19
19
|
export * from "./run-retention.js";
|
|
20
20
|
export * from "./side-effect-audit.js";
|
|
21
|
-
export * from "./managed-key.js";
|
|
22
21
|
export * from "./stable.js";
|
|
23
22
|
export * from "./sdk-secrets.js";
|
|
24
23
|
export * from "./sdk-errors.js";
|
package/dist/_contracts/index.js
CHANGED
|
@@ -18,7 +18,6 @@ export * from "./run-cost.js";
|
|
|
18
18
|
export * from "./run-custody.js";
|
|
19
19
|
export * from "./run-retention.js";
|
|
20
20
|
export * from "./side-effect-audit.js";
|
|
21
|
-
export * from "./managed-key.js";
|
|
22
21
|
export * from "./stable.js";
|
|
23
22
|
export * from "./sdk-secrets.js";
|
|
24
23
|
export * from "./sdk-errors.js";
|
|
@@ -37,8 +37,7 @@ export const MODEL_PROVIDER_IDS = {
|
|
|
37
37
|
// China Volcengine Ark gateway (`doubao-cn`). Ark accepts the API-format
|
|
38
38
|
// model NAME directly in the chat-completions `model` field (no `ep-…`
|
|
39
39
|
// inference-endpoint id). The native strings are the same Ark catalog ids on
|
|
40
|
-
// both gateways
|
|
41
|
-
// (both providers ship `live-unverified` until then — provider-support.ts).
|
|
40
|
+
// both gateways.
|
|
42
41
|
// pro — Doubao Seed 1.8 (flagship, 256K context).
|
|
43
42
|
// flash — Doubao Seed 1.6 Flash (fast/cheap, 256K context).
|
|
44
43
|
"doubao-seed-pro": { doubao: "doubao-seed-1-8-251228", "doubao-cn": "doubao-seed-1-8-251228" },
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { HttpClient } from "./http.js";
|
|
2
2
|
import type { RunUnit } from "./run-unit.js";
|
|
3
|
-
import type { AgentsMdRecord, FileRecord, Output, OutputFileDownload, OutputFileSelector, Run, RunEvent, SecretRecord, SecretReveal,
|
|
3
|
+
import type { AgentsMdRecord, FileRecord, Output, OutputLink, OutputLinkOptions, OutputFileDownload, OutputFileSelector, OutputFileType, OutputQuery, Run, RunEvent, SecretRecord, SecretReveal, Skill, WhoAmI } from "./runtime-types.js";
|
|
4
4
|
import type { PlatformRunSubmissionInput } from "./submission.js";
|
|
5
5
|
/**
|
|
6
6
|
* The single source of truth for SDK<->BFF transport. The SDK class
|
|
@@ -47,8 +47,13 @@ export interface CoordinatorTicket {
|
|
|
47
47
|
* coordinator is configured for the deployment (HTTP 503).
|
|
48
48
|
*/
|
|
49
49
|
export declare function getCoordinatorTicket(http: HttpClient, runId: string): Promise<CoordinatorTicket>;
|
|
50
|
-
export declare function listOutputs(http: HttpClient, runId: string): Promise<readonly Output[]>;
|
|
51
|
-
export declare function
|
|
50
|
+
export declare function listOutputs(http: HttpClient, runId: string, query?: OutputQuery): Promise<readonly Output[]>;
|
|
51
|
+
export declare function findOutputs(http: HttpClient, runId: string, query: OutputQuery): Promise<readonly Output[]>;
|
|
52
|
+
export declare function findOutput(http: HttpClient, runId: string, query: OutputQuery): Promise<Output | null>;
|
|
53
|
+
export type OutputLinkSelector = string | OutputFileSelector | OutputQuery;
|
|
54
|
+
export declare function outputLink(http: HttpClient, runId: string, selectorOrQuery: OutputLinkSelector, options?: OutputLinkOptions): Promise<OutputLink>;
|
|
55
|
+
export declare function createOutputLink(http: HttpClient, runId: string, selectorOrQuery: OutputLinkSelector, options?: OutputLinkOptions): Promise<OutputLink>;
|
|
56
|
+
export declare function eventArchiveLink(http: HttpClient, runId: string, options?: OutputLinkOptions): Promise<OutputLink>;
|
|
52
57
|
export declare function resolveOutputFileSelector(outputs: readonly Output[], selector: OutputFileSelector, runId?: string): Output;
|
|
53
58
|
export declare function downloadOutput(http: HttpClient, runId: string, selector: OutputFileSelector): Promise<OutputFileDownload>;
|
|
54
59
|
export declare function cancelRun(http: HttpClient, runId: string): Promise<void>;
|
|
@@ -61,6 +66,9 @@ export declare function deleteRun(http: HttpClient, runId: string): Promise<void
|
|
|
61
66
|
*/
|
|
62
67
|
export declare function deleteWorkspaceAsset(http: HttpClient, hash: string): Promise<void>;
|
|
63
68
|
export declare function whoami(http: HttpClient): Promise<WhoAmI>;
|
|
69
|
+
export declare function filterOutputs(outputs: readonly Output[], query: OutputQuery): readonly Output[];
|
|
70
|
+
export declare function classifyOutput(output: Pick<Output, "filename" | "contentType">): OutputFileType;
|
|
71
|
+
export declare function normalizeOutputLinkExpiresIn(input?: OutputLinkOptions["expiresIn"]): number;
|
|
64
72
|
/**
|
|
65
73
|
* Download EVERYTHING public about a run as one zip, organised into the three
|
|
66
74
|
* namespace folders:
|
|
@@ -215,8 +223,8 @@ export declare function createSecret(http: HttpClient, args: {
|
|
|
215
223
|
export declare function listSecrets(http: HttpClient): Promise<readonly SecretRecord[]>;
|
|
216
224
|
/** Metadata for one workspace secret by name. Never returns the value. */
|
|
217
225
|
export declare function getSecret(http: HttpClient, name: string): Promise<SecretRecord>;
|
|
218
|
-
/** Audited value read — the
|
|
219
|
-
export declare function
|
|
226
|
+
/** Audited value read — the preferred path that returns a workspace secret value. */
|
|
227
|
+
export declare function getSecretValue(http: HttpClient, name: string): Promise<SecretReveal>;
|
|
220
228
|
/** Replace the value of an existing workspace secret; bumps its version. */
|
|
221
229
|
export declare function rotateSecret(http: HttpClient, args: {
|
|
222
230
|
readonly name: string;
|
|
@@ -64,12 +64,50 @@ export async function listRunEvents(http, runId) {
|
|
|
64
64
|
export async function getCoordinatorTicket(http, runId) {
|
|
65
65
|
return http.request(`/api/runs/${encodeURIComponent(runId)}/events/ticket`, { method: "POST" });
|
|
66
66
|
}
|
|
67
|
-
export async function listOutputs(http, runId) {
|
|
67
|
+
export async function listOutputs(http, runId, query) {
|
|
68
68
|
const result = await http.request(`/api/runs/${encodeURIComponent(runId)}/outputs`);
|
|
69
|
-
return result.outputs;
|
|
69
|
+
return query === undefined ? result.outputs : filterOutputs(result.outputs, query);
|
|
70
|
+
}
|
|
71
|
+
export async function findOutputs(http, runId, query) {
|
|
72
|
+
return listOutputs(http, runId, query);
|
|
73
|
+
}
|
|
74
|
+
export async function findOutput(http, runId, query) {
|
|
75
|
+
const matches = await findOutputs(http, runId, query);
|
|
76
|
+
if (matches.length === 0)
|
|
77
|
+
return null;
|
|
78
|
+
if (matches.length === 1)
|
|
79
|
+
return matches[0];
|
|
80
|
+
throw new RunStateError("findOutput: output query matched multiple files", {
|
|
81
|
+
runId,
|
|
82
|
+
matches: matches.map((output) => output.filename ?? output.id)
|
|
83
|
+
});
|
|
70
84
|
}
|
|
71
|
-
export async function
|
|
72
|
-
|
|
85
|
+
export async function outputLink(http, runId, selectorOrQuery, options) {
|
|
86
|
+
const output = await resolveOutputLinkTarget(http, runId, selectorOrQuery);
|
|
87
|
+
const expiresInSeconds = normalizeOutputLinkExpiresIn(options?.expiresIn);
|
|
88
|
+
const result = await http.request(`/api/runs/${encodeURIComponent(runId)}/outputs/${encodeURIComponent(output.id)}/link`, {
|
|
89
|
+
method: "POST",
|
|
90
|
+
body: JSON.stringify({ expiresInSeconds })
|
|
91
|
+
});
|
|
92
|
+
return {
|
|
93
|
+
...result,
|
|
94
|
+
expiresInSeconds: result.expiresInSeconds ?? expiresInSeconds,
|
|
95
|
+
output: result.output ?? output
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export async function createOutputLink(http, runId, selectorOrQuery, options) {
|
|
99
|
+
return outputLink(http, runId, selectorOrQuery, options);
|
|
100
|
+
}
|
|
101
|
+
export async function eventArchiveLink(http, runId, options) {
|
|
102
|
+
const expiresInSeconds = normalizeOutputLinkExpiresIn(options?.expiresIn);
|
|
103
|
+
const result = await http.request(`/api/runs/${encodeURIComponent(runId)}/events/link`, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
body: JSON.stringify({ expiresInSeconds })
|
|
106
|
+
});
|
|
107
|
+
return {
|
|
108
|
+
...result,
|
|
109
|
+
expiresInSeconds: result.expiresInSeconds ?? expiresInSeconds
|
|
110
|
+
};
|
|
73
111
|
}
|
|
74
112
|
export function resolveOutputFileSelector(outputs, selector, runId) {
|
|
75
113
|
if (isPathSelector(selector)) {
|
|
@@ -173,6 +211,178 @@ function isPathSelector(selector) {
|
|
|
173
211
|
function normalizeOutputLookupPath(path) {
|
|
174
212
|
return path.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
175
213
|
}
|
|
214
|
+
export function filterOutputs(outputs, query) {
|
|
215
|
+
return outputs.filter((output) => outputMatchesQuery(output, query));
|
|
216
|
+
}
|
|
217
|
+
export function classifyOutput(output) {
|
|
218
|
+
const contentType = normalizeContentType(output.contentType);
|
|
219
|
+
if (contentType) {
|
|
220
|
+
if (contentType === "application/json" || contentType.endsWith("+json") || contentType.includes("json")) {
|
|
221
|
+
return "json";
|
|
222
|
+
}
|
|
223
|
+
if (contentType.startsWith("text/"))
|
|
224
|
+
return "text";
|
|
225
|
+
if (contentType.startsWith("image/"))
|
|
226
|
+
return "image";
|
|
227
|
+
if (contentType.startsWith("audio/"))
|
|
228
|
+
return "audio";
|
|
229
|
+
if (contentType.startsWith("video/"))
|
|
230
|
+
return "video";
|
|
231
|
+
if (contentType === "application/pdf")
|
|
232
|
+
return "pdf";
|
|
233
|
+
if (contentType === "application/zip" ||
|
|
234
|
+
contentType === "application/gzip" ||
|
|
235
|
+
contentType === "application/x-gzip" ||
|
|
236
|
+
contentType === "application/x-tar" ||
|
|
237
|
+
contentType === "application/x-7z-compressed" ||
|
|
238
|
+
contentType === "application/vnd.rar" ||
|
|
239
|
+
contentType === "application/zstd") {
|
|
240
|
+
return "archive";
|
|
241
|
+
}
|
|
242
|
+
if (contentType === "application/octet-stream")
|
|
243
|
+
return "binary";
|
|
244
|
+
return "unknown";
|
|
245
|
+
}
|
|
246
|
+
const extension = extensionOf(output.filename);
|
|
247
|
+
if (!extension)
|
|
248
|
+
return "unknown";
|
|
249
|
+
if (["json", "jsonl", "ndjson"].includes(extension))
|
|
250
|
+
return "json";
|
|
251
|
+
if (["txt", "log", "md", "markdown", "csv", "tsv", "xml", "html", "htm", "yaml", "yml"].includes(extension)) {
|
|
252
|
+
return "text";
|
|
253
|
+
}
|
|
254
|
+
if (["png", "jpg", "jpeg", "gif", "webp", "avif", "bmp", "tif", "tiff", "svg"].includes(extension)) {
|
|
255
|
+
return "image";
|
|
256
|
+
}
|
|
257
|
+
if (["mp3", "wav", "flac", "m4a", "aac", "ogg", "oga", "opus"].includes(extension))
|
|
258
|
+
return "audio";
|
|
259
|
+
if (["mp4", "mov", "m4v", "webm", "mkv", "avi"].includes(extension))
|
|
260
|
+
return "video";
|
|
261
|
+
if (extension === "pdf")
|
|
262
|
+
return "pdf";
|
|
263
|
+
if (["zip", "tar", "tgz", "gz", "bz2", "xz", "7z", "rar", "zst"].includes(extension))
|
|
264
|
+
return "archive";
|
|
265
|
+
if (["bin", "exe", "dll", "so", "dylib", "dmg", "iso"].includes(extension))
|
|
266
|
+
return "binary";
|
|
267
|
+
return "unknown";
|
|
268
|
+
}
|
|
269
|
+
export function normalizeOutputLinkExpiresIn(input = "1h") {
|
|
270
|
+
if (typeof input === "number") {
|
|
271
|
+
if (!Number.isFinite(input) || input <= 0) {
|
|
272
|
+
throw new RunStateError("outputLink: expiresIn must be a positive number of seconds", {
|
|
273
|
+
expiresIn: input
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
return Math.floor(input);
|
|
277
|
+
}
|
|
278
|
+
if (input === "15m")
|
|
279
|
+
return 15 * 60;
|
|
280
|
+
if (input === "1h")
|
|
281
|
+
return 60 * 60;
|
|
282
|
+
if (input === "1d")
|
|
283
|
+
return 24 * 60 * 60;
|
|
284
|
+
throw new RunStateError("outputLink: expiresIn must be seconds, \"15m\", \"1h\", or \"1d\"", {
|
|
285
|
+
expiresIn: input
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
async function resolveOutputLinkTarget(http, runId, selectorOrQuery) {
|
|
289
|
+
if (typeof selectorOrQuery === "string") {
|
|
290
|
+
if (selectorOrQuery.length === 0) {
|
|
291
|
+
throw new RunStateError("outputLink: selector must include an output id or query", { runId });
|
|
292
|
+
}
|
|
293
|
+
return { id: selectorOrQuery };
|
|
294
|
+
}
|
|
295
|
+
if (hasOutputId(selectorOrQuery)) {
|
|
296
|
+
if (selectorOrQuery.id.length === 0) {
|
|
297
|
+
throw new RunStateError("outputLink: selector must include an output id or query", { runId });
|
|
298
|
+
}
|
|
299
|
+
return selectorOrQuery;
|
|
300
|
+
}
|
|
301
|
+
if (isPathSelector(selectorOrQuery) && selectorOrQuery.match === "suffix") {
|
|
302
|
+
return resolveOutputFileSelector(await listOutputs(http, runId), selectorOrQuery, runId);
|
|
303
|
+
}
|
|
304
|
+
const match = await findOutput(http, runId, selectorOrQuery);
|
|
305
|
+
if (match)
|
|
306
|
+
return match;
|
|
307
|
+
throw new RunStateError("outputLink: output query matched no files", { runId });
|
|
308
|
+
}
|
|
309
|
+
function outputMatchesQuery(output, query) {
|
|
310
|
+
const normalizedPath = typeof output.filename === "string" ? normalizeOutputQueryPath(output.filename) : "";
|
|
311
|
+
if (query.path !== undefined && normalizedPath !== normalizeOutputQueryPath(query.path)) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
if (query.filename !== undefined) {
|
|
315
|
+
const basename = basenameOf(normalizedPath);
|
|
316
|
+
if (typeof query.filename === "string") {
|
|
317
|
+
if (basename !== query.filename)
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
query.filename.lastIndex = 0;
|
|
322
|
+
if (!query.filename.test(basename))
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (query.dir !== undefined && !directoryMatches(normalizedPath, query.dir, query.recursive ?? true)) {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
if (query.extension !== undefined && extensionOf(normalizedPath) !== normalizeExtension(query.extension)) {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
if (query.contentType !== undefined && !contentTypeMatches(output.contentType, query.contentType)) {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
if (query.type !== undefined && classifyOutput(output) !== query.type) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
function hasOutputId(value) {
|
|
341
|
+
return Boolean(value && typeof value === "object" && "id" in value && typeof value.id === "string");
|
|
342
|
+
}
|
|
343
|
+
function normalizeOutputQueryPath(path) {
|
|
344
|
+
let normalized = path.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
345
|
+
while (normalized === "outputs" || normalized.startsWith("outputs/")) {
|
|
346
|
+
normalized = normalized === "outputs" ? "" : normalized.slice("outputs/".length);
|
|
347
|
+
}
|
|
348
|
+
return normalized.replace(/\/+$/, "");
|
|
349
|
+
}
|
|
350
|
+
function basenameOf(path) {
|
|
351
|
+
return path.split("/").filter(Boolean).pop() ?? "";
|
|
352
|
+
}
|
|
353
|
+
function directoryMatches(path, dir, recursive) {
|
|
354
|
+
const normalizedDir = normalizeOutputQueryPath(dir);
|
|
355
|
+
if (normalizedDir.length === 0)
|
|
356
|
+
return true;
|
|
357
|
+
const prefix = `${normalizedDir}/`;
|
|
358
|
+
if (!path.startsWith(prefix))
|
|
359
|
+
return false;
|
|
360
|
+
const remainder = path.slice(prefix.length);
|
|
361
|
+
return remainder.length > 0 && (recursive || !remainder.includes("/"));
|
|
362
|
+
}
|
|
363
|
+
function normalizeExtension(extension) {
|
|
364
|
+
return extension.replace(/^\.+/, "").toLowerCase();
|
|
365
|
+
}
|
|
366
|
+
function extensionOf(path) {
|
|
367
|
+
if (!path)
|
|
368
|
+
return "";
|
|
369
|
+
const basename = basenameOf(normalizeOutputQueryPath(path));
|
|
370
|
+
const index = basename.lastIndexOf(".");
|
|
371
|
+
return index > 0 && index < basename.length - 1 ? basename.slice(index + 1).toLowerCase() : "";
|
|
372
|
+
}
|
|
373
|
+
function normalizeContentType(contentType) {
|
|
374
|
+
return (contentType ?? "").split(";")[0].trim().toLowerCase();
|
|
375
|
+
}
|
|
376
|
+
function contentTypeMatches(actual, expected) {
|
|
377
|
+
const normalizedActual = normalizeContentType(actual);
|
|
378
|
+
const normalizedExpected = normalizeContentType(expected);
|
|
379
|
+
if (!normalizedActual || !normalizedExpected)
|
|
380
|
+
return false;
|
|
381
|
+
if (normalizedExpected.endsWith("/*")) {
|
|
382
|
+
return normalizedActual.startsWith(normalizedExpected.slice(0, -1));
|
|
383
|
+
}
|
|
384
|
+
return normalizedActual === normalizedExpected;
|
|
385
|
+
}
|
|
176
386
|
/**
|
|
177
387
|
* Download EVERYTHING public about a run as one zip, organised into the three
|
|
178
388
|
* namespace folders:
|
|
@@ -468,7 +678,7 @@ export async function findSkillByName(http, name) {
|
|
|
468
678
|
return skills.find((skill) => skill.name === name) ?? null;
|
|
469
679
|
}
|
|
470
680
|
// ===========================================================================
|
|
471
|
-
// AgentsMd
|
|
681
|
+
// AgentsMd upload helpers. Launch submissions use content-addressed asset refs.
|
|
472
682
|
// ===========================================================================
|
|
473
683
|
/**
|
|
474
684
|
* Upload a workspace AgentsMd file as a markdown string. The BFF
|
|
@@ -505,7 +715,7 @@ function unwrapAgentsMd(result) {
|
|
|
505
715
|
return result;
|
|
506
716
|
}
|
|
507
717
|
// ===========================================================================
|
|
508
|
-
// File
|
|
718
|
+
// File upload helpers. Launch submissions use content-addressed asset refs.
|
|
509
719
|
// ===========================================================================
|
|
510
720
|
/**
|
|
511
721
|
* Upload a workspace File as a zip bundle. The BFF canonicalises the
|
|
@@ -547,7 +757,7 @@ function unwrapFile(result) {
|
|
|
547
757
|
// Value-bearing requests (create/rotate) carry the value in the JSON BODY,
|
|
548
758
|
// never the URL/query, so it never lands in logs or the request line. Reads
|
|
549
759
|
// split by sensitivity: `getSecret`/`listSecrets` return METADATA only;
|
|
550
|
-
// `
|
|
760
|
+
// `getSecretValue` is the audited value read (POST so it's a logged action).
|
|
551
761
|
// ===========================================================================
|
|
552
762
|
/** Create a named workspace secret. The value travels in the body. */
|
|
553
763
|
export async function createSecret(http, args) {
|
|
@@ -569,9 +779,9 @@ export async function getSecret(http, name) {
|
|
|
569
779
|
const result = await http.request(`/api/secrets/${encodeURIComponent(name)}`);
|
|
570
780
|
return unwrapSecret(result);
|
|
571
781
|
}
|
|
572
|
-
/** Audited value read — the
|
|
573
|
-
export async function
|
|
574
|
-
return http.request(`/api/secrets/${encodeURIComponent(name)}/
|
|
782
|
+
/** Audited value read — the preferred path that returns a workspace secret value. */
|
|
783
|
+
export async function getSecretValue(http, name) {
|
|
784
|
+
return http.request(`/api/secrets/${encodeURIComponent(name)}/get_value`, {
|
|
575
785
|
method: "POST"
|
|
576
786
|
});
|
|
577
787
|
}
|