withruntime 0.12.0 → 0.14.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 (50) hide show
  1. package/README.md +3 -3
  2. package/README.src.md +2 -2
  3. package/dist/billing-cli.js +8 -4
  4. package/dist/blaxel/sandbox.d.ts +1 -1
  5. package/dist/blaxel/sandbox.js +1 -1
  6. package/dist/cli.d.ts +17 -2
  7. package/dist/cli.js +242 -69
  8. package/dist/credentials.d.ts +3 -0
  9. package/dist/credentials.js +15 -0
  10. package/dist/daytona/daytona.d.ts +1 -1
  11. package/dist/daytona/daytona.js +1 -1
  12. package/dist/e2b/sandbox.d.ts +1 -1
  13. package/dist/errors.d.ts +1 -1
  14. package/dist/errors.js +1 -1
  15. package/dist/index.d.ts +2 -1
  16. package/dist/machine.d.ts +15 -1
  17. package/dist/machine.js +26 -11
  18. package/dist/mcp.js +144 -20
  19. package/dist/products/api-keys.d.ts +73 -0
  20. package/dist/products/api-keys.js +23 -0
  21. package/dist/products/apps.d.ts +12 -31
  22. package/dist/products/apps.js +8 -113
  23. package/dist/products/balancers.d.ts +87 -0
  24. package/dist/products/balancers.js +8 -0
  25. package/dist/products/builds.d.ts +30 -0
  26. package/dist/products/builds.js +6 -0
  27. package/dist/products/index.d.ts +2 -0
  28. package/dist/products/index.js +2 -0
  29. package/dist/products/jobs.d.ts +2 -2
  30. package/dist/products/jobs.js +1 -1
  31. package/dist/products/observability.d.ts +7 -2
  32. package/dist/products/resources.d.ts +54 -0
  33. package/dist/products/resources.js +122 -0
  34. package/dist/products/secrets.d.ts +22 -0
  35. package/dist/products/secrets.js +8 -0
  36. package/dist/products/volumes.d.ts +3 -0
  37. package/dist/sandbox.d.ts +4 -0
  38. package/dist/sandbox.js +27 -2
  39. package/dist/transport.d.ts +4 -1
  40. package/dist/transport.js +25 -4
  41. package/dist/types.d.ts +9 -5
  42. package/dist/vercel/sandbox.d.ts +1 -1
  43. package/dist/vercel/sandbox.js +1 -1
  44. package/dist/wallet.d.ts +4 -1
  45. package/dist/wallet.js +2 -1
  46. package/package.json +2 -14
  47. package/dist/cli/logs.test.d.ts +0 -1
  48. package/dist/cli/logs.test.js +0 -272
  49. package/dist/products/logs.test.d.ts +0 -1
  50. package/dist/products/logs.test.js +0 -343
@@ -0,0 +1,87 @@
1
+ import type { Transport } from "../transport.js";
2
+ import { type ResourceRecord } from "./resources.js";
3
+ /** Private until launch: load balancers and the inbound firewall
4
+ * (packages/cloud/src/api/products/balancers.ts). A change answers
5
+ * `applying`; it is `active` once every router serves it, and `waitingOn`
6
+ * names any router that does not yet. */
7
+ export type AddressState = "applying" | "active" | "failed" | "deleting" | "deleted";
8
+ type Common = ResourceRecord & {
9
+ name: string | null;
10
+ labels: Record<string, string>;
11
+ state: AddressState;
12
+ createdAt: number;
13
+ updatedAt: number;
14
+ deletedAt: number | null;
15
+ /** Routers that do not yet serve the newest change. */
16
+ waitingOn: string[];
17
+ };
18
+ /** A sandbox with the port it serves on, or an App or Function by id. */
19
+ export type BalancerTarget = {
20
+ id: string;
21
+ port?: number;
22
+ };
23
+ export type Balancer = Common & {
24
+ hostname: string;
25
+ targets: BalancerTarget[];
26
+ };
27
+ export type CreateBalancer = {
28
+ name?: string;
29
+ labels?: Record<string, string>;
30
+ /** Up to 1000, all the organization's own. */
31
+ targets?: BalancerTarget[];
32
+ };
33
+ export type UpdateBalancer = {
34
+ name?: string | null;
35
+ labels?: Record<string, string>;
36
+ /** Replaces the whole list. */
37
+ targets?: BalancerTarget[];
38
+ };
39
+ export type FirewallRule = Common & {
40
+ address: string;
41
+ effect: "allow" | "deny" | "limit";
42
+ source?: string;
43
+ ports?: string;
44
+ requests?: number;
45
+ seconds?: number;
46
+ burst?: number;
47
+ priority: number;
48
+ };
49
+ export type CreateFirewallRule = {
50
+ name?: string;
51
+ labels?: Record<string, string>;
52
+ /** The load balancer it guards. */
53
+ address: string;
54
+ priority?: number;
55
+ } & ({
56
+ effect: "allow" | "deny";
57
+ source?: string;
58
+ ports?: string;
59
+ } | {
60
+ effect: "limit";
61
+ requests: number;
62
+ seconds: number;
63
+ burst?: number;
64
+ });
65
+ export type UpdateFirewallRule = {
66
+ name?: string | null;
67
+ labels?: Record<string, string>;
68
+ };
69
+ export declare function balancers(t: Transport): {
70
+ base: string;
71
+ create: (input: CreateBalancer, options?: import("../transport.js").RequestOptions) => Promise<Balancer>;
72
+ get: (id: string, options?: import("./resources.js").ResourceGetOptions<"active" | "deleted" | "failed" | "deleting" | "applying">) => Promise<Balancer>;
73
+ list: (query?: import("./resources.js").ResourceListQuery<"active" | "deleted" | "failed" | "deleting" | "applying">, options?: import("../transport.js").RequestOptions) => Promise<import("../page.js").Page<Balancer>>;
74
+ update: (id: string, input: UpdateBalancer, options?: import("../transport.js").RequestOptions) => Promise<Balancer>;
75
+ wait: (id: string, target: "active" | "deleted" | "failed" | "deleting" | "applying", options?: import("./resources.js").ResourceWaitOptions) => Promise<Balancer>;
76
+ delete: (id: string, options?: import("./resources.js").ResourceDeleteOptions) => Promise<Balancer>;
77
+ };
78
+ export declare function firewall(t: Transport): {
79
+ base: string;
80
+ create: (input: CreateFirewallRule, options?: import("../transport.js").RequestOptions) => Promise<FirewallRule>;
81
+ get: (id: string, options?: import("./resources.js").ResourceGetOptions<"active" | "deleted" | "failed" | "deleting" | "applying">) => Promise<FirewallRule>;
82
+ list: (query?: import("./resources.js").ResourceListQuery<"active" | "deleted" | "failed" | "deleting" | "applying">, options?: import("../transport.js").RequestOptions) => Promise<import("../page.js").Page<FirewallRule>>;
83
+ update: (id: string, input: UpdateFirewallRule, options?: import("../transport.js").RequestOptions) => Promise<FirewallRule>;
84
+ wait: (id: string, target: "active" | "deleted" | "failed" | "deleting" | "applying", options?: import("./resources.js").ResourceWaitOptions) => Promise<FirewallRule>;
85
+ delete: (id: string, options?: import("./resources.js").ResourceDeleteOptions) => Promise<FirewallRule>;
86
+ };
87
+ export {};
@@ -0,0 +1,8 @@
1
+ import { resource } from "./resources.js";
2
+ const SETTLED = ["failed", "deleted"];
3
+ export function balancers(t) {
4
+ return resource(t, "balancers", SETTLED);
5
+ }
6
+ export function firewall(t) {
7
+ return resource(t, "firewall/rules", SETTLED);
8
+ }
@@ -16,16 +16,33 @@ type Resource = {
16
16
  export type Build = Resource & {
17
17
  repositoryId: string;
18
18
  ref: string;
19
+ /** The branch whose move asked for it, when the caller said. */
20
+ branch: string | null;
19
21
  commit: string | null;
20
22
  state: BuildState;
21
23
  reason: string | null;
24
+ /** Milliseconds per step once the runner is done (install, build, mirror,
25
+ * fork, snapshot, total, ...); null until then. */
26
+ timings: Record<string, number> | null;
27
+ /** The saved server an App deploys, once a build of a repository with a
28
+ * start command succeeded; null otherwise. */
29
+ outputSnapshotId: string | null;
22
30
  };
23
31
  export type CreateBuild = {
24
32
  repositoryId: string;
25
33
  ref: string;
34
+ /** The same branch and commit is one build; a newer build of the branch
35
+ * supersedes older ones not yet ended. */
36
+ branch?: string;
26
37
  name?: string;
27
38
  labels?: Record<string, string>;
28
39
  };
40
+ /** A connected repository's settings changed in place; its id and builds
41
+ * stay. A field left out stays as it is; a null name clears it. */
42
+ export type ChangeRepository = Partial<Omit<ConnectRepository, "provider" | "name" | "labels">> & {
43
+ name?: string | null;
44
+ labels?: Record<string, string>;
45
+ };
29
46
  export type ConnectRepository = {
30
47
  provider: "github" | "git";
31
48
  repository: string;
@@ -37,6 +54,14 @@ export type ConnectRepository = {
37
54
  /** Leave out to choose by the lockfile (bun, pnpm, Yarn or npm). */
38
55
  installCommand?: string | null;
39
56
  buildCommand?: string | null;
57
+ /** Run after the build with PORT set; the build ends as this server,
58
+ * answering healthPath with 200 and saved with its memory. */
59
+ startCommand?: string | null;
60
+ port?: number;
61
+ healthPath?: string | null;
62
+ /** Settings install and build are given. Never a secret: anyone who reads
63
+ * the repository sees them. */
64
+ buildEnv?: Record<string, string>;
40
65
  name?: string;
41
66
  labels?: Record<string, string>;
42
67
  };
@@ -49,6 +74,10 @@ export type Repository = Resource & {
49
74
  rootDirectory: string;
50
75
  installCommand: string | null;
51
76
  buildCommand: string | null;
77
+ startCommand: string | null;
78
+ port: number;
79
+ healthPath: string | null;
80
+ buildEnv: Record<string, string>;
52
81
  state: "connected" | "disconnected";
53
82
  };
54
83
  export type ListBuilds = {
@@ -120,6 +149,7 @@ export declare function builds(t: Transport): {
120
149
  repositories: {
121
150
  connect: (input: ConnectRepository, options?: RequestOptions) => Promise<Repository>;
122
151
  list: (query?: ListRepositories, options?: RequestOptions) => Promise<Page<Repository>>;
152
+ update: (id: string, change: ChangeRepository, options?: RequestOptions) => Promise<Repository>;
123
153
  disconnect: (id: string, options?: RequestOptions) => Promise<Repository>;
124
154
  };
125
155
  };
@@ -211,6 +211,12 @@ export function builds(t) {
211
211
  repositories: {
212
212
  connect: (input, options) => t.json({ method: "POST", path: "/v1/repositories", body: input, ...options }),
213
213
  list: listRepositories,
214
+ update: (id, change, options) => t.json({
215
+ method: "POST",
216
+ path: `/v1/repositories/${encodeURIComponent(id)}:update`,
217
+ body: change,
218
+ ...options,
219
+ }),
214
220
  disconnect: (id, options) => t.json({
215
221
  method: "DELETE",
216
222
  path: `/v1/repositories/${encodeURIComponent(id)}`,
@@ -13,6 +13,7 @@ import { audit } from "./audit.js";
13
13
  import { sso } from "./sso.js";
14
14
  import { switching } from "./switching.js";
15
15
  import { limits } from "./limits.js";
16
+ import { keys } from "./api-keys.js";
16
17
  import { volumes } from "./volumes.js";
17
18
  import { jobs } from "./jobs.js";
18
19
  import { sandboxMounts } from "./mounts.js";
@@ -27,6 +28,7 @@ export declare const clientExtensions: {
27
28
  referrals: typeof referrals;
28
29
  billing: typeof billing;
29
30
  limits: typeof limits;
31
+ keys: typeof keys;
30
32
  switching: typeof switching;
31
33
  secrets: typeof secrets;
32
34
  webhooks: typeof webhooks;
@@ -11,6 +11,7 @@ import { audit } from "./audit.js";
11
11
  import { sso } from "./sso.js";
12
12
  import { switching } from "./switching.js";
13
13
  import { limits } from "./limits.js";
14
+ import { keys } from "./api-keys.js";
14
15
  import { volumes } from "./volumes.js";
15
16
  import { jobs } from "./jobs.js";
16
17
  import { sandboxMounts } from "./mounts.js";
@@ -40,6 +41,7 @@ export const clientExtensions = {
40
41
  referrals,
41
42
  billing,
42
43
  limits,
44
+ keys,
43
45
  switching,
44
46
  secrets,
45
47
  webhooks,
@@ -1,7 +1,7 @@
1
1
  import { Page } from "../page.js";
2
2
  import type { RequestOptions, Transport } from "../transport.js";
3
3
  /** A run's sandbox. Every field has a default; a run is a sandbox of this
4
- * size, on the included usage first and then on credit at the sandbox rates. */
4
+ * size, on the free usage first and then on credit at the sandbox rates. */
5
5
  export type JobCompute = {
6
6
  region: string;
7
7
  vcpu: number;
@@ -176,7 +176,7 @@ export declare function jobBody(input: CreateJob): Record<string, unknown>;
176
176
  * request id, and says no retry will change it. */
177
177
  export declare function switchedOff(product: string): (error: unknown) => never;
178
178
  /** Scheduled jobs: a command run in a fresh sandbox, once or on a cron
179
- * schedule. Each run is a sandbox, paid for as one: from the included usage first while the run fits the limits without credit, then from credit
179
+ * schedule. Each run is a sandbox, paid for as one: from the free usage first while the run fits the limits without credit, then from credit
180
180
  * at the sandbox rates, through the same holds and spending limits.
181
181
  *
182
182
  * const job = await runtime.jobs.create({
@@ -131,7 +131,7 @@ export function switchedOff(product) {
131
131
  };
132
132
  }
133
133
  /** Scheduled jobs: a command run in a fresh sandbox, once or on a cron
134
- * schedule. Each run is a sandbox, paid for as one: from the included usage first while the run fits the limits without credit, then from credit
134
+ * schedule. Each run is a sandbox, paid for as one: from the free usage first while the run fits the limits without credit, then from credit
135
135
  * at the sandbox rates, through the same holds and spending limits.
136
136
  *
137
137
  * const job = await runtime.jobs.create({
@@ -35,7 +35,7 @@ export type SandboxMetrics = {
35
35
  latest: MetricPoint | null;
36
36
  points: MetricPoint[];
37
37
  };
38
- export type WebhookEventType = "sandbox.created" | "sandbox.running" | "sandbox.paused" | "sandbox.woken" | "sandbox.stopped" | "sandbox.start_failed" | "sandbox.wake_failed" | "sandbox.host_unreachable" | "sandbox.host_reachable" | "sandbox.recovered" | "sandbox.recovery_failed" | "sandbox.files.changed" | "sandbox.out_of_memory" | "sandbox.secret_not_applied" | "snapshot.ready" | "snapshot.failed" | "snapshot.deleted" | "volume.ready" | "volume.failed" | "volume.deleted" | "egress_secret.created" | "egress_secret.updated" | "egress_secret.deleted" | "upstream_proxy.set" | "upstream_proxy.removed";
38
+ export type WebhookEventType = "sandbox.created" | "sandbox.running" | "sandbox.paused" | "sandbox.woken" | "sandbox.stopped" | "sandbox.start_failed" | "sandbox.wake_failed" | "sandbox.host_unreachable" | "sandbox.host_reachable" | "sandbox.recovered" | "sandbox.recovery_failed" | "sandbox.files.changed" | "sandbox.out_of_memory" | "sandbox.secret_not_applied" | "snapshot.ready" | "snapshot.failed" | "snapshot.deleted" | "volume.ready" | "volume.failed" | "volume.deleted" | "volume.recovered" | "egress_secret.created" | "egress_secret.updated" | "egress_secret.deleted" | "upstream_proxy.set" | "upstream_proxy.removed";
39
39
  /** An event as /v1/events lists it and a webhook delivers it. */
40
40
  export type RuntimeEvent = {
41
41
  id: string;
@@ -113,7 +113,7 @@ export type UsageExportRow = {
113
113
  name: string | null;
114
114
  kind: string;
115
115
  meter: string;
116
- /** credit, the trial's hours, or the month's included usage. */
116
+ /** credit, the trial's hours, or the month's free usage. */
117
117
  fundedBy: "credit" | "trial" | "allowance";
118
118
  startsAt: string;
119
119
  endsAt: string;
@@ -128,12 +128,17 @@ export type UsageExportRow = {
128
128
  /** Exact JSON text, preserving integers beyond JavaScript's number range. */
129
129
  measurementsJson: string | null;
130
130
  evidence: "measured" | "partial" | "missing" | null;
131
+ /** The resource is deleted and every charge it will ever have settled
132
+ * before finalThrough. A volume's backups bill on until retention ends. */
133
+ final: boolean;
131
134
  };
132
135
  export type UsageExportPage = {
133
136
  since: string;
134
137
  until: string;
135
138
  data: UsageExportRow[];
136
139
  nextCursor: string | null;
140
+ /** No row with settledAt before this can still appear or change. */
141
+ finalThrough: string | null;
137
142
  /** Concatenate pages in order; only the first page includes the header. */
138
143
  csv: string;
139
144
  };
@@ -0,0 +1,54 @@
1
+ import { Page } from "../page.js";
2
+ import type { RequestOptions, Transport } from "../transport.js";
3
+ /** The five verbs every resource product shares (CLOUD.md section 4, "One
4
+ * shape for every interface"; packages/cloud/src/api/resource-product.ts),
5
+ * and following one to a state. A product builds its client on these and
6
+ * adds only its own verbs. */
7
+ export type ResourceRecord = {
8
+ id: string;
9
+ state: string;
10
+ error: {
11
+ code: string;
12
+ message: string;
13
+ } | null;
14
+ };
15
+ export type ResourceListQuery<S extends string = string> = {
16
+ name?: string;
17
+ /** Repeated key:value filters; all must match. */
18
+ label?: readonly string[];
19
+ /** Defaults to the product's live states. */
20
+ state?: readonly S[];
21
+ /** 1 to 100; defaults to 50. */
22
+ limit?: number;
23
+ cursor?: string;
24
+ };
25
+ export type ResourceGetOptions<S extends string = string> = RequestOptions & {
26
+ waitFor?: S;
27
+ /** Server wait, 1 to 60 seconds; defaults to 30. Check the returned state. */
28
+ timeoutSeconds?: number;
29
+ };
30
+ export type ResourceWaitOptions = RequestOptions & {
31
+ /** Whole wait, including retries; defaults to 300000 ms. 0 disables it. */
32
+ timeoutMs?: number;
33
+ /** Delay between reads; defaults to 250 ms. */
34
+ pollMs?: number;
35
+ };
36
+ export type ResourceDeleteOptions = ResourceWaitOptions & {
37
+ wait?: boolean;
38
+ };
39
+ export declare const enc: typeof encodeURIComponent;
40
+ /** One deadline covers the write, retries and every read. A timed-out delete
41
+ * can be retried after a process restart with this same key. */
42
+ export declare function following<T>(options: ResourceWaitOptions, work: (request: RequestOptions, pause: () => Promise<void>) => Promise<T>): Promise<T>;
43
+ /** A resource product's client at `/v1/<path>`. A wait ends at its target
44
+ * or at any of `settled`, which answers with the resource's error. */
45
+ export declare function resource<R extends ResourceRecord, C, U, S extends string = R["state"]>(t: Transport, path: string, settled: readonly S[]): {
46
+ base: string;
47
+ /** Defaults belong to the API, so the SDK does not override them. */
48
+ create: (input: C, options?: RequestOptions) => Promise<R>;
49
+ get: (id: string, options?: ResourceGetOptions<S>) => Promise<R>;
50
+ list: (query?: ResourceListQuery<S>, options?: RequestOptions) => Promise<Page<R>>;
51
+ update: (id: string, input: U, options?: RequestOptions) => Promise<R>;
52
+ wait: (id: string, target: S, options?: ResourceWaitOptions) => Promise<R>;
53
+ delete: (id: string, options?: ResourceDeleteOptions) => Promise<R>;
54
+ };
@@ -0,0 +1,122 @@
1
+ import { RuntimeError } from "../errors.js";
2
+ import { Page } from "../page.js";
3
+ import { pause } from "../wait.js";
4
+ export const enc = encodeURIComponent;
5
+ function invalid(field) {
6
+ return new RuntimeError({
7
+ code: "invalid_request",
8
+ status: 0,
9
+ message: `${field} must be a finite, nonnegative number.`,
10
+ hint: "Fix the option and call again; nothing was sent.",
11
+ details: { field },
12
+ });
13
+ }
14
+ /** One deadline covers the write, retries and every read. A timed-out delete
15
+ * can be retried after a process restart with this same key. */
16
+ export async function following(options, work) {
17
+ const { pollMs = 250, timeoutMs = 300_000, signal, ...rest } = options;
18
+ if (!Number.isFinite(pollMs) || pollMs < 0)
19
+ throw invalid("pollMs");
20
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0)
21
+ throw invalid("timeoutMs");
22
+ const deadline = new AbortController();
23
+ const combined = signal ? AbortSignal.any([signal, deadline.signal]) : deadline.signal;
24
+ const timer = timeoutMs > 0 ? setTimeout(() => deadline.abort(), timeoutMs) : undefined;
25
+ const request = { ...rest, timeoutMs: 0, signal: combined };
26
+ const poll = () => pause(pollMs, combined);
27
+ try {
28
+ combined.throwIfAborted();
29
+ const result = await work(request, poll);
30
+ combined.throwIfAborted();
31
+ return result;
32
+ }
33
+ catch (cause) {
34
+ if (!combined.aborted) {
35
+ // A read failure still belongs to the write that started this wait.
36
+ // Keep its key so a caller can recover after losing the reply.
37
+ if (cause instanceof RuntimeError && rest.idempotencyKey && !cause.idempotencyKey)
38
+ throw new RuntimeError({
39
+ message: cause.message,
40
+ code: cause.code,
41
+ status: cause.status,
42
+ requestId: cause.requestId,
43
+ hint: cause.hint,
44
+ details: cause.details,
45
+ retryAfterMs: cause.retryAfterMs,
46
+ idempotencyKey: rest.idempotencyKey,
47
+ cause,
48
+ });
49
+ throw cause;
50
+ }
51
+ throw new RuntimeError({
52
+ code: "timeout",
53
+ status: 0,
54
+ message: "Waiting was cancelled or reached its deadline; the resource may still change.",
55
+ hint: "Read the resource to resume waiting. Retry a write with the same idempotencyKey.",
56
+ ...(rest.idempotencyKey === undefined ? {} : { idempotencyKey: rest.idempotencyKey }),
57
+ cause,
58
+ });
59
+ }
60
+ finally {
61
+ clearTimeout(timer);
62
+ }
63
+ }
64
+ /** A resource product's client at `/v1/<path>`. A wait ends at its target
65
+ * or at any of `settled`, which answers with the resource's error. */
66
+ export function resource(t, path, settled) {
67
+ const base = `/v1/${path}`;
68
+ const get = (id, options = {}) => {
69
+ const { waitFor, timeoutSeconds, ...request } = options;
70
+ return t.json({
71
+ method: "GET",
72
+ path: `${base}/${enc(id)}`,
73
+ query: { waitFor, timeoutSeconds },
74
+ ...request,
75
+ });
76
+ };
77
+ const list = async (query = {}, options) => {
78
+ const page = await t.json({
79
+ method: "GET",
80
+ path: base,
81
+ query,
82
+ ...options,
83
+ });
84
+ return new Page(page.data, page.nextCursor, (cursor) => list({ ...query, cursor }, options));
85
+ };
86
+ const follow = async (id, target, request, pause, initial) => {
87
+ let record = initial ?? (await get(id, request));
88
+ for (;;) {
89
+ if (record.state === target)
90
+ return record;
91
+ if (settled.includes(record.state))
92
+ throw new RuntimeError({
93
+ code: record.error?.code ?? "unexpected_state",
94
+ status: 0,
95
+ message: record.error?.message ?? `Expected ${target}, but ${id} reached ${record.state}.`,
96
+ hint: "Read the resource and inspect its error before starting another change.",
97
+ details: { id, state: record.state, target, resource: record },
98
+ ...(request.idempotencyKey === undefined
99
+ ? {}
100
+ : { idempotencyKey: request.idempotencyKey }),
101
+ });
102
+ await pause();
103
+ record = await get(id, request);
104
+ }
105
+ };
106
+ return {
107
+ base,
108
+ /** Defaults belong to the API, so the SDK does not override them. */
109
+ create: (input, options) => t.json({ method: "POST", path: base, body: input, ...options }),
110
+ get,
111
+ list,
112
+ update: (id, input, options) => t.json({ method: "POST", path: `${base}/${enc(id)}:update`, body: input, ...options }),
113
+ wait: (id, target, options = {}) => following(options, (request, pause) => follow(id, target, request, pause)),
114
+ delete: (id, options = {}) => {
115
+ const { wait = false, pollMs, ...request } = options;
116
+ const remove = (rest) => t.json({ method: "DELETE", path: `${base}/${enc(id)}`, ...rest });
117
+ if (!wait)
118
+ return remove(request);
119
+ return following({ ...request, pollMs, idempotencyKey: request.idempotencyKey ?? crypto.randomUUID() }, async (rest, pause) => follow(id, "deleted", rest, pause, await remove(rest)));
120
+ },
121
+ };
122
+ }
@@ -9,6 +9,17 @@ export type SecretRule = {
9
9
  methods?: string[];
10
10
  paths: string[];
11
11
  };
12
+ /** Which sandboxes get a secret: those whose labels hold every pair of any one
13
+ * selector in `labels` (including sandboxes created or labelled later), and
14
+ * those named in `sandboxes`. Any other sandbox of the organization sees no
15
+ * placeholder, and its requests never carry the value. Absent on a secret:
16
+ * every sandbox of the organization. */
17
+ export type SecretScope = {
18
+ /** 1 to 8 selectors of 1 to 8 key/value pairs: `[{ role: "hub" }]`. */
19
+ labels?: Array<Record<string, string>>;
20
+ /** 1 to 100 sandbox ids of the organization. */
21
+ sandboxes?: string[];
22
+ };
12
23
  /** A secret your sandboxes use without seeing it. Every sandbox of your
13
24
  * organization has an environment variable of the secret's name holding
14
25
  * `placeholder`; the egress proxy sets `header` on every HTTPS request to
@@ -22,6 +33,8 @@ export type Secret = {
22
33
  header?: string;
23
34
  format?: string;
24
35
  rules?: SecretRule[];
36
+ /** Absent: every sandbox of the organization. */
37
+ only?: SecretScope;
25
38
  placeholder: string;
26
39
  valueBytes: number;
27
40
  createdAt: string;
@@ -45,6 +58,10 @@ export type SetSecret = {
45
58
  * Absent: every request to the hosts; replacing a secret without rules
46
59
  * clears them. */
47
60
  rules?: SecretRule[];
61
+ /** Only these sandboxes get it. Absent when replacing a secret: the scope it
62
+ * has stays, so a new value never widens who gets it. `{}`: every sandbox of
63
+ * the organization again. */
64
+ only?: SecretScope;
48
65
  /** Also keep a copy for scheduled jobs, which a job puts into its run's
49
66
  * environment (`jobs.create({ secrets: [...] })`). Unlike the sandboxes'
50
67
  * copy, the jobs copy is yours rather than the account's, versioned, and can
@@ -98,6 +115,11 @@ export declare function secrets(t: Transport): {
98
115
  enforced: boolean;
99
116
  }>>;
100
117
  };
118
+ /** Changes which sandboxes get a secret, without its value: `{}` is every
119
+ * sandbox of the organization again. */
120
+ scope: (name: string, only: SecretScope, options?: RequestOptions) => Promise<Secret & {
121
+ enforced: boolean;
122
+ }>;
101
123
  /** The sandboxes' secrets: names, hosts and placeholders. Never values.
102
124
  * `all()` includes the jobs copies. */
103
125
  list: (options?: RequestOptions) => Promise<Secret[]>;
@@ -153,6 +153,14 @@ export function secrets(t) {
153
153
  * at once store both; if only the first lands, `SecretPartlyStoredError`
154
154
  * says so and the same call again finishes it. */
155
155
  set,
156
+ /** Changes which sandboxes get a secret, without its value: `{}` is every
157
+ * sandbox of the organization again. */
158
+ scope: (name, only, options) => t.json({
159
+ method: "PUT",
160
+ path: `${path(name)}/sandboxes`,
161
+ body: only,
162
+ ...options,
163
+ }),
156
164
  /** The sandboxes' secrets: names, hosts and placeholders. Never values.
157
165
  * `all()` includes the jobs copies. */
158
166
  list,
@@ -90,6 +90,9 @@ export type VolumeBackup = {
90
90
  * the host never began; back it up again. */
91
91
  errorCode?: string | null;
92
92
  createdAt: string;
93
+ /** When its copy off the volume's server began; null while it waits its
94
+ * turn behind other copies there. */
95
+ copyStartedAt?: string | null;
93
96
  readyAt: string | null;
94
97
  expiresAt: string;
95
98
  };
package/dist/sandbox.d.ts CHANGED
@@ -8,6 +8,10 @@ import type { StreamUploadSource } from "./file-source.js";
8
8
  import { Machine, type MachineProduct } from "./machine.js";
9
9
  /** Sandboxes, as a product of the machine engine (machine.ts). */
10
10
  export declare const SANDBOXES: MachineProduct;
11
+ /** How long past its timeoutMs a streamed exec waits for an exit that never came. */
12
+ export declare const EXEC_EXIT_GRACE: {
13
+ ms: number;
14
+ };
11
15
  /** A running (or stopped) sandbox. Every method is one API call or a short
12
16
  * sequence of them; `await using sbx = await Sandbox.create()` deletes it at
13
17
  * the end of the block, or stops it, keeping its disk, if created with
package/dist/sandbox.js CHANGED
@@ -87,6 +87,8 @@ function commandBody(command, options) {
87
87
  ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
88
88
  };
89
89
  }
90
+ /** How long past its timeoutMs a streamed exec waits for an exit that never came. */
91
+ export const EXEC_EXIT_GRACE = { ms: 120_000 };
90
92
  // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
91
93
  export class Sandbox extends Machine {
92
94
  #t;
@@ -147,8 +149,22 @@ export class Sandbox extends Machine {
147
149
  let exit;
148
150
  let processId;
149
151
  let dropped = false;
152
+ // A command with a time limit ends by it, the server killing it if need
153
+ // be; an exit that never comes after (a follow that resumes forever)
154
+ // ends the wait past the limit and EXEC_EXIT_GRACE.ms, as a timeout,
155
+ // its process named and sent SIGTERM. The hub's release train waited
156
+ // 29 min on a 10 min command (15 October 2026).
157
+ // A timer cleared when the call ends, so none outlives the exec.
158
+ const deadline = options.timeoutMs === undefined ? undefined : new AbortController();
159
+ const timer = deadline
160
+ ? setTimeout(() => deadline.abort(), options.timeoutMs + EXEC_EXIT_GRACE.ms)
161
+ : undefined;
162
+ timer?.unref?.();
163
+ const signal = deadline && options.signal
164
+ ? AbortSignal.any([options.signal, deadline.signal])
165
+ : (deadline?.signal ?? options.signal);
150
166
  try {
151
- for await (const event of this.execStream(command, options)) {
167
+ for await (const event of this.execStream(command, { ...options, signal })) {
152
168
  if (event.type === "start")
153
169
  processId = event.processId;
154
170
  else if (event.type === "stdout" || event.type === "stderr") {
@@ -162,7 +178,16 @@ export class Sandbox extends Machine {
162
178
  }
163
179
  }
164
180
  catch (error) {
165
- throw await this.#unfinished(error, processId, options.signal);
181
+ if (!deadline?.signal.aborted || options.signal?.aborted)
182
+ throw await this.#unfinished(error, processId, signal);
183
+ if (processId)
184
+ await new Process(this.#t, this.id, { id: processId })
185
+ .kill("SIGTERM")
186
+ .catch(() => { });
187
+ exit = { type: "exit", exitCode: null, timedOut: true };
188
+ }
189
+ finally {
190
+ clearTimeout(timer);
166
191
  }
167
192
  result = {
168
193
  exitCode: exit?.exitCode ?? null,
@@ -1,5 +1,5 @@
1
1
  import { RuntimeError } from "./errors.js";
2
- export declare const VERSION = "0.12.0";
2
+ export declare const VERSION = "0.14.0";
3
3
  export declare const DEFAULT_BASE_URL = "https://api.withruntime.com";
4
4
  /** Runtime's API as code inside a Runtime sandbox reaches it: the sandbox's
5
5
  * own host sends each request on to DEFAULT_BASE_URL over HTTPS. The API runs
@@ -7,6 +7,9 @@ export declare const DEFAULT_BASE_URL = "https://api.withruntime.com";
7
7
  * because the hop never leaves the machine: it goes from the program to the
8
8
  * guest's own proxy and over the sandbox's private channel to its host. */
9
9
  export declare const SANDBOX_BASE_URL = "http://runtime.internal";
10
+ /** How a Runtime secret's placeholder starts, the value a sandbox sees in its
11
+ * place (packages/cloud-guide/docs/security.md). */
12
+ export declare const PLACEHOLDER = "rtsec_";
10
13
  /** Every Runtime sandbox has this file; guest-net.py keeps it current. Tests
11
14
  * move it, so the machine running them does not decide what they see. */
12
15
  export declare const sandboxMarker: {