diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b32f49e9c..c3e0da432 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -788,40 +788,76 @@ addresses from the validated installation public URL, never inbound Host headers
current selection from the live deployment API; there is no startup configuration
read.
-Provider, resources and Runtime changes require maintenance, the current generation
-and verified zero retained or pending resources. They never change the Core
-address, which comes from the installation public URL. Maintenance prevents fresh hosted Sessions and allocations while
-retaining admitted work, known receipts, queries and explicit cleanup. Unreleased
-allocations include stopped compute, snapshots, uncertain operations and pending
-cleanup. Pending hosted Environments without allocations also block changes.
-Failed checks never authorize resource deletion. Explicitly resume after a successful
-change. Preserve historical allocation ownership and Session placement; never migrate
-an existing Session to another provider or recreate a released allocation.
-
-Explicit administrator Session archive requires a Web-managed deployment in
-maintenance and its current generation. Keep Project scope and managed-hosted
-eligibility checks in the existing administrative Store transaction, alongside
-Environment expiry (preserving failure), cancellation requests, Runtime authority
-revocation and audit. The ordinary Worker and provider lifecycle own compute and
-snapshot release; archive must not call a provider in that transaction or delete
-the Session. Retain history and persisted Files/Artifacts. The archive GET reports
-actual resource disposition, not archive provenance or Turn finalization. Keep
-unknown cleanup blocking, never automatically retry the mutation, and never
-resume an archived Session or restore its unpersisted workspace.
+All sandbox writes require the observed generation, including initial POST at
+zero; reject stale state before provider preparation, reset or no-op checks, and
+repeat it under the committing row lock. Same-provider PUT currently requires no
+reset and zero held resources. A different backend or unspecified old selection
+requires reset. No online rollout is implemented at this stage; omit that projection.
+Preserve historical allocation ownership and placement, never migrate a Session.
+
+Reset is durable execution state, advanced by the existing manager outside its
+counted work. Serialize start, escalation, cancel, setup, update and finalization
+through the mutation gate. Auto waits only for in-progress/waiting root/subagent
+Turns and pending file writes; queued work, idle and suspended Sessions can archive.
+Recheck idleness with Session then deployment locks; never invert that order during
+finalization. Keyset-page bounded work, bind candidates to the reset request time
+as well as generation, and persist the original absolute deadline and validated
+audit provenance. Cancellation cannot revive archived work. Auto's deadline
+escalates to force durably; self-hosted Sessions are excluded.
+
+Use one snapshot and timestamp for the held-resource partition: unreleased
+allocations plus pending hosted Environments without any allocation row. Cleanup
+has precedence, then busy/idle. Count deleted/expired receipts until release.
+Offline ownership comes from allocation or active placement node identity, with
+the existing 45-second connection/owner-epoch predicate; provider readiness is
+independent. Project every offline node and make its sum equal on_offline_nodes.
+No cleanup failure, offline state or empty read authorizes a synthetic release.
+
+At zero held resources, drain outside database transactions, recheck under the
+deployment lock, and atomically clear all provider policy, E2B credential/build
+metadata and specification, retire nodes/tokens, advance generation/owner epoch
+and audit completion. Publish a generation-bearing nil provider tombstone without
+fallible work after commit, preventing delayed loads from reviving the old provider.
+On a failed final write or interrupted drain, restore the committed provider using
+a bounded owner context before releasing the mutation gate; recovery failure keeps
+admission fenced and stops the owner. Ordinary migration resumes old Web-managed
+maintenance with reset null. Refuse downgrade during reset or for an unconfigured
+completed reset with generation above zero; never discard the generation.
+
+Explicit administrator Session archive requires a Web-managed deployment and its
+current generation, without a reset precondition. Keep Project scope and hosted
+eligibility checks in its Session-first transaction, alongside Environment expiry,
+cancellation, Runtime authority revocation and audit. Background reset reconstructs
+the actual Project scope from trusted records while retaining requester provenance.
+The ordinary provider lifecycle owns compute/snapshot release. Retain history and
+persisted Files/Artifacts; archived Sessions cannot resume unpersisted workspace.
+The archive GET reports resource disposition, not provenance or Turn finalization.
+
+Archive cancellation must not destroy a healthy receipt path before its terminal
+commit. Only the archive that first revokes a device may persist its exact
+`archive_cancel_turn_id`; ordinary revocation clears it, and repeated cleanup
+preserves rather than recreates it. The existing authenticated delivery can drain
+its cancellation for at most 20 seconds from the Turn's original
+`cancel_requested_at`. Track that delivery through Done, cancellation ACK and
+terminal commit, independently of subscription removal. Heartbeats and both normal
+and checkpoint cleanup use the same identity/deadline check; no new connection,
+input, file/MCP authority or lease renewal is granted. Do not hold a transaction or
+lifecycle gate waiting for the receipt. Lost peers, expiry and restart retain the
+ordinary failure/cleanup fallback, never a fabricated cancelled outcome. Preserve
+unknown Create ownership and reject schema downgrade with unsettled markers.
Core rejects `AGENTS_API_MANAGED_RUNTIMES_FILE`; there is no file-managed startup
path or embedded local node. An older file-managed database is not automatically
adopted after its environment variable is removed. Settle and drain that deployment
with its previous release and original backend, preserving business data, private
-receipts, identities and storage. This change provides no old-database conversion,
-force reset or automatic deletion. The supported current path is a database-managed
+receipts, identities and storage. The legacy file-managed path has no automatic adoption or force conversion. The supported current path is a database-managed
deployment. Harness selection and public/self-hosted contracts remain unchanged.
The paired console serves only matched, non-secret distribution artifacts for node
installation. Never serve private installation files or arbitrary paths. Installation
reads `GET /api/v1/sandbox-node/configuration` using an unconsumed enrollment token,
or a retained node credential with `X-OAC-Node-ID`. Reads never consume enrollment;
-registered nodes can read their matching configuration during maintenance. Validate
+registered nodes can read their matching configuration during reset. Validate
installation, generation, specification digest and release before writing node files,
registering or reconnecting. Reject drift rather than overwriting retained identity
or using local resource defaults. Registration consumes a token only after these
@@ -908,7 +944,19 @@ existing allocation must agree with the placement. Never choose another node.
E2B direct allocations use one serial lifecycle without a node identity.
The coordinator stops accepting work and cancels and drains all node workers and
direct callers before releasing the sole execution lease. Lease loss is global;
-ordinary provider failures stay within their node. Session locks, deployment
+ordinary provider failures stay within their node. A planned deployment drain or
+inventory retirement cancels lifecycle contexts synchronously between leased
+operations, using the existing lease gate with its five-second bound. Include
+an active manual reconcile's cancel function: an asynchronous `AfterFunc` alone
+can cancel a later leased query after the gate reopens. Never cancel an in-flight
+leased query merely to change deployment configuration. A failed cancellation
+fence synchronously closes manager admission and reports owner failure, even
+before its coordinator starts. Failed inventory retirement retains the original
+lifecycle identity and gate through owner shutdown; gate availability is not
+proof that cancellation succeeded. The drain barrier stays closed and cannot
+activate a replacement. Release the lease gate before waiting for provider
+settlement or lifecycle accounting. Ordinary caller deadlines and owner shutdown retain their
+existing cancellation and fail-closed lease-loss behavior. Session locks, deployment
capacity transactions and revision/one-shot receipts remain authoritative, with
no external operation holding a database lock.
@@ -925,7 +973,7 @@ Do not add a startup adoption path to bypass the database-managed selection.
Do not add node-level drain controls. Refuse node removal with pending allocations,
instances, snapshots, unknown results or cleanup resources. Offline ownership is
retained. Removing a node does not delete compute. Local and remote nodes share
-the same resource guard. Keep the deployment-wide maintenance and configuration
+the same resource guard. Keep the deployment-wide reset and configuration
change guard. This boundary does not add cross-node Session
migration, Core multi-active, autoscaling, Kubernetes or harness residency.
@@ -1740,18 +1788,22 @@ project; config.json's independent schema format stays 1. The operator command i
`oac` (`oac_cli.py`, packaged as `oac.pyz`), with default installation directory
`~/.oac/core`, private `~/.oac`, generated `x-oac` annotations and `.oac.lock`.
Only `install.sh --convert` reads pre-rename state. Its `rename.py` coordinator first
-validates the source, target, bundle and generated files. A stopped old Core may be
-started from its own files for authenticated drain checks; refusal restores only
-services the probe started and reports any failed restoration. Require maintenance,
-zero unreleased allocations and pending work, and no registered nodes. One explicit
-confirmation covers legacy config-layout conversion and the rename. Never delete a
-target volume or copy container without matching the recorded conversion ownership;
-copy the stopped PostgreSQL volume, retain the old volume, and carry the atomic
-journal across the default-directory move. Preserve ports, secrets, installation
-identity and stored history. Resume only with the recorded bundle. Docker and
-microsandbox replace the Runtime through the existing guarded PUT and resume
-admission; E2B stays in maintenance for its rebuilt template. Node and self-hosted
-installations are outside this converter.
+validates the source, target, bundle and generated files. The previous Core must
+already be running and readable: do not start an unknown provider to inspect it.
+Keep old-release maintenance, zero-resource and no-registered-node preflight.
+Refuse E2B conversion before any conversion mutation, also on journal resume after
+the original source-bundle guard. Its safe Core-owned upgrade preparation remains
+a release blocker; never patch execution tables or change ordinary migration's
+resume decision to work around it. One explicit confirmation covers legacy layout
+and rename. Never delete a target volume/container without recorded ownership;
+copy the stopped database, retain the old volume and journal across the atomic move.
+Preserve ports, secrets, installation identity and history. Resume only with the
+recorded bundle. The new-Core node-provider leg requires matching identity/provider,
+reset null and zero resources; send one expected-generation PUT of exact bundle
+Runtime and prior resources. Read back exact generation/specification. Reconcile an
+uncertain write through reads, never blind replay or the retired maintenance PATCH.
+Keep native stop proof and interrupted-copy recovery. Node/self-hosted installations
+are outside this converter.
Core process settings use `OAC_*`, Web settings use `OAC_WEB_*`, and shared Go
logging uses `OAC_LOG_*`. Retired settings fail startup even when empty or when
the new name is also set; report every matching name without values. Only the
@@ -2918,8 +2970,12 @@ replaced; do not carry obsolete compatibility code forward to satisfy this secti
every Session transaction: binding, claim/reconciliation, journal/Items/Usage,
function callbacks/application receipts and terminal/native continuity. Serialize
these short transactions and lease pings; execution transactions have a five-second
- deadline including gate and Session-lock waits. Never hold a transaction across
- daemon/model work, reconnect the writer or fall back to the pool after lease loss.
+ deadline including gate and Session-lock waits. Sandbox reset snapshots bound the
+ deployment relation explicitly to its legal singleton row before resource joins.
+ Keep this cardinality visible even on fresh databases without statistics: inflated
+ join estimates can trigger expensive JIT compilation inside the lease deadline.
+ Never hold a transaction across daemon/model work, reconnect the writer or fall
+ back to the pool after lease loss.
The original Store handles public admission and device/auth maintenance on pooled
connections. Execution reads may also use the pool; a read grants no write authority.
At startup, reconcile previously claimed work as failed, preserve queued inputs
diff --git a/Makefile b/Makefile
index b1e3112bf..90c1b28c2 100644
--- a/Makefile
+++ b/Makefile
@@ -57,7 +57,9 @@ build-agents-api-release:
./scripts/build-agents-api-release.sh
check-agents-api: build-agents-api
- go test ./services/agents-api/... ./packages/agents-client/... -count=1
+ # Persistence integration tests include bounded lifecycle waits that together exceed Go's 10m default.
+ go test ./services/agents-api/... ./packages/agents-client/... -count=1 -timeout=20m
+ PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s services/agents-api/tests -p 'official_diagnostics_test.py'
PYTHONDONTWRITEBYTECODE=1 python3 services/agents-api/deploy/runtime/initialize_receipt_test.py
PYTHONDONTWRITEBYTECODE=1 python3 services/agents-api/deploy/e2b/managed_init_test.py
diff --git a/README.md b/README.md
index fb6bd0525..e3d6d192a 100644
--- a/README.md
+++ b/README.md
@@ -74,7 +74,7 @@ native differences.
| [Configuration reference](docs/configuration.md) | Every setting in `config.json` and in Web |
| [Call the API](docs/getting-started/quickstart.md) | The application developer's quickstart |
| [API reference](docs/api/README.md) | The `/v1`, `/core/v1` and `/api/v1` namespaces |
-| [Nodes and sandbox backends: operator reference](services/agents-api/HOSTED-SANDBOX-MANAGER.md) | Node protocol, manual registration, placement, maintenance |
+| [Nodes and sandbox backends: operator reference](services/agents-api/HOSTED-SANDBOX-MANAGER.md) | Node protocol, manual registration, placement, reset |
| [Protocol coverage](contracts/agents-api/README.md) and [harness selection](contracts/agents-api/harness-selection.md) | Supported operations and native differences |
| [Landing page source](site/index.html) | The public site |
diff --git a/apps/docs/FOLLOW-UP.md b/apps/docs/FOLLOW-UP.md
index 232f913c1..e74b7e1a4 100644
--- a/apps/docs/FOLLOW-UP.md
+++ b/apps/docs/FOLLOW-UP.md
@@ -1,14 +1,13 @@
# Feature integration follow-up
-This candidate documents the actual maintenance and drain model at source
-`75bf4484ba957344ae374354858bbdfe104f5c4f`. It is not final documentation for design 50.
-The Core error-envelope foundation is included; the later configuration validators and
-diagnostic endpoints are not. Do not replace current procedures with the planned
-behavior before the features land.
+This candidate documents durable reset and the interim zero-resource same-provider
+update rule at source `f36ac3592bfe9f77a2172680aa52b14c6f525062`.
+The Core error-envelope foundation is included; online generations, later configuration
+validators and diagnostic endpoints are not. Do not replace current procedures with
+planned behavior before those features land.
| Feature dependency | Site pages to reconcile | Source and verification work |
| --- | --- | --- |
-| Reset replaces maintenance (design 50 PR-R) | configure, hosted-providers, troubleshooting, admin-api, observability, API reference/core | Regenerate the canonical guides and management contract; describe reset admission, cleanup, failures, archive preconditions and offline-node limits from merged code. Remove retired maintenance instructions only after their replacement exists. |
| Online generations and E2B changes (design 50 PR-G) | configure, hosted-providers, troubleshooting, execution-model, API reference/core | Document actual generation/rollout states, the supported change matrix and existing-sandbox behavior. Verify PUT examples against the final contract. |
| Node update protocol (design 50 PR-N) | hosted-providers, troubleshooting, API reference/machine | Explain the implemented update command, old-node handling and ownership preservation. Do not claim an update command now. |
| Later configuration/error changes (design 40 and remaining installer work) | install, configure, troubleshooting, quickstart | Regenerate current configuration fields and revise diagnostics against merged code; keep secrets out of examples. |
diff --git a/apps/docs/content/docs/admin-api.mdx b/apps/docs/content/docs/admin-api.mdx
index 1e9b830cc..d0b79bf04 100644
--- a/apps/docs/content/docs/admin-api.mdx
+++ b/apps/docs/content/docs/admin-api.mdx
@@ -8,7 +8,7 @@ resource inspection, Project and key management, credential issuance, audit, usa
and sandbox operations. It never calls `/v1` or `/api/v1`, and has no Agent
execution, copy or arbitrary asset editing operation.
-Core request failures use the [Core administration error envelope](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/core-errors.md),
+Core request failures use the [Core administration error envelope](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/core-errors.md),
including optional typed safe details. Console sign-in endpoints retain their
separate error shape described below.
@@ -49,9 +49,9 @@ label, but could set any value. Never use it for authorization or as proof of
origin.
Web calls Core only through the typed Core clients in `packages/agents-client`:
-[AdminClient](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/packages/agents-client/src/admin-client.ts) (`/core/v1`),
+[AdminClient](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/packages/agents-client/src/admin-client.ts) (`/core/v1`),
`SandboxAdminClient` (`/core/v1/sandbox`) and `CoreMetricsClient`
-(`/core/v1/metrics`). See [the complete administrator reference](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/admin-api.md)
+(`/core/v1/metrics`). See [the complete administrator reference](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/admin-api.md)
for methods, fields, filters, pagination and response shapes.
Routes below are relative to `/core/v1`:
@@ -62,8 +62,8 @@ Routes below are relative to `/core/v1`:
| Resource lists/details/deletion | `/projects/{id}/agents`, `/sessions`, `/environment-templates`, `/skills`, `/files`, `/vaults`, including the documented nested reads |
| Asset ownership | `GET /projects/{id}/resource-owners` with batched resource IDs |
| Key operation history | `GET /projects/{id}/write-operations` with key/resource/time filters |
-| Executor credentials | `GET/POST /projects/{id}/environments/{environment_id}/executor-credentials`, `DELETE …/executor-credentials/{key_id}` ([contract](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/environment-executor-credentials.md)) |
-| Deployment model providers | `GET /harnesses`, `GET/PUT/DELETE /harnesses/{harness}/model-provider`; the key is write-only ([contract](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/model-execution.md#deployment-defaults)) |
+| Executor credentials | `GET/POST /projects/{id}/environments/{environment_id}/executor-credentials`, `DELETE …/executor-credentials/{key_id}` ([contract](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/environment-executor-credentials.md)) |
+| Deployment model providers | `GET /harnesses`, `GET/PUT/DELETE /harnesses/{harness}/model-provider`; the key is write-only ([contract](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/model-execution.md#deployment-defaults)) |
| Usage and health | `GET /summary`, `/sandbox/runtime-observations`, `/metrics` |
| Administrator audit | `GET /audit-log`; deployment-wide entries have `project_id: null` |
@@ -76,21 +76,21 @@ it is operational attribution, not per-key billing.
## Sandbox administration
-The [deployment configuration contract](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/sandbox-deployment.md),
-[nodes and sandbox backends reference](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/HOSTED-SANDBOX-MANAGER.md)
-and [generated OpenAPI](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/core.openapi.yaml)
+The [deployment configuration contract](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/sandbox-deployment.md),
+[nodes and sandbox backends reference](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/HOSTED-SANDBOX-MANAGER.md)
+and [generated OpenAPI](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/core.openapi.yaml)
define deployment and node operations:
- `GET/POST/PUT /core/v1/sandbox/deployment` and
- `PATCH /core/v1/sandbox/deployment/maintenance`.
+ `POST/DELETE /core/v1/sandbox/deployment/reset`.
- `GET /core/v1/sandbox/nodes`, `PATCH/DELETE /core/v1/sandbox/nodes/{node_id}`,
and `GET /core/v1/sandbox/nodes/{node_id}/allocations`.
- `POST /core/v1/sandbox/enrollment-tokens` for a one-time node installation command.
Its non-secret `enrollment_id` reappears on the node that command registers.
PostgreSQL owns one provider, per-sandbox resource specification and immutable
-Runtime selection. POST initializes it; PUT replaces the complete selection using
-`expected_generation`. Requests carry `resources` and, for Docker/microsandbox,
+Runtime selection. POST initializes it and PUT updates the same provider; both
+require the observed `expected_generation`, including zero at first setup. Requests carry `resources` and, for Docker/microsandbox,
`runtime`; safe responses return `specification` and `specification_digest`.
Response `resources.allocations` and `resources.pending` are cleanup counts, not
CPU, memory or disk settings. E2B accepts a write-only key and exact template build
@@ -99,23 +99,26 @@ may omit `resources` to adopt the validated build's CPU and memory; responses sh
the build as read at selection time in `e2b.template_build`. Microsandbox responses
return its idle `suspension` policy; other providers return null.
-Provider, resource and Runtime changes all require global maintenance and verified
-cleanup of retained/pending resources. Core validates the candidate before commit;
-a rejection preserves the previous configuration. A changed commit advances the
-generation and retires old nodes and enrollment tokens atomically. Explicitly resume
-after success. Neither switching nor editing configuration deletes resources or
-migrates existing Sessions. During maintenance, administrators can explicitly
-[archive each retained hosted Session](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/admin-api.md#administrative-session-archive)
-through the Core administrator API at the current generation, then read its
-resource disposition and recheck deployment counts. Archive preserves history
-and persisted Files/Artifacts; unpersisted workspace contents are lost and the
-original Session cannot resume. This API does not add a console archive control.
+Same-provider updates currently require zero retained/pending resources and no
+reset. Changing backend requires explicit durable reset before a new POST.
+Auto archives idle/queued/suspended hosted Sessions, waits for started work and
+file writes, and escalates at its persisted deadline; force requests cancellation
+and verified cleanup. The deployment response supplies the authoritative
+`reset.remaining` partition and offline-node subset; Web must not derive either
+from independently loaded lists. No rollout projection is provided yet.
+
+Administrators may [archive an individual hosted Session](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/admin-api.md#administrative-session-archive)
+at the current generation without reset. History and persisted Files/Artifacts
+survive; unpersisted workspace is lost and the original Session cannot resume.
+Cancel reset stops further archives, not cleanup already requested. After zero
+resources Core clears the selection and advances generation; configure again using
+that new generation. Never automatically replay an uncertain write.
Public Environment Templates, the `/v1` contract and
caller-owned `self_hosted` provisioning remain unchanged.
`GET /api/v1/sandbox-node/configuration` uses an enrollment Bearer token, or a
retained node Bearer credential with `X-OAC-Node-ID`. This read does not consume
-enrollment. Retained matching nodes can read their configuration during maintenance.
+enrollment. Retained matching nodes can read their configuration during reset.
Installers must verify the returned generation, specification digest and Runtime
before registration; local files cannot override the saved limits. A mismatch
returns `sandbox_specification_mismatch` without replacing node state.
@@ -145,7 +148,7 @@ generated schema; do not interpret every empty or failed read as an absent resou
`GET /core/v1/metrics?range=1h|6h|24h|7d` returns Core process, execution
queue/slots, PostgreSQL and background-job measurements. It requires the Core key,
rejects arbitrary query filters and never grants
-Agent execution access. See the [exact measurement contract](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/core-metrics.md)
+Agent execution access. See the [exact measurement contract](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/core-metrics.md)
for complete buckets, null values, units and process-local retention. Frontend
implementation is maintained separately; this backend change does not modify
Agent metrics or the public Agent API.
@@ -154,7 +157,7 @@ Agent metrics or the public Agent API.
The Core key reads a node and its host history through
`GET /core/v1/sandbox/nodes/{node_id}?range=1h|6h|24h`. See the
-[node host history contract](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/node-host-history.md)
+[node host history contract](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/node-host-history.md)
for nullable observations, freshness and aggregation. The node list is unchanged.
-[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/docs/api/web-management.md)
+[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/docs/api/web-management.md)
diff --git a/apps/docs/content/docs/agents-and-tools.mdx b/apps/docs/content/docs/agents-and-tools.mdx
index ed238409b..1f5432c42 100644
--- a/apps/docs/content/docs/agents-and-tools.mdx
+++ b/apps/docs/content/docs/agents-and-tools.mdx
@@ -4,7 +4,7 @@ description: "Native harness tools, shared declarations and the limits of execut
---
Assessed against Core `206474a4c10901442b1faa094281bfb5559082b5` on 2026-09-22.
-The contract remains [OpenAI Python 3.13.0 at `d7c41ef`](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/upstream.json), Beta
+The contract remains [OpenAI Python 3.13.0 at `d7c41ef`](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/upstream.json), Beta
`agents=v1`. This matrix records the bounded execution/tools milestone. It does not
establish complete protocol compatibility; the remaining limits below stay open.
@@ -22,20 +22,20 @@ and Environment Plugin MCP have separate inventories and qualification.
| Operation | Implemented behavior and real qualification | Unsupported or unverified boundary |
| --- | --- | --- |
-| Initial message input, `sessions.create` | String input and ordered user-message arrays share atomic admission. Codex/Claude text and inline image execution: M1/M2; ordinary MiniMax text: M1/P1. [Input contract](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/message-input.md), [initial parser](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/internal/api/session_initial_input.go). | Whitespace-only text is admitted verbatim for Codex and rejected at admission for Claude SDK and MiniMax Code ([text content](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/message-input.md#text-content)). Empty parts beside text (SES-08) and local size-limit parity need upstream evidence. Images are only qualified for inline PNG/JPEG on `none` and Core-managed Docker `openai_hosted`; MiniMax, self-hosted images and remote URLs remain gaps. |
-| Prepared and active messages, `sessions.events.create` | Ordered `input_text`/`input_image` arrays retain original content and distinct public user Items. HTTP 202 confirms persistence; native receipts establish application. Initial/prepared/active Docker PNG/JPEG: M2; active PNG on `none`: M1. [Shared admission](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/internal/api/inputs.go). | Codex flattens native messages with blank-line separators. Claude can fold or queue native turns; it does not promise Codex's same-native-turn behavior. Public durability does not prove native consumption. Claude SDK and MiniMax Code reject messages without an image or non-whitespace text at admission (`unsupported_or_invalid_configuration`); Codex delivers them unchanged. |
-| Structured output, `agent.text.format` | Save/inherit/freeze `{type:json_schema,schema:...}`. Claude SDK object-root, single Agent, medium verbosity, ordinary functions returning text: S1 (`none`) and S2 (Docker, including prepared/active input and Files/Artifacts). Native final text is retained unchanged; its stream follows the official message sequence with the whole text in one `output_text.delta` ([item serialization](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/history-events-usage.md#item-serialization-2026-09-23)). [Contract](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/structured-output.md), [profile](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/internal/engine/claude.go). | An explicit non-object root type is a protocol error for every harness ([validation](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/official-semantics-alignment.md#agent-configuration-validation--september-23)). Codex/MiniMax, schemas without an object root, schema numbers changed by binary64, self-hosted, Skills/Plugins, MCP, Subagent and discovery combinations reject execution. No output repair, coercion or extra model loop. Arbitrary schema dialects are unverified. |
-| Function configuration, saved/inline Agents | Required name/description/schema; `defer_loading` defaults false. Protocol errors, repeated names and explicit non-object root types reject with the official fields ([validation](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/official-semantics-alignment.md#agent-configuration-validation--september-23)). Saved references resolve into an immutable Session snapshot. Codex/Claude real calls: F1/F2/M2/S1/S2. [Parser](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/internal/api/function_configuration.go), [saved tools](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/internal/api/saved_tools.go). | MiniMax public functions reject. Claude requires an explicit object root. Local nonblank/512-byte name and 64-definition Session bounds are compatibility gaps. Saving configuration alone does not qualify execution. |
-| Function-result admission, `events.create` | Required `turn_id`, `call_id`, `success`; optional nullable `error` and `output`. Output is string or ordered text/image content. Scoped atomic batches retain field presence, original content and retry identity in storage; public result Items and events always carry `output` and `error`, null when not submitted ([item serialization](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/history-events-usage.md#item-serialization-2026-09-23)). Same result retries are accepted; changed results and results after cancellation return 409 `conflict_error`, including after terminal state. In the caller's Session, an unknown call or a call of another Turn returns 400 `invalid_request_error` without changing the pending action; missing and foreign Sessions stay 404 ([error rows](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/official-semantics-alignment.md#session-input-conflicts-and-result-targets--september-23)). F1/F2/M2 plus [controlled SDK/raw checks](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/tests/official_function_inputs.py). [Parser](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/internal/api/function_inputs.go), [Store](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/internal/store/function_inputs.go). | Admission is separate from application and public Item publication. Invalid or unqualified content cannot consume a pending call. Core messages omit the call and executor IDs that official messages name; hosted defaults and publication timing remain unverified. |
-| Function text results and native application | Codex waits for a matching live root dynamic-tool completion; Claude waits for a matching live root native tool result. Text/error results, retry/conflict, cancellation and cold continuation: F1/F2. [Receipt contract](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/function-result-images.md). | Transport writes alone do not confirm application. Confirmation is not provider consumption, crash recovery or exactly-once external effects. No automatic result replay. |
+| Initial message input, `sessions.create` | String input and ordered user-message arrays share atomic admission. Codex/Claude text and inline image execution: M1/M2; ordinary MiniMax text: M1/P1. [Input contract](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/message-input.md), [initial parser](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/internal/api/session_initial_input.go). | Whitespace-only text is admitted verbatim for Codex and rejected at admission for Claude SDK and MiniMax Code ([text content](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/message-input.md#text-content)). Empty parts beside text (SES-08) and local size-limit parity need upstream evidence. Images are only qualified for inline PNG/JPEG on `none` and Core-managed Docker `openai_hosted`; MiniMax, self-hosted images and remote URLs remain gaps. |
+| Prepared and active messages, `sessions.events.create` | Ordered `input_text`/`input_image` arrays retain original content and distinct public user Items. HTTP 202 confirms persistence; native receipts establish application. Initial/prepared/active Docker PNG/JPEG: M2; active PNG on `none`: M1. [Shared admission](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/internal/api/inputs.go). | Codex flattens native messages with blank-line separators. Claude can fold or queue native turns; it does not promise Codex's same-native-turn behavior. Public durability does not prove native consumption. Claude SDK and MiniMax Code reject messages without an image or non-whitespace text at admission (`unsupported_or_invalid_configuration`); Codex delivers them unchanged. |
+| Structured output, `agent.text.format` | Save/inherit/freeze `{type:json_schema,schema:...}`. Claude SDK object-root, single Agent, medium verbosity, ordinary functions returning text: S1 (`none`) and S2 (Docker, including prepared/active input and Files/Artifacts). Native final text is retained unchanged; its stream follows the official message sequence with the whole text in one `output_text.delta` ([item serialization](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/history-events-usage.md#item-serialization-2026-09-23)). [Contract](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/structured-output.md), [profile](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/internal/engine/claude.go). | An explicit non-object root type is a protocol error for every harness ([validation](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/official-semantics-alignment.md#agent-configuration-validation--september-23)). Codex/MiniMax, schemas without an object root, schema numbers changed by binary64, self-hosted, Skills/Plugins, MCP, Subagent and discovery combinations reject execution. No output repair, coercion or extra model loop. Arbitrary schema dialects are unverified. |
+| Function configuration, saved/inline Agents | Required name/description/schema; `defer_loading` defaults false. Protocol errors, repeated names and explicit non-object root types reject with the official fields ([validation](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/official-semantics-alignment.md#agent-configuration-validation--september-23)). Saved references resolve into an immutable Session snapshot. Codex/Claude real calls: F1/F2/M2/S1/S2. [Parser](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/internal/api/function_configuration.go), [saved tools](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/internal/api/saved_tools.go). | MiniMax public functions reject. Claude requires an explicit object root. Local nonblank/512-byte name and 64-definition Session bounds are compatibility gaps. Saving configuration alone does not qualify execution. |
+| Function-result admission, `events.create` | Required `turn_id`, `call_id`, `success`; optional nullable `error` and `output`. Output is string or ordered text/image content. Scoped atomic batches retain field presence, original content and retry identity in storage; public result Items and events always carry `output` and `error`, null when not submitted ([item serialization](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/history-events-usage.md#item-serialization-2026-09-23)). Same result retries are accepted; changed results and results after cancellation return 409 `conflict_error`, including after terminal state. In the caller's Session, an unknown call or a call of another Turn returns 400 `invalid_request_error` without changing the pending action; missing and foreign Sessions stay 404 ([error rows](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/official-semantics-alignment.md#session-input-conflicts-and-result-targets--september-23)). F1/F2/M2 plus [controlled SDK/raw checks](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/tests/official_function_inputs.py). [Parser](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/internal/api/function_inputs.go), [Store](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/internal/store/function_inputs.go). | Admission is separate from application and public Item publication. Invalid or unqualified content cannot consume a pending call. Core messages omit the call and executor IDs that official messages name; hosted defaults and publication timing remain unverified. |
+| Function text results and native application | Codex waits for a matching live root dynamic-tool completion; Claude waits for a matching live root native tool result. Text/error results, retry/conflict, cancellation and cold continuation: F1/F2. [Receipt contract](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/function-result-images.md). | Transport writes alone do not confirm application. Confirmation is not provider consumption, crash recovery or exactly-once external effects. No automatic result replay. |
| Function image results | Successful ordered inline PNG/JPEG with text, large PNG and image-only JPEG: Codex/Claude `none` F1/F2; Docker M2. Public Items retain submitted bytes. Codex receipt regression: F2. | Claude rejects failed images and remote references before persistence; native resizing may change its image bytes. MiniMax functions and self-hosted image workflows are unqualified. Other Codex image/error/reference combinations cannot be inferred from successful-inline evidence. |
-| Deferred function discovery | Type-only `tool_search` plus mixed eager/deferred functions: Claude SDK 0.3.269/native 2.1.269, Kimi K3, single Agent, medium, `none`, text results; text and PNG input D1. Native provider observations establish lazy schema loading. [Contract](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/tool-search.md). | A repeated `tool_search` is a protocol error. Codex/MiniMax discovery, search-only/missing-search, workspace, MCP, structured-output and Subagent combinations remain gaps. Opaque native policy changes lack a reliable pre-input deferral signal. Saved tools include `tool_search`; the pinned Session response union excludes it. Exact hosted projection is unverified. |
-| Explicit disabled search/PTC | Saved and inline `web_search.mode=disabled` and `programmatic_tool_calling.enabled=false`; shared native controls on initial and cold execution. All three harnesses on `none`: P1, including native inventory/control evidence. [Contract](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/tool-policy.md), [parser](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/internal/api/disabled_tools.go). | Unsupported explicit enablement rejects at Session admission; saved Agents keep every pinned search mode as resource data (TV-05), and Sessions from such Agents reject unless they replace the tools. A repeated `web_search` is a protocol error. Omission retains approved native behavior, which does not establish official default-on PTC parity. Enabled search and default/error parity remain gaps; unrelated native utilities are not implicitly removed. |
-| Agent service-origin MCP | Implemented Codex/Claude HTTP `none` profiles, anonymous/static bearer, scoped Vault selection and native `mcp_call` Items. [Configuration and qualification limits](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/README.md#http-mcp-execution), [profiles](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/internal/engine/profile.go). | This closure batch does not requalify MCP/provider combinations. MiniMax, self-hosted/hosted service-origin MCP, OAuth, nonempty inline headers/metadata and other transports/origins reject. Claude requires a static connected inventory; original MCP-envelope fidelity and continuing server health are unverified. |
-| Environment Plugin MCP/native tools | Separate [Docker Plugin transport matrix](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/environment-templates.md#environment-origin-mcp-plugins) and [V1 deployment evidence](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/user-managed-runtime-v1.md). Native tools stay within the existing colocated Runtime. | Plugin MCP is not Agent service-origin MCP or a public function action. No new optional cross-product is qualified here. Native utility inventories need not be identical. |
-| Required action: `function_call` | Session GET/list and Session SSE expose persisted `{arguments,call_id,name,turn_id,type}`. Actions remain pending until native application or cancellation/terminal settlement. [Projection](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/internal/store/function_state.go), [confirmation](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/internal/store/function_results.go); real handling F1/F2/M2/S1/S2/D1; explicit client disconnect/query/reconnect: F3. | Function-call history is not pending-state authority. Client reconnect does not replay events or redo external application effects. |
-| Required action: `environment_connection` | Offline waiting input exposes `{environment_id,type}` before Turn creation. Connect the exact Session Environment through our scoped daemon enrollment. Three-harness Docker/E2B execution: E1; explicit initial pending-input/query/connection/native completion: E2; expiry: [controlled initial-input test](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/tests/official_self_hosted_initial.py). | Idle offline Sessions without pending input request nothing. Registration alone is not connectivity or native readiness. Stock `exec-server`/Noise transport is outside the approved V1 route. Exact upstream registration/action-removal timing remains unverified. |
-| Stream disconnect and execution loss | SSE is live-only. Reconnect, buffer events, retrieve Session/Turn/Items and deduplicate Item IDs. Worker recovery fails previously claimed work without replay; queued work can remain. [Recovery contract](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/README.md#public-execution-admission), [connection reconciliation](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/internal/store/environment_connection_recovery.go). | Client disconnect and daemon/Core loss are different cases. F3 qualifies client-stream loss and continued pending function handling; F2 qualifies native process loss with an unapplied saved result. Neither promises replay or recovery of unknown external effects. |
+| Deferred function discovery | Type-only `tool_search` plus mixed eager/deferred functions: Claude SDK 0.3.269/native 2.1.269, Kimi K3, single Agent, medium, `none`, text results; text and PNG input D1. Native provider observations establish lazy schema loading. [Contract](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/tool-search.md). | A repeated `tool_search` is a protocol error. Codex/MiniMax discovery, search-only/missing-search, workspace, MCP, structured-output and Subagent combinations remain gaps. Opaque native policy changes lack a reliable pre-input deferral signal. Saved tools include `tool_search`; the pinned Session response union excludes it. Exact hosted projection is unverified. |
+| Explicit disabled search/PTC | Saved and inline `web_search.mode=disabled` and `programmatic_tool_calling.enabled=false`; shared native controls on initial and cold execution. All three harnesses on `none`: P1, including native inventory/control evidence. [Contract](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/tool-policy.md), [parser](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/internal/api/disabled_tools.go). | Unsupported explicit enablement rejects at Session admission; saved Agents keep every pinned search mode as resource data (TV-05), and Sessions from such Agents reject unless they replace the tools. A repeated `web_search` is a protocol error. Omission retains approved native behavior, which does not establish official default-on PTC parity. Enabled search and default/error parity remain gaps; unrelated native utilities are not implicitly removed. |
+| Agent service-origin MCP | Implemented Codex/Claude HTTP `none` profiles, anonymous/static bearer, scoped Vault selection and native `mcp_call` Items. [Configuration and qualification limits](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/README.md#http-mcp-execution), [profiles](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/internal/engine/profile.go). | This closure batch does not requalify MCP/provider combinations. MiniMax, self-hosted/hosted service-origin MCP, OAuth, nonempty inline headers/metadata and other transports/origins reject. Claude requires a static connected inventory; original MCP-envelope fidelity and continuing server health are unverified. |
+| Environment Plugin MCP/native tools | Separate [Docker Plugin transport matrix](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/environment-templates.md#environment-origin-mcp-plugins) and [V1 deployment evidence](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/user-managed-runtime-v1.md). Native tools stay within the existing colocated Runtime. | Plugin MCP is not Agent service-origin MCP or a public function action. No new optional cross-product is qualified here. Native utility inventories need not be identical. |
+| Required action: `function_call` | Session GET/list and Session SSE expose persisted `{arguments,call_id,name,turn_id,type}`. Actions remain pending until native application or cancellation/terminal settlement. [Projection](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/internal/store/function_state.go), [confirmation](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/internal/store/function_results.go); real handling F1/F2/M2/S1/S2/D1; explicit client disconnect/query/reconnect: F3. | Function-call history is not pending-state authority. Client reconnect does not replay events or redo external application effects. |
+| Required action: `environment_connection` | Offline waiting input exposes `{environment_id,type}` before Turn creation. Connect the exact Session Environment through our scoped daemon enrollment. Three-harness Docker/E2B execution: E1; explicit initial pending-input/query/connection/native completion: E2; expiry: [controlled initial-input test](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/tests/official_self_hosted_initial.py). | Idle offline Sessions without pending input request nothing. Registration alone is not connectivity or native readiness. Stock `exec-server`/Noise transport is outside the approved V1 route. Exact upstream registration/action-removal timing remains unverified. |
+| Stream disconnect and execution loss | SSE is live-only. Reconnect, buffer events, retrieve Session/Turn/Items and deduplicate Item IDs. Worker recovery fails previously claimed work without replay; queued work can remain. [Recovery contract](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/README.md#public-execution-admission), [connection reconciliation](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/internal/store/environment_connection_recovery.go). | Client disconnect and daemon/Core loss are different cases. F3 qualifies client-stream loss and continued pending function handling; F2 qualifies native process loss with an unapplied saved result. Neither promises replay or recovery of unknown external effects. |
## Required-action recovery contract
@@ -52,7 +52,7 @@ application already performed the function, keep and submit that saved result;
do not run an external effect again merely because an action remains visible.
Core's acknowledgement boundary is native application. For an environment action,
connect its exact Environment using the existing enrollment authorization.
-[Connection observations](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/internal/store/environment_connections.go)
+[Connection observations](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/internal/store/environment_connections.go)
fence stale generations; transport connection can clear pre-Turn activity before
native preparation. Neither registration nor an action disappearing proves that
a model ran successfully. Query the matching Turn and Items for its outcome.
@@ -61,8 +61,8 @@ Two timing questions remain open. Repeated `requires_action` notification order
and acknowledgement timing are local implementation choices, not proven upstream
semantics. Also, an admitted result that is cancelled before native observation
can remain internally saved without a public output Item or `item.added`.
-[Item publication](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/internal/store/item_projection.go) and
-[the existing coverage record](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/README.md#public-function-configuration) preserve
+[Item publication](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/internal/store/item_projection.go) and
+[the existing coverage record](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/README.md#public-function-configuration) preserve
this gap. Successful retry cannot manufacture the missing observation or establish
that the model consumed the result.
@@ -82,12 +82,12 @@ not a claim that every historical workflow was rerun at the current baseline.
| M2 | `20260922/workspace-images/validation-summary.json`; Codex `public-run-_lr5uiy_`, Claude `public-run-sb65p9e9`; candidate `2c295116d66ed9aafc808ec49a8cb6dcb5c674c2`, merge `1adb2489bc3415039440224e9a7905c53e68a557` ([#17](https://github.com/MiniMax-AI/parsar-core/pull/17)). Kimi K3, Codex 0.153.4, Claude SDK 0.3.269/native 2.1.269; Docker seven-Turn image/workspace workflow. Codex's receipt conclusion is superseded by F2. |
| F1 | `20260922/function-result-images/validation-summary.json`, `validated/function-image-public-2567456666/public.json`; candidate `d3cdb225bc7628b968b76c7e1b400101894ce8cd`, merge `fd23f169e1ede1b2f1c39a1d9dcce71886e3c006` ([#16](https://github.com/MiniMax-AI/parsar-core/pull/16)). Claude SDK 0.3.269/native 2.1.269 with Kimi K3 on `none`; success PNG/JPEG, failed text, retries, cancellation, history continuation. Earlier Codex transport-only evidence does not qualify current receipts. |
| F2 | `20260922/function-result-receipts/validation-summary.json`, `candidate-public-owner-sdk.log`; hosted `public-run-4fn70foy` (104.13s), `none` `function-image-public-945075091` (83.918s). Candidate `6dceb98e1c56469fcc70cd82025ce77fc870ed3d`, merge `206474a4c10901442b1faa094281bfb5559082b5` ([#20](https://github.com/MiniMax-AI/parsar-core/pull/20)); real Kimi, Codex 0.153.4. Qualifies native receipts, not completed provider consumption. |
-| F3 | `20260922/execution-tools-closure/pending-actions/validation-summary.json`; Codex `codex/public-run-8gexf5a8` (65.58s), Claude `claude_sdk/public-run-5f2tr7e_` (74.33s), production source `206474a`. The same [public verifier](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/tests/official_pending_actions_native.py) checks disconnected-client pending queries, success/error/cancel, original results, target isolation, retry/conflict and no implicit call replay with real Kimi on Docker. No Core restart or native-loss injection is claimed by these runs. |
+| F3 | `20260922/execution-tools-closure/pending-actions/validation-summary.json`; Codex `codex/public-run-8gexf5a8` (65.58s), Claude `claude_sdk/public-run-5f2tr7e_` (74.33s), production source `206474a`. The same [public verifier](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/tests/official_pending_actions_native.py) checks disconnected-client pending queries, success/error/cancel, original results, target isolation, retry/conflict and no implicit call replay with real Kimi on Docker. No Core restart or native-loss injection is claimed by these runs. |
| S1 | `20260922/structured-output/acceptance-summary.json`, `structured-public-1905281777/public.json`; real candidate `7f533d46c472f3cdf7c16ce9471c225a2ba7eded`, merge `8716e699dbc34904499c94b6b0b03857bbebfaad` ([#11](https://github.com/MiniMax-AI/parsar-core/pull/11)). Claude/Kimi `none` schema/function execution and cold continuation. Schema-specific active steering was not separately qualified here. |
| S2 | `20260922/hosted-structured-output/validation-summary.json`, `validated/public-inline.log`, `public-run-qvq3csf1` (186.09s); candidate `f6d2c3f2a1dc2ff5f177213bd2b20d78496011d0`, merge `aa85d8ecc60e0a8b7f2494b73caa81216ee197e2` ([#18](https://github.com/MiniMax-AI/parsar-core/pull/18)). Claude SDK 0.3.269/native 2.1.269, real Kimi; Docker saved/inline schema, prepared/active input, native files and four completed structured answers. |
| D1 | `20260922/deferred-tools/tool-search-public-2039155387/public.json`, `public-image-isolated-tests.log` (86.71s); merge `178507ae7a63d4068e1e82bef9cd256ba398ae00` ([#13](https://github.com/MiniMax-AI/parsar-core/pull/13)). Claude SDK 0.3.269/native 2.1.269/Kimi K3 `none`; text results with initial/active PNGs, cancellation and cold continuation. `claude-native-1790052750` separately records provider schema inventories and native feasibility. |
| P1 | `20260922/tool-policy/acceptance.json`, `server-evidence/tool-policy-{codex-2495445746,claude_sdk-2539346823,mcode-3747575401}/public.json`; candidate `eeb432c7495265ae3a9309e32f5b4323611c3245`, merge `4b8754a6eda6696d9b18eb8d583953ac16c6800f` ([#14](https://github.com/MiniMax-AI/parsar-core/pull/14)). Codex/Claude Kimi K3, MiniMax M2.7; four `none` Sessions per harness, native disable controls and cold continuation. |
-| E1 | `20260921/self-hosted-onboarding/{source-candidate.json,source-verified.json,live,live-e2b}`; candidate `8f0cd2530d7b58cb7fb3ea124a1fc43dacecca36`. [Exact Docker/E2B run paths and limits](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/user-managed-runtime-v1.md#evidence-and-verification-boundaries): Codex Kimi K3 Responses, Claude Kimi K3 Anthropic-compatible API, MiniMax M2.7, immutable Linux amd64 Runtime builds. Native execution, restart/history, cancellation, Files/Artifacts and isolation; shared credential lifecycle evidence is not a separate live rotation run for every E2B profile. |
+| E1 | `20260921/self-hosted-onboarding/{source-candidate.json,source-verified.json,live,live-e2b}`; candidate `8f0cd2530d7b58cb7fb3ea124a1fc43dacecca36`. [Exact Docker/E2B run paths and limits](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/user-managed-runtime-v1.md#evidence-and-verification-boundaries): Codex Kimi K3 Responses, Claude Kimi K3 Anthropic-compatible API, MiniMax M2.7, immutable Linux amd64 Runtime builds. Native execution, restart/history, cancellation, Files/Artifacts and isolation; shared credential lifecycle evidence is not a separate live rotation run for every E2B profile. |
| E2 | `20260922/execution-tools-closure/environment-actions/result.json` contains Codex/Claude Kimi passes (13 checks each) and the retained MiniMax direct-network failure; `mcode-relay-retry/result.json` separately passes MiniMax M2.7 (13 checks) after correcting provider routing. Source `206474a`; same shared SDK/raw workflow, isolated Core/PostgreSQL and user-managed Docker. Covers initial creation SSE, client disconnect, exact pending Environment/no Turn or Items, scoped enrollment, one actual completed model Turn and creation retries preserving history. This is connection-action qualification, not another full lifecycle or E2B regression. |
Controlled tests establish parser, projection, atomicity and race behavior; native
@@ -121,4 +121,4 @@ remain separate limitations. They do not authorize silently relabeling an unprov
principal workflow as deferred. OAuth, full Usage, release publication, new tool
kinds and new execution loops are outside this closure batch.
-[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/execution-tools.md)
+[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/execution-tools.md)
diff --git a/apps/docs/content/docs/api-reference/core/core-administration.mdx b/apps/docs/content/docs/api-reference/core/core-administration.mdx
index af6d73f49..0ed580f68 100644
--- a/apps/docs/content/docs/api-reference/core/core-administration.mdx
+++ b/apps/docs/content/docs/api-reference/core/core-administration.mdx
@@ -30,12 +30,12 @@ _openapi:
assert active Turn settlement. Read this after an uncertain archive
response; never infer released from a missing sandbox alone.
- content: >-
- Core key only. Requires maintenance and the current deployment
- generation. Permanently closes execution, requests cancellation and
- releases sandbox/snapshots through existing cleanup. Session history
- and persisted files/artifacts remain; unpersisted workspace contents
- are lost. A cleanup_pending response is not proof of resource release.
- Does not affect caller-managed Runtime.
+ Core key only. Requires the current deployment generation; no
+ maintenance mode is required. Permanently closes execution, requests
+ cancellation and releases sandbox/snapshots through existing cleanup.
+ Session history and persisted files/artifacts remain; unpersisted
+ workspace contents are lost. A cleanup_pending response is not proof
+ of resource release. Does not affect caller-managed Runtime.
- content: >-
Core key only. Each observation is labelled with its owning Project
ID. Uses the existing read-only Runtime sampler, with bounded
diff --git a/apps/docs/content/docs/api-reference/core/sandbox-manager.mdx b/apps/docs/content/docs/api-reference/core/sandbox-manager.mdx
index f05ee6363..c0f819bcf 100644
--- a/apps/docs/content/docs/api-reference/core/sandbox-manager.mdx
+++ b/apps/docs/content/docs/api-reference/core/sandbox-manager.mdx
@@ -21,21 +21,29 @@ _openapi:
sandbox_configuration_error while the public URL is loopback. E2B
credentials are write-only. E2B may omit resources to adopt the
validated template build's CPU and memory, returned in
- specification.resources. Exact retries return the existing selection;
- differing selections and file-managed deployments reject. This does
- not create compute or execute work.
- - content: >-
- Requires maintenance, the current generation and verified cleanup of
- all old resources. Credentials are write-only. E2B may omit resources
- to adopt the validated template build's CPU and memory. A core_url
- member is rejected with 400; the address comes from the installation
- public URL. Historical records are retained; old node credentials and
- enrollments are retired. Explicitly resume after success. Never
- automatically retry an uncertain write.
- - content: >-
- Requires the current generation. Maintenance preserves execution,
- cleanup and history. Resume requires successfully activated provider
- configuration.
+ specification.resources. Requires explicit expected_generation,
+ including zero at first setup. Stale retries reject before provider
+ validation. An identical selection at the current generation is a
+ no-op; differing selections and file-managed deployments reject. This
+ does not create compute or execute work.
+ - content: >-
+ Requires the current generation, the same backend type, no active
+ reset and verified cleanup of all old resources. A backend type change
+ requires reset. Credentials are write-only. E2B may omit resources to
+ adopt the validated template build's CPU and memory. A core_url member
+ is rejected with 400; the address comes from the installation public
+ URL. Historical records are retained; old node credentials and
+ enrollments are retired. Never automatically retry an uncertain write.
+ - content: >-
+ Archives hosted Sessions and waits for confirmed provider cleanup,
+ preserving history and Files/Artifacts. Auto waits for started or
+ waiting Turns and file writes until the durable deadline; force
+ cancels them. The same clear is idempotent; force escalates auto.
+ Requires the current generation. Self-hosted Sessions are unchanged.
+ - content: >-
+ Restores admission but never restores Sessions already archived. With
+ no reset running this is an idempotent read, provided the generation
+ still matches.
- content: >-
Core key only. Does not grant project resource access. Responses
contain only explicit safe fields.
@@ -58,4 +66,4 @@ _openapi:
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/apps/docs/content/docs/bootstrap-projects-keys.mdx b/apps/docs/content/docs/bootstrap-projects-keys.mdx
index 31aef04ce..7b44799a4 100644
--- a/apps/docs/content/docs/bootstrap-projects-keys.mdx
+++ b/apps/docs/content/docs/bootstrap-projects-keys.mdx
@@ -69,7 +69,7 @@ and display only. Keep deployment, application, node and provider credentials ou
Installation creates no Project or application key. An administrator creates a
Project and issues named keys on the console's **Projects and keys** page, which
-uses the [administrator API](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/admin-api.md), or with that
+uses the [administrator API](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/admin-api.md), or with that
API directly.
The Project owns one tenant and one principal; its keys share assets and permissions.
Writes retain each key's provenance. All Projects and application keys live in
@@ -98,4 +98,4 @@ management request points to the console's Core key or Core connection. A resour
deletion conflict must remain visible; it does not authorize an execution call.
Node and daemon `/api/v1` routes and native Runtime interfaces retain their own authentication.
-[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/docs/web/core-connection.md)
+[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/docs/web/core-connection.md)
diff --git a/apps/docs/content/docs/concepts.mdx b/apps/docs/content/docs/concepts.mdx
index cd2b5bd00..9f8505337 100644
--- a/apps/docs/content/docs/concepts.mdx
+++ b/apps/docs/content/docs/concepts.mdx
@@ -83,6 +83,6 @@ ownership. No request bodies, secrets or file contents enter audit records.
Do not add product users, RBAC, cross-Project shared assets, administrator execution, or old
private-protocol compatibility to this management model. Existing public
-execution and Runtime ownership rules remain in [CONTRIBUTING.md](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/CONTRIBUTING.md).
+execution and Runtime ownership rules remain in [CONTRIBUTING.md](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/CONTRIBUTING.md).
-[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/docs/design-principles.md)
+[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/docs/design-principles.md)
diff --git a/apps/docs/content/docs/configure.mdx b/apps/docs/content/docs/configure.mdx
index 11653093f..33a9b1aaf 100644
--- a/apps/docs/content/docs/configure.mdx
+++ b/apps/docs/content/docs/configure.mdx
@@ -116,7 +116,7 @@ output or in Core's settings snapshot. Model providers are not process settings;
The schema is
-[`deploy/install/config.schema.json`](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/deploy/install/config.schema.json); each
+[`deploy/install/config.schema.json`](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/deploy/install/config.schema.json); each
installation keeps a copy in `generated/config.schema.json` for editors. Core serves
the non-secret settings snapshot, with the path of `config.json` and the apply
command, at `GET /core/v1/installation`.
@@ -128,7 +128,7 @@ Core API with the Core key.
| Setting | Where in Web | Core API | Notes |
| --- | --- | --- | --- |
-| Sandbox backend: Docker, microsandbox or E2B | **Nodes** (**Sandbox backend** with E2B): the setup wizard, ending with **Save configuration** | `/core/v1/sandbox/deployment` | One backend per deployment. `install.sh --sandbox` saves the first choice. To change it, see the Nodes page; that flow will change in a coming release |
+| Sandbox backend: Docker, microsandbox or E2B | **Nodes** (**Sandbox backend** with E2B): the setup wizard, ending with **Save configuration** | `/core/v1/sandbox/deployment` | One backend per deployment. `install.sh --sandbox` saves the first choice. Changing backend requires explicit reset and a new setup at the resulting generation |
| Sandbox size and Runtime release | **Nodes**: the setup wizard | `/core/v1/sandbox/deployment` | Every sandbox gets the same size. See [Sandbox deployment](#sandbox-deployment) |
| E2B API key and template build | **Nodes**: the setup wizard's **E2B cloud** (the page is then called **Sandbox backend**) | `/core/v1/sandbox/deployment` | The key is write-only and encrypted |
| Nodes and their capacity | **Nodes**: **Add node**, **Edit node**, **Remove node** on a node's page (**Remove** in its list row) | `/core/v1/sandbox/enrollment-tokens`, `/core/v1/sandbox/nodes` | See [Node capacity](#node-capacity) and the [nodes guide](/hosted-providers) |
@@ -145,7 +145,7 @@ Which harnesses are enabled, and the default one, are process settings
Exactly one provider serves the deployment: Docker, microsandbox or E2B. The
deployment specification holds the per-sandbox resources and, for Docker and
microsandbox, the Runtime release. Web's setup proposes the Standard size from
-[`standard-sizes.json`](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/apps/web/src/features/sandbox/standard-sizes.json) and
+[`standard-sizes.json`](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/apps/web/src/features/sandbox/standard-sizes.json) and
derives Small and Large from it; the installer applies the bundle's copy of the same
file. The database owns the saved values.
@@ -165,8 +165,8 @@ Changing the provider, the size or the Runtime applies to the whole deployment;
**Nodes** page, and note that this flow will change in a coming release. A successful
change retires the old nodes and enrollment commands; history stays, and existing
Sessions never move between providers. See the
-[operator reference](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/HOSTED-SANDBOX-MANAGER.md#removal-and-maintenance)
-and the [deployment contract](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/sandbox-deployment.md).
+[operator reference](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/HOSTED-SANDBOX-MANAGER.md#removal-and-reset)
+and the [deployment contract](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/sandbox-deployment.md).
### Node capacity
@@ -193,7 +193,7 @@ Session freezes the provider it starts with, so a later change affects only new
Sessions. Self-hosted Sessions never use it. A hosted or self-hosted Session with no
provider is refused with 400 `model_provider_required`. See
[model providers](/quickstart#model-providers) and the
-[model execution contract](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/model-execution.md#deployment-defaults).
+[model execution contract](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/model-execution.md#deployment-defaults).
The operator file `AGENTS_API_EXECUTION_OPTIONS_FILE` is retired; see
[Upgrade notes](/troubleshooting#upgrade-notes).
@@ -213,7 +213,7 @@ A Web-only installation has only `secrets/core.key`, a copy of its Core's key.
Core reads only its environment. The installer renders `generated/core.env` from
`config.json`; if you run Core yourself (see
-[Maintainers and advanced deployments](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/docs/maintainers.md)), set these variables. Compose
+[Maintainers and advanced deployments](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/docs/maintainers.md)), set these variables. Compose
loads the file with `env_file` and systemd with `EnvironmentFile`, so Compose must be
2.26.0 or newer.
@@ -228,7 +228,7 @@ loads the file with `env_file` and systemd with `EnvironmentFile`, so Compose mu
| `OAC_INSTALLATION_ID` | `state.json`; pinned to the database |
| `OAC_SETTINGS_FILE` | `generated/settings.json`, the snapshot Core serves at `GET /core/v1/installation`; Core does not act on it |
| `OAC_EXECUTION_CONCURRENCY`, `OAC_DEFAULT_HARNESS`, `OAC_HARNESSES`, `OAC_WRITE_AUDIT_RETENTION`, `OAC_OAUTH_TRUSTED_ORIGINS` | The matching `core.*` settings |
-| `OAC_HISTORY_SETTINGS_FILE` | `generated/runtime-history.json` when `core.runtime_history` is set; see the [history contract](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/runtime-history-api.md) |
+| `OAC_HISTORY_SETTINGS_FILE` | `generated/runtime-history.json` when `core.runtime_history` is set; see the [history contract](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/runtime-history-api.md) |
| `OAC_LOG_LEVEL`, `OAC_LOG_FORMAT`, `OAC_LOG_ADD_SOURCE` | `log.*`; Web reads the same three |
| `OAC_E2B_STATE_DIR`, `OAC_E2B_PROVIDER_BIN` | The E2B receipt directory and, for native Core, the bundled helper. The E2B key and template live in the database |
@@ -250,4 +250,4 @@ paths must be canonical absolute paths without control characters, quotes, backs
or wildcards. Keep the installation ID and the database together; Core refuses a
missing installation ID when its database already has a deployment.
-[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/docs/configuration.md)
+[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/docs/configuration.md)
diff --git a/apps/docs/content/docs/console.mdx b/apps/docs/content/docs/console.mdx
index de30947cf..6af130720 100644
--- a/apps/docs/content/docs/console.mdx
+++ b/apps/docs/content/docs/console.mdx
@@ -24,10 +24,10 @@ management client, and it sends nothing to `/v1`.
| Monitor | Sandbox metrics | Node capacity and hosted Runtime CPU and memory across Projects |
| Monitor | Session log | Every Session, opening one Session's read-only conversation, trace and Turns; a self-hosted Session's page also manages its executor credentials and gives the command that connects a host |
| Resources | Agents, Environment templates, Skills, Files, Vaults | Inspection and permitted deletion |
-| Platform | Projects and keys, Nodes, System | Project and key lifecycle; sandbox deployment and nodes; System: the installation's public address, API base URL, ID and source commit (read-only), each harness's default model provider (write-only key) beside its read-only startup state, the sandbox configuration every Project shares: provider, sandbox size, Runtime or E2B template build, idle suspension and maintenance, and Core's config.json startup settings with where to change them |
+| Platform | Projects and keys, Nodes, System | Project and key lifecycle; sandbox deployment and nodes; System: the installation's public address, API base URL, ID and source commit (read-only), each harness's default model provider (write-only key) beside its read-only startup state, the sandbox configuration every Project shares: provider, sandbox size, Runtime or E2B template build, idle suspension and reset, and Core's config.json startup settings with where to change them |
Missing data is shown as missing (—), never as zero. How each figure is read and
-bounded is recorded in [management interface coverage](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/docs/web/protocol-coverage.md).
+bounded is recorded in [management interface coverage](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/docs/web/protocol-coverage.md).
## Management scope
@@ -60,15 +60,15 @@ only the console server sends it to Core.
- [Connection and authentication](/bootstrap-projects-keys)
- [Architecture and ownership](/execution-model)
-- [Management interface coverage](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/docs/web/protocol-coverage.md)
-- [Frontend handoff and acceptance](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/docs/web/roadmap.md)
-- [React application](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/apps/web/README.md)
+- [Management interface coverage](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/docs/web/protocol-coverage.md)
+- [Frontend handoff and acceptance](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/docs/web/roadmap.md)
+- [React application](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/apps/web/README.md)
-The [administrator API contract](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/admin-api.md) defines
-management routes and resource behavior. The [public API contracts](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/README.md)
+The [administrator API contract](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/admin-api.md) defines
+management routes and resource behavior. The [public API contracts](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/README.md)
define the separate application interface. See the [design principles](/concepts)
-for ownership and [contributor guide](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/CONTRIBUTING.md) for required checks.
+for ownership and [contributor guide](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/CONTRIBUTING.md) for required checks.
-OpenAgentCore Web is available under the [MIT License](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/LICENSE).
+OpenAgentCore Web is available under the [MIT License](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/LICENSE).
-[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/docs/web/README.md)
+[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/docs/web/README.md)
diff --git a/apps/docs/content/docs/environments-and-files.mdx b/apps/docs/content/docs/environments-and-files.mdx
index ab045960d..566f1d3c9 100644
--- a/apps/docs/content/docs/environments-and-files.mdx
+++ b/apps/docs/content/docs/environments-and-files.mdx
@@ -3,16 +3,16 @@ title: "Environments and files"
description: "Execution placement, Environment resources and workspace ownership."
---
-This assessment covers the fixed [Python SDK contract](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/upstream.json), with partial
+This assessment covers the fixed [Python SDK contract](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/upstream.json), with partial
coverage. Core-managed Docker runs the three qualified native harnesses. V1
user-managed `self_hosted` enrollment uses the same colocated Runtime for Codex,
-Claude SDK and MiniMax at `/workspace`; see its [real deployment qualification](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/user-managed-runtime-v1.md).
+Claude SDK and MiniMax at `/workspace`; see its [real deployment qualification](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/user-managed-runtime-v1.md).
For hosted compute, the deployment selects E2B, Docker or microsandbox through
-[Hosted Sandbox Manager](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/HOSTED-SANDBOX-MANAGER.md).
+[Hosted Sandbox Manager](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/HOSTED-SANDBOX-MANAGER.md).
For the separate caller-managed E2B path, the user owns allocation, renewal and
-cleanup through the official SDK and [Runtime packaging](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/deploy/e2b/README.md).
-[Templates](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/environment-templates.md) remain a hosted-only resource path;
-[Files](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/environment-files.md) reuse the exact authorized local workspace.
+cleanup through the official SDK and [Runtime packaging](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/deploy/e2b/README.md).
+[Templates](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/environment-templates.md) remain a hosted-only resource path;
+[Files](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/environment-files.md) reuse the exact authorized local workspace.
The internal Store now owns a durable Environment association for newly created
`self_hosted` and `openai_hosted` snapshots, atomically with Session creation.
@@ -30,15 +30,15 @@ readiness. Rotation/revocation, Session deletion and ownership loss deny further
access without promising immediate cessation of native effects. Native history
remains local to the bound Runtime and cannot be replaced on retry. There is no
registry/Noise relay or transient service-side harness credential.
-See the [enrollment guide](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/README.md#user-managed-runtime-enrollment).
+See the [enrollment guide](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/README.md#user-managed-runtime-enrollment).
## Basic public Docker-hosted profile
An explicitly configured default managed Docker provider enables `type=openai_hosted`
for the qualified Codex, Claude Code and MiniMax Code profiles. Each uses the same
Runtime lifecycle and workspace interfaces with its own native adapter/isolation.
-See the [engine profile guides](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/README.md#public-engine-profiles) for setup and limits.
-The standalone [operator configuration](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/deploy/codex/README.md#standalone-operator-configuration)
+See the [engine profile guides](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/README.md#public-engine-profiles) for setup and limits.
+The standalone [operator configuration](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/deploy/codex/README.md#standalone-operator-configuration)
selects the qualified immutable Runtime image; advertised capabilities alone do
not enable admission. An idle or initial-text creation commits Session, Environment
and retry identity before the existing leased Worker provisions its allocation.
@@ -50,7 +50,7 @@ policies use the same qualified image with adapter-selected immutable native pol
Templates and inline configuration share initial files, env, packages, ordered setup
and inline or tenant-owned referenced Skills through the hosted initializer.
Unsupported hostname forms and installation combinations reject explicitly; see
-the [Template coverage and limits](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/environment-templates.md). Empty/null installation
+the [Template coverage and limits](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/environment-templates.md). Empty/null installation
defaults produce safe empty metadata, not a live workspace inventory. Service-origin
hosted MCP remains unsupported; Environment Plugin MCP has its
own qualified transport matrix.
@@ -70,11 +70,11 @@ semantics; this profile does not establish complete Environment compatibility.
The application creates, renews and destroys its E2B sandbox through the official
SDK. It deploys the shared Runtime, then enrolls that Runtime into a `self_hosted`
Session. Core neither keeps an E2B allocation nor issues Provider renew/kill calls.
-The [E2B guide](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/deploy/e2b/README.md) owns packaging and
+The [E2B guide](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/deploy/e2b/README.md) owns packaging and
user-side lifecycle instructions. Expiry or lost workspace/history must not trigger
-transparent replacement or replay. The [new enrollment qualification](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/user-managed-runtime-v1.md)
+transparent replacement or replay. The [new enrollment qualification](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/user-managed-runtime-v1.md)
records its own real deployment evidence.
-The [prior E2B qualification](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/README.md#e2b-v1-qualification) records a historical
+The [prior E2B qualification](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/README.md#e2b-v1-qualification) records a historical
Core-managed route; it does not establish this caller-managed enrollment result.
Current deployment-managed E2B is a separate hosted configuration in the manager guide.
@@ -111,7 +111,7 @@ connection alone is not readiness. Retries preserve identity and the five-minute
database deadline. HTTP disconnect retains the reservation. Local expired/cancelled
outcomes return 409, lost ownership 503, and deletion 404; exact hosted error
status/body and pending-input crash recovery are unverified. See the
-[canonical wait rules](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/CONTRIBUTING.md#environment-ownership-and-placement).
+[canonical wait rules](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/CONTRIBUTING.md#environment-ownership-and-placement).
Cancellation-only batches use the existing locked admission and native delivery.
An idle cancellation creates no Turn, and retry identity preserves the original
@@ -191,7 +191,7 @@ from Session and Turn status.
## Ownership and placement decision
-The canonical [architecture rules](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/CONTRIBUTING.md#environment-ownership-and-placement)
+The canonical [architecture rules](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/CONTRIBUTING.md#environment-ownership-and-placement)
keep Environment lifecycle common while leaving process placement and native
transport to adapters. Logical ownership does not require a machine per object.
@@ -215,4 +215,4 @@ native history independently of disposable compute, or prove native restoration;
never treat an Environment ID as a filesystem or history backup. Do not silently
move an existing Session away from its bound device.
-[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/environments.md)
+[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/environments.md)
diff --git a/apps/docs/content/docs/execution-model.mdx b/apps/docs/content/docs/execution-model.mdx
index 07b150217..1c57f5eee 100644
--- a/apps/docs/content/docs/execution-model.mdx
+++ b/apps/docs/content/docs/execution-model.mdx
@@ -10,7 +10,7 @@ Agents API independently with their own Project keys. The management backend,
`AdminClient` and the React console built on them are implemented.
The [design principles](/concepts) define identity and authority.
-The [administrator contract](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/admin-api.md) defines exact
+The [administrator contract](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/admin-api.md) defines exact
routes, payloads, pagination and audit records.
## Request boundaries
@@ -79,7 +79,7 @@ route.
Deployment sandbox management selects one provider at a time: E2B, Docker or
microsandbox. E2B uses the deployment's provider integration; Docker and microsandbox
-use operator-managed machines. Provider setup, maintenance and node administration
+use operator-managed machines. Provider setup, reset and node administration
belong to the existing sandbox management surface.
An application's `self_hosted` Runtime, including one it provisions in its own E2B
@@ -106,6 +106,6 @@ comes from the deployment's allocations; the hosted Runtime total counts hosted
observation records across projects and reported lifecycle states. These sources
have different coverage and refresh independently, so the console does not infer
resource retention or cleanup from their difference.
-Native execution ownership remains governed by [CONTRIBUTING.md](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/CONTRIBUTING.md).
+Native execution ownership remains governed by [CONTRIBUTING.md](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/CONTRIBUTING.md).
-[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/docs/web/architecture.md)
+[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/docs/web/architecture.md)
diff --git a/apps/docs/content/docs/hosted-providers.mdx b/apps/docs/content/docs/hosted-providers.mdx
index 7b8638a06..bad34441a 100644
--- a/apps/docs/content/docs/hosted-providers.mdx
+++ b/apps/docs/content/docs/hosted-providers.mdx
@@ -10,7 +10,7 @@ With E2B you need no nodes. Self-hosted executors, which applications run for th
own Sessions, are a different thing; see [Self-hosted executors](/self-hosted-execution).
You add a node by generating a command in Web and pasting it on the host. The
-[operator reference](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/HOSTED-SANDBOX-MANAGER.md) covers the
+[operator reference](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/HOSTED-SANDBOX-MANAGER.md) covers the
node protocol, manual registration, placement and failure handling.
## Before you add a node
@@ -187,7 +187,7 @@ The installation ID is in the command (`--installation-id`) and on the **System*
1. In Web, open **Nodes** and choose **Remove node** on the node's page, or
**Remove** in its list row, then **Confirm removal**. Core refuses while the node
still holds sandboxes, snapshots or pending cleanup; let them finish, or
- [archive their Sessions](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/HOSTED-SANDBOX-MANAGER.md#removal-and-maintenance)
+ [archive their Sessions](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/HOSTED-SANDBOX-MANAGER.md#removal-and-reset)
through the Core API. Removal is permanent: the host can come back only as a new
node.
2. Web then shows **Clean up the host** with the uninstall command. Run it on the host:
@@ -271,7 +271,7 @@ this flow will change in a coming release. A change applies to the whole deploym
and retires every node. Retired nodes drop out of the list, so there is nothing to
remove in Web: on each host, run the [uninstall command](#remove-a-node) (Core no longer
accepts the node, so it needs no `--force`), then add the host again with a new command.
-The [operator reference](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/HOSTED-SANDBOX-MANAGER.md#removal-and-maintenance)
+The [operator reference](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/HOSTED-SANDBOX-MANAGER.md#removal-and-reset)
describes today's procedure and API.
@@ -306,4 +306,4 @@ installation's resources:
Then use the new Core's Add node command. The new account, service and state use
`oac-node` and `~/.oac/nodes`; historical Core Session records remain in Core.
-[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/docs/getting-started/nodes.md)
+[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/docs/getting-started/nodes.md)
diff --git a/apps/docs/content/docs/index.mdx b/apps/docs/content/docs/index.mdx
index 1be8be55b..9311b1bfb 100644
--- a/apps/docs/content/docs/index.mdx
+++ b/apps/docs/content/docs/index.mdx
@@ -19,10 +19,10 @@ applications call Core with.
| [API reference](/public-api) | Developers | The `/v1`, `/core/v1` and `/api/v1` namespaces and their contracts |
Deeper references: the
-[nodes and sandbox backends](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/HOSTED-SANDBOX-MANAGER.md)
+[nodes and sandbox backends](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/HOSTED-SANDBOX-MANAGER.md)
reference for operators, the
-[protocol coverage and native differences](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/README.md), and
-[maintainers and advanced deployments](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/docs/maintainers.md) for building distributions
+[protocol coverage and native differences](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/README.md), and
+[maintainers and advanced deployments](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/docs/maintainers.md) for building distributions
and running Core without the installer.
-[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/docs/getting-started/README.md)
+[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/docs/getting-started/README.md)
diff --git a/apps/docs/content/docs/install.mdx b/apps/docs/content/docs/install.mdx
index 4778a096b..a4934a55a 100644
--- a/apps/docs/content/docs/install.mdx
+++ b/apps/docs/content/docs/install.mdx
@@ -139,7 +139,7 @@ A deployment runs its sandboxes on exactly one backend:
| `none` | Nothing yet | Choose the backend on the **Nodes** page in Web |
The Standard sizes come from the bundle's copy of Web's
-[`standard-sizes.json`](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/apps/web/src/features/sandbox/standard-sizes.json). The
+[`standard-sizes.json`](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/apps/web/src/features/sandbox/standard-sizes.json). The
installer saves the choice through the same administrator API that Web uses once the
services are healthy. If Core refuses it, for example because E2B rejects the key,
the installer prints Core's message and exits with an error; the services keep
@@ -163,7 +163,7 @@ it installs anything:
```
Prepare the template build with the
-[E2B guide](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/deploy/e2b/README.md). To change the backend or
+[E2B guide](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/deploy/e2b/README.md). To change the backend or
the sandbox size later, use the Nodes page in Web; see
[Nodes](/hosted-providers#change-the-sandbox-backend-or-size).
@@ -530,4 +530,4 @@ missing images, restores a missing `oac` command, copies node files newly placed
`--install-dir`. It refuses a bundle from another release; see
[Upgrades](/troubleshooting#upgrade-an-installation).
-[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/docs/getting-started/install.md)
+[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/docs/getting-started/install.md)
diff --git a/apps/docs/content/docs/observability.mdx b/apps/docs/content/docs/observability.mdx
index 91d7f2d57..4ca748148 100644
--- a/apps/docs/content/docs/observability.mdx
+++ b/apps/docs/content/docs/observability.mdx
@@ -7,7 +7,7 @@ Status: Phase 2 and initial Core Web consumption implemented. The current-snapsh
`packages/agents-client` projection, and generated `core.openapi.yaml` contract are
implemented and consumed by the Dashboard through complete Session/observation
identity joins. Durable history uses the separate optional
-[Runtime history API](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/runtime-history-api.md); lifecycle controls remain outside
+[Runtime history API](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/runtime-history-api.md); lifecycle controls remain outside
this phase.
These are administrator reads under `/core/v1`, authenticated by the Core key,
@@ -92,7 +92,7 @@ Authorization: Bearer ...
```
This returns the same object shape as a list item's `observation`, without the
-administrator list's `disk` (see the [administrator contract](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/admin-api.md)). It never starts a Turn, creates
+administrator list's `disk` (see the [administrator contract](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/admin-api.md)). It never starts a Turn, creates
an Environment, provisions compute, renews a lease, or changes lifecycle state.
A valid `environment:none` Session returns `200` with status `unsupported`; the
@@ -193,7 +193,7 @@ Use the existing Agents API error envelope.
| 400 | `invalid_request_error` / `invalid_request_error` | List: a repeated supported query key, or an empty or invalid limit or order, with the shared Beta list messages. Unknown list query keys are ignored. |
| 400 | `invalid_request_error` / `unsupported_parameter` | Single-Session retrieval with any query parameter. |
| 401 | `invalid_request_error` / `invalid_admin_key` | Missing or invalid Core key. |
-| 404 | `not_found_error` / `not_found_error` | Missing, malformed or foreign Session/cursor, indistinguishably, as for the [Session list cursor](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/list-query-semantics.md#list-cursor-errors--september-23-2026). |
+| 404 | `not_found_error` / `not_found_error` | Missing, malformed or foreign Session/cursor, indistinguishably, as for the [Session list cursor](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/list-query-semantics.md#list-cursor-errors--september-23-2026). |
| 500 | `server_error` / `internal_error` | Integrity, ownership, or invalid provider evidence. |
| 503 | `server_error` / `execution_unavailable` | Required Runtime observation service is not configured, or list collection exceeded its request budget. |
@@ -273,10 +273,10 @@ deletion makes the candidate incomplete and prevents publication.
- Token usage: use existing Session/Turn Usage.
- Billing and cost: product/backend concern.
- Historical series in these routes: the optional capability uses the separate
- [Runtime history API](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/runtime-history-api.md).
+ [Runtime history API](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/runtime-history-api.md).
- Container logs and command output.
- Provider credentials or native configuration.
- Start, stop, pause, resume, restart, renew, or delete operations.
- Idle classification and automatic shutdown.
-[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/runtime-observability-api.md)
+[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/runtime-observability-api.md)
diff --git a/apps/docs/content/docs/public-api.mdx b/apps/docs/content/docs/public-api.mdx
index 13cec4d9c..5c55bf834 100644
--- a/apps/docs/content/docs/public-api.mdx
+++ b/apps/docs/content/docs/public-api.mdx
@@ -9,8 +9,8 @@ no credential works in another namespace.
| Namespace | Caller | Credential | Contents | Reference |
| --- | --- | --- | --- | --- |
| `/v1` | Applications (business systems, SDKs) | Project API key | Exactly the pinned official Agents API routes. Core-only fields live only in `x_agents_core` (`harness`, `model_provider`) | [Public API](/sessions) |
-| `/core/v1` | Core Web's server and operator scripts | [Core key](/troubleshooting#core-key) | Installation facts, Projects and keys, resource reads and deletion, Session archive, credential issuance, metrics, audit, sandbox deployment and nodes, deployment model providers | [Core API](#core-api), [Web API](/admin-api), [Core OpenAPI](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/core.openapi.yaml) |
-| `/api/v1` | Nodes, Runtime daemons, self-hosted executors | Machine credentials: node enrollment tokens and executor credentials issued through `/core/v1`, node credentials registered with an enrollment token, and daemon credentials Core writes into hosted sandboxes | Machine connections only: `/api/v1/sandbox-node/*` and `/api/v1/agent-daemon/*`, including WebSockets; each credential works only on its own routes | [Node operations](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/HOSTED-SANDBOX-MANAGER.md#register-a-host), [executor credentials](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/environment-executor-credentials.md), [machine OpenAPI](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/runtime.openapi.yaml) |
+| `/core/v1` | Core Web's server and operator scripts | [Core key](/troubleshooting#core-key) | Installation facts, Projects and keys, resource reads and deletion, Session archive, credential issuance, metrics, audit, sandbox deployment and nodes, deployment model providers | [Core API](#core-api), [Web API](/admin-api), [Core OpenAPI](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/core.openapi.yaml) |
+| `/api/v1` | Nodes, Runtime daemons, self-hosted executors | Machine credentials: node enrollment tokens and executor credentials issued through `/core/v1`, node credentials registered with an enrollment token, and daemon credentials Core writes into hosted sandboxes | Machine connections only: `/api/v1/sandbox-node/*` and `/api/v1/agent-daemon/*`, including WebSockets; each credential works only on its own routes | [Node operations](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/HOSTED-SANDBOX-MANAGER.md#register-a-host), [executor credentials](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/environment-executor-credentials.md), [machine OpenAPI](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/runtime.openapi.yaml) |
A Project API key gets 401 on `/core/v1` and `/api/v1`; the Core key gets 401 on
`/v1` and `/api/v1`. Projects own assets. Multiple equally privileged keys share
@@ -34,7 +34,7 @@ with the Core key on Core's loopback port, not through the public entry.
Applications call every `/v1` route with a Project API key. The routes are exactly
the 58 method and path pairs in
-[upstream-routes.json](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/upstream-routes.json); the
+[upstream-routes.json](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/upstream-routes.json); the
[Public API guide](/sessions) groups them by resource. `/v1` has no
Core-only route.
@@ -45,17 +45,17 @@ with the Core key.
| Routes | Contents | Contract |
| --- | --- | --- |
-| `projects`, `projects/{project_id}[/archive]`, `projects/{project_id}/keys[/{key_id}]` | Projects and their API keys | [Administrator contract](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/admin-api.md) |
-| `projects/{project_id}/{agents,environment-templates,skills,files,vaults,sessions}/**` | Resource reads and deletion, Session history, artifacts and archive | [Administrator contract](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/admin-api.md) |
-| `projects/{project_id}/sessions/{session_id}/execution-configuration` | Committed harness and model selection | [Execution configuration](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/execution-configuration.md) |
+| `projects`, `projects/{project_id}[/archive]`, `projects/{project_id}/keys[/{key_id}]` | Projects and their API keys | [Administrator contract](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/admin-api.md) |
+| `projects/{project_id}/{agents,environment-templates,skills,files,vaults,sessions}/**` | Resource reads and deletion, Session history, artifacts and archive | [Administrator contract](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/admin-api.md) |
+| `projects/{project_id}/sessions/{session_id}/execution-configuration` | Committed harness and model selection | [Execution configuration](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/execution-configuration.md) |
| `projects/{project_id}/sessions/{session_id}/runtime-observation`, `sandbox/runtime-observations` | Current Runtime observations | [Runtime observations](/observability) |
-| `projects/{project_id}/sessions/{session_id}/runtime-history` | Stored Runtime history | [Runtime history](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/runtime-history-api.md) |
-| `projects/{project_id}/{resource-owners,write-operations}`, `audit-log`, `summary` | Provenance, write history, administrator audit and usage summary | [Write audit](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/write-audit.md), [administrator contract](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/admin-api.md) |
-| `projects/{project_id}/environments/{environment_id}/executor-credentials[/{key_id}]` | Executor credentials for a self-hosted Environment | [Executor credentials](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/environment-executor-credentials.md) |
-| `installation` | Public URL, API base URL, source commit, the installer's process settings and what is bound to the public URL; available before any deployment | [Installation](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/installation.md) |
-| `metrics` | Core's own process metrics | [Core metrics](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/core-metrics.md) |
-| `sandbox/deployment[/maintenance]`, `sandbox/enrollment-tokens`, `sandbox/nodes[/{node_id}[/allocations]]` | Sandbox deployment, node enrollment tokens and nodes | [Sandbox deployment](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/sandbox-deployment.md), [node operations](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/HOSTED-SANDBOX-MANAGER.md), [node host history](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/node-host-history.md) |
-| `harnesses`, `harnesses/{harness}/model-provider` | Supported harnesses and each harness's deployment default model provider (write-only key) | [Model execution](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/model-execution.md#deployment-defaults) |
+| `projects/{project_id}/sessions/{session_id}/runtime-history` | Stored Runtime history | [Runtime history](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/runtime-history-api.md) |
+| `projects/{project_id}/{resource-owners,write-operations}`, `audit-log`, `summary` | Provenance, write history, administrator audit and usage summary | [Write audit](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/write-audit.md), [administrator contract](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/admin-api.md) |
+| `projects/{project_id}/environments/{environment_id}/executor-credentials[/{key_id}]` | Executor credentials for a self-hosted Environment | [Executor credentials](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/environment-executor-credentials.md) |
+| `installation` | Public URL, API base URL, source commit, the installer's process settings and what is bound to the public URL; available before any deployment | [Installation](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/installation.md) |
+| `metrics` | Core's own process metrics | [Core metrics](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/core-metrics.md) |
+| `sandbox/deployment[/reset]`, `sandbox/enrollment-tokens`, `sandbox/nodes[/{node_id}[/allocations]]` | Sandbox deployment, node enrollment tokens and nodes | [Sandbox deployment](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/sandbox-deployment.md), [node operations](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/HOSTED-SANDBOX-MANAGER.md), [node host history](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/node-host-history.md) |
+| `harnesses`, `harnesses/{harness}/model-provider` | Supported harnesses and each harness's deployment default model provider (write-only key) | [Model execution](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/model-execution.md#deployment-defaults) |
## Machine connection API
@@ -64,42 +64,42 @@ the Core key or a Project API key.
| Routes | Caller | Credential | Contract |
| --- | --- | --- | --- |
-| `POST sandbox-node/enroll` | Node installer | One-use enrollment token from `POST /core/v1/sandbox/enrollment-tokens` | [Node operations](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/HOSTED-SANDBOX-MANAGER.md#register-a-host), [machine OpenAPI](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/runtime.openapi.yaml) |
-| `GET sandbox-node/configuration` | Node installer and node | Enrollment token, or node credential with `X-OAC-Node-ID` | [Sandbox deployment](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/sandbox-deployment.md), [machine OpenAPI](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/runtime.openapi.yaml) |
-| `GET sandbox-node/identity`, WebSocket `GET sandbox-node/connect` | Node | Node credential registered at enrollment | [Node operations](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/HOSTED-SANDBOX-MANAGER.md#register-a-host) |
-| `POST agent-daemon/enroll`, `GET agent-daemon/connection` | Self-hosted executor and its installer | Executor credential from `/core/v1/projects/{project_id}/environments/{environment_id}/executor-credentials` | [Executor credentials](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/environment-executor-credentials.md) |
-| WebSocket `GET agent-daemon/ws`, `POST agent-daemon/bootstrap`, `GET agent-daemon/device-status` | Runtime daemons | Daemon credential: Core writes one into each hosted sandbox it prepares; a self-hosted executor uses its executor credential | [Runtime enrollment](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/README.md#user-managed-runtime-enrollment) |
+| `POST sandbox-node/enroll` | Node installer | One-use enrollment token from `POST /core/v1/sandbox/enrollment-tokens` | [Node operations](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/HOSTED-SANDBOX-MANAGER.md#register-a-host), [machine OpenAPI](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/runtime.openapi.yaml) |
+| `GET sandbox-node/configuration` | Node installer and node | Enrollment token, or node credential with `X-OAC-Node-ID` | [Sandbox deployment](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/sandbox-deployment.md), [machine OpenAPI](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/runtime.openapi.yaml) |
+| `GET sandbox-node/identity`, WebSocket `GET sandbox-node/connect` | Node | Node credential registered at enrollment | [Node operations](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/HOSTED-SANDBOX-MANAGER.md#register-a-host) |
+| `POST agent-daemon/enroll`, `GET agent-daemon/connection` | Self-hosted executor and its installer | Executor credential from `/core/v1/projects/{project_id}/environments/{environment_id}/executor-credentials` | [Executor credentials](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/environment-executor-credentials.md) |
+| WebSocket `GET agent-daemon/ws`, `POST agent-daemon/bootstrap`, `GET agent-daemon/device-status` | Runtime daemons | Daemon credential: Core writes one into each hosted sandbox it prepares; a self-hosted executor uses its executor credential | [Runtime enrollment](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/README.md#user-managed-runtime-enrollment) |
## Contract sources
-- [Pinned upstream baseline](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/upstream.json): OpenAI
+- [Pinned upstream baseline](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/upstream.json): OpenAI
Python SDK 3.13.0, exact upstream commit and `agents=v1`.
-- [Pinned routes](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/upstream-routes.json) and
- [fields](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/upstream-fields.json): extracted from the
+- [Pinned routes](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/upstream-routes.json) and
+ [fields](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/upstream-fields.json): extracted from the
pinned SDK by `scripts/extract-agents-api-upstream.py`. Contract tests require the
public OpenAPI to have exactly these routes and to keep every other field inside
`x_agents_core`.
-- [Public OpenAPI](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/openapi.yaml): public schema snapshot;
- combine it with the fixed SDK and [operation evidence](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/operation-evidence.md).
-- [Core OpenAPI](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/core.openapi.yaml): generated `/core/v1`
+- [Public OpenAPI](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/openapi.yaml): public schema snapshot;
+ combine it with the fixed SDK and [operation evidence](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/operation-evidence.md).
+- [Core OpenAPI](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/core.openapi.yaml): generated `/core/v1`
routes, all authenticated by the Core key.
-- [Machine connection OpenAPI](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/runtime.openapi.yaml):
+- [Machine connection OpenAPI](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/runtime.openapi.yaml):
generated `/api/v1` sandbox node routes. The node WebSocket and the private
daemon transport are described in the node and Runtime credential guides.
-- [Administrator contract](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/admin-api.md): Project/key
+- [Administrator contract](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/admin-api.md): Project/key
lifecycle, resources, explicit hosted Session archive, summary,
errors/deletion preconditions, audit and historical copy provenance.
- [Web integration](/admin-api): browser, console and Core boundaries and frontend handoff.
-- [Design rules](/concepts) and [contributor guide](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/CONTRIBUTING.md):
+- [Design rules](/concepts) and [contributor guide](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/CONTRIBUTING.md):
ownership, security and change requirements.
Generated schemas do not establish complete compatibility or real execution
-support. The [coverage record](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/README.md) identifies
+support. The [coverage record](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/README.md) identifies
qualified workflows, native differences and unresolved behavior. Update the
relevant contract and this index when adding or moving an API surface.
-Core administration failures use the [Core error envelope](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/core-errors.md),
+Core administration failures use the [Core error envelope](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/core-errors.md),
including typed optional safe details and distinct console proxy rejection codes.
The public and machine error contracts remain unchanged.
-[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/docs/api/README.md)
+[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/docs/api/README.md)
diff --git a/apps/docs/content/docs/quickstart.mdx b/apps/docs/content/docs/quickstart.mdx
index b6ebfacd8..bd3558799 100644
--- a/apps/docs/content/docs/quickstart.mdx
+++ b/apps/docs/content/docs/quickstart.mdx
@@ -101,8 +101,8 @@ The provider's protocol must match the harness:
| Claude Code (`claude_sdk`) | `anthropic` | The provider's exact model ID |
| MiniMax Code (`mcode`) | `anthropic` | `context_window` and `max_output_tokens` |
-See [harness selection](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/harness-selection.md) and
-[model execution](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/model-execution.md) for the full contract.
+See [harness selection](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/harness-selection.md) and
+[model execution](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/model-execution.md) for the full contract.
Selecting a harness doesn't make an unsupported model or operation work.
## Model providers
@@ -142,8 +142,8 @@ not replay history. Don't submit the same work as a new Session when a response
lost.
Files, Artifacts, cancellation and their current limits are in the
-[coverage ledger](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/README.md). MCP credentials use the
+[coverage ledger](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/README.md). MCP credentials use the
separate Vault API; they are neither Project API keys nor model provider keys. The
[API index](/public-api) lists every route.
-[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/docs/getting-started/quickstart.md)
+[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/docs/getting-started/quickstart.md)
diff --git a/apps/docs/content/docs/self-hosted-execution.mdx b/apps/docs/content/docs/self-hosted-execution.mdx
index 3de987fef..3e81ea259 100644
--- a/apps/docs/content/docs/self-hosted-execution.mdx
+++ b/apps/docs/content/docs/self-hosted-execution.mdx
@@ -143,7 +143,7 @@ key_id=$(python3 -c 'import uuid; print(uuid.uuid4())'); echo "credential ID: $k
Rotate with `{"key_id":"…","rotate":true}` on the same route; revoke with
`DELETE …/executor-credentials/`. After an uncertain response, list the
credentials with `GET` before trying again. The
-[credential contract](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/environment-executor-credentials.md)
+[credential contract](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/environment-executor-credentials.md)
has every rule and error.
@@ -166,4 +166,4 @@ Credential replacement for executors created by the new installer still updates
and restarts the same owned container. The legacy refusal does not authorize
adopting an old container to replace its credential.
-[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/docs/getting-started/self-hosted.md)
+[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/docs/getting-started/self-hosted.md)
diff --git a/apps/docs/content/docs/sessions.mdx b/apps/docs/content/docs/sessions.mdx
index c70cbe237..36944b8b4 100644
--- a/apps/docs/content/docs/sessions.mdx
+++ b/apps/docs/content/docs/sessions.mdx
@@ -34,8 +34,8 @@ leaves the others valid; archiving the Project revokes all its keys. The optiona
## Public resources
Paths are relative to `/v1`. Detailed fields, union types and response shapes are
-in the [public OpenAPI](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/openapi.yaml), fixed SDK source
-and linked semantic records. The [resource inventory](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/README.md#upstream-resource-inventory)
+in the [public OpenAPI](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/openapi.yaml), fixed SDK source
+and linked semantic records. The [resource inventory](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/README.md#upstream-resource-inventory)
lists each operation and its coverage.
| Resource | Route family | Semantics |
@@ -44,11 +44,11 @@ lists each operation and its coverage.
| Sessions and Turns | `/agents/sessions`, nested `/turns` | Admission, metadata, state and recovery queries |
| Items and events | Session `/items`, `/events` | Persisted history and live SSE |
| Artifacts | Session `/artifacts` | Captured output metadata, immutable content and deletion |
-| Subagents | Session `/subagents`, nested `/turns` and `/items` | Scoped child execution reads; [native limits](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/subagents.md) |
-| Environments and workspace Files | `/agents/environments`, nested `/files` | [Environment state](/environments-and-files) and [workspace access](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/environment-files.md) |
-| Environment Templates | `/agents/environments/templates` | [Reusable initialization](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/environment-templates.md) |
-| Source Files | `/files` | [Uploaded content resources](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/source-files.md), distinct from live workspace files |
-| Skills and versions | `/skills`, nested `/versions` | [Bundles and versions](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/file-resource-semantics.md) |
+| Subagents | Session `/subagents`, nested `/turns` and `/items` | Scoped child execution reads; [native limits](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/subagents.md) |
+| Environments and workspace Files | `/agents/environments`, nested `/files` | [Environment state](/environments-and-files) and [workspace access](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/environment-files.md) |
+| Environment Templates | `/agents/environments/templates` | [Reusable initialization](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/environment-templates.md) |
+| Source Files | `/files` | [Uploaded content resources](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/source-files.md), distinct from live workspace files |
+| Skills and versions | `/skills`, nested `/versions` | [Bundles and versions](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/file-resource-semantics.md) |
| Vaults and Credentials | `/vaults`, nested `/credentials` | [Scoped write-only credential storage](/environments-and-files) |
SSE is live, not a historical replay service. After a disconnect, query the Session,
@@ -58,14 +58,14 @@ native execution. Do not automatically replay uncertain tool or file effects.
## Core extension fields
The `/v1` route set is exactly the pinned SDK's operations, listed in
-[upstream-routes.json](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/upstream-routes.json); Core adds
+[upstream-routes.json](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/upstream-routes.json); Core adds
no route. It adds fields only inside `x_agents_core`, because it runs several
harnesses and accepts custom model access:
| Field | Where | Reference |
| --- | --- | --- |
-| `x_agents_core.harness` | Saved Agent create, update and read; the inline Session `agent`; the Session's effective `agent` | [Harness selection](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/harness-selection.md) |
-| `x_agents_core.model_provider` | Saved Agent create, update and read; Session creation. It takes `protocol`, `base_url`, optional `context_window` and `max_output_tokens`, and a write-only `api_key`; reads return `api_key_configured` instead of the key | [Model execution](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/model-execution.md) |
+| `x_agents_core.harness` | Saved Agent create, update and read; the inline Session `agent`; the Session's effective `agent` | [Harness selection](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/harness-selection.md) |
+| `x_agents_core.model_provider` | Saved Agent create, update and read; Session creation. It takes `protocol`, `base_url`, optional `context_window` and `max_output_tokens`, and a write-only `api_key`; reads return `api_key_configured` instead of the key | [Model execution](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/model-execution.md) |
Any other member of `x_agents_core` is rejected with 400. Deployment, placement,
credential issuance and operational reads are not part of `/v1`; they belong to
@@ -78,7 +78,7 @@ never to `self_hosted`; `none` Sessions accept only the deployment default.
`openai_hosted` and `self_hosted` Sessions must end up with one: otherwise creation
returns 400 with code `model_provider_required` and param
`x_agents_core.model_provider`, and nothing is written. See
-[model execution](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/model-execution.md).
+[model execution](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/model-execution.md).
An application reads a Session's model from `agent.model` and an explicitly
selected harness from `agent.x_agents_core.harness`; a Session on the deployment
@@ -90,8 +90,8 @@ The deployment chooses E2B or its own Docker/microsandbox nodes. A public
`self_hosted` Environment uses caller-owned compute and the same colocated
Runtime. For a `self_hosted` Session, the deployment operator issues the
Environment's executor credential with the Core key; see
-[executor credentials](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/environment-executor-credentials.md).
+[executor credentials](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/environment-executor-credentials.md).
Our daemon transport and the supported native harness differences are
explicit; they do not imply interoperability with OpenAI's stock executor transport.
-[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/docs/api/public-agent-api.md)
+[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/docs/api/public-agent-api.md)
diff --git a/apps/docs/content/docs/troubleshooting.mdx b/apps/docs/content/docs/troubleshooting.mdx
index 3c9cd97f3..8369b2b1e 100644
--- a/apps/docs/content/docs/troubleshooting.mdx
+++ b/apps/docs/content/docs/troubleshooting.mdx
@@ -123,7 +123,7 @@ refuses. `rotate-core-key` refuses to run on a Web-only installation.
## Projects and API keys
Create Projects and issue their keys in Web, on **Projects and keys**, or with the
-[Core API](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/admin-api.md) under `/core/v1/projects`. A key's
+[Core API](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/admin-api.md) under `/core/v1/projects`. A key's
plaintext is shown once; Core stores only its digest. All keys of a Project share its
assets and execution principal, and each write records the key that made it.
@@ -195,10 +195,12 @@ The latter rename their directory, project and payload without copying a databas
Preflight reports generated-file edits, invalid configuration, destination conflicts,
insufficient database-copy space, and every available drain blocker. It reads the
-old Core with its Core key. When that Core is stopped and static checks pass, it
-announces a temporary start using only the existing files. Refusal restores the
-services this probe started; a restoration failure is reported explicitly. No
-installation conversion happens before confirmation.
+old Core with its Core key. Start the previous release yourself before conversion:
+a stopped or unreadable Core is refused without starting services. This is required
+to identify the provider before any conversion writes. E2B conversion is currently
+refused, including interrupted journal resumes; keep the previous release until
+the safe Core-owned E2B upgrade transition is available. No installation conversion
+happens before confirmation.
The confirmation prints the backup command for the old project and the directory
and secret moves. For a config.json installation, take the backup before confirming:
@@ -223,9 +225,11 @@ The new state format is 2; config.json's schema format remains 1. Ports, public
secrets, Core key, installation ID and database contents are preserved. The old
`parsar` path becomes a stub naming the new `oac` command. New services use `OAC_*`
settings and `io.oac.inputs` labels. Docker and microsandbox deployments retain
-their resources, replace their Runtime with the bundle's release through the normal
-maintenance-only API, then resume admission. E2B remains in maintenance: rebuild its
-template with this release's `build-template.py`, replace it in Web, then resume.
+their resources and replace their Runtime with the bundle's exact release through
+an expected-generation same-provider PUT. New Core must report reset null and zero
+held resources; conversion reads back the exact specification and generation.
+Ordinary migration retires old maintenance and resumes admission, which is why E2B
+conversion remains blocked until its safe upgrade preparation is implemented.
Rerun `./install.sh --convert` with the **same bundle** after any interruption,
including after the directory move. For a custom directory, repeat `--install-dir`.
@@ -252,7 +256,7 @@ replacement:
| `AGENTS_API_EXECUTION_OPTIONS_FILE` | Default models, set per harness in Web (**System**, **Default model**) or with `PUT /core/v1/harnesses/{harness}/model-provider` | `config/core.env`. `--convert` does not carry it over; it leaves the file, which may hold model keys, and reports it. Delete the file once the defaults are set |
| `AGENTS_API_DAEMON_WS_URL` (a `wss://…/api/v1/agent-daemon/ws` URL) | `OAC_PUBLIC_URL` (the origin, such as `https://core.example`), or `public_url` in `config.json` | `config/core.env` |
| `AGENTS_API_CONFIG_FILE` | None; delete the line | `config/core.env` |
-| `AGENTS_API_MANAGED_RUNTIMES_FILE` | The sandbox deployment in the database, set in Web | `config/core.env`; see [older file-managed installations](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/HOSTED-SANDBOX-MANAGER.md#older-file-managed-installations) |
+| `AGENTS_API_MANAGED_RUNTIMES_FILE` | The sandbox deployment in the database, set in Web | `config/core.env`; see [older file-managed installations](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/services/agents-api/HOSTED-SANDBOX-MANAGER.md#older-file-managed-installations) |
| `admin/sandbox-admin.key`, `admin/digests.json` | `admin/core.key`, `admin/core-key-digests.json` (conversion then moves them to `secrets/` and `generated/`) | Files and their `compose.json` mounts |
| `AGENTS_API_SANDBOX_ADMIN_DIGESTS_FILE` | `OAC_CORE_KEY_DIGESTS_FILE` | `config/core.env` |
| `CORE_CONSOLE_ADMIN_TOKEN_FILE`, `install.sh --admin-token-file` | `OAC_WEB_CORE_KEY_FILE`, `--core-key-file` | Web environment; installer flag |
@@ -329,10 +333,11 @@ Nodes from releases that used the removed `/core/v1/sandbox/enroll` and
microsandbox deployment:
1. Drain with the previous release while its nodes are connected: enter maintenance,
- archive retained hosted Sessions and wait until nothing is retained (the
- [maintenance procedure](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/services/agents-api/HOSTED-SANDBOX-MANAGER.md#removal-and-maintenance)).
+ archive retained hosted Sessions and wait until nothing is retained using that
+ previous release's maintenance procedure.
2. Upgrade Core and Web, and change the routing as above.
-3. Save the new release's Runtime, resume, and add the nodes again. On each node host,
+3. Reset a drained selection that lacks a specification, then configure the new
+ release's Runtime and add nodes again. On each node host,
stop the old node service and move its state directory aside as a backup first.
A Web-selected Docker or microsandbox deployment saved before deployment specifications
@@ -399,9 +404,9 @@ Core records which key made each public resource write, for the console. Set
`core.write_audit_retention` in `config.json` (Go duration, at least `1h`, default
`2160h`) to control how long non-creation history is kept; creation ownership is kept
for good. Removing keys or resources does not delete these records. See the
-[query contract](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/write-audit.md); administrator mutations
+[query contract](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/write-audit.md); administrator mutations
have a separate
-[audit log](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/contracts/agents-api/admin-api.md#monitoring-and-audit). Neither
+[audit log](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/contracts/agents-api/admin-api.md#monitoring-and-audit). Neither
logs bodies or secrets.
-[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/75bf4484ba957344ae374354858bbdfe104f5c4f/docs/getting-started/operations.md)
+[Repository source](https://github.com/MiniMax-AI/parsar-core/blob/f36ac3592bfe9f77a2172680aa52b14c6f525062/docs/getting-started/operations.md)
diff --git a/apps/docs/content/guide-sources.json b/apps/docs/content/guide-sources.json
index e768b0c2b..614a1706f 100644
--- a/apps/docs/content/guide-sources.json
+++ b/apps/docs/content/guide-sources.json
@@ -1,61 +1,61 @@
{
- "revision": "75bf4484ba957344ae374354858bbdfe104f5c4f",
+ "revision": "f36ac3592bfe9f77a2172680aa52b14c6f525062",
"sources": {
"docs/getting-started/README.md": "e4ca3bcbfdcbf3832c1d9e0525f98083ca11a4e42148f9f45090bbe1bc0f2b5c",
"docs/design-principles.md": "5e1bff3cdcc77cd02ad692f67977cb4e0a5654989982d722bac0e3c79dbf82cb",
- "docs/web/architecture.md": "23e5758d72687c2c821975f326f24a789545ce0b0f566802cbe901edf2ebe3f1",
+ "docs/web/architecture.md": "80bdb8053c06c2b2030b193aae17d5434545f9c72af50e9d3df3ade522fe25ed",
"docs/getting-started/install.md": "15cfe81d4005a7337ac82bf6c35a8b441e9f9b0f2a2299021ad6c539f3f336a3",
- "docs/configuration.md": "7fe061b93a09c0f7baaa70f33f0863d8b29d308bbbc9b0e42b35cbef0ef0c648",
+ "docs/configuration.md": "401bf72003cd2eb03ae217d49591273ea0a5c42d68190d1a7ad2f12169c287ea",
"docs/web/core-connection.md": "46f86520e70bf41079708e2c398655aa9087ad9488c8e661d9a73035d11098c4",
"docs/getting-started/quickstart.md": "5effeffcfb343f7939910f0c1758e1492d49e7bd66c409b1819cefe67af592d7",
- "docs/api/README.md": "f8c3872b012af7c490a06578c7d56560e0ec5998865ef043523bc66cc10ae4be",
+ "docs/api/README.md": "b2b4c7fdf9fbde62d2169014180698a77c671f85c1c68c2e5de0e73bbdf3b13c",
"contracts/agents-api/execution-tools.md": "e0b61f6c0c236c362186c5f6d0ad69a16dd8a8d9afabe1329a1fe22e5f4a71e9",
"docs/api/public-agent-api.md": "18829b366bd2e49fe5beb76dc8a709bc1aa8e35d4fbfd2484a0c52b46cf75417",
"contracts/agents-api/environments.md": "89ffc903ee44d00ee87c16950202a3ca4c8345160780dcdd4ed2a71b25b1c2dc",
- "docs/getting-started/nodes.md": "00d98f03d0ac967575bfa35ee960ff24b971bff1531a264517b384376986ac6f",
+ "docs/getting-started/nodes.md": "a79e0007c1712c457b40b7573423fd1fdb95d52e283567b4c3d732393fac5109",
"docs/getting-started/self-hosted.md": "3f1b402cf28bb320ba414eaf36110e9df4d792b81da0456a3c42d469357d8d99",
"apps/web/public/onboarding/monitor-en.webp": "29dc220cb1250c7016b7c4bf7f30e9510b07c07b814a48c3aa9303b2d76f20b2",
- "docs/web/README.md": "3b96e673591da67a6f666dddafd49a565c066c347bdfa2eaba0f92ab03daf514",
- "docs/api/web-management.md": "5480bfcf9dd24a8ffc8d442e4d8582e007dadd22dba6a686d71b4bede0616506",
+ "docs/web/README.md": "9fce295f6f6ff04acefe338679c1e54723e123dd0027a778202adfa9ab95b6c4",
+ "docs/api/web-management.md": "ce248fbeb1ded0f3fe4c6021c800c268ebaf4bd37eb1292aecc3d919d138a391",
"contracts/agents-api/runtime-observability-api.md": "cd46e777fe716a7e76374f4655c96dfbad9d8be1bfca6203ed6118ab87874c2b",
- "docs/getting-started/operations.md": "abc965e6ba19d2a2c148c8b5afde7203ca39891383840d85379d0d433b39df1d",
+ "docs/getting-started/operations.md": "b930724d4518621bd6bd8457c70e6aac8c041677ad532a2b63894ef0d20ba892",
"apps/docs/scripts/guides.json": "9d1ba424f8fadd65ffa6175eaa9c7bc4878d3e1437971099812c984e1e593080"
},
"outputs": {
- "content/docs/index.mdx": "2cd2e1d7adb6b38bfb1808bc5f9d9b2f699d02b02eb233c0ee72438884dffe9a",
+ "content/docs/index.mdx": "75339c35292bb1f3c6b4313d717b82686bee8caa265bb575fcecbd0ba33bb780",
"content/docs/index.zh.mdx": "5bdd41893946f1d229eb4cf1599a97f2e38cb9e09722d2a69f62053376ac88a9",
- "content/docs/concepts.mdx": "32120464dd98c1b80d5d4c57d7f2c7e4365f623532024a632b9fccbda730384a",
+ "content/docs/concepts.mdx": "304e18737cdf807562501981e5b8312466a7c7ce29db3bf4823075850dec10c4",
"content/docs/concepts.zh.mdx": "c60644f395dbac5ee607cdb053249e05ec8352d28de97ef1c38d6c1e4443bbb1",
- "content/docs/execution-model.mdx": "b3c928e353e173aa7eea5c4fbad06b6896fd1e97f69c87701de8af39fae054fe",
+ "content/docs/execution-model.mdx": "8dd05250255fdb39a4aee6ffa51a16379fbd19d2761f510e1ad9cc3a193f750c",
"content/docs/execution-model.zh.mdx": "9976a9203c6a83d49c2c753e3a8528a369078e87aeb88850654da948ac8c17e1",
- "content/docs/install.mdx": "439809c11f5d805e6c6f964c779ec01468dcdad88bd11a56bb180a6e6a13b98a",
+ "content/docs/install.mdx": "cba5c14e55c6f474d810dc8e545fadbb4f35bcad50eeb6ceef0cbd967b3fb1ab",
"content/docs/install.zh.mdx": "7ed0b726cf721b18e85b89145ef8ebd2f5d382afd0489633a7b63962aa2fc4ed",
- "content/docs/configure.mdx": "4ab159c54f7b9c6e1a2d0c3873de5e3641b48c4e1badf47f63311bef94f8778f",
+ "content/docs/configure.mdx": "ac8a8788881707287805811474aaabfc0d0f273aa2423b010b167443996d2530",
"content/docs/configure.zh.mdx": "a0c3a1df3e9f75e672e1d0a8c68c82c24ac36a8479d10a951de441960d07927f",
- "content/docs/bootstrap-projects-keys.mdx": "4a39e95986851828e3fbcc0550398146de9bb37cfe22f10df566c51a911ca9ed",
+ "content/docs/bootstrap-projects-keys.mdx": "fc2da0aa255278281d8a57c664d7d30e5a458add4e58402386e0374f23973da4",
"content/docs/bootstrap-projects-keys.zh.mdx": "509b4dcea4c865d6834bcaef78baa681180d48274931230f958a39e46011c88f",
- "content/docs/quickstart.mdx": "92f62b5df213c363bd7148de0462b2ca66f2054f4458d790196e75daa678bb24",
+ "content/docs/quickstart.mdx": "4ebc8ac16df66042e816ff51cf6672d8a85f44e119211efba584025666dce845",
"content/docs/quickstart.zh.mdx": "c5f119b100b102918a45763834d0ed7fd436479fb411e16a62a408411dbcb3e1",
- "content/docs/public-api.mdx": "a3d39d873054614bdf410d8c2a1e5c33320f1f3bffe155657b64579ea28fdc48",
+ "content/docs/public-api.mdx": "ee1be51bf72bc48809efc0af8a460c26ae1cbf962771e132ec521447b89d5479",
"content/docs/public-api.zh.mdx": "fb0efb9be2fba3f4a3dc71bf053d2c7eddffbfe010f59fa5a1ae60a3f068545b",
- "content/docs/agents-and-tools.mdx": "78789f7e6a1d327ff514d872b9f8b55a88754ffbfc34b7b10e5f7c115f242e3c",
+ "content/docs/agents-and-tools.mdx": "b8cc50792e579cbf66bcc597685b745d8721266970b75e928d8edee2ce11b08b",
"content/docs/agents-and-tools.zh.mdx": "cc8d4e0ef0ea894f068f3bbd143d5ee70c6346bf98f425b1d5fe07ec3931f128",
- "content/docs/sessions.mdx": "eaef17849bce13c9f3c22966910e081fcc4851c59a7a8b972e917a45bdebfb57",
+ "content/docs/sessions.mdx": "8dba8c2a94ef5e95fde6f53e7af3cc481e4d3c5db71815372f01ddd1bf729316",
"content/docs/sessions.zh.mdx": "b1b1a7f8aacd9a902320f9692173ede24036124f85761cf02f23ef3ef2bcc4c1",
- "content/docs/environments-and-files.mdx": "411328cb87962ee9849021ed14de144ab9d4b31ecb8a42954079c9142d479e98",
+ "content/docs/environments-and-files.mdx": "0fce7c8806d532d64eff6319bba7d0a8e44da46f888d224644b3ab5c23428c41",
"content/docs/environments-and-files.zh.mdx": "ee64577220a798f023b330c65302c14438f25a34a78c4e36144bfdb4304a3ae5",
- "content/docs/hosted-providers.mdx": "7d1825a5ef7e81d71a0c64bd00ce8fcc3c00d632a318eaa4063dacf876fdfee5",
+ "content/docs/hosted-providers.mdx": "e73657db13cbde92c1291534d910eba76172781a58645916ec7bfa770c42a5cf",
"content/docs/hosted-providers.zh.mdx": "f54d80d6e65596dca6b20262abaa3dcb5af245227ffad146fcaed4bd69454dee",
- "content/docs/self-hosted-execution.mdx": "67a134939247a5a3d16b4a935895259c37d8264377de53179e6598bdb82957d5",
+ "content/docs/self-hosted-execution.mdx": "34877624962b24c22e8371ebd2e2f83bbf5a0d004c74d5d9b5db57f5133b692c",
"content/docs/self-hosted-execution.zh.mdx": "f11d34ce30d48039376256474bc9509a508a2f6b9906f49b5cf6e396707ae5c0",
"public/images/source/apps/web/public/onboarding/monitor-en.webp": "29dc220cb1250c7016b7c4bf7f30e9510b07c07b814a48c3aa9303b2d76f20b2",
- "content/docs/console.mdx": "a708f26941840375299f14b26dc69a278c1f44a7aedca90d50bb806ba05685c0",
+ "content/docs/console.mdx": "cf0e6a1f9dd690ffe74c5d83111bb18df7eebe287848497c01257692ab3edc6a",
"content/docs/console.zh.mdx": "72f953ba46dcc95933773ac0c4dffdc0d0a499be9d9a1cb9467db2659f1fa10d",
- "content/docs/admin-api.mdx": "23b0ebdbb3ab93cee6f93e083026318465fae581546de5af9953fbf779444452",
+ "content/docs/admin-api.mdx": "1f4917510d3335ec1e9fe0dd7527d0e625b4b9257df39c1691d2564261603959",
"content/docs/admin-api.zh.mdx": "07006de30df3080e54c3cd79ed0ea5337dce557aa2f11afea84edb7f92884d38",
- "content/docs/observability.mdx": "4894808be45568b3d6e1039183350a5315dbbde1144db94cc592eb8bd8c433e4",
+ "content/docs/observability.mdx": "c5e2963a4dd7abddc60869a3ecd3b6b32a97c2ba1ae28b88b7453136656c1201",
"content/docs/observability.zh.mdx": "9574b3412a5754f41ab2d3a16062ac1cad06766be481a8e6bff1b84acbde7e7e",
- "content/docs/troubleshooting.mdx": "2008f1bcdc5e8cfb55602e2d849093140a61682c68795d9c9f707d62e30b0c0a",
+ "content/docs/troubleshooting.mdx": "7ecf60de8a60e8765e7fe6cd3eba681c9ee6b76e3e374dcf1731bfc93a5f1a94",
"content/docs/troubleshooting.zh.mdx": "a47714ea9890385e811b163bbb2dc1412ddd9c12747792cf53630fc06062e5e2"
}
}
diff --git a/apps/docs/openapi/core-api.yaml b/apps/docs/openapi/core-api.yaml
index 472e1f59e..87bd862f1 100644
--- a/apps/docs/openapi/core-api.yaml
+++ b/apps/docs/openapi/core-api.yaml
@@ -1696,7 +1696,7 @@ paths:
tags:
- Core Administration
post:
- description: Core key only. Requires maintenance and the current deployment generation. Permanently closes execution, requests cancellation and releases sandbox/snapshots through existing cleanup. Session history and persisted files/artifacts remain; unpersisted workspace contents are lost. A cleanup_pending response is not proof of resource release. Does not affect caller-managed Runtime.
+ description: Core key only. Requires the current deployment generation; no maintenance mode is required. Permanently closes execution, requests cancellation and releases sandbox/snapshots through existing cleanup. Session history and persisted files/artifacts remain; unpersisted workspace contents are lost. A cleanup_pending response is not proof of resource release. Does not affect caller-managed Runtime.
parameters:
- schema:
type: string
@@ -3257,7 +3257,7 @@ paths:
tags:
- Sandbox Manager
post:
- description: Selects a provider, enforced resource limits and pinned Runtime release. Core derives the deployment's core_url from the installation public URL and rejects a core_url member with 400. E2B returns 409 sandbox_configuration_error while the public URL is loopback. E2B credentials are write-only. E2B may omit resources to adopt the validated template build's CPU and memory, returned in specification.resources. Exact retries return the existing selection; differing selections and file-managed deployments reject. This does not create compute or execute work.
+ description: Selects a provider, enforced resource limits and pinned Runtime release. Core derives the deployment's core_url from the installation public URL and rejects a core_url member with 400. E2B returns 409 sandbox_configuration_error while the public URL is loopback. E2B credentials are write-only. E2B may omit resources to adopt the validated template build's CPU and memory, returned in specification.resources. Requires explicit expected_generation, including zero at first setup. Stale retries reject before provider validation. An identical selection at the current generation is a no-op; differing selections and file-managed deployments reject. This does not create compute or execute work.
responses:
'200':
description: OK
@@ -3308,7 +3308,7 @@ paths:
description: Deployment selection
required: true
put:
- description: Requires maintenance, the current generation and verified cleanup of all old resources. Credentials are write-only. E2B may omit resources to adopt the validated template build's CPU and memory. A core_url member is rejected with 400; the address comes from the installation public URL. Historical records are retained; old node credentials and enrollments are retired. Explicitly resume after success. Never automatically retry an uncertain write.
+ description: Requires the current generation, the same backend type, no active reset and verified cleanup of all old resources. A backend type change requires reset. Credentials are write-only. E2B may omit resources to adopt the validated template build's CPU and memory. A core_url member is rejected with 400; the address comes from the installation public URL. Historical records are retained; old node credentials and enrollments are retired. Never automatically retry an uncertain write.
responses:
'200':
description: OK
@@ -3358,9 +3358,16 @@ paths:
$ref: '#/components/schemas/api.SandboxDeploymentChangeInput'
description: Replacement deployment selection
required: true
- /core/v1/sandbox/deployment/maintenance:
- patch:
- description: Requires the current generation. Maintenance preserves execution, cleanup and history. Resume requires successfully activated provider configuration.
+ /core/v1/sandbox/deployment/reset:
+ delete:
+ description: Restores admission but never restores Sessions already archived. With no reset running this is an idempotent read, provided the generation still matches.
+ parameters:
+ - schema:
+ type: integer
+ description: Current deployment generation
+ in: query
+ name: expected_generation
+ required: true
responses:
'200':
description: OK
@@ -3400,15 +3407,59 @@ paths:
$ref: '#/components/schemas/api.CoreErrorResponse'
security:
- DeploymentAdminAuth: []
- summary: Pause or resume new hosted allocations
+ summary: Cancel a sandbox deployment reset
+ tags:
+ - Sandbox Manager
+ post:
+ description: Archives hosted Sessions and waits for confirmed provider cleanup, preserving history and Files/Artifacts. Auto waits for started or waiting Turns and file writes until the durable deadline; force cancels them. The same clear is idempotent; force escalates auto. Requires the current generation. Self-hosted Sessions are unchanged.
+ responses:
+ '200':
+ description: OK
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/store.RuntimeDeploymentView'
+ '400':
+ description: Bad Request
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/api.CoreErrorResponse'
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/api.CoreErrorResponse'
+ '409':
+ description: Conflict
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/api.CoreErrorResponse'
+ '500':
+ description: Internal Server Error
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/api.CoreErrorResponse'
+ '503':
+ description: Service Unavailable
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/api.CoreErrorResponse'
+ security:
+ - DeploymentAdminAuth: []
+ summary: Start or escalate a durable sandbox deployment reset
tags:
- Sandbox Manager
requestBody:
content:
application/json:
schema:
- $ref: '#/components/schemas/store.SandboxMaintenanceRequest'
- description: Maintenance state
+ $ref: '#/components/schemas/store.SandboxResetRequest'
+ description: Reset mode and current deployment generation
required: true
/core/v1/sandbox/enrollment-tokens:
post:
@@ -4368,11 +4419,15 @@ components:
them; Core then uses the validated template build's cpus and memory_mib.
runtime:
$ref: '#/components/schemas/sandbox.RuntimeRelease'
+ required:
+ - expected_generation
type: object
api.SandboxDeploymentInput:
properties:
e2b:
$ref: '#/components/schemas/api.SandboxE2BInput'
+ expected_generation:
+ type: integer
provider:
type: string
resources:
@@ -4383,6 +4438,8 @@ components:
them; Core then uses the validated template build's cpus and memory_mib.
runtime:
$ref: '#/components/schemas/sandbox.RuntimeRelease'
+ required:
+ - expected_generation
type: object
api.SandboxE2BInput:
properties:
@@ -4902,14 +4959,17 @@ components:
type: integer
installation_id:
type: string
- maintenance:
- type: boolean
mode:
type: string
owner_epoch:
type: integer
provider:
type: string
+ reset:
+ anyOf:
+ - allOf:
+ - $ref: '#/components/schemas/store.SandboxResetView'
+ - type: 'null'
resources:
$ref: '#/components/schemas/store.SandboxDeploymentResources'
specification:
@@ -5140,12 +5200,64 @@ components:
- $ref: '#/components/schemas/store.SandboxE2BTemplateBuildView'
description: The fixed template build as Core read it when this selection was saved.
type: object
- store.SandboxMaintenanceRequest:
+ store.SandboxResetOfflineNode:
properties:
+ name:
+ type: string
+ node_id:
+ type: string
+ resources:
+ type: integer
+ type: object
+ store.SandboxResetRemaining:
+ properties:
+ busy:
+ type: integer
+ cleanup:
+ type: integer
+ idle:
+ type: integer
+ offline_nodes:
+ items:
+ $ref: '#/components/schemas/store.SandboxResetOfflineNode'
+ type: array
+ on_offline_nodes:
+ type: integer
+ type: object
+ store.SandboxResetRequest:
+ properties:
+ clear:
+ enum:
+ - auto
+ - force
+ type: string
+ deadline_seconds:
+ maximum: 86400
+ minimum: 300
+ type: integer
expected_generation:
+ minimum: 0
type: integer
- maintenance:
- type: boolean
+ required:
+ - clear
+ - expected_generation
+ type: object
+ store.SandboxResetView:
+ properties:
+ clear:
+ type: string
+ deadline_at:
+ anyOf:
+ - type: string
+ - type: 'null'
+ forced_at:
+ anyOf:
+ - type: string
+ - type: 'null'
+ remaining:
+ $ref: '#/components/schemas/store.SandboxResetRemaining'
+ requested_at:
+ type: string
type: object
store.SandboxSuspensionView:
properties:
diff --git a/apps/docs/openapi/sources.json b/apps/docs/openapi/sources.json
index ddcaef8ac..3633bab31 100644
--- a/apps/docs/openapi/sources.json
+++ b/apps/docs/openapi/sources.json
@@ -1,7 +1,7 @@
{
"sources": {
"contracts/agents-api/openapi.yaml": "a8896e8b67d78ed0608c7cc5636077ec33f4b4895a04bd6cfb8b038afe941e01",
- "contracts/agents-api/core.openapi.yaml": "488a50fa308194224ee5ee879b7f6e38166d1b08dedc2bbc5bb39f38a844c87f",
+ "contracts/agents-api/core.openapi.yaml": "26500cd07c72e72bfda32dcc83594e8d182aeb08debf4b67ac3938b5a9a078fe",
"contracts/agents-api/runtime.openapi.yaml": "b3fb025b20a0bcf4635d60339479b2b07f6dbed80f51e72f40a63b31e42b5f4a"
},
"outputs": {
@@ -21,7 +21,7 @@
"openapi/public-api.yaml": "84ae8a33717c817950d757990018edd10a4d31d5a343c8172baa099d101c5a1f",
"content/docs/api-reference/meta.json": "56bf1dc145ccc8adde38c5f956f1a06f18ef18b646122250d01a294c516f8f83",
"content/docs/api-reference/index.mdx": "9fd6b1c54149874d2999235067c101326f9e8a2e707a70b6672c81eb3ebfe63f",
- "content/docs/api-reference/core/core-administration.mdx": "a1261c7998017de3761ca90ca073a0b89a6c528ff37a634f3d8b986ee9540fc5",
+ "content/docs/api-reference/core/core-administration.mdx": "5fc3ada51d2190a141f13738151e4d513390cc3bd66ce6f5013994bda68569b0",
"content/docs/api-reference/core/deployment-model-providers.mdx": "b2795fadcc05a1e7195b6d34c3f234cb29cc5e3f580d1521e31965453e654347",
"content/docs/api-reference/core/administrator-projects.mdx": "8fd687bb2a7119fa16e938d7c71710c2816afb9cf959eb6849ef5e6300c5419b",
"content/docs/api-reference/core/agents.mdx": "b6c089fbea4b07bc4249e7c7ca5699d47f1512a4fc2d8ee01cb26910ae541a84",
@@ -39,8 +39,8 @@
"content/docs/api-reference/core/skills.mdx": "ed899ade7b032b1732b3097d81419a8fdbb9ead0594fca9f2107da644b97031f",
"content/docs/api-reference/core/vaults.mdx": "12d0aac96cd4a3472ef4eaa287f7945f78862017956e79bfc3ed0ef8e772e78a",
"content/docs/api-reference/core/credentials.mdx": "27d586b9a8fd738b9947802f32c80458195370252886bc26abedccf2125ee981",
- "content/docs/api-reference/core/sandbox-manager.mdx": "36f2895b9969d27be843d5cebf935bf14ca9400ad076e56074a25905e3df5d1b",
- "openapi/core-api.yaml": "9872f80cec2661c8539b6652a294c16c668f581399afd20f76b198279faae9ed",
+ "content/docs/api-reference/core/sandbox-manager.mdx": "4e0d90b158d3cdb9632db53dfacaae8c2749596373422c554f81178c8a96e7e2",
+ "openapi/core-api.yaml": "38614bb103da6b89dceaedc8c82874bc92590a49da005f07a5e8a0147b291676",
"content/docs/api-reference/core/meta.json": "588b163ed2de2100b0f8376688274f80698a6bf6ac037eef56d17c93fa8a5062",
"content/docs/api-reference/core/index.mdx": "ddf0a8d87a6c21761beac703a32f2a40486a1c74abb341bd726f40c44a866405",
"content/docs/api-reference/machine/sandbox-node.mdx": "5b367a08d43cea82686345d2ba1c75cc90627a7b2970fe42a0df9d77fb3e1e80",
diff --git a/apps/docs/scripts/check-python.test.mjs b/apps/docs/scripts/check-python.test.mjs
index e6bce3365..05d5b6697 100644
--- a/apps/docs/scripts/check-python.test.mjs
+++ b/apps/docs/scripts/check-python.test.mjs
@@ -21,7 +21,7 @@ test('route gate and Python regressions honor the selected interpreter without s
cwd: appRoot, env: { ...process.env, OAC_TEST_OFFICIAL_SDK_PYTHON: selected }, encoding: 'utf8',
})
assert.equal(result.status, 0, result.stdout + result.stderr)
- if (mode === 'routes') assert.match(result.stdout, /Contract operations:\s+130/)
+ if (mode === 'routes') assert.match(result.stdout, /Contract operations:\s+[1-9]\d*\b/)
else assert.match(result.stderr, /Ran 2 tests/)
}
const invocations = fs.readFileSync(log, 'utf8').trim().split('\n').map(line => JSON.parse(line))
diff --git a/apps/docs/scripts/generate-guides.mjs b/apps/docs/scripts/generate-guides.mjs
index a40b69592..29cbb97b0 100644
--- a/apps/docs/scripts/generate-guides.mjs
+++ b/apps/docs/scripts/generate-guides.mjs
@@ -8,7 +8,7 @@ const app = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
const repo = path.resolve(app, '../..')
const guides = JSON.parse(fs.readFileSync(path.join(app, 'scripts/guides.json'), 'utf8'))
const routes = new Map(guides.map(g => [g.source, g.slug === 'index' ? '/' : '/' + g.slug]))
-const sourceRevision = '75bf4484ba957344ae374354858bbdfe104f5c4f'
+const sourceRevision = 'f36ac3592bfe9f77a2172680aa52b14c6f525062'
const sourceURL = relative => `https://github.com/MiniMax-AI/parsar-core/blob/${sourceRevision}/${relative}`
const digest = text => crypto.createHash('sha256').update(text).digest('hex')
function mdx(text) {
diff --git a/apps/web/DESIGN.md b/apps/web/DESIGN.md
index 07de356dd..73d80fc76 100644
--- a/apps/web/DESIGN.md
+++ b/apps/web/DESIGN.md
@@ -736,8 +736,7 @@ while the deployment cannot be read; Show Getting started, a quiet row above the
sidebar's account controls, opens it again at any time.
### Sandbox setup
-Setting up hosted sandboxes, and changing the provider or resources in maintenance,
-is a set of pages inside the Nodes page, one decision each: where sandboxes run (own
+Setting up hosted sandboxes is a set of pages inside the Nodes page, one decision each: where sandboxes run (own
machines or E2B), then the backend or the E2B account, then the size of each sandbox
(three presets; E2B skips it, since each sandbox takes the template build's size),
then a review. Choices are large cards that advance on a click; short indigo dashes
@@ -756,8 +755,41 @@ then says to enter it again, with a link to that step.
Advanced settings, one link away, hold the complete form: resources (not for
E2B), the Runtime release and the E2B template. A change keeps the saved size
and Runtime while the backend stays the same (a saved size outside the presets is
-offered as Current); another backend starts from its standard size and this
-console's Runtime, and E2B always needs its key again. Rules sit behind help tips.
+offered as Current). Same-backend editing starts at size or E2B credentials with
+the provider fixed, and is available only with zero held resources;
+changing the backend requires reset and then a new setup. E2B always needs its key
+again. Optional explanations sit behind help tips; errors and safety consequences
+remain visible.
+
+### Sandbox reset
+The deployment section offers explicit reset rather than maintenance/resume. Reuse
+its existing panels and confirmation dialogs. Auto clear is selected first, with a
+one-hour deadline editable from 5 minutes to 24 hours; Force clear and escalation
+require destructive confirmation. State directly that hosted work is archived,
+remaining active work may be cancelled, archived Sessions cannot resume and
+unpersisted workspace contents may be lost. Histories and persisted Files/Artifacts
+remain, and self-hosted execution is unaffected. Cancelling an active reset stops
+further clearing but cannot undo completed archives.
+
+A persistent progress panel uses Core's busy, idle and cleanup counts, deadline and
+named offline-node blockers. Bring blocked nodes online for confirmed cleanup;
+never offer a browser-side force-release shortcut. Poll the deployment every five
+seconds only while its reset is non-null. A passed deadline does not establish force
+or completion; only a Core response does. Completion opens the existing setup flow,
+with a new explicit save using the generation read from Core, including zero on a
+fresh install. No automatic configuration replay or online-rollout progress exists.
+
+Read deployment progress independently of node details. Partial failures retain
+successful facts with a visible stale/unavailable notice. An uncertain write opens
+the existing recovery dialog and requires a new authoritative read before another
+mutation; refresh reads state and never resubmits the write. The connection's
+QueryClient owns both the authoritative deployment and pending or uncertain writes
+across route transitions. Leaving Nodes cannot cancel or forget a submitted reset,
+and a cached node snapshot cannot replace a newer reset or completion learned on
+Overview or System. Returning to Nodes reads the shared deployment immediately and
+refreshes node evidence separately. Only a successful authoritative read begun after
+the write settles can release the mutation block; an earlier or still-pending read
+cannot. Logout clears this connection-scoped state.
### System page
Four sections, each saying where it changes. Installation: the public address, API
diff --git a/apps/web/PRODUCT.md b/apps/web/PRODUCT.md
index 7455ee3dd..881c7d8f0 100644
--- a/apps/web/PRODUCT.md
+++ b/apps/web/PRODUCT.md
@@ -107,6 +107,18 @@ workbench.
- A node enrolled with an earlier Core address gets no new sandboxes, so on the Nodes
list and its page its status is Old address, with "Remove and add again", never
Available.
+- **Sandbox reset** is an explicit administrator operation on Nodes. Auto clear is
+ the default, with a one-hour deadline (5 minutes–24 hours); Force clear requires
+ destructive confirmation. Reset stops new hosted Session admission, clears idle,
+ suspended and pending hosted work, and waits for busy Turns and file writes until
+ Core forces the remaining work. It does not affect self-hosted execution.
+ Histories and persisted Files/Artifacts remain; archived Sessions cannot resume,
+ and unpersisted workspace contents may be lost. Cancel stops further clearing
+ without undoing archives. Core alone reports progress and completion, including
+ resources blocked on named offline nodes; force does not bypass their cleanup.
+ Completion clears the backend configuration and retires old nodes/enrollment
+ credentials. A new configuration is then a separate deliberate save. Same-backend
+ editing currently requires zero held resources; online rollout is not available.
- **E2B deployments** have no machines: the Nodes entry becomes Sandbox backend,
and Overview and Sandbox metrics show the sandboxes Core holds in E2B's cloud
(running, starting, size, template build) instead of node capacity, with no node column
diff --git a/apps/web/e2e/console.ts b/apps/web/e2e/console.ts
index 91e00a8d5..82aba5d5b 100644
--- a/apps/web/e2e/console.ts
+++ b/apps/web/e2e/console.ts
@@ -1,3 +1,4 @@
+import type { SandboxDeployment } from "@agents-core-web/agents-client";
import { expect, type APIRequestContext, type Page } from "@playwright/test";
const fixture = `http://127.0.0.1:${process.env.AGENTS_FIXTURE_PORT ?? 18092}`;
@@ -42,6 +43,13 @@ export async function setNode(request: APIRequestContext, node: { id: string; na
await request.post(`${fixture}/__fixture/node`, { data: node });
}
+/** Supplies a later Core observation; only an explicit completion retires nodes and advances generation. */
+export async function setDeployment(request: APIRequestContext, deployment: Partial | { complete_reset: true }): Promise {
+ const response = await request.post(`${fixture}/__fixture/deployment`, { data: deployment });
+ expect(response.ok()).toBe(true);
+ return response.json() as Promise;
+}
+
/** Archives a project behind the console's back, as another administrator would. */
export async function archiveProject(request: APIRequestContext, projectId: string) {
await request.post(`${fixture}/core/v1/projects/${projectId}/archive`, { headers: { cookie: "core_console=fixture-session" } });
diff --git a/apps/web/e2e/fixture-console.mjs b/apps/web/e2e/fixture-console.mjs
index e2b25125b..459334ed7 100644
--- a/apps/web/e2e/fixture-console.mjs
+++ b/apps/web/e2e/fixture-console.mjs
@@ -71,8 +71,12 @@ function installation() {
};
}
+function unconfiguredDeployment(generation = 0, ownerEpoch = 3) {
+ return { installation_id: INSTALLATION_ID, provider: "", core_url: publicUrl(), reset: null, owner_epoch: ownerEpoch, generation, mode: "", resources: { allocations: 0, pending: 0 }, suspension: null };
+}
+
function configuredDeployment() {
- return { installation_id: INSTALLATION_ID, provider: "docker", core_url: publicUrl(), maintenance: false, owner_epoch: 3, generation: 1, mode: "nodes", resources: { allocations: 0, pending: 0 }, specification: { resources: { cpus: 2, memory_mib: 4096 }, runtime: release }, specification_digest: "fixture", suspension: null };
+ return { installation_id: INSTALLATION_ID, provider: "docker", core_url: publicUrl(), reset: null, owner_epoch: 3, generation: 1, mode: "nodes", resources: { allocations: 0, pending: 0 }, specification: { resources: { cpus: 2, memory_mib: 4096 }, runtime: release }, specification_digest: "fixture", suspension: null };
}
/** 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 } };
@@ -117,7 +121,7 @@ function reset(mode = "login", fresh = false, sandbox = "configured", nodes = "d
// The providers whose node files the console serves (/console/config node_artifacts).
nodeArtifacts: artifacts.split(",").filter(Boolean),
};
- state.deployment = sandbox === "none" ? null : sandbox === "e2b" ? e2bDeployment() : configuredDeployment();
+ state.deployment = sandbox === "none" ? unconfiguredDeployment() : sandbox === "e2b" ? e2bDeployment() : configuredDeployment();
// Each node reports the address it enrolled with; in "stale" mode the first one enrolled before public_url changed.
// The seeded nodes enrolled before Core recorded enrollment IDs.
state.nodes.forEach((node, index) => { node.core_url = address === "stale" && index === 0 ? OLD_URL : publicUrl(); node.enrollment_id = null; });
@@ -302,7 +306,34 @@ function nodeDetail(node) {
};
}
-async function sandboxRoute(request, response, path) {
+async function sandboxRoute(request, response, path, url) {
+ // Retired even for authenticated callers; never reinterpret maintenance as reset.
+ if (path === "/deployment/maintenance") return error(response, 404, "Not found.");
+ if (path === "/deployment/reset" && (request.method === "POST" || request.method === "DELETE")) {
+ const input = request.method === "POST" ? await body(request) : { expected_generation: Number(url.searchParams.get("expected_generation")) };
+ if (!Number.isInteger(input.expected_generation) || input.expected_generation < 0 || (request.method === "DELETE" && !url.searchParams.has("expected_generation"))) return error(response, 400, "expected_generation is required.", "invalid_request_error");
+ if (input.expected_generation !== state.deployment.generation) return error(response, 409, "The sandbox configuration changed. Refresh before submitting again.", "sandbox_generation_stale");
+ if (request.method === "DELETE") {
+ state.deployment.reset = null;
+ return send(response, 200, state.deployment);
+ }
+ if (input.clear !== "auto" && input.clear !== "force") return error(response, 400, "clear must be auto or force.", "invalid_request_error");
+ const seconds = input.deadline_seconds ?? 3600;
+ if (!Number.isInteger(seconds) || seconds < 300 || seconds > 86400 || (input.clear === "force" && input.deadline_seconds !== undefined)) return error(response, 400, "Invalid reset deadline.", "invalid_request_error");
+ if (state.deployment.reset?.clear === "force" && input.clear === "auto") return error(response, 409, "A force reset cannot return to auto clear.", "sandbox_reset_in_progress");
+ if (!state.deployment.provider || state.deployment.reset?.clear === input.clear) return send(response, 200, state.deployment);
+ const now = new Date().toISOString();
+ const previous = state.deployment.reset;
+ state.deployment.reset = {
+ clear: input.clear, requested_at: previous?.requested_at ?? now,
+ deadline_at: previous?.deadline_at ?? (input.clear === "auto" ? new Date(Date.now() + seconds * 1000).toISOString() : null),
+ forced_at: input.clear === "force" ? now : null,
+ remaining: previous?.remaining ?? { busy: state.deployment.resources.allocations, idle: state.deployment.resources.pending, cleanup: 0, on_offline_nodes: 0, offline_nodes: [] },
+ };
+ // The fixture has no cleanup worker or deadline timer. Tests explicitly supply
+ // subsequent Core projections; a browser clock never completes a reset.
+ return send(response, 200, state.deployment);
+ }
if (path === "/runtime-observations" && request.method === "GET") {
// Only E2B reports a sandbox's disk.
const e2b = state.deployment?.provider === "e2b";
@@ -314,8 +345,13 @@ async function sandboxRoute(request, response, path) {
const initialize = request.method === "POST";
// As Core, before any state check: the address is config.json's public_url and read-only.
if ("core_url" in input) return error(response, 400, "core_url is derived from the installation public URL (public_url in config.json, OAC_PUBLIC_URL for Core) and cannot be set here. Remove it.", "invalid_request_error", "core_url");
- if (initialize && state.deployment) return error(response, 409, "The sandbox deployment is already configured.", "sandbox_deployment_conflict");
- if (!initialize && !state.deployment) return error(response, 409, "The sandbox deployment is not configured.", "sandbox_deployment_conflict");
+ if (!Number.isInteger(input.expected_generation) || input.expected_generation < 0) return error(response, 400, "expected_generation is required.", "invalid_request_error");
+ if (input.expected_generation !== state.deployment.generation) return error(response, 409, "The sandbox configuration changed. Refresh before submitting again.", "sandbox_generation_stale");
+ if (state.deployment.reset) return error(response, 409, "A sandbox reset is in progress.", "sandbox_reset_in_progress");
+ if (!initialize && input.provider !== state.deployment.provider) return error(response, 409, "Reset before changing the sandbox backend.", "sandbox_reset_required");
+ if (!initialize && state.deployment.resources.allocations + state.deployment.resources.pending > 0) return error(response, 409, "Sandbox resources remain.", "sandbox_in_use");
+ if (initialize && state.deployment.provider) return error(response, 409, "The sandbox deployment is already configured.", "sandbox_deployment_conflict");
+ if (!initialize && !state.deployment.provider) return error(response, 409, "The sandbox deployment is not configured.", "sandbox_deployment_conflict");
const e2b = input.provider === "e2b";
if (!e2b && (!input.resources || !input.runtime)) return error(response, 400, "resources and runtime are required.", "invalid_sandbox_configuration");
// As Core (ErrSandboxPublicURLUnreachable): E2B sandboxes reach Core over the internet, which a loopback public_url cannot serve.
@@ -324,7 +360,7 @@ async function sandboxRoute(request, response, path) {
const resources = input.resources ?? { cpus: templateBuild.resources.cpus, memory_mib: templateBuild.resources.memory_mib };
state.deployment = {
...configuredDeployment(), provider: input.provider, mode: e2b ? "direct" : "nodes",
- ...(initialize ? {} : { maintenance: true, generation: state.deployment.generation + 1 }),
+ generation: state.deployment.generation + 1, owner_epoch: state.deployment.owner_epoch,
specification: { resources, ...(input.runtime ? { runtime: input.runtime } : {}) },
...(e2b ? { e2b: { template: input.e2b?.template ?? "", credential_configured: true, template_build: templateBuild } } : {}),
suspension: input.provider === "microsandbox" ? { idle_seconds: 300, retention_seconds: 86400 } : null,
@@ -332,10 +368,11 @@ async function sandboxRoute(request, response, path) {
return send(response, 200, state.deployment);
}
if (path === "/deployment") {
- return send(response, 200, state.deployment ?? { installation_id: INSTALLATION_ID, provider: "", core_url: publicUrl(), maintenance: false, owner_epoch: 3, generation: 0, mode: "", resources: { allocations: 0, pending: 0 }, suspension: null });
+ return send(response, 200, state.deployment);
}
if (path === "/nodes") return send(response, 200, { data: state.nodes });
if (path === "/enrollment-tokens" && request.method === "POST") {
+ if (state.deployment.reset) return error(response, 409, "A sandbox reset is in progress.", "sandbox_reset_in_progress");
// As Core: a one-time token valid for ten minutes, whose limits and enrollment ID the node it enrolls takes;
// only microsandbox keeps a retained limit above the active one.
const input = await body(request);
@@ -498,6 +535,17 @@ async function fixtureRoute(request, response, url) {
reset(url.searchParams.get("auth") ?? "login", url.searchParams.get("projects") === "none", url.searchParams.get("sandbox") ?? "configured", url.searchParams.get("nodes") ?? "demo", url.searchParams.get("installation") ?? "public", url.searchParams.get("credentials") ?? "configured", url.searchParams.get("installers") !== "none", url.searchParams.get("artifacts") ?? undefined);
return send(response, 200, { ok: true });
}
+ if (url.pathname === "/__fixture/deployment" && request.method === "POST") {
+ // Explicit backend observations, never a simulation driven by browser time.
+ const input = await body(request);
+ if (input.complete_reset) {
+ state.deployment = unconfiguredDeployment(state.deployment.generation + 1, state.deployment.owner_epoch + 1);
+ state.nodes = [];
+ state.allocations = [];
+ state.enrollment = null;
+ } else Object.assign(state.deployment, input);
+ return send(response, 200, state.deployment);
+ }
if (url.pathname === "/__fixture/fail-next" && request.method === "POST") {
state.failNext = await body(request); // { method, path, status, code?, message? }
return send(response, 200, { ok: true });
@@ -537,7 +585,7 @@ http.createServer(async (request, response) => {
state.failNext = null;
return error(response, fail.status, fail.message ?? "Injected failure.", fail.code ?? null);
}
- if (url.pathname.startsWith("/core/v1/sandbox/")) return await sandboxRoute(request, response, url.pathname.slice("/core/v1/sandbox".length));
+ if (url.pathname.startsWith("/core/v1/sandbox/")) return await sandboxRoute(request, response, url.pathname.slice("/core/v1/sandbox".length), url);
if (url.pathname.startsWith("/core/v1/")) {
const path = url.pathname.slice("/core/v1".length);
if (path === "/harnesses" || path.startsWith("/harnesses/")) return await harnessRoute(request, response, path);
diff --git a/apps/web/e2e/nodes.spec.ts b/apps/web/e2e/nodes.spec.ts
index 031c20562..9b56415b0 100644
--- a/apps/web/e2e/nodes.spec.ts
+++ b/apps/web/e2e/nodes.spec.ts
@@ -257,71 +257,30 @@ test("saves E2B without opening Add node, as it has no machines", async ({ page,
await expect(page.getByRole("dialog")).toHaveCount(0);
});
-test("keeps the saved size and Runtime for the same backend, and starts another from its defaults", async ({ page, request }) => {
+test("edits only the saved backend, preserving a custom size and Runtime", async ({ page, request }) => {
const runtime = { source_commit: "0".repeat(40), image_id: `sha256:${"a".repeat(64)}`, image_manifest_digest: `sha256:${"b".repeat(64)}`,
microsandbox_ref: `oac-runtime@sha256:${"b".repeat(64)}`, runtime_sha256: "c".repeat(64), firmware_sha256: "d".repeat(64) };
const current = { resources: { cpus: 7, memory_mib: 8192 }, runtime };
- const deployment = { installation_id: "94be54a1-138c-4f30-bc87-b13686272dbe", provider: "docker", core_url: "https://core.example", maintenance: true,
+ let deployment = { installation_id: "94be54a1-138c-4f30-bc87-b13686272dbe", provider: "docker", core_url: "https://core.example", reset: null,
owner_epoch: 1, generation: 1, mode: "nodes", resources: { allocations: 0, pending: 0 }, specification: current, specification_digest: "e".repeat(64),
suspension: null };
let submitted: Record | null = null;
await page.route("**/core/v1/sandbox/deployment", async (route) => {
- if (route.request().method() === "PUT") submitted = route.request().postDataJSON() as Record;
+ if (route.request().method() === "PUT") {
+ submitted = route.request().postDataJSON() as Record;
+ deployment = { ...deployment, generation: 2 };
+ }
await route.fulfill({ json: deployment });
});
await page.route("**/core/v1/sandbox/nodes", (route) => route.fulfill({ json: { data: [] } }));
await openConsole(page, request, "nodes");
- const save = page.getByRole("button", { name: "Save and stay in maintenance" });
- const back = page.getByRole("button", { name: "Back" });
-
- // The same backend keeps its saved size and Runtime.
- await page.getByRole("button", { name: "Change provider or resources" }).click();
- await page.getByRole("button", { name: "Own machines" }).click();
- await page.getByRole("button", { name: "Docker" }).click();
+ await page.getByRole("button", { name: "Change resources" }).click();
+ await expect(page.getByRole("button", { name: "Own machines" })).toHaveCount(0);
+ await expect(page.getByRole("button", { name: "E2B cloud" })).toHaveCount(0);
await page.getByRole("button", { name: /^Current/ }).click();
- await save.click();
- await expect.poll(() => submitted?.resources).toEqual(current.resources);
- expect(submitted?.runtime).toEqual(runtime);
- // Core's address is config.json's public_url: a change never sends it.
+ await page.getByRole("button", { name: "Save configuration" }).click();
+ await expect.poll(() => submitted).toMatchObject({ provider: "docker", expected_generation: 1, resources: current.resources, runtime });
expect(submitted).not.toHaveProperty("core_url");
-
- // Another backend starts from its own size, with disks, and this console's Runtime.
- await back.click();
- await back.click();
- await page.getByRole("button", { name: "microsandbox" }).click();
- await page.getByRole("button", { name: /^Standard/ }).click();
- await save.click();
- await expect.poll(() => submitted?.provider).toBe("microsandbox");
- expect(submitted?.resources).toEqual({ cpus: 2, memory_mib: 4096, root_disk_mib: 8192, environment_disk_mib: 8192 });
- expect(submitted?.runtime).toMatchObject({ source_commit: "c0ffee".padEnd(40, "0") });
-
- // E2B needs its key again and takes its size from the template build: no size, Runtime or disks.
- await back.click();
- await back.click();
- await back.click();
- await page.getByRole("button", { name: "E2B cloud" }).click();
- await page.getByLabel("E2B API key").fill("fixture-private-key");
- await page.getByLabel("Template build").fill("template:94be54a1-138c-4f30-bc87-b13686272dbe");
- await page.getByRole("button", { name: "Next" }).click();
- await save.click();
- await expect.poll(() => submitted?.provider).toBe("e2b");
- expect(submitted?.resources).toBeUndefined();
- expect(submitted?.runtime).toBeUndefined();
-});
-
-test("reports a failed sandbox change in a dialog, then reads the state again", async ({ page, request }) => {
- await openConsole(page, request, "nodes");
- // Core's answer is lost, so the change may have been saved.
- await page.route("**/core/v1/sandbox/deployment/maintenance", (route) => route.fulfill({
- status: 503,
- contentType: "application/json",
- body: JSON.stringify({ error: { message: "Unavailable.", type: "server_error", code: null, param: null } }),
- }));
- await page.getByRole("button", { name: "Enter maintenance to change provider" }).click();
- const failed = page.getByRole("dialog", { name: "Couldn't confirm the sandbox change" });
- await failed.getByRole("button", { name: "Refresh sandbox state" }).click();
- await expect(failed).toBeHidden();
- await expect(page.getByRole("button", { name: "Enter maintenance to change provider" })).toBeEnabled();
});
test("keeps the page usable when Core refuses a sandbox change, and shows Core's reason", async ({ page, request }) => {
diff --git a/apps/web/e2e/public-url.spec.ts b/apps/web/e2e/public-url.spec.ts
index 156e4f4df..b406bd4c7 100644
--- a/apps/web/e2e/public-url.spec.ts
+++ b/apps/web/e2e/public-url.spec.ts
@@ -19,6 +19,7 @@ test("sets up sandboxes with Core's address read-only, never sending it", async
await expect(page.getByRole("dialog", { name: "Add node" })).toBeVisible();
expect(await writes(request)).toEqual(["POST /core/v1/sandbox/deployment"]);
expect(bodies.some((entry) => entry.includes("core_url"))).toBe(false);
+ expect(bodies.filter(Boolean).map((entry) => JSON.parse(entry))).toEqual([expect.objectContaining({ expected_generation: 0 })]);
});
test("explains an E2B rejection in the wizard, with the file to edit and the command to apply it", async ({ page, request }) => {
diff --git a/apps/web/e2e/sandbox-reset.spec.ts b/apps/web/e2e/sandbox-reset.spec.ts
new file mode 100644
index 000000000..c7a302f32
--- /dev/null
+++ b/apps/web/e2e/sandbox-reset.spec.ts
@@ -0,0 +1,345 @@
+import { expect, test, type Page, type TestInfo } from "@playwright/test";
+import type { SandboxReset } from "@agents-core-web/agents-client";
+
+import { expectManagementBoundary, FIXTURE_CORE_KEY, openConsole, setDeployment, writes } from "./console";
+
+const deploymentURL = "**/core/v1/sandbox/deployment";
+const resetURL = "**/core/v1/sandbox/deployment/reset";
+const resetPath = "/core/v1/sandbox/deployment/reset";
+const unavailable = { status: 503, json: { error: { type: "server_error", code: null, param: null, message: "Deployment read unavailable." } } };
+const observedReset = (clear: "auto" | "force" = "auto"): SandboxReset => ({
+ clear, requested_at: "2026-09-26T10:00:00Z", deadline_at: "2026-09-26T11:00:00Z",
+ forced_at: clear === "force" ? "2026-09-26T11:00:01Z" : null,
+ remaining: { busy: 2, idle: 1, cleanup: 1, on_offline_nodes: 2, offline_nodes: [{ node_id: "offline-owned", name: "offline-build-worker", resources: 2 }] },
+});
+const progress = (page: Page) => page.getByRole("region", { name: "Reset in progress" });
+async function openReset(page: Page) {
+ await page.getByRole("button", { name: "Reset deployment", exact: true }).click();
+ return page.getByRole("dialog", { name: "Reset sandbox deployment?", exact: true });
+}
+async function capture(page: Page, info: TestInfo, name: string) {
+ const path = info.outputPath(`${name}.png`);
+ await page.screenshot({ path, fullPage: true });
+ await info.attach(name, { path, contentType: "image/png" });
+}
+
+test.afterEach(async ({ request }) => {
+ await expectManagementBoundary(request);
+ expect((await writes(request)).filter((entry) => entry.includes("/maintenance"))).toEqual([]);
+});
+
+test("auto reset requires a bounded deadline, then preserves Core progress past that deadline", async ({ page, request }, info) => {
+ await page.setViewportSize({ width: 1280, height: 1000 });
+ await page.emulateMedia({ colorScheme: "light", reducedMotion: "reduce" });
+ await openConsole(page, request, "nodes");
+ await setDeployment(request, { resources: { allocations: 3, pending: 1 } });
+ await page.getByRole("button", { name: "Refresh sandbox state", exact: true }).click();
+ const dialog = await openReset(page);
+ await expect(dialog).toBeVisible();
+ await page.keyboard.press("Escape");
+ await expect(dialog).toBeHidden();
+ const entry = page.getByRole("button", { name: "Reset deployment", exact: true });
+ await expect(entry).toBeFocused();
+ expect(await writes(request)).toEqual([]);
+ await entry.press("Enter");
+ await expect(dialog).toBeVisible();
+ const deadline = dialog.getByLabel(/Force remaining work after \(seconds\)/);
+ await expect(deadline).toHaveValue("3600");
+ await deadline.fill("299");
+ await expect(dialog.getByRole("button", { name: "Reset deployment", exact: true })).toBeDisabled();
+ await deadline.fill("86401");
+ await expect(dialog.getByRole("button", { name: "Reset deployment", exact: true })).toBeDisabled();
+ await deadline.fill("300");
+ await expect(dialog).toContainText("Archived Sessions cannot be resumed");
+ await expect(dialog).toContainText("self-hosted");
+ await expect(page.locator("html")).toHaveAttribute("data-theme", "light");
+ await capture(page, info, "reset-confirm-en-light-1280");
+ const submitted = page.waitForRequest((sent) => sent.method() === "POST" && sent.url().endsWith(resetPath));
+ await dialog.getByRole("button", { name: "Reset deployment", exact: true }).click();
+ expect((await submitted).postDataJSON()).toEqual({ expected_generation: 1, clear: "auto", deadline_seconds: 300 });
+ await expect(progress(page)).toBeVisible();
+ // These timestamps are in the past: the browser still cannot invent force or completion.
+ await setDeployment(request, { resources: { allocations: 3, pending: 1 }, reset: observedReset() });
+ await page.getByRole("button", { name: "Refresh sandbox state", exact: true }).click();
+ await expect(progress(page)).toContainText("offline-build-worker");
+ await expect(progress(page)).toContainText("Busy work can finish until the deadline.");
+ await expect(page.getByRole("button", { name: "Add node", exact: true })).toBeDisabled();
+ await expect(page.getByRole("heading", { name: "Where should sandboxes run?" })).toHaveCount(0);
+ await capture(page, info, "reset-progress-en-light-1280");
+ expect(await writes(request)).toEqual([`POST ${resetPath}`]);
+});
+
+test("escalation and cancellation require explicit confirmation and never undo previous clearing", async ({ page, request }) => {
+ await openConsole(page, request, "nodes");
+ await setDeployment(request, { resources: { allocations: 3, pending: 1 }, reset: observedReset() });
+ await page.reload();
+ await progress(page).getByRole("button", { name: "Force reset now" }).click();
+ let confirm = page.getByRole("dialog", { name: "Force reset now?" });
+ await confirm.getByRole("button", { name: "Back", exact: true }).click();
+ expect(await writes(request)).toEqual([]);
+ await progress(page).getByRole("button", { name: "Force reset now" }).click();
+ confirm = page.getByRole("dialog", { name: "Force reset now?" });
+ const force = page.waitForRequest((sent) => sent.method() === "POST" && sent.url().endsWith(resetPath));
+ await confirm.getByRole("button", { name: "Force reset now", exact: true }).click();
+ expect((await force).postDataJSON()).toEqual({ expected_generation: 1, clear: "force" });
+ await expect(progress(page)).toContainText("Remaining hosted Sessions are being archived");
+ await expect(progress(page)).toContainText("offline-build-worker");
+ await expect(progress(page).getByRole("button", { name: "Force reset now" })).toHaveCount(0);
+ await progress(page).getByRole("button", { name: "Cancel reset" }).click();
+ confirm = page.getByRole("dialog", { name: "Cancel reset?" });
+ await expect(confirm).toContainText("Sessions already archived stay archived");
+ const cancel = page.waitForRequest((sent) => sent.method() === "DELETE" && sent.url().includes(resetPath));
+ await confirm.getByRole("button", { name: "Cancel reset", exact: true }).click();
+ expect(new URL((await cancel).url()).searchParams.get("expected_generation")).toBe("1");
+ await expect(progress(page)).toHaveCount(0);
+ await expect(page.getByRole("button", { name: "Reset deployment", exact: true })).toBeEnabled();
+ expect(await writes(request)).toEqual([`POST ${resetPath}`, `DELETE ${resetPath}`]);
+});
+
+test("force reset requires confirmation and reconfiguration uses the completed generation once", async ({ page, request }) => {
+ await openConsole(page, request, "nodes");
+ await setDeployment(request, { resources: { allocations: 1, pending: 0 } });
+ await page.getByRole("button", { name: "Refresh sandbox state", exact: true }).click();
+ const dialog = await openReset(page);
+ await dialog.getByLabel("Force — cancel remaining work now", { exact: true }).check();
+ await expect(dialog).toContainText("Force immediately cancels remaining work");
+ expect(await writes(request)).toEqual([]);
+ await dialog.getByRole("button", { name: "Reset deployment", exact: true }).click();
+ await expect(progress(page)).toContainText("Remaining hosted Sessions are being archived");
+ // Simulate the cleanup worker's completed projection, not elapsed browser time.
+ await setDeployment(request, { complete_reset: true });
+ await page.getByRole("button", { name: "Refresh sandbox state", exact: true }).click();
+ await page.getByRole("button", { name: "Own machines" }).click();
+ await page.getByRole("button", { name: "microsandbox Recommended" }).click();
+ await page.getByRole("button", { name: /^Standard/ }).click();
+ const configured = page.waitForRequest((sent) => sent.method() === "POST" && sent.url().endsWith("/sandbox/deployment"));
+ await page.getByRole("button", { name: "Save configuration" }).click();
+ expect((await configured).postDataJSON()).toMatchObject({ expected_generation: 2, provider: "microsandbox" });
+ await expect(page.getByRole("dialog", { name: "Add node" })).toBeVisible();
+ expect(await writes(request)).toEqual([`POST ${resetPath}`, "POST /core/v1/sandbox/deployment"]);
+});
+
+test("an applied reset with a lost response stays blocked through failed reads and is never replayed", async ({ page, request }) => {
+ await openConsole(page, request, "nodes");
+ await expect(page.getByRole("button", { name: "Reset deployment", exact: true })).toBeEnabled();
+ await setDeployment(request, { resources: { allocations: 1, pending: 0 } });
+ await page.getByRole("button", { name: "Refresh sandbox state", exact: true }).click();
+ await expect(page.getByRole("button", { name: "Reset deployment", exact: true })).toBeEnabled();
+ let failReads = true;
+ await page.route(resetURL, async (route) => {
+ await route.fetch(); // Core accepted the mutation, but its response never reached this browser.
+ await route.abort("failed");
+ });
+ await page.route(deploymentURL, (route) => failReads ? route.fulfill(unavailable) : route.continue());
+ const dialog = await openReset(page);
+ await dialog.getByRole("button", { name: "Reset deployment", exact: true }).click();
+ const uncertain = page.getByRole("dialog", { name: "Couldn't confirm the sandbox change" });
+ await expect(uncertain).toBeVisible();
+ await uncertain.getByRole("button", { name: "Refresh sandbox state" }).click();
+ await expect(page.getByRole("button", { name: "Reset deployment", exact: true })).toBeDisabled();
+ expect(await writes(request)).toEqual([`POST ${resetPath}`]);
+ failReads = false;
+ await page.getByRole("button", { name: "Refresh sandbox state", exact: true }).click();
+ await expect(progress(page)).toContainText("Busy work can finish until the deadline.");
+ await expect(progress(page).getByRole("button", { name: "Cancel reset" })).toBeEnabled();
+ expect(await writes(request)).toEqual([`POST ${resetPath}`]);
+});
+
+test("a stale reset is refused until fresh state is reviewed, with no automatic resubmission", async ({ page, request }) => {
+ await openConsole(page, request, "nodes");
+ await expect(page.getByRole("button", { name: "Reset deployment", exact: true })).toBeEnabled();
+ await setDeployment(request, { generation: 2, resources: { allocations: 3, pending: 1 } });
+ const dialog = await openReset(page);
+ const staleSubmission = page.waitForRequest((sent) => sent.method() === "POST" && sent.url().endsWith(resetPath));
+ const freshRead = page.waitForResponse(async (received) => received.request().method() === "GET"
+ && received.url().endsWith("/sandbox/deployment") && received.ok() && (await received.json()).generation === 2);
+ await dialog.getByRole("button", { name: "Reset deployment", exact: true }).click();
+ expect((await staleSubmission).postDataJSON()).toMatchObject({ expected_generation: 1 });
+ await expect(page.getByText("Core has a newer sandbox configuration. Refresh and review it before submitting again.")).toBeVisible();
+ expect(await (await freshRead).json()).toMatchObject({ generation: 2, reset: null });
+ // A new generation discards the old confirmation. The read can reconcile the
+ // page, but submitting its old draft again would be an unauthorized replay.
+ await expect(dialog).toBeHidden();
+ await expect(page.getByRole("button", { name: "Reset deployment", exact: true })).toBeEnabled();
+ expect(await writes(request)).toEqual([`POST ${resetPath}`]);
+
+ // The administrator reviews a new confirmation before the next explicit POST.
+ const reviewed = await openReset(page);
+ await expect(reviewed).toContainText("Archived Sessions cannot be resumed");
+ expect(await writes(request)).toEqual([`POST ${resetPath}`]);
+ const resubmitted = page.waitForRequest((sent) => sent.method() === "POST" && sent.url().endsWith(resetPath));
+ await reviewed.getByRole("button", { name: "Reset deployment", exact: true }).click();
+ expect((await resubmitted).postDataJSON()).toEqual({ expected_generation: 2, clear: "auto", deadline_seconds: 3600 });
+ await expect(progress(page)).toContainText("Busy work can finish until the deadline.");
+ expect(await writes(request)).toEqual([`POST ${resetPath}`, `POST ${resetPath}`]);
+});
+
+test("reset progress survives failed node and deployment reads with visible qualifications in Chinese dark mode", async ({ page, request }, info) => {
+ await page.setViewportSize({ width: 1280, height: 1000 });
+ await page.emulateMedia({ colorScheme: "dark" });
+ await openConsole(page, request, "nodes");
+ await setDeployment(request, { resources: { allocations: 3, pending: 1 }, reset: observedReset() });
+ await page.route("**/core/v1/sandbox/nodes", (route) => route.fulfill(unavailable));
+ await page.reload();
+ await expect(progress(page)).toContainText("offline-build-worker");
+ // Node detail failure cannot block a control needing only fresh deployment state.
+ await expect(progress(page).getByRole("button", { name: "Cancel reset" })).toBeEnabled();
+ await page.getByRole("button", { name: "Language and appearance" }).click();
+ await page.getByRole("menuitemradio", { name: "简体中文" }).click();
+ const localized = page.getByRole("region", { name: "正在重置", exact: true });
+ await expect(localized).toContainText("offline-build-worker");
+ await expect(localized).toContainText("正在归档空闲沙箱");
+ await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
+ await capture(page, info, "reset-progress-zh-dark-1280");
+ await page.route(deploymentURL, (route) => route.fulfill(unavailable));
+ await page.getByRole("button", { name: "刷新沙箱状态", exact: true }).click();
+ await expect(localized).toContainText("offline-build-worker");
+ await expect(localized.getByRole("button", { name: "取消重置" })).toBeDisabled();
+ await expect(localized).toContainText("无法刷新重置进度。当前显示的是上次确认的数量。");
+ expect(await writes(request)).toEqual([]);
+});
+
+test("a pending reset survives navigation and a lost response cannot reopen stale write controls", async ({ page, request }) => {
+ await openConsole(page, request, "nodes");
+ await expect(page.getByRole("button", { name: "Reset deployment", exact: true })).toBeEnabled();
+ await setDeployment(request, { resources: { allocations: 1, pending: 0 } });
+ await page.getByRole("button", { name: "Refresh sandbox state", exact: true }).click();
+ const dialog = await openReset(page);
+ let accepted!: () => void;
+ const coreAccepted = new Promise((resolve) => { accepted = resolve; });
+ let release!: () => void;
+ const responseHeld = new Promise((resolve) => { release = resolve; });
+ let failReads = true;
+ await page.route(deploymentURL, (route) => failReads ? route.fulfill(unavailable) : route.continue());
+ await page.route(resetURL, async (route) => {
+ await route.fetch();
+ accepted();
+ await responseHeld;
+ // Navigation may already have aborted the original browser request.
+ await route.abort("failed").catch(() => {});
+ });
+ const responseLost = page.waitForEvent("requestfailed", {
+ predicate: (sent) => sent.method() === "POST" && sent.url().endsWith(resetPath),
+ });
+ try {
+ await dialog.getByRole("button", { name: "Reset deployment", exact: true }).click();
+ await coreAccepted;
+ const cachedAt = Date.now();
+ // A hash navigation keeps the application/query cache alive, unlike a reload.
+ await page.evaluate(() => { window.location.hash = "overview"; });
+ await expect(page.getByRole("heading", { name: "Overview", level: 1 })).toBeVisible();
+ await page.getByRole("button", { name: "Nodes", exact: true }).click();
+ const resetButton = page.getByRole("button", { name: "Reset deployment", exact: true });
+ await expect(resetButton).toBeDisabled();
+ await expect(page.getByRole("button", { name: "Add node", exact: true })).toBeDisabled();
+ expect(Date.now() - cachedAt).toBeLessThan(30_000);
+ expect(await writes(request)).toEqual([`POST ${resetPath}`]);
+
+ release();
+ await responseLost;
+ // Ownership survives routing, but a departed page's local error dialog must
+ // not be resurrected on the new Nodes instance when that request settles.
+ await expect(page.getByRole("dialog", { name: "Couldn't confirm the sandbox change" })).toHaveCount(0);
+ await page.getByRole("button", { name: "Overview", exact: true }).click();
+ await expect(page.getByRole("heading", { name: "Overview", level: 1 })).toBeVisible();
+ await page.getByRole("button", { name: "Nodes", exact: true }).click();
+ await expect(resetButton).toBeDisabled();
+ // An explicit refresh also fails; neither navigation nor failure replays POST.
+ await page.getByRole("button", { name: "Refresh sandbox state", exact: true }).click();
+ await expect(resetButton).toBeDisabled();
+ expect(await writes(request)).toEqual([`POST ${resetPath}`]);
+
+ failReads = false;
+ await page.getByRole("button", { name: "Refresh sandbox state", exact: true }).click();
+ await expect(progress(page)).toContainText("Busy work can finish until the deadline.");
+ await expect(progress(page).getByRole("button", { name: "Cancel reset" })).toBeEnabled();
+ expect(await writes(request)).toEqual([`POST ${resetPath}`]);
+ } finally { release(); }
+});
+
+test("Nodes adopts the reset and completion learned on Overview before its cached node data expires", async ({ page, request }) => {
+ await openConsole(page, request, "nodes");
+ await expect(page.getByRole("button", { name: "Reset deployment", exact: true })).toBeEnabled();
+ await expect(page.getByRole("button", { name: "Add node", exact: true })).toBeEnabled();
+ const cachedAt = Date.now();
+ await page.getByRole("button", { name: "Overview", exact: true }).click();
+ await setDeployment(request, { resources: { allocations: 3, pending: 1 }, reset: observedReset() });
+ await page.getByRole("button", { name: "Refresh", exact: true }).click();
+ await expect(page.getByRole("heading", { name: "Sandbox reset in progress" })).toBeVisible();
+ await page.getByRole("button", { name: "Nodes", exact: true }).click();
+ await expect(progress(page)).toContainText("offline-build-worker");
+ await expect(page.getByRole("button", { name: "Add node", exact: true })).toBeDisabled();
+ await expect(page.getByRole("button", { name: "Change resources", exact: true })).toHaveCount(0);
+ expect(Date.now() - cachedAt).toBeLessThan(30_000);
+
+ // No refresh click: the shared active reset starts bounded read-only polling on Nodes.
+ const forced = observedReset("force");
+ forced.remaining = { busy: 0, idle: 0, cleanup: 1, on_offline_nodes: 1, offline_nodes: [{ node_id: "offline-owned", name: "offline-build-worker", resources: 1 }] };
+ await setDeployment(request, { resources: { allocations: 1, pending: 0 }, reset: forced });
+ await expect(progress(page)).toContainText("Remaining hosted Sessions are being archived", { timeout: 12_000 });
+ await expect(progress(page).getByRole("button", { name: "Force reset now" })).toHaveCount(0);
+
+ const completedAt = Date.now();
+ await page.getByRole("button", { name: "Overview", exact: true }).click();
+ await setDeployment(request, { complete_reset: true });
+ await page.getByRole("button", { name: "Refresh", exact: true }).click();
+ await expect(page.getByRole("heading", { name: "Sandbox reset in progress" })).toHaveCount(0);
+ await page.getByRole("button", { name: "Nodes", exact: true }).click();
+ await expect(page.getByRole("heading", { name: "Where should sandboxes run?" })).toBeVisible();
+ await expect(progress(page)).toHaveCount(0);
+ expect(Date.now() - completedAt).toBeLessThan(30_000);
+ expect(await writes(request)).toEqual([]);
+});
+
+
+test("signing out clears pending reset ownership and a late response cannot overwrite the new login", async ({ page, request }) => {
+ await openConsole(page, request, "nodes");
+ await setDeployment(request, { resources: { allocations: 1, pending: 0 } });
+ await page.getByRole("button", { name: "Refresh sandbox state", exact: true }).click();
+ const dialog = await openReset(page);
+ let accepted!: () => void;
+ const coreAccepted = new Promise((resolve) => { accepted = resolve; });
+ let release!: () => void;
+ const responseHeld = new Promise((resolve) => { release = resolve; });
+ await page.route(resetURL, async (route) => {
+ const response = await route.fetch();
+ accepted();
+ await responseHeld;
+ await route.fulfill({ response });
+ });
+ try {
+ await dialog.getByRole("button", { name: "Reset deployment", exact: true }).click();
+ await coreAccepted;
+ await page.evaluate(() => { window.location.hash = "overview"; });
+ await expect(page.getByRole("heading", { name: "Overview", level: 1 })).toBeVisible();
+ await page.getByRole("button", { name: "Sign out", exact: true }).click();
+ await expect(page.getByRole("heading", { name: "Sign in to OpenAgentCore" })).toBeVisible();
+ // Core completes cleanup while signed out. The new login must read generation 2.
+ await setDeployment(request, { complete_reset: true });
+ await page.getByLabel("Core key", { exact: true }).fill(FIXTURE_CORE_KEY);
+ await page.getByRole("button", { name: "Sign in", exact: true }).click();
+ await expect(page.getByRole("heading", { name: "Overview", level: 1 })).toBeVisible();
+ await page.getByRole("button", { name: "Nodes", exact: true }).click();
+ await page.getByRole("button", { name: "Own machines" }).click();
+ await page.getByRole("button", { name: "microsandbox Recommended" }).click();
+ await page.getByRole("button", { name: /^Standard/ }).click();
+ const configured = page.waitForRequest((sent) => sent.method() === "POST" && sent.url().endsWith("/sandbox/deployment"));
+ await page.getByRole("button", { name: "Save configuration", exact: true }).click();
+ expect((await configured).postDataJSON()).toMatchObject({ expected_generation: 2, provider: "microsandbox" });
+ const add = page.getByRole("dialog", { name: "Add node" });
+ await expect(add).toBeVisible();
+ await add.getByRole("button", { name: "Close dialog" }).click();
+
+ const lateResponse = page.waitForResponse((received) => received.request().method() === "POST" && received.url().endsWith(resetPath));
+ release();
+ await (await lateResponse).finished();
+ // The old generation-1 reset response cannot restore reset progress, block
+ // this login, or show the old route's result/error dialog.
+ const newDialog = await openReset(page);
+ await expect(newDialog).toBeVisible();
+ await newDialog.getByRole("button", { name: "Back", exact: true }).click();
+ await expect(progress(page)).toHaveCount(0);
+ await expect(page.getByRole("button", { name: "Add node", exact: true })).toBeEnabled();
+ expect(await writes(request)).toEqual([`POST ${resetPath}`, "POST /core/v1/sandbox/deployment"]);
+ } finally { release(); }
+});
diff --git a/apps/web/src/features/first-run/ConsoleAccess.tsx b/apps/web/src/features/first-run/ConsoleAccess.tsx
index daa583713..f70521b1d 100644
--- a/apps/web/src/features/first-run/ConsoleAccess.tsx
+++ b/apps/web/src/features/first-run/ConsoleAccess.tsx
@@ -6,6 +6,7 @@ import { HelpTip } from "../../components/console-ui";
import { ThemeMenu } from "../../components/ThemeMenu";
import { useToast } from "../../components/Toast";
import { setLanguage } from "../../i18n";
+import { queryClient } from "../../lib/queries";
import { OnboardingLayout } from "../onboarding/OnboardingLayout";
import { withTransition } from "../onboarding/view-transition";
import { changeConsoleAuth, ConsoleAuthError, readConsoleAuth, type ConsoleAuth } from "./auth";
@@ -49,16 +50,24 @@ export function ConsoleAccess({ children }: { children: ReactNode }) {
const [failed, setFailed] = useState(false);
const [revision, setRevision] = useState(0);
const generation = useRef(0);
+ const authMode = useRef(null);
+ const acceptStatus = useCallback((next: ConsoleAuth, newSession = false) => {
+ // clear() cancels query owners synchronously. Late mutations must retain
+ // their own ownership check before publishing into the new session.
+ if (newSession || (authMode.current === "authenticated" && next.mode === "login")) queryClient.clear();
+ authMode.current = next.mode;
+ setStatus(next);
+ }, []);
const refresh = useCallback(() => setRevision((current) => current + 1), []);
useEffect(() => {
const controller = new AbortController();
const current = ++generation.current;
setFailed(false);
void readConsoleAuth(controller.signal).then((value) => {
- if (generation.current === current) setStatus(value);
+ if (generation.current === current) acceptStatus(value);
}).catch(() => { if (!controller.signal.aborted && generation.current === current) setFailed(true); });
return () => { controller.abort(); generation.current++; };
- }, [revision]);
+ }, [revision, acceptStatus]);
useEffect(() => {
if (status?.mode !== "authenticated") return;
const check = () => { if (document.visibilityState === "visible") refresh(); };
@@ -69,15 +78,16 @@ export function ConsoleAccess({ children }: { children: ReactNode }) {
if (status?.mode === "authenticated") return {
const next = await changeConsoleAuth({ action: "logout" });
generation.current++;
- setStatus(next);
+ acceptStatus(next, true);
} }}>{children};
return >}>
{status && !failed ? {
+ queryClient.clear();
// The console opens on the Overview, revealed from the pressed button.
withTransition("enter", () => {
generation.current++;
- setStatus(next);
+ acceptStatus(next);
}, from);
}} /> :
{t(failed ? "Could not connect to your console." : "Connecting to your console…")}
diff --git a/apps/web/src/features/fleet/SandboxResetNotice.test.tsx b/apps/web/src/features/fleet/SandboxResetNotice.test.tsx
new file mode 100644
index 000000000..eeefdd1df
--- /dev/null
+++ b/apps/web/src/features/fleet/SandboxResetNotice.test.tsx
@@ -0,0 +1,33 @@
+import type { SandboxDeployment, SandboxReset } from "@agents-core-web/agents-client";
+import { renderToStaticMarkup } from "react-dom/server";
+import { describe, expect, it } from "vitest";
+
+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 => ({ installation_id: "i", provider: "docker", core_url: "http://core", reset, owner_epoch: 1, generation: 1, mode: "nodes", resources: { allocations: 3, pending: 0 }, suspension: null });
+const render = (value: SandboxDeployment | undefined, failed = false) => renderToStaticMarkup( {}} />);
+
+describe("reset notices on read-only surfaces", () => {
+ it("shows active reset consequences and its existing management destination", () => {
+ const html = render(deployment(reset));
+ expect(html).toContain("Sandbox reset in progress");
+ expect(html).toContain("Waiting for Sessions and cleanup");
+ expect(html).toContain("Self-hosted Sessions are unaffected");
+ expect(html).toContain("View reset");
+ });
+
+ it("shows force once Core reports the mode transition", () => {
+ expect(render(deployment({ ...reset, clear: "force", forced_at: "2026-09-27T11:00:00Z" }))).toContain("Forced reset");
+ });
+
+ it("retains and qualifies the last reset on failure, then hides only after Core returns null", () => {
+ const html = render(deployment(reset), true);
+ expect(html).toContain("Sandbox reset in progress");
+ expect(html).toContain("Retry");
+ expect(render(deployment(null))).toBe("");
+ expect(render(undefined)).toBe("");
+ expect(render(undefined, true)).toContain("Retry");
+ });
+});
diff --git a/apps/web/src/features/fleet/SandboxResetNotice.tsx b/apps/web/src/features/fleet/SandboxResetNotice.tsx
new file mode 100644
index 000000000..75e760da7
--- /dev/null
+++ b/apps/web/src/features/fleet/SandboxResetNotice.tsx
@@ -0,0 +1,23 @@
+import type { SandboxDeployment } from "@agents-core-web/agents-client";
+import { useTranslation } from "react-i18next";
+
+import { Section, StatusDot } from "../../components/console-ui";
+import { ReadFailure } from "../../components/ReadFailure";
+import { useConsoleNavigation } from "../../lib/console-navigation";
+
+/** A reset is a deployment operation, separate from Core health and node reachability. */
+export function SandboxResetNotice({ deployment, failed, onRetry }: {
+ deployment: SandboxDeployment | undefined;
+ failed: boolean;
+ onRetry: () => void;
+}) {
+ const { t } = useTranslation("overview");
+ const { navigate } = useConsoleNavigation();
+ return <>
+ {failed ? : null}
+ {deployment?.reset ? navigate("nodes")}>{t("reset.view")}}>
+
+
{t("reset.body")}
+ : null}
+ >;
+}
diff --git a/apps/web/src/features/fleet/fleet-model.test.ts b/apps/web/src/features/fleet/fleet-model.test.ts
index dcf634fa3..97b4fe71f 100644
--- a/apps/web/src/features/fleet/fleet-model.test.ts
+++ b/apps/web/src/features/fleet/fleet-model.test.ts
@@ -19,10 +19,9 @@ describe("fleet model", () => {
expect(nodeHealth(node("a", { online: false }))).toBe("offline");
});
- it("derives Core's own status from the Web API and maintenance", () => {
- expect(coreStatus({ webApiReachable: null, maintenance: null })).toBe("checking");
- expect(coreStatus({ webApiReachable: false, maintenance: false })).toBe("unreachable");
- expect(coreStatus({ webApiReachable: true, maintenance: null })).toBe("running");
- expect(coreStatus({ webApiReachable: true, maintenance: true })).toBe("maintenance");
+ it("reports Core reachability independently of sandbox operations", () => {
+ expect(coreStatus({ webApiReachable: null })).toBe("checking");
+ expect(coreStatus({ webApiReachable: false })).toBe("unreachable");
+ expect(coreStatus({ webApiReachable: true })).toBe("running");
});
});
diff --git a/apps/web/src/features/fleet/fleet-model.ts b/apps/web/src/features/fleet/fleet-model.ts
index 375589410..c7007ff35 100644
--- a/apps/web/src/features/fleet/fleet-model.ts
+++ b/apps/web/src/features/fleet/fleet-model.ts
@@ -55,14 +55,13 @@ export function capacitySummary(nodes: readonly SandboxNode[]): CapacitySummary
}
/**
- * Core itself, as far as the console can tell: whether the Web API answers
- * and whether the sandbox deployment is in maintenance. Core reports no CPU or
- * memory figures of its own yet.
+ * Core reachability is independent of a sandbox reset. Reset progress comes
+ * from the deployment read and does not imply a Core health failure.
*/
-export type CoreStatus = "checking" | "running" | "maintenance" | "unreachable";
+export type CoreStatus = "checking" | "running" | "unreachable";
-export function coreStatus(input: { webApiReachable: boolean | null; maintenance: boolean | null }): CoreStatus {
+export function coreStatus(input: { webApiReachable: boolean | null }): CoreStatus {
if (input.webApiReachable === false) return "unreachable";
if (input.webApiReachable === null) return "checking";
- return input.maintenance ? "maintenance" : "running";
+ return "running";
}
diff --git a/apps/web/src/features/fleet/fleet-queries.test.ts b/apps/web/src/features/fleet/fleet-queries.test.ts
new file mode 100644
index 000000000..caa9f236d
--- /dev/null
+++ b/apps/web/src/features/fleet/fleet-queries.test.ts
@@ -0,0 +1,50 @@
+import type { SandboxDeployment } from "@agents-core-web/agents-client";
+import { QueryClient } from "@tanstack/react-query";
+import { afterEach, describe, expect, it, vi } from "vitest";
+
+import { sandboxAdmin, sandboxDeploymentQuery, sandboxScope } from "../sandbox/sandbox-queries";
+import { fleetQuery } from "./fleet-queries";
+
+const deployment: SandboxDeployment = { installation_id: "i", provider: "docker", core_url: "http://core", owner_epoch: 1, generation: 1, mode: "nodes", resources: { allocations: 1, pending: 0 }, 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(); });
+
+describe("fleet deployment evidence", () => {
+ it("retains a successful reset read in the shared cache when node reads fail", async () => {
+ const cache = new QueryClient({ defaultOptions: { queries: { retry: false } } });
+ vi.spyOn(sandboxAdmin, "retrieveDeployment").mockResolvedValue(deployment);
+ vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("node read failed")));
+ await expect(cache.fetchQuery(fleetQuery(false))).rejects.toThrow();
+ expect(cache.getQueryData(sandboxDeploymentQuery.queryKey)).toEqual(deployment);
+ cache.clear();
+ });
+
+ it("does not clear the previous reset when its next deployment read fails", async () => {
+ const cache = new QueryClient({ defaultOptions: { queries: { retry: false } } });
+ cache.setQueryData(sandboxDeploymentQuery.queryKey, deployment);
+ vi.spyOn(sandboxAdmin, "retrieveDeployment").mockRejectedValue(new Error("deployment read failed"));
+ vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response(JSON.stringify({ object: "list", data: [] }), { status: 200 })));
+ await expect(cache.fetchQuery(fleetQuery(false))).rejects.toThrow();
+ expect(cache.getQueryData(sandboxDeploymentQuery.queryKey)).toEqual(deployment);
+ expect(cache.getQueryState(sandboxDeploymentQuery.queryKey)?.status).toBe("error");
+ cache.clear();
+ });
+
+ it("cannot publish an older reset read after a sandbox write cancels the shared query", async () => {
+ const cache = new QueryClient({ defaultOptions: { queries: { retry: false } } });
+ let resolveRead!: (value: SandboxDeployment) => void;
+ vi.spyOn(sandboxAdmin, "retrieveDeployment").mockImplementation(() => new Promise((resolve) => { resolveRead = resolve; }));
+ vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response(JSON.stringify({ object: "list", data: [] }), { status: 200 })));
+ const fleetRead = cache.fetchQuery(fleetQuery(false));
+ const rejected = expect(fleetRead).rejects.toThrow();
+ await cache.cancelQueries({ queryKey: sandboxScope });
+ const confirmed = { ...deployment, reset: null };
+ cache.setQueryData(sandboxDeploymentQuery.queryKey, confirmed);
+ resolveRead(deployment);
+ await rejected;
+ expect(cache.getQueryData(sandboxDeploymentQuery.queryKey)).toEqual(confirmed);
+ cache.clear();
+ });
+
+});
diff --git a/apps/web/src/features/fleet/fleet-queries.ts b/apps/web/src/features/fleet/fleet-queries.ts
index f0caaa4d0..275039af0 100644
--- a/apps/web/src/features/fleet/fleet-queries.ts
+++ b/apps/web/src/features/fleet/fleet-queries.ts
@@ -1,6 +1,7 @@
-import { queryOptions } from "@tanstack/react-query";
+import { queryOptions, type QueryClient } from "@tanstack/react-query";
import { SandboxAdminClient, type SandboxAllocation, type SandboxDeployment, type SandboxNode, type SandboxNodeHistoryRange } from "@agents-core-web/agents-client";
+import { sandboxDeploymentQuery } from "../sandbox/sandbox-queries";
import { sandboxConsoleConfig } from "../sandbox/console-config";
export interface FleetSnapshot {
@@ -23,10 +24,11 @@ export const consoleConfigQuery = queryOptions({
queryFn: ({ signal }) => sandboxConsoleConfig(signal),
});
-async function loadFleet(readAllocations: boolean, signal: AbortSignal): Promise {
+async function loadFleet(readAllocations: boolean, signal: AbortSignal, cache: QueryClient): Promise {
const sandbox = client();
const [deployment, nodes] = await Promise.all([
- sandbox.retrieveDeployment({ signal }),
+ // Keep reset truth in the shared deployment cache, even if a node read fails.
+ cache.fetchQuery({ ...sandboxDeploymentQuery, staleTime: 0 }),
sandbox.listNodes({ signal }),
]);
const allocations = readAllocations
@@ -44,7 +46,7 @@ async function loadFleet(readAllocations: boolean, signal: AbortSignal): Promise
export function fleetQuery(allocations: boolean) {
return queryOptions({
queryKey: ["sandbox-fleet", allocations ? "with-allocations" : "nodes"],
- queryFn: ({ signal }) => loadFleet(allocations, signal),
+ queryFn: ({ signal, client }) => loadFleet(allocations, signal, client),
});
}
diff --git a/apps/web/src/features/fleet/use-sandbox-fleet.test.tsx b/apps/web/src/features/fleet/use-sandbox-fleet.test.tsx
new file mode 100644
index 000000000..c15a6dff3
--- /dev/null
+++ b/apps/web/src/features/fleet/use-sandbox-fleet.test.tsx
@@ -0,0 +1,53 @@
+import type { SandboxDeployment } from "@agents-core-web/agents-client";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { renderToStaticMarkup } from "react-dom/server";
+import { describe, expect, it } from "vitest";
+
+import { gettingStartedSteps } from "../overview/getting-started";
+import { node } from "../overview/test-fixtures";
+import type { SandboxConsoleConfig } from "../sandbox/console-config";
+import { sandboxDeploymentQuery } from "../sandbox/sandbox-queries";
+import { consoleConfigQuery, fleetQuery, type FleetSnapshot } from "./fleet-queries";
+import { fleetSnapshot, useSandboxFleet } from "./use-sandbox-fleet";
+
+const configured: SandboxDeployment = { installation_id: "i", provider: "docker", core_url: "http://core", reset: null, owner_epoch: 1, generation: 1, mode: "nodes", resources: { allocations: 1, pending: 0 }, suspension: null };
+
+function Probe() {
+ const { state, deployment } = useSandboxFleet();
+ const snapshot = fleetSnapshot(state);
+ const sandboxes = gettingStartedSteps({ fleet: state, sandboxReset: deployment.data ? deployment.data.reset !== null : undefined, localOnly: false, projects: [], sessions: 0, harnesses: [] }).sandboxes;
+ return
{snapshot ? `${snapshot.nodes.length} nodes / ${snapshot.deployment.resources.allocations} allocations` : "No current fleet evidence"}
- {t("One provider serves this entire Core deployment. Switching requires maintenance and verified cleanup of all existing execution resources.")}
+ {t("One provider serves this deployment. Reset it before choosing a different backend.")}
{deployment.provider === "e2b" ? ` ${t("Core creates E2B sandboxes directly. No node enrollment is needed.")} ${t("Saved configuration does not confirm execution readiness. Session and Environment state report actual execution.")}` : ""}
- {t(clean ? "Core reports no remaining execution resources. You can choose the next provider." : "Provider changes are blocked until Core confirms complete cleanup.")}
- {t("Maintenance pauses new hosted placement. Clean up existing execution resources before changing provider; historical Sessions and results are preserved by the switch.")} {t("Stopped or offline resources, snapshots, uncertain creates and pending environments still block switching. Deleting a Session alone does not prove cleanup; Core must confirm both counts are zero.")}
-
- {!changing ? : <>
+ {!deployment.reset ? <>
+
+
+ {t("Editing keeps the current provider and requires zero allocated resources and pending environments. It is not an online upgrade.")}
+
+ {!clean ?
{t("Changes require Core to confirm that no hosted resources remain.")}
: null}
+ {changing ? <>
-
-
- {t(deployment.provider === "e2b" ? "Saving does not migrate Sessions or resume placement automatically." : "Saving retires old node identities and enrollment credentials. It does not migrate Sessions or resume placement automatically.")}
-
- >}
-
-
- {t("Resume is explicit and succeeds only when Core activates the saved configuration. A failed activation leaves maintenance enabled.")}
-
- >}
+
{t(deployment.provider === "e2b" ? "Saving changes only this provider's configuration; it does not confirm execution readiness." : "Saving retires existing node identities and enrollment commands. Add the nodes again after saving.")}
+
+ > : null}
+ > : null}
+ ;
}
diff --git a/apps/web/src/features/sandbox/SandboxManagerView.css b/apps/web/src/features/sandbox/SandboxManagerView.css
index 7d82b9362..3eeeb1eef 100644
--- a/apps/web/src/features/sandbox/SandboxManagerView.css
+++ b/apps/web/src/features/sandbox/SandboxManagerView.css
@@ -27,7 +27,7 @@
.sandbox-enrollment-status.connected { color: color-mix(in srgb, var(--success) 65%, var(--fg)); }
.sandbox-add-node .sandbox-next-step { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px; font-size: 13px; }
.sandbox-add-node .sandbox-command-expiry { font-size: 12px; }
-.sandbox-maintenance { padding: 12px 16px; border: 1px solid var(--line); border-radius: 8px; font-size: 13px; }
+.sandbox-notice { padding: 12px 16px; border: 1px solid var(--line); border-radius: 8px; font-size: 13px; }
@media (max-width: 600px) {
.sandbox-manager { padding: 22px 14px; }
.sandbox-enrollment { padding: 18px; }
@@ -99,3 +99,14 @@
background: color-mix(in srgb, var(--warning) 9%, var(--surface));
border-color: color-mix(in srgb, var(--warning) 32%, var(--line));
}
+
+/* Reset stays in the provider panel; only irreversible decisions interrupt with a dialog. */
+.sandbox-reset { border-top: 1px solid var(--line); padding-top: 16px; }
+.sandbox-reset h3 { margin: 0; font-size: 15px; font-weight: 600; }
+.sandbox-reset-offline { display: grid; gap: 6px; }
+.sandbox-reset-offline ul { margin: 0; padding-left: 20px; overflow-wrap: anywhere; }
+.sandbox-reset-options { display: grid; gap: 10px; border: 0; padding: 0; margin: 0; }
+.sandbox-reset-options legend { font-weight: 600; margin-bottom: 10px; }
+.sandbox-reset-options > label:not(.field) { display: flex; align-items: center; gap: 8px; }
+.sandbox-reset-options input[type="number"] { max-width: 180px; }
+.sandbox-reset-dialog p { line-height: 1.6; }
diff --git a/apps/web/src/features/sandbox/SandboxManagerView.tsx b/apps/web/src/features/sandbox/SandboxManagerView.tsx
index 91412e694..b6fcc0c7e 100644
--- a/apps/web/src/features/sandbox/SandboxManagerView.tsx
+++ b/apps/web/src/features/sandbox/SandboxManagerView.tsx
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useRef, useState, type ReactNode, type RefObject } from "react";
-import type { InitializeSandboxDeployment, SandboxDeployment, SandboxNode } from "@agents-core-web/agents-client";
+import { type InitializeSandboxDeployment, type SandboxDeployment, type SandboxNode, type StartSandboxReset } from "@agents-core-web/agents-client";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { ArrowLeft, Pencil, Plus, Server, Trash2 } from "lucide-react";
import { useTranslation } from "react-i18next";
@@ -13,7 +13,9 @@ import { InstallationNotice } from "../../components/InstallationNotice";
import { installationQuery } from "../../lib/installation";
import { sandboxConfigurationRejection, sandboxRequestError, sandboxWriteUncertain } from "../../lib/sandbox-labels";
import type { SandboxConsoleConfig } from "./console-config";
-import { sandboxAdmin, sandboxConsoleConfigQuery, sandboxDeploymentQuery, sandboxScope, sandboxSnapshotQuery, type SandboxSnapshot } from "./sandbox-queries";
+import { sandboxAdmin, sandboxConsoleConfigQuery } from "./sandbox-queries";
+import { useSandboxManagerState } from "./use-sandbox-manager-state";
+import { writeSandboxDeployment } from "./sandbox-deployment-write";
import { SandboxSetupWizard } from "./SandboxSetupWizard";
import { SandboxDeploymentSettings } from "./SandboxDeploymentSettings";
import { NodeEnrollment } from "./NodeEnrollment";
@@ -62,15 +64,14 @@ function SandboxManager({ consoleConfig }: { consoleConfig: SandboxConsoleConfig
const { params, navigate, back: goBack } = useConsoleNavigation();
const client = sandboxAdmin;
const queryClient = useQueryClient();
- const query = useQuery(sandboxSnapshotQuery);
+ const { ownership, deploymentQuery, query, deployment, compatible, inventoryLoading, snapshot } = useSandboxManagerState();
+ const { refetch: refetchSnapshot } = query;
const installation = useQuery(installationQuery);
const localOnly = installation.data?.local_only === true;
const { t: tCommon } = useTranslation("common");
- const snapshot: SandboxSnapshot | null = query.data ?? null;
- const loading = query.isFetching;
- // As before a reload clears the last error, a running read hides it.
- const error: unknown = query.isFetching ? null : query.error;
- const [busy, setBusy] = useState(false);
+ const loading = deploymentQuery.isFetching;
+ const error: unknown = deploymentQuery.error;
+ const busy = ownership.data.phase === "pending";
const [revision, setRevision] = useState(0);
const [removeTarget, setRemoveTarget] = useState(null);
const [editTarget, setEditTarget] = useState(null);
@@ -80,16 +81,20 @@ function SandboxManager({ consoleConfig }: { consoleConfig: SandboxConsoleConfig
const [removeError, setRemoveError] = useState(null);
// After a removal, the host's uninstall command; it stays for the closing animation.
const [cleanup, setCleanup] = useState<{ node: NodeCleanup; open: boolean } | null>(null);
- // When a deployment write last had an uncertain outcome (no response, a timeout or a 5xx); only a read begun after it confirms the state again.
- const [uncertainSince, setUncertainSince] = useState(null);
- const setupNeedsRefresh = uncertainSince !== null && !(snapshot && snapshot.readAt > uncertainSince);
- // The state on screen is Core's last successful read, with no uncertain write since.
- const confirmed = snapshot !== null && !query.isError && !setupNeedsRefresh;
+ // QueryClient owns this across route lifetimes. Only a successful authoritative
+ // read begun after settlement releases uncertain/pending mutation ownership.
+ const setupNeedsRefresh = ownership.data.phase === "reconcile";
+ const confirmed = deployment !== undefined && !deploymentQuery.isError && ownership.data.phase === "idle";
// Writes additionally wait for any read in flight.
const fresh = confirmed && !loading;
// A write with an uncertain outcome opens a dialog with the reason; the error stays for the closing animation.
const [writeFailure, setWriteFailure] = useState<{ error: unknown; open: boolean } | null>(null);
- const { refetch } = query;
+ const { refetch: refetchDeployment } = deploymentQuery;
+ const refetch = useCallback(async () => {
+ const result = await refetchDeployment();
+ if (!result.isError) await refetchSnapshot();
+ return result;
+ }, [refetchDeployment, refetchSnapshot]);
const refresh = useCallback(() => {
// Each refresh starts a new read (cancelling one in flight) and resets the forms, as a reload did.
setRevision((value) => value + 1);
@@ -121,36 +126,17 @@ function SandboxManager({ consoleConfig }: { consoleConfig: SandboxConsoleConfig
async function changeDeployment(operation: (signal: AbortSignal) => Promise, fromWizard = false): Promise {
const controller = lifetime.current;
if (!controller || busy || loading || setupNeedsRefresh || !fresh) return false;
- setBusy(true); setWriteFailure(null);
+ setWriteFailure(null);
try {
- const deployment = await operation(controller.signal);
- if (!controller.signal.aborted) {
- // Core's confirmed response replaces the deployment; nodes of an older generation no longer apply.
- queryClient.setQueryData(sandboxSnapshotQuery.queryKey, (current): SandboxSnapshot => {
- const same = current !== undefined && deployment.generation === current.deployment.generation;
- return { deployment, nodes: same ? current.nodes : [], allocations: same ? current.allocations : [], readAt: current?.readAt ?? performance.now() };
- });
- // A later visit reads the whole snapshot again; other pages re-read the deployment now.
- void queryClient.invalidateQueries({ queryKey: sandboxSnapshotQuery.queryKey, refetchType: "none" });
- void queryClient.invalidateQueries({ queryKey: sandboxDeploymentQuery.queryKey });
- // Overview and Sandbox metrics read the fleet separately and lay out by provider.
- void queryClient.invalidateQueries({ queryKey: ["sandbox-fleet"] });
- return true;
- }
+ const deployment = await writeSandboxDeployment(queryClient, operation);
+ return deployment !== null && !controller.signal.aborted;
} catch (error) {
if (!controller.signal.aborted) {
- // Core rejected the configuration and saved nothing: the wizard explains why.
if (fromWizard && sandboxConfigurationRejection(error) !== null) throw error;
- if (sandboxWriteUncertain(error)) {
- setUncertainSince(performance.now()); setWriteFailure({ error, open: true });
- // Nothing re-reads on its own: the operator refreshes to confirm. A later visit reads again.
- void queryClient.invalidateQueries({ queryKey: sandboxScope, refetchType: "none" });
- } else {
- // Core refused the change, so nothing changed: its reason, and the page stays usable as it was.
- toast.show(t("Core rejected the sandbox change"), { tone: "error", detail: sandboxRequestError(error, locale), key: "sandbox-write" });
- }
+ if (sandboxWriteUncertain(error)) setWriteFailure({ error, open: true });
+ else toast.show(t("Core rejected the sandbox change"), { tone: "error", detail: sandboxRequestError(error, locale), key: "sandbox-write" });
}
- } finally { if (!controller.signal.aborted) setBusy(false); }
+ }
return false;
}
/** First setup; own machines continue straight to adding the first node. */
@@ -160,9 +146,8 @@ function SandboxManager({ consoleConfig }: { consoleConfig: SandboxConsoleConfig
async function update(input: InitializeSandboxDeployment) {
await changeDeployment((signal) => client.updateDeployment({ ...input, expected_generation: snapshot!.deployment.generation }, { signal }), true);
}
- async function maintenance(maintenance: boolean) {
- await changeDeployment((signal) => client.setMaintenance({ maintenance, expected_generation: snapshot!.deployment.generation }, { signal }));
- }
+ const startReset = (input: StartSandboxReset) => changeDeployment((signal) => client.startReset(input, { signal }));
+ const cancelReset = (expectedGeneration: number) => changeDeployment((signal) => client.cancelReset(expectedGeneration, { signal }));
const writeDialog = setAdding(true));
// A node enrolled with another address than Core's current one (config.json's public_url) gets no new sandboxes until it is added again.
const staleNodes = hostedNodes && snapshot ? nodes.filter((node) => onOldAddress(node, snapshot.deployment.core_url)).map((node) => node.name || node.id) : [];
@@ -216,10 +202,13 @@ function SandboxManager({ consoleConfig }: { consoleConfig: SandboxConsoleConfig
const refreshButton = ;
const readFailure = error !== null ? sandboxRequestError(error, locale) : null;
// A failed refresh keeps the last state on screen and says so in a toast, once while the failure lasts.
- useFailureToast(snapshot && query.isError ? sandboxRequestError(query.error, locale) : null, t("Refresh failed; showing the last loaded state."), "sandbox-read");
+ useFailureToast(snapshot && deploymentQuery.isError ? sandboxRequestError(deploymentQuery.error, locale) : null, t("Refresh failed; showing the last loaded state."), "sandbox-read");
const status = <>
- {!snapshot && (loading || query.isPending) ?
{t("Maintenance is enabled. New sandbox placement is paused.")}
: null}
- {staleNodes.length ?
{staleNodes.length === 1
+ {staleNodes.length ?
{staleNodes.length === 1
? t("{{name}} is still bound to an old Core address. Remove it and add it again.", { name: staleNodes[0] })
: t("{{count}} nodes are still bound to an old Core address: {{names}}. Remove them and add them again.", { count: staleNodes.length, names: new Intl.ListFormat(i18n.resolvedLanguage, { type: "conjunction" }).format(staleNodes) })}
{t("Progress comes from Core. Offline resources still need confirmed cleanup, even in force mode.")}
+
{t(reset.clear === "auto" ? "Idle sandboxes are being archived. Busy work can finish until the deadline." : "Remaining hosted Sessions are being archived and their work cancelled.")}
+ {stale ?
{t("Reset progress could not be refreshed. These are the last confirmed counts.")}
{t("Stop the reset and allow new hosted Sessions again. Sessions already archived stay archived and cannot be resumed.")}
: <>
+
{t("Reset archives this deployment's hosted Sessions. Archived Sessions cannot be resumed. History, Files and Artifacts are kept; self-hosted Sessions are unchanged.")}
+
{t("Unsaved workspace contents may be lost.")}
+
{t("After cleanup, Core clears the provider and saved E2B credential, retires nodes and enrollment commands, and returns to setup.")}
+ {dialog?.action === "force" ?
{t("Force immediately cancels remaining work. Offline resources can still block cleanup.")}
: }
+ >}
+ {changed ?
{t("Sandbox state changed. Close this dialog and review the current state.")}