@claxedo/sandbox-manager 0.10.2 → 0.10.4
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 +67 -29
- package/dist/checkpoint-manager.mjs +11 -0
- package/dist/contract.d.ts +28 -16
- package/dist/driver-billing.d.ts +23 -0
- package/dist/driver-catalog.d.ts +1 -2
- package/dist/driver-catalog.mjs +61 -11
- package/dist/driver-ensure-input.d.ts +3 -2
- package/dist/drivers/agent37-client.d.ts +87 -0
- package/dist/drivers/agent37.d.ts +25 -0
- package/dist/drivers/agent37.mjs +886 -0
- package/dist/drivers/boat-client.d.ts +5 -5
- package/dist/drivers/boat-machine.d.ts +7 -17
- package/dist/drivers/boat-template.mjs +87 -35
- package/dist/drivers/boat.d.ts +0 -5
- package/dist/drivers/boat.mjs +81 -26
- package/dist/drivers/cloudflare.d.ts +5 -9
- package/dist/drivers/cloudflare.mjs +97 -43
- package/dist/drivers/docker.mjs +70 -19
- package/dist/drivers/local-brokering.mjs +90 -18
- package/dist/drivers/modal.mjs +56 -11
- package/dist/drivers/transient-error.d.ts +0 -1
- package/dist/drivers/vercel.mjs +65 -15
- package/dist/egress-policy.d.ts +33 -32
- package/dist/egress-warnings.d.ts +4 -6
- package/dist/hosted-network-policy.d.ts +10 -64
- package/dist/hosted-network-policy.mjs +1 -1
- package/dist/image-name.d.ts +0 -1
- package/dist/index.mjs +103 -78
- package/dist/json.d.ts +1 -13
- package/dist/lease-failure.d.ts +3 -3
- package/dist/lease-types.d.ts +1 -10
- package/dist/runtime-env.d.ts +6 -7
- package/dist/runtime-env.mjs +5 -10
- package/dist/runtime-version.d.ts +4 -0
- package/dist/runtime-version.mjs +6 -0
- package/dist/start-admission.d.ts +32 -0
- package/package.json +12 -4
|
@@ -1,67 +1,17 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The egress allowlist a HOSTED, multi-tenant sandbox is provisioned with.
|
|
3
|
-
*
|
|
4
|
-
* A hosted sandbox runs agent-authored code on the operator's infrastructure,
|
|
5
|
-
* next to other tenants' sandboxes, holding a checkout of the user's private
|
|
6
|
-
* repository and (via `SandboxBrokeredSecret`) the ability to spend the
|
|
7
|
-
* operator's model credits. The default for that shape of workload is
|
|
8
|
-
* restrictive, not allow-all: the interesting attack is not "the agent
|
|
9
|
-
* downloads a package", it is "the agent POSTs the repository to a host the
|
|
10
|
-
* attacker controls". An allowlist is what makes that fail.
|
|
11
|
-
*
|
|
12
|
-
* Every entry below has to earn its place, so the list is assembled from three
|
|
13
|
-
* sources of truth rather than from taste:
|
|
14
|
-
*
|
|
15
|
-
* 1. DERIVED PER REQUEST — the control plane the sandbox reports back to and
|
|
16
|
-
* the git host it clones from. These differ per deployment and per
|
|
17
|
-
* workspace, so they are inputs, not constants.
|
|
18
|
-
* 2. MODEL PROVIDERS — the endpoints an agent harness must reach to do any
|
|
19
|
-
* work at all. Taken from the harness→provider mapping the credential
|
|
20
|
-
* layer already maintains (`PROVIDER_TO_GROUP` /`DEFAULT_ALLOWLIST` in
|
|
21
|
-
* claxedo-server's `network/types.ts`), narrowed to the API hostnames.
|
|
22
|
-
* 3. PACKAGE REGISTRIES — an agent that cannot install a dependency cannot
|
|
23
|
-
* edit a real repository. Same source, again narrowed.
|
|
24
|
-
*
|
|
25
|
-
* What is deliberately NOT here, despite being in that same default list:
|
|
26
|
-
* general-purpose object storage and CDN wildcards (`*.workers.dev`,
|
|
27
|
-
* `r2.cloudflarestorage.com`, `storage.googleapis.com`,
|
|
28
|
-
* `*.blob.core.windows.net`, `*.amazonaws.com`). Every one of those is a
|
|
29
|
-
* bucket an attacker can create in their own account, so allowlisting them
|
|
30
|
-
* hands back exactly the exfiltration channel the policy exists to close. A
|
|
31
|
-
* deployment that genuinely needs one adds it through `extraHosts` as an
|
|
32
|
-
* explicit, auditable decision.
|
|
33
|
-
*
|
|
34
|
-
* This module is data + string handling only, so it runs unchanged inside the
|
|
35
|
-
* hosted Worker (no DNS, no node builtins). That matters: name-based policy is
|
|
36
|
-
* the only kind the Worker can compute, which is why `egressControl` treats
|
|
37
|
-
* name filtering as the capability that qualifies a driver for hosted use.
|
|
38
|
-
*/
|
|
39
1
|
import type { SandboxSource } from "./contract";
|
|
40
2
|
import type { SandboxNetworkPolicy } from "./egress-policy";
|
|
41
|
-
/**
|
|
42
|
-
* Model-provider endpoints. Derived from the harness→provider groups in
|
|
43
|
-
* claxedo-server `network/types.ts` (`PROVIDER_TO_GROUP`): claude-sdk →
|
|
44
|
-
* anthropic, codex-app-server/cursor-sdk → openai,
|
|
45
|
-
* google → google_ai; plus the aggregator and catalog endpoints the engine
|
|
46
|
-
* itself fetches at startup.
|
|
47
|
-
*/
|
|
48
3
|
export declare const SANDBOX_MODEL_PROVIDER_HOSTS: readonly ["api.anthropic.com", "api.openai.com", "generativelanguage.googleapis.com", "openrouter.ai", "models.dev", "opencode.ai"];
|
|
49
4
|
/**
|
|
50
|
-
*
|
|
51
|
-
* `
|
|
52
|
-
*
|
|
53
|
-
*
|
|
5
|
+
* Object-storage and CDN wildcards (`*.workers.dev`, `*.amazonaws.com`,
|
|
6
|
+
* `storage.googleapis.com`) stay out: each is a bucket an attacker can create
|
|
7
|
+
* in their own account, which reopens the exfiltration channel the allowlist
|
|
8
|
+
* closes. A deployment that needs one adds it through `extraHosts`.
|
|
54
9
|
*/
|
|
55
10
|
export declare const SANDBOX_PACKAGE_REGISTRY_HOSTS: readonly ["registry.npmjs.org", "nodejs.org", "pypi.org", "files.pythonhosted.org", "crates.io", "static.crates.io", "index.crates.io", "proxy.golang.org", "sum.golang.org"];
|
|
56
|
-
/** The deployment-independent floor: providers + registries. */
|
|
57
11
|
export declare const SANDBOX_BASELINE_EGRESS_HOSTS: readonly ["api.anthropic.com", "api.openai.com", "generativelanguage.googleapis.com", "openrouter.ai", "models.dev", "opencode.ai", "registry.npmjs.org", "nodejs.org", "pypi.org", "files.pythonhosted.org", "crates.io", "static.crates.io", "index.crates.io", "proxy.golang.org", "sum.golang.org"];
|
|
58
12
|
/**
|
|
59
|
-
* The
|
|
60
|
-
*
|
|
61
|
-
* Scoped to the ONE repository host in the request rather than to a forge
|
|
62
|
-
* allowlist, matching how `githubCloneSecret` scopes the clone
|
|
63
|
-
* credential it brokers (`hosts: ["github.com"]`) — the credential and the
|
|
64
|
-
* network policy should describe the same reachable surface.
|
|
13
|
+
* The one repository host the request clones from, matching the hosts the
|
|
14
|
+
* brokered clone credential is scoped to.
|
|
65
15
|
*
|
|
66
16
|
* The host is caller-selected, so only a public spelling earns an allowlist
|
|
67
17
|
* entry: `169.254.169.254` or `localhost` named as a repoUrl would otherwise
|
|
@@ -78,17 +28,13 @@ export type HostedSandboxNetworkPolicyInput = {
|
|
|
78
28
|
* plus register/heartbeat. URLs or bare hostnames; blanks are ignored.
|
|
79
29
|
*/
|
|
80
30
|
controlPlane: Array<string | undefined>;
|
|
81
|
-
/**
|
|
31
|
+
/** Its git host is allowed, nothing else on that forge. */
|
|
82
32
|
source?: SandboxSource;
|
|
83
|
-
/** Operator escape hatch for deployment-specific hosts. */
|
|
84
33
|
extraHosts?: readonly string[];
|
|
85
34
|
};
|
|
86
35
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* allow-all — a hosted sandbox with an empty or unresolvable control-plane URL
|
|
91
|
-
* is a misconfiguration, and answering it with "then allow everything" is how
|
|
92
|
-
* the gap this closes came to exist in the first place.
|
|
36
|
+
* Always `mode: "restricted"`: a hosted sandbox with an empty or unresolvable
|
|
37
|
+
* control-plane URL is a misconfiguration, never a reason to allow everything.
|
|
38
|
+
* Name-based only (no DNS), so it also runs inside the Worker.
|
|
93
39
|
*/
|
|
94
40
|
export declare function hostedSandboxNetworkPolicy(input: HostedSandboxNetworkPolicyInput): SandboxNetworkPolicy;
|
|
@@ -221,7 +221,7 @@ var SANDBOX_MODEL_PROVIDER_HOSTS = [
|
|
|
221
221
|
"api.openai.com",
|
|
222
222
|
"generativelanguage.googleapis.com",
|
|
223
223
|
"openrouter.ai",
|
|
224
|
-
//
|
|
224
|
+
// The model catalog and engine config a runner fetches at startup.
|
|
225
225
|
"models.dev",
|
|
226
226
|
"opencode.ai"
|
|
227
227
|
];
|
package/dist/image-name.d.ts
CHANGED
|
@@ -13,5 +13,4 @@ export declare function sandboxImageRepository(env?: SandboxImageEnv): string;
|
|
|
13
13
|
export declare function defaultSandboxImage(version?: string, buildId?: string, env?: SandboxImageEnv, options?: {
|
|
14
14
|
agentPlugins?: boolean;
|
|
15
15
|
}): string;
|
|
16
|
-
/** Whether an image's tag names a build that carries the Agent Plugins runtime entry. */
|
|
17
16
|
export declare function sandboxImageCarriesAgentPlugins(image: string): boolean;
|
package/dist/index.mjs
CHANGED
|
@@ -83,9 +83,9 @@ function sandboxNetworkPolicyWithBrokeredHosts(policy, secrets) {
|
|
|
83
83
|
...hosts.length ? { hosts } : {}
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
|
-
function sandboxEgressDisposition(control, net) {
|
|
86
|
+
function sandboxEgressDisposition(control, net, uncontained = "refuse") {
|
|
87
87
|
if (!net || net.mode === "allow-all") return { action: "enforce" };
|
|
88
|
-
if (control === "none") return { action: "withhold", reason: "sandbox_egress_uncontained" };
|
|
88
|
+
if (control === "none") return { action: uncontained === "allow" ? "withhold" : "refuse", reason: "sandbox_egress_uncontained" };
|
|
89
89
|
const hosts = net.hosts?.length ?? 0;
|
|
90
90
|
const cidrs = net.cidrs?.length ?? 0;
|
|
91
91
|
if (hosts === 0 && cidrs === 0) return { action: "enforce" };
|
|
@@ -110,6 +110,27 @@ var boatBilling = {
|
|
|
110
110
|
large: { size: "large", vcpu: 8, memoryGb: 16, usdPerHour: 0.072 }
|
|
111
111
|
}
|
|
112
112
|
};
|
|
113
|
+
var AGENT37_USD_PER_MONTH = { vcpu: 0.8, memoryGb: 0.7, diskGb: 0.09 };
|
|
114
|
+
var AGENT37_MONTH_HOURS = 730;
|
|
115
|
+
var agent37MachineShapes = {
|
|
116
|
+
small: { cpu: 2, memory: 4, disk: 10 },
|
|
117
|
+
default: { cpu: 4, memory: 8, disk: 20 },
|
|
118
|
+
large: { cpu: 4, memory: 16, disk: 40 }
|
|
119
|
+
};
|
|
120
|
+
function agent37Rate(shape) {
|
|
121
|
+
const usdPerMonth = shape.cpu * AGENT37_USD_PER_MONTH.vcpu + shape.memory * AGENT37_USD_PER_MONTH.memoryGb + shape.disk * AGENT37_USD_PER_MONTH.diskGb;
|
|
122
|
+
return { size: `${shape.cpu} vCPU / ${shape.memory} GB`, vcpu: shape.cpu, memoryGb: shape.memory, usdPerHour: usdPerMonth / AGENT37_MONTH_HOURS };
|
|
123
|
+
}
|
|
124
|
+
var agent37Billing = {
|
|
125
|
+
source: { url: "https://www.agent37.com/docs/agents-api/billing", readOn: "2026-11-11" },
|
|
126
|
+
meter: "Agent37 meters an instance per minute at a monthly rate over 730 hours: $0.80 per vCPU, $0.70 per GB of memory and $0.09 per GB of disk while it runs, and the disk alone while it is stopped.",
|
|
127
|
+
rates: {
|
|
128
|
+
small: agent37Rate(agent37MachineShapes.small),
|
|
129
|
+
default: agent37Rate(agent37MachineShapes.default),
|
|
130
|
+
large: agent37Rate(agent37MachineShapes.large)
|
|
131
|
+
},
|
|
132
|
+
excludes: `the disk of a stopped instance, billed at $${AGENT37_USD_PER_MONTH.diskGb} per GB a month until the instance is deleted`
|
|
133
|
+
};
|
|
113
134
|
var CLOUDFLARE_USD_PER_GIB_SECOND_MEMORY = 25e-7;
|
|
114
135
|
var CLOUDFLARE_USD_PER_GB_SECOND_DISK = 7e-8;
|
|
115
136
|
var CLOUDFLARE_USD_PER_VCPU_SECOND_ACTIVE = 2e-5;
|
|
@@ -318,12 +339,13 @@ var sandboxCheckpointUnusableCauses = ["expired", "runtime_modified", "not_resto
|
|
|
318
339
|
function isSandboxCheckpointUnusableCause(value) {
|
|
319
340
|
return sandboxCheckpointUnusableCauses.some((cause) => cause === value);
|
|
320
341
|
}
|
|
321
|
-
var sandboxDriverIds = ["modal", "vercel", "cloudflare", "boat", "docker"];
|
|
342
|
+
var sandboxDriverIds = ["modal", "vercel", "cloudflare", "boat", "agent37", "docker"];
|
|
322
343
|
var sandboxDriverLabels = {
|
|
323
344
|
modal: "Modal",
|
|
324
345
|
vercel: "Vercel",
|
|
325
346
|
cloudflare: "Cloudflare",
|
|
326
347
|
boat: "Boat",
|
|
348
|
+
agent37: "Agent37",
|
|
327
349
|
docker: "Docker"
|
|
328
350
|
};
|
|
329
351
|
function isSandboxDriverID(input) {
|
|
@@ -434,6 +456,13 @@ function sandboxDriverLabel(driver) {
|
|
|
434
456
|
}
|
|
435
457
|
var PROVIDER_KEY_REFUSED = "provider_key_refused:";
|
|
436
458
|
var PROVIDER_FAILED = "provider_failed:";
|
|
459
|
+
var EGRESS_UNCONTAINED = "sandbox_egress_uncontained:";
|
|
460
|
+
function sandboxEgressUncontainedError(driver) {
|
|
461
|
+
return `${EGRESS_UNCONTAINED}${driver}`;
|
|
462
|
+
}
|
|
463
|
+
function uncontainedEgressReason(provider) {
|
|
464
|
+
return `${provider} cannot contain a sandbox's network; an org admin can allow unrestricted internet for ${provider} machines in Settings \u2192 Sandbox providers.`;
|
|
465
|
+
}
|
|
437
466
|
function sandboxProviderKeyRefusedError(driver, message) {
|
|
438
467
|
return `${PROVIDER_KEY_REFUSED}${driver}: ${message}`;
|
|
439
468
|
}
|
|
@@ -524,10 +553,8 @@ function sandboxRuntimeBootFailure(error) {
|
|
|
524
553
|
return workspaceRuntimeBootReason(diagnostic) ?? diagnostic;
|
|
525
554
|
}
|
|
526
555
|
var SANDBOX_RUNTIME_UNHEALTHY = "runtime_unhealthy";
|
|
527
|
-
var SANDBOX_RETRY_CAP_EXCEEDED = "runtime_retry_cap_exceeded";
|
|
528
556
|
var CODED_REASONS = /* @__PURE__ */ new Map([
|
|
529
557
|
[SANDBOX_RUNTIME_UNHEALTHY, "The workspace machine stopped answering."],
|
|
530
|
-
[SANDBOX_RETRY_CAP_EXCEEDED, "Several tries in a row failed; try again later."],
|
|
531
558
|
[SANDBOX_UPDATE_DOWNLOAD_INTERRUPTED, "The update couldn't finish downloading; your workspace is still running. Try again."]
|
|
532
559
|
]);
|
|
533
560
|
function firstLine(text) {
|
|
@@ -542,6 +569,8 @@ function sandboxStartFailureReason(error) {
|
|
|
542
569
|
if (keyRefused !== void 0) return `${sandboxDriverLabel(keyRefused)} refused the sandbox provider key; fix it in Settings \u2192 Sandbox providers.`;
|
|
543
570
|
const failed = codedDriver(error, PROVIDER_FAILED);
|
|
544
571
|
if (failed !== void 0) return `${sandboxDriverLabel(failed)} returned an error.`;
|
|
572
|
+
const uncontained = codedDriver(error, EGRESS_UNCONTAINED);
|
|
573
|
+
if (uncontained !== void 0) return uncontainedEgressReason(sandboxDriverLabel(uncontained));
|
|
545
574
|
return CODED_REASONS.get(error) ?? firstLine(error);
|
|
546
575
|
}
|
|
547
576
|
var LEASE_ENDED_ERRORS = /* @__PURE__ */ new Set(["runtime_lease_changed", "runtime_lease_stopped", "runtime_lease_destroyed"]);
|
|
@@ -587,6 +616,11 @@ var PUBLIC_API_ERRORS = {
|
|
|
587
616
|
org_invitation_invalid: { family: "access", status: 409, message: "Invitation is invalid or unavailable" },
|
|
588
617
|
org_invitation_pending: { family: "access", status: 409, message: "An invitation to this address is already pending" },
|
|
589
618
|
org_invitation_rate_limited: { family: "access", status: 429, message: "Organization invitation limit exceeded" },
|
|
619
|
+
org_limit_reached: { family: "access", status: 403, message: "An account owns at most 3 organizations" },
|
|
620
|
+
org_creation_rate_limited: { family: "access", status: 429, message: "Organization creation limit exceeded" },
|
|
621
|
+
project_limit_reached: { family: "access", status: 402, message: "Free accounts have 3 projects. Get Cloud or Self-host for unlimited projects." },
|
|
622
|
+
seats_full: { family: "access", status: 402, message: "Add teammate seats to invite more people." },
|
|
623
|
+
email_verification_required: { family: "access", status: 403, message: "A verified email address is required" },
|
|
590
624
|
org_invitation_email_mismatch: { family: "access", status: 403, message: "Sign in with the invited verified email address" },
|
|
591
625
|
org_invitation_delivery_unavailable: { family: "access", status: 503, message: "Invitation delivery is unavailable" },
|
|
592
626
|
team_not_found: { family: "access", status: 404, message: "Team not found" },
|
|
@@ -596,6 +630,12 @@ var PUBLIC_API_ERRORS = {
|
|
|
596
630
|
project_member_not_found: { family: "access", status: 404, message: "Project member not found" },
|
|
597
631
|
project_member_owner_immutable: { family: "access", status: 409, message: "The project owner's access cannot be changed" },
|
|
598
632
|
resource_conflict: { family: "access", status: 409, message: "Organization or team authority changed concurrently" },
|
|
633
|
+
org_not_deletable: { family: "access", status: 409, message: "A personal organization goes with its account, and a deployment's with the deployment" },
|
|
634
|
+
org_has_workspaces: { family: "access", status: 409, message: "Delete the organization's workspaces first" },
|
|
635
|
+
account_owns_workspaces: { family: "access", status: 409, message: "Delete your workspaces first" },
|
|
636
|
+
account_founds_organization: { family: "access", status: 409, message: "Delete the organizations you founded first" },
|
|
637
|
+
account_has_cloud_subscription: { family: "access", status: 409, message: "Cancel Cloud before deleting this account" },
|
|
638
|
+
recent_sign_in_required: { family: "access", status: 403, message: "Sign in again to confirm this deletion" },
|
|
599
639
|
session_not_found: { family: "session_share", status: 404, message: "This session is not on the control plane, so it cannot be shared from People yet." },
|
|
600
640
|
session_share_admin_required: { family: "session_share", status: 403, message: "Only the person who started this session can manage its People." },
|
|
601
641
|
session_share_target_required: { family: "session_share", status: 400, message: "A session is shared with exactly one person" },
|
|
@@ -799,7 +839,7 @@ var SANDBOX_MODEL_PROVIDER_HOSTS = [
|
|
|
799
839
|
"api.openai.com",
|
|
800
840
|
"generativelanguage.googleapis.com",
|
|
801
841
|
"openrouter.ai",
|
|
802
|
-
//
|
|
842
|
+
// The model catalog and engine config a runner fetches at startup.
|
|
803
843
|
"models.dev",
|
|
804
844
|
"opencode.ai"
|
|
805
845
|
];
|
|
@@ -893,7 +933,7 @@ function leaseHostId(driver, workspaceId, lease, managerInput) {
|
|
|
893
933
|
return managerInput?.hostId ?? lease.hostId ?? sandboxId(driver.id, workspaceId);
|
|
894
934
|
}
|
|
895
935
|
function ensureHostInput(input) {
|
|
896
|
-
const egress = sandboxEgressDisposition(input.driver.metadata.egressControl, input.managerInput?.net);
|
|
936
|
+
const egress = sandboxEgressDisposition(input.driver.metadata.egressControl, input.managerInput?.net, input.managerInput?.uncontainedEgress);
|
|
897
937
|
const labels = {
|
|
898
938
|
app: input.appLabel,
|
|
899
939
|
workspaceId: input.workspaceId,
|
|
@@ -921,10 +961,10 @@ function ensureHostInput(input) {
|
|
|
921
961
|
...input.managerInput?.updateImage ? { updateImage: true } : {},
|
|
922
962
|
...input.lease.machineClass ?? input.managerInput?.machineClass ? { machineClass: input.lease.machineClass ?? input.managerInput?.machineClass } : {},
|
|
923
963
|
exposure: input.managerInput?.exposure ?? (input.driver.metadata.targetAccess === "loopback" ? { kind: "loopback" } : { kind: "relay" }),
|
|
924
|
-
// Withheld, never downgraded: `undefined`
|
|
925
|
-
// that asked for no containment produces,
|
|
926
|
-
// their allow-all path and their throws stay
|
|
927
|
-
// them a policy directly.
|
|
964
|
+
// Withheld where the start allows it, never downgraded: `undefined`
|
|
965
|
+
// here is the same shape a caller that asked for no containment produces,
|
|
966
|
+
// so the throwing drivers take their allow-all path and their throws stay
|
|
967
|
+
// intact for anyone who hands them a policy directly.
|
|
928
968
|
...egress.action === "enforce" && input.managerInput?.net ? { net: sandboxNetworkPolicyWithBrokeredHosts(input.managerInput.net, input.managerInput.secrets) } : {},
|
|
929
969
|
snapshot: input.managerInput?.snapshot
|
|
930
970
|
};
|
|
@@ -932,11 +972,8 @@ function ensureHostInput(input) {
|
|
|
932
972
|
|
|
933
973
|
// src/egress-warnings.ts
|
|
934
974
|
function egressUnenforcedMessage(input) {
|
|
935
|
-
|
|
936
|
-
const withheld = input.requested ? ` The requested allowlist (${(input.requested.hosts ?? []).length} host(s), ${(input.requested.cidrs ?? []).length} cidr(s)) was withheld, not applied.` : "";
|
|
937
|
-
return `[sandbox-manager] SANDBOX EGRESS IS UNRESTRICTED: driver "${input.driver}" declares egressControl: "none", so ${scope} can reach ANY host on the internet, including attacker-controlled buckets \u2014 agent-authored code inside the sandbox has an unmonitored exfiltration path.${withheld} Select a driver that can enforce egress (vercel) to close it. See public-docs/sandbox-egress.md.`;
|
|
975
|
+
return `[sandbox-manager] SANDBOX EGRESS IS UNRESTRICTED: driver "${input.driver}" declares egressControl: "none" and this start allows uncontained egress, so workspace ${input.workspaceId} can reach ANY host on the internet, including attacker-controlled buckets \u2014 agent-authored code inside the sandbox has an unmonitored exfiltration path. The requested allowlist (${(input.requested.hosts ?? []).length} host(s), ${(input.requested.cidrs ?? []).length} cidr(s)) was withheld, not applied. Select a driver that can enforce egress (cloudflare, vercel), or stop allowing uncontained egress so such starts are refused. See public-docs/sandbox-egress.md.`;
|
|
938
976
|
}
|
|
939
|
-
var compositionEgressWarnings = /* @__PURE__ */ new Set();
|
|
940
977
|
var ensureEgressWarnings = /* @__PURE__ */ new Set();
|
|
941
978
|
function defaultEgressUnenforcedSink(event) {
|
|
942
979
|
console.warn(event.message);
|
|
@@ -945,26 +982,15 @@ function createEgressWarnings(input) {
|
|
|
945
982
|
const { id, egressControl } = input.driver;
|
|
946
983
|
const sink = input.sink ?? defaultEgressUnenforcedSink;
|
|
947
984
|
const key = `${id}|${egressControl}`;
|
|
948
|
-
if (egressControl === "none" && !compositionEgressWarnings.has(key)) {
|
|
949
|
-
compositionEgressWarnings.add(key);
|
|
950
|
-
sink({
|
|
951
|
-
phase: "composition",
|
|
952
|
-
reason: "sandbox_egress_uncontained",
|
|
953
|
-
driver: id,
|
|
954
|
-
egressControl,
|
|
955
|
-
message: egressUnenforcedMessage({ phase: "composition", driver: id })
|
|
956
|
-
});
|
|
957
|
-
}
|
|
958
985
|
return {
|
|
959
986
|
withheld(start) {
|
|
960
987
|
if (ensureEgressWarnings.has(key)) return;
|
|
961
988
|
ensureEgressWarnings.add(key);
|
|
962
989
|
sink({
|
|
963
|
-
phase: "ensure",
|
|
964
990
|
reason: "sandbox_egress_uncontained",
|
|
965
991
|
driver: id,
|
|
966
992
|
egressControl,
|
|
967
|
-
message: egressUnenforcedMessage({
|
|
993
|
+
message: egressUnenforcedMessage({ driver: id, workspaceId: start.workspaceId, requested: start.requested }),
|
|
968
994
|
workspaceId: start.workspaceId,
|
|
969
995
|
...start.labels ? { labels: start.labels } : {},
|
|
970
996
|
requested: start.requested
|
|
@@ -973,6 +999,51 @@ function createEgressWarnings(input) {
|
|
|
973
999
|
};
|
|
974
1000
|
}
|
|
975
1001
|
|
|
1002
|
+
// src/runtime-env.ts
|
|
1003
|
+
var WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS = [
|
|
1004
|
+
"WORKSPACE_RUNTIME_WORKSPACE_ID",
|
|
1005
|
+
"WORKSPACE_RUNTIME_HOST_ID",
|
|
1006
|
+
"WORKSPACE_RUNTIME_RELAY_WORKSPACE_IDS"
|
|
1007
|
+
];
|
|
1008
|
+
function workspaceRuntimeIdentityEnvConflicts(env) {
|
|
1009
|
+
if (!env) return [];
|
|
1010
|
+
return WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS.filter((key) => env[key] !== void 0);
|
|
1011
|
+
}
|
|
1012
|
+
var WORKSPACE_RUNTIME_PUBLIC_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
1013
|
+
...WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS,
|
|
1014
|
+
"WORKSPACE_RUNTIME_DIRECTORY",
|
|
1015
|
+
"WORKSPACE_RUNTIME_PORT",
|
|
1016
|
+
"WORKSPACE_RUNTIME_HOST",
|
|
1017
|
+
"WORKSPACE_RUNTIME_SOURCE_KIND",
|
|
1018
|
+
"WORKSPACE_RUNTIME_GIT_REPO_URL",
|
|
1019
|
+
"WORKSPACE_RUNTIME_GIT_BRANCH",
|
|
1020
|
+
"WORKSPACE_RUNTIME_NATIVE_HARNESS",
|
|
1021
|
+
"WORKSPACE_RUNTIME_RELAY_JWKS_URL",
|
|
1022
|
+
"WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL",
|
|
1023
|
+
"WORKSPACE_RUNTIME_SESSION_AUTHORITY_URL"
|
|
1024
|
+
]);
|
|
1025
|
+
|
|
1026
|
+
// src/start-admission.ts
|
|
1027
|
+
function refusedStart(input) {
|
|
1028
|
+
const { driver, workspaceId, start } = input;
|
|
1029
|
+
const egress = sandboxEgressDisposition(driver.metadata.egressControl, start.net, start.uncontainedEgress);
|
|
1030
|
+
if (egress.action === "withhold" && start.net) {
|
|
1031
|
+
input.egressWarnings.withheld({ workspaceId, ...start.labels ? { labels: start.labels } : {}, requested: start.net });
|
|
1032
|
+
}
|
|
1033
|
+
if (egress.action === "refuse") {
|
|
1034
|
+
const error = egress.reason === "sandbox_egress_uncontained" ? sandboxEgressUncontainedError(driver.id) : egress.reason;
|
|
1035
|
+
return { status: "unavailable", error, homeRegion: start.homeRegion };
|
|
1036
|
+
}
|
|
1037
|
+
const identityConflicts = workspaceRuntimeIdentityEnvConflicts(start.env);
|
|
1038
|
+
if (identityConflicts.length) {
|
|
1039
|
+
return { status: "unavailable", error: `sandbox env cannot set runtime identity: ${identityConflicts.join(", ")}`, homeRegion: start.homeRegion };
|
|
1040
|
+
}
|
|
1041
|
+
if (start.machineClass && !driver.metadata.machineClasses?.includes(start.machineClass)) {
|
|
1042
|
+
return { status: "unavailable", error: `sandbox_machine_class_unsupported: ${driver.id} has no ${start.machineClass} machines`, homeRegion: start.homeRegion };
|
|
1043
|
+
}
|
|
1044
|
+
return void 0;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
976
1047
|
// src/garbage-collect.ts
|
|
977
1048
|
async function sweepSandboxes(input) {
|
|
978
1049
|
const { driver, leaseStore, appLabel } = input;
|
|
@@ -1176,30 +1247,6 @@ async function startSandboxFresh(input) {
|
|
|
1176
1247
|
return { ok: true, status };
|
|
1177
1248
|
}
|
|
1178
1249
|
|
|
1179
|
-
// src/runtime-env.ts
|
|
1180
|
-
var WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS = [
|
|
1181
|
-
"WORKSPACE_RUNTIME_WORKSPACE_ID",
|
|
1182
|
-
"WORKSPACE_RUNTIME_HOST_ID",
|
|
1183
|
-
"WORKSPACE_RUNTIME_RELAY_WORKSPACE_IDS"
|
|
1184
|
-
];
|
|
1185
|
-
function workspaceRuntimeIdentityEnvConflicts(env) {
|
|
1186
|
-
if (!env) return [];
|
|
1187
|
-
return WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS.filter((key) => env[key] !== void 0);
|
|
1188
|
-
}
|
|
1189
|
-
var WORKSPACE_RUNTIME_PUBLIC_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
1190
|
-
...WORKSPACE_RUNTIME_IDENTITY_ENV_KEYS,
|
|
1191
|
-
"WORKSPACE_RUNTIME_DIRECTORY",
|
|
1192
|
-
"WORKSPACE_RUNTIME_PORT",
|
|
1193
|
-
"WORKSPACE_RUNTIME_HOST",
|
|
1194
|
-
"WORKSPACE_RUNTIME_SOURCE_KIND",
|
|
1195
|
-
"WORKSPACE_RUNTIME_GIT_REPO_URL",
|
|
1196
|
-
"WORKSPACE_RUNTIME_GIT_BRANCH",
|
|
1197
|
-
"WORKSPACE_RUNTIME_NATIVE_HARNESS",
|
|
1198
|
-
"WORKSPACE_RUNTIME_RELAY_JWKS_URL",
|
|
1199
|
-
"WORKSPACE_RUNTIME_MANAGEMENT_JWKS_URL",
|
|
1200
|
-
"WORKSPACE_RUNTIME_SESSION_AUTHORITY_URL"
|
|
1201
|
-
]);
|
|
1202
|
-
|
|
1203
1250
|
// src/manager.ts
|
|
1204
1251
|
var DEFAULT_APP_LABEL = "claxedo";
|
|
1205
1252
|
function createSandboxManager(options) {
|
|
@@ -1494,14 +1541,6 @@ function createSandboxManager(options) {
|
|
|
1494
1541
|
...existing.retryCount >= maxRetryCount ? { exhausted: true } : {}
|
|
1495
1542
|
};
|
|
1496
1543
|
}
|
|
1497
|
-
if (existing.status === "unavailable" && existing.retryCount >= maxRetryCount && existing.nextRetryAt === void 0 && sandboxProviderRateLimit(existing.lastError) !== "day") {
|
|
1498
|
-
return {
|
|
1499
|
-
status: "unavailable",
|
|
1500
|
-
error: existing.lastError ?? SANDBOX_RETRY_CAP_EXCEEDED,
|
|
1501
|
-
epoch: existing.epoch,
|
|
1502
|
-
homeRegion: existing.homeRegion
|
|
1503
|
-
};
|
|
1504
|
-
}
|
|
1505
1544
|
return void 0;
|
|
1506
1545
|
}
|
|
1507
1546
|
function continues(lease) {
|
|
@@ -1511,24 +1550,8 @@ function createSandboxManager(options) {
|
|
|
1511
1550
|
return lease.sandboxId ? lease.nextRetryAt !== void 0 || stale : lease.nextRetryAt !== void 0 && !stale;
|
|
1512
1551
|
}
|
|
1513
1552
|
async function admit(workspaceId, input) {
|
|
1514
|
-
const
|
|
1515
|
-
if (
|
|
1516
|
-
egressWarnings.withheld({ workspaceId, ...input.labels ? { labels: input.labels } : {}, requested: input.net });
|
|
1517
|
-
}
|
|
1518
|
-
if (egress.action === "refuse") {
|
|
1519
|
-
return { status: "unavailable", error: egress.reason, homeRegion: input.homeRegion };
|
|
1520
|
-
}
|
|
1521
|
-
const identityConflicts = workspaceRuntimeIdentityEnvConflicts(input.env);
|
|
1522
|
-
if (identityConflicts.length) {
|
|
1523
|
-
return {
|
|
1524
|
-
status: "unavailable",
|
|
1525
|
-
error: `sandbox env cannot set runtime identity: ${identityConflicts.join(", ")}`,
|
|
1526
|
-
homeRegion: input.homeRegion
|
|
1527
|
-
};
|
|
1528
|
-
}
|
|
1529
|
-
if (input.machineClass && !options.driver.metadata.machineClasses?.includes(input.machineClass)) {
|
|
1530
|
-
return { status: "unavailable", error: `sandbox_machine_class_unsupported: ${options.driver.id} has no ${input.machineClass} machines`, homeRegion: input.homeRegion };
|
|
1531
|
-
}
|
|
1553
|
+
const refused = refusedStart({ driver: options.driver, workspaceId, start: input, egressWarnings });
|
|
1554
|
+
if (refused) return refused;
|
|
1532
1555
|
const existing = await options.leaseStore.get(workspaceId);
|
|
1533
1556
|
const wait = waiting(existing);
|
|
1534
1557
|
if (wait) return wait;
|
|
@@ -1724,7 +1747,6 @@ export {
|
|
|
1724
1747
|
SANDBOX_IMAGE_LABEL,
|
|
1725
1748
|
SANDBOX_MODEL_PROVIDER_HOSTS,
|
|
1726
1749
|
SANDBOX_PACKAGE_REGISTRY_HOSTS,
|
|
1727
|
-
SANDBOX_RETRY_CAP_EXCEEDED,
|
|
1728
1750
|
SANDBOX_RUNTIME_UNHEALTHY,
|
|
1729
1751
|
SANDBOX_START_WINDOWS,
|
|
1730
1752
|
SANDBOX_UPDATE_DOWNLOAD_INTERRUPTED,
|
|
@@ -1735,6 +1757,8 @@ export {
|
|
|
1735
1757
|
SandboxProviderRateLimitedError,
|
|
1736
1758
|
SandboxRuntimeBootError,
|
|
1737
1759
|
SandboxStartRefusedError,
|
|
1760
|
+
agent37Billing,
|
|
1761
|
+
agent37MachineShapes,
|
|
1738
1762
|
applySandboxLeaseFailure,
|
|
1739
1763
|
applySandboxLeasePatch,
|
|
1740
1764
|
applySandboxProvisionedTarget,
|
|
@@ -1763,6 +1787,7 @@ export {
|
|
|
1763
1787
|
sandboxCheckpointUnusableCauses,
|
|
1764
1788
|
sandboxCheckpointUnusableError,
|
|
1765
1789
|
sandboxEgressDisposition,
|
|
1790
|
+
sandboxEgressUncontainedError,
|
|
1766
1791
|
sandboxHostGoneError,
|
|
1767
1792
|
sandboxLeaseEndedUnderStart,
|
|
1768
1793
|
sandboxLeaseFailure,
|
package/dist/json.d.ts
CHANGED
|
@@ -1,13 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Reading untrusted JSON: driver API responses, CLI output, stored records.
|
|
3
|
-
*
|
|
4
|
-
* Drivers all face the same problem — a provider hands back `any`, and the code
|
|
5
|
-
* needs a few named fields out of it. Narrowing through these keeps the check
|
|
6
|
-
* and the type in the same place, instead of each site asserting a shape it
|
|
7
|
-
* never verified. The object check is `@claxedo/helpers/guards`, re-exported
|
|
8
|
-
* under the names the drivers use.
|
|
9
|
-
*/
|
|
10
|
-
export { asRecord as record, isRecord } from "@claxedo/helpers/guards";
|
|
11
|
-
/** A non-empty string, or undefined — the shape almost every provider field wants. */
|
|
12
|
-
export declare function text(input: unknown): string | undefined;
|
|
13
|
-
export declare function num(input: unknown): number | undefined;
|
|
1
|
+
export { asNumber as num, asRecord as record, isRecord, nonEmptyString as text } from "@claxedo/helpers/guards";
|
package/dist/lease-failure.d.ts
CHANGED
|
@@ -91,6 +91,8 @@ export declare class SandboxStartRefusedError extends Error implements SandboxSt
|
|
|
91
91
|
readonly startRefused: true;
|
|
92
92
|
constructor(reason: string);
|
|
93
93
|
}
|
|
94
|
+
/** The error a start answers when `driver` declares no egress control and the start does not allow uncontained egress. */
|
|
95
|
+
export declare function sandboxEgressUncontainedError(driver: string): string;
|
|
94
96
|
/** The error a lease keeps when `driver`'s provider refused its key; the driver's message stays for the operator. */
|
|
95
97
|
export declare function sandboxProviderKeyRefusedError(driver: string, message: string): string;
|
|
96
98
|
/** The error a lease keeps when `driver` failed at its provider; the driver's message, which names hosts, requests and provider codes, stays for the operator. */
|
|
@@ -144,7 +146,7 @@ export declare function workspaceRuntimeBootReason(diagnostic: string): string |
|
|
|
144
146
|
/**
|
|
145
147
|
* What a driver keeps of a runtime's log within `maxChars`. A Node stack
|
|
146
148
|
* under the boot-failure line is longer than the bound a driver puts on the
|
|
147
|
-
* tail (600 chars on Boat
|
|
149
|
+
* tail (600 chars on Boat), so a tail alone keeps the
|
|
148
150
|
* frames and loses the one line that names the reason.
|
|
149
151
|
*/
|
|
150
152
|
export declare function runtimeBootDiagnostic(log: string, maxChars: number): string;
|
|
@@ -156,8 +158,6 @@ export declare function runtimeBootDiagnostic(log: string, maxChars: number): st
|
|
|
156
158
|
export declare function sandboxRuntimeBootFailure(error: string | undefined): string | undefined;
|
|
157
159
|
/** The error a lease keeps when the runtime it serves stopped answering its health reports. */
|
|
158
160
|
export declare const SANDBOX_RUNTIME_UNHEALTHY = "runtime_unhealthy";
|
|
159
|
-
/** The error a capped lease answers when it kept none of its own. */
|
|
160
|
-
export declare const SANDBOX_RETRY_CAP_EXCEEDED = "runtime_retry_cap_exceeded";
|
|
161
161
|
/**
|
|
162
162
|
* The one line a person is told for a start step's error: the runtime's own
|
|
163
163
|
* reason for a boot that failed, the provider's refusal in Claxedo's words,
|
package/dist/lease-types.d.ts
CHANGED
|
@@ -6,16 +6,7 @@ export type SandboxLeaseRow = {
|
|
|
6
6
|
home_region?: string;
|
|
7
7
|
epoch: number;
|
|
8
8
|
status: SandboxLeaseRowStatus;
|
|
9
|
-
/**
|
|
10
|
-
* The id the driver was registered with, which is any string: the manager
|
|
11
|
-
* dispatches on whatever a composition registers (`stores/d1.test.ts`
|
|
12
|
-
* provisions one called `"test-provider"`), and every other type this value
|
|
13
|
-
* flows between — `SandboxLease["driver"]`, `SandboxLeaseAcquireInput["driver"]`,
|
|
14
|
-
* `SandboxTarget["driver"]["id"]` — is already `string`. Typing the row as the
|
|
15
|
-
* closed `SandboxDriverID` made it the odd one out and forced every store to
|
|
16
|
-
* assert into it. Narrow with `isSandboxDriverID` where a CATALOG entry is
|
|
17
|
-
* needed, which is the only place the closed set is the right question.
|
|
18
|
-
*/
|
|
9
|
+
/** Any string: the manager dispatches on whatever a composition registers. Narrow with `isSandboxDriverID` where a catalog entry is needed. */
|
|
19
10
|
driver: string;
|
|
20
11
|
driver_resource_id: string | null;
|
|
21
12
|
driver_snapshot_id: string | null;
|
package/dist/runtime-env.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { SandboxDriverID } from "@claxedo/sandbox-contract";
|
|
2
1
|
export type WorkspaceRuntimeControlEnv = {
|
|
3
2
|
relayJwksUrl?: string;
|
|
4
3
|
relayVerifyPem?: string;
|
|
@@ -8,6 +7,12 @@ export type WorkspaceRuntimeControlEnv = {
|
|
|
8
7
|
export declare function workspaceRuntimeIdentityEnvConflicts(env: Record<string, string> | undefined): string[];
|
|
9
8
|
export declare function assertWorkspaceRuntimeIdentityEnv(env: Record<string, string> | undefined): void;
|
|
10
9
|
export declare function workspaceRuntimeBootSecrets(env: Record<string, string>): string[];
|
|
10
|
+
/**
|
|
11
|
+
* `text` with every boot secret in it replaced. Redact before bounding the
|
|
12
|
+
* text, so a secret the bound would cut in half is already gone. Values
|
|
13
|
+
* shorter than the floor (ports, flags) are left readable.
|
|
14
|
+
*/
|
|
15
|
+
export declare function redactWorkspaceRuntimeBootSecrets(text: string, secrets: readonly string[]): string;
|
|
11
16
|
export declare function workspaceRuntimeBootEnv(input: Parameters<typeof workspaceRuntimeTargetEnv>[0] & Parameters<typeof workspaceRuntimeSourceEnv>[0] & {
|
|
12
17
|
env?: Record<string, string>;
|
|
13
18
|
nativeHarness?: string;
|
|
@@ -29,9 +34,3 @@ export declare function workspaceRuntimeSourceEnv(input: {
|
|
|
29
34
|
kind: "empty";
|
|
30
35
|
};
|
|
31
36
|
}): Record<string, string>;
|
|
32
|
-
export declare function workspaceRuntimeServiceExposureEnv(input: {
|
|
33
|
-
driver: SandboxDriverID;
|
|
34
|
-
source: "driver-service-url";
|
|
35
|
-
access: "private" | "public" | "driver-authenticated" | "unknown";
|
|
36
|
-
note?: string;
|
|
37
|
-
}): Record<string, string>;
|
package/dist/runtime-env.mjs
CHANGED
|
@@ -31,6 +31,10 @@ var WORKSPACE_RUNTIME_PUBLIC_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
|
31
31
|
function workspaceRuntimeBootSecrets(env) {
|
|
32
32
|
return Object.entries(env).flatMap(([key, value]) => WORKSPACE_RUNTIME_PUBLIC_ENV_KEYS.has(key) ? [] : [value]);
|
|
33
33
|
}
|
|
34
|
+
var REDACTED_VALUE_MIN_LENGTH = 8;
|
|
35
|
+
function redactWorkspaceRuntimeBootSecrets(text, secrets) {
|
|
36
|
+
return secrets.filter((secret) => secret.length >= REDACTED_VALUE_MIN_LENGTH).reduce((output, secret) => output.split(secret).join("[redacted]"), text);
|
|
37
|
+
}
|
|
34
38
|
function workspaceRuntimeBootEnv(input) {
|
|
35
39
|
assertWorkspaceRuntimeIdentityEnv(input.env);
|
|
36
40
|
const env = {
|
|
@@ -62,21 +66,12 @@ function workspaceRuntimeSourceEnv(input) {
|
|
|
62
66
|
...input.source.branch ? { WORKSPACE_RUNTIME_GIT_BRANCH: input.source.branch } : {}
|
|
63
67
|
};
|
|
64
68
|
}
|
|
65
|
-
function workspaceRuntimeServiceExposureEnv(input) {
|
|
66
|
-
return {
|
|
67
|
-
...input.driver === "docker" ? { WORKSPACE_RUNTIME_HOST: "0.0.0.0" } : {},
|
|
68
|
-
WORKSPACE_RUNTIME_SERVICE_EXPOSURE_SOURCE: input.source,
|
|
69
|
-
WORKSPACE_RUNTIME_SERVICE_EXPOSURE_ACCESS: input.access,
|
|
70
|
-
WORKSPACE_RUNTIME_SERVICE_EXPOSURE_DRIVER: input.driver,
|
|
71
|
-
...input.note ? { WORKSPACE_RUNTIME_SERVICE_EXPOSURE_NOTE: input.note } : {}
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
69
|
export {
|
|
75
70
|
assertWorkspaceRuntimeIdentityEnv,
|
|
71
|
+
redactWorkspaceRuntimeBootSecrets,
|
|
76
72
|
workspaceRuntimeBootEnv,
|
|
77
73
|
workspaceRuntimeBootSecrets,
|
|
78
74
|
workspaceRuntimeIdentityEnvConflicts,
|
|
79
|
-
workspaceRuntimeServiceExposureEnv,
|
|
80
75
|
workspaceRuntimeSourceEnv,
|
|
81
76
|
workspaceRuntimeTargetEnv
|
|
82
77
|
};
|
|
@@ -22,3 +22,7 @@ export declare const SERVED_WORKSPACE_RUNTIME_FLOOR: {
|
|
|
22
22
|
readonly image: "ghcr.io/kyashrathore/claxedo-sandbox:workspace-runtime-0-10-1-40706b7356-agent-plugins-v8";
|
|
23
23
|
};
|
|
24
24
|
export declare const MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION: "0.10.1";
|
|
25
|
+
/** Where the sandbox image keeps the version of the runtime it ships, from the image's root. */
|
|
26
|
+
export declare const SANDBOX_IMAGE_RUNTIME_VERSION_FILE = "opt/workspace-runtime/workspace-runtime-version";
|
|
27
|
+
/** A shell test that passes when the runtime version in `file`, a shell word, is one this control plane serves. */
|
|
28
|
+
export declare function servedRuntimeVersionTest(file: string): string;
|
package/dist/runtime-version.mjs
CHANGED
|
@@ -8,8 +8,14 @@ var SERVED_WORKSPACE_RUNTIME_FLOOR = {
|
|
|
8
8
|
image: "ghcr.io/kyashrathore/claxedo-sandbox:workspace-runtime-0-10-1-40706b7356-agent-plugins-v8"
|
|
9
9
|
};
|
|
10
10
|
var MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION = SERVED_WORKSPACE_RUNTIME_FLOOR.version;
|
|
11
|
+
var SANDBOX_IMAGE_RUNTIME_VERSION_FILE = "opt/workspace-runtime/workspace-runtime-version";
|
|
12
|
+
function servedRuntimeVersionTest(file) {
|
|
13
|
+
return `version=$(cat ${file} 2>/dev/null) && [ -n "$version" ] && [ "$(printf '%s\\n%s\\n' ${MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION} "$version" | sort -V | head -n 1)" = ${MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION} ]`;
|
|
14
|
+
}
|
|
11
15
|
export {
|
|
12
16
|
MINIMUM_SERVED_WORKSPACE_RUNTIME_VERSION,
|
|
17
|
+
SANDBOX_IMAGE_RUNTIME_VERSION_FILE,
|
|
13
18
|
SERVED_WORKSPACE_RUNTIME_FLOOR,
|
|
19
|
+
servedRuntimeVersionTest,
|
|
14
20
|
workspaceRuntimeVersion
|
|
15
21
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { SandboxDriver, SandboxEnsureResult, SandboxManagerInput } from "./contract";
|
|
2
|
+
import type { EgressWarnings } from "./egress-warnings";
|
|
3
|
+
export type SandboxStartRefusal = Extract<SandboxEnsureResult, {
|
|
4
|
+
status: "unavailable";
|
|
5
|
+
}>;
|
|
6
|
+
/**
|
|
7
|
+
* The composition's standing answers to a start's input, decided BEFORE a
|
|
8
|
+
* lease is touched: none of them is a provisioning failure, so none may burn a
|
|
9
|
+
* lease epoch or enter retry backoff, and each repeats until the composition
|
|
10
|
+
* or the input changes.
|
|
11
|
+
*
|
|
12
|
+
* Egress: a caller that hands over a restricted policy is stating that this
|
|
13
|
+
* sandbox must not reach the open internet. That is honoured where the driver
|
|
14
|
+
* can honour it. Where it cannot, the start is refused, unless the start
|
|
15
|
+
* allows uncontained egress: then the sandbox runs with the policy withheld
|
|
16
|
+
* (`ensureHostInput` never hands it down) and the gap is reported here, once
|
|
17
|
+
* per driver per isolate. `sandbox_egress_policy_unenforceable` is refused too,
|
|
18
|
+
* and deliberately so: that driver DOES enforce egress, it just cannot express
|
|
19
|
+
* this policy's encoding, and degrading an enforcing driver to "unrestricted"
|
|
20
|
+
* would weaken the path that does enforce.
|
|
21
|
+
*
|
|
22
|
+
* Identity: caller env restating the identity the driver's target reports,
|
|
23
|
+
* which `recordTarget` persists, would boot the runtime bound to a hostId the
|
|
24
|
+
* lease never authorized; the driver rejects it again at compose time for
|
|
25
|
+
* callers that reach it directly.
|
|
26
|
+
*/
|
|
27
|
+
export declare function refusedStart(input: {
|
|
28
|
+
driver: SandboxDriver;
|
|
29
|
+
workspaceId: string;
|
|
30
|
+
start: SandboxManagerInput;
|
|
31
|
+
egressWarnings: EgressWarnings;
|
|
32
|
+
}): SandboxStartRefusal | undefined;
|