Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
47 changes: 42 additions & 5 deletions contracts/agents-api/sandbox-deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,21 @@ does not grant node enrollment authority.

Node capacity is separate from the deployment specification. The administrator's
`POST /core/v1/sandbox/enrollment-tokens` accepts optional `max_active` and
`max_retained`, defaulting to 2 and 8. Core stores that approval with the token and
copies it to the registered node. Node enrollment cannot submit capacity overrides.
The existing node configuration/identity reads expose approved limits for that
credential; node updates remain administrator operations. Local host capacity
checks may reject a deployment that cannot run safely, but never raise its limits.
`max_retained`, defaulting to 2 and 8. Microsandbox uses both limits. Docker never
suspends, so its `max_retained` always equals `max_active`; Core replaces any
submitted value, here and in `PATCH /core/v1/sandbox/nodes/{node_id}`. Core stores
that approval with the token and copies it to the registered node. Node enrollment
cannot submit capacity overrides. The existing node configuration/identity reads
expose approved limits for that credential; node updates remain administrator
operations. Local host capacity checks may reject a deployment that cannot run
safely, but never raise its limits.

`GET /core/v1/sandbox/nodes/{node_id}/allocations` lists the node's unreleased
allocations. Each item's `compute_phase_changed_at` is the time the allocation
entered its current `compute_phase`, or null when unknown; an allocation that
existed before Core recorded it reports null until its next phase change. A
suspended microsandbox allocation's age, combined with the deployment's snapshot
retention, tells roughly when Core reclaims it.

## Selection request

Expand Down Expand Up @@ -233,6 +243,33 @@ token consumption. Retained node authentication checks the same generation and
digest. A changed local resource setting, Runtime or generation must fail rather
than rewrite the retained identity or silently use a local default.

## What each field means per sandbox provider

Some fields keep one name across providers but differ in meaning, or do not apply.
Deployment fields come from `GET /core/v1/sandbox/deployment`; node and allocation
fields from the administrator node routes; runtime fields from the
[Runtime observation API](runtime-observability-api.md), with `disk` only in the
administrator `GET /core/v1/admin/runtime-observations`, and the
[Runtime history API](runtime-history-api.md).

| Field | E2B | Docker | microsandbox |
| --- | --- | --- | --- |
| Deployment `specification.resources` | `cpus` and `memory_mib`, equal to the ready template build's and taken from it when omitted; no disk fields | `cpus` and `memory_mib`; no disk quota | `cpus`, `memory_mib`, `root_disk_mib` and `environment_disk_mib` |
| Deployment `specification.runtime` | Absent; the build is selected by `e2b.template` | The full [release](#runtime-release); nodes match `image_id` or `image_manifest_digest` | The full [release](#runtime-release); nodes match `microsandbox_ref`, `runtime_sha256` and `firmware_sha256` |
| Deployment `e2b.template_build` | The build as Core read it when the selection was saved | Absent, with the whole `e2b` object | Absent, with the whole `e2b` object |
| Deployment `suspension` | `null`; Core does not suspend E2B sandboxes | `null` | `{idle_seconds, retention_seconds}` |
| Deployment `resources.allocations`, `resources.pending` | Core's unreleased E2B sandboxes, and hosted Environments waiting for one | Totals across all nodes | Totals across all nodes |
| Enrollment-token `max_active`, `max_retained` | 409 `sandbox_deployment_conflict`, after the 400 capacity checks; E2B has no nodes | `max_retained` always equals `max_active` | Both limits apply |
| Node list and detail | Empty list; detail returns 404 | Enrolled nodes | Enrolled nodes |
| Node `retained`, `snapshots`, `max_retained` | Not applicable | Docker never suspends: `retained` equals `active`, `snapshots` is 0 and `max_retained` equals `max_active` | Suspended sandboxes are `retained` minus `active` |
| Node `diagnostic` | Not applicable | `docker_unavailable`, `docker_limits_unsupported`, `runtime_image_unavailable`, `capacity_insufficient` or `provider_unavailable` | `kvm_unavailable`, `microsandbox_artifacts_unavailable`, `capacity_insufficient` or `provider_unavailable` |
| Node `host.available_disk_bytes` | Not applicable | Free space on the filesystem of the node state directory, not a container's disk | Free space on the filesystem of the node state directory; sandbox disks have their own quotas |
| Allocation `compute_phase`, `compute_phase_changed_at` | Not applicable: no node allocations | Always `disabled`, counted as running until release; the time is the allocation's creation | Includes `suspended`; its time plus `suspension.retention_seconds` tells roughly when Core reclaims the snapshot |
| Runtime observation `cpu`, `memory` | From E2B metrics: `cpu.utilization_ratio` and `capacity_cores`, memory usage and limit; no cumulative CPU time | From Docker stats: `cpu.usage_seconds_total`, CPU and memory limits, memory usage | From the VM: `cpu.usage_seconds_total`, CPU and memory limits, memory usage |
| Runtime observation `disk` | E2B `diskUsed` and `diskTotal`; `null` when the template does not report them | `null`: no disk quota | `null` for now |
| Runtime observation `lifecycle_state: sleeping` | Never | Never | While suspended |
| Runtime history CPU | Mean of the utilization ratios E2B reported in each bucket | Derived from cumulative CPU time | Derived from cumulative CPU time |

## Failure and upgrade boundaries

Malformed selections return 400; validated configuration diagnostics use
Expand Down
6 changes: 6 additions & 0 deletions contracts/agents-api/sandbox-manager.openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ definitions:
max_active:
type: integer
max_retained:
description: Docker never suspends, so Core replaces this with max_active; microsandbox uses both limits.
type: integer
type: object
api.SandboxMutationResponse:
Expand Down Expand Up @@ -830,6 +831,10 @@ definitions:
properties:
compute_phase:
type: string
compute_phase_changed_at:
description: The time the allocation entered its current compute_phase, or null when unknown; an allocation that existed before Core recorded it reports null until its next phase change. For a suspended microsandbox allocation, this time plus the deployment's snapshot retention tells roughly when Core reclaims it.
type: string
x-nullable: true
created_at:
type: string
diagnostic:
Expand Down Expand Up @@ -929,6 +934,7 @@ definitions:
max_active:
type: integer
max_retained:
description: Docker never suspends, so Core replaces this with max_active; microsandbox uses both limits.
type: integer
name:
type: string
Expand Down
2 changes: 2 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ approves capacity when issuing its enrollment token: defaults are **2 active** a
**8 retained** sandboxes. An administrator may update capacity later through the
node API. `max_retained >= max_active >= 1`; reservations and uncertain cleanup
also consume capacity. Lowering a limit does not kill existing resources.
Microsandbox uses both limits. Docker never suspends, so its `max_retained` always
equals `max_active`; Core replaces any submitted value.

The generated private `provider.json` includes the provider, installation identity,
Core address, generation, approved specification copy and one adapter object:
Expand Down
2 changes: 1 addition & 1 deletion docs/web/protocol-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ deployment only.
| Node detail | `GET /core/v1/sandbox/nodes/{node_id}?range=1h\|6h\|24h` | Sandbox metrics node dialog: the host's CPU busy share and memory from its last heartbeat, and their history over the page's range |
| Allocations | `GET /core/v1/sandbox/nodes/{node_id}/allocations` | Nodes page; Sandbox metrics |
| Enrollment | `POST /core/v1/sandbox/enrollment-tokens` | **Add node**: the administrator sets the node's sandbox limits (`max_active`; `max_retained` only for microsandbox, equal to `max_active` for Docker) before Core issues a single-use token inside a command that verifies the installer checksum |
| Update node | `PATCH /core/v1/sandbox/nodes/{node_id}` | **Edit node**: the name and sandbox limits together (the retained limit only for microsandbox; Docker keeps its saved one, raised to at least the active limit) |
| Update node | `PATCH /core/v1/sandbox/nodes/{node_id}` | **Edit node**: the name and sandbox limits together (the retained limit only for microsandbox; under Docker, Core sets it to the active limit) |
| Remove node | `DELETE /core/v1/sandbox/nodes/{node_id}` | Confirmed node removal; the row goes only after Core acknowledges the deletion |

These pages appear only when `/console/config` reports `sandbox_admin: true`. An E2B
Expand Down
4 changes: 3 additions & 1 deletion packages/agents-client/src/sandbox-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export interface SandboxNodeDetail extends SandboxNode {
host: SandboxNodeHost;
history: { resolution_seconds: number; points: SandboxNodeHostPoint[] };
}
/** A node's name and sandbox limits; Core takes all three, with a retained limit of at least the active one. */
/** A node's name and sandbox limits; Core takes all three, with a retained limit of at least the active one. Under Docker, Core sets the retained limit to the active one. */
export interface SandboxNodeUpdate {
name: string;
max_active: number;
Expand All @@ -107,6 +107,8 @@ export interface SandboxAllocation {
environment_id: string;
state: string;
compute_phase: string;
/** When the allocation entered its current compute_phase, or null when unknown; an allocation that existed before Core recorded it reports null until its next phase change. */
compute_phase_changed_at: string | null;
diagnostic: SandboxDiagnostic;
initialization: string;
created_at: string;
Expand Down
7 changes: 7 additions & 0 deletions services/agents-api/HOSTED-SANDBOX-MANAGER.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ readiness. To prepare the qualified E2B Runtime build, use
[the E2B build guide](deploy/e2b/README.md); it is not a public Environment Template.
Microsandbox suspends eligible idle Sessions after 300 seconds and retains their
snapshots for 86400 seconds. Docker and E2B have no memory snapshot policy.
A node's allocation list reports `compute_phase_changed_at`, the time each
allocation entered its current `compute_phase`, or null when unknown; an allocation
that existed before Core recorded it reports null until its next phase change. A
suspended allocation's age, combined with this retention, tells roughly when Core
reclaims it.
See [what each field means per sandbox provider](../../contracts/agents-api/sandbox-deployment.md#what-each-field-means-per-sandbox-provider)
for fields that differ between E2B, Docker and microsandbox.

For source/manual deployments using E2B, install the packaged helper and set
`AGENTS_API_E2B_PROVIDER_BIN` to its absolute executable path. Set
Expand Down
3 changes: 2 additions & 1 deletion services/agents-api/internal/api/sandbox_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ type SandboxMutationResponse struct {
Updated bool `json:"updated,omitempty"`
}
type SandboxEnrollmentTokenRequest struct {
MaxActive *int `json:"max_active,omitempty"`
MaxActive *int `json:"max_active,omitempty"`
// Docker never suspends, so Core replaces this with max_active; microsandbox uses both limits.
MaxRetained *int `json:"max_retained,omitempty"`
}

Expand Down
2 changes: 0 additions & 2 deletions services/agents-api/internal/api/sandbox_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,6 @@ func TestSandboxEnrollmentCapacityIsAdministratorOnly(t *testing.T) {
}
for _, test := range []struct{ path, token, body string }{
{"/core/v1/sandbox/enrollment-tokens", "administrator", `{"max_active":0}`},
{"/core/v1/sandbox/enrollment-tokens", "administrator", `{"max_active":9,"max_retained":8}`},
{"/core/v1/sandbox/enrollment-tokens", "administrator", `{"max_retained":1000001}`},
{"/api/v1/sandbox-node/enroll", "one-use", `{"max_active":100}`},
{"/api/v1/sandbox-node/enroll", "one-use", `{"max_retained":100}`},
} {
Expand Down
2 changes: 1 addition & 1 deletion services/agents-api/internal/db/queries/runtime_nodes.sql
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ UPDATE runtime_allocations SET node_id=$1,compute_activity_at=clock_timestamp()
WHERE provider_key=$2 AND node_id IS NULL AND state<>'released';

-- name: ListNodeRuntimeAllocations :many
SELECT a.id,a.node_id,a.observation_error,a.state,a.compute_phase,a.initialization,a.created_at,a.environment_id,e.session_id,s.tenant_id
SELECT a.id,a.node_id,a.observation_error,a.state,a.compute_phase,a.compute_phase_changed_at,a.initialization,a.created_at,a.environment_id,e.session_id,s.tenant_id
FROM runtime_allocations a JOIN environments e ON e.id=a.environment_id JOIN sessions s ON s.id=e.session_id
WHERE a.node_id=$1 AND a.state<>'released' ORDER BY a.created_at,a.id LIMIT 1000;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
-- name: SetRuntimeCompute :one
UPDATE runtime_allocations
SET compute_phase = sqlc.arg(phase), compute_state = sqlc.arg(state)::jsonb,
SET compute_phase_changed_at = CASE WHEN compute_phase = sqlc.arg(phase)::text THEN compute_phase_changed_at ELSE clock_timestamp() END,
compute_phase = sqlc.arg(phase), compute_state = sqlc.arg(state)::jsonb,
compute_revision = compute_revision + 1,
compute_retained_until = sqlc.narg(retained_until),
kept_at = CASE WHEN sqlc.arg(phase)::text = 'running' THEN clock_timestamp() ELSE kept_at END
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 19 additions & 18 deletions services/agents-api/internal/db/sqlc/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading