@cofy-x/axern-sdk 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.
Files changed (68) hide show
  1. package/README.md +28 -76
  2. package/dist/client/index.d.ts +49 -11
  3. package/dist/client/index.js +207 -58
  4. package/dist/config/index.d.ts +0 -1
  5. package/dist/config/index.js +2 -4
  6. package/dist/generated/proto.js +0 -1
  7. package/dist/index.d.ts +4 -5
  8. package/dist/index.js +1 -2
  9. package/dist/node/archive.js +1 -1
  10. package/dist/node/attached_process.d.ts +1 -2
  11. package/dist/node/attached_process.js +14 -28
  12. package/dist/node/capabilities.js +1 -1
  13. package/dist/node/client.d.ts +4 -6
  14. package/dist/node/client.js +2 -9
  15. package/dist/node/computer_use.js +1 -1
  16. package/dist/node/context.d.ts +1 -1
  17. package/dist/node/context.js +1 -1
  18. package/dist/node/exec.d.ts +1 -3
  19. package/dist/node/exec.js +1 -44
  20. package/dist/node/files.js +1 -1
  21. package/dist/node/process.d.ts +11 -6
  22. package/dist/node/process.js +45 -6
  23. package/dist/proto/axern/control/admin/v1/access.proto +12 -5
  24. package/dist/proto/axern/control/admin/v1/audit.proto +14 -16
  25. package/dist/proto/axern/control/admin/v1/node.proto +23 -67
  26. package/dist/proto/axern/control/admin/v1/reliability.proto +11 -78
  27. package/dist/proto/axern/control/capability/v1/capability.proto +34 -94
  28. package/dist/proto/axern/control/common/v1/common.proto +41 -98
  29. package/dist/proto/axern/control/environment/v1/environment.proto +42 -26
  30. package/dist/proto/axern/control/identity/v1/identity.proto +1 -1
  31. package/dist/proto/axern/control/namespace/v1/namespace.proto +2 -3
  32. package/dist/proto/axern/control/quota/v1/quota.proto +22 -32
  33. package/dist/proto/axern/control/run/v1/run.proto +25 -22
  34. package/dist/proto/axern/control/secret/v1/secret.proto +1 -3
  35. package/dist/proto/axern/control/tunnel/v1/tunnel.proto +20 -29
  36. package/dist/proto/axern/node/sandbox/v1/node.proto +106 -375
  37. package/dist/sandbox/index.d.ts +5 -11
  38. package/dist/sandbox/index.js +24 -25
  39. package/dist/sandbox/lifecycle.d.ts +1 -1
  40. package/dist/sandbox/lifecycle.js +55 -17
  41. package/dist/tunnel/connector.js +1 -1
  42. package/dist/tunnel/control.d.ts +0 -1
  43. package/dist/tunnel/control.js +0 -1
  44. package/dist/tunnel/runtime.d.ts +0 -1
  45. package/dist/tunnel/runtime.js +0 -1
  46. package/dist/tunnel/types.d.ts +0 -1
  47. package/dist/types.d.ts +4 -24
  48. package/dist/types.js +1 -3
  49. package/dist/version.d.ts +1 -1
  50. package/dist/version.js +1 -1
  51. package/package.json +1 -1
  52. package/dist/proto/axern/control/admin/v1/allocation_lifecycle.proto +0 -91
  53. package/dist/proto/axern/control/admin/v1/service.proto +0 -18
  54. package/dist/proto/axern/control/admin/v1/storage.proto +0 -81
  55. package/dist/proto/axern/control/agentprofile/v1/agent_profile.proto +0 -169
  56. package/dist/proto/axern/control/catalog/v1/catalog.proto +0 -127
  57. package/dist/proto/axern/control/function/v1/function.proto +0 -130
  58. package/dist/proto/axern/control/function/v1/function_types.proto +0 -233
  59. package/dist/proto/axern/control/gateway/v1/gateway.proto +0 -92
  60. package/dist/proto/axern/control/node/v1/node_control.proto +0 -443
  61. package/dist/proto/axern/control/rollout/v1/rollout.proto +0 -344
  62. package/dist/proto/axern/control/service/v1/service.proto +0 -96
  63. package/dist/proto/axern/control/service/v1/service_event.proto +0 -47
  64. package/dist/proto/axern/control/service/v1/service_replica.proto +0 -78
  65. package/dist/proto/axern/control/service/v1/service_types.proto +0 -155
  66. package/dist/proto/axern/control/storage/v1/storage.proto +0 -107
  67. package/dist/proto/axern/control/storage/v1/storage_types.proto +0 -125
  68. package/dist/proto/axern/data/artifact/v1/artifact.proto +0 -19
package/README.md CHANGED
@@ -28,18 +28,14 @@ pnpm install
28
28
  make sdk-typescript-verify
29
29
  ```
30
30
 
31
- The SDK dynamically loads Axern proto definitions from `sdk/proto`. Set
32
- `AXERN_PROTO_ROOT` when running from a different layout.
31
+ The SDK dynamically loads Axern proto definitions from `sdk/proto`. Set `AXERN_PROTO_ROOT` when running from a different layout.
33
32
 
34
33
  ## Basic Usage
35
34
 
36
35
  ```ts
37
36
  import { AxernClient, Sandbox } from "@cofy-x/axern-sdk";
38
37
 
39
- const client = AxernClient.fromContext(
40
- process.env.AXERN_CONFIG ?? `${process.env.HOME}/.config/axern/config.json`,
41
- process.env.AXERN_CONTEXT,
42
- );
38
+ const client = AxernClient.fromContext(process.env.AXERN_CONFIG ?? `${process.env.HOME}/.config/axern/config.json`, process.env.AXERN_CONTEXT);
43
39
 
44
40
  const sandbox = await new Sandbox({
45
41
  client,
@@ -70,9 +66,7 @@ try {
70
66
 
71
67
  ## Network Policies
72
68
 
73
- Omitting `networkPolicy` keeps unrestricted v0.5 behavior. Strict policies are
74
- fail-closed; `denyDns` only refuses matching traditional UDP/TCP DNS queries and
75
- does not block direct IP traffic, DoH, DoT, or already-resolved addresses.
69
+ Omitting `networkPolicy` requests unrestricted networking. Strict policies are fail-closed; `denyDns` only refuses matching traditional UDP/TCP DNS queries and does not block direct IP traffic, DoH, DoT, or already-resolved addresses.
76
70
 
77
71
  ```ts
78
72
  import { NetworkPolicy, Sandbox } from "@cofy-x/axern-sdk";
@@ -80,89 +74,47 @@ import { NetworkPolicy, Sandbox } from "@cofy-x/axern-sdk";
80
74
  const sandbox = await new Sandbox({
81
75
  client,
82
76
  image: "docker.io/library/python:3.12-slim",
83
- networkPolicy: NetworkPolicy.denyDns(
84
- "github.com",
85
- "*.github.com",
86
- "githubusercontent.com",
87
- "*.githubusercontent.com",
88
- ),
77
+ networkPolicy: NetworkPolicy.denyDns("github.com", "*.github.com", "githubusercontent.com", "*.githubusercontent.com"),
89
78
  }).start();
90
79
  ```
91
80
 
92
- `NetworkPolicy.allowDomains("example.com", "*.example.com")` allows only
93
- strict HTTP/HTTPS destinations validated by DNS plus HTTP Host or TLS SNI.
94
- `NetworkPolicy.strict({ cidrRules: [...] })` adds explicit TCP/UDP CIDR and port
95
- grants; `NetworkPolicy.denyAll()` allows no egress.
81
+ `NetworkPolicy.allowDomains("example.com", "*.example.com")` allows only strict HTTP/HTTPS destinations validated by DNS plus HTTP Host or TLS SNI. `NetworkPolicy.strict({ cidrRules: [...] })` adds explicit TCP/UDP CIDR and port grants; `NetworkPolicy.denyAll()` allows no egress.
96
82
 
97
- Run a tool from a separate image with `execImage` or `processImage`. OCI and
98
- Nydus refs use the same image field. When `mounts` is omitted, the SDK requests
99
- `/workspace -> /workspace`; pass `mounts: []` for no shared paths. Use
100
- `new Sandbox({ image })` when the image should be the sandbox rootfs with normal
101
- files, exec, process, tunnel, and lifecycle APIs; image-backed processes are
102
- temporary side processes attached to an existing sandbox.
103
-
104
- ```ts
105
- import { workspaceMount } from "@cofy-x/axern-sdk";
106
-
107
- const result = await sandbox.execImage("ghcr.io/cofy-x/agent:latest", "tool run", {
108
- check: true,
109
- mounts: [workspaceMount("/workspace")],
110
- });
111
- console.log(result.stdoutText());
112
- ```
113
-
114
- `AxernClient` requires an explicit endpoint. Use `AxernClient.fromContext()` in
115
- interactive examples or `AxernClient.fromEnv()` in environment-driven
116
- automation. Neither the client constructor nor `Sandbox` silently reads the
117
- user directory.
83
+ `AxernClient` requires an explicit endpoint. Use `AxernClient.fromContext()` in interactive examples or `AxernClient.fromEnv()` in environment-driven automation. Neither the client constructor nor `Sandbox` silently reads the user directory.
118
84
 
119
85
  ## Configuration
120
86
 
121
- `Sandbox` requires exactly one source: `templateId`, `image`, or
122
- `environmentId`.
87
+ `Sandbox` requires exactly one source: `templateId`, `image`, or `environmentId`.
123
88
 
124
89
  Common options:
125
90
 
126
91
  - `namespace`: control-plane namespace, default `default`
127
92
  - `client`: explicit `AxernClient` shared by the sandbox
128
93
  - `argv`, `env`, `cwd`: initial sandbox process configuration
129
- - `runtimeClass`: runtime selector, for example `runsc` or `runc`
130
- - `requestCpu`, `requestMemory`, `requestEphemeralStorage`: scheduler resource
131
- requests such as `500m`, `512MiB`, and `1GiB`; numeric CPU values are cores
132
- and numeric memory/storage values are bytes
133
- - `limitCpu`, `limitMemory`, `limitEphemeralStorage`: runtime hard limits;
134
- numeric CPU values are cores and numeric memory/storage values are bytes
135
- - `readyTimeoutMs`: service replica readiness timeout
94
+ - `requestCpu`, `requestMemory`, `requestEphemeralStorage`: scheduler resource requests such as `500m`, `512MiB`, and `1GiB`; numeric CPU values are cores and numeric memory/storage values are bytes
95
+ - `limitCpu`, `limitMemory`, `limitEphemeralStorage`: runtime hard limits; numeric CPU values are cores and numeric memory/storage values are bytes
96
+ - `readyTimeoutMs`: Run allocation startup timeout
97
+ - `declaredOutputs`: bounded files or directory-as-tar outputs sealed before runtime cleanup
136
98
 
137
99
  Tunnel options:
138
100
 
139
- - `tunnel.upstream`: local TCP address reached by the SDK connector, for
140
- example `127.0.0.1:8080`
101
+ - `tunnel.upstream`: local TCP address reached by the SDK connector, for example `127.0.0.1:8080`
141
102
  - `tunnel.proxyPort`: sandbox-local port bound by Axern, default `8786`
142
103
  - `tunnel.ttlSeconds`: control-plane tunnel session TTL, renewed by the SDK
143
104
 
144
- Tunnel traffic reuses the client gateway endpoint, mTLS identity, server name,
145
- and proxy policy.
105
+ Tunnel traffic reuses the client gateway endpoint, mTLS identity, server name, and proxy policy.
146
106
 
147
- After `start()`, use `sandbox.metadata.tunnel?.boundAddr` from inside the
148
- sandbox, for example `http://127.0.0.1:8786`.
107
+ After `start()`, use `sandbox.metadata.tunnel?.boundAddr` from inside the sandbox, for example `http://127.0.0.1:8786`.
149
108
 
150
109
  ## Sandbox API
151
110
 
152
111
  - Lifecycle: `start()`, `close()`, `state`, `metadata`
153
112
  - Execution: `exec(command, options)`, `process(command, options)`
154
- - Agent sandbox: `capabilityStatus`, `computerUseStatus`,
155
- `computerUseScreenshot`, `computerUseDisplay`, `computerUseMouse`,
156
- `computerUseKeyboard`
157
- - Files: `readFile`, `readText`, `writeFile`, `writeText`, `stat`, `listDir`,
158
- `exists`, `mkdir`, `remove`, `copy`, `move`, `chmod`, `touch`
159
- - Directories: `uploadDir(localPath, remotePath)`,
160
- `downloadDir(remotePath, localPath)`
161
- - Tunnel: `new Sandbox({ tunnel: { upstream, proxyPort } })`,
162
- `metadata.tunnel`
163
- - Errors: `AxernRpcError`, `SandboxExecError`, `SandboxStateError`,
164
- `SandboxValidationError`, `isNotFound`, `isPermissionDenied`, `isTimeout`,
165
- `rpcCode`
113
+ - Agent sandbox: `capabilityStatus`, `computerUseStatus`, `computerUseScreenshot`, `computerUseDisplay`, `computerUseMouse`, `computerUseKeyboard`
114
+ - Files: `readFile`, `readText`, `writeFile`, `writeText`, `stat`, `listDir`, `exists`, `mkdir`, `remove`, `copy`, `move`, `chmod`, `touch`
115
+ - Directories: `uploadDir(localPath, remotePath)`, `downloadDir(remotePath, localPath)`
116
+ - Tunnel: `new Sandbox({ tunnel: { upstream, proxyPort } })`, `metadata.tunnel`
117
+ - Errors: `AxernRpcError`, `SandboxExecError`, `SandboxStateError`, `SandboxValidationError`, `isNotFound`, `isPermissionDenied`, `isTimeout`, `rpcCode`
166
118
 
167
119
  ## Local Smoke
168
120
 
@@ -173,9 +125,7 @@ pnpm --filter @cofy-x/axern-sdk run smoke:local
173
125
  pnpm --filter @cofy-x/axern-sdk run smoke:tunnel
174
126
  ```
175
127
 
176
- The smoke loads `deploy/local/state/compose/axern.env` when present. It uses the
177
- `python311` template by default; set `AXERN_TS_SMOKE_IMAGE` to verify an image
178
- source instead.
128
+ The smoke loads `deploy/local/state/compose/axern.env` when present. It uses the `python311` template by default; set `AXERN_TS_SMOKE_IMAGE` to verify an image source instead.
179
129
 
180
130
  Check package contents with:
181
131
 
@@ -185,12 +135,14 @@ pnpm --filter @cofy-x/axern-sdk run pack:dry-run
185
135
 
186
136
  ## Proto Boundary
187
137
 
188
- The SDK loads protobuf definitions through `@grpc/proto-loader`. Dynamic proto
189
- access remains isolated under `src/generated`; public callers depend only on
190
- the stable TypeScript DTOs and error types.
138
+ The SDK loads protobuf definitions through `@grpc/proto-loader`. Dynamic proto access remains isolated under `src/generated`; public callers depend only on the stable TypeScript DTOs and error types.
191
139
 
192
140
  ## Scope
193
141
 
194
- This SDK is Node.js-first. The higher-level Browser API, generated TypeScript
195
- proto stubs, and full control-plane administration APIs remain outside the
196
- v0.2 public contract.
142
+ This SDK is Node.js-first. Browser automation runs as caller-owned workload software through process and Computer Use operations; generated TypeScript proto stubs and full control-plane administration APIs remain outside its public contract.
143
+
144
+ ## Declared And Stream Output
145
+
146
+ Pass `declaredOutputs` when creating a Run or Sandbox, then use `getSealedOutputManifest(runId)` and `downloadSealedOutput(runId, outputId, writable)` after the Run becomes terminal. Full downloads verify size and SHA-256 while respecting the destination stream's backpressure. Declared output is Node-local for 15 minutes after cleanup starts: it survives axnoded restart but not Node-disk loss. Limits are 16 paths, 64 MiB per file, 256 MiB per tar, and 256 MiB total. It is not a persistent workspace or object store.
147
+
148
+ Attached Process queues at most 64 unread events, pauses the gRPC stream at the bound, and resumes below the low-water mark. `write`, `closeStdin`, signal, and resize promises resolve only after the gRPC write callback. Closing an attached process attempts `TERM`; durable workload termination remains Run cancellation. `Sandbox.close()` reports cleanup failures and does not wait for a terminal Run, so call `waitRun()` when terminal confirmation is required.
@@ -4,11 +4,11 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import * as grpc from "@grpc/grpc-js";
7
- import { NodeSandboxClient } from "../node/client.js";
7
+ import type { Writable } from "node:stream";
8
+ import { AllocationClient } from "../node/client.js";
8
9
  import type { ResourceQuantity } from "../resources.js";
9
10
  import type { NetworkPolicy } from "../network-policy.js";
10
11
  import { TunnelControlClient } from "../tunnel/control.js";
11
- import type { VolumeMount } from "../types.js";
12
12
  import type { GatewayTransportOptions } from "../tunnel/relay.js";
13
13
  export interface AxernClientOptions {
14
14
  endpoint: string;
@@ -27,24 +27,51 @@ export interface CreateEnvironmentOptions {
27
27
  rootfsReadonly?: boolean;
28
28
  labels?: Record<string, string>;
29
29
  }
30
- export interface CreateServiceOptions {
30
+ export interface CreateRunOptions {
31
31
  namespace?: string;
32
32
  environmentId: string;
33
33
  argv?: string[];
34
34
  env?: Record<string, string>;
35
35
  cwd?: string;
36
- runtimeClass?: string;
37
36
  networkPolicy?: NetworkPolicy;
38
37
  extensionCapabilities?: readonly ExtensionCapability[];
39
- volumes?: readonly VolumeMount[];
40
38
  requestCpu?: ResourceQuantity;
41
39
  requestMemory?: ResourceQuantity;
42
40
  requestEphemeralStorage?: ResourceQuantity;
43
41
  limitCpu?: ResourceQuantity;
44
42
  limitMemory?: ResourceQuantity;
45
43
  limitEphemeralStorage?: ResourceQuantity;
44
+ declaredOutputs?: readonly DeclaredOutput[];
46
45
  labels?: Record<string, string>;
47
46
  }
47
+ export type DeclaredOutputFormat = "file" | "tar";
48
+ export interface DeclaredOutput {
49
+ path: string;
50
+ format: DeclaredOutputFormat;
51
+ mediaType?: string;
52
+ }
53
+ export interface SealedOutput {
54
+ outputId: string;
55
+ path: string;
56
+ sizeBytes: number;
57
+ sha256: string;
58
+ mediaType: string;
59
+ format?: DeclaredOutputFormat;
60
+ status: "available" | "missing" | "rejected" | "capture_failed" | "node_unavailable" | "unspecified";
61
+ reason: string;
62
+ sealedAt?: Record<string, unknown>;
63
+ expiresAt?: Record<string, unknown>;
64
+ }
65
+ export interface ListOptions {
66
+ namespace?: string;
67
+ labels?: Record<string, string>;
68
+ cursor?: string;
69
+ pageSize?: number;
70
+ }
71
+ export interface ListResult<T> {
72
+ items: T[];
73
+ nextCursor: string;
74
+ }
48
75
  export interface ExtensionCapability {
49
76
  name: string;
50
77
  value?: string;
@@ -53,13 +80,16 @@ export interface ReadRunOutputOptions {
53
80
  cursor?: string;
54
81
  follow?: boolean;
55
82
  }
83
+ export interface WatchRunOptions {
84
+ afterVersion?: number;
85
+ signal?: AbortSignal;
86
+ }
56
87
  export declare class AxernClient {
57
88
  readonly endpoint: string;
58
89
  private readonly credentials;
59
90
  private readonly controlOptions;
60
91
  private readonly environmentControl;
61
92
  private readonly runControl;
62
- private readonly serviceControl;
63
93
  private readonly tunnelControl;
64
94
  private readonly gatewayTransport;
65
95
  constructor(options: AxernClientOptions);
@@ -68,12 +98,20 @@ export declare class AxernClient {
68
98
  close(): void;
69
99
  createEnvironment(options: CreateEnvironmentOptions): Promise<Record<string, unknown>>;
70
100
  deleteEnvironment(environmentId: string): Promise<void>;
71
- watchRun(runId: string, afterVersion?: number): AsyncGenerator<Record<string, unknown>>;
101
+ getEnvironment(environmentId: string): Promise<Record<string, unknown>>;
102
+ listEnvironments(options?: ListOptions): Promise<ListResult<Record<string, unknown>>>;
103
+ createRun(options: CreateRunOptions): Promise<Record<string, unknown>>;
104
+ getRun(runId: string): Promise<Record<string, unknown>>;
105
+ listRuns(options?: ListOptions & {
106
+ statuses?: number[];
107
+ }): Promise<ListResult<Record<string, unknown>>>;
108
+ waitRun(runId: string, timeoutMs?: number): Promise<Record<string, unknown>>;
109
+ cancelRun(runId: string): Promise<void>;
110
+ watchRun(runId: string, options?: WatchRunOptions): AsyncGenerator<Record<string, unknown>>;
72
111
  readRunOutput(runId: string, options?: ReadRunOutputOptions): AsyncGenerator<Record<string, unknown>>;
73
- createService(options: CreateServiceOptions): Promise<Record<string, unknown>>;
74
- deleteService(serviceId: string): Promise<void>;
75
- listServiceReplicas(serviceId: string): Promise<Record<string, unknown>[]>;
76
- nodeSandbox(allocationId: string): NodeSandboxClient;
112
+ getSealedOutputManifest(runId: string): Promise<SealedOutput[]>;
113
+ downloadSealedOutput(runId: string, outputId: string, destination: Writable): Promise<SealedOutput>;
114
+ allocation(allocationId: string): AllocationClient;
77
115
  tunnelClient(): TunnelControlClient;
78
116
  tunnelTransport(): GatewayTransportOptions;
79
117
  }
@@ -4,11 +4,12 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import * as grpc from "@grpc/grpc-js";
7
+ import { createHash } from "node:crypto";
7
8
  import { readFileSync } from "node:fs";
8
9
  import { loadAxernContext, loadAxernEnv, normalizeProxyMode } from "../config/index.js";
9
- import { mapRpcError } from "../errors/index.js";
10
+ import { SandboxStateError, SandboxTimeoutError, mapRpcError } from "../errors/index.js";
10
11
  import { serviceConstructor, unary } from "../generated/proto.js";
11
- import { NodeSandboxClient } from "../node/client.js";
12
+ import { AllocationClient } from "../node/client.js";
12
13
  import { buildResourceSpec } from "../resources.js";
13
14
  import { TunnelControlClient } from "../tunnel/control.js";
14
15
  import { required } from "../validation.js";
@@ -18,7 +19,6 @@ export class AxernClient {
18
19
  controlOptions;
19
20
  environmentControl;
20
21
  runControl;
21
- serviceControl;
22
22
  tunnelControl;
23
23
  gatewayTransport;
24
24
  constructor(options) {
@@ -57,11 +57,9 @@ export class AxernClient {
57
57
  "EnvironmentControl",
58
58
  ]);
59
59
  const RunControl = serviceConstructor(["axern", "control", "run", "v1", "RunControl"]);
60
- const ServiceControl = serviceConstructor(["axern", "control", "service", "v1", "ServiceControl"]);
61
60
  const TunnelControl = serviceConstructor(["axern", "control", "tunnel", "v1", "TunnelControl"]);
62
61
  this.environmentControl = new EnvironmentControl(this.endpoint, this.credentials, this.controlOptions);
63
62
  this.runControl = new RunControl(this.endpoint, this.credentials, this.controlOptions);
64
- this.serviceControl = new ServiceControl(this.endpoint, this.credentials, this.controlOptions);
65
63
  this.tunnelControl = new TunnelControl(this.endpoint, this.credentials, this.controlOptions);
66
64
  }
67
65
  static fromEnv(overrides = {}) {
@@ -82,7 +80,6 @@ export class AxernClient {
82
80
  close() {
83
81
  this.environmentControl.close();
84
82
  this.runControl.close();
85
- this.serviceControl.close();
86
83
  this.tunnelControl.close();
87
84
  }
88
85
  async createEnvironment(options) {
@@ -118,17 +115,118 @@ export class AxernClient {
118
115
  throw mapRpcError(error, "delete environment");
119
116
  }
120
117
  }
121
- async *watchRun(runId, afterVersion = 0) {
118
+ async getEnvironment(environmentId) {
119
+ try {
120
+ const response = await unary(this.environmentControl, "GetEnvironment", { environment_id: required("environmentId", environmentId) });
121
+ return response.environment;
122
+ }
123
+ catch (error) {
124
+ throw mapRpcError(error, "get environment");
125
+ }
126
+ }
127
+ async listEnvironments(options = {}) {
128
+ try {
129
+ const response = await unary(this.environmentControl, "ListEnvironments", { filter: { namespace: options.namespace ?? "", labels: options.labels ?? {}, cursor: options.cursor ?? "", page_size: options.pageSize ?? 0 } });
130
+ return { items: response.environments ?? [], nextCursor: response.next_cursor ?? "" };
131
+ }
132
+ catch (error) {
133
+ throw mapRpcError(error, "list environments");
134
+ }
135
+ }
136
+ async createRun(options) {
137
+ const resources = buildResourceSpec(options);
138
+ try {
139
+ const response = await unary(this.runControl, "CreateRun", {
140
+ namespace: options.namespace ?? "default",
141
+ environment_id: required("environmentId", options.environmentId),
142
+ config: {
143
+ argv: options.argv ?? [],
144
+ env: options.env ?? {},
145
+ cwd: options.cwd ?? "",
146
+ ...(options.networkPolicy === undefined
147
+ ? {}
148
+ : { network: { egress_policy: options.networkPolicy.toWire() } }),
149
+ extension_capability_requirements: (options.extensionCapabilities ?? []).map((capability) => ({
150
+ capability: { name: capability.name, value: capability.value ?? "" },
151
+ })),
152
+ declared_outputs: (options.declaredOutputs ?? []).map((output) => ({
153
+ path: output.path,
154
+ format: output.format === "file" ? 1 : 2,
155
+ media_type: output.mediaType ?? "",
156
+ })),
157
+ resources,
158
+ },
159
+ labels: options.labels ?? {},
160
+ });
161
+ return response.run;
162
+ }
163
+ catch (error) {
164
+ throw mapRpcError(error, "create run");
165
+ }
166
+ }
167
+ async getRun(runId) {
168
+ try {
169
+ const response = await unary(this.runControl, "GetRun", { run_id: required("runId", runId) });
170
+ return response.run;
171
+ }
172
+ catch (error) {
173
+ throw mapRpcError(error, "get run");
174
+ }
175
+ }
176
+ async listRuns(options = {}) {
177
+ try {
178
+ const response = await unary(this.runControl, "ListRuns", { filter: { namespace: options.namespace ?? "", labels: options.labels ?? {}, statuses: options.statuses ?? [], cursor: options.cursor ?? "", page_size: options.pageSize ?? 0 } });
179
+ return { items: response.runs ?? [], nextCursor: response.next_cursor ?? "" };
180
+ }
181
+ catch (error) {
182
+ throw mapRpcError(error, "list runs");
183
+ }
184
+ }
185
+ async waitRun(runId, timeoutMs) {
186
+ const initial = await this.getRun(runId);
187
+ if (terminalRun(initial))
188
+ return initial;
189
+ const controller = new AbortController();
190
+ const timer = timeoutMs === undefined ? undefined : setTimeout(() => controller.abort(), timeoutMs);
191
+ try {
192
+ for await (const run of this.watchRun(runId, { afterVersion: Number(initial.version ?? 0), signal: controller.signal })) {
193
+ if (terminalRun(run))
194
+ return run;
195
+ }
196
+ }
197
+ finally {
198
+ if (timer !== undefined)
199
+ clearTimeout(timer);
200
+ }
201
+ if (controller.signal.aborted)
202
+ throw new SandboxTimeoutError(`run ${runId} wait timed out`);
203
+ throw new SandboxStateError(`run ${runId} watch ended before a terminal state`);
204
+ }
205
+ async cancelRun(runId) {
206
+ try {
207
+ await unary(this.runControl, "CancelRun", { run_id: required("runId", runId) });
208
+ }
209
+ catch (error) {
210
+ throw mapRpcError(error, "cancel run");
211
+ }
212
+ }
213
+ async *watchRun(runId, options = {}) {
214
+ const afterVersion = options.afterVersion ?? 0;
215
+ const signal = options.signal;
122
216
  if (afterVersion < 0) {
123
217
  throw new Error("afterVersion must be non-negative");
124
218
  }
125
219
  let version = afterVersion;
126
220
  let retryDelayMs = 100;
127
221
  for (;;) {
222
+ if (signal?.aborted)
223
+ return;
128
224
  const stream = serverStream(this.runControl, "WatchRun", {
129
225
  run_id: required("runId", runId),
130
226
  after_version: version,
131
227
  });
228
+ const cancel = () => stream.cancel();
229
+ signal?.addEventListener("abort", cancel, { once: true });
132
230
  try {
133
231
  for await (const response of stream) {
134
232
  const run = response.run;
@@ -144,14 +242,21 @@ export class AxernClient {
144
242
  return;
145
243
  }
146
244
  catch (error) {
245
+ if (signal?.aborted)
246
+ return;
147
247
  if (!transientReadError(error))
148
248
  throw mapRpcError(error, "watch run");
149
249
  }
150
- await sleep(retryDelayMs);
250
+ finally {
251
+ signal?.removeEventListener("abort", cancel);
252
+ }
253
+ await sleep(retryDelayMs, signal);
151
254
  retryDelayMs = Math.min(retryDelayMs * 2, 2_000);
152
255
  }
153
256
  }
154
257
  async *readRunOutput(runId, options = {}) {
258
+ // Output is Allocation-local and may be unavailable after cleanup; callers
259
+ // that need durable bytes must consume and persist them before then.
155
260
  const response = await unary(this.runControl, "GetRun", { run_id: required("runId", runId) });
156
261
  const allocationId = String(response.run?.allocation_id ?? "");
157
262
  if (allocationId === "")
@@ -195,57 +300,65 @@ export class AxernClient {
195
300
  retryDelayMs = Math.min(retryDelayMs * 2, 2_000);
196
301
  }
197
302
  }
198
- async createService(options) {
199
- const resources = buildResourceSpec(options);
303
+ async getSealedOutputManifest(runId) {
304
+ const run = await this.getRun(runId);
305
+ const allocationId = String(run.allocation_id ?? "");
306
+ if (allocationId === "")
307
+ throw new Error(`run ${runId} has no allocation`);
308
+ const NodeSandbox = serviceConstructor(["axern", "node", "sandbox", "v1", "NodeSandbox"]);
309
+ const node = new NodeSandbox(this.endpoint, this.credentials, this.controlOptions);
200
310
  try {
201
- const response = await unary(this.serviceControl, "CreateService", {
202
- namespace: options.namespace ?? "default",
203
- environment_id: required("environmentId", options.environmentId),
204
- replicas: 1,
205
- config: {
206
- argv: options.argv ?? [],
207
- env: options.env ?? {},
208
- cwd: options.cwd ?? "",
209
- runtime_class: options.runtimeClass ?? "",
210
- ...(options.networkPolicy === undefined
211
- ? {}
212
- : { network: { egress_policy: options.networkPolicy.toWire() } }),
213
- extension_capability_requirements: (options.extensionCapabilities ?? []).map((capability) => ({
214
- capability: { name: capability.name, value: capability.value ?? "" },
215
- })),
216
- volume_mounts: serviceVolumeMounts(options.volumes),
217
- resources,
218
- },
219
- labels: options.labels ?? {},
220
- });
221
- return response.service;
311
+ const response = await unary(node, "GetSealedOutputManifest", { allocation_id: allocationId });
312
+ return (response.outputs ?? []).map(sealedOutput);
222
313
  }
223
314
  catch (error) {
224
- throw mapRpcError(error, "create service");
315
+ throw mapRpcError(error, "get sealed output manifest", allocationId);
225
316
  }
226
- }
227
- async deleteService(serviceId) {
228
- try {
229
- await unary(this.serviceControl, "DeleteService", { service_id: required("serviceId", serviceId) });
230
- }
231
- catch (error) {
232
- throw mapRpcError(error, "delete service");
317
+ finally {
318
+ node.close();
233
319
  }
234
320
  }
235
- async listServiceReplicas(serviceId) {
321
+ async downloadSealedOutput(runId, outputId, destination) {
322
+ const outputs = await this.getSealedOutputManifest(runId);
323
+ const selected = outputs.find((output) => output.outputId === outputId);
324
+ if (selected === undefined || selected.status !== "available") {
325
+ throw new Error(`sealed output ${outputId} is not available`);
326
+ }
327
+ const run = await this.getRun(runId);
328
+ const allocationId = String(run.allocation_id ?? "");
329
+ const NodeSandbox = serviceConstructor(["axern", "node", "sandbox", "v1", "NodeSandbox"]);
330
+ const node = new NodeSandbox(this.endpoint, this.credentials, this.controlOptions);
331
+ const stream = serverStream(node, "DownloadSealedOutput", { allocation_id: allocationId, output_id: outputId, offset: "0" });
332
+ const digest = createHash("sha256");
333
+ let offset = 0;
236
334
  try {
237
- const response = await unary(this.serviceControl, "ListServiceReplicas", {
238
- service_id: required("serviceId", serviceId),
239
- filter: { view: 2 },
240
- });
241
- return response.replicas ?? [];
335
+ for await (const response of stream) {
336
+ const data = Buffer.from(response.data ?? []);
337
+ const nextOffset = Number(response.next_offset ?? offset + data.length);
338
+ if (nextOffset !== offset + data.length)
339
+ throw new Error("sealed output returned a non-contiguous offset");
340
+ digest.update(data);
341
+ await writeChunk(destination, data);
342
+ offset = nextOffset;
343
+ }
242
344
  }
243
345
  catch (error) {
244
- throw mapRpcError(error, "list service replicas");
346
+ if (typeof error === "object" && error !== null && "code" in error) {
347
+ throw mapRpcError(error, "download sealed output", allocationId);
348
+ }
349
+ throw error;
350
+ }
351
+ finally {
352
+ node.close();
245
353
  }
354
+ if (offset !== selected.sizeBytes)
355
+ throw new Error("sealed output size does not match its manifest");
356
+ if (digest.digest("hex") !== selected.sha256)
357
+ throw new Error("sealed output digest does not match its manifest");
358
+ return selected;
246
359
  }
247
- nodeSandbox(allocationId) {
248
- return new NodeSandboxClient({
360
+ allocation(allocationId) {
361
+ return new AllocationClient({
249
362
  allocationId: required("allocationId", allocationId),
250
363
  target: this.endpoint,
251
364
  credentials: this.credentials,
@@ -259,14 +372,6 @@ export class AxernClient {
259
372
  return { ...this.gatewayTransport };
260
373
  }
261
374
  }
262
- function serviceVolumeMounts(mounts) {
263
- return (mounts ?? []).map((mount) => ({
264
- name: mount.name,
265
- target: mount.target,
266
- readonly: mount.readonly ?? false,
267
- options: [...(mount.options ?? [])],
268
- }));
269
- }
270
375
  function serverStream(client, method, request) {
271
376
  const fn = client[method];
272
377
  return fn.call(client, request);
@@ -275,8 +380,52 @@ function transientReadError(error) {
275
380
  const code = error.code;
276
381
  return code === grpc.status.UNAVAILABLE || code === grpc.status.DEADLINE_EXCEEDED;
277
382
  }
278
- function sleep(milliseconds) {
279
- return new Promise((resolve) => setTimeout(resolve, milliseconds));
383
+ function terminalRun(run) {
384
+ const status = Number(run.status ?? 0);
385
+ return status === 4 || status === 5 || status === 6;
386
+ }
387
+ function sealedOutput(value) {
388
+ const formats = { 1: "file", 2: "tar" };
389
+ const statuses = ["unspecified", "available", "missing", "rejected", "capture_failed", "node_unavailable"];
390
+ return {
391
+ outputId: String(value.output_id ?? ""),
392
+ path: String(value.path ?? ""),
393
+ sizeBytes: Number(value.size_bytes ?? 0),
394
+ sha256: String(value.sha256 ?? ""),
395
+ mediaType: String(value.media_type ?? ""),
396
+ format: formats[Number(value.format ?? 0)],
397
+ status: statuses[Number(value.status ?? 0)] ?? "unspecified",
398
+ reason: String(value.reason ?? ""),
399
+ sealedAt: value.sealed_at,
400
+ expiresAt: value.expires_at,
401
+ };
402
+ }
403
+ function writeChunk(destination, chunk) {
404
+ if (destination.write(chunk))
405
+ return Promise.resolve();
406
+ return new Promise((resolve, reject) => {
407
+ const cleanup = () => {
408
+ destination.off("drain", drained);
409
+ destination.off("error", failed);
410
+ };
411
+ const drained = () => { cleanup(); resolve(); };
412
+ const failed = (error) => { cleanup(); reject(error); };
413
+ destination.once("drain", drained);
414
+ destination.once("error", failed);
415
+ });
416
+ }
417
+ function sleep(milliseconds, signal) {
418
+ if (signal?.aborted)
419
+ return Promise.resolve();
420
+ return new Promise((resolve) => {
421
+ const complete = () => {
422
+ clearTimeout(timer);
423
+ signal?.removeEventListener("abort", complete);
424
+ resolve();
425
+ };
426
+ const timer = setTimeout(complete, milliseconds);
427
+ signal?.addEventListener("abort", complete, { once: true });
428
+ });
280
429
  }
281
430
  function controlCredentials(options) {
282
431
  const configured = [options.tlsCaCert, options.tlsCert, options.tlsKey].filter((value) => value !== undefined && value !== "");
@@ -1,7 +1,6 @@
1
1
  /** Explicit environment and context loaders for the Axern SDK. */
2
2
  export interface AxernConfig {
3
3
  endpoint: string;
4
- serviceUrl?: string;
5
4
  sshEndpoint?: string;
6
5
  sshIdentityFile?: string;
7
6
  tlsCaCert?: string;