@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.
- package/README.md +245 -42
- package/dist/checkpoint-manager.d.ts +29 -4
- package/dist/checkpoint-manager.mjs +133 -24
- package/dist/command.d.ts +9 -2
- package/dist/command.mjs +8 -8
- package/dist/contract.d.ts +687 -0
- package/dist/driver-billing.d.ts +29 -0
- package/dist/driver-catalog.d.ts +43 -58
- package/dist/driver-catalog.mjs +192 -229
- package/dist/drivers/boat-client.d.ts +95 -0
- package/dist/drivers/boat-named-snapshots.d.ts +31 -0
- package/dist/drivers/boat-template.d.ts +49 -0
- package/dist/drivers/boat-template.mjs +1196 -0
- package/dist/drivers/boat.d.ts +64 -0
- package/dist/drivers/boat.mjs +990 -0
- package/dist/drivers/cloudflare.d.ts +5 -8
- package/dist/drivers/cloudflare.mjs +364 -213
- package/dist/drivers/docker.d.ts +4 -3
- package/dist/drivers/docker.mjs +244 -165
- package/dist/drivers/local-brokering.d.ts +14 -0
- package/dist/drivers/local-brokering.mjs +1029 -0
- package/dist/drivers/modal.d.ts +4 -7
- package/dist/drivers/modal.mjs +220 -155
- package/dist/drivers/transient-error.d.ts +13 -0
- package/dist/drivers/vercel.d.ts +21 -23
- package/dist/drivers/vercel.mjs +279 -169
- package/dist/egress-policy.d.ts +134 -0
- package/dist/egress-warnings.d.ts +22 -0
- package/dist/garbage-collect.d.ts +14 -0
- package/dist/hosted-network-policy.d.ts +11 -4
- package/dist/hosted-network-policy.mjs +221 -4
- package/dist/image-name.d.ts +12 -2
- package/dist/image-name.mjs +43 -0
- package/dist/image.d.ts +1 -20
- package/dist/image.mjs +11 -66
- package/dist/index.d.ts +8 -533
- package/dist/index.mjs +1420 -315
- package/dist/json.d.ts +13 -0
- package/dist/lease-failure.d.ts +173 -0
- package/dist/lease-lifecycle.d.ts +65 -0
- package/dist/lease-retry-wait.d.ts +8 -0
- package/dist/lease-start.d.ts +21 -0
- package/dist/lease-types.d.ts +15 -5
- package/dist/manager.d.ts +2 -0
- package/dist/runtime-env.d.ts +15 -2
- package/dist/runtime-env.mjs +50 -1
- package/dist/runtime-snapshot.d.ts +49 -0
- package/dist/runtime-version.d.ts +9 -0
- package/dist/runtime-version.mjs +2 -0
- package/dist/service-url-exposure.d.ts +1 -3
- package/dist/service-url-exposure.mjs +1 -26
- package/dist/start-fresh.d.ts +17 -0
- package/dist/start-telemetry.d.ts +37 -0
- package/dist/stores/memory.d.ts +1 -1
- package/dist/stores/memory.mjs +65 -54
- package/package.json +67 -81
- package/dist/daytona-allow-list.d.ts +0 -16
- package/dist/daytona-allow-list.mjs +0 -11
- package/dist/drivers/box.d.ts +0 -52
- package/dist/drivers/box.mjs +0 -499
- package/dist/drivers/cloudflare-egress.d.ts +0 -40
- package/dist/drivers/cloudflare-egress.mjs +0 -113
- package/dist/drivers/daytona.d.ts +0 -127
- package/dist/drivers/daytona.mjs +0 -541
- package/dist/drivers/exe.d.ts +0 -26
- package/dist/drivers/exe.mjs +0 -469
- package/dist/drivers/fetch-bridge.d.ts +0 -10
- package/dist/drivers/fetch-bridge.mjs +0 -111
- package/dist/lease-policy.d.ts +0 -48
- 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 {
|
|
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-
|
|
43
|
-
*
|
|
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 `
|
|
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
|
|
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();
|
package/dist/image-name.d.ts
CHANGED
|
@@ -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
|
|
7
|
-
|
|
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,
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
51
|
+
assertSandboxImageReference,
|
|
105
52
|
defaultSandboxImage,
|
|
106
|
-
defaultSnapshotName,
|
|
107
|
-
ensureSnapshot,
|
|
108
53
|
snapshotVersion
|
|
109
54
|
};
|