Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
99c4850
Seal preparation input to its Environment and share the Environment c…
SaladDay Nov 5, 2026
a7ed1e5
Create and list standalone Environments
SaladDay Nov 5, 2026
3362178
Attach a Session to a standalone Environment
SaladDay Nov 5, 2026
917b1b0
Run a standalone Environment's lifecycle without a Session
SaladDay Nov 5, 2026
7cd92f3
Project the attached Environment in an Automation target
SaladDay Nov 5, 2026
cd79e54
Drop the obsolete environment_id unsupported-field case
SaladDay Nov 5, 2026
3dea464
Document standalone Environments
SaladDay Nov 5, 2026
03dd07a
Test standalone Environments and fix their reservation
SaladDay Nov 5, 2026
62ab846
Replay a standalone create before resolving its Template; allow 256-b…
SaladDay Nov 5, 2026
200f27a
Expire unattached standalone Environments on a sandbox reset
SaladDay Nov 5, 2026
f22eb81
Fix review findings on standalone Environments
SaladDay Nov 5, 2026
a59b23b
End detached standalone Environments, delete ended workspaces and att…
SaladDay Nov 5, 2026
b88a79f
Scope the standalone Environment list to its creator
SaladDay Nov 5, 2026
8dc8f3b
Make the idle timeout one deployment setting for every Provider
SaladDay Nov 5, 2026
728326b
Fix attach and create error members, accept keys as sent, drop the un…
SaladDay Nov 5, 2026
bf84f5d
Test detach, workspace deletion, lock order, creator scoping and conc…
SaladDay Nov 5, 2026
b20c1a6
Use one private-address classifier and tolerate Sessions without an E…
SaladDay Nov 5, 2026
ee2f87d
Document the idle timeout, detach expiry, creator-scoped list and pag…
SaladDay Nov 5, 2026
b8b2e51
Keep merged migration 000014 unchanged, drop the unused Harness step …
SaladDay Nov 5, 2026
482e1e8
Give the Web acceptance fixtures the top-level idle timeout
SaladDay Nov 5, 2026
d19a158
Release failed unattached compute on every Provider, start the idle c…
SaladDay Nov 5, 2026
d1984ad
Release an ended Environment's placement on detach, admit standalone …
SaladDay Nov 5, 2026
c77f4e0
Align standalone Environment documentation with the idle timeout beha…
SaladDay Nov 5, 2026
4c272c6
Derive Environment liveness once and end unattached Environments at t…
SaladDay Nov 6, 2026
68f3460
Release the placements of inactive Environments in one sweep
SaladDay Nov 6, 2026
03bd512
Reject an empty Idempotency-Key and test the placement sweep order
SaladDay Nov 6, 2026
5618b37
Correct the standalone Environment documentation
SaladDay Nov 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/api-acceptance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ jobs:
run: |
python services/core/tests/official_schema_test.py
python services/core/tests/official_client.py
go test ./services/core/tests/integration -run '^(TestFunctionStateOfficialClientReadsAndLiveEvents|TestSavedReferenceRetryOfficialClient|TestAgentUpdateOfficialClient|TestAgentDeletionOfficialClient|TestAutomationsOfficialClient|TestSessionAgentFilterOfficialClient|TestSessionDeletionOfficialClient|TestEnvironmentInitialFailureOfficialClient|TestSelfHostedInitialCreationOfficialClient|TestSelfHostedCancellationOfficialClient)$' -count=1
go test ./services/core/tests/integration -run '^(TestFunctionStateOfficialClientReadsAndLiveEvents|TestSavedReferenceRetryOfficialClient|TestAgentUpdateOfficialClient|TestAgentDeletionOfficialClient|TestAutomationsOfficialClient|TestSessionAgentFilterOfficialClient|TestSessionDeletionOfficialClient|TestEnvironmentInitialFailureOfficialClient|TestSelfHostedInitialCreationOfficialClient|TestSelfHostedCancellationOfficialClient|TestStandaloneEnvironmentOfficialClientPostgres)$' -count=1
- uses: ./.github/actions/e2b-provider
if: inputs.container
- name: Verify the distribution's Core image
Expand Down
2 changes: 1 addition & 1 deletion apps/web/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ Settings is the single platform configuration entry, with one tab per [group](..

The hierarchy is page, tab, section, row. The page header loses its rule to an underline tab row: 40px text tabs in Graphite 13px/500, the current one Ink with a 2px Ink underline. Below 640px the tabs wrap onto a second row rather than scroll out of sight. The body is a left-aligned column at most 880px wide with 32px between sections. A section is the shared Section heading (14px/600, help tip, section-wide outline actions on the right) over one card of label/value rows (`settings.css`): a 200px Graphite label column with its help tip and an Ink value column, 8px by 16px padding, 32px minimum line height, Faint Rules between rows; below 640px the label sits above the value. An action that changes one value sits at the end of its row (Edit tenant capacity; Save beside the idle-seconds field, whose error appears beneath it). Read-only status is a row too (Configuration rollout, Snapshot retention); tables sit in one framed card. Settings never repeats the KPI strips of Nodes or the metrics pages. The irreversible operation closes the page in its own section: a red-ringed card with one Ink line stating the consequence and a red-text outline button.

Sandbox and scheduling contains sandbox setup or Deployment provider (Provider, Each sandbox, the E2B connection rows and the rollout row, with Change resources in the section header), then Automatic suspension (the deployment-wide idle policy and read-only snapshot retention, shown only when the deployment declares suspension), then Reset. While a reset runs, its progress section leads the page with Force reset now and Cancel reset in its header.
Sandbox and scheduling contains sandbox setup or Deployment provider (Provider, Each sandbox, the E2B connection rows and the rollout row, with Change resources in the section header), then the idle setting, always shown: Automatic suspension with checkpoint support (Idle before suspension (seconds) and read-only snapshot retention) or Idle timeout without it (Idle before expiry (seconds)), then Reset. While a reset runs, its progress section leads the page with Force reset now and Cancel reset in its header.

Workspace storage shows the selected configuration's adapter and tenant capacity (in bytes units, or No tenant capacity limit) under Storage configuration, with Select new configuration in the section header and Edit tenant capacity on the capacity row. The configuration ID and parameters JSON sit in a closed Configuration details disclosure at the end of the card. Explain adapter preparation and existing-workspace behavior behind help tips, with a link to the owning configuration document; keep required form labels and validation visible.

Expand Down
2 changes: 1 addition & 1 deletion apps/web/PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ The console runs beside the administrator's own Core, with execution, files and
- **Online sandbox configuration** changes the same backend's resources or E2B template without retiring existing nodes or changing existing Sessions' resource ownership. New placement follows Core's qualified capacity; saving a target does not promise immediate placement on it. Configuration rollout shows Core's target preparation and retained previous-generation sandbox count. A settled rollout can still have failed or unknown nodes and old resources. An offline node stays offline even when it has a recorded serving generation. Node and allocation detail distinguish the serving pin, target preparation and each resource's configuration generation.
- **E2B credential replacement** uses the same configuration form. Setup requires a key; leaving it blank during an update keeps the saved key. An explicit key, even the same value, is verified as a replacement and advances the target generation after successful verification. Another backend or E2B team requires a deliberate reset. A rejected or uncertain replacement never clears the committed configuration or replays the write.
- **E2B deployments** have no machines: Nodes offers a link to Settings → Sandbox and scheduling. Overview and Sandbox metrics show the sandboxes Core holds in E2B's cloud (running, starting or changing, and the size of each) instead of node capacity, with no Add node action; a sandbox's dialog adds its disk use.
- **Automatic suspension** appears when the deployment declares a suspension policy. Nodes shows each node's suspended count and retained limit, the most sandboxes it keeps running and suspended together, with Edit node for its limits; Overview adds the suspended total of all registered nodes under its sandbox slots. Settings → Sandbox and scheduling owns **Automatic suspension**: deployment-wide idle seconds are editable there; snapshot retention remains read-only. These are Core records, not measured CPU or memory savings. Node allocations show the recorded compute phase and elapsed phase time, never an inferred snapshot-expiry countdown. Sandbox metrics' Hosted Runtimes has one Status column: Core's lifecycle record of each sandbox (Active, Suspended, Changing state, Waiting for allocation or Stopped), which Core derives from the allocation's recorded phase. The Session page's Runtime state reads the same way. When an active sandbox has no sample, its CPU and Memory cells name why. Independent workspace persistence is explained only when the deployment declares it. Nodes refreshes its inventory every 30 seconds while visible; reset and active preparation retain their faster polling.
- With checkpoint support, Nodes shows each node's suspended count and retained limit, the most sandboxes it keeps running and suspended together, with Edit node for its limits; Overview adds the suspended total of all registered nodes under its sandbox slots. Settings → Sandbox and scheduling always shows the idle setting: **Automatic suspension** with checkpoint support (deployment-wide idle seconds are editable; snapshot retention remains read-only) and **Idle timeout** without it (idle seconds only). These are Core records, not measured CPU or memory savings. Node allocations show the recorded compute phase and elapsed phase time, never an inferred snapshot-expiry countdown. Sandbox metrics' Hosted Runtimes has one Status column: Core's lifecycle record of each sandbox (Active, Suspended, Changing state, Waiting for allocation or Stopped), which Core derives from the allocation's recorded phase. The Session page's Runtime state reads the same way. When an active sandbox has no sample, its CPU and Memory cells name why. Independent workspace persistence is explained only when the deployment declares it. Nodes refreshes its inventory every 30 seconds while visible; reset and active preparation retain their faster polling.
- **Getting started**: signing in opens the console on the Overview; nothing is forced first. While a step is to do, a Getting started checklist on the Overview shows four steps, in any order, each with its state and one action: sandboxes ready (a saved deployment and a node online and ready, or a saved E2B deployment), a default model provider on the default Harness (on any enabled Harness when none is default), a project with an active key, and a first Session, whose action opens the call samples of the newest active project, preferring one with an active key. Completion comes from reads the console already makes. It can be hidden; Show Getting started in the sidebar opens it again, and it ends with a brief "You're set". While it is open, Add node ends with the next step once its node is ready: the default model provider while that is to do, otherwise back to the checklist. The optional three-chapter tour of the console (Monitor, Resources, Platform) opens from it, on the sign-in stage.
- Terminology: Empty lists read "No … yet" ("还没有…"). Sandbox provider readiness is "Provider not ready / 沙箱提供商未就绪"; revoked credentials and keys use "Revoked / 已撤销". A default model provider is a service address and write-only key; the application's Agent `model` chooses the provider-supported model name. A sandbox is Core-managed compute; Runtime names Core's execution observations, and Environment is the Session's API execution environment. These are distinct counts and resources, not synonyms.

Expand Down
10 changes: 5 additions & 5 deletions apps/web/e2e/fixture-console.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,11 @@ function nodeRollout(previous = 0) {
}

function unconfiguredDeployment(generation = 0, ownerEpoch = 3) {
return { installation_id: INSTALLATION_ID, provider: "", credential_configured: false, core_url: publicUrl(), reset: null, rollout: noNodeRollout(), owner_epoch: ownerEpoch, generation, mode: "", resources: { allocations: 0, running: 0, pending: 0 }, suspension: null };
return { installation_id: INSTALLATION_ID, provider: "", credential_configured: false, core_url: publicUrl(), reset: null, rollout: noNodeRollout(), owner_epoch: ownerEpoch, generation, mode: "", resources: { allocations: 0, running: 0, pending: 0 }, idle_seconds: 300, suspension: null };
}

function configuredDeployment() {
return { installation_id: INSTALLATION_ID, provider: "microsandbox", credential_configured: false, configuration: {}, metadata: {}, core_url: publicUrl(), reset: null, rollout: nodeRollout(), owner_epoch: 3, generation: 1, mode: "nodes", resources: { allocations: 0, running: 0, pending: 0 }, specification: { resources: { cpus: 2, memory_mib: 4096 }, network: { public: true, domains: [] } }, specification_digest: "fixture", suspension: { idle_seconds: 300, retention_seconds: 86400 } };
return { installation_id: INSTALLATION_ID, provider: "microsandbox", credential_configured: false, configuration: {}, metadata: {}, core_url: publicUrl(), reset: null, rollout: nodeRollout(), owner_epoch: 3, generation: 1, mode: "nodes", resources: { allocations: 0, running: 0, pending: 0 }, specification: { resources: { cpus: 2, memory_mib: 4096 }, network: { public: true, domains: [] } }, specification_digest: "fixture", idle_seconds: 300, suspension: { retention_seconds: 86400 } };
}
/** The E2B template build as Core read it when the selection was saved. */
const templateBuild = { status: "ready", resources: { cpus: 2, memory_mib: 2048, root_disk_mib: 10240 } };
Expand Down Expand Up @@ -382,9 +382,9 @@ async function sandboxRoute(request, response, path, url) {
}
if (path === "/deployment" && request.method === "PATCH") {
const input = await body(request);
if (!state.deployment.suspension || state.deployment.reset) return error(response, 409, "Suspension policy is unavailable.", "sandbox_deployment_conflict");
if (!state.deployment.provider || state.deployment.reset) return error(response, 409, "Idle policy is unavailable.", "sandbox_deployment_conflict");
if (!Number.isInteger(input.idle_seconds) || input.idle_seconds < 30 || input.idle_seconds > 86400) return error(response, 400, "Invalid idle seconds.", "invalid_request_error");
state.deployment.suspension.idle_seconds = input.idle_seconds;
state.deployment.idle_seconds = input.idle_seconds;
return send(response, 200, state.deployment);
}
if (path === "/deployment" && (request.method === "POST" || request.method === "PUT")) {
Expand Down Expand Up @@ -428,7 +428,7 @@ async function sandboxRoute(request, response, path, url) {
rollout: e2b ? { ...noNodeRollout(), previous_generation_sandboxes: held.allocations + held.pending } : nodeRollout(held.allocations + held.pending),
specification,
...(e2b ? { configuration: { template: input.configuration?.template ?? "", api_url: input.configuration?.api_url ?? state.deployment.configuration?.api_url ?? "https://api.e2b.app", domain: input.configuration?.domain ?? state.deployment.configuration?.domain ?? "e2b.app" } , credential_configured: true, metadata: { template_build: templateBuild } } : { configuration: {}, metadata: {}, credential_configured: false }),
suspension: input.provider === "microsandbox" ? { idle_seconds: 300, retention_seconds: 86400 } : null,
idle_seconds: previous.idle_seconds, suspension: input.provider === "microsandbox" ? { retention_seconds: 86400 } : null,
};
return send(response, 200, state.deployment);
}
Expand Down
2 changes: 1 addition & 1 deletion apps/web/e2e/nodes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ test("edits only the saved backend, preserving a custom size", async ({ page, re
const current = { resources: { cpus: 7, memory_mib: 8192, root_disk_mib: 10240 }, network: { public: true, domains: [] }, workspace: { attachment: "host_directory", user_xattr: true, capacity_quota: false } };
let deployment = { configuration: {}, metadata: {}, credential_configured: false, installation_id: "94be54a1-138c-4f30-bc87-b13686272dbe", provider: "microsandbox", core_url: "https://core.example", reset: null, rollout: { state: "settled", previous_generation_sandboxes: 0, nodes: { ready: 0, preparing: 0, failed: 0, unknown: 0 } },
owner_epoch: 1, generation: 1, mode: "nodes", resources: { allocations: 0, running: 0, pending: 0 }, specification: current, specification_digest: "e".repeat(64),
suspension: null };
idle_seconds: 300, suspension: null };
let submitted: Record<string, unknown> | null = null;
await page.route("**/core/v1/sandbox/deployment", async (route) => {
if (route.request().method() === "PUT") {
Expand Down
7 changes: 4 additions & 3 deletions apps/web/e2e/sandbox-lifecycle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ test("shows each node's retained limit and elapsed phase time without inventing
await page.route("**/core/v1/sandbox/deployment", async (route) => {
const response = await route.fetch();
const deployment = await response.json();
await route.fulfill({ response, json: { ...deployment, provider: "microsandbox", suspension: { idle_seconds: 300, retention_seconds: 86400 }, specification: { ...deployment.specification, resources: { ...deployment.specification.resources, root_disk_mib: 8192 } } } });
await route.fulfill({ response, json: { ...deployment, provider: "microsandbox", suspension: { retention_seconds: 86400 }, specification: { ...deployment.specification, resources: { ...deployment.specification.resources, root_disk_mib: 8192 } } } });
});
await page.route("**/core/v1/sandbox/nodes", async (route) => {
const response = await route.fetch();
Expand Down Expand Up @@ -43,7 +43,7 @@ test("edits deployment idle time without changing generation", async ({ page, re
const nodeReads: string[] = [];
page.on("request", (sent) => { if (new URL(sent.url()).pathname.startsWith("/core/v1/sandbox/nodes")) nodeReads.push(sent.url()); });
await openConsole(page, request, "settings?id=sandbox");
const deployment = await setDeployment(request, { suspension: { idle_seconds: 300, retention_seconds: 86400 } });
const deployment = await setDeployment(request, { idle_seconds: 300, suspension: { retention_seconds: 86400 } });
await setDeployment(request, { provider: "microsandbox", specification: { ...deployment.specification!, resources: { ...deployment.specification!.resources, root_disk_mib: 8192 } } });
await page.getByRole("button", { name: "Refresh sandbox state" }).click();
const area = page.getByRole("region", { name: "Automatic suspension" });
Expand All @@ -57,7 +57,8 @@ test("edits deployment idle time without changing generation", async ({ page, re
await area.getByRole("button", { name: "Save", exact: true }).click();
const saved = await (await response).json();
expect(saved.generation).toBe(deployment.generation);
expect(saved.suspension).toEqual({ idle_seconds: 60, retention_seconds: 86400 });
expect(saved.idle_seconds).toBe(60);
expect(saved.suspension).toEqual({ retention_seconds: 86400 });
await expect(idle).toHaveValue("60");
await page.reload();
await expect(idle).toHaveValue("60");
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/features/fleet/SandboxResetNotice.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { SandboxResetNotice } from "./SandboxResetNotice";

const reset: SandboxReset = { clear: "auto", requested_at: "2026-09-27T10:00:00Z", deadline_at: "2026-09-27T11:00:00Z", forced_at: null,
remaining: { busy: 1, idle: 0, cleanup: 2, on_offline_nodes: 1, offline_nodes: [{ node_id: "n1", name: "Node 1", resources: 1 }] } };
const deployment = (reset: SandboxReset | null): SandboxDeployment => ({ credential_configured: false, configuration: {}, metadata: {}, rollout: { state: "settled", previous_generation_sandboxes: 0, nodes: { ready: 1, preparing: 0, failed: 0, unknown: 0 } }, installation_id: "i", provider: "microsandbox", core_url: "http://core", reset, owner_epoch: 1, generation: 1, mode: "nodes", resources: { allocations: 3, running: 3, pending: 0 }, suspension: null });
const deployment = (reset: SandboxReset | null): SandboxDeployment => ({ credential_configured: false, configuration: {}, metadata: {}, rollout: { state: "settled", previous_generation_sandboxes: 0, nodes: { ready: 1, preparing: 0, failed: 0, unknown: 0 } }, installation_id: "i", provider: "microsandbox", core_url: "http://core", reset, owner_epoch: 1, generation: 1, mode: "nodes", resources: { allocations: 3, running: 3, pending: 0 }, idle_seconds: 300, suspension: null });
const render = (value: SandboxDeployment | undefined, failed = false) => renderToStaticMarkup(<SandboxResetNotice deployment={value} failed={failed} onRetry={() => {}} />);

describe("reset notices on read-only surfaces", () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/features/fleet/fleet-queries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { sandboxAdmin, sandboxDeploymentQuery, sandboxScope } from "../sandbox/sandbox-queries";
import { fleetQuery } from "./fleet-queries";

const deployment: SandboxDeployment = { credential_configured: false, configuration: {}, metadata: {}, rollout: { state: "settled", previous_generation_sandboxes: 0, nodes: { ready: 1, preparing: 0, failed: 0, unknown: 0 } }, installation_id: "i", provider: "microsandbox", core_url: "http://core", owner_epoch: 1, generation: 1, mode: "nodes", resources: { allocations: 1, running: 1, pending: 0 }, suspension: null,
const deployment: SandboxDeployment = { credential_configured: false, configuration: {}, metadata: {}, rollout: { state: "settled", previous_generation_sandboxes: 0, nodes: { ready: 1, preparing: 0, failed: 0, unknown: 0 } }, installation_id: "i", provider: "microsandbox", core_url: "http://core", owner_epoch: 1, generation: 1, mode: "nodes", resources: { allocations: 1, running: 1, pending: 0 }, idle_seconds: 300, suspension: null,
reset: { clear: "auto", requested_at: "2026-09-27T10:00:00Z", deadline_at: "2026-09-27T11:00:00Z", forced_at: null, remaining: { busy: 1, idle: 0, cleanup: 0, on_offline_nodes: 0, offline_nodes: [] } } };

afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); });
Expand Down
Loading
Loading