@cofy-x/axern-sdk 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -80,18 +80,6 @@ const sandbox = await new Sandbox({
80
80
 
81
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.
82
82
 
83
- Run a tool from a separate image with `execImage` or `processImage`. OCI and Nydus refs use the same image field. When `mounts` is omitted, the SDK requests `/workspace -> /workspace`; pass `mounts: []` for no shared paths. Use `new Sandbox({ image })` when the image should be the sandbox rootfs with normal files, exec, process, tunnel, and lifecycle APIs; image-backed processes are temporary side processes attached to an existing sandbox.
84
-
85
- ```ts
86
- import { workspaceMount } from "@cofy-x/axern-sdk";
87
-
88
- const result = await sandbox.execImage("ghcr.io/cofy-x/agent:latest", "tool run", {
89
- check: true,
90
- mounts: [workspaceMount("/workspace")],
91
- });
92
- console.log(result.stdoutText());
93
- ```
94
-
95
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.
96
84
 
97
85
  ## Configuration
@@ -106,6 +94,7 @@ Common options:
106
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
107
95
  - `limitCpu`, `limitMemory`, `limitEphemeralStorage`: runtime hard limits; numeric CPU values are cores and numeric memory/storage values are bytes
108
96
  - `readyTimeoutMs`: Run allocation startup timeout
97
+ - `declaredOutputs`: bounded files or directory-as-tar outputs sealed before runtime cleanup
109
98
 
110
99
  Tunnel options:
111
100
 
@@ -152,6 +141,8 @@ The SDK loads protobuf definitions through `@grpc/proto-loader`. Dynamic proto a
152
141
 
153
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.
154
143
 
155
- ## Run Output Retention
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.
156
147
 
157
- Run output reads expose Allocation-local stdout/stderr after runtime cleanup until `output_expires_at`, fixed at 15 minutes after cleanup begins. The combined readable limit is 64 MiB, with an explicit truncation signal. Node-process restart preserves sealed output; node-disk loss does not. Ordinary writable files still require explicit download before termination. No durable output object or persistent workspace is created.
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,6 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import * as grpc from "@grpc/grpc-js";
7
+ import type { Writable } from "node:stream";
7
8
  import { AllocationClient } from "../node/client.js";
8
9
  import type { ResourceQuantity } from "../resources.js";
9
10
  import type { NetworkPolicy } from "../network-policy.js";
@@ -40,8 +41,37 @@ export interface CreateRunOptions {
40
41
  limitCpu?: ResourceQuantity;
41
42
  limitMemory?: ResourceQuantity;
42
43
  limitEphemeralStorage?: ResourceQuantity;
44
+ declaredOutputs?: readonly DeclaredOutput[];
43
45
  labels?: Record<string, string>;
44
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
+ }
45
75
  export interface ExtensionCapability {
46
76
  name: string;
47
77
  value?: string;
@@ -68,10 +98,19 @@ export declare class AxernClient {
68
98
  close(): void;
69
99
  createEnvironment(options: CreateEnvironmentOptions): Promise<Record<string, unknown>>;
70
100
  deleteEnvironment(environmentId: string): Promise<void>;
101
+ getEnvironment(environmentId: string): Promise<Record<string, unknown>>;
102
+ listEnvironments(options?: ListOptions): Promise<ListResult<Record<string, unknown>>>;
71
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>>;
72
109
  cancelRun(runId: string): Promise<void>;
73
110
  watchRun(runId: string, options?: WatchRunOptions): AsyncGenerator<Record<string, unknown>>;
74
111
  readRunOutput(runId: string, options?: ReadRunOutputOptions): AsyncGenerator<Record<string, unknown>>;
112
+ getSealedOutputManifest(runId: string): Promise<SealedOutput[]>;
113
+ downloadSealedOutput(runId: string, outputId: string, destination: Writable): Promise<SealedOutput>;
75
114
  allocation(allocationId: string): AllocationClient;
76
115
  tunnelClient(): TunnelControlClient;
77
116
  tunnelTransport(): GatewayTransportOptions;
@@ -4,9 +4,10 @@
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
12
  import { AllocationClient } from "../node/client.js";
12
13
  import { buildResourceSpec } from "../resources.js";
@@ -114,6 +115,24 @@ export class AxernClient {
114
115
  throw mapRpcError(error, "delete environment");
115
116
  }
116
117
  }
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
+ }
117
136
  async createRun(options) {
118
137
  const resources = buildResourceSpec(options);
119
138
  try {
@@ -130,6 +149,11 @@ export class AxernClient {
130
149
  extension_capability_requirements: (options.extensionCapabilities ?? []).map((capability) => ({
131
150
  capability: { name: capability.name, value: capability.value ?? "" },
132
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
+ })),
133
157
  resources,
134
158
  },
135
159
  labels: options.labels ?? {},
@@ -140,6 +164,44 @@ export class AxernClient {
140
164
  throw mapRpcError(error, "create run");
141
165
  }
142
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
+ }
143
205
  async cancelRun(runId) {
144
206
  try {
145
207
  await unary(this.runControl, "CancelRun", { run_id: required("runId", runId) });
@@ -238,6 +300,63 @@ export class AxernClient {
238
300
  retryDelayMs = Math.min(retryDelayMs * 2, 2_000);
239
301
  }
240
302
  }
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);
310
+ try {
311
+ const response = await unary(node, "GetSealedOutputManifest", { allocation_id: allocationId });
312
+ return (response.outputs ?? []).map(sealedOutput);
313
+ }
314
+ catch (error) {
315
+ throw mapRpcError(error, "get sealed output manifest", allocationId);
316
+ }
317
+ finally {
318
+ node.close();
319
+ }
320
+ }
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;
334
+ try {
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
+ }
344
+ }
345
+ catch (error) {
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();
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;
359
+ }
241
360
  allocation(allocationId) {
242
361
  return new AllocationClient({
243
362
  allocationId: required("allocationId", allocationId),
@@ -261,6 +380,40 @@ function transientReadError(error) {
261
380
  const code = error.code;
262
381
  return code === grpc.status.UNAVAILABLE || code === grpc.status.DEADLINE_EXCEEDED;
263
382
  }
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
+ }
264
417
  function sleep(milliseconds, signal) {
265
418
  if (signal?.aborted)
266
419
  return Promise.resolve();
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  export { AxernClient } from "./client/index.js";
7
- export type { AxernClientOptions, CreateEnvironmentOptions, CreateRunOptions, } from "./client/index.js";
7
+ export type { AxernClientOptions, CreateEnvironmentOptions, CreateRunOptions, DeclaredOutput, DeclaredOutputFormat, ListOptions, ListResult, SealedOutput, } from "./client/index.js";
8
8
  export { loadAxernContext, loadAxernEnv } from "./config/index.js";
9
9
  export type { AxernConfig } from "./config/index.js";
10
10
  export { AxernError, AxernRpcError, isNotFound, isCancelled, isUnavailable, errorRetryable, isPermissionDenied, isTimeout, rpcCode, SandboxExecError, SandboxStateError, SandboxTimeoutError, SandboxValidationError, } from "./errors/index.js";
@@ -8,7 +8,7 @@ import { nonEmptyPath } from "../validation.js";
8
8
  import { downloadArchiveStream, uploadArchiveStream } from "./streams.js";
9
9
  export async function uploadArchive(ctx, path, chunks, options = {}) {
10
10
  try {
11
- await ctx.withAuthRetry(options.leaseTtlSeconds ?? 300, (client) => uploadArchiveStream(client, ctx.authRequest({
11
+ await ctx.withAuthRetry((client) => uploadArchiveStream(client, ctx.authRequest({
12
12
  path: nonEmptyPath(path),
13
13
  format: 1,
14
14
  create_parents: options.createParents ?? true,
@@ -18,12 +18,12 @@ export async function process(ctx, command, options = {}) {
18
18
  call,
19
19
  closeClient: () => client.close(),
20
20
  });
21
- call.write({
21
+ await new Promise((resolve, reject) => call.write({
22
22
  open: ctx.authRequest({
23
23
  spec: execSpec(argv, options),
24
24
  ...(initialSize === undefined ? {} : { initial_size: initialSize }),
25
25
  }),
26
- });
26
+ }, (error) => error == null ? resolve() : reject(error)));
27
27
  try {
28
28
  await sandboxProcess.waitReady();
29
29
  return sandboxProcess;
@@ -7,7 +7,7 @@ import { mapRpcError } from "../errors/index.js";
7
7
  import { unary } from "../generated/proto.js";
8
8
  export async function capabilityStatus(ctx, options = {}) {
9
9
  try {
10
- const response = await ctx.withAuthRetry(60, (client) => unary(client, "CapabilityStatus", ctx.authRequest({}), options.rpcTimeoutMs));
10
+ const response = await ctx.withAuthRetry((client) => unary(client, "CapabilityStatus", ctx.authRequest({}), options.rpcTimeoutMs));
11
11
  return {
12
12
  ready: response.ready === true,
13
13
  capabilities: strings(response.capabilities),
@@ -68,7 +68,7 @@ export async function computerUseKeyboard(ctx, options = {}) {
68
68
  }
69
69
  async function computerUseUnary(ctx, method, payload, rpcTimeoutMs) {
70
70
  try {
71
- return await ctx.withAuthRetry(60, (client) => unary(client, method, ctx.authRequest(payload), rpcTimeoutMs));
71
+ return await ctx.withAuthRetry((client) => unary(client, method, ctx.authRequest(payload), rpcTimeoutMs));
72
72
  }
73
73
  catch (error) {
74
74
  throw mapRpcError(error, `sandbox ${method}`, ctx.allocationId);
@@ -16,7 +16,7 @@ export declare class NodeClientContext {
16
16
  private readonly credentials;
17
17
  private readonly channelOptions;
18
18
  constructor(options: NodeClientContextOptions);
19
- withAuthRetry<T>(_leaseTtlSeconds: number, operation: (client: grpc.Client) => Promise<T>): Promise<T>;
19
+ withAuthRetry<T>(operation: (client: grpc.Client) => Promise<T>): Promise<T>;
20
20
  rpcClient(): grpc.Client;
21
21
  authRequest(payload: Record<string, unknown>): Record<string, unknown>;
22
22
  }
@@ -15,7 +15,7 @@ export class NodeClientContext {
15
15
  this.credentials = options.credentials;
16
16
  this.channelOptions = options.channelOptions ?? {};
17
17
  }
18
- async withAuthRetry(_leaseTtlSeconds, operation) {
18
+ async withAuthRetry(operation) {
19
19
  const client = this.rpcClient();
20
20
  try {
21
21
  return await operation(client);
package/dist/node/exec.js CHANGED
@@ -9,7 +9,7 @@ import { normalizeCommand } from "../validation.js";
9
9
  export async function exec(ctx, command, options = {}) {
10
10
  const argv = normalizeCommand(command);
11
11
  try {
12
- const response = await ctx.withAuthRetry(options.leaseTtlSeconds ?? 300, async (client) => unary(client, "Exec", ctx.authRequest({
12
+ const response = await ctx.withAuthRetry(async (client) => unary(client, "Exec", ctx.authRequest({
13
13
  spec: execSpec(argv, options),
14
14
  }), options.rpcTimeoutMs));
15
15
  const result = execResult(response);
@@ -70,7 +70,7 @@ export async function touch(ctx, path, options = {}) {
70
70
  }
71
71
  async function fileUnary(ctx, method, payload) {
72
72
  try {
73
- return await ctx.withAuthRetry(300, (client) => unary(client, method, ctx.authRequest(payload)));
73
+ return await ctx.withAuthRetry((client) => unary(client, method, ctx.authRequest(payload)));
74
74
  }
75
75
  catch (error) {
76
76
  throw mapRpcError(error, `sandbox ${method}`, ctx.allocationId);
@@ -19,18 +19,23 @@ export declare class SandboxProcess {
19
19
  private error;
20
20
  private ended;
21
21
  private exit?;
22
+ private paused;
23
+ private closed;
24
+ private static readonly maxQueuedEvents;
25
+ private static readonly resumeQueuedEvents;
22
26
  constructor(options: SandboxProcessOptions);
23
- write(data: Buffer | Uint8Array | string): void;
24
- closeStdin(): void;
25
- resize(cols: number, rows: number): void;
26
- signal(signal: string): void;
27
- terminate(): void;
28
- kill(): void;
27
+ write(data: Buffer | Uint8Array | string): Promise<void>;
28
+ closeStdin(): Promise<void>;
29
+ resize(cols: number, rows: number): Promise<void>;
30
+ signal(signal: string): Promise<void>;
31
+ terminate(): Promise<void>;
32
+ kill(): Promise<void>;
29
33
  waitReady(): Promise<void>;
30
34
  wait(): Promise<ProcessResult>;
31
35
  close(): Promise<void>;
32
36
  events(): AsyncIterable<ProcessEvent>;
33
37
  private nextEvent;
34
38
  private enqueue;
39
+ private writeRequest;
35
40
  private finish;
36
41
  }
@@ -13,6 +13,10 @@ export class SandboxProcess {
13
13
  error;
14
14
  ended = false;
15
15
  exit;
16
+ paused = false;
17
+ closed = false;
18
+ static maxQueuedEvents = 64;
19
+ static resumeQueuedEvents = 32;
16
20
  constructor(options) {
17
21
  this.allocationId = options.allocationId;
18
22
  this.call = options.call;
@@ -25,22 +29,22 @@ export class SandboxProcess {
25
29
  this.call.on("end", () => this.finish());
26
30
  }
27
31
  write(data) {
28
- this.call.write({ stdin: Buffer.isBuffer(data) ? data : Buffer.from(data) });
32
+ return this.writeRequest({ stdin: Buffer.isBuffer(data) ? data : Buffer.from(data) });
29
33
  }
30
34
  closeStdin() {
31
- this.call.write({ close_stdin: true });
35
+ return this.writeRequest({ close_stdin: true });
32
36
  }
33
37
  resize(cols, rows) {
34
- this.call.write({ resize: { cols, rows } });
38
+ return this.writeRequest({ resize: { cols, rows } });
35
39
  }
36
40
  signal(signal) {
37
- this.call.write({ signal: { signal } });
41
+ return this.writeRequest({ signal: { signal } });
38
42
  }
39
43
  terminate() {
40
- this.signal("TERM");
44
+ return this.signal("TERM");
41
45
  }
42
46
  kill() {
43
- this.signal("KILL");
47
+ return this.signal("KILL");
44
48
  }
45
49
  async waitReady() {
46
50
  const next = await this.nextEvent();
@@ -69,7 +73,14 @@ export class SandboxProcess {
69
73
  throw new Error("sandbox process stream ended before exit");
70
74
  }
71
75
  async close() {
76
+ if (this.closed)
77
+ return;
78
+ if (!this.ended && this.exit === undefined) {
79
+ await this.terminate().catch(() => undefined);
80
+ }
81
+ this.closed = true;
72
82
  this.call.end();
83
+ this.call.cancel();
73
84
  this.closeClient();
74
85
  }
75
86
  async *events() {
@@ -90,6 +101,10 @@ export class SandboxProcess {
90
101
  nextEvent() {
91
102
  const event = this.queue.shift();
92
103
  if (event !== undefined) {
104
+ if (this.paused && this.queue.length <= SandboxProcess.resumeQueuedEvents) {
105
+ this.paused = false;
106
+ this.call.resume();
107
+ }
93
108
  return Promise.resolve({ done: false, value: event });
94
109
  }
95
110
  if (this.ended) {
@@ -108,8 +123,32 @@ export class SandboxProcess {
108
123
  waiter({ done: false, value: event });
109
124
  }
110
125
  else {
126
+ if (this.queue.length >= SandboxProcess.maxQueuedEvents) {
127
+ this.error = new Error("sandbox process consumer is too slow; event queue limit exceeded");
128
+ this.call.cancel();
129
+ this.finish();
130
+ return;
131
+ }
111
132
  this.queue.push(event);
133
+ if (!this.paused && this.queue.length >= SandboxProcess.maxQueuedEvents) {
134
+ this.paused = true;
135
+ this.call.pause();
136
+ }
137
+ }
138
+ }
139
+ writeRequest(request) {
140
+ if (this.closed || this.ended) {
141
+ return Promise.reject(new Error("sandbox process stream is closed"));
112
142
  }
143
+ return new Promise((resolve, reject) => {
144
+ this.call.write(request, (error) => {
145
+ if (error !== undefined && error !== null) {
146
+ reject(mapRpcError(error, "sandbox process write", this.allocationId));
147
+ return;
148
+ }
149
+ resolve();
150
+ });
151
+ });
113
152
  }
114
153
  finish() {
115
154
  if (this.ended) {
@@ -103,6 +103,21 @@ message ImageMount {
103
103
  bool readonly = 3;
104
104
  }
105
105
 
106
+ enum DeclaredOutputFormat {
107
+ DECLARED_OUTPUT_FORMAT_UNSPECIFIED = 0;
108
+ DECLARED_OUTPUT_FORMAT_FILE = 1;
109
+ DECLARED_OUTPUT_FORMAT_TAR = 2;
110
+ }
111
+
112
+ // DeclaredOutput identifies one bounded Allocation-local result that must be
113
+ // sealed before runtime cleanup. It is a Run output contract, not a durable
114
+ // artifact or workspace identity.
115
+ message DeclaredOutput {
116
+ string path = 1;
117
+ DeclaredOutputFormat format = 2;
118
+ string media_type = 3;
119
+ }
120
+
106
121
  message ExecutionConfig {
107
122
  repeated string argv = 1;
108
123
  map<string, string> env = 2;
@@ -114,6 +129,7 @@ message ExecutionConfig {
114
129
  repeated SecretEnvVar secret_env = 8;
115
130
  repeated SecretFile secret_files = 9;
116
131
  repeated ImageMount image_mounts = 10;
132
+ repeated DeclaredOutput declared_outputs = 11;
117
133
  }
118
134
 
119
135
  // AllocationLifecycleState tracks only placement and infrastructure cleanup.
@@ -36,13 +36,12 @@ message Run {
36
36
  // Latest allocation capability condition projection. It is independent
37
37
  // from Run lifecycle status and message.
38
38
  axern.control.capability.v1.CapabilityConditionSet capability_conditions = 14;
39
- string node_id = 15;
40
39
  // Immutable Environment input frozen atomically with Run admission. The
41
40
  // source record may be deleted after this Run is created.
42
- axern.control.environment.v1.EnvironmentSpec environment_spec = 16;
43
- axern.control.environment.v1.ResolvedEnvironmentSpec resolved_environment_spec = 17;
41
+ axern.control.environment.v1.EnvironmentSpec environment_spec = 15;
42
+ axern.control.environment.v1.ResolvedEnvironmentSpec resolved_environment_spec = 16;
44
43
  // Node-local stdout/stderr availability deadline; node loss may make bytes unavailable earlier.
45
- google.protobuf.Timestamp output_expires_at = 18;
44
+ google.protobuf.Timestamp output_expires_at = 17;
46
45
  }
47
46
 
48
47
  message RunListFilter {
@@ -66,22 +66,21 @@ enum TunnelSessionEventReasonCode {
66
66
  message TunnelSession {
67
67
  string session_id = 1;
68
68
  string allocation_id = 2;
69
- string node_id = 3;
70
- int32 remote_port = 4;
71
- string client_edge_target = 5;
72
- TunnelSessionStatus status = 6;
73
- string reason = 7;
74
- string bound_addr = 8;
75
- google.protobuf.Timestamp created_at = 9;
76
- google.protobuf.Timestamp updated_at = 10;
77
- google.protobuf.Timestamp expires_at = 11;
78
- string relay_id = 12;
79
- google.protobuf.Timestamp ready_at = 13;
80
- google.protobuf.Timestamp last_peer_event_at = 14;
81
- int64 bytes_in = 15;
82
- int64 bytes_out = 16;
83
- string namespace = 17;
84
- string creator_principal_id = 18;
69
+ int32 remote_port = 3;
70
+ string client_edge_target = 4;
71
+ TunnelSessionStatus status = 5;
72
+ string reason = 6;
73
+ string bound_addr = 7;
74
+ google.protobuf.Timestamp created_at = 8;
75
+ google.protobuf.Timestamp updated_at = 9;
76
+ google.protobuf.Timestamp expires_at = 10;
77
+ string relay_id = 11;
78
+ google.protobuf.Timestamp ready_at = 12;
79
+ google.protobuf.Timestamp last_peer_event_at = 13;
80
+ int64 bytes_in = 14;
81
+ int64 bytes_out = 15;
82
+ string namespace = 16;
83
+ string creator_principal_id = 17;
85
84
  }
86
85
 
87
86
  message TunnelSessionEvent {
@@ -122,9 +121,8 @@ message GetTunnelSessionResponse {
122
121
 
123
122
  message ListTunnelSessionsRequest {
124
123
  string allocation_id = 1;
125
- string node_id = 2;
126
- bool include_terminal = 3;
127
- string namespace = 4;
124
+ bool include_terminal = 2;
125
+ string namespace = 3;
128
126
  }
129
127
 
130
128
  message ListTunnelSessionsResponse {
@@ -3,6 +3,8 @@ syntax = "proto3";
3
3
  package axern.node.sandbox.v1;
4
4
 
5
5
  import "axern/common/file/v1/file.proto";
6
+ import "axern/control/common/v1/common.proto";
7
+ import "google/protobuf/timestamp.proto";
6
8
 
7
9
  option go_package = "github.com/cofy-x/axern/sdk/go/gen/axern/node/sandbox/v1;nodesandboxv1";
8
10
 
@@ -92,6 +94,49 @@ message ReadOutputResponse {
92
94
  int64 observed_at_unix_milli = 6;
93
95
  }
94
96
 
97
+ enum SealedOutputStatus {
98
+ SEALED_OUTPUT_STATUS_UNSPECIFIED = 0;
99
+ SEALED_OUTPUT_STATUS_AVAILABLE = 1;
100
+ SEALED_OUTPUT_STATUS_MISSING = 2;
101
+ SEALED_OUTPUT_STATUS_REJECTED = 3;
102
+ SEALED_OUTPUT_STATUS_CAPTURE_FAILED = 4;
103
+ SEALED_OUTPUT_STATUS_NODE_UNAVAILABLE = 5;
104
+ }
105
+
106
+ message SealedOutput {
107
+ string output_id = 1;
108
+ string path = 2;
109
+ int64 size_bytes = 3;
110
+ string sha256 = 4;
111
+ string media_type = 5;
112
+ axern.control.common.v1.DeclaredOutputFormat format = 6;
113
+ SealedOutputStatus status = 7;
114
+ string reason = 8;
115
+ google.protobuf.Timestamp sealed_at = 9;
116
+ google.protobuf.Timestamp expires_at = 10;
117
+ }
118
+
119
+ message GetSealedOutputManifestRequest {
120
+ string allocation_id = 1;
121
+ }
122
+
123
+ message GetSealedOutputManifestResponse {
124
+ repeated SealedOutput outputs = 1;
125
+ google.protobuf.Timestamp expires_at = 2;
126
+ }
127
+
128
+ message DownloadSealedOutputRequest {
129
+ string allocation_id = 1;
130
+ string output_id = 2;
131
+ int64 offset = 3;
132
+ }
133
+
134
+ message DownloadSealedOutputResponse {
135
+ bytes data = 1;
136
+ int64 next_offset = 2;
137
+ bool eof = 3;
138
+ }
139
+
95
140
  // Capabilities
96
141
 
97
142
  message CapabilityStatusRequest {
@@ -338,6 +383,8 @@ service NodeSandbox {
338
383
  rpc Exec(ExecRequest) returns (ExecResponse) {}
339
384
  rpc Process(stream ProcessRequest) returns (stream ProcessResponse) {}
340
385
  rpc ReadOutput(ReadOutputRequest) returns (stream ReadOutputResponse) {}
386
+ rpc GetSealedOutputManifest(GetSealedOutputManifestRequest) returns (GetSealedOutputManifestResponse) {}
387
+ rpc DownloadSealedOutput(DownloadSealedOutputRequest) returns (stream DownloadSealedOutputResponse) {}
341
388
  rpc CapabilityStatus(CapabilityStatusRequest) returns (CapabilityStatusResponse) {}
342
389
  rpc StatFile(StatFileRequest) returns (StatFileResponse) {}
343
390
  rpc ListDir(ListDirRequest) returns (ListDirResponse) {}
@@ -4,7 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
  import { AxernClient } from "../client/index.js";
7
- import type { ExtensionCapability } from "../client/index.js";
7
+ import type { DeclaredOutput, ExtensionCapability } from "../client/index.js";
8
8
  import type { SandboxProcess } from "../node/process.js";
9
9
  import type { ResourceQuantity } from "../resources.js";
10
10
  import type { NetworkPolicy } from "../network-policy.js";
@@ -20,6 +20,7 @@ export interface SandboxOptions {
20
20
  cwd?: string;
21
21
  networkPolicy?: NetworkPolicy;
22
22
  extensionCapabilities?: readonly ExtensionCapability[];
23
+ declaredOutputs?: readonly DeclaredOutput[];
23
24
  requestCpu?: ResourceQuantity;
24
25
  requestMemory?: ResourceQuantity;
25
26
  requestEphemeralStorage?: ResourceQuantity;
@@ -36,7 +37,6 @@ export interface SandboxState {
36
37
  environmentId: string;
37
38
  runId: string;
38
39
  allocationId: string;
39
- nodeId: string;
40
40
  startedAt: Date;
41
41
  }
42
42
  export interface SandboxMetadata extends SandboxState {
@@ -60,6 +60,7 @@ export class Sandbox {
60
60
  cwd: this.options.cwd,
61
61
  networkPolicy: this.options.networkPolicy,
62
62
  extensionCapabilities: this.options.extensionCapabilities,
63
+ declaredOutputs: this.options.declaredOutputs,
63
64
  requestCpu: this.options.requestCpu,
64
65
  requestMemory: this.options.requestMemory,
65
66
  requestEphemeralStorage: this.options.requestEphemeralStorage,
@@ -74,7 +75,6 @@ export class Sandbox {
74
75
  environmentId,
75
76
  runId: this.runId,
76
77
  allocationId: String(runningRun.allocation_id ?? ""),
77
- nodeId: String(runningRun.node_id ?? ""),
78
78
  startedAt: new Date(),
79
79
  };
80
80
  this.currentMetadata = sandboxMetadata(this.options, this.currentState);
@@ -93,11 +93,17 @@ export class Sandbox {
93
93
  return this;
94
94
  }
95
95
  catch (error) {
96
- await this.close();
96
+ try {
97
+ await this.close();
98
+ }
99
+ catch (cleanupError) {
100
+ throw new AggregateError([error, cleanupError], "sandbox start and cleanup failed");
101
+ }
97
102
  throw error;
98
103
  }
99
104
  }
100
105
  async close() {
106
+ const errors = [];
101
107
  const runId = this.runId;
102
108
  const tunnelRuntime = this.tunnelRuntime;
103
109
  this.runId = "";
@@ -105,17 +111,19 @@ export class Sandbox {
105
111
  this.currentState = undefined;
106
112
  this.currentMetadata = undefined;
107
113
  if (tunnelRuntime !== undefined) {
108
- await tunnelRuntime.stop().catch(() => undefined);
114
+ await tunnelRuntime.stop().catch((error) => errors.push(error));
109
115
  }
110
116
  if (runId !== "") {
111
- await this.client.cancelRun(runId).catch(() => undefined);
117
+ await this.client.cancelRun(runId).catch((error) => errors.push(error));
112
118
  }
113
119
  if (this.createdEnvironment && this.environmentId !== "") {
114
120
  const environmentId = this.environmentId;
115
121
  this.environmentId = "";
116
122
  this.createdEnvironment = false;
117
- await this.client.deleteEnvironment(environmentId).catch(() => undefined);
123
+ await this.client.deleteEnvironment(environmentId).catch((error) => errors.push(error));
118
124
  }
125
+ if (errors.length > 0)
126
+ throw new AggregateError(errors, "sandbox cleanup failed");
119
127
  }
120
128
  async exec(command, options = {}) {
121
129
  return this.nodeClient().exec(command, options);
package/dist/types.d.ts CHANGED
@@ -12,7 +12,6 @@ export interface ExecOptions {
12
12
  user?: string;
13
13
  tty?: boolean;
14
14
  check?: boolean;
15
- leaseTtlSeconds?: number;
16
15
  rpcTimeoutMs?: number;
17
16
  }
18
17
  export interface ExecResult {
@@ -91,11 +90,9 @@ export interface DownloadDirOptions {
91
90
  export interface UploadArchiveOptions {
92
91
  createParents?: boolean;
93
92
  overwrite?: boolean;
94
- leaseTtlSeconds?: number;
95
93
  rpcTimeoutMs?: number;
96
94
  }
97
95
  export interface DownloadArchiveOptions {
98
- leaseTtlSeconds?: number;
99
96
  rpcTimeoutMs?: number;
100
97
  }
101
98
  export interface TunnelOptions {
package/dist/version.d.ts CHANGED
@@ -3,5 +3,5 @@
3
3
  * Copyright 2026 cofy-x
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
- export declare const AXERN_VERSION = "0.7.0";
6
+ export declare const AXERN_VERSION = "0.8.0";
7
7
  export declare function platformName(): string;
package/dist/version.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * Copyright 2026 cofy-x
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
- export const AXERN_VERSION = "0.7.0";
6
+ export const AXERN_VERSION = "0.8.0";
7
7
  export function platformName() {
8
8
  return "axern";
9
9
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cofy-x/axern-sdk",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "TypeScript SDK for Axern agentic infrastructure and isolated sandboxes",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -1,51 +0,0 @@
1
- syntax = "proto3";
2
-
3
- package axern.control.gateway.v1;
4
-
5
- option go_package = "github.com/cofy-x/axern/sdk/go/gen/axern/control/gateway/v1;gatewayv1";
6
-
7
- import "google/protobuf/timestamp.proto";
8
- import "google/protobuf/empty.proto";
9
- message ResolveAllocationTerminalRequest {
10
- string allocation_id = 1;
11
- int64 ttl_seconds = 2;
12
- string credential_fingerprint = 3;
13
- AllocationAccessPurpose purpose = 4;
14
- string credential_kind = 5;
15
- }
16
-
17
- enum AllocationAccessPurpose {
18
- ALLOCATION_ACCESS_PURPOSE_UNSPECIFIED = 0;
19
- ALLOCATION_ACCESS_PURPOSE_INTERACTIVE = 1;
20
- ALLOCATION_ACCESS_PURPOSE_RUN_OUTPUT = 2;
21
- }
22
-
23
- message ResolveAllocationTerminalResponse {
24
- string allocation_id = 1;
25
- string run_id = 2;
26
- string node_id = 3;
27
- string node_target = 4;
28
- AllocationAccessGrant access_grant = 5;
29
- }
30
-
31
- message AllocationAccessGrant {
32
- string grant_id = 1;
33
- string allocation_id = 2;
34
- string node_id = 3;
35
- string plaintext_token = 4;
36
- google.protobuf.Timestamp expires_at = 5;
37
- }
38
-
39
- message ResolveTunnelRelayTargetRequest {
40
- string session_id = 1;
41
- }
42
-
43
- message ResolveTunnelRelayTargetResponse {
44
- string node_edge_target = 1;
45
- }
46
-
47
- service GatewayControl {
48
- rpc AuthorizeAllocationAccess(ResolveAllocationTerminalRequest) returns (google.protobuf.Empty) {}
49
- rpc ResolveAllocationTerminal(ResolveAllocationTerminalRequest) returns (ResolveAllocationTerminalResponse) {}
50
- rpc ResolveTunnelRelayTarget(ResolveTunnelRelayTargetRequest) returns (ResolveTunnelRelayTargetResponse) {}
51
- }