@aexhq/sdk 0.38.1 → 0.40.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/README.md +23 -17
- package/dist/_contracts/api-key.d.ts +49 -0
- package/dist/_contracts/api-key.js +87 -0
- package/dist/_contracts/bundle-manifest.d.ts +86 -0
- package/dist/_contracts/bundle-manifest.js +157 -0
- package/dist/_contracts/error-codes.d.ts +26 -0
- package/dist/_contracts/error-codes.js +79 -0
- package/dist/_contracts/error-factory.d.ts +32 -0
- package/dist/_contracts/error-factory.js +142 -0
- package/dist/_contracts/event-envelope.d.ts +33 -10
- package/dist/_contracts/event-envelope.js +46 -10
- package/dist/_contracts/event-view.d.ts +123 -0
- package/dist/_contracts/event-view.js +120 -0
- package/dist/_contracts/http.js +12 -3
- package/dist/_contracts/index.d.ts +8 -4
- package/dist/_contracts/index.js +11 -7
- package/dist/_contracts/models.d.ts +15 -0
- package/dist/_contracts/models.js +33 -0
- package/dist/_contracts/operations.d.ts +70 -2
- package/dist/_contracts/operations.js +143 -7
- package/dist/_contracts/run-config.d.ts +35 -10
- package/dist/_contracts/run-config.js +17 -5
- package/dist/_contracts/run-record.d.ts +3 -2
- package/dist/_contracts/runtime-types.d.ts +148 -36
- package/dist/_contracts/runtime-types.js +33 -1
- package/dist/_contracts/sdk-errors.d.ts +61 -2
- package/dist/_contracts/sdk-errors.js +83 -3
- package/dist/_contracts/sdk-secrets.js +31 -6
- package/dist/_contracts/stable.d.ts +14 -0
- package/dist/_contracts/stable.js +14 -0
- package/dist/_contracts/status.d.ts +28 -1
- package/dist/_contracts/status.js +48 -3
- package/dist/_contracts/submission.d.ts +157 -14
- package/dist/_contracts/submission.js +319 -55
- package/dist/_contracts/suggest.d.ts +15 -0
- package/dist/_contracts/suggest.js +54 -0
- package/dist/asset-upload.d.ts +27 -1
- package/dist/asset-upload.js +219 -4
- package/dist/asset-upload.js.map +1 -1
- package/dist/bundle.d.ts +23 -14
- package/dist/bundle.js +39 -20
- package/dist/bundle.js.map +1 -1
- package/dist/canonical-zip.d.ts +68 -0
- package/dist/canonical-zip.js +307 -0
- package/dist/canonical-zip.js.map +1 -0
- package/dist/cli.mjs +1927 -326
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/client.d.ts +273 -70
- package/dist/client.js +885 -335
- package/dist/client.js.map +1 -1
- package/dist/fetch-archive.js +14 -14
- package/dist/fetch-archive.js.map +1 -1
- package/dist/file.d.ts +33 -6
- package/dist/file.js +120 -54
- package/dist/file.js.map +1 -1
- package/dist/index.d.ts +23 -14
- package/dist/index.js +31 -14
- package/dist/index.js.map +1 -1
- package/dist/node-fs.d.ts +26 -9
- package/dist/node-fs.js +13 -38
- package/dist/node-fs.js.map +1 -1
- package/dist/node-walk.d.ts +69 -0
- package/dist/node-walk.js +146 -0
- package/dist/node-walk.js.map +1 -0
- package/dist/retry.d.ts +9 -13
- package/dist/retry.js +18 -17
- package/dist/retry.js.map +1 -1
- package/dist/skill.d.ts +151 -0
- package/dist/skill.js +298 -0
- package/dist/skill.js.map +1 -0
- package/dist/tool.d.ts +14 -2
- package/dist/tool.js +33 -7
- package/dist/tool.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/authentication.md +29 -5
- package/docs/billing.md +6 -0
- package/docs/concepts/agent-tools.md +11 -0
- package/docs/concepts/composition.md +3 -3
- package/docs/defaults.md +1 -0
- package/docs/errors.md +64 -4
- package/docs/events.md +84 -49
- package/docs/limits-and-quotas.md +24 -0
- package/docs/mcp.md +3 -2
- package/docs/networking.md +7 -1
- package/docs/outputs.md +36 -7
- package/docs/provider-runtime-capabilities.md +1 -1
- package/docs/quickstart.md +17 -7
- package/docs/run-config.md +3 -3
- package/docs/secrets.md +14 -0
- package/docs/skills.md +74 -44
- package/docs/vision-skills.md +3 -3
- package/examples/feature-tour.ts +4 -6
- package/examples/spike-settle-latency.ts +125 -0
- package/package.json +4 -3
- package/dist/_contracts/event-guards.d.ts +0 -67
- package/dist/_contracts/event-guards.js +0 -36
- package/dist/skill-tool.d.ts +0 -102
- package/dist/skill-tool.js +0 -190
- package/dist/skill-tool.js.map +0 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ The package ships:
|
|
|
10
10
|
|
|
11
11
|
- `Aex` for sessions, one-shot runs, inspect, download, cancel, and delete.
|
|
12
12
|
- `sessions` / `openSession()` for durable, resumable agent sessions.
|
|
13
|
-
- Typed run primitives: `Models`, `Providers`, `Sizes`, `Tool` / `Tools
|
|
13
|
+
- Typed run primitives: `Models`, `Providers`, `Sizes`, `Skill`, `Tool` / `Tools`, `AgentsMd`, `File`, `McpServer`, and `Secret`.
|
|
14
14
|
- A bundled `aex` CLI with the same run, status, events, outputs, download, cancel, delete, and whoami operations.
|
|
15
15
|
|
|
16
16
|
## Install
|
|
@@ -19,9 +19,11 @@ The package ships:
|
|
|
19
19
|
npm i @aexhq/sdk
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
This installs the TypeScript SDK exports and the bundled `aex` CLI.
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
This installs the TypeScript SDK exports and the bundled `aex` CLI. The CLI
|
|
23
|
+
ships inside the package — invoke it with `npx aex …` after a local install, or
|
|
24
|
+
`npm i -g @aexhq/sdk` to put a bare `aex` on your PATH. Set both credentials
|
|
25
|
+
before running the examples: `AEX_API_KEY` authenticates to aex, and
|
|
26
|
+
`ANTHROPIC_API_KEY` is your BYOK provider key for Claude.
|
|
25
27
|
|
|
26
28
|
```bash
|
|
27
29
|
export AEX_API_KEY="<your-aex-api-key>"
|
|
@@ -33,7 +35,7 @@ export ANTHROPIC_API_KEY="<your-anthropic-api-key>"
|
|
|
33
35
|
```ts
|
|
34
36
|
import { Aex, Models, Sizes } from "@aexhq/sdk";
|
|
35
37
|
|
|
36
|
-
const aex = new Aex(
|
|
38
|
+
const aex = new Aex(process.env.AEX_API_KEY!);
|
|
37
39
|
|
|
38
40
|
const session = await aex.openSession({
|
|
39
41
|
model: Models.CLAUDE_HAIKU_4_5,
|
|
@@ -44,8 +46,10 @@ const session = await aex.openSession({
|
|
|
44
46
|
apiKeys: { anthropic: process.env.ANTHROPIC_API_KEY! }
|
|
45
47
|
});
|
|
46
48
|
|
|
49
|
+
// done() awaits settle by default, so status is the terminal outcome
|
|
50
|
+
// ("succeeded"), and costUsd/usage are populated.
|
|
47
51
|
const first = await session.send("Summarize this repo.").done();
|
|
48
|
-
console.log(first.status, first.text);
|
|
52
|
+
console.log(first.status, first.costUsd, first.text);
|
|
49
53
|
|
|
50
54
|
const resumed = await aex.openSession(session.id);
|
|
51
55
|
await resumed.send("Continue with the follow-up validation.").done();
|
|
@@ -78,10 +82,10 @@ await aex.run({
|
|
|
78
82
|
});
|
|
79
83
|
```
|
|
80
84
|
|
|
81
|
-
The same request can run from the bundled CLI:
|
|
85
|
+
The same request can run from the bundled CLI (`npx aex` on a local install):
|
|
82
86
|
|
|
83
87
|
```bash
|
|
84
|
-
aex run \
|
|
88
|
+
npx aex run \
|
|
85
89
|
--api-key "$AEX_API_KEY" \
|
|
86
90
|
--anthropic-api-key "$ANTHROPIC_API_KEY" \
|
|
87
91
|
--model claude-haiku-4-5 \
|
|
@@ -97,20 +101,22 @@ your default `--aex-url`) is persisted to a `0600` config file
|
|
|
97
101
|
on Windows). An explicit `--api-key` flag always overrides the stored one.
|
|
98
102
|
|
|
99
103
|
```bash
|
|
100
|
-
aex login --api-key "$AEX_API_KEY" [--aex-url https://api.aex.dev]
|
|
101
|
-
aex whoami # no --api-key needed after login
|
|
102
|
-
aex
|
|
103
|
-
aex
|
|
104
|
+
npx aex login --api-key "$AEX_API_KEY" [--aex-url https://api.aex.dev]
|
|
105
|
+
npx aex whoami # no --api-key needed after login
|
|
106
|
+
npx aex whoami --json # machine-readable workspace + scopes + limits
|
|
107
|
+
npx aex auth status # show the resolved config (the token value is never printed)
|
|
108
|
+
npx aex logout # clear the stored token
|
|
104
109
|
```
|
|
105
110
|
|
|
106
111
|
Discover the closed sets the platform accepts — no token, no network (human table
|
|
107
|
-
by default, machine JSON under `--json`):
|
|
112
|
+
by default, machine JSON under `--json`). Per-verb `--help` is also key-free:
|
|
108
113
|
|
|
109
114
|
```bash
|
|
110
|
-
aex models list # canonical models + their default provider
|
|
111
|
-
aex providers list # providers + the models each serves
|
|
112
|
-
aex tools list # complete builtin tool set
|
|
113
|
-
aex runtime-sizes list # managed runtime presets (cpus / memory / default)
|
|
115
|
+
npx aex models list # canonical models + their default provider
|
|
116
|
+
npx aex providers list # providers + the models each serves
|
|
117
|
+
npx aex tools list # complete builtin tool set
|
|
118
|
+
npx aex runtime-sizes list # managed runtime presets (cpus / memory / default)
|
|
119
|
+
npx aex run --help # flags for one verb (no API key required)
|
|
114
120
|
```
|
|
115
121
|
|
|
116
122
|
Errors are typed and actionable. Every `openSession()` / `run()` config-validation
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-describing API-key codec (SSoT).
|
|
3
|
+
*
|
|
4
|
+
* An aex API key is `aex_<plane>_<regionCode>_<workspaceId>_<secret>_<crc>` —
|
|
5
|
+
* it embeds its own plane and region, CRC-protected. This is the canonical
|
|
6
|
+
* client-side codec: the SDK constructor parses the key to DERIVE the target
|
|
7
|
+
* plane (zero-network) and to fail fast on a plane/baseUrl mismatch instead of
|
|
8
|
+
* surfacing a bare `token_invalid` after a full round-trip.
|
|
9
|
+
*
|
|
10
|
+
* Ported byte-for-byte from the platform codec
|
|
11
|
+
* (`aex-platform apps/dashboard/src/server/auth.ts`), which is pinned to this
|
|
12
|
+
* module by a cross-repo parity test. Parse-only: it makes NO trust decision
|
|
13
|
+
* (the server still validates the secret).
|
|
14
|
+
*/
|
|
15
|
+
export declare const API_KEY_PLANES: readonly ["dev", "prd"];
|
|
16
|
+
export type ApiKeyPlane = (typeof API_KEY_PLANES)[number];
|
|
17
|
+
/** Supported region → embedded region code. */
|
|
18
|
+
export declare const API_KEY_REGION_TO_CODE: Readonly<Record<string, string>>;
|
|
19
|
+
export interface ParsedApiKey {
|
|
20
|
+
readonly plane: ApiKeyPlane;
|
|
21
|
+
readonly regionCode: string;
|
|
22
|
+
readonly region: string;
|
|
23
|
+
/** The dash-free workspace id embedded in the key. */
|
|
24
|
+
readonly workspaceId: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Canonical form of a workspaceId for EMBEDDING in a key: dashes stripped so the
|
|
28
|
+
* whole key is a single double-click-selectable word (`_` is a word char, `-`
|
|
29
|
+
* is not). Kept byte-for-byte in sync with the platform codec.
|
|
30
|
+
*/
|
|
31
|
+
export declare function normalizeWorkspaceId(workspaceId: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* Parse a self-describing API key, or `null` for any legacy/opaque/tampered
|
|
34
|
+
* value. Validates the `aex_` prefix, the 6-part shape, a known plane and
|
|
35
|
+
* region code, and the CRC over the first 5 parts.
|
|
36
|
+
*/
|
|
37
|
+
export declare function parseApiKey(token: string): ParsedApiKey | null;
|
|
38
|
+
/**
|
|
39
|
+
* Assemble a valid API key from its parts (the inverse of {@link parseApiKey}).
|
|
40
|
+
* Unlike the server's `mintApiKeyValue` this takes an EXPLICIT `secret` so it is
|
|
41
|
+
* deterministic — used by codec round-trip / cross-repo parity tests. The
|
|
42
|
+
* embedded workspace id is dash-normalized; `workspaceId` must not contain `_`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function formatApiKey(input: {
|
|
45
|
+
readonly plane: ApiKeyPlane;
|
|
46
|
+
readonly region: string;
|
|
47
|
+
readonly workspaceId: string;
|
|
48
|
+
readonly secret: string;
|
|
49
|
+
}): string;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-describing API-key codec (SSoT).
|
|
3
|
+
*
|
|
4
|
+
* An aex API key is `aex_<plane>_<regionCode>_<workspaceId>_<secret>_<crc>` —
|
|
5
|
+
* it embeds its own plane and region, CRC-protected. This is the canonical
|
|
6
|
+
* client-side codec: the SDK constructor parses the key to DERIVE the target
|
|
7
|
+
* plane (zero-network) and to fail fast on a plane/baseUrl mismatch instead of
|
|
8
|
+
* surfacing a bare `token_invalid` after a full round-trip.
|
|
9
|
+
*
|
|
10
|
+
* Ported byte-for-byte from the platform codec
|
|
11
|
+
* (`aex-platform apps/dashboard/src/server/auth.ts`), which is pinned to this
|
|
12
|
+
* module by a cross-repo parity test. Parse-only: it makes NO trust decision
|
|
13
|
+
* (the server still validates the secret).
|
|
14
|
+
*/
|
|
15
|
+
export const API_KEY_PLANES = ["dev", "prd"];
|
|
16
|
+
/** Supported region → embedded region code. */
|
|
17
|
+
export const API_KEY_REGION_TO_CODE = {
|
|
18
|
+
"eu-west-2": "euw2",
|
|
19
|
+
"us-west-2": "usw2",
|
|
20
|
+
"ap-northeast-1": "apn1"
|
|
21
|
+
};
|
|
22
|
+
const CODE_TO_REGION = Object.fromEntries(Object.entries(API_KEY_REGION_TO_CODE).map(([region, code]) => [code, region]));
|
|
23
|
+
const API_KEY_PLANE_SET = new Set(API_KEY_PLANES);
|
|
24
|
+
/**
|
|
25
|
+
* Canonical form of a workspaceId for EMBEDDING in a key: dashes stripped so the
|
|
26
|
+
* whole key is a single double-click-selectable word (`_` is a word char, `-`
|
|
27
|
+
* is not). Kept byte-for-byte in sync with the platform codec.
|
|
28
|
+
*/
|
|
29
|
+
export function normalizeWorkspaceId(workspaceId) {
|
|
30
|
+
return workspaceId.replace(/-/g, "");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Parse a self-describing API key, or `null` for any legacy/opaque/tampered
|
|
34
|
+
* value. Validates the `aex_` prefix, the 6-part shape, a known plane and
|
|
35
|
+
* region code, and the CRC over the first 5 parts.
|
|
36
|
+
*/
|
|
37
|
+
export function parseApiKey(token) {
|
|
38
|
+
if (typeof token !== "string" || !token.startsWith("aex_"))
|
|
39
|
+
return null;
|
|
40
|
+
const parts = token.split("_");
|
|
41
|
+
if (parts.length !== 6)
|
|
42
|
+
return null;
|
|
43
|
+
const [prefix, plane, regionCode, workspaceId, secret, crc] = parts;
|
|
44
|
+
if (prefix !== "aex" || !API_KEY_PLANE_SET.has(plane) || !regionCode || !workspaceId || !secret || !crc) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
if (crc32Base36(parts.slice(0, 5).join("_")) !== crc)
|
|
48
|
+
return null;
|
|
49
|
+
const region = CODE_TO_REGION[regionCode];
|
|
50
|
+
if (region === undefined)
|
|
51
|
+
return null;
|
|
52
|
+
return { plane: plane, regionCode, region, workspaceId };
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Assemble a valid API key from its parts (the inverse of {@link parseApiKey}).
|
|
56
|
+
* Unlike the server's `mintApiKeyValue` this takes an EXPLICIT `secret` so it is
|
|
57
|
+
* deterministic — used by codec round-trip / cross-repo parity tests. The
|
|
58
|
+
* embedded workspace id is dash-normalized; `workspaceId` must not contain `_`.
|
|
59
|
+
*/
|
|
60
|
+
export function formatApiKey(input) {
|
|
61
|
+
const code = API_KEY_REGION_TO_CODE[input.region];
|
|
62
|
+
if (code === undefined) {
|
|
63
|
+
throw new Error(`API key region is not supported: ${input.region}`);
|
|
64
|
+
}
|
|
65
|
+
if (!input.workspaceId || input.workspaceId.includes("_")) {
|
|
66
|
+
throw new Error("workspaceId must be non-empty and contain no '_'");
|
|
67
|
+
}
|
|
68
|
+
if (!input.secret || input.secret.includes("_")) {
|
|
69
|
+
throw new Error("secret must be non-empty and contain no '_'");
|
|
70
|
+
}
|
|
71
|
+
const body = ["aex", input.plane, code, normalizeWorkspaceId(input.workspaceId), input.secret].join("_");
|
|
72
|
+
return `${body}_${crc32Base36(body)}`;
|
|
73
|
+
}
|
|
74
|
+
function crc32Base36(input) {
|
|
75
|
+
return crc32(input).toString(36);
|
|
76
|
+
}
|
|
77
|
+
function crc32(input) {
|
|
78
|
+
let crc = 0xffffffff;
|
|
79
|
+
for (const byte of Buffer.from(input, "utf8")) {
|
|
80
|
+
crc ^= byte;
|
|
81
|
+
for (let i = 0; i < 8; i += 1) {
|
|
82
|
+
crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=api-key.js.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundle fidelity sidecar — the shared source of truth for the `.aexmeta.json`
|
|
3
|
+
* metadata entry that rides inside a canonical bundle zip.
|
|
4
|
+
*
|
|
5
|
+
* A canonical bundle (skill / tool / file / agentsMd) is a deterministic zip of
|
|
6
|
+
* regular files. fflate's `unzipSync` on the restore side is asymmetric: it
|
|
7
|
+
* surfaces only entry bytes, never external attributes / os field / symlink
|
|
8
|
+
* flags. So exec bits and symlinks — the metadata a local dir → zip →
|
|
9
|
+
* container-restore round-trip must preserve — cannot travel in the zip's
|
|
10
|
+
* external attributes and be read back. Instead they travel in ONE reserved
|
|
11
|
+
* entry, `.aexmeta.json`, which both the SDK (encode) and the container
|
|
12
|
+
* materializer (restore) agree on. This module is that agreement.
|
|
13
|
+
*
|
|
14
|
+
* SECURITY: {@link symlinkTargetEscapes} is a restore-time boundary — it decides
|
|
15
|
+
* whether a captured symlink target may be recreated on the container FS. It is
|
|
16
|
+
* PURELY LEXICAL (no realpath, no FS access → no TOCTOU) and is fuzzed. Both
|
|
17
|
+
* repos import it; do not fork it.
|
|
18
|
+
*
|
|
19
|
+
* Determinism: the sidecar is emitted ONLY when there is metadata to carry (exec
|
|
20
|
+
* or symlinks non-empty) and is serialized canonically (fixed field order, sorted
|
|
21
|
+
* arrays, no insignificant whitespace, no trailing newline). A pure-content
|
|
22
|
+
* bundle emits NO sidecar → its bytes stay identical to the pre-fidelity output,
|
|
23
|
+
* so existing content-addressed dedup is preserved.
|
|
24
|
+
*/
|
|
25
|
+
/** Reserved zip entry carrying the fidelity sidecar. Rejected as a user file path. */
|
|
26
|
+
export declare const RESERVED_META_ENTRY = ".aexmeta.json";
|
|
27
|
+
/** Normalised mode for an executable regular file (has any exec bit). */
|
|
28
|
+
export declare const EXEC_MODE = 493;
|
|
29
|
+
/** Normalised mode for an ordinary regular file. */
|
|
30
|
+
export declare const DEFAULT_FILE_MODE = 420;
|
|
31
|
+
/** Bundle-time cap on a captured symlink target string. */
|
|
32
|
+
export declare const MAX_SYMLINK_TARGET_LENGTH = 4096;
|
|
33
|
+
/** A captured symlink: `path` is the bundle-relative link name, `target` the raw `readlink()` string. */
|
|
34
|
+
export interface BundleSymlink {
|
|
35
|
+
readonly path: string;
|
|
36
|
+
readonly target: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The fidelity sidecar. `exec` lists the bundle-relative paths that restore to
|
|
40
|
+
* {@link EXEC_MODE} (all other files stay {@link DEFAULT_FILE_MODE}); `symlinks`
|
|
41
|
+
* lists links to recreate (subject to {@link symlinkTargetEscapes}).
|
|
42
|
+
*/
|
|
43
|
+
export interface BundleManifest {
|
|
44
|
+
readonly v: 1;
|
|
45
|
+
readonly exec: readonly string[];
|
|
46
|
+
readonly symlinks: readonly BundleSymlink[];
|
|
47
|
+
}
|
|
48
|
+
/** True when a manifest carries no metadata worth emitting (so no sidecar is written). */
|
|
49
|
+
export declare function bundleManifestIsEmpty(manifest: {
|
|
50
|
+
readonly exec?: readonly string[];
|
|
51
|
+
readonly symlinks?: readonly {
|
|
52
|
+
readonly path: string;
|
|
53
|
+
}[];
|
|
54
|
+
}): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Serialize a manifest to canonical, byte-stable UTF-8 bytes: fixed field order
|
|
57
|
+
* (`v`, `exec`, `symlinks`), `exec` sorted ascending, `symlinks` sorted by
|
|
58
|
+
* `path`, no insignificant whitespace (compact JSON), no trailing newline. Two
|
|
59
|
+
* equal manifests always serialize to identical bytes → the sidecar never
|
|
60
|
+
* perturbs dedup determinism.
|
|
61
|
+
*/
|
|
62
|
+
export declare function serializeBundleManifest(manifest: BundleManifest): Uint8Array;
|
|
63
|
+
/**
|
|
64
|
+
* Parse the sidecar bytes into a validated {@link BundleManifest}, or `null` when
|
|
65
|
+
* the bytes are absent, not valid canonical JSON, or carry an unknown version.
|
|
66
|
+
* Forward-compatible: an unknown `v` yields `null` so a restore falls back to
|
|
67
|
+
* content-only (metadata is dropped, never a hard failure). Structurally-invalid
|
|
68
|
+
* entries are dropped individually; the parse never throws.
|
|
69
|
+
*/
|
|
70
|
+
export declare function parseBundleManifest(bytes: Uint8Array | null | undefined): BundleManifest | null;
|
|
71
|
+
/**
|
|
72
|
+
* SECURITY boundary — decide whether recreating a symlink `linkRelPath` → `target`
|
|
73
|
+
* would let the link escape the bundle root. PURELY LEXICAL (no realpath / no FS
|
|
74
|
+
* access → no TOCTOU). Returns `true` (REJECT / do not create) when ANY of:
|
|
75
|
+
*
|
|
76
|
+
* - `target` is empty or contains a NUL;
|
|
77
|
+
* - `target` is absolute — starts with `/`, matches a Windows drive (`C:`), is a
|
|
78
|
+
* UNC path (`\\host`), or contains any backslash;
|
|
79
|
+
* - the lexically-resolved target (join the link's dirname with the target, then
|
|
80
|
+
* normalise `.`/`..`) climbs at or above the bundle root (leading `..`).
|
|
81
|
+
*
|
|
82
|
+
* Otherwise the link is relative and resolves to a path inside (or at) the bundle
|
|
83
|
+
* root — safe to recreate with the ORIGINAL relative `target`. A dangling in-root
|
|
84
|
+
* target (pointing at a not-yet-existing sibling) is SAFE and allowed.
|
|
85
|
+
*/
|
|
86
|
+
export declare function symlinkTargetEscapes(linkRelPath: string, target: string): boolean;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundle fidelity sidecar — the shared source of truth for the `.aexmeta.json`
|
|
3
|
+
* metadata entry that rides inside a canonical bundle zip.
|
|
4
|
+
*
|
|
5
|
+
* A canonical bundle (skill / tool / file / agentsMd) is a deterministic zip of
|
|
6
|
+
* regular files. fflate's `unzipSync` on the restore side is asymmetric: it
|
|
7
|
+
* surfaces only entry bytes, never external attributes / os field / symlink
|
|
8
|
+
* flags. So exec bits and symlinks — the metadata a local dir → zip →
|
|
9
|
+
* container-restore round-trip must preserve — cannot travel in the zip's
|
|
10
|
+
* external attributes and be read back. Instead they travel in ONE reserved
|
|
11
|
+
* entry, `.aexmeta.json`, which both the SDK (encode) and the container
|
|
12
|
+
* materializer (restore) agree on. This module is that agreement.
|
|
13
|
+
*
|
|
14
|
+
* SECURITY: {@link symlinkTargetEscapes} is a restore-time boundary — it decides
|
|
15
|
+
* whether a captured symlink target may be recreated on the container FS. It is
|
|
16
|
+
* PURELY LEXICAL (no realpath, no FS access → no TOCTOU) and is fuzzed. Both
|
|
17
|
+
* repos import it; do not fork it.
|
|
18
|
+
*
|
|
19
|
+
* Determinism: the sidecar is emitted ONLY when there is metadata to carry (exec
|
|
20
|
+
* or symlinks non-empty) and is serialized canonically (fixed field order, sorted
|
|
21
|
+
* arrays, no insignificant whitespace, no trailing newline). A pure-content
|
|
22
|
+
* bundle emits NO sidecar → its bytes stay identical to the pre-fidelity output,
|
|
23
|
+
* so existing content-addressed dedup is preserved.
|
|
24
|
+
*/
|
|
25
|
+
/** Reserved zip entry carrying the fidelity sidecar. Rejected as a user file path. */
|
|
26
|
+
export const RESERVED_META_ENTRY = ".aexmeta.json";
|
|
27
|
+
/** Normalised mode for an executable regular file (has any exec bit). */
|
|
28
|
+
export const EXEC_MODE = 0o755;
|
|
29
|
+
/** Normalised mode for an ordinary regular file. */
|
|
30
|
+
export const DEFAULT_FILE_MODE = 0o644;
|
|
31
|
+
/** Bundle-time cap on a captured symlink target string. */
|
|
32
|
+
export const MAX_SYMLINK_TARGET_LENGTH = 4096;
|
|
33
|
+
/** True when a manifest carries no metadata worth emitting (so no sidecar is written). */
|
|
34
|
+
export function bundleManifestIsEmpty(manifest) {
|
|
35
|
+
return (manifest.exec?.length ?? 0) === 0 && (manifest.symlinks?.length ?? 0) === 0;
|
|
36
|
+
}
|
|
37
|
+
const META_TEXT_ENCODER = new TextEncoder();
|
|
38
|
+
const META_TEXT_DECODER = new TextDecoder("utf-8", { fatal: false });
|
|
39
|
+
/**
|
|
40
|
+
* Serialize a manifest to canonical, byte-stable UTF-8 bytes: fixed field order
|
|
41
|
+
* (`v`, `exec`, `symlinks`), `exec` sorted ascending, `symlinks` sorted by
|
|
42
|
+
* `path`, no insignificant whitespace (compact JSON), no trailing newline. Two
|
|
43
|
+
* equal manifests always serialize to identical bytes → the sidecar never
|
|
44
|
+
* perturbs dedup determinism.
|
|
45
|
+
*/
|
|
46
|
+
export function serializeBundleManifest(manifest) {
|
|
47
|
+
const exec = [...manifest.exec].sort(byString);
|
|
48
|
+
const symlinks = [...manifest.symlinks]
|
|
49
|
+
.map((s) => ({ path: s.path, target: s.target }))
|
|
50
|
+
.sort((a, b) => byString(a.path, b.path));
|
|
51
|
+
// Object literal key order is the wire order under JSON.stringify (fixed).
|
|
52
|
+
const json = JSON.stringify({ v: 1, exec, symlinks });
|
|
53
|
+
return META_TEXT_ENCODER.encode(json);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Parse the sidecar bytes into a validated {@link BundleManifest}, or `null` when
|
|
57
|
+
* the bytes are absent, not valid canonical JSON, or carry an unknown version.
|
|
58
|
+
* Forward-compatible: an unknown `v` yields `null` so a restore falls back to
|
|
59
|
+
* content-only (metadata is dropped, never a hard failure). Structurally-invalid
|
|
60
|
+
* entries are dropped individually; the parse never throws.
|
|
61
|
+
*/
|
|
62
|
+
export function parseBundleManifest(bytes) {
|
|
63
|
+
if (!bytes || bytes.byteLength === 0)
|
|
64
|
+
return null;
|
|
65
|
+
let parsed;
|
|
66
|
+
try {
|
|
67
|
+
parsed = JSON.parse(META_TEXT_DECODER.decode(bytes));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
73
|
+
return null;
|
|
74
|
+
const record = parsed;
|
|
75
|
+
if (record.v !== 1)
|
|
76
|
+
return null;
|
|
77
|
+
const exec = [];
|
|
78
|
+
if (Array.isArray(record.exec)) {
|
|
79
|
+
for (const item of record.exec) {
|
|
80
|
+
if (typeof item === "string" && item.length > 0)
|
|
81
|
+
exec.push(item);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const symlinks = [];
|
|
85
|
+
if (Array.isArray(record.symlinks)) {
|
|
86
|
+
for (const item of record.symlinks) {
|
|
87
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
88
|
+
continue;
|
|
89
|
+
const rec = item;
|
|
90
|
+
const path = rec.path;
|
|
91
|
+
const target = rec.target;
|
|
92
|
+
if (typeof path === "string" && path.length > 0 && typeof target === "string") {
|
|
93
|
+
symlinks.push({ path, target });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { v: 1, exec, symlinks };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* SECURITY boundary — decide whether recreating a symlink `linkRelPath` → `target`
|
|
101
|
+
* would let the link escape the bundle root. PURELY LEXICAL (no realpath / no FS
|
|
102
|
+
* access → no TOCTOU). Returns `true` (REJECT / do not create) when ANY of:
|
|
103
|
+
*
|
|
104
|
+
* - `target` is empty or contains a NUL;
|
|
105
|
+
* - `target` is absolute — starts with `/`, matches a Windows drive (`C:`), is a
|
|
106
|
+
* UNC path (`\\host`), or contains any backslash;
|
|
107
|
+
* - the lexically-resolved target (join the link's dirname with the target, then
|
|
108
|
+
* normalise `.`/`..`) climbs at or above the bundle root (leading `..`).
|
|
109
|
+
*
|
|
110
|
+
* Otherwise the link is relative and resolves to a path inside (or at) the bundle
|
|
111
|
+
* root — safe to recreate with the ORIGINAL relative `target`. A dangling in-root
|
|
112
|
+
* target (pointing at a not-yet-existing sibling) is SAFE and allowed.
|
|
113
|
+
*/
|
|
114
|
+
export function symlinkTargetEscapes(linkRelPath, target) {
|
|
115
|
+
if (typeof target !== "string" || target.length === 0)
|
|
116
|
+
return true;
|
|
117
|
+
if (target.includes("\0"))
|
|
118
|
+
return true;
|
|
119
|
+
// Absolute / platform-specific targets are always rejected.
|
|
120
|
+
if (target.startsWith("/"))
|
|
121
|
+
return true;
|
|
122
|
+
if (/^[A-Za-z]:/.test(target))
|
|
123
|
+
return true;
|
|
124
|
+
if (target.includes("\\"))
|
|
125
|
+
return true;
|
|
126
|
+
// Lexically resolve relative to the link's own directory.
|
|
127
|
+
const linkDir = posixDirname(linkRelPath);
|
|
128
|
+
const joined = linkDir.length > 0 ? `${linkDir}/${target}` : target;
|
|
129
|
+
const stack = [];
|
|
130
|
+
for (const segment of joined.split("/")) {
|
|
131
|
+
if (segment === "" || segment === ".")
|
|
132
|
+
continue;
|
|
133
|
+
if (segment === "..") {
|
|
134
|
+
// Pop an in-root component, or record an escaping climb.
|
|
135
|
+
if (stack.length > 0 && stack[stack.length - 1] !== "..") {
|
|
136
|
+
stack.pop();
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
stack.push("..");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
stack.push(segment);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// A leading `..` means the resolved path is at or above the bundle root.
|
|
147
|
+
return stack.length > 0 && stack[0] === "..";
|
|
148
|
+
}
|
|
149
|
+
/** POSIX dirname of a forward-slash relative path ("" when there is no parent segment). */
|
|
150
|
+
function posixDirname(path) {
|
|
151
|
+
const idx = path.lastIndexOf("/");
|
|
152
|
+
return idx <= 0 ? "" : path.slice(0, idx);
|
|
153
|
+
}
|
|
154
|
+
function byString(a, b) {
|
|
155
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=bundle-manifest.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSoT for the platform's STABLE API error codes.
|
|
3
|
+
*
|
|
4
|
+
* The server (aex-platform `api.ts`) imports this table instead of keeping its
|
|
5
|
+
* own private copy, so a route emitting a code absent from the union fails to
|
|
6
|
+
* compile and adding a code without a message is caught. The SDK error factory
|
|
7
|
+
* ({@link import("./error-factory.js").apiErrorFromResponse}) dispatches on the
|
|
8
|
+
* code to a typed subclass; the CLI maps it to a human remedy.
|
|
9
|
+
*
|
|
10
|
+
* The code is the stable, machine-branchable identity of a failure — distinct
|
|
11
|
+
* from the human `message`. `idempotency_conflict` and `insufficient_scope`
|
|
12
|
+
* (previously message-less bare 409/403 bodies) are first-class here.
|
|
13
|
+
*/
|
|
14
|
+
export declare const AEX_API_ERROR_CODES: readonly ["unauthorized", "forbidden", "insufficient_scope", "token_invalid", "token_revoked", "token_expired", "malformed_token", "not_found", "idempotency_conflict", "session_busy", "run_not_terminal", "unknown_workspace", "workspace_concurrency_exceeded", "workspace_submit_rate_exceeded", "workspace_spend_cap_exceeded", "insufficient_balance", "rate_limited", "upstream_error", "internal_error"];
|
|
15
|
+
export type AexApiErrorCode = (typeof AEX_API_ERROR_CODES)[number];
|
|
16
|
+
/**
|
|
17
|
+
* Human message per stable code. A `Record` (not `Partial`) so a code added to
|
|
18
|
+
* {@link AEX_API_ERROR_CODES} without a message is a COMPILE error. These are
|
|
19
|
+
* the fallback messages the factory uses when the server sends a BARE code with
|
|
20
|
+
* no `message` detail.
|
|
21
|
+
*/
|
|
22
|
+
export declare const AEX_API_ERROR_MESSAGES: Record<AexApiErrorCode, string>;
|
|
23
|
+
/** Optional per-code fix hint the CLI surfaces alongside the error message. */
|
|
24
|
+
export declare const AEX_API_ERROR_REMEDIES: Partial<Record<AexApiErrorCode, string>>;
|
|
25
|
+
/** Narrow an arbitrary value to a known {@link AexApiErrorCode}. */
|
|
26
|
+
export declare function isAexApiErrorCode(value: unknown): value is AexApiErrorCode;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSoT for the platform's STABLE API error codes.
|
|
3
|
+
*
|
|
4
|
+
* The server (aex-platform `api.ts`) imports this table instead of keeping its
|
|
5
|
+
* own private copy, so a route emitting a code absent from the union fails to
|
|
6
|
+
* compile and adding a code without a message is caught. The SDK error factory
|
|
7
|
+
* ({@link import("./error-factory.js").apiErrorFromResponse}) dispatches on the
|
|
8
|
+
* code to a typed subclass; the CLI maps it to a human remedy.
|
|
9
|
+
*
|
|
10
|
+
* The code is the stable, machine-branchable identity of a failure — distinct
|
|
11
|
+
* from the human `message`. `idempotency_conflict` and `insufficient_scope`
|
|
12
|
+
* (previously message-less bare 409/403 bodies) are first-class here.
|
|
13
|
+
*/
|
|
14
|
+
export const AEX_API_ERROR_CODES = [
|
|
15
|
+
"unauthorized",
|
|
16
|
+
"forbidden",
|
|
17
|
+
"insufficient_scope",
|
|
18
|
+
"token_invalid",
|
|
19
|
+
"token_revoked",
|
|
20
|
+
"token_expired",
|
|
21
|
+
"malformed_token",
|
|
22
|
+
"not_found",
|
|
23
|
+
"idempotency_conflict",
|
|
24
|
+
"session_busy",
|
|
25
|
+
"run_not_terminal",
|
|
26
|
+
"unknown_workspace",
|
|
27
|
+
"workspace_concurrency_exceeded",
|
|
28
|
+
"workspace_submit_rate_exceeded",
|
|
29
|
+
"workspace_spend_cap_exceeded",
|
|
30
|
+
"insufficient_balance",
|
|
31
|
+
"rate_limited",
|
|
32
|
+
"upstream_error",
|
|
33
|
+
"internal_error"
|
|
34
|
+
];
|
|
35
|
+
/**
|
|
36
|
+
* Human message per stable code. A `Record` (not `Partial`) so a code added to
|
|
37
|
+
* {@link AEX_API_ERROR_CODES} without a message is a COMPILE error. These are
|
|
38
|
+
* the fallback messages the factory uses when the server sends a BARE code with
|
|
39
|
+
* no `message` detail.
|
|
40
|
+
*/
|
|
41
|
+
export const AEX_API_ERROR_MESSAGES = {
|
|
42
|
+
unauthorized: "The request was not authenticated.",
|
|
43
|
+
forbidden: "The API key is not permitted to perform this action.",
|
|
44
|
+
insufficient_scope: "The API key is missing a scope this endpoint requires.",
|
|
45
|
+
token_invalid: "The API key is not valid.",
|
|
46
|
+
token_revoked: "The API key has been revoked.",
|
|
47
|
+
token_expired: "The API key has expired.",
|
|
48
|
+
malformed_token: "The API key is malformed.",
|
|
49
|
+
not_found: "The requested resource was not found.",
|
|
50
|
+
idempotency_conflict: "This idempotency key was already used with a different request body.",
|
|
51
|
+
session_busy: "The session is busy handling another turn.",
|
|
52
|
+
run_not_terminal: "The run has not reached a terminal state yet.",
|
|
53
|
+
unknown_workspace: "The workspace could not be resolved from the API key.",
|
|
54
|
+
workspace_concurrency_exceeded: "The workspace has reached its concurrent-run limit.",
|
|
55
|
+
workspace_submit_rate_exceeded: "The workspace submit-rate limit was exceeded.",
|
|
56
|
+
workspace_spend_cap_exceeded: "The workspace monthly spend cap was reached.",
|
|
57
|
+
insufficient_balance: "The workspace prepaid balance is insufficient to submit this run.",
|
|
58
|
+
rate_limited: "Too many requests — retry after a short backoff.",
|
|
59
|
+
upstream_error: "An upstream provider returned an error.",
|
|
60
|
+
internal_error: "The aex API encountered an internal error."
|
|
61
|
+
};
|
|
62
|
+
/** Optional per-code fix hint the CLI surfaces alongside the error message. */
|
|
63
|
+
export const AEX_API_ERROR_REMEDIES = {
|
|
64
|
+
insufficient_scope: "Mint an API key that includes the scope this endpoint requires.",
|
|
65
|
+
idempotency_conflict: "Use a fresh idempotency key, or resubmit the byte-identical request body to replay the original result.",
|
|
66
|
+
token_invalid: "Check the API key value and that its plane matches your baseUrl.",
|
|
67
|
+
token_expired: "Mint a new API key.",
|
|
68
|
+
token_revoked: "Mint a new API key.",
|
|
69
|
+
workspace_spend_cap_exceeded: "Raise the workspace spend cap or wait for the next billing cycle.",
|
|
70
|
+
insufficient_balance: "Top up the workspace balance or add a payment method.",
|
|
71
|
+
workspace_concurrency_exceeded: "Wait for in-flight runs to finish or raise the concurrency limit.",
|
|
72
|
+
workspace_submit_rate_exceeded: "Slow the submit rate or raise the workspace submit-rate limit."
|
|
73
|
+
};
|
|
74
|
+
const API_ERROR_CODE_SET = new Set(AEX_API_ERROR_CODES);
|
|
75
|
+
/** Narrow an arbitrary value to a known {@link AexApiErrorCode}. */
|
|
76
|
+
export function isAexApiErrorCode(value) {
|
|
77
|
+
return typeof value === "string" && API_ERROR_CODE_SET.has(value);
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=error-codes.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SINGLE wire→exception mapping. `apiErrorFromResponse` reads a non-2xx
|
|
3
|
+
* body's stable `error` code, attaches it (typed) as `apiCode` plus the
|
|
4
|
+
* `requestId`, and dispatches to the right {@link AexApiError} subclass. Both
|
|
5
|
+
* `HttpClient.request` and `HttpClient.download` construct errors ONLY through
|
|
6
|
+
* here, so the wire→exception mapping lives in one place — no inline
|
|
7
|
+
* `new AexApiError(...)` with a hardcoded coarse code.
|
|
8
|
+
*/
|
|
9
|
+
import { AexApiError } from "./sdk-errors.js";
|
|
10
|
+
import { type AexApiErrorCode } from "./error-codes.js";
|
|
11
|
+
export interface ApiErrorFromResponseInput {
|
|
12
|
+
readonly status: number;
|
|
13
|
+
readonly body: unknown;
|
|
14
|
+
/**
|
|
15
|
+
* Precomputed human message (e.g. `HttpClient`'s `extractErrorMessage`). When
|
|
16
|
+
* absent the factory derives one from the stable code table.
|
|
17
|
+
*/
|
|
18
|
+
readonly message?: string;
|
|
19
|
+
readonly requestId?: string;
|
|
20
|
+
readonly cause?: unknown;
|
|
21
|
+
}
|
|
22
|
+
type ApiErrorKind = "auth" | "idempotency" | "not_found" | "rate_limit" | "generic";
|
|
23
|
+
/**
|
|
24
|
+
* EXHAUSTIVE stable-code → subclass-kind map. There is NO `default`: adding a
|
|
25
|
+
* code to {@link AEX_API_ERROR_CODES} without a case here fails to compile
|
|
26
|
+
* (the function would fall through and return `undefined`, which the
|
|
27
|
+
* `ApiErrorKind` return type forbids). This is the whole-class guard that a new
|
|
28
|
+
* server code can never silently collapse to the base error.
|
|
29
|
+
*/
|
|
30
|
+
export declare function apiErrorKindForCode(code: AexApiErrorCode): ApiErrorKind;
|
|
31
|
+
export declare function apiErrorFromResponse(input: ApiErrorFromResponseInput): AexApiError;
|
|
32
|
+
export {};
|