@claxedo/sandbox-manager 0.7.0 → 0.10.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 (70) hide show
  1. package/README.md +245 -42
  2. package/dist/checkpoint-manager.d.ts +29 -4
  3. package/dist/checkpoint-manager.mjs +133 -24
  4. package/dist/command.d.ts +9 -2
  5. package/dist/command.mjs +8 -8
  6. package/dist/contract.d.ts +687 -0
  7. package/dist/driver-billing.d.ts +29 -0
  8. package/dist/driver-catalog.d.ts +43 -58
  9. package/dist/driver-catalog.mjs +192 -229
  10. package/dist/drivers/boat-client.d.ts +95 -0
  11. package/dist/drivers/boat-named-snapshots.d.ts +31 -0
  12. package/dist/drivers/boat-template.d.ts +49 -0
  13. package/dist/drivers/boat-template.mjs +1196 -0
  14. package/dist/drivers/boat.d.ts +64 -0
  15. package/dist/drivers/boat.mjs +990 -0
  16. package/dist/drivers/cloudflare.d.ts +5 -8
  17. package/dist/drivers/cloudflare.mjs +364 -213
  18. package/dist/drivers/docker.d.ts +4 -3
  19. package/dist/drivers/docker.mjs +244 -165
  20. package/dist/drivers/local-brokering.d.ts +14 -0
  21. package/dist/drivers/local-brokering.mjs +1029 -0
  22. package/dist/drivers/modal.d.ts +4 -7
  23. package/dist/drivers/modal.mjs +220 -155
  24. package/dist/drivers/transient-error.d.ts +13 -0
  25. package/dist/drivers/vercel.d.ts +21 -23
  26. package/dist/drivers/vercel.mjs +279 -169
  27. package/dist/egress-policy.d.ts +134 -0
  28. package/dist/egress-warnings.d.ts +22 -0
  29. package/dist/garbage-collect.d.ts +14 -0
  30. package/dist/hosted-network-policy.d.ts +11 -4
  31. package/dist/hosted-network-policy.mjs +221 -4
  32. package/dist/image-name.d.ts +12 -2
  33. package/dist/image-name.mjs +43 -0
  34. package/dist/image.d.ts +1 -20
  35. package/dist/image.mjs +11 -66
  36. package/dist/index.d.ts +8 -533
  37. package/dist/index.mjs +1420 -315
  38. package/dist/json.d.ts +13 -0
  39. package/dist/lease-failure.d.ts +173 -0
  40. package/dist/lease-lifecycle.d.ts +65 -0
  41. package/dist/lease-retry-wait.d.ts +8 -0
  42. package/dist/lease-start.d.ts +21 -0
  43. package/dist/lease-types.d.ts +15 -5
  44. package/dist/manager.d.ts +2 -0
  45. package/dist/runtime-env.d.ts +15 -2
  46. package/dist/runtime-env.mjs +50 -1
  47. package/dist/runtime-snapshot.d.ts +49 -0
  48. package/dist/runtime-version.d.ts +9 -0
  49. package/dist/runtime-version.mjs +2 -0
  50. package/dist/service-url-exposure.d.ts +1 -3
  51. package/dist/service-url-exposure.mjs +1 -26
  52. package/dist/start-fresh.d.ts +17 -0
  53. package/dist/start-telemetry.d.ts +37 -0
  54. package/dist/stores/memory.d.ts +1 -1
  55. package/dist/stores/memory.mjs +65 -54
  56. package/package.json +67 -81
  57. package/dist/daytona-allow-list.d.ts +0 -16
  58. package/dist/daytona-allow-list.mjs +0 -11
  59. package/dist/drivers/box.d.ts +0 -52
  60. package/dist/drivers/box.mjs +0 -499
  61. package/dist/drivers/cloudflare-egress.d.ts +0 -40
  62. package/dist/drivers/cloudflare-egress.mjs +0 -113
  63. package/dist/drivers/daytona.d.ts +0 -127
  64. package/dist/drivers/daytona.mjs +0 -541
  65. package/dist/drivers/exe.d.ts +0 -26
  66. package/dist/drivers/exe.mjs +0 -469
  67. package/dist/drivers/fetch-bridge.d.ts +0 -10
  68. package/dist/drivers/fetch-bridge.mjs +0 -111
  69. package/dist/lease-policy.d.ts +0 -48
  70. package/dist/lease-policy.mjs +0 -280
@@ -0,0 +1,134 @@
1
+ export type SandboxNetworkPolicy = {
2
+ mode: "allow-all" | "restricted";
3
+ hosts?: string[];
4
+ cidrs?: string[];
5
+ };
6
+ /** Brokered destinations are the minimum egress needed to use the credential. */
7
+ export declare function sandboxNetworkPolicyWithBrokeredHosts(policy: SandboxNetworkPolicy, secrets: readonly SandboxBrokeredSecret[] | undefined): SandboxNetworkPolicy;
8
+ /** @see SandboxDriverMetadata.egressControl */
9
+ export type SandboxEgressControl = "none" | "hosts" | "hosts-and-cidrs";
10
+ /**
11
+ * What the manager does with a caller's `net` for a given driver: enforce
12
+ * where the driver can, and say so loudly rather than refuse where it cannot.
13
+ *
14
+ * - `"enforce"` — the driver gets the policy verbatim and contains the
15
+ * sandbox. Also the answer when the caller asked for no containment
16
+ * (absent / `allow-all`), which is nothing to enforce.
17
+ * - `"withhold"` — the driver declares `egressControl: "none"`, so there is no
18
+ * containment to be had. The policy is NOT handed down: half the `"none"`
19
+ * drivers throw on a restricted policy (docker, modal, boat) and the
20
+ * other half silently drop it (cloudflare, the fetch bridge). Withholding at
21
+ * the manager means the throwing drivers never see one — their throws stay
22
+ * in place as their own last line of defence — and the silently-dropping
23
+ * ones stop pretending. The sandbox comes up with UNRESTRICTED egress, which
24
+ * is a real exposure, so the manager warns (see `onEgressUnenforced`).
25
+ * - `"refuse"` — the driver HAS egress control but cannot express this
26
+ * particular policy's encoding (a hosts-only driver handed an address-only
27
+ * allowlist). Deliberately still fail-closed: this is a composition mistake
28
+ * on an otherwise-enforcing driver, and degrading it to "withhold" would
29
+ * weaken the enforcing path. `hostedSandboxNetworkPolicy` only ever emits
30
+ * hosts, so no production path reaches it.
31
+ *
32
+ * Pure, like `validateSandboxPersistenceCapabilities`, so the manager, the
33
+ * composition layer, and tests can all ask the same question.
34
+ */
35
+ export type SandboxEgressDisposition = {
36
+ action: "enforce";
37
+ } | {
38
+ action: "withhold";
39
+ reason: "sandbox_egress_uncontained";
40
+ } | {
41
+ action: "refuse";
42
+ reason: "sandbox_egress_policy_unenforceable";
43
+ };
44
+ /** @see SandboxEgressDisposition */
45
+ export declare function sandboxEgressDisposition(control: SandboxEgressControl, net: SandboxNetworkPolicy | undefined): SandboxEgressDisposition;
46
+ /**
47
+ * Emitted whenever a restricted egress policy is withheld because the composed
48
+ * driver declares no egress control: an unrestricted sandbox that nobody was
49
+ * told about is the exposure, so the degrade is never silent.
50
+ *
51
+ * Two phases, because a per-create line is easy to miss in request logs:
52
+ * - `"composition"` — once per `createSandboxManager` per driver, at boot.
53
+ * - `"ensure"` — every time a caller's policy is actually withheld, naming the
54
+ * workspace and the allowlist that did not take effect.
55
+ */
56
+ export type SandboxEgressUnenforcedEvent = {
57
+ phase: "composition" | "ensure";
58
+ reason: "sandbox_egress_uncontained";
59
+ /** `SandboxDriver.id` of the composed driver. */
60
+ driver: string;
61
+ /** Always `"none"` today — carried so a sink can group without re-deriving. */
62
+ egressControl: SandboxEgressControl;
63
+ /** Ready-to-log sentence. Sinks that only forward text can use this alone. */
64
+ message: string;
65
+ /** Present on `"ensure"`. */
66
+ workspaceId?: string;
67
+ /** The start's placement labels. Present on `"ensure"`. */
68
+ labels?: Record<string, string>;
69
+ /** The allowlist the caller asked for and did NOT get. Present on `"ensure"`. */
70
+ requested?: SandboxNetworkPolicy;
71
+ };
72
+ /**
73
+ * A credential the SANDBOX must be able to USE for outbound requests but must
74
+ * NEVER be able to READ. Unlike `env` (plaintext, readable — reserved for
75
+ * credentials the agent is trusted with, e.g. the user's own model
76
+ * subscription), a brokered secret's `value` never enters the sandbox: the
77
+ * provider injects it on egress to `hosts` only. Non-model credentials
78
+ * (connection tokens, deploy tokens) belong here.
79
+ *
80
+ * Guarantees enforced by the manager: brokered secrets are never written to
81
+ * labels, never logged, and never captured in a driver snapshot. A driver
82
+ * whose `metadata.secretBrokering` is not `"native"` cannot honor them and the
83
+ * manager refuses to provision (fail-closed).
84
+ */
85
+ export type SandboxBrokeredSecret = {
86
+ /** Env var name the sandbox references. */
87
+ name: string;
88
+ /** The secret material. Never enters the sandbox in plaintext. */
89
+ value: string;
90
+ /** Egress allowlist: the only hosts for which the value is substituted/injected. */
91
+ hosts: string[];
92
+ /**
93
+ * The HTTP header the value is injected as on egress to `hosts`. Every
94
+ * native driver needs one and refuses a secret without it.
95
+ */
96
+ header?: string;
97
+ /**
98
+ * The authentication scheme `header` carries, when it has one (`Bearer`).
99
+ *
100
+ * A driver that WRITES the whole header composes `"<scheme> <value>"`; one
101
+ * that SUBSTITUTES a placeholder the sandbox already wrote after the scheme
102
+ * ignores it, because the scheme is in the request before the value is.
103
+ */
104
+ scheme?: string;
105
+ /**
106
+ * The methods and path prefixes the credential may be attached to, on top of
107
+ * `hosts`. A vendor host serves far more than the routes a turn needs —
108
+ * api.anthropic.com also answers the organization-admin API — and everything
109
+ * sharing the sandbox reaches the same host.
110
+ *
111
+ * Optional so a producer that has not been taught to state a policy still
112
+ * type-checks, and fails closed instead: a driver that can express them
113
+ * attaches the credential only within them, so an absent or empty policy
114
+ * names a host and nothing else, and nothing is spendable at a host alone. A
115
+ * driver whose provider cannot express them documents that it drops them,
116
+ * and the host allowlist is all the containment there is.
117
+ */
118
+ methods?: readonly string[];
119
+ pathPrefixes?: readonly string[];
120
+ };
121
+ /**
122
+ * What the sandbox presents so a header-injecting driver recognizes the request
123
+ * as one asking for that secret.
124
+ *
125
+ * Carries no authority: the value is attached at the edge, keyed by the host
126
+ * and this string, and the string itself is never accepted by a vendor. The
127
+ * Cloudflare sandbox Worker is deployed on its own and cannot depend on this
128
+ * package, so it matches on its own copy of the prefix; a driver minting
129
+ * anything else sends every brokered request upstream bare, which is what
130
+ * `brokered-placeholder.test.ts` exists to catch.
131
+ */
132
+ export declare function brokeredSecretPlaceholder(name: string): string;
133
+ /** The environment a header-injecting driver must add so the sandbox can present each secret. */
134
+ export declare function brokeredPlaceholderEnv(secrets: readonly SandboxBrokeredSecret[] | undefined): Record<string, string>;
@@ -0,0 +1,22 @@
1
+ import type { SandboxEgressControl, SandboxEgressUnenforcedEvent, SandboxNetworkPolicy } from "./egress-policy";
2
+ /**
3
+ * The two halves of the warning that a driver runs sandboxes with unrestricted
4
+ * egress. The composition half is raised here, once per driver per isolate: a
5
+ * per-create line lands in request logs and is easy to miss, this one lands
6
+ * wherever the process starts, so an operator learns of it BEFORE the first
7
+ * workspace exists. `withheld` is the start half, raised once per driver per
8
+ * isolate from the first start whose policy the driver could not enforce.
9
+ */
10
+ export declare function createEgressWarnings(input: {
11
+ driver: {
12
+ id: string;
13
+ egressControl: SandboxEgressControl;
14
+ };
15
+ sink?: (event: SandboxEgressUnenforcedEvent) => void;
16
+ }): {
17
+ withheld(start: {
18
+ workspaceId: string;
19
+ labels?: Record<string, string>;
20
+ requested: SandboxNetworkPolicy;
21
+ }): void;
22
+ };
@@ -0,0 +1,14 @@
1
+ import { type SandboxDriver, type SandboxGarbageCollectResult, type SandboxLeaseStore } from "./contract";
2
+ /**
3
+ * Destroys every sandbox the driver lists under `appLabel` that no lease owns.
4
+ * The lease store is authoritative for which provider resource a workspace
5
+ * owns, so the sweep reads the provider first and the leases second: a driver
6
+ * that cannot enumerate answers "we could not look", never "nothing is
7
+ * orphaned", because an empty listing reaching the sweep would make every live
8
+ * sandbox look like an orphan and destroy the fleet.
9
+ */
10
+ export declare function sweepSandboxes(input: {
11
+ driver: SandboxDriver;
12
+ leaseStore: SandboxLeaseStore;
13
+ appLabel: string;
14
+ }): Promise<SandboxGarbageCollectResult>;
@@ -36,11 +36,12 @@
36
36
  * the only kind the Worker can compute, which is why `egressControl` treats
37
37
  * name filtering as the capability that qualifies a driver for hosted use.
38
38
  */
39
- import type { SandboxNetworkPolicy, SandboxSource } from ".";
39
+ import type { SandboxSource } from "./contract";
40
+ import type { SandboxNetworkPolicy } from "./egress-policy";
40
41
  /**
41
42
  * Model-provider endpoints. Derived from the harness→provider groups in
42
- * claxedo-server `network/types.ts` (`PROVIDER_TO_GROUP`): claude-acp and
43
- * claude-sdk → anthropic, codex-acp/codex-app-server/cursor-acp → openai,
43
+ * claxedo-server `network/types.ts` (`PROVIDER_TO_GROUP`): claude-sdk →
44
+ * anthropic, codex-app-server/cursor-sdk → openai,
44
45
  * google → google_ai; plus the aggregator and catalog endpoints the engine
45
46
  * itself fetches at startup.
46
47
  */
@@ -58,9 +59,15 @@ export declare const SANDBOX_BASELINE_EGRESS_HOSTS: readonly ["api.anthropic.com
58
59
  * The git host this workspace clones from.
59
60
  *
60
61
  * Scoped to the ONE repository host in the request rather than to a forge
61
- * allowlist, matching how `authenticatedGitHubCloneSource` scopes the clone
62
+ * allowlist, matching how `githubCloneSecret` scopes the clone
62
63
  * credential it brokers (`hosts: ["github.com"]`) — the credential and the
63
64
  * network policy should describe the same reachable surface.
65
+ *
66
+ * The host is caller-selected, so only a public spelling earns an allowlist
67
+ * entry: `169.254.169.254` or `localhost` named as a repoUrl would otherwise
68
+ * open the sandbox's egress to the provisioning network's own addresses. A
69
+ * DNS name cannot be seen through here (no resolver in this module); a name
70
+ * that hides a private answer is clone admission's refusal, not this one's.
64
71
  */
65
72
  export declare function sandboxSourceHost(source: SandboxSource | undefined): string | undefined;
66
73
  export type HostedSandboxNetworkPolicyInput = {
@@ -1,3 +1,220 @@
1
+ // ../sandbox-contract/dist/index.mjs
2
+ function ipv4Number(part) {
3
+ let value;
4
+ if (/^0[xX][0-9a-fA-F]+$/.test(part))
5
+ value = Number.parseInt(part.slice(2), 16);
6
+ else if (/^0[0-7]+$/.test(part))
7
+ value = Number.parseInt(part.slice(1), 8);
8
+ else if (/^\d+$/.test(part))
9
+ value = Number.parseInt(part, 10);
10
+ else
11
+ return;
12
+ return Number.isSafeInteger(value) ? value : void 0;
13
+ }
14
+ function parseIpv4(input) {
15
+ const parts = input.split(".");
16
+ if (parts.length > 1 && parts[parts.length - 1] === "")
17
+ parts.pop();
18
+ if (parts.length < 1 || parts.length > 4)
19
+ return;
20
+ const numbers = [];
21
+ for (const part of parts) {
22
+ const value = ipv4Number(part);
23
+ if (value === void 0)
24
+ return;
25
+ numbers.push(value);
26
+ }
27
+ const last = numbers[numbers.length - 1];
28
+ if (last >= 256 ** (5 - numbers.length))
29
+ return;
30
+ for (const value of numbers.slice(0, -1))
31
+ if (value > 255)
32
+ return;
33
+ let address = last;
34
+ for (let i = 0; i < numbers.length - 1; i++)
35
+ address += numbers[i] * 256 ** (3 - i);
36
+ return address >>> 0;
37
+ }
38
+ function hextet(part) {
39
+ return /^[0-9a-fA-F]{1,4}$/.test(part) ? Number.parseInt(part, 16) : void 0;
40
+ }
41
+ function embeddedQuad(input) {
42
+ const parts = input.split(".");
43
+ if (parts.length !== 4)
44
+ return;
45
+ const octets = [];
46
+ for (const part of parts) {
47
+ if (!/^\d{1,3}$/.test(part))
48
+ return;
49
+ const value = Number.parseInt(part, 10);
50
+ if (value > 255)
51
+ return;
52
+ octets.push(value);
53
+ }
54
+ return octets;
55
+ }
56
+ function parseIpv6(input) {
57
+ let body = input;
58
+ let tail;
59
+ const lastColon = body.lastIndexOf(":");
60
+ if (lastColon >= 0 && body.includes(".")) {
61
+ tail = embeddedQuad(body.slice(lastColon + 1));
62
+ if (!tail)
63
+ return;
64
+ body = body.slice(0, lastColon + 1) + "0:0";
65
+ }
66
+ if ((body.match(/::/g) ?? []).length > 1)
67
+ return;
68
+ let halves;
69
+ if (body.includes("::")) {
70
+ const [left = "", right = ""] = body.split("::");
71
+ const leftParts = left === "" ? [] : left.split(":");
72
+ const rightParts = right === "" ? [] : right.split(":");
73
+ const missing = 8 - leftParts.length - rightParts.length;
74
+ if (missing < 1)
75
+ return;
76
+ halves = [...leftParts, ...Array.from({ length: missing }, () => "0"), ...rightParts];
77
+ } else {
78
+ halves = body.split(":");
79
+ if (halves.length !== 8)
80
+ return;
81
+ }
82
+ if (halves.length !== 8)
83
+ return;
84
+ const segments = [];
85
+ for (const part of halves) {
86
+ const value = hextet(part);
87
+ if (value === void 0)
88
+ return;
89
+ segments.push(value);
90
+ }
91
+ if (tail) {
92
+ segments[6] = tail[0] * 256 + tail[1];
93
+ segments[7] = tail[2] * 256 + tail[3];
94
+ }
95
+ return segments;
96
+ }
97
+ function parseIpAddress(input) {
98
+ if (!input)
99
+ return;
100
+ let host = input.trim().toLowerCase();
101
+ if (host.startsWith("[") && host.endsWith("]") && host.includes(":"))
102
+ host = host.slice(1, -1);
103
+ if (host.includes(":")) {
104
+ const segments = parseIpv6(host);
105
+ return segments ? { kind: "v6", segments } : void 0;
106
+ }
107
+ const value = parseIpv4(host);
108
+ return value === void 0 ? void 0 : { kind: "v4", value };
109
+ }
110
+ var V4_PRIVATE = [
111
+ [0, 8],
112
+ [167772160, 8],
113
+ [1681915904, 10],
114
+ [2130706432, 8],
115
+ [2851995648, 16],
116
+ [2886729728, 12],
117
+ [3221225472, 24],
118
+ [3221225984, 24],
119
+ [3223307264, 24],
120
+ [3224682752, 24],
121
+ [3227017984, 24],
122
+ [3232235520, 16],
123
+ [3232706560, 24],
124
+ [3323068416, 15],
125
+ [3325256704, 24],
126
+ [3405803776, 24],
127
+ [3758096384, 4],
128
+ [4026531840, 4]
129
+ ];
130
+ function inCidr(value, base, bits) {
131
+ return (value ^ base) >>> 32 - bits === 0;
132
+ }
133
+ function isPrivateIpv4(value) {
134
+ return V4_PRIVATE.some(([base, bits]) => inCidr(value, base, bits));
135
+ }
136
+ function embeddedIpv4(segments) {
137
+ const [a, b, c, d, e, f, g, h] = segments;
138
+ if (a === 0 && b === 0 && c === 0 && d === 0 && e === 0) {
139
+ if (f === 65535 || f === 0)
140
+ return g * 65536 + h >>> 0;
141
+ }
142
+ if (a === 100 && b === 65435 && c === 0 && d === 0 && e === 0 && f === 0)
143
+ return g * 65536 + h >>> 0;
144
+ if (a === 8194)
145
+ return b * 65536 + c >>> 0;
146
+ if (a === 8193 && b === 0)
147
+ return (g ^ 65535) * 65536 + (h ^ 65535) >>> 0;
148
+ return;
149
+ }
150
+ function isUnspecified(segments) {
151
+ return segments.every((segment) => segment === 0);
152
+ }
153
+ function isIpv6Loopback(segments) {
154
+ return segments.slice(0, 7).every((segment) => segment === 0) && segments[7] === 1;
155
+ }
156
+ function isPrivateIpAddress(ip) {
157
+ if (ip.kind === "v4")
158
+ return isPrivateIpv4(ip.value);
159
+ const segments = ip.segments;
160
+ if (isUnspecified(segments) || isIpv6Loopback(segments))
161
+ return true;
162
+ const first = segments[0];
163
+ if ((first & 65280) === 65280)
164
+ return true;
165
+ if ((first & 65024) === 64512)
166
+ return true;
167
+ if ((first & 65472) === 65152)
168
+ return true;
169
+ if ((first & 65472) === 65216)
170
+ return true;
171
+ if (first === 256 && segments[1] === 0 && segments[2] === 0 && segments[3] === 0)
172
+ return true;
173
+ if (first === 8193 && segments[1] === 3512)
174
+ return true;
175
+ const embedded = embeddedIpv4(segments);
176
+ if (embedded !== void 0)
177
+ return isPrivateIpv4(embedded);
178
+ return false;
179
+ }
180
+ function safeRepoUrl(input) {
181
+ try {
182
+ const url = new URL(input);
183
+ return url.protocol === "https:" || url.protocol === "http:" || url.protocol === "ssh:" ? input : void 0;
184
+ } catch {
185
+ return /^[\w.-]+@[\w.-]+:[\w./-]+$/.test(input) ? input : void 0;
186
+ }
187
+ }
188
+ function repoUrlHost(input) {
189
+ if (!safeRepoUrl(input))
190
+ return;
191
+ try {
192
+ const host = new URL(input).hostname;
193
+ return host || void 0;
194
+ } catch {
195
+ return /^[\w.-]+@([\w.-]+):[\w./-]+$/.exec(input)?.[1]?.toLowerCase();
196
+ }
197
+ }
198
+ function bareRepoHost(host) {
199
+ const bare = host.trim().toLowerCase();
200
+ return bare.startsWith("[") && bare.endsWith("]") && bare.includes(":") ? bare.slice(1, -1) : bare;
201
+ }
202
+ function loopbackRepoName(bare) {
203
+ return bare === "localhost" || bare.endsWith(".localhost");
204
+ }
205
+ function publicRepoHost(repoUrl) {
206
+ const host = repoUrlHost(repoUrl);
207
+ if (!host)
208
+ return;
209
+ const bare = bareRepoHost(host);
210
+ if (loopbackRepoName(bare))
211
+ return;
212
+ const ip = parseIpAddress(bare);
213
+ if (ip && isPrivateIpAddress(ip))
214
+ return;
215
+ return host;
216
+ }
217
+
1
218
  // src/hosted-network-policy.ts
2
219
  var SANDBOX_MODEL_PROVIDER_HOSTS = [
3
220
  "api.anthropic.com",
@@ -25,17 +242,17 @@ var SANDBOX_BASELINE_EGRESS_HOSTS = [
25
242
  ];
26
243
  function hostOf(value) {
27
244
  const raw = value?.trim();
28
- if (!raw) return;
245
+ if (!raw) return void 0;
29
246
  try {
30
247
  const host = new URL(raw).hostname.trim().toLowerCase();
31
248
  return host ? host : void 0;
32
249
  } catch {
33
- return;
250
+ return void 0;
34
251
  }
35
252
  }
36
253
  function sandboxSourceHost(source) {
37
- if (!source || source.kind !== "git") return;
38
- return hostOf(source.repoUrl);
254
+ if (!source || source.kind !== "git") return void 0;
255
+ return publicRepoHost(source.repoUrl);
39
256
  }
40
257
  function hostedSandboxNetworkPolicy(input) {
41
258
  const hosts = /* @__PURE__ */ new Set();
@@ -2,6 +2,16 @@ export declare const SNAPSHOT_SCHEMA_VERSION = 8;
2
2
  export declare const DEFAULT_SANDBOX_IMAGE_REPOSITORY = "ghcr.io/kyashrathore/claxedo-sandbox";
3
3
  export type SandboxImageEnv = Record<string, string | undefined>;
4
4
  export declare function snapshotVersion(value: string): string;
5
+ /**
6
+ * A caller-supplied image string lands verbatim inside provider argv or
7
+ * commands (`docker create` argv, `docker run` inside a Boat VM). A value that
8
+ * parses as an option — a leading `-` — or that splits into multiple words
9
+ * would be consumed as provider flags, not as an image reference.
10
+ */
11
+ export declare function assertSandboxImageReference(image: string): string;
5
12
  export declare function sandboxImageRepository(env?: SandboxImageEnv): string;
6
- export declare function defaultSandboxImage(version?: string, buildId?: string, env?: SandboxImageEnv): string;
7
- export declare function defaultSnapshotName(version?: string, buildId?: string, env?: SandboxImageEnv): string;
13
+ export declare function defaultSandboxImage(version?: string, buildId?: string, env?: SandboxImageEnv, options?: {
14
+ agentPlugins?: boolean;
15
+ }): string;
16
+ /** Whether an image's tag names a build that carries the Agent Plugins runtime entry. */
17
+ export declare function sandboxImageCarriesAgentPlugins(image: string): boolean;
@@ -0,0 +1,43 @@
1
+ // src/runtime-version.ts
2
+ var DEFAULT_WORKSPACE_RUNTIME_VERSION = "0.5.2";
3
+ function workspaceRuntimeVersion() {
4
+ return process.env.WORKSPACE_RUNTIME_VERSION || DEFAULT_WORKSPACE_RUNTIME_VERSION;
5
+ }
6
+
7
+ // src/image-name.ts
8
+ var SNAPSHOT_SCHEMA_VERSION = 8;
9
+ var DEFAULT_SANDBOX_IMAGE_REPOSITORY = "ghcr.io/kyashrathore/claxedo-sandbox";
10
+ function snapshotVersion(value) {
11
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
12
+ }
13
+ function assertSandboxImageReference(image) {
14
+ if (!image || image !== image.trim() || image.startsWith("-") || /\s/.test(image)) {
15
+ throw new Error(`sandbox image reference is not a legal image identifier: ${JSON.stringify(image)}`);
16
+ }
17
+ return image;
18
+ }
19
+ function sandboxImageRepository(env = process.env) {
20
+ const repository = env.CLAXEDO_SANDBOX_IMAGE_REPOSITORY?.trim();
21
+ return repository ? repository : DEFAULT_SANDBOX_IMAGE_REPOSITORY;
22
+ }
23
+ function buildIdSuffix(buildId, env) {
24
+ const id = (buildId ?? env.CLAXEDO_SANDBOX_BUILD_ID)?.trim();
25
+ return id ? `-${snapshotVersion(id)}` : "";
26
+ }
27
+ var AGENT_PLUGINS_TAG = "-agent-plugins";
28
+ function defaultSandboxImage(version = workspaceRuntimeVersion(), buildId, env = process.env, options = {}) {
29
+ const feature = options.agentPlugins ? AGENT_PLUGINS_TAG : "";
30
+ return `${sandboxImageRepository(env)}:workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}${feature}-v${SNAPSHOT_SCHEMA_VERSION}`;
31
+ }
32
+ function sandboxImageCarriesAgentPlugins(image) {
33
+ return new RegExp(`${AGENT_PLUGINS_TAG}-v\\d+$`).test(image);
34
+ }
35
+ export {
36
+ DEFAULT_SANDBOX_IMAGE_REPOSITORY,
37
+ SNAPSHOT_SCHEMA_VERSION,
38
+ assertSandboxImageReference,
39
+ defaultSandboxImage,
40
+ sandboxImageCarriesAgentPlugins,
41
+ sandboxImageRepository,
42
+ snapshotVersion
43
+ };
package/dist/image.d.ts CHANGED
@@ -1,23 +1,4 @@
1
- import { Daytona, Image } from "@daytona/sdk";
2
1
  export { RUNTIME_DIR, WORKSPACE_RUNTIME_PORT, WORKSPACE_DIR } from "./defaults";
3
- export { SNAPSHOT_SCHEMA_VERSION, snapshotVersion, defaultSandboxImage, defaultSnapshotName, } from "./image-name";
4
- /**
5
- * Sink for snapshot-build progress. Every message here reports a long
6
- * operation an operator is actively waiting on — a snapshot build takes
7
- * minutes, and the Daytona builder's `onLogs` stream is the only view into
8
- * whether it is progressing or wedged.
9
- *
10
- * Defaults to `console.info`, deliberately unconditional: the same reasoning as
11
- * `onEgressUnenforced` in index.ts — someone who never wires a sink still sees
12
- * the build. Pass your own to route it into a logger; pass `() => {}` only if
13
- * you have another way to observe the build.
14
- */
15
- export type SandboxImageLogSink = (message: string, context?: Record<string, unknown>) => void;
16
- export type SandboxImageOptions = {
17
- log?: SandboxImageLogSink;
18
- };
2
+ export { SNAPSHOT_SCHEMA_VERSION, snapshotVersion, assertSandboxImageReference, defaultSandboxImage, } from "./image-name";
19
3
  export declare const SANDBOX_IMAGE_REPOSITORY: string;
20
4
  export declare const SANDBOX_IMAGE: string;
21
- export declare const SNAPSHOT_NAME: string;
22
- export declare function buildSandboxImage(): Image;
23
- export declare function ensureSnapshot(daytona: Daytona, options?: SandboxImageOptions): Promise<string>;
package/dist/image.mjs CHANGED
@@ -1,6 +1,3 @@
1
- // src/image.ts
2
- import { Image, DaytonaNotFoundError } from "@daytona/sdk";
3
-
4
1
  // src/constants.ts
5
2
  var DEFAULT_WORKSPACE_RUNTIME_PORT = 2593;
6
3
 
@@ -21,6 +18,12 @@ var DEFAULT_SANDBOX_IMAGE_REPOSITORY = "ghcr.io/kyashrathore/claxedo-sandbox";
21
18
  function snapshotVersion(value) {
22
19
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
23
20
  }
21
+ function assertSandboxImageReference(image) {
22
+ if (!image || image !== image.trim() || image.startsWith("-") || /\s/.test(image)) {
23
+ throw new Error(`sandbox image reference is not a legal image identifier: ${JSON.stringify(image)}`);
24
+ }
25
+ return image;
26
+ }
24
27
  function sandboxImageRepository(env = process.env) {
25
28
  const repository = env.CLAXEDO_SANDBOX_IMAGE_REPOSITORY?.trim();
26
29
  return repository ? repository : DEFAULT_SANDBOX_IMAGE_REPOSITORY;
@@ -29,81 +32,23 @@ function buildIdSuffix(buildId, env) {
29
32
  const id = (buildId ?? env.CLAXEDO_SANDBOX_BUILD_ID)?.trim();
30
33
  return id ? `-${snapshotVersion(id)}` : "";
31
34
  }
32
- function defaultSandboxImage(version = workspaceRuntimeVersion(), buildId, env = process.env) {
33
- return `${sandboxImageRepository(env)}:workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}-v${SNAPSHOT_SCHEMA_VERSION}`;
34
- }
35
- function defaultSnapshotName(version = workspaceRuntimeVersion(), buildId, env = process.env) {
36
- return `claxedo-workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}-v${SNAPSHOT_SCHEMA_VERSION}`;
35
+ var AGENT_PLUGINS_TAG = "-agent-plugins";
36
+ function defaultSandboxImage(version = workspaceRuntimeVersion(), buildId, env = process.env, options = {}) {
37
+ const feature = options.agentPlugins ? AGENT_PLUGINS_TAG : "";
38
+ return `${sandboxImageRepository(env)}:workspace-runtime-${snapshotVersion(version)}${buildIdSuffix(buildId, env)}${feature}-v${SNAPSHOT_SCHEMA_VERSION}`;
37
39
  }
38
40
 
39
41
  // src/image.ts
40
- function defaultImageLogSink(message, context) {
41
- if (context) console.info(message, context);
42
- else console.info(message);
43
- }
44
42
  var SANDBOX_IMAGE_REPOSITORY = sandboxImageRepository();
45
43
  var SANDBOX_IMAGE = process.env.CLAXEDO_SANDBOX_IMAGE || defaultSandboxImage();
46
- var SNAPSHOT_NAME = process.env.CLAXEDO_SNAPSHOT_NAME || defaultSnapshotName();
47
- function buildSandboxImage() {
48
- return Image.base(SANDBOX_IMAGE);
49
- }
50
- function snapshotUsable(state) {
51
- return state === "ready" || state === "active";
52
- }
53
- async function createSnapshot(daytona, log) {
54
- log("Creating sandbox snapshot (this may take a few minutes)...", { name: SNAPSHOT_NAME });
55
- await daytona.snapshot.create(
56
- { name: SNAPSHOT_NAME, image: buildSandboxImage() },
57
- {
58
- timeout: 120,
59
- onLogs: (chunk) => log("[snapshot]", { text: chunk })
60
- }
61
- );
62
- return SNAPSHOT_NAME;
63
- }
64
- async function ensureSnapshot(daytona, options = {}) {
65
- const log = options.log ?? defaultImageLogSink;
66
- try {
67
- const existing = await daytona.snapshot.get(SNAPSHOT_NAME);
68
- if (existing.state === "inactive") {
69
- log("Snapshot is inactive, reactivating...", { name: SNAPSHOT_NAME, id: existing.id });
70
- await daytona.snapshot.activate({ id: existing.id });
71
- for (let i = 0; i < 120; i++) {
72
- const snap = await daytona.snapshot.get(SNAPSHOT_NAME);
73
- if (snapshotUsable(snap.state)) return SNAPSHOT_NAME;
74
- if (snap.state === "error") throw new Error(`Snapshot activation failed: ${snap.errorReason}`);
75
- await new Promise((resolve) => setTimeout(resolve, 2e3));
76
- }
77
- throw new Error("Snapshot activation timed out");
78
- }
79
- if (existing.state === "building") {
80
- log("Snapshot is building, waiting (up to 10 min)...", { name: SNAPSHOT_NAME });
81
- for (let i = 0; i < 300; i++) {
82
- const snap = await daytona.snapshot.get(SNAPSHOT_NAME);
83
- if (snapshotUsable(snap.state)) return SNAPSHOT_NAME;
84
- if (snap.state === "error") throw new Error(`Snapshot build failed: ${snap.errorReason}`);
85
- if (i % 15 === 0) log("Still waiting for snapshot...", { state: snap.state, elapsed: `${i * 2}s` });
86
- await new Promise((resolve) => setTimeout(resolve, 2e3));
87
- }
88
- throw new Error("Snapshot build timed out after 10 minutes");
89
- }
90
- return SNAPSHOT_NAME;
91
- } catch (err) {
92
- if (!(err instanceof DaytonaNotFoundError)) throw err;
93
- }
94
- return createSnapshot(daytona, log);
95
- }
96
44
  export {
97
45
  RUNTIME_DIR,
98
46
  SANDBOX_IMAGE,
99
47
  SANDBOX_IMAGE_REPOSITORY,
100
- SNAPSHOT_NAME,
101
48
  SNAPSHOT_SCHEMA_VERSION,
102
49
  WORKSPACE_DIR,
103
50
  WORKSPACE_RUNTIME_PORT,
104
- buildSandboxImage,
51
+ assertSandboxImageReference,
105
52
  defaultSandboxImage,
106
- defaultSnapshotName,
107
- ensureSnapshot,
108
53
  snapshotVersion
109
54
  };