From 19279d9a459789ce40f3397b67201cf6938f57bb Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 7 Oct 2026 17:59:25 +0000 Subject: [PATCH 1/8] Start oac-sandbox-io in every hosted sandbox Provisioning mints the allocation's Serve credential with its device credential and stores only its digest, in the same transaction that records the allocation. sandbox.Bootstrap carries the Sandbox I/O service's input (Link URL, credential, allocation resource). Core validates the whole Bootstrap once with Bootstrap.Validate before Create; the Docker, E2B and microsandbox copies of that validation are gone. An invalid input creates nothing and releases the allocation as settled absent. Docker, E2B and microsandbox write the input to the private file /home/runtime/sandbox-io-bootstrap.json and start oac-sandbox-io beside the daemon as UID/GID 1000. The node wire carries the new field, so node.ProtocolVersion is 5; the E2B helper contract and the microsandbox helper's stdin input change with it. Every Runtime image ships /usr/local/bin/oac-sandbox-io, and the distribution verifies it. A public URL that gives no Link URL now rejects hosted selection and admission with ErrNoLink (409 sandbox_configuration_error). The opt-in Docker tests need a fixture image that contains oac-sandbox-io; a new one proves that a real Docker sandbox Serves its allocation through a test relay. --- .../agents-api/node-generation-protocol.md | 2 + contracts/agents-api/sandbox-deployment.md | 2 +- .../agents-api/zh/node-generation-protocol.md | 4 +- contracts/agents-api/zh/sandbox-deployment.md | 4 +- docs/configuration.md | 2 +- docs/sandbox-bootstrap.md | 2 +- docs/sandbox-provider.md | 10 +- docs/zh/configuration.md | 4 +- docs/zh/sandbox-bootstrap.md | 4 +- docs/zh/sandbox-provider.md | 12 +- scripts/build-claude-runtime.sh | 2 +- scripts/build-codex-runtime.sh | 2 +- scripts/build-core-distribution.sh | 4 +- scripts/build-mcode-runtime.sh | 2 +- scripts/core-distribution-manifest.py | 4 +- services/core/cmd/server/managed_nodes.go | 6 +- services/core/cmd/server/managed_setup.go | 11 +- services/core/deploy/claude/Dockerfile | 2 +- services/core/deploy/codex/Dockerfile | 2 +- .../deploy/e2b/helper_contract_generated.py | 2 +- services/core/deploy/e2b/managed_init.py | 25 ++- services/core/deploy/e2b/managed_init_test.py | 27 ++- services/core/deploy/mcode/Dockerfile | 2 +- .../db/queries/runtime_allocations.sql | 4 +- .../db/sqlc/runtime_allocations.sql.go | 6 +- .../core/internal/deployment/allocation.go | 3 + .../core/internal/deployment/allocations.go | 15 +- .../internal/deployment/allocations_test.go | 25 +-- .../archive_cancellation_cleanup_test.go | 2 +- .../internal/execution/runtime_lifecycle.go | 29 ++- .../sandbox_deployment_drain_test.go | 4 +- .../sandbox_deployment_setup_test.go | 8 +- .../sandbox_deployment_switch_test.go | 10 +- .../execution/sandbox_generations_test.go | 4 +- .../sandbox_provider_contract_test.go | 2 +- .../internal/execution/sandbox_reset_test.go | 4 +- .../execution/sandbox_snapshot_budget_test.go | 2 +- .../postgres/deploymentpg/allocations.go | 1 + .../postgres/deploymentpg/allocations_test.go | 14 +- .../internal/sandbox/contracttest/provider.go | 18 +- .../core/internal/sandbox/docker/bootstrap.go | 8 +- .../internal/sandbox/docker/bootstrap_test.go | 36 ++-- .../internal/sandbox/docker/contract_test.go | 2 +- .../core/internal/sandbox/docker/provider.go | 11 +- .../internal/sandbox/docker/provider_test.go | 27 +-- .../internal/sandbox/docker/recovery_test.go | 3 +- .../internal/sandbox/docker/serve_test.go | 165 ++++++++++++++++++ .../internal/sandbox/e2b/contract_test.go | 4 +- .../sandbox/e2b/internal/contractgen/main.go | 5 +- .../core/internal/sandbox/e2b/provider.go | 13 +- .../internal/sandbox/e2b/provider_test.go | 7 +- .../sandbox/microsandbox/contract_test.go | 5 +- .../internal/sandbox/microsandbox/identity.go | 9 +- .../internal/sandbox/node/docker_live_test.go | 4 +- services/core/internal/sandbox/node/wire.go | 2 +- .../core/internal/sandbox/sandbox_provider.go | 24 +++ .../integration/admin_session_archive_test.go | 6 +- .../admin_session_archive_worker_http_test.go | 2 +- .../integration/archive_cancellation_test.go | 2 +- .../credential_matrix_http_test.go | 4 +- .../device_bootstrap_binding_test.go | 4 +- .../tests/integration/link_authority_test.go | 11 +- .../tests/integration/root_fixture_test.go | 4 +- .../integration/runtime_adoption_test.go | 2 +- .../integration/runtime_allocations_test.go | 16 +- .../runtime_compute_lifecycle_test.go | 2 +- .../integration/runtime_connection_test.go | 10 +- .../integration/runtime_deployment_test.go | 16 +- .../runtime_deployment_worker_test.go | 2 +- .../runtime_environment_terminal_test.go | 4 +- .../integration/runtime_idle_clock_test.go | 2 +- .../runtime_lifecycle_nodes_test.go | 4 +- .../integration/runtime_lifecycle_test.go | 5 +- .../runtime_node_generations_test.go | 2 +- .../runtime_node_lifecycle_fixture_test.go | 4 +- .../tests/integration/runtime_nodes_test.go | 6 +- .../integration/runtime_observation_test.go | 2 +- .../tests/integration/runtime_pending_test.go | 2 +- .../integration/runtime_suspension_test.go | 6 +- .../runtime_wake_hint_integration_test.go | 2 +- .../sandbox_deployment_resources_test.go | 2 +- .../sandbox_deployment_switch_test.go | 8 +- .../sandbox_deployment_switch_worker_test.go | 6 +- .../sandbox_deployment_worker_test.go | 6 +- .../tests/integration/sandbox_reset_test.go | 2 +- .../sandbox_specification_lifecycle_test.go | 10 +- .../integration/session_diagnostics_test.go | 2 +- .../session_execution_configuration_test.go | 2 +- services/core/tools/e2b-provider/README.md | 2 +- .../e2b-provider/helper_contract_generated.py | 2 +- .../core/tools/e2b-provider/provider_test.py | 9 +- .../tools/e2b-provider/testdata/contract.json | 107 ++++++------ .../tools/microsandbox-provider/README.md | 2 +- .../tools/microsandbox-provider/bootstrap.go | 30 +++- .../core/tools/microsandbox-provider/go.mod | 3 + .../core/tools/microsandbox-provider/go.sum | 56 ++++++ 96 files changed, 671 insertions(+), 311 deletions(-) create mode 100644 services/core/internal/sandbox/docker/serve_test.go diff --git a/contracts/agents-api/node-generation-protocol.md b/contracts/agents-api/node-generation-protocol.md index a7b293f41..e9e390731 100644 --- a/contracts/agents-api/node-generation-protocol.md +++ b/contracts/agents-api/node-generation-protocol.md @@ -53,6 +53,8 @@ Each operation carries its own arguments and returns the following result on suc | `resume` | `Resume` | `resume` | `state` | | `delete_snapshot` | `DeleteSnapshot` | `snapshot` | None | +`bootstrap` is the Provider's `sandbox.Bootstrap`, including the [Sandbox bootstrap](../../docs/sandbox-bootstrap.md) input in `SandboxIO`; Core validates it before it sends `create`. + A request whose `connection_id`, `owner_epoch` or `sequence` does not match closes the connection. A malformed request gets an `invalid` response. A node without generation management accepts only its enrolled `deployment_generation`; a generation-managing node runs the request on that generation's provider and answers `unconfirmed` when it cannot. Core sends `create` and a `resume` that is not observe-only only to a generation that is ready on that node, and keeps at most 32 requests pending per connection. The budget is relative: the node anchors `timeout_ms` to its own clock on receipt and consumes it while the request waits in its queue, so the hosts' clocks need not agree. Core still bounds its own wait. A full node queue closes the connection. diff --git a/contracts/agents-api/sandbox-deployment.md b/contracts/agents-api/sandbox-deployment.md index 94e62f090..dbee59e5c 100644 --- a/contracts/agents-api/sandbox-deployment.md +++ b/contracts/agents-api/sandbox-deployment.md @@ -39,7 +39,7 @@ POST and PUT take the same complete selection and require `expected_generation` | `configuration` | The provider's public selectors. E2B: the immutable `template` build and the optional paired `api_url` and `domain`. Docker and microsandbox accept only `{}` or omission | | `credential` | The provider's write-only credential. E2B: `{api_key}`, required at first setup and omitted on PUT to keep the current key; a null or empty key is invalid. Docker and microsandbox reject it | -The request has no Core address. Core derives the deployment's `core_url` from the installation public URL (`public_url` in `config.json`, `OAC_PUBLIC_URL` for Core): the origin nodes and sandbox guests use to reach Core. A request that contains `core_url` is rejected with 400 `invalid_request` like any other unknown member. E2B guests reach Core from E2B's cloud, so an E2B selection is rejected with 409 `sandbox_configuration_error` while the public URL is loopback. Docker and microsandbox selections accept a loopback public URL, which serves only local development because a guest's loopback address does not reach its host. Changing the public URL is an installation change: nodes enrolled with the old address receive no new sandboxes and must be removed and added again. +The request has no Core address. Core derives the deployment's `core_url` from the installation public URL (`public_url` in `config.json`, `OAC_PUBLIC_URL` for Core): the origin nodes and sandbox guests use to reach Core. A request that contains `core_url` is rejected with 400 `invalid_request` like any other unknown member. E2B guests reach Core from E2B's cloud, so an E2B selection is rejected with 409 `sandbox_configuration_error` while the public URL is loopback. Every hosted sandbox dials the [sandbox Link](../../docs/configuration.md#changing-the-public-url), so any selection is rejected the same way while the public URL is http on a host that is not loopback. Docker and microsandbox selections accept a loopback public URL, which serves only local development because a guest's loopback address does not reach its host. Changing the public URL is an installation change: nodes enrolled with the old address receive no new sandboxes and must be removed and added again. ### Resources diff --git a/contracts/agents-api/zh/node-generation-protocol.md b/contracts/agents-api/zh/node-generation-protocol.md index e6e5de8fc..6aeb0802e 100644 --- a/contracts/agents-api/zh/node-generation-protocol.md +++ b/contracts/agents-api/zh/node-generation-protocol.md @@ -1,7 +1,7 @@ --- title: "沙箱节点协议" source: contracts/agents-api/node-generation-protocol.md -source_hash: 1ee43dfcdd0eec0806ea3bc8a4c1227e10bd8ac5e69486505cb113a98e3f548a +source_hash: 1562f69c8c7a5937a10b98c8b7dea2518bfbf1f875c2bd67ff0461467ed72cb9 --- 沙箱节点在其主机上运行 Docker 或 microsandbox Provider,并通过一个 WebSocket 与 Core 相连。Core 通过该连接发送 Provider 操作;节点针对本地 Provider 执行这些操作,并报告就绪状态、主机测量值及其持有的部署代次。Core 始终是唯一的生命周期所有者:节点绝不重试变更操作或调度工作。帧和校验器位于 [`services/core/internal/sandbox/node`](https://github.com/MiniMax-AI/OpenAgentCore/tree/main/services/core/internal/sandbox/node)(`wire.go`、`generation_wire.go`);节点用于注册和读取配置的 HTTP 路由位于[机器连接 API](machine-api.md#node-routes)。 @@ -55,6 +55,8 @@ Core 发送包含以下内容的 `request` 帧: | `resume` | `Resume` | `resume` | `state` | | `delete_snapshot` | `DeleteSnapshot` | `snapshot` | 无 | +`bootstrap` 是 Provider 的 `sandbox.Bootstrap`,其 `SandboxIO` 包含[沙箱引导](../../../docs/zh/sandbox-bootstrap.md)输入;Core 在发送 `create` 前完成校验。 + 只要 `connection_id`、`owner_epoch` 或 `sequence` 中任一值不匹配,请求就会关闭连接。格式错误的请求会得到 `invalid` 响应。未启用代次管理的节点仅接受其登记的 `deployment_generation`;支持代次管理的节点在对应代次的 Provider 上运行请求,无法运行时回复 `unconfirmed`。Core 仅向节点上已就绪的代次发送 `create` 和非 observe-only 的 `resume`,并且每条连接最多保留 32 个待处理请求。 预算采用相对计时:节点收到请求时以自己的时钟为基准锚定 `timeout_ms`,并在请求排队等待期间持续消耗该预算,因此各主机的时钟无需保持一致。Core 仍会限制自身等待时长。节点队列已满时会关闭连接。 diff --git a/contracts/agents-api/zh/sandbox-deployment.md b/contracts/agents-api/zh/sandbox-deployment.md index cf026eb4b..7b5620408 100644 --- a/contracts/agents-api/zh/sandbox-deployment.md +++ b/contracts/agents-api/zh/sandbox-deployment.md @@ -1,7 +1,7 @@ --- title: "沙箱部署" source: contracts/agents-api/sandbox-deployment.md -source_hash: 6f765be45518f23ace6384938616eb12aba7554e7f8fbfadb89738f26c692dd5 +source_hash: 4e75b037063e5d83f9c0528b5dae4e0284c7c3e2ab9c88a699bd334f2e388576 --- 沙箱部署为 Core 管理的 `openai_hosted` 执行选择 Sandbox Provider、每个沙箱的资源以及不可变的 Runtime 发行版。PostgreSQL 为每个安装维护一个当前有效选择;Web 和 Core API 写入同一配置。节点文件保存其已安装副本和特定于主机的路径,且不能覆盖其资源或 Runtime。该选择独立于 Harness;部署可以保持未配置状态,既无节点,也不接受托管准入。 @@ -41,7 +41,7 @@ POST 和 PUT 接受相同的完整选择,并要求提供先前 GET 返回的 ` | `configuration` | 提供商的公开选择器。E2B:不可变的 `template` 构建以及可选且配套的 `api_url` 和 `domain`。Docker 和 microsandbox 仅接受 `{}` 或省略 | | `credential` | 提供商的只写凭据。E2B:`{api_key}`,首次设置时必填,在 PUT 中省略以保留当前密钥;null 或空密钥无效。Docker 和 microsandbox 拒绝该字段 | -请求中没有 Core 地址。Core 根据安装公开 URL(`config.json` 中的 `public_url`,Core 对应 `OAC_PUBLIC_URL`)派生部署的 `core_url`:这是节点和沙箱客户机访问 Core 时使用的源地址。包含 `core_url` 的请求会像包含任何其他未知成员一样被拒绝,并返回 400 `invalid_request`。E2B 客户机从 E2B 云访问 Core,因此当公开 URL 为回环地址时,E2B 选择会被拒绝,并返回 409 `sandbox_configuration_error`。Docker 和 microsandbox 选择接受回环公开 URL,但这仅适用于本地开发,因为客户机的回环地址无法访问其主机。更改公开 URL 属于安装变更:使用旧地址注册的节点不会收到新沙箱,必须移除后重新添加。 +请求中没有 Core 地址。Core 根据安装公开 URL(`config.json` 中的 `public_url`,Core 对应 `OAC_PUBLIC_URL`)派生部署的 `core_url`:这是节点和沙箱客户机访问 Core 时使用的源地址。包含 `core_url` 的请求会像包含任何其他未知成员一样被拒绝,并返回 400 `invalid_request`。E2B 客户机从 E2B 云访问 Core,因此当公开 URL 为回环地址时,E2B 选择会被拒绝,并返回 409 `sandbox_configuration_error`。每个托管沙箱都要连接[沙箱 Link](../../../docs/zh/configuration.md#changing-the-public-url),因此当公开 URL 是非回环主机上的 http 地址时,任何选择都会以同样方式被拒绝。Docker 和 microsandbox 选择接受回环公开 URL,但这仅适用于本地开发,因为客户机的回环地址无法访问其主机。更改公开 URL 属于安装变更:使用旧地址注册的节点不会收到新沙箱,必须移除后重新添加。 ### 资源 {#resources} diff --git a/docs/configuration.md b/docs/configuration.md index 54ffaa1d8..2e67248b0 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -31,7 +31,7 @@ Use `docker compose ps` to check the services. See [stop and restart](./getting- `OAC_PUBLIC_URL` is the one origin that applications, nodes, sandboxes and self-hosted executors use. Core derives the daemon WebSocket URL, the sandbox Link URL, the self-hosted `remote_url` and each sandbox's connection address from it. It is an http or https origin: the address browsers and nodes use. The installation serves Web over HTTP on `OAC_WEB_PORT`; a reverse proxy or hosting platform terminates HTTPS when you put one in front. -Sandboxes and agent hosts dial the [sandbox Link](./sandbox-link-protocol.md) at `wss:///api/v1/sandbox-link` when the origin is https. An http origin on `localhost` or a loopback address gives `ws:///api/v1/sandbox-link`, which only peers in Core's own network namespace can reach. An http origin on any other host gives no Link URL: nothing can use the Link until the origin is https. +Sandboxes and agent hosts dial the [sandbox Link](./sandbox-link-protocol.md) at `wss:///api/v1/sandbox-link` when the origin is https. An http origin on `localhost` or a loopback address gives `ws:///api/v1/sandbox-link`, which only peers in Core's own network namespace can reach. An http origin on any other host gives no Link URL: until the origin is https, nothing can use the Link and Core selects and admits no hosted sandbox. To change it, point the reverse proxy at the new address first, then edit `OAC_PUBLIC_URL` and run `oac apply`. Afterwards: diff --git a/docs/sandbox-bootstrap.md b/docs/sandbox-bootstrap.md index a1f93ab52..27a11c0a3 100644 --- a/docs/sandbox-bootstrap.md +++ b/docs/sandbox-bootstrap.md @@ -9,7 +9,7 @@ A Sandbox Provider starts the Sandbox I/O service by handing it one bootstrap fi Deliver one JSON object in a regular file that only the service's account and trusted provisioning processes can read (mode 0600 on Linux), and pass its absolute path: ```sh -oac-sandbox-io --bootstrap-file /home/sandbox/sandbox-io-bootstrap.json +oac-sandbox-io --bootstrap-file /home/runtime/sandbox-io-bootstrap.json ``` The command takes no other argument and reads no environment variable or configuration file. diff --git a/docs/sandbox-provider.md b/docs/sandbox-provider.md index 7b4774a29..3f5838a8a 100644 --- a/docs/sandbox-provider.md +++ b/docs/sandbox-provider.md @@ -160,7 +160,7 @@ Node readiness binds to the exact generation, the current connection and the own ### Allocation lifecycle -The allocation, its dedicated daemon credential digest and the exact Session binding commit atomically before `Create`, under the execution lease and the Session lock. Only a fresh allocation receipt permits `Create`; retries and a Core restart observe the same reference without replaying it or rotating the credential. An allocation is private compute ownership, separate from public Environment connection and native readiness; adapters qualify bootstrap completion, and Core never infers it from an engine or provider name. +The allocation, its dedicated daemon credential digest, its Serve credential digest and the exact Session binding commit atomically before `Create`, under the execution lease and the Session lock. Only a fresh allocation receipt permits `Create`; retries and a Core restart observe the same reference without replaying it or rotating the credential. An allocation is private compute ownership, separate from public Environment connection and native readiness; adapters qualify bootstrap completion, and Core never infers it from an engine or provider name. With a configured provider, the Worker scans committed pending hosted Environments that have no allocation, which covers idle Session creation and recovery after an interruption between commit and bootstrap; an existing allocation never re-enters that path. The scan is bounded and serialized by the lifecycle owner and needs no caller action. An initial reservation without a Turn leaves its Session idle, and a daemon connection is never treated as native readiness. The same scan publishes authenticated connection observations with durable generations, after verifying the exact Session and device binding and a settled bootstrap. @@ -168,6 +168,10 @@ Connected, observed compute receives service keepalives between Turns. Keepalive Terminal cleanup atomically revokes the device's authority, records the Environment's failure or expiry, settles pending input and requests cancellation, and only then calls `Kill`; original input deadlines and retry outcomes are kept. Temporary provider outages, unknown Create results and stopped compute never prove a permanent failure. After public Session deletion Core keeps the allocation and marks it released only after owned compute and volume cleanup and proof that the original Create settled; an unknown creation keeps cleanup ownership even after an absence observation, and bounded scans continue to catch late resources without another `Create`. +### Sandbox I/O service + +Every hosted sandbox also runs `oac-sandbox-io`, which Serves the allocation over the [Sandbox link](./sandbox-link-protocol.md). `Bootstrap.SandboxIO` is its [Sandbox bootstrap](./sandbox-bootstrap.md) input: the Link URL derived from the [public URL](./configuration.md#changing-the-public-url), the allocation's Serve credential, and the allocation with its Serve generation as resource. Core validates the whole `Bootstrap` once, with `Bootstrap.Validate`, before `Create`, and adapters deliver it as given. `Create` writes `SandboxIO` to a private file, `/home/runtime/sandbox-io-bootstrap.json` (mode 0600, UID 1000) in the reference adapters, and starts `oac-sandbox-io --bootstrap-file` with that path as the daemon's account, beside the daemon. The input never travels in an argument or environment variable. Every Runtime image ships `/usr/local/bin/oac-sandbox-io`. Allocation cleanup revokes the resource at the relay before it calls `Kill`. + ### Per-node lifecycle workers Each registered node has one serial lifecycle worker that owns its gate, allocation and pending cursors, connections and wake hints; E2B allocations share one serial lifecycle without a node. A thin coordinator discovers nodes and shuts workers down, and never holds its map mutex during database, provider or wait operations. Workers advance independently, so a stuck provider on one online node never stalls another: lifecycle concurrency is one operation per node and grows with the node count. Offline workers stay, so their retained resources remain observable after reconnection. @@ -206,7 +210,7 @@ Run `make check-sandbox-provider-contract` while developing. It runs the shared Node tests separately cover disconnect and reconnect fencing and cleanup after a lost Create response. Helper protocols and the [sandbox node protocol](../contracts/agents-api/node-generation-protocol.md) require an exact version match; direct in-process interfaces have no separate wire version. -Native acceptance proves what fixtures cannot: creation, lease behavior, owned partial cleanup, declared isolation and limits, and snapshots where supported. The opt-in Docker lifecycle and recovery tests use `AGENTS_RUNTIME_DOCKER_TEST_IMAGE`; the SDK helpers use `make check-e2b-provider` and `make check-microsandbox-provider`. Mocked compute proves neither reclamation nor isolation. +Native acceptance proves what fixtures cannot: creation, lease behavior, owned partial cleanup, declared isolation and limits, and snapshots where supported. The opt-in Docker lifecycle, recovery, Serve and node transport tests use `AGENTS_RUNTIME_DOCKER_TEST_IMAGE`, an image digest whose entry point sleeps and that contains `/usr/local/bin/oac-sandbox-io`; the SDK helpers use `make check-e2b-provider` and `make check-microsandbox-provider`. Mocked compute proves neither reclamation nor isolation. ## Reference adapters @@ -226,7 +230,7 @@ The Docker Sandbox Provider ([`sandbox/docker`](https://github.com/MiniMax-AI/Op - two named volumes labelled with the installation, tenant, Environment and allocation: `-home` at `/home` and `-environment` at `/environment`, whose `workspace` subdirectory is also mounted at `/workspace`. The Docker Engine must support volume subpath mounts; - with the configured `nested_sandbox` option, Docker's `/proc` masks are lifted (`/sys/firmware` and `/sys/devices/virtual/powercap` stay masked) and the container runs an init process. -Create refuses to reuse retained volumes that have no container. It copies the [Runtime bootstrap](./runtime-bootstrap.md) file to `/home/runtime/runtime-bootstrap.json` (mode 0600, UID 1000) and the `/environment` workspace, staging, initialization and package directories into the container, then starts `oac-daemon connect --profile default --bootstrap-file /home/runtime/runtime-bootstrap.json`. When the created container does not have the configured CPU, memory and exact image, Create returns the error with `CreateSettled`. Docker has no lease, so Renew only reads the container state. Kill checks the ownership labels of the container and both volumes before removing any of them, then confirms that all three are gone. +Create refuses to reuse retained volumes that have no container. It copies the [Runtime bootstrap](./runtime-bootstrap.md) file to `/home/runtime/runtime-bootstrap.json` (mode 0600, UID 1000) and the `/environment` workspace, staging, initialization and package directories into the container, then starts `oac-daemon connect --profile default --bootstrap-file /home/runtime/runtime-bootstrap.json` and the [Sandbox I/O service](#sandbox-io-service). When the created container does not have the configured CPU, memory and exact image, Create returns the error with `CreateSettled`. Docker has no lease, so Renew only reads the container state. Kill checks the ownership labels of the container and both volumes before removing any of them, then confirms that all three are gone. The node uses the explicit Unix socket in its [provider configuration](./configuration.md#docker-node-configuration) and ignores `DOCKER_HOST`. No Docker socket, host home or Core credential is mounted into a Runtime. diff --git a/docs/zh/configuration.md b/docs/zh/configuration.md index 022462ffc..3866a92cb 100644 --- a/docs/zh/configuration.md +++ b/docs/zh/configuration.md @@ -1,7 +1,7 @@ --- title: "配置参考" source: docs/configuration.md -source_hash: a53e41d4fad00b955ac4c2efde459526952e5b247514b510a05d3d5de268d2cb +source_hash: 1ef873d0104c482bef9b947a945cefb5141889ffcba2007506e0407c986bf8bb --- Core 安装的每项设置都恰好只有一个归属位置,分属以下三类: @@ -33,7 +33,7 @@ Web 的 **System** 页面显示该安装的地址、默认模型和沙箱配置 `OAC_PUBLIC_URL` 是应用、节点、沙箱和自托管执行器使用的唯一源地址。Core 从中派生守护进程 WebSocket URL、沙箱 Link URL、自托管 `remote_url` 和每个沙箱的连接地址。它是 http 或 https 源地址,也就是浏览器和节点使用的地址。安装通过 `OAC_WEB_PORT` 以 HTTP 提供 Web;前面有反向代理或托管平台时,由它们终止 HTTPS。 -源地址为 https 时,沙箱和 agent host 通过 `wss:///api/v1/sandbox-link` 连接[沙箱 Link](./sandbox-link-protocol.md)。`localhost` 或回环地址上的 http 源地址得到 `ws:///api/v1/sandbox-link`,只有 Core 自身网络命名空间内的 peer 能访问。其他主机上的 http 源地址没有 Link URL:在源地址改为 https 之前,任何组件都无法使用 Link。 +源地址为 https 时,沙箱和 agent host 通过 `wss:///api/v1/sandbox-link` 连接[沙箱 Link](./sandbox-link-protocol.md)。`localhost` 或回环地址上的 http 源地址得到 `ws:///api/v1/sandbox-link`,只有 Core 自身网络命名空间内的 peer 能访问。其他主机上的 http 源地址没有 Link URL:在源地址改为 https 之前,任何组件都无法使用 Link,Core 也不会选择或准入任何托管沙箱。 要更改它,先把反向代理指向新地址,然后编辑 `OAC_PUBLIC_URL` 并运行 `oac apply`。之后: diff --git a/docs/zh/sandbox-bootstrap.md b/docs/zh/sandbox-bootstrap.md index 1f6530ae0..097064d12 100644 --- a/docs/zh/sandbox-bootstrap.md +++ b/docs/zh/sandbox-bootstrap.md @@ -1,7 +1,7 @@ --- title: "沙箱引导" source: docs/sandbox-bootstrap.md -source_hash: 593dea87f35a7df92253a7053e99326cababc16cae7e0cc811fc9ae6bfc166d7 +source_hash: 1e2f61c5a7bafae47a72cfd66c9e604a5aa3b7120ca29b11e700241d892c5ada --- Sandbox Provider 通过交付一个引导文件来启动 Sandbox I/O 服务。本文负责 Provider 到该服务的启动输入。类型与验证器位于 [`internal/sandboxbootstrap`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/sandboxbootstrap/bootstrap.go)。服务凭此输入以 [沙箱 Link 协议](./sandbox-link-protocol.md)的 serve peer 身份连接 relay。 @@ -11,7 +11,7 @@ Sandbox Provider 通过交付一个引导文件来启动 Sandbox I/O 服务。 将一个 JSON 对象交付到普通文件中,该文件仅允许服务账户和可信资源供应进程读取(Linux 上权限为 0600),并传入其绝对路径: ```sh -oac-sandbox-io --bootstrap-file /home/sandbox/sandbox-io-bootstrap.json +oac-sandbox-io --bootstrap-file /home/runtime/sandbox-io-bootstrap.json ``` 该命令不接受其他参数,也不读取环境变量或配置文件。 diff --git a/docs/zh/sandbox-provider.md b/docs/zh/sandbox-provider.md index 539343160..c8af7d82a 100644 --- a/docs/zh/sandbox-provider.md +++ b/docs/zh/sandbox-provider.md @@ -1,7 +1,7 @@ --- title: "添加 Sandbox Provider" source: docs/sandbox-provider.md -source_hash: 36ed532c778ec8c1c4c596a011a37613ed2aa0e6ffdf20854ea30ef9a8e0c953 +source_hash: 729f9dd34c2810e08347a57a192c0509619960faefec1a1876f87fa7a7bf0367 --- **Sandbox Provider** 为 Core 管理的 Environment 提供 Runtime daemon 运行所需的外层计算资源,以及启动 daemon 的有界引导流程。本指南说明如何添加 Provider,并作为 Core 驱动 Provider 的参考。接口为 [`SandboxProvider`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/internal/sandbox/sandbox_provider.go)。 @@ -162,7 +162,7 @@ Node readiness 绑定到精确 generation、当前连接和 owner epoch。持久 ### Allocation 生命周期 {#allocation-lifecycle} -allocation、专用 daemon credential digest 和精确 Session binding 在 `Create` 前、execution lease 与 Session lock 下原子提交。只有新 allocation receipt 允许 `Create`;重试和 Core 重启观察同一 reference,不重放或轮换凭据。allocation 是私有计算资源所有权,与公开 Environment connection 和原生 readiness 独立;adapter 验证 bootstrap completion,Core 不从 engine 或 provider name 推断。 +allocation、专用 daemon credential digest、Serve credential digest 和精确 Session binding 在 `Create` 前、execution lease 与 Session lock 下原子提交。只有新 allocation receipt 允许 `Create`;重试和 Core 重启观察同一 reference,不重放或轮换凭据。allocation 是私有计算资源所有权,与公开 Environment connection 和原生 readiness 独立;adapter 验证 bootstrap completion,Core 不从 engine 或 provider name 推断。 配置 provider 后,Worker 扫描已提交且没有 allocation 的 pending hosted Environment,涵盖空闲 Session 创建以及 commit 与 bootstrap 之间中断后的恢复;已有 allocation 不重新进入此路径。scan 有界,由 lifecycle owner 串行化,不需要调用方操作。没有 Turn 的初始预约让 Session 保持空闲,daemon 连接不被当作原生 readiness。同一 scan 在验证精确 Session、device binding 和已结算 bootstrap 后,发布带持久 generation 的认证连接观测。 @@ -170,6 +170,10 @@ allocation、专用 daemon credential digest 和精确 Session binding 在 `Crea 终结清理原子撤销 device authority、记录 Environment 失败或到期、结算 pending input 并请求取消,然后才调用 `Kill`;原 input deadline 与 retry outcome 保留。临时 provider outage、未知 Create result 和停止的计算资源不证明永久失败。公开 Session 删除后 Core 保留 allocation,仅在所属 compute 与 volume 清理完成且原 Create 已结算的证明成立后标记 released;未知创建即使观察到不存在也保留 cleanup ownership,有界 scan 继续捕捉延迟资源,不再调用 `Create`。 +### Sandbox I/O 服务 {#sandbox-io-service} + +每个托管 sandbox 还会运行 `oac-sandbox-io`,它通过[沙箱 Link](./sandbox-link-protocol.md) Serve 该 allocation。`Bootstrap.SandboxIO` 是它的[沙箱引导](./sandbox-bootstrap.md)输入:从[公开 URL](./configuration.md#changing-the-public-url) 派生的 Link URL、allocation 的 Serve credential,以及作为 resource 的 allocation 及其 Serve generation。Core 在 `Create` 前用 `Bootstrap.Validate` 对整个 `Bootstrap` 校验一次,adapter 原样交付。`Create` 将 `SandboxIO` 写入私有文件(参考 adapter 中为 `/home/runtime/sandbox-io-bootstrap.json`,mode 0600、UID 1000),并以 daemon 的账户在 daemon 旁边启动 `oac-sandbox-io --bootstrap-file`,参数为该路径。该输入从不通过命令参数或环境变量传递。每个 Runtime 镜像都包含 `/usr/local/bin/oac-sandbox-io`。allocation cleanup 在调用 `Kill` 前先在 relay 撤销该 resource。 + ### 每节点生命周期 worker {#per-node-lifecycle-workers} 每个注册 node 有一个串行 lifecycle worker,负责 gate、allocation 与 pending cursor、connection 和 wake hint;E2B allocation 共享一个没有 node 的串行 lifecycle。薄 coordinator 发现 node 并关闭 worker,数据库、provider 或等待操作期间不持有 map mutex。worker 独立推进,因此一个在线 node 的 provider 卡住不会阻塞其他 node:lifecycle 并发为每 node 一项操作,随 node 数量增长。离线 worker 保留,因此其保留资源在重连后仍可观察。 @@ -208,7 +212,7 @@ installer 与 Go adapter 的 E2B template 和 endpoint validator 消费共享 [s node 测试单独覆盖 disconnect、reconnect fencing,以及 Create response 丢失后的 cleanup。helper protocol 和 [sandbox node 协议](../../contracts/agents-api/zh/node-generation-protocol.md)要求精确版本匹配;直接进程内接口没有独立 wire version。 -原生验收证明 fixture 无法证明的事实:creation、lease 行为、所属 partial cleanup、声明的 isolation 与 limit,以及支持时的 snapshot。显式启用的 Docker lifecycle 和 recovery 测试使用 `AGENTS_RUNTIME_DOCKER_TEST_IMAGE`;SDK helper 使用 `make check-e2b-provider` 和 `make check-microsandbox-provider`。mock compute 不能证明 reclamation 或 isolation。 +原生验收证明 fixture 无法证明的事实:creation、lease 行为、所属 partial cleanup、声明的 isolation 与 limit,以及支持时的 snapshot。显式启用的 Docker lifecycle、recovery、Serve 和 node transport 测试使用 `AGENTS_RUNTIME_DOCKER_TEST_IMAGE`,即一个 entry point 为 sleep 且包含 `/usr/local/bin/oac-sandbox-io` 的镜像 digest;SDK helper 使用 `make check-e2b-provider` 和 `make check-microsandbox-provider`。mock compute 不能证明 reclamation 或 isolation。 ## 参考 adapter {#reference-adapters} @@ -228,7 +232,7 @@ Docker Sandbox Provider([`sandbox/docker`](https://github.com/MiniMax-AI/OpenA - 两个 named volume,label 包含 installation、tenant、Environment 和 allocation:`-home` 挂载到 `/home`,`-environment` 挂载到 `/environment`,后者的 `workspace` 子目录也挂载到 `/workspace`。Docker Engine 必须支持 volume subpath mount; - 配置 `nested_sandbox` option 时,解除 Docker `/proc` mask(`/sys/firmware` 和 `/sys/devices/virtual/powercap` 保持 mask),container 运行 init process。 -Create 拒绝复用没有 container 的保留 volume。它将 [Runtime 引导](runtime-bootstrap.md)文件复制到 `/home/runtime/runtime-bootstrap.json`(mode 0600、UID 1000),并将 `/environment` workspace、staging、initialization 和 package directory 放入 container,然后启动 `oac-daemon connect --profile default --bootstrap-file /home/runtime/runtime-bootstrap.json`。创建的 container 不具备配置的 CPU、memory 和精确 image 时,Create 返回 error 和 `CreateSettled`。Docker 没有 lease,因此 Renew 仅读取 container state。Kill 在删除前检查 container 和两个 volume 的 ownership label,再确认三者都已不存在。 +Create 拒绝复用没有 container 的保留 volume。它将 [Runtime 引导](runtime-bootstrap.md)文件复制到 `/home/runtime/runtime-bootstrap.json`(mode 0600、UID 1000),并将 `/environment` workspace、staging、initialization 和 package directory 放入 container,然后启动 `oac-daemon connect --profile default --bootstrap-file /home/runtime/runtime-bootstrap.json` 和 [Sandbox I/O 服务](#sandbox-io-service)。创建的 container 不具备配置的 CPU、memory 和精确 image 时,Create 返回 error 和 `CreateSettled`。Docker 没有 lease,因此 Renew 仅读取 container state。Kill 在删除前检查 container 和两个 volume 的 ownership label,再确认三者都已不存在。 node 使用 [provider 配置](configuration.md#docker-node-configuration)中的明确 Unix socket,忽略 `DOCKER_HOST`。不将 Docker socket、host home 或 Core credential 挂载进 Runtime。 diff --git a/scripts/build-claude-runtime.sh b/scripts/build-claude-runtime.sh index af5b3b587..c4fa06d90 100755 --- a/scripts/build-claude-runtime.sh +++ b/scripts/build-claude-runtime.sh @@ -19,7 +19,7 @@ node "$repo_root/scripts/check-claude-sdk-runtime.mjs" "$context/claude-sdk" ( cd "$repo_root" CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod=readonly -trimpath \ - -o "$context/oac-daemon" ./apps/daemon/cmd/oac-daemon + -o "$context/" ./apps/daemon/cmd/oac-daemon ./apps/sandboxio/cmd/oac-sandbox-io ) cp "$repo_root/services/core/deploy/claude/Dockerfile" "$context/Dockerfile" mkdir -p "$output_dir" diff --git a/scripts/build-codex-runtime.sh b/scripts/build-codex-runtime.sh index d2dd85eb0..ae14f43ca 100755 --- a/scripts/build-codex-runtime.sh +++ b/scripts/build-codex-runtime.sh @@ -28,7 +28,7 @@ trap 'rm -rf "$context"' EXIT ( cd "$repo_root" CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod=readonly -trimpath \ - -o "$context/oac-daemon" ./apps/daemon/cmd/oac-daemon + -o "$context/" ./apps/daemon/cmd/oac-daemon ./apps/sandboxio/cmd/oac-sandbox-io ) cp "$native_dir/bin/codex" "$native_dir/bin/codex-code-mode-host" "$context/" cp -R "$native_dir/codex-resources" "$context/codex-resources" diff --git a/scripts/build-core-distribution.sh b/scripts/build-core-distribution.sh index ac40a609a..8102bfae5 100755 --- a/scripts/build-core-distribution.sh +++ b/scripts/build-core-distribution.sh @@ -149,7 +149,7 @@ cp -R apps/web/dist "$stage/web/dist" cp services/web/Dockerfile "$stage/web/Dockerfile" build_image web "$stage/web" -CGO_ENABLED=0 go build -mod=readonly -trimpath -o "$stage/oac-daemon" ./apps/daemon/cmd/oac-daemon +CGO_ENABLED=0 go build -mod=readonly -trimpath -o "$stage/" ./apps/daemon/cmd/oac-daemon ./apps/sandboxio/cmd/oac-sandbox-io cp "$stage/oac-daemon" "$bundle/native/bin/oac-daemon" codex_image="${CORE_DISTRIBUTION_CODEX_IMAGE:-}" claude_image="${CORE_DISTRIBUTION_CLAUDE_IMAGE:-}" @@ -173,7 +173,7 @@ else mcode_image="$(cat "$stage/mcode.id")" fi for image in "$codex_image" "$claude_image" "$mcode_image"; do - python3 scripts/core-distribution-manifest.py verify-runtime "$image" "$stage/oac-daemon" "$source_dir" + python3 scripts/core-distribution-manifest.py verify-runtime "$image" "$stage/oac-daemon" "$stage/oac-sandbox-io" "$source_dir" done tag_suffix="${stage##*.}" for harness in codex claude mcode; do diff --git a/scripts/build-mcode-runtime.sh b/scripts/build-mcode-runtime.sh index 04a507698..6f3211ade 100644 --- a/scripts/build-mcode-runtime.sh +++ b/scripts/build-mcode-runtime.sh @@ -20,7 +20,7 @@ cp -RL "$companion/." "$context/mcode-harness/" ( cd "$repo_root" CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod=readonly -trimpath \ - -o "$context/oac-daemon" ./apps/daemon/cmd/oac-daemon + -o "$context/" ./apps/daemon/cmd/oac-daemon ./apps/sandboxio/cmd/oac-sandbox-io ) cp "$repo_root/services/core/deploy/mcode/Dockerfile" "$context/Dockerfile" mkdir -p "$output" diff --git a/scripts/core-distribution-manifest.py b/scripts/core-distribution-manifest.py index 3529fb8fe..256c33891 100644 --- a/scripts/core-distribution-manifest.py +++ b/scripts/core-distribution-manifest.py @@ -185,10 +185,10 @@ def control_archive(bundle, stage, revision, architecture): target.with_name(target.name + ".sha256").write_text(sha256(target) + " " + target.name + "\n") -def verify_runtime(image, daemon, source): +def verify_runtime(image, daemon, sandbox_io, source): details = verify_image(image) source = pathlib.Path(source) - files = {"/usr/local/bin/oac-daemon": pathlib.Path(daemon)} + files = {"/usr/local/bin/oac-daemon": pathlib.Path(daemon), "/usr/local/bin/oac-sandbox-io": pathlib.Path(sandbox_io)} environment = dict(value.split("=", 1) for value in details["Config"]["Env"] if "=" in value) if "OAC_RUNTIME_MCODE_BIN" in environment: for name in ("launch.mjs", "bridge.mjs", "check.mjs", "tool-executor.mjs", "subagent-snapshot.mjs", "source.json"): diff --git a/services/core/cmd/server/managed_nodes.go b/services/core/cmd/server/managed_nodes.go index a6d8f434b..acc1d8f69 100644 --- a/services/core/cmd/server/managed_nodes.go +++ b/services/core/cmd/server/managed_nodes.go @@ -74,8 +74,10 @@ func configureManagedNodes(nodes *deployment.Service, reader deployment.Reader, return nodes.Heartbeat(ctx, n.NodeID, connection, epoch, nodeHealthRecord(health)) }, }) - // Load requires OAC_PUBLIC_URL with an installation ID. - result.setup = &managedSetup{processPaths: config.ProviderPaths, registry: registry, deployment: nodes, allocations: reader, hub: result.hub, installationID: config.InstallationID, runtimeAPI: config.PublicOrigin.RuntimeAPI()} + // Load requires OAC_PUBLIC_URL with an installation ID. An origin without a + // sandbox Link admits no hosted sandbox. + link, _ := config.PublicOrigin.SandboxLink() + result.setup = &managedSetup{processPaths: config.ProviderPaths, registry: registry, deployment: nodes, allocations: reader, hub: result.hub, installationID: config.InstallationID, runtimeAPI: config.PublicOrigin.RuntimeAPI(), sandboxLink: link} result.runtime = execution.NewDeferredRuntimeProvider(config.InstallationID, result.setup.load, result.setup.prepare) result.runtime.PublishUnconfigured = result.setup.publishUnconfigured return result diff --git a/services/core/cmd/server/managed_setup.go b/services/core/cmd/server/managed_setup.go index 0ca908728..566c90570 100644 --- a/services/core/cmd/server/managed_setup.go +++ b/services/core/cmd/server/managed_setup.go @@ -30,10 +30,11 @@ type managedSetup struct { hub *node.Hub installationID string // runtimeAPI is the /api/v1 base of OAC_PUBLIC_URL; every sandbox reaches - // Core through it. - runtimeAPI string - selected atomic.Pointer[managedSelection] - providerCalls sandbox.CallFence + // Core through it and Serves on sandboxLink, empty when the origin has no + // Link. + runtimeAPI, sandboxLink string + selected atomic.Pointer[managedSelection] + providerCalls sandbox.CallFence } // deploymentSetups reads the committed deployment setup and its retained @@ -146,7 +147,7 @@ func (s *managedSetup) configuration(setup deployment.Setup) (execution.Prepared return execution.PreparedRuntimeDeployment{}, fmt.Errorf("%w: %v", execution.ErrExecutionUnavailable, err) } selected := &execution.RuntimeProvider{InstallationID: setup.InstallationID, ProviderKind: setup.Provider, Generation: setup.Generation, Mode: setup.Mode, AdmissionPaused: setup.AdmissionPaused, - CoreURL: s.runtimeAPI, BackendFingerprint: setup.BackendFingerprint, Provider: provider} + CoreURL: s.runtimeAPI, SandboxLink: s.sandboxLink, BackendFingerprint: setup.BackendFingerprint, Provider: provider} if setup.Suspension != nil { selected.Suspension = &execution.RuntimeSuspensionPolicy{IdleTimeout: time.Duration(setup.Suspension.IdleSeconds) * time.Second, Retention: time.Duration(setup.Suspension.RetentionSeconds) * time.Second, MaxActive: 4, MaxRetained: 16} diff --git a/services/core/deploy/claude/Dockerfile b/services/core/deploy/claude/Dockerfile index 115878e19..ca50b3ebf 100644 --- a/services/core/deploy/claude/Dockerfile +++ b/services/core/deploy/claude/Dockerfile @@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates bash git python3 python3-pip ripgrep \ && rm -rf /var/lib/apt/lists/* \ && mkdir -p /environment/workspace /workspace /home/runtime -COPY --chmod=0555 oac-daemon /usr/local/bin/ +COPY --chmod=0555 oac-daemon oac-sandbox-io /usr/local/bin/ COPY claude-sdk /opt/claude-sdk ENV HOME=/home/runtime OAC_RUNTIME_HOME=/home/runtime/.oac \ OAC_RUNTIME_CLAUDE_SDK_NODE=/usr/local/bin/node \ diff --git a/services/core/deploy/codex/Dockerfile b/services/core/deploy/codex/Dockerfile index e79a93cd5..76d193f58 100644 --- a/services/core/deploy/codex/Dockerfile +++ b/services/core/deploy/codex/Dockerfile @@ -8,7 +8,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates bash git python3 python3-pip ripgrep \ && rm -rf /var/lib/apt/lists/* \ && mkdir -p /environment/workspace /workspace /home/runtime -COPY --chmod=0555 oac-daemon codex /usr/local/bin/ +COPY --chmod=0555 oac-daemon oac-sandbox-io codex /usr/local/bin/ COPY codex-resources /usr/local/codex-resources ENV HOME=/home/runtime OAC_RUNTIME_HOME=/home/runtime/.oac \ OAC_RUNTIME_CODEX_BIN=/usr/local/bin/codex \ diff --git a/services/core/deploy/e2b/helper_contract_generated.py b/services/core/deploy/e2b/helper_contract_generated.py index ebd7fadf2..cccd7d2e4 100644 --- a/services/core/deploy/e2b/helper_contract_generated.py +++ b/services/core/deploy/e2b/helper_contract_generated.py @@ -1,7 +1,7 @@ # Code generated by contractgen; DO NOT EDIT. """Adapter-private wire declarations. No SDK or repository dependency.""" ERROR_CODES = ["","invalid","ownership","exists","not_found","command_unconfirmed","unconfirmed","template_invalid","team_mismatch","unauthorized"] -MANAGED_BOOTSTRAP_FIELDS = ["TenantID","EnvironmentID","AllocationID","SessionID","DeviceID","NetworkAccess","AllowedDomains","InstallationID","RuntimeBootstrap"] +MANAGED_BOOTSTRAP_FIELDS = ["TenantID","EnvironmentID","AllocationID","SessionID","DeviceID","NetworkAccess","AllowedDomains","SandboxIO","InstallationID","RuntimeBootstrap"] MANAGED_IDENTITY_FIELDS = ["TenantID","EnvironmentID","AllocationID","SessionID","DeviceID","InstallationID"] MAX_COMMAND_INPUT = 52428832 MAX_CREDENTIAL_REFERENCES = 32 diff --git a/services/core/deploy/e2b/managed_init.py b/services/core/deploy/e2b/managed_init.py index a64931f14..e56619663 100644 --- a/services/core/deploy/e2b/managed_init.py +++ b/services/core/deploy/e2b/managed_init.py @@ -53,17 +53,24 @@ def initialize(): OAC_RUNTIME_ALLOWED_DOMAINS=json.dumps(payload['AllowedDomains'] or [])) connection = Path(environment['OAC_RUNTIME_HOME']).parent / 'runtime-bootstrap.json' shared.write_private(connection, payload['RuntimeBootstrap'], owner=1000) + serve = connection.with_name('sandbox-io-bootstrap.json') + shared.write_private(serve, payload['SandboxIO'], owner=1000) source.unlink() - with (shared.PROFILE / 'daemon.log').open('xb') as stream: - os.fchmod(stream.fileno(), 0o600) - os.fchown(stream.fileno(), 1000, 1000) - child = subprocess.Popen(['/usr/local/bin/oac-daemon', 'connect', '--profile', 'default', - '--bootstrap-file', str(connection)], - cwd='/environment/workspace', env=environment, user=1000, group=1000, - extra_groups=[], start_new_session=True, stdin=subprocess.DEVNULL, - stdout=stream, stderr=subprocess.STDOUT, umask=0o077) + # The Sandbox I/O service runs beside the daemon, as the same account; its + # file is its only input. + pids = [] + for log, command, env in [ + (shared.PROFILE / 'daemon.log', ['/usr/local/bin/oac-daemon', 'connect', '--profile', 'default', + '--bootstrap-file', str(connection)], environment), + (connection.with_name('sandbox-io.log'), ['/usr/local/bin/oac-sandbox-io', '--bootstrap-file', str(serve)], {})]: + with log.open('xb') as stream: + os.fchmod(stream.fileno(), 0o600) + os.fchown(stream.fileno(), 1000, 1000) + pids.append(subprocess.Popen(command, cwd='/environment/workspace', env=env, user=1000, group=1000, + extra_groups=[], start_new_session=True, stdin=subprocess.DEVNULL, + stdout=stream, stderr=subprocess.STDOUT, umask=0o077).pid) shared.write_private(root / 'managed-ready.tmp', - {'identity': binding, 'status': 'daemon_started', 'daemon_pid': child.pid}) + {'identity': binding, 'status': 'daemon_started', 'daemon_pid': pids[0]}) os.replace(root / 'managed-ready.tmp', root / 'managed-ready.json') shared.sync_directory(root) diff --git a/services/core/deploy/e2b/managed_init_test.py b/services/core/deploy/e2b/managed_init_test.py index d9017ba82..0af13ac36 100644 --- a/services/core/deploy/e2b/managed_init_test.py +++ b/services/core/deploy/e2b/managed_init_test.py @@ -18,6 +18,9 @@ def payload(): 'AllocationID', 'SessionID', 'DeviceID']} return dict(value, RuntimeBootstrap={'version': 1, 'core_url': 'https://core.example/api/v1', 'device_id': value['DeviceID'], 'credential': 'private-managed-token'}, + SandboxIO={'version': 1, 'link_url': 'wss://core.example/api/v1/sandbox-link', 'credential': 'private-serve-token', + 'resource': {'tenant_id': value['TenantID'], 'environment_id': value['EnvironmentID'], + 'kind': 'allocation', 'id': value['AllocationID'], 'generation': 1}}, NetworkAccess='restricted', AllowedDomains=['example.com']) @@ -76,23 +79,35 @@ def exercise(self, failed=False): connection_file = Path(temporary) / 'runtime-bootstrap.json' self.assertEqual(json.loads(connection_file.read_text()), data['RuntimeBootstrap']) self.assertEqual(connection_file.stat().st_mode & 0o777, 0o600) + serve_file = Path(temporary) / 'sandbox-io-bootstrap.json' + self.assertEqual(json.loads(serve_file.read_text()), data['SandboxIO']) + self.assertEqual(serve_file.stat().st_mode & 0o777, 0o600) self.assertFalse((profile / 'auth.json').exists()) - self.assertEqual(process.call_args.args[0][-2:], ['--bootstrap-file', str(connection_file)]) self.assertFalse(source.exists()) - self.assertNotIn(data['RuntimeBootstrap']['credential'], json.dumps(process.call_args.args)) - self.assertNotIn(data['RuntimeBootstrap']['credential'], json.dumps(process.call_args.kwargs['env'])) - self.assertEqual(process.call_args.kwargs['env']['OAC_RUNTIME_ENVIRONMENT_ID'], data['EnvironmentID']) - self.assertEqual(process.call_args.kwargs['user'], 1000) + daemon = process.call_args_list[0] + self.assertEqual(daemon.args[0][-2:], ['--bootstrap-file', str(connection_file)]) + self.assertEqual(daemon.kwargs['env']['OAC_RUNTIME_ENVIRONMENT_ID'], data['EnvironmentID']) + if not failed: + serve = process.call_args_list[1] + self.assertEqual(serve.args[0], ['/usr/local/bin/oac-sandbox-io', '--bootstrap-file', str(serve_file)]) + self.assertEqual(serve.kwargs['env'], {}) + for call in process.call_args_list: + self.assertEqual(call.kwargs['user'], 1000) + for credential in (data['RuntimeBootstrap']['credential'], data['SandboxIO']['credential']): + self.assertNotIn(credential, json.dumps(call.args)) + self.assertNotIn(credential, json.dumps(call.kwargs['env'])) if failed: self.assertFalse((root / 'managed-ready.json').exists()) else: receipt = json.loads((root / 'managed-ready.json').read_text()) self.assertEqual(receipt['identity'], managed_init.identity(data)) self.assertNotIn(data['RuntimeBootstrap']['credential'], json.dumps(receipt)) + self.assertEqual(receipt['daemon_pid'], 456) + calls = process.call_count source.write_text(json.dumps(data)) with self.assertRaises(RuntimeError): managed_init.initialize() - process.assert_called_once() + self.assertEqual(process.call_count, calls) def test_managed_credentials_and_environment(self): self.exercise() diff --git a/services/core/deploy/mcode/Dockerfile b/services/core/deploy/mcode/Dockerfile index 774c1be79..4255fd014 100644 --- a/services/core/deploy/mcode/Dockerfile +++ b/services/core/deploy/mcode/Dockerfile @@ -5,7 +5,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates bash git python3 python3-pip ripgrep \ && rm -rf /var/lib/apt/lists/* \ && mkdir -p /environment/workspace /workspace /home/runtime -COPY --chmod=0555 oac-daemon /usr/local/bin/ +COPY --chmod=0555 oac-daemon oac-sandbox-io /usr/local/bin/ COPY mcode-harness /opt/mcode-harness ENV HOME=/home/runtime OAC_RUNTIME_HOME=/home/runtime/.oac \ OAC_RUNTIME_MCODE_NODE=/usr/local/bin/node \ diff --git a/services/core/internal/db/queries/runtime_allocations.sql b/services/core/internal/db/queries/runtime_allocations.sql index fc62d9deb..d662f80d1 100644 --- a/services/core/internal/db/queries/runtime_allocations.sql +++ b/services/core/internal/db/queries/runtime_allocations.sql @@ -1,6 +1,6 @@ -- name: CreateRuntimeAllocation :one -INSERT INTO runtime_allocations (id, environment_id, device_id, provider_key, node_id, deployment_generation) -VALUES ($1, $2, $3, $4, $5, $6) RETURNING *; +INSERT INTO runtime_allocations (id, environment_id, device_id, provider_key, node_id, deployment_generation, serve_credential_hash) +VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *; -- name: GetRuntimeAllocation :one SELECT sqlc.embed(a), e.session_id, s.tenant_id, s.deleted_at, (CASE WHEN a.compute_phase NOT IN ('disabled', 'running') THEN a.compute_retained_until IS NOT NULL AND a.compute_retained_until <= clock_timestamp() ELSE a.node_id IS NULL AND (SELECT mode FROM runtime_deployment) <> 'direct' AND a.kept_at <= clock_timestamp() - interval '1 hour' END)::boolean AS expired diff --git a/services/core/internal/db/sqlc/runtime_allocations.sql.go b/services/core/internal/db/sqlc/runtime_allocations.sql.go index 459a93258..30259ce0f 100644 --- a/services/core/internal/db/sqlc/runtime_allocations.sql.go +++ b/services/core/internal/db/sqlc/runtime_allocations.sql.go @@ -12,8 +12,8 @@ import ( ) const createRuntimeAllocation = `-- name: CreateRuntimeAllocation :one -INSERT INTO runtime_allocations (id, environment_id, device_id, provider_key, node_id, deployment_generation) -VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, environment_id, device_id, provider_key, state, create_settled, created_at, kept_at, released_at, compute_phase, compute_revision, compute_state, compute_activity_at, compute_wake_requested, compute_retained_until, node_id, observation_error, compute_phase_changed_at, deployment_generation, serve_credential_hash, serve_generation +INSERT INTO runtime_allocations (id, environment_id, device_id, provider_key, node_id, deployment_generation, serve_credential_hash) +VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, environment_id, device_id, provider_key, state, create_settled, created_at, kept_at, released_at, compute_phase, compute_revision, compute_state, compute_activity_at, compute_wake_requested, compute_retained_until, node_id, observation_error, compute_phase_changed_at, deployment_generation, serve_credential_hash, serve_generation ` type CreateRuntimeAllocationParams struct { @@ -23,6 +23,7 @@ type CreateRuntimeAllocationParams struct { ProviderKey pgtype.UUID `json:"provider_key"` NodeID pgtype.UUID `json:"node_id"` DeploymentGeneration pgtype.Int8 `json:"deployment_generation"` + ServeCredentialHash pgtype.Text `json:"serve_credential_hash"` } func (q *Queries) CreateRuntimeAllocation(ctx context.Context, arg CreateRuntimeAllocationParams) (RuntimeAllocation, error) { @@ -33,6 +34,7 @@ func (q *Queries) CreateRuntimeAllocation(ctx context.Context, arg CreateRuntime arg.ProviderKey, arg.NodeID, arg.DeploymentGeneration, + arg.ServeCredentialHash, ) var i RuntimeAllocation err := row.Scan( diff --git a/services/core/internal/deployment/allocation.go b/services/core/internal/deployment/allocation.go index a0ae9a37b..95f90b5d8 100644 --- a/services/core/internal/deployment/allocation.go +++ b/services/core/internal/deployment/allocation.go @@ -70,6 +70,9 @@ type NewAllocation struct { // NodeID is empty when no node serves the allocation. NodeID string Generation uint64 + // ServeCredentialHash is the SHA-256 hex digest of the allocation's Link + // Serve credential. + ServeCredentialHash string } // SessionDevice is the Runtime device a Session is bound to. diff --git a/services/core/internal/deployment/allocations.go b/services/core/internal/deployment/allocations.go index 2d10c6c1d..36f2dce8a 100644 --- a/services/core/internal/deployment/allocations.go +++ b/services/core/internal/deployment/allocations.go @@ -2,6 +2,8 @@ package deployment import ( "context" + "crypto/sha256" + "encoding/hex" "encoding/json" "fmt" "time" @@ -16,10 +18,10 @@ import ( // ReserveAllocation commits the allocation of the tenant's hosted Environment // and its dedicated device together, before the provider creates compute. // installation is the provider key the allocation is provisioned for, and -// credentialHash the SHA-256 digest of the device credential. An existing -// allocation for the same installation returns Replayed; only a fresh one -// authorizes the one Create call. -func (e *ExecutionOperations) ReserveAllocation(ctx context.Context, key AllocationKey, installation, credentialHash string) (Allocation, error) { +// credentialHash and serveCredentialHash the SHA-256 digests of the device +// and Link Serve credentials. An existing allocation for the same installation +// returns Replayed; only a fresh one authorizes the one Create call. +func (e *ExecutionOperations) ReserveAllocation(ctx context.Context, key AllocationKey, installation, credentialHash, serveCredentialHash string) (Allocation, error) { key, err := key.parse() if err != nil { return Allocation{}, err @@ -28,7 +30,8 @@ func (e *ExecutionOperations) ReserveAllocation(ctx context.Context, key Allocat return Allocation{}, err } registration, err := sessions.NewDeviceRegistration("managed-runtime", credentialHash) - if err != nil { + serve, serveErr := hex.DecodeString(serveCredentialHash) + if err != nil || serveErr != nil || len(serve) != sha256.Size { return Allocation{}, fmt.Errorf("%w: SHA-256 credential digest required", ErrInvalidInput) } var result Allocation @@ -67,7 +70,7 @@ func (e *ExecutionOperations) ReserveAllocation(ctx context.Context, key Allocat if environment.Status == "failed" || environment.Status == "expired" { return ErrInvalidInput } - allocation := NewAllocation{ID: uuid.NewString(), EnvironmentID: environment.ID, DeviceID: uuid.NewString(), ProviderKey: installation, Generation: d.Generation} + allocation := NewAllocation{ID: uuid.NewString(), EnvironmentID: environment.ID, DeviceID: uuid.NewString(), ProviderKey: installation, Generation: d.Generation, ServeCredentialHash: hex.EncodeToString(serve)} if d.Mode == "nodes" { reserved, err := tx.LoadReserved() if err != nil { diff --git a/services/core/internal/deployment/allocations_test.go b/services/core/internal/deployment/allocations_test.go index cdb44204d..3c01e888b 100644 --- a/services/core/internal/deployment/allocations_test.go +++ b/services/core/internal/deployment/allocations_test.go @@ -292,21 +292,21 @@ func TestReserveAllocationReplaysBeforeAdmission(t *testing.T) { existing := Allocation{ID: uuid.NewString(), EnvironmentID: key.EnvironmentID, ProviderKey: installation} tx := &fakeReservationTx{t: t, loadEnvironment: hostedEnvironment(key.EnvironmentID), findAllocation: func() (Allocation, bool, error) { return existing, true, nil }} - replayed, err := allocationOperations(t, tx, sessions.LockedSession{}, nil).ReserveAllocation(t.Context(), key, installation, testCredentialHash()) + replayed, err := allocationOperations(t, tx, sessions.LockedSession{}, nil).ReserveAllocation(t.Context(), key, installation, testCredentialHash(), testCredentialHash()) if err != nil || !replayed.Replayed || replayed.ID != existing.ID { t.Fatal("reservation did not replay", replayed, err) } - if _, err := allocationOperations(t, tx, sessions.LockedSession{}, nil).ReserveAllocation(t.Context(), key, uuid.NewString(), testCredentialHash()); !errors.Is(err, ErrAllocationConflict) { + if _, err := allocationOperations(t, tx, sessions.LockedSession{}, nil).ReserveAllocation(t.Context(), key, uuid.NewString(), testCredentialHash(), testCredentialHash()); !errors.Is(err, ErrAllocationConflict) { t.Fatal("another installation replayed the allocation", err) } deleted := &fakeReservationTx{t: t} - if _, err := allocationOperations(t, deleted, sessions.LockedSession{Deleted: true}, nil).ReserveAllocation(t.Context(), key, installation, testCredentialHash()); !errors.Is(err, sessions.ErrNotFound) { + if _, err := allocationOperations(t, deleted, sessions.LockedSession{Deleted: true}, nil).ReserveAllocation(t.Context(), key, installation, testCredentialHash(), testCredentialHash()); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("a deleted Session reserved an allocation", err) } selfHosted := &fakeReservationTx{t: t, loadEnvironment: func() (sessions.Environment, error) { return sessions.Environment{ID: key.EnvironmentID, Configuration: json.RawMessage(`{"type":"self_hosted"}`)}, nil }} - if _, err := allocationOperations(t, selfHosted, sessions.LockedSession{}, nil).ReserveAllocation(t.Context(), key, installation, testCredentialHash()); !errors.Is(err, ErrInvalidInput) { + if _, err := allocationOperations(t, selfHosted, sessions.LockedSession{}, nil).ReserveAllocation(t.Context(), key, installation, testCredentialHash(), testCredentialHash()); !errors.Is(err, ErrInvalidInput) { t.Fatal("a self-hosted Environment reserved an allocation", err) } } @@ -328,14 +328,14 @@ func TestReserveAllocationAdmitsAndTakesTheReservedNode(t *testing.T) { d.AdmissionPaused = true return d, nil } - if _, err := allocationOperations(t, paused, sessions.LockedSession{}, nil).ReserveAllocation(t.Context(), key, installation, testCredentialHash()); !errors.Is(err, placement.ErrAdmissionClosed) { + if _, err := allocationOperations(t, paused, sessions.LockedSession{}, nil).ReserveAllocation(t.Context(), key, installation, testCredentialHash(), testCredentialHash()); !errors.Is(err, placement.ErrAdmissionClosed) { t.Fatal("paused admission reserved an allocation", err) } released := fresh() released.loadReserved = func() (placement.Reserved, error) { return placement.Reserved{NodeID: node, Generation: 5, Released: true, Available: true}, nil } - if _, err := allocationOperations(t, released, sessions.LockedSession{}, nil).ReserveAllocation(t.Context(), key, installation, testCredentialHash()); !errors.Is(err, placement.ErrNodeUnavailable) { + if _, err := allocationOperations(t, released, sessions.LockedSession{}, nil).ReserveAllocation(t.Context(), key, installation, testCredentialHash(), testCredentialHash()); !errors.Is(err, placement.ErrNodeUnavailable) { t.Fatal("a released placement reserved an allocation", err) } var device sessions.ExecutionDevice @@ -356,8 +356,9 @@ func TestReserveAllocationAdmitsAndTakesTheReservedNode(t *testing.T) { inserted = a return Allocation{ID: a.ID, DeviceID: a.DeviceID, NodeID: a.NodeID}, nil } - result, err := allocationOperations(t, tx, sessions.LockedSession{}, nil).ReserveAllocation(t.Context(), key, installation, testCredentialHash()) - if err != nil || result.Replayed || inserted.NodeID != node || inserted.Generation != 5 || inserted.ProviderKey != installation || inserted.DeviceID != device.ID || device.EnvironmentID != key.EnvironmentID { + serveHash := testCredentialHash() + result, err := allocationOperations(t, tx, sessions.LockedSession{}, nil).ReserveAllocation(t.Context(), key, installation, testCredentialHash(), serveHash) + if err != nil || result.Replayed || inserted.NodeID != node || inserted.Generation != 5 || inserted.ProviderKey != installation || inserted.DeviceID != device.ID || device.EnvironmentID != key.EnvironmentID || inserted.ServeCredentialHash != serveHash { t.Fatal("reservation", result, inserted, device, err) } } @@ -425,10 +426,12 @@ func TestSetComputeValidatesBeforeStorage(t *testing.T) { // A malformed credential digest is deployment's invalid input and never // reaches storage. -func TestReserveAllocationValidatesTheCredentialDigest(t *testing.T) { +func TestReserveAllocationValidatesTheCredentialDigests(t *testing.T) { key := AllocationKey{TenantID: uuid.NewString(), EnvironmentID: uuid.NewString()} - if _, err := allocationOperations(t, nil, sessions.LockedSession{}, nil).ReserveAllocation(t.Context(), key, uuid.NewString(), "not-a-digest"); !errors.Is(err, ErrInvalidInput) { - t.Fatal("a malformed digest reserved an allocation", err) + for _, digests := range [][2]string{{"not-a-digest", testCredentialHash()}, {testCredentialHash(), "not-a-digest"}} { + if _, err := allocationOperations(t, nil, sessions.LockedSession{}, nil).ReserveAllocation(t.Context(), key, uuid.NewString(), digests[0], digests[1]); !errors.Is(err, ErrInvalidInput) { + t.Fatal("a malformed digest reserved an allocation", err) + } } } diff --git a/services/core/internal/execution/archive_cancellation_cleanup_test.go b/services/core/internal/execution/archive_cancellation_cleanup_test.go index 8a0005021..e00834597 100644 --- a/services/core/internal/execution/archive_cancellation_cleanup_test.go +++ b/services/core/internal/execution/archive_cancellation_cleanup_test.go @@ -86,7 +86,7 @@ func TestArchiveWaitingCleanupReceiptBarrier(t *testing.T) { session := created.Session secret := uuid.NewString() key := deployment.AllocationKey{TenantID: project.TenantID, EnvironmentID: session.Environment.ID} - owner, err := leased.Deployment.ReserveAllocation(t.Context(), key, installation, runtimedevice.HashCredential(secret)) + owner, err := leased.Deployment.ReserveAllocation(t.Context(), key, installation, runtimedevice.HashCredential(secret), runtimedevice.HashCredential(secret)) if err != nil { t.Fatal(err) } diff --git a/services/core/internal/execution/runtime_lifecycle.go b/services/core/internal/execution/runtime_lifecycle.go index 040853289..940a0a2d4 100644 --- a/services/core/internal/execution/runtime_lifecycle.go +++ b/services/core/internal/execution/runtime_lifecycle.go @@ -35,6 +35,7 @@ type RuntimeProvider struct { LocalCredentialSHA256 string LocalMaxActive, LocalMaxRetained int CoreURL string + SandboxLink string InstallationID string BackendFingerprint string Provider sandbox.SandboxProvider @@ -181,6 +182,9 @@ func (r *runtimeLifecycle) provision(ctx context.Context, tenant, environment, p if providerKey != r.config.InstallationID { return deployment.Allocation{}, sandbox.ErrInvalid } + if r.config.SandboxLink == "" { + return deployment.Allocation{}, deployment.ErrNoLink + } environmentValue, err := r.sessions.GetEnvironment(ctx, tenant, environment) if err != nil { return deployment.Allocation{}, err @@ -209,12 +213,13 @@ func (r *runtimeLifecycle) provision(ctx context.Context, tenant, environment, p } else if err != nil { return deployment.Allocation{}, err } - secret := make([]byte, 32) + secret := make([]byte, 64) if _, err := rand.Read(secret); err != nil { return deployment.Allocation{}, err } - token := hex.EncodeToString(secret) - owner, err := r.deployment.ReserveAllocation(ctx, key, providerKey, runtimedevice.HashCredential(token)) + // The device and Serve credentials; only their digests are stored. + token, serve := hex.EncodeToString(secret[:32]), hex.EncodeToString(secret[32:]) + owner, err := r.deployment.ReserveAllocation(ctx, key, providerKey, runtimedevice.HashCredential(token), runtimedevice.HashCredential(serve)) if err != nil { return owner, err } @@ -227,10 +232,16 @@ func (r *runtimeLifecycle) provision(ctx context.Context, tenant, environment, p if err := r.lease.CheckOwnership(ctx); err != nil { return owner, err } - info, err := provider.Create(ctx, sandbox.Bootstrap{ + bootstrap := sandbox.Bootstrap{ Reference: runtimeReference(owner), SessionID: owner.SessionID, DeviceID: owner.DeviceID, CoreURL: r.config.CoreURL, Credential: token, NetworkAccess: placement.NetworkAccess, AllowedDomains: placement.AllowedDomains, - }) + SandboxIO: sandboxbootstrap.Input{Version: sandboxbootstrap.Version, LinkURL: r.config.SandboxLink, Credential: serve, Resource: serveResource(owner)}, + } + // An invalid input creates nothing: release the allocation as settled absent. + info := sandbox.Info{Reference: bootstrap.Reference, State: "absent", CreateSettled: true} + if err = bootstrap.Validate(); err == nil { + info, err = provider.Create(ctx, bootstrap) + } if info.Reference == runtimeReference(owner) && info.CreateSettled && info.State == "absent" { record, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second) released, releaseErr := r.deployment.ReleaseAbsentCreation(record, owner) @@ -434,11 +445,15 @@ func (r *runtimeLifecycle) requestCleanup(ctx context.Context, owner deployment. if err != nil { return pending, err } - r.links.RevokeResource(sandboxbootstrap.Resource{TenantID: pending.TenantID, EnvironmentID: pending.EnvironmentID, - Kind: "allocation", ID: pending.ID, Generation: pending.ServeGeneration}.Ref()) + r.links.RevokeResource(serveResource(pending).Ref()) return pending, nil } +// serveResource is the allocation's Link resource. +func serveResource(owner deployment.Allocation) sandboxbootstrap.Resource { + return sandboxbootstrap.Resource{TenantID: owner.TenantID, EnvironmentID: owner.EnvironmentID, Kind: "allocation", ID: owner.ID, Generation: owner.ServeGeneration} +} + // Environment identity owns connectivity; preparation has an independent owner. func (r *runtimeLifecycle) clearRuntimeState(owner deployment.Allocation) { delete(r.connections, owner.EnvironmentID) diff --git a/services/core/internal/execution/sandbox_deployment_drain_test.go b/services/core/internal/execution/sandbox_deployment_drain_test.go index 0dd9235d2..723c44a93 100644 --- a/services/core/internal/execution/sandbox_deployment_drain_test.go +++ b/services/core/internal/execution/sandbox_deployment_drain_test.go @@ -93,7 +93,7 @@ func testLifecycleCancellationPreservesLease(t *testing.T, mode string) { hub := node.NewHub(node.HubOptions{}) defer hub.Close() id := uuid.NewString() - configuration := &RuntimeProvider{InstallationID: id, ProviderKind: "docker", Mode: "nodes", Generation: 1, CoreURL: "https://core.example/api/v1", BackendFingerprint: strings.Repeat("a", 64), Provider: hub.Proxy(uuid.NewString(), "docker", docker.Operations(), 1)} + configuration := &RuntimeProvider{InstallationID: id, ProviderKind: "docker", Mode: "nodes", Generation: 1, CoreURL: "https://core.example/api/v1", SandboxLink: "wss://core.example/api/v1/sandbox-link", BackendFingerprint: strings.Repeat("a", 64), Provider: hub.Proxy(uuid.NewString(), "docker", docker.Operations(), 1)} m, err := newRuntimeManager(owner, deployments, reader, nil, runtimegateway.NewRegistry(), relay.New(nil), NewDeferredRuntimeProvider(id, func(context.Context) (*RuntimeProvider, error) { return configuration, nil })) if err != nil { t.Fatal(err) @@ -231,7 +231,7 @@ func TestSandboxDeploymentDrainFailureCannotReactivate(t *testing.T) { hub := node.NewHub(node.HubOptions{}) defer hub.Close() id := uuid.NewString() - configuration := &RuntimeProvider{InstallationID: id, ProviderKind: "docker", Mode: "nodes", Generation: 1, CoreURL: "https://core.example/api/v1", BackendFingerprint: strings.Repeat("a", 64), Provider: hub.Proxy(uuid.NewString(), "docker", docker.Operations(), 1)} + configuration := &RuntimeProvider{InstallationID: id, ProviderKind: "docker", Mode: "nodes", Generation: 1, CoreURL: "https://core.example/api/v1", SandboxLink: "wss://core.example/api/v1/sandbox-link", BackendFingerprint: strings.Repeat("a", 64), Provider: hub.Proxy(uuid.NewString(), "docker", docker.Operations(), 1)} m, err := newRuntimeManager(owner, deployments, reader, nil, runtimegateway.NewRegistry(), relay.New(nil), NewDeferredRuntimeProvider(id, func(context.Context) (*RuntimeProvider, error) { return configuration, nil })) if err != nil { t.Fatal(err) diff --git a/services/core/internal/execution/sandbox_deployment_setup_test.go b/services/core/internal/execution/sandbox_deployment_setup_test.go index 5cf03759c..9b668e10d 100644 --- a/services/core/internal/execution/sandbox_deployment_setup_test.go +++ b/services/core/internal/execution/sandbox_deployment_setup_test.go @@ -25,7 +25,7 @@ func TestDeferredSandboxDeploymentLoadsOnceBeforeNodeCreation(t *testing.T) { id := uuid.NewString() var selected atomic.Bool var loads atomic.Int32 - configuration := &RuntimeProvider{InstallationID: id, ProviderKind: "docker", CoreURL: "https://core.example/api/v1", BackendFingerprint: strings.Repeat("a", 64), Provider: hub.Proxy(uuid.NewString(), "docker", docker.Operations(), 1)} + configuration := &RuntimeProvider{InstallationID: id, ProviderKind: "docker", CoreURL: "https://core.example/api/v1", SandboxLink: "wss://core.example/api/v1/sandbox-link", BackendFingerprint: strings.Repeat("a", 64), Provider: hub.Proxy(uuid.NewString(), "docker", docker.Operations(), 1)} m, err := newRuntimeManager(Owner{Lease: heldLease{}}, nil, nil, nil, runtimegateway.NewRegistry(), relay.New(nil), NewDeferredRuntimeProvider(id, func(context.Context) (*RuntimeProvider, error) { loads.Add(1) if !selected.Load() { @@ -95,7 +95,7 @@ func TestDeferredSandboxProviderFailureKeepsRecoveryAvailable(t *testing.T) { id := uuid.NewString() available := false loadErr := ErrExecutionUnavailable - configuration := &RuntimeProvider{InstallationID: id, ProviderKind: "docker", CoreURL: "https://core.example/api/v1", BackendFingerprint: strings.Repeat("a", 64), Provider: hub.Proxy(uuid.NewString(), "docker", docker.Operations(), 1)} + configuration := &RuntimeProvider{InstallationID: id, ProviderKind: "docker", CoreURL: "https://core.example/api/v1", SandboxLink: "wss://core.example/api/v1/sandbox-link", BackendFingerprint: strings.Repeat("a", 64), Provider: hub.Proxy(uuid.NewString(), "docker", docker.Operations(), 1)} m, err := newRuntimeManager(Owner{Lease: heldLease{}}, nil, nil, nil, runtimegateway.NewRegistry(), relay.New(nil), NewDeferredRuntimeProvider(id, func(context.Context) (*RuntimeProvider, error) { if !available { return nil, loadErr @@ -126,7 +126,7 @@ func TestRejectedSandboxCandidatePreservesActiveGeneration(t *testing.T) { hub := node.NewHub(node.HubOptions{}) defer hub.Close() id := uuid.NewString() - config := &RuntimeProvider{InstallationID: id, ProviderKind: "docker", Mode: "nodes", Generation: 1, CoreURL: "https://core.example/api/v1", BackendFingerprint: strings.Repeat("a", 64), Provider: hub.Proxy(uuid.NewString(), "docker", docker.Operations(), 1)} + config := &RuntimeProvider{InstallationID: id, ProviderKind: "docker", Mode: "nodes", Generation: 1, CoreURL: "https://core.example/api/v1", SandboxLink: "wss://core.example/api/v1/sandbox-link", BackendFingerprint: strings.Repeat("a", 64), Provider: hub.Proxy(uuid.NewString(), "docker", docker.Operations(), 1)} rejected := errors.New("candidate provider unavailable") m, err := newRuntimeManager(Owner{Lease: heldLease{}}, unitDeploymentService(t), nil, nil, runtimegateway.NewRegistry(), relay.New(nil), NewDeferredRuntimeProvider(id, func(context.Context) (*RuntimeProvider, error) { return config, nil }, func(context.Context, deployment.Setup) (PreparedRuntimeDeployment, error) { @@ -200,7 +200,7 @@ func TestCommittedSandboxCandidatePublishesAfterShutdown(t *testing.T) { if err := m.pauseDeployment(t.Context()); err != nil { t.Fatal(err) } - config := &RuntimeProvider{InstallationID: id, ProviderKind: "e2b", Mode: "direct", CoreURL: "https://core.example/api/v1", BackendFingerprint: strings.Repeat("b", 64), Provider: hub.Proxy(uuid.NewString(), "docker", docker.Operations(), 1)} + config := &RuntimeProvider{InstallationID: id, ProviderKind: "e2b", Mode: "direct", CoreURL: "https://core.example/api/v1", SandboxLink: "wss://core.example/api/v1/sandbox-link", BackendFingerprint: strings.Repeat("b", 64), Provider: hub.Proxy(uuid.NewString(), "docker", docker.Operations(), 1)} var published *RuntimeProvider candidate := PreparedRuntimeDeployment{Config: config, Publish: func(value *RuntimeProvider) { published = value }} m.stop() diff --git a/services/core/internal/execution/sandbox_deployment_switch_test.go b/services/core/internal/execution/sandbox_deployment_switch_test.go index a0d63173c..badabc17b 100644 --- a/services/core/internal/execution/sandbox_deployment_switch_test.go +++ b/services/core/internal/execution/sandbox_deployment_switch_test.go @@ -19,7 +19,7 @@ func TestSandboxManagerSwitchDrainsBeforeDirectActivation(t *testing.T) { hub := node.NewHub(node.HubOptions{}) defer hub.Close() id := uuid.NewString() - config := &RuntimeProvider{InstallationID: id, ProviderKind: "docker", Mode: "nodes", Generation: 1, CoreURL: "https://core.example/api/v1", BackendFingerprint: strings.Repeat("a", 64), Provider: hub.Proxy(uuid.NewString(), "docker", docker.Operations(), 1)} + config := &RuntimeProvider{InstallationID: id, ProviderKind: "docker", Mode: "nodes", Generation: 1, CoreURL: "https://core.example/api/v1", SandboxLink: "wss://core.example/api/v1/sandbox-link", BackendFingerprint: strings.Repeat("a", 64), Provider: hub.Proxy(uuid.NewString(), "docker", docker.Operations(), 1)} m, err := newRuntimeManager(Owner{Lease: heldLease{}}, nil, nil, nil, runtimegateway.NewRegistry(), relay.New(nil), NewDeferredRuntimeProvider(id, func(context.Context) (*RuntimeProvider, error) { return config, nil })) if err != nil { t.Fatal(err) @@ -63,7 +63,7 @@ func TestSandboxManagerSwitchDrainsBeforeDirectActivation(t *testing.T) { case <-time.After(time.Second): t.Fatal("switch drain blocked") } - config = &RuntimeProvider{InstallationID: id, ProviderKind: "e2b", Mode: "direct", Generation: 2, CoreURL: "https://core.example/api/v1", BackendFingerprint: strings.Repeat("b", 64), Provider: hub.Proxy(uuid.NewString(), "docker", docker.Operations(), 1)} + config = &RuntimeProvider{InstallationID: id, ProviderKind: "e2b", Mode: "direct", Generation: 2, CoreURL: "https://core.example/api/v1", SandboxLink: "wss://core.example/api/v1/sandbox-link", BackendFingerprint: strings.Repeat("b", 64), Provider: hub.Proxy(uuid.NewString(), "docker", docker.Operations(), 1)} if err := m.activateDeployment(t.Context(), deployment.View{InstallationID: id, Generation: 2, Mode: "direct", Provider: "e2b"}); err != nil { t.Fatal(err) } @@ -101,7 +101,7 @@ func TestSandboxManagerCancelledSwitchCannotResumeBeforeDrain(t *testing.T) { hub := node.NewHub(node.HubOptions{}) defer hub.Close() id := uuid.NewString() - config := &RuntimeProvider{InstallationID: id, ProviderKind: "docker", Mode: "nodes", Generation: 1, CoreURL: "https://core.example/api/v1", BackendFingerprint: strings.Repeat("a", 64), Provider: hub.Proxy(uuid.NewString(), "docker", docker.Operations(), 1)} + config := &RuntimeProvider{InstallationID: id, ProviderKind: "docker", Mode: "nodes", Generation: 1, CoreURL: "https://core.example/api/v1", SandboxLink: "wss://core.example/api/v1/sandbox-link", BackendFingerprint: strings.Repeat("a", 64), Provider: hub.Proxy(uuid.NewString(), "docker", docker.Operations(), 1)} m, err := newRuntimeManager(Owner{Lease: heldLease{}}, nil, nil, nil, runtimegateway.NewRegistry(), relay.New(nil), NewDeferredRuntimeProvider(id, func(context.Context) (*RuntimeProvider, error) { return config, nil })) if err != nil { t.Fatal(err) @@ -130,7 +130,7 @@ func TestSandboxManagerCancelledSwitchCannotResumeBeforeDrain(t *testing.T) { m.mu.Lock() originalDrain := m.switchDrained m.mu.Unlock() - config = &RuntimeProvider{InstallationID: id, ProviderKind: "e2b", Mode: "direct", Generation: 2, CoreURL: "https://core.example/api/v1", BackendFingerprint: strings.Repeat("b", 64), Provider: hub.Proxy(uuid.NewString(), "docker", docker.Operations(), 1)} + config = &RuntimeProvider{InstallationID: id, ProviderKind: "e2b", Mode: "direct", Generation: 2, CoreURL: "https://core.example/api/v1", SandboxLink: "wss://core.example/api/v1/sandbox-link", BackendFingerprint: strings.Repeat("b", 64), Provider: hub.Proxy(uuid.NewString(), "docker", docker.Operations(), 1)} expected := deployment.View{InstallationID: id, Generation: 2, Mode: "direct", Provider: "e2b"} ctx, cancel = context.WithTimeout(t.Context(), 10*time.Millisecond) if err := m.activateDeployment(ctx, expected); !errors.Is(err, context.DeadlineExceeded) { @@ -159,7 +159,7 @@ func TestSandboxActivationCannotBypassOutstandingDrain(t *testing.T) { m, err := newRuntimeManager(Owner{Lease: heldLease{}}, nil, nil, nil, runtimegateway.NewRegistry(), relay.New(nil), NewDeferredRuntimeProvider(id, func(context.Context) (*RuntimeProvider, error) { return nil, nil }, func(_ context.Context, setup deployment.Setup) (PreparedRuntimeDeployment, error) { - return PreparedRuntimeDeployment{Config: &RuntimeProvider{InstallationID: setup.InstallationID, ProviderKind: setup.Provider, Mode: setup.Mode, CoreURL: "https://core.example/api/v1", BackendFingerprint: setup.BackendFingerprint, Provider: hub.Proxy(uuid.NewString(), "docker", docker.Operations(), 1)}}, nil + return PreparedRuntimeDeployment{Config: &RuntimeProvider{InstallationID: setup.InstallationID, ProviderKind: setup.Provider, Mode: setup.Mode, CoreURL: "https://core.example/api/v1", SandboxLink: "wss://core.example/api/v1/sandbox-link", BackendFingerprint: setup.BackendFingerprint, Provider: hub.Proxy(uuid.NewString(), "docker", docker.Operations(), 1)}}, nil })) if err != nil { t.Fatal(err) diff --git a/services/core/internal/execution/sandbox_generations_test.go b/services/core/internal/execution/sandbox_generations_test.go index c8927d983..34d6582f1 100644 --- a/services/core/internal/execution/sandbox_generations_test.go +++ b/services/core/internal/execution/sandbox_generations_test.go @@ -39,9 +39,9 @@ func TestE2BReplacementVerifiesTwiceAndNeverPublishesFailedCommit(t *testing.T) if err != nil { return nil, err } - return &RuntimeProvider{InstallationID: id, ProviderKind: "e2b", Mode: "direct", Generation: setup.Generation, CoreURL: "https://core.example/api/v1", BackendFingerprint: setup.BackendFingerprint, Provider: provider}, nil + return &RuntimeProvider{InstallationID: id, ProviderKind: "e2b", Mode: "direct", Generation: setup.Generation, CoreURL: "https://core.example/api/v1", SandboxLink: "wss://core.example/api/v1/sandbox-link", BackendFingerprint: setup.BackendFingerprint, Provider: provider}, nil }, func(ctx context.Context, setup deployment.Setup) (PreparedRuntimeDeployment, error) { - return PreparedRuntimeDeployment{Config: &RuntimeProvider{InstallationID: id, ProviderKind: "e2b", Mode: "direct", CoreURL: "https://core.example/api/v1", BackendFingerprint: setup.BackendFingerprint, Provider: provider}, + return PreparedRuntimeDeployment{Config: &RuntimeProvider{InstallationID: id, ProviderKind: "e2b", Mode: "direct", CoreURL: "https://core.example/api/v1", SandboxLink: "wss://core.example/api/v1/sandbox-link", BackendFingerprint: setup.BackendFingerprint, Provider: provider}, VerifyCredential: func(context.Context) error { verifyCalls++ if verifyCalls == rejectAt { diff --git a/services/core/internal/execution/sandbox_provider_contract_test.go b/services/core/internal/execution/sandbox_provider_contract_test.go index 33de667df..d2bc6b687 100644 --- a/services/core/internal/execution/sandbox_provider_contract_test.go +++ b/services/core/internal/execution/sandbox_provider_contract_test.go @@ -18,7 +18,7 @@ func TestSandboxProviderRegistrationDoesNotRequireAnExecutionVendorBranch(t *tes t.Run(mode, func(t *testing.T) { id := uuid.NewString() config := &RuntimeProvider{InstallationID: id, ProviderKind: "contract-fixture", Mode: mode, - CoreURL: "https://core.example/api/v1", BackendFingerprint: strings.Repeat("a", 64), Provider: &lifecycleOnlySandbox{}} + CoreURL: "https://core.example/api/v1", SandboxLink: "wss://core.example/api/v1/sandbox-link", BackendFingerprint: strings.Repeat("a", 64), Provider: &lifecycleOnlySandbox{}} m, err := newRuntimeManager(Owner{Lease: heldLease{}}, nil, nil, nil, runtimegateway.NewRegistry(), relay.New(nil), NewDeferredRuntimeProvider(id, func(context.Context) (*RuntimeProvider, error) { return config, nil })) if err != nil { t.Fatal(err) diff --git a/services/core/internal/execution/sandbox_reset_test.go b/services/core/internal/execution/sandbox_reset_test.go index f4a174314..875da48d3 100644 --- a/services/core/internal/execution/sandbox_reset_test.go +++ b/services/core/internal/execution/sandbox_reset_test.go @@ -101,7 +101,7 @@ func TestSandboxResetPageTimeoutRecoversCommittedOwner(t *testing.T) { if err != nil || setup.Provider == "" { return nil, err } - return &RuntimeProvider{InstallationID: id, ProviderKind: setup.Provider, Mode: setup.Mode, Generation: setup.Generation, CoreURL: "https://core.example/api/v1", BackendFingerprint: setup.BackendFingerprint, Provider: hub.Proxy(uuid.NewString(), "docker", docker.Operations(), 1)}, nil + return &RuntimeProvider{InstallationID: id, ProviderKind: setup.Provider, Mode: setup.Mode, Generation: setup.Generation, CoreURL: "https://core.example/api/v1", SandboxLink: "wss://core.example/api/v1/sandbox-link", BackendFingerprint: setup.BackendFingerprint, Provider: hub.Proxy(uuid.NewString(), "docker", docker.Operations(), 1)}, nil }) m, err := newRuntimeManager(owner, deployments, reader, nil, runtimegateway.NewRegistry(), relay.New(nil), config) if err != nil { @@ -215,7 +215,7 @@ func TestSandboxResetPublishesCommittedGenerationWithoutReading(t *testing.T) { if err != nil || setup.Provider == "" { return nil, err } - return &RuntimeProvider{InstallationID: id, ProviderKind: setup.Provider, Mode: setup.Mode, Generation: setup.Generation, CoreURL: "https://core.example/api/v1", BackendFingerprint: setup.BackendFingerprint, Provider: hub.Proxy(uuid.NewString(), "docker", docker.Operations(), 1)}, nil + return &RuntimeProvider{InstallationID: id, ProviderKind: setup.Provider, Mode: setup.Mode, Generation: setup.Generation, CoreURL: "https://core.example/api/v1", SandboxLink: "wss://core.example/api/v1/sandbox-link", BackendFingerprint: setup.BackendFingerprint, Provider: hub.Proxy(uuid.NewString(), "docker", docker.Operations(), 1)}, nil }) var published []uint64 config.PublishUnconfigured = func(generation uint64) { diff --git a/services/core/internal/execution/sandbox_snapshot_budget_test.go b/services/core/internal/execution/sandbox_snapshot_budget_test.go index bcdaf4d5b..56dcd8751 100644 --- a/services/core/internal/execution/sandbox_snapshot_budget_test.go +++ b/services/core/internal/execution/sandbox_snapshot_budget_test.go @@ -73,7 +73,7 @@ func TestSandboxResetSnapshotFitsPageBudget(t *testing.T) { if err != nil || setup.Provider == "" { return nil, err } - return &RuntimeProvider{InstallationID: id, ProviderKind: setup.Provider, Generation: setup.Generation, Mode: setup.Mode, CoreURL: "https://core.example/api/v1", BackendFingerprint: setup.BackendFingerprint, Provider: hub.Proxy(uuid.NewString(), "docker", docker.Operations(), 1)}, nil + return &RuntimeProvider{InstallationID: id, ProviderKind: setup.Provider, Generation: setup.Generation, Mode: setup.Mode, CoreURL: "https://core.example/api/v1", SandboxLink: "wss://core.example/api/v1/sandbox-link", BackendFingerprint: setup.BackendFingerprint, Provider: hub.Proxy(uuid.NewString(), "docker", docker.Operations(), 1)}, nil }) m, err := newRuntimeManager(owner, deployments, reader, nil, runtimegateway.NewRegistry(), relay.New(nil), configuration) if err != nil { diff --git a/services/core/internal/persistence/postgres/deploymentpg/allocations.go b/services/core/internal/persistence/postgres/deploymentpg/allocations.go index 2c9550d7e..9d65bd003 100644 --- a/services/core/internal/persistence/postgres/deploymentpg/allocations.go +++ b/services/core/internal/persistence/postgres/deploymentpg/allocations.go @@ -217,6 +217,7 @@ func (t *reservationTx) InsertAllocation(a deployment.NewAllocation) (deployment row, err := t.q.CreateRuntimeAllocation(t.ctx, sqlc.CreateRuntimeAllocationParams{ ID: id, EnvironmentID: t.environment, DeviceID: device, ProviderKey: provider, NodeID: node, DeploymentGeneration: pgtype.Int8{Int64: int64(a.Generation), Valid: true}, + ServeCredentialHash: pgtype.Text{String: a.ServeCredentialHash, Valid: true}, }) if err != nil { return deployment.Allocation{}, err diff --git a/services/core/internal/persistence/postgres/deploymentpg/allocations_test.go b/services/core/internal/persistence/postgres/deploymentpg/allocations_test.go index 0fed81384..1496d60a8 100644 --- a/services/core/internal/persistence/postgres/deploymentpg/allocations_test.go +++ b/services/core/internal/persistence/postgres/deploymentpg/allocations_test.go @@ -67,7 +67,7 @@ func TestConcurrentReservationsCommitOneAllocation(t *testing.T) { var wg sync.WaitGroup for i := range results { wg.Go(func() { - results[i], errs[i] = changes.ReserveAllocation(t.Context(), key, installation, credentialHash()) + results[i], errs[i] = changes.ReserveAllocation(t.Context(), key, installation, credentialHash(), credentialHash()) }) } wg.Wait() @@ -87,7 +87,7 @@ func TestConcurrentReservationsCommitOneAllocation(t *testing.T) { if allocations, state, _, _, _ := allocationRows(t, f, key); fresh != 1 || allocations != 1 || devices != 1 || state != "creating" { t.Fatal("reservations committed more than one allocation", fresh, allocations, devices, state) } - if _, err := changes.ReserveAllocation(t.Context(), key, uuid.NewString(), credentialHash()); !errors.Is(err, deployment.ErrAllocationConflict) { + if _, err := changes.ReserveAllocation(t.Context(), key, uuid.NewString(), credentialHash(), credentialHash()); !errors.Is(err, deployment.ErrAllocationConflict) { t.Fatal("another installation replayed the allocation", err) } } @@ -100,12 +100,12 @@ func TestAllocationWritesNeedTheLease(t *testing.T) { changes, _ := f.execution(t) installation, _ := f.initialize(t, changes, setupE2BSelection()) key := hostedEnvironment(t, f.pool) - owner, err := changes.ReserveAllocation(t.Context(), key, installation, credentialHash()) + owner, err := changes.ReserveAllocation(t.Context(), key, installation, credentialHash(), credentialHash()) if err != nil { t.Fatal(err) } unallocated := hostedEnvironment(t, f.pool) - if _, err := closed.ReserveAllocation(t.Context(), unallocated, installation, credentialHash()); !errors.Is(err, pgunit.ErrLeaseClosed) { + if _, err := closed.ReserveAllocation(t.Context(), unallocated, installation, credentialHash(), credentialHash()); !errors.Is(err, pgunit.ErrLeaseClosed) { t.Fatal("reserved without the lease", err) } if allocations, _, _, _, _ := allocationRows(t, f, unallocated); allocations != 0 { @@ -138,7 +138,7 @@ func TestFailedCleanupSettlementRollsBackRevocation(t *testing.T) { changes, _ := f.execution(t) installation, _ := f.initialize(t, changes, setupE2BSelection()) key := hostedEnvironment(t, f.pool) - owner, err := changes.ReserveAllocation(t.Context(), key, installation, credentialHash()) + owner, err := changes.ReserveAllocation(t.Context(), key, installation, credentialHash(), credentialHash()) if err != nil { t.Fatal(err) } @@ -175,7 +175,7 @@ func TestFailedPruneRollsBackTheAllocationWrite(t *testing.T) { changes, _ := f.execution(t) installation, _ := f.initialize(t, changes, setupE2BSelection()) key := hostedEnvironment(t, f.pool) - owner, err := changes.ReserveAllocation(t.Context(), key, installation, credentialHash()) + owner, err := changes.ReserveAllocation(t.Context(), key, installation, credentialHash(), credentialHash()) if err != nil { t.Fatal(err) } @@ -190,7 +190,7 @@ func TestFailedPruneRollsBackTheAllocationWrite(t *testing.T) { t.Fatal("a failed prune kept the change", state) } unallocated := hostedEnvironment(t, f.pool) - if _, err := changes.ReserveAllocation(t.Context(), unallocated, installation, credentialHash()); err == nil || !strings.Contains(err.Error(), "injected prune failure") { + if _, err := changes.ReserveAllocation(t.Context(), unallocated, installation, credentialHash(), credentialHash()); err == nil || !strings.Contains(err.Error(), "injected prune failure") { t.Fatal("a reservation committed without the prune", err) } var devices int diff --git a/services/core/internal/sandbox/contracttest/provider.go b/services/core/internal/sandbox/contracttest/provider.go index e72d8b13d..e94eb63ec 100644 --- a/services/core/internal/sandbox/contracttest/provider.go +++ b/services/core/internal/sandbox/contracttest/provider.go @@ -5,11 +5,15 @@ package contracttest import ( "context" "errors" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/providercontract" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" "reflect" "testing" "time" + + "github.com/google/uuid" + + "github.com/MiniMax-AI/OpenAgentCore/internal/sandboxbootstrap" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/providercontract" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" ) type Fault string @@ -36,6 +40,13 @@ type Fixture struct { WantCalls []string } +// Bootstrap returns a valid Create input for r. +func Bootstrap(r sandbox.Reference) sandbox.Bootstrap { + return sandbox.Bootstrap{Reference: r, SessionID: uuid.NewString(), DeviceID: uuid.NewString(), CoreURL: "https://core.example/api/v1", Credential: "synthetic", NetworkAccess: "enabled", + SandboxIO: sandboxbootstrap.Input{Version: sandboxbootstrap.Version, LinkURL: "wss://core.example/api/v1/sandbox-link", Credential: "synthetic-serve", + Resource: sandboxbootstrap.Resource{TenantID: r.TenantID, EnvironmentID: r.EnvironmentID, Kind: "allocation", ID: r.AllocationID, Generation: 1}}} +} + // Factory configures native responses. Invoke cancel only after dispatch begins. type Factory func(t *testing.T, scenario Scenario, cancel context.CancelFunc) Fixture @@ -56,6 +67,9 @@ func RunFailures(t *testing.T, factory Factory) { if err := sandbox.ValidateProvider(f.Provider); err != nil { t.Fatal(err) } + if err := f.Bootstrap.Validate(); err != nil { + t.Fatal("fixture Bootstrap is not a valid Create input", err) + } var info sandbox.Info var err error switch operation { diff --git a/services/core/internal/sandbox/docker/bootstrap.go b/services/core/internal/sandbox/docker/bootstrap.go index 3fee133a4..eb2cfde95 100644 --- a/services/core/internal/sandbox/docker/bootstrap.go +++ b/services/core/internal/sandbox/docker/bootstrap.go @@ -17,14 +17,20 @@ type entry struct { } func (p *Provider) bootstrap(ctx context.Context, id string, b sandbox.Bootstrap) error { - // Deliver the Runtime-owned connection contract before native work can start. + // Deliver the daemon's and the Sandbox I/O service's launch inputs before + // native work can start. auth, e := b.RuntimeConnection().Marshal() if e != nil { return e } + serve, e := b.SandboxIO.Marshal() + if e != nil { + return e + } if e = copyRuntimeFiles(ctx, p.client, id, "/home", []entry{ {name: "runtime", directory: true}, {name: "runtime/.oac", directory: true}, {name: "runtime/runtime-bootstrap.json", content: auth}, + {name: "runtime/sandbox-io-bootstrap.json", content: serve}, }); e != nil { return e } diff --git a/services/core/internal/sandbox/docker/bootstrap_test.go b/services/core/internal/sandbox/docker/bootstrap_test.go index 31d2845ff..56744ed08 100644 --- a/services/core/internal/sandbox/docker/bootstrap_test.go +++ b/services/core/internal/sandbox/docker/bootstrap_test.go @@ -9,13 +9,16 @@ import ( "testing" "github.com/MiniMax-AI/OpenAgentCore/internal/runtimebootstrap" + "github.com/MiniMax-AI/OpenAgentCore/internal/sandboxbootstrap" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/contracttest" + "github.com/google/uuid" "github.com/moby/moby/client" ) -func TestBootstrapDeliversOnlyPublicConnectionInput(t *testing.T) { - b := sandbox.Bootstrap{CoreURL: "https://core.example/api/v1", DeviceID: "da912024-1543-4242-a2c1-5f4f7ebbc6c7", Credential: "test-secret"} - found := false +func TestBootstrapDeliversOnlyPublicLaunchInputs(t *testing.T) { + b := contracttest.Bootstrap(sandbox.Reference{TenantID: uuid.NewString(), EnvironmentID: uuid.NewString(), AllocationID: uuid.NewString()}) + found := map[string]bool{} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != "PUT" || !strings.HasSuffix(r.URL.Path, "/containers/test/archive") { t.Errorf("unexpected Docker operation %s", r.URL.Path) @@ -33,16 +36,23 @@ func TestBootstrapDeliversOnlyPublicConnectionInput(t *testing.T) { if strings.Contains(h.Name, "auth.json") { t.Error("provider wrote Runtime private storage") } + if h.Name != "runtime/runtime-bootstrap.json" && h.Name != "runtime/sandbox-io-bootstrap.json" { + continue + } + found[h.Name] = true + raw, err := io.ReadAll(tr) + if err != nil { + t.Error(err) + } + if h.Mode != 0600 || h.Uid != 1000 || h.Gid != 1000 { + t.Error("launch input permissions", h.Name) + } if h.Name == "runtime/runtime-bootstrap.json" { - found = true - raw, err := io.ReadAll(tr) - if err != nil { - t.Error(err) - } - c, err := runtimebootstrap.Decode(raw) - if err != nil || c != b.RuntimeConnection() || h.Mode != 0600 || h.Uid != 1000 || h.Gid != 1000 { - t.Error("invalid launch input or permissions") + if c, err := runtimebootstrap.Decode(raw); err != nil || c != b.RuntimeConnection() { + t.Error("invalid Runtime launch input") } + } else if in, err := sandboxbootstrap.Decode(raw); err != nil || in != b.SandboxIO { + t.Error("invalid Sandbox I/O launch input") } } w.WriteHeader(200) @@ -56,7 +66,7 @@ func TestBootstrapDeliversOnlyPublicConnectionInput(t *testing.T) { if err := (&Provider{client: c}).bootstrap(t.Context(), "test", b); err != nil { t.Fatal(err) } - if !found { - t.Fatal("missing Runtime input") + if len(found) != 2 { + t.Fatal("missing launch input", found) } } diff --git a/services/core/internal/sandbox/docker/contract_test.go b/services/core/internal/sandbox/docker/contract_test.go index 09db3fef8..ed359916e 100644 --- a/services/core/internal/sandbox/docker/contract_test.go +++ b/services/core/internal/sandbox/docker/contract_test.go @@ -26,7 +26,7 @@ type contractStep struct { func dockerContractFixture(t *testing.T, cancel context.CancelFunc, script func(*Provider, sandbox.Reference) []contractStep) contracttest.Fixture { t.Helper() - b := sandbox.Bootstrap{Reference: sandbox.Reference{TenantID: uuid.NewString(), EnvironmentID: uuid.NewString(), AllocationID: uuid.NewString()}, SessionID: uuid.NewString(), DeviceID: uuid.NewString(), CoreURL: "https://core.example/api/v1", Credential: "synthetic", NetworkAccess: "enabled"} + b := contracttest.Bootstrap(sandbox.Reference{TenantID: uuid.NewString(), EnvironmentID: uuid.NewString(), AllocationID: uuid.NewString()}) var mu sync.Mutex var calls []string var steps []contractStep diff --git a/services/core/internal/sandbox/docker/provider.go b/services/core/internal/sandbox/docker/provider.go index 474c0d59d..8cfb8dfde 100644 --- a/services/core/internal/sandbox/docker/provider.go +++ b/services/core/internal/sandbox/docker/provider.go @@ -113,9 +113,6 @@ func (p *Provider) Renew(ctx context.Context, r sandbox.Reference) (sandbox.Info func (p *Provider) Create(ctx context.Context, b sandbox.Bootstrap) (sandbox.Info, error) { info := sandbox.Info{Reference: b.Reference} policy := agentnetwork.Policy{Access: b.NetworkAccess, AllowedDomains: b.AllowedDomains} - if policy.Validate() != nil || !validReference(b.Reference) || !validID(b.SessionID) || !validID(b.DeviceID) || b.RuntimeConnection().Validate() != nil { - return info, sandbox.ErrInvalid - } if existing, e := p.GetInfo(ctx, b.Reference); e == nil { return existing, sandbox.ErrExists } else if !errors.Is(e, sandbox.ErrNotFound) { @@ -180,6 +177,14 @@ func (p *Provider) Create(ctx context.Context, b sandbox.Bootstrap) (sandbox.Inf if _, e = p.client.ContainerStart(ctx, v.ID, client.ContainerStartOptions{}); e != nil { return info, e } + // The Sandbox I/O service runs beside the daemon, as the same account. + exec, e := p.client.ExecCreate(ctx, v.ID, client.ExecCreateOptions{User: "1000:1000", Cmd: []string{"/usr/local/bin/oac-sandbox-io", "--bootstrap-file", "/home/runtime/sandbox-io-bootstrap.json"}}) + if e != nil { + return info, e + } + if _, e = p.client.ExecStart(ctx, exec.ID, client.ExecStartOptions{Detach: true}); e != nil { + return info, e + } return p.GetInfo(ctx, b.Reference) } diff --git a/services/core/internal/sandbox/docker/provider_test.go b/services/core/internal/sandbox/docker/provider_test.go index a7f71289b..aa0e2ecb8 100644 --- a/services/core/internal/sandbox/docker/provider_test.go +++ b/services/core/internal/sandbox/docker/provider_test.go @@ -12,6 +12,7 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/internal/runtimebootstrap" + "github.com/MiniMax-AI/OpenAgentCore/internal/sandboxbootstrap" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimeobs" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/contracttest" @@ -36,22 +37,6 @@ func TestProviderRejectsUnsafeOperatorConfiguration(t *testing.T) { } } -func TestBootstrapRequiresCompleteNetworkPolicyBeforeDockerEffects(t *testing.T) { - p := &Provider{} - for _, policy := range []sandbox.Bootstrap{ - {}, {NetworkAccess: "restricted"}, - {NetworkAccess: "restricted", AllowedDomains: []string{"*.example.com"}}, - {NetworkAccess: "enabled", AllowedDomains: []string{"example.com"}}, - } { - policy.Reference = sandbox.Reference{TenantID: uuid.NewString(), EnvironmentID: uuid.NewString(), AllocationID: uuid.NewString()} - policy.SessionID, policy.DeviceID = uuid.NewString(), uuid.NewString() - policy.CoreURL, policy.Credential = "http://core.invalid/api/v1", "synthetic" - if _, err := p.Create(t.Context(), policy); !errors.Is(err, sandbox.ErrInvalid) { - t.Fatal("invalid bootstrap reached Docker", err) - } - } -} - // This optional Docker mechanism test uses a pinned fixture image whose entrypoint // is sleep. It is not native/model acceptance; the real Runtime has separate checks. func TestDockerProviderLifecycle(t *testing.T) { @@ -76,7 +61,9 @@ func TestDockerProviderLifecycle(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) defer cancel() bootstrap := func() sandbox.Bootstrap { - return sandbox.Bootstrap{Reference: sandbox.Reference{TenantID: uuid.NewString(), EnvironmentID: uuid.NewString(), AllocationID: uuid.NewString()}, SessionID: uuid.NewString(), DeviceID: uuid.NewString(), CoreURL: "http://core.invalid/api/v1", Credential: "synthetic-test-credential", NetworkAccess: "enabled"} + b := contracttest.Bootstrap(sandbox.Reference{TenantID: uuid.NewString(), EnvironmentID: uuid.NewString(), AllocationID: uuid.NewString()}) + b.Credential, b.SandboxIO.Credential = "synthetic-test-credential", "synthetic-serve-credential" + return b } b := bootstrap() b.NetworkAccess, b.AllowedDomains = "restricted", []string{"Example.com", "api.example.com"} @@ -120,7 +107,7 @@ func TestDockerProviderLifecycle(t *testing.T) { if e != nil { t.Fatal(e) } - if strings.Contains(string(inspected.Raw), b.Credential) || inspected.Container.Config.User != "1000:1000" || !inspected.Container.HostConfig.ReadonlyRootfs || inspected.Container.HostConfig.Privileged { + if strings.Contains(string(inspected.Raw), b.Credential) || strings.Contains(string(inspected.Raw), b.SandboxIO.Credential) || inspected.Container.Config.User != "1000:1000" || !inspected.Container.HostConfig.ReadonlyRootfs || inspected.Container.HostConfig.Privileged { t.Fatal("unsafe Docker configuration") } for _, value := range []string{"OAC_RUNTIME_NETWORK_ACCESS=restricted", `OAC_RUNTIME_ALLOWED_DOMAINS=["api.example.com","example.com"]`} { @@ -145,6 +132,10 @@ func TestDockerProviderLifecycle(t *testing.T) { if decodeErr != nil || auth.Credential != b.Credential || auth.DeviceID != b.DeviceID { t.Fatal("bootstrap changed or malformed") } + r, e = p.RunCommand(ctx, b.Reference, sandbox.Command{Args: []string{"cat", "/home/runtime/sandbox-io-bootstrap.json"}}) + if serve, decodeErr := sandboxbootstrap.Decode([]byte(r.Stdout)); e != nil || decodeErr != nil || serve != b.SandboxIO { + t.Fatal("Sandbox I/O bootstrap changed or malformed", e) + } r, e = p.RunCommand(ctx, b.Reference, sandbox.Command{Args: []string{"sh", "-c", "printf retained > /environment/workspace/history; printf failed >&2; exit 7"}}) if e != nil || r.ExitCode != 7 || r.Stderr != "failed" { t.Fatalf("lost command status: %+v %v", r, e) diff --git a/services/core/internal/sandbox/docker/recovery_test.go b/services/core/internal/sandbox/docker/recovery_test.go index 06ce2e8ae..68fb54447 100644 --- a/services/core/internal/sandbox/docker/recovery_test.go +++ b/services/core/internal/sandbox/docker/recovery_test.go @@ -18,6 +18,7 @@ import ( "github.com/moby/moby/client" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/contracttest" ) // These controlled transport faults use real Docker compute and volumes. They @@ -87,7 +88,7 @@ func TestDockerProviderRecoveryObservations(t *testing.T) { if e != nil { t.Fatal(e) } - b := sandbox.Bootstrap{Reference: sandbox.Reference{TenantID: uuid.NewString(), EnvironmentID: uuid.NewString(), AllocationID: uuid.NewString()}, SessionID: uuid.NewString(), DeviceID: uuid.NewString(), CoreURL: "http://core.invalid/api/v1", Credential: "synthetic-recovery-token", NetworkAccess: "enabled"} + b := contracttest.Bootstrap(sandbox.Reference{TenantID: uuid.NewString(), EnvironmentID: uuid.NewString(), AllocationID: uuid.NewString()}) direct, e := client.New(client.WithHost("unix:///var/run/docker.sock")) if e != nil { t.Fatal(e) diff --git a/services/core/internal/sandbox/docker/serve_test.go b/services/core/internal/sandbox/docker/serve_test.go new file mode 100644 index 000000000..3e32f3deb --- /dev/null +++ b/services/core/internal/sandbox/docker/serve_test.go @@ -0,0 +1,165 @@ +package docker + +import ( + "archive/tar" + "bytes" + "context" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "errors" + "net" + "net/http/httptest" + "os" + "slices" + "strconv" + "testing" + "time" + + "github.com/google/uuid" + "github.com/moby/moby/api/types/container" + "github.com/moby/moby/client" + + "github.com/MiniMax-AI/OpenAgentCore/internal/sandboxfs" + "github.com/MiniMax-AI/OpenAgentCore/internal/sandboxlink" + "github.com/MiniMax-AI/OpenAgentCore/internal/sandboxlink/relay" + "github.com/MiniMax-AI/OpenAgentCore/internal/sandboxlink/sandboxlinktest" + "github.com/MiniMax-AI/OpenAgentCore/internal/sandboxwire" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/contracttest" +) + +// A real Docker sandbox Serves its allocation over the Link: an Attach from a +// test agent host lists / through File. +func TestDockerSandboxServesItsAllocation(t *testing.T) { + base := os.Getenv("AGENTS_RUNTIME_DOCKER_TEST_IMAGE") + if base == "" { + t.Skip("explicit Docker fixture image required") + } + seccomp, err := os.ReadFile("../../../deploy/codex/seccomp.json") + if err != nil { + t.Fatal(err) + } + c, err := client.New(client.FromEnv) + if err != nil { + t.Fatal(err) + } + defer c.Close() + authority := sandboxlinktest.NewAuthority() + links := relay.New(authority) + t.Cleanup(func() { links.Close() }) + // The sandbox reaches the relay through the host gateway as example.com, + // a name the test certificate carries; the image trusts that certificate. + server := httptest.NewUnstartedServer(links) + listener, err := net.Listen("tcp", "0.0.0.0:0") + if err != nil { + t.Fatal(err) + } + server.Listener.Close() + server.Listener = listener + server.StartTLS() + t.Cleanup(server.Close) + port := strconv.Itoa(listener.Addr().(*net.TCPAddr).Port) + // Derive a labeled image whose only trust anchor is the test certificate. + ca := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: server.Certificate().Raw}) + var content bytes.Buffer + archive := tar.NewWriter(&content) + if err := archive.WriteHeader(&tar.Header{Name: "etc/ssl/certs/ca-certificates.crt", Mode: 0o644, Size: int64(len(ca)), Typeflag: tar.TypeReg}); err != nil { + t.Fatal(err) + } + if _, err := archive.Write(ca); err != nil { + t.Fatal(err) + } + if err := archive.Close(); err != nil { + t.Fatal(err) + } + created, err := c.ContainerCreate(t.Context(), client.ContainerCreateOptions{Image: base, Config: &container.Config{Labels: map[string]string{"io.oac.test": "docker-serve"}}}) + if err != nil { + t.Fatal(err) + } + defer c.ContainerRemove(context.Background(), created.ID, client.ContainerRemoveOptions{}) + if _, err := c.CopyToContainer(t.Context(), created.ID, client.CopyToContainerOptions{DestinationPath: "/", Content: &content}); err != nil { + t.Fatal(err) + } + image, err := c.ContainerCommit(t.Context(), created.ID, client.ContainerCommitOptions{}) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if _, err := c.ImageRemove(context.Background(), image.ID, client.ImageRemoveOptions{}); err != nil { + t.Error(err) + } + }) + p, err := New(c, Config{InstallationID: uuid.NewString(), Image: image.ID, Network: "bridge", Seccomp: string(seccomp), ExtraHosts: []string{"example.com:host-gateway"}}) + if err != nil { + t.Fatal(err) + } + b := contracttest.Bootstrap(sandbox.Reference{TenantID: uuid.NewString(), EnvironmentID: uuid.NewString(), AllocationID: uuid.NewString()}) + b.SandboxIO.LinkURL = "wss://example.com:" + port + "/api/v1/sandbox-link" + resource := b.SandboxIO.Resource.Ref() + authority.AddServe([]byte(b.SandboxIO.Credential), sandboxlink.ServePeer{PeerID: sandboxwire.NewID(), Resource: resource}) + runtimeID := sandboxwire.NewID() + authority.AddRuntime([]byte("agent-host"), runtimeID) + open := sandboxlink.Open{Resource: resource, Service: sandboxlink.ServiceFile, Version: sandboxfs.Version, AttachmentID: sandboxwire.NewID(), + SessionID: sandboxwire.NewID(), AssignmentID: sandboxwire.NewID(), AssignmentEpoch: 1, AttachGrant: []byte("grant")} + authority.AddGrant(open.AttachGrant, sandboxlinktest.Grant{RuntimeID: runtimeID, Resource: resource, SessionID: open.SessionID, + AssignmentID: open.AssignmentID, AssignmentEpoch: 1, Services: []sandboxlink.Service{sandboxlink.ServiceFile}, Lease: time.Minute}) + + ctx, cancel := context.WithTimeout(t.Context(), 90*time.Second) + defer cancel() + t.Cleanup(func() { + cleanup, stop := context.WithTimeout(context.Background(), 20*time.Second) + defer stop() + if err := p.Kill(cleanup, b.Reference); err != nil { + t.Error(err) + } + }) + if _, err := p.Create(ctx, b); err != nil { + t.Fatal(err) + } + roots := x509.NewCertPool() + roots.AddCert(server.Certificate()) + link, err := sandboxlink.DialAttach(ctx, sandboxlink.AttachConfig{URL: "wss://127.0.0.1:" + port + "/api/v1/sandbox-link", + TLS: &tls.Config{RootCAs: roots, MinVersion: tls.VersionTLS12}, RuntimeID: runtimeID, Credential: []byte("agent-host")}) + if err != nil { + t.Fatal(err) + } + defer link.Close() + // Open fails with ServiceUnavailable until the sandbox's serve peer connects. + var stream sandboxlink.Stream + for { + stream, _, err = link.OpenService(ctx, open) + if !errors.Is(err, sandboxlink.ServiceUnavailable) || ctx.Err() != nil { + break + } + time.Sleep(50 * time.Millisecond) + } + if err != nil { + t.Fatal("open File:", err) + } + files := sandboxfs.NewClient(stream) + defer files.Close() + attached, err := files.Attach(ctx, &sandboxfs.AttachRequest{Export: sandboxfs.WorldExport}) + if err != nil { + t.Fatal(err) + } + var handles sandboxfs.HandleIDs + dir := handles.Next() + if _, err := files.OpenDir(ctx, &sandboxfs.OpenDirRequest{Handle: dir, Node: attached.Root.Node}); err != nil { + t.Fatal(err) + } + var names []string + for cookie, end := uint64(0), false; !end; { + listed, err := files.ReadDir(ctx, &sandboxfs.ReadDirRequest{Handle: dir, Cookie: cookie, Limit: 64 << 10}) + if err != nil { + t.Fatal(err) + } + for _, entry := range listed.Entries { + names, cookie = append(names, string(entry.Name)), entry.Cookie + } + end = listed.End + } + if !slices.Contains(names, "environment") || !slices.Contains(names, "workspace") { + t.Fatalf("/ of the sandbox lists %q", names) + } +} diff --git a/services/core/internal/sandbox/e2b/contract_test.go b/services/core/internal/sandbox/e2b/contract_test.go index b4d64227b..f9144d70b 100644 --- a/services/core/internal/sandbox/e2b/contract_test.go +++ b/services/core/internal/sandbox/e2b/contract_test.go @@ -16,7 +16,7 @@ func (f contractCaller) Call(ctx context.Context, q Request) (Response, error) { func TestProviderContract(t *testing.T) { contracttest.RunFailures(t, func(t *testing.T, s contracttest.Scenario, cancel context.CancelFunc) contracttest.Fixture { p, _, r := fixture(t) - b := sandbox.Bootstrap{Reference: r, SessionID: uuid.NewString(), DeviceID: uuid.NewString(), CoreURL: "https://core.example/api/v1", Credential: "synthetic", NetworkAccess: "enabled"} + b := contracttest.Bootstrap(r) var calls []string p.caller = contractCaller(func(ctx context.Context, q Request) (Response, error) { calls = append(calls, q.Operation) @@ -47,7 +47,7 @@ func TestProviderContract(t *testing.T) { func TestProviderContractObservation(t *testing.T) { p, f, r := fixture(t) f.response.Info = &sandbox.Info{Reference: r, ProviderID: "native-owned", State: "running", CreateSettled: true, BootstrapComplete: true} - b := sandbox.Bootstrap{Reference: r, SessionID: uuid.NewString(), DeviceID: uuid.NewString(), CoreURL: "https://core.example/api/v1", Credential: "synthetic", NetworkAccess: "enabled"} + b := contracttest.Bootstrap(r) got, err := p.Create(bounded(t), b) contracttest.AssertObservation(t, got, err, r, "native-owned", "running") got, err = p.GetInfo(bounded(t), r) diff --git a/services/core/internal/sandbox/e2b/internal/contractgen/main.go b/services/core/internal/sandbox/e2b/internal/contractgen/main.go index 6d79d9544..72afa5895 100644 --- a/services/core/internal/sandbox/e2b/internal/contractgen/main.go +++ b/services/core/internal/sandbox/e2b/internal/contractgen/main.go @@ -21,6 +21,7 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/internal/agentnetwork" "github.com/MiniMax-AI/OpenAgentCore/internal/runtimebootstrap" + "github.com/MiniMax-AI/OpenAgentCore/internal/sandboxbootstrap" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/e2b" ) @@ -113,7 +114,9 @@ func main() { // envelopes and managed bootstrap inputs in both implementations. func fixtures() []byte { r := sandbox.Reference{TenantID: "11111111-1111-4111-8111-111111111111", EnvironmentID: "22222222-2222-4222-8222-222222222222", AllocationID: "33333333-3333-4333-8333-333333333333"} - b := sandbox.Bootstrap{Reference: r, SessionID: "44444444-4444-4444-8444-444444444444", DeviceID: "55555555-5555-4555-8555-555555555555", CoreURL: "https://core.example/api/v1", Credential: "fixture-only", NetworkAccess: "enabled"} + b := sandbox.Bootstrap{Reference: r, SessionID: "44444444-4444-4444-8444-444444444444", DeviceID: "55555555-5555-4555-8555-555555555555", CoreURL: "https://core.example/api/v1", Credential: "fixture-only", NetworkAccess: "enabled", + SandboxIO: sandboxbootstrap.Input{Version: sandboxbootstrap.Version, LinkURL: "wss://core.example/api/v1/sandbox-link", Credential: "fixture-serve-only", + Resource: sandboxbootstrap.Resource{TenantID: r.TenantID, EnvironmentID: r.EnvironmentID, Kind: "allocation", ID: r.AllocationID, Generation: 1}}} installation := "66666666-6666-4666-8666-666666666666" connection := b.RuntimeConnection() q := e2b.Request{Version: e2b.ProtocolVersion, Operation: "create", Config: e2b.Config{InstallationID: installation}, Reference: r, Bootstrap: &b, RuntimeBootstrap: &connection, Deadline: time.Date(2099, 1, 1, 0, 0, 0, 0, time.UTC)} diff --git a/services/core/internal/sandbox/e2b/provider.go b/services/core/internal/sandbox/e2b/provider.go index 3328eb0db..d259b65c2 100644 --- a/services/core/internal/sandbox/e2b/provider.go +++ b/services/core/internal/sandbox/e2b/provider.go @@ -162,9 +162,6 @@ func (p *Provider) call(ctx context.Context, operation string, r sandbox.Referen var connection *runtimebootstrap.Connection if b != nil { value := b.RuntimeConnection() - if value.Validate() != nil { - return unstarted(operation, r), sandbox.ErrInvalid - } connection = &value } out, err := p.caller.Call(ctx, Request{Version: ProtocolVersion, Operation: operation, Config: p.config, Reference: r, Bootstrap: b, RuntimeBootstrap: connection, Command: command, Deadline: deadline}) @@ -234,15 +231,7 @@ func (p *Provider) info(ctx context.Context, operation string, r sandbox.Referen return sandbox.Info{Reference: r}, err } func (p *Provider) Create(ctx context.Context, b sandbox.Bootstrap) (sandbox.Info, error) { - policy := agentnetwork.Policy{Access: b.NetworkAccess, AllowedDomains: b.AllowedDomains} - if !validReference(b.Reference) || !validID(b.SessionID) || !validID(b.DeviceID) || policy.Validate() != nil || b.RuntimeConnection().Validate() != nil { - info := sandbox.Info{Reference: b.Reference} - if validReference(b.Reference) { - info.State, info.CreateSettled = "absent", true - } - return info, sandbox.ErrInvalid - } - b.AllowedDomains = policy.Hosts() + b.AllowedDomains = agentnetwork.Policy{Access: b.NetworkAccess, AllowedDomains: b.AllowedDomains}.Hosts() return p.info(ctx, "create", b.Reference, &b) } func (p *Provider) GetInfo(ctx context.Context, r sandbox.Reference) (sandbox.Info, error) { diff --git a/services/core/internal/sandbox/e2b/provider_test.go b/services/core/internal/sandbox/e2b/provider_test.go index 406160671..b5c199ee9 100644 --- a/services/core/internal/sandbox/e2b/provider_test.go +++ b/services/core/internal/sandbox/e2b/provider_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/contracttest" "github.com/google/uuid" ) @@ -103,13 +104,13 @@ func TestKillRequiresTerminalProof(t *testing.T) { } func TestCreateAndCommandUseOnlyPrivateRequest(t *testing.T) { p, f, r := fixture(t) - b := sandbox.Bootstrap{Reference: r, SessionID: uuid.NewString(), DeviceID: uuid.NewString(), CoreURL: "https://core.example/api/v1", Credential: "private-bootstrap", NetworkAccess: "enabled"} + b := contracttest.Bootstrap(r) f.response.Info = &sandbox.Info{Reference: r, State: "running", ProviderID: "native-id", CreateSettled: true, BootstrapComplete: true} if _, err := p.Create(bounded(t), b); err != nil { t.Fatal(err) } q := f.requests[0] - if q.Operation != "create" || q.Bootstrap == nil || q.Bootstrap.Credential != b.Credential || q.Config.APIKey != p.config.APIKey { + if q.Operation != "create" || q.Bootstrap == nil || q.Bootstrap.Credential != b.Credential || q.Bootstrap.SandboxIO != b.SandboxIO || q.Config.APIKey != p.config.APIKey { t.Fatal("private request lost") } f.response.Info = nil @@ -143,7 +144,7 @@ func TestDeadlineAndCommandAdmission(t *testing.T) { func TestDefinitePreHelperCreateFailureCarriesAbsenceProof(t *testing.T) { p, f, r := fixture(t) - b := sandbox.Bootstrap{Reference: r, SessionID: uuid.NewString(), DeviceID: uuid.NewString(), CoreURL: "https://core.example/api/v1", Credential: "private", NetworkAccess: "enabled"} + b := contracttest.Bootstrap(r) for _, err := range []error{errHelperNotStarted, context.DeadlineExceeded} { f.err = err info, gotErr := p.Create(bounded(t), b) diff --git a/services/core/internal/sandbox/microsandbox/contract_test.go b/services/core/internal/sandbox/microsandbox/contract_test.go index e4b11b963..4ecd42dfe 100644 --- a/services/core/internal/sandbox/microsandbox/contract_test.go +++ b/services/core/internal/sandbox/microsandbox/contract_test.go @@ -3,7 +3,6 @@ package microsandbox import ( "context" "errors" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/contracttest" "github.com/google/uuid" "testing" @@ -12,7 +11,7 @@ import ( func TestProviderContract(t *testing.T) { contracttest.RunFailures(t, func(t *testing.T, s contracttest.Scenario, cancel context.CancelFunc) contracttest.Fixture { c, r := testConfig(), testRef() - b := sandbox.Bootstrap{Reference: r, SessionID: uuid.NewString(), DeviceID: uuid.NewString(), CoreURL: "https://core.example/api/v1", Credential: "synthetic", NetworkAccess: "enabled"} + b := contracttest.Bootstrap(r) var calls []string p, err := NewWithCaller(c, callerFunc(func(ctx context.Context, q Request) (Response, error) { calls = append(calls, q.Operation) @@ -62,7 +61,7 @@ func TestProviderContractObservation(t *testing.T) { if err != nil { t.Fatal(err) } - b := sandbox.Bootstrap{Reference: r, SessionID: uuid.NewString(), DeviceID: uuid.NewString(), CoreURL: "https://core.example/api/v1", Credential: "synthetic", NetworkAccess: "enabled"} + b := contracttest.Bootstrap(r) got, err := p.Create(deadline(t), b) contracttest.AssertObservation(t, got, err, r, "native-owned", "running") got, err = p.GetInfo(deadline(t), r) diff --git a/services/core/internal/sandbox/microsandbox/identity.go b/services/core/internal/sandbox/microsandbox/identity.go index 2b95e1628..5464f66ad 100644 --- a/services/core/internal/sandbox/microsandbox/identity.go +++ b/services/core/internal/sandbox/microsandbox/identity.go @@ -7,7 +7,6 @@ import ( "path/filepath" "strings" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentnetwork" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" "github.com/google/uuid" ) @@ -75,12 +74,6 @@ func ValidateSnapshot(c Config, r sandbox.Reference, s SnapshotIdentity) error { } return nil } -func ValidateBootstrap(b sandbox.Bootstrap) error { - if !ValidReference(b.Reference) || !validID(b.SessionID) || !validID(b.DeviceID) || b.RuntimeConnection().Validate() != nil { - return sandbox.ErrInvalid - } - return (agentnetwork.Policy{Access: b.NetworkAccess, AllowedDomains: b.AllowedDomains}).Validate() -} func ValidateCommand(c sandbox.Command) error { if len(c.Args) == 0 || c.Args[0] == "" || len(c.Stdin) > sandbox.MaxCommandInputBytes || (c.Directory != "" && !filepath.IsAbs(c.Directory)) { return sandbox.ErrInvalid @@ -98,7 +91,7 @@ func ValidateRequest(q Request) error { } switch q.Operation { case "create": - if q.Bootstrap == nil || q.Bootstrap.Reference != q.Reference || ValidateBootstrap(*q.Bootstrap) != nil { + if q.Bootstrap == nil || q.Bootstrap.Reference != q.Reference { return sandbox.ErrInvalid } case "inspect", "kill", "resume_compute", "metrics": diff --git a/services/core/internal/sandbox/node/docker_live_test.go b/services/core/internal/sandbox/node/docker_live_test.go index 13ab16b51..6dc295325 100644 --- a/services/core/internal/sandbox/node/docker_live_test.go +++ b/services/core/internal/sandbox/node/docker_live_test.go @@ -10,9 +10,9 @@ import ( "time" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/contracttest" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/docker" sandboxdocker "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/docker" - "github.com/google/uuid" "github.com/moby/moby/client" ) @@ -87,7 +87,7 @@ func TestDockerNodeTransportLifecycle(t *testing.T) { }() ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) defer cancel() - b := sandbox.Bootstrap{Reference: r, SessionID: uuid.NewString(), DeviceID: uuid.NewString(), CoreURL: "http://core.invalid/api/v1", Credential: "synthetic-node-transport-credential", NetworkAccess: "enabled"} + b := contracttest.Bootstrap(r) callCtx, callCancel := context.WithTimeout(ctx, 25*time.Second) info, err := proxy.Create(callCtx, b) callCancel() diff --git a/services/core/internal/sandbox/node/wire.go b/services/core/internal/sandbox/node/wire.go index 0194e3c82..77a7b4bd6 100644 --- a/services/core/internal/sandbox/node/wire.go +++ b/services/core/internal/sandbox/node/wire.go @@ -17,7 +17,7 @@ import ( "github.com/gorilla/websocket" ) -const ProtocolVersion = 4 +const ProtocolVersion = 5 const MaxControlFrameBytes = 32 * 1024 const MaxFrameBytes = 72 * 1024 * 1024 const maxPending = 32 diff --git a/services/core/internal/sandbox/sandbox_provider.go b/services/core/internal/sandbox/sandbox_provider.go index 663526b8e..a95151940 100644 --- a/services/core/internal/sandbox/sandbox_provider.go +++ b/services/core/internal/sandbox/sandbox_provider.go @@ -21,6 +21,11 @@ package sandbox import ( "context" "errors" + + "github.com/google/uuid" + + "github.com/MiniMax-AI/OpenAgentCore/internal/agentnetwork" + "github.com/MiniMax-AI/OpenAgentCore/internal/sandboxbootstrap" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/providercontract" ) @@ -42,6 +47,25 @@ type Bootstrap struct { SessionID, DeviceID, CoreURL, Credential string NetworkAccess string AllowedDomains []string + // SandboxIO is the Sandbox I/O service's launch input, which the Provider + // delivers as a private file (docs/sandbox-bootstrap.md). + SandboxIO sandboxbootstrap.Input +} + +// Validate checks a Create input once, before Create: Providers deliver it +// as given. SandboxIO serves the Reference's allocation. +func (b Bootstrap) Validate() error { + for _, id := range []string{b.TenantID, b.EnvironmentID, b.AllocationID, b.SessionID, b.DeviceID} { + if u, err := uuid.Parse(id); err != nil || u == uuid.Nil || u.String() != id { + return ErrInvalid + } + } + resource := sandboxbootstrap.Resource{TenantID: b.TenantID, EnvironmentID: b.EnvironmentID, Kind: "allocation", ID: b.AllocationID, Generation: b.SandboxIO.Resource.Generation} + if b.RuntimeConnection().Validate() != nil || (agentnetwork.Policy{Access: b.NetworkAccess, AllowedDomains: b.AllowedDomains}).Validate() != nil || + b.SandboxIO.Validate() != nil || b.SandboxIO.Resource != resource { + return ErrInvalid + } + return nil } // Info describes compute only. Running does not establish daemon authentication, diff --git a/services/core/tests/integration/admin_session_archive_test.go b/services/core/tests/integration/admin_session_archive_test.go index 846c69b12..de09da6f5 100644 --- a/services/core/tests/integration/admin_session_archive_test.go +++ b/services/core/tests/integration/admin_session_archive_test.go @@ -75,7 +75,7 @@ func managedArchiveSession(t *testing.T, s *Store, input sessions.CreateSession) func archiveAllocation(t *testing.T, w *Store, tenant string, session sessions.Session, installation string) deployment.Allocation { t.Helper() - owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, installation, runtimedevice.HashCredential(uuid.NewString())) + owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, installation, runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())) if err != nil { t.Fatal(err) } @@ -132,7 +132,7 @@ func TestManagedSessionArchiveUnallocatedAndGuards(t *testing.T) { if status, err := sessionAdapter(s).GetManagedSessionArchive(ctx, tenant, session.ID); err != nil || status != result { t.Fatal("status differs from committed archive", status, err) } - if _, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, installation, runtimedevice.HashCredential(uuid.NewString())); !errors.Is(err, deployment.ErrInvalidInput) { + if _, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, installation, runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())); !errors.Is(err, deployment.ErrInvalidInput) { t.Fatal("archived Environment allocated after archive", err) } if _, err := sessionService(t, s).ReserveEnvironmentInput(t.Context(), tenant, session.ID, "later", []sessions.Input{messageInput("later")}); !errors.Is(err, sessions.ErrEnvironmentUnavailable) { @@ -179,7 +179,7 @@ func TestManagedSessionArchiveRetainsHistoryAndSettledResources(t *testing.T) { if _, err := deploymentExecution(t, w).ReleaseAllocation(t.Context(), owner); !errors.Is(err, deployment.ErrAllocationConflict) { t.Fatal("archive discarded unknown Create ownership", err) } - replay, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, installation, runtimedevice.HashCredential(uuid.NewString())) + replay, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, installation, runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())) if err != nil || !replay.Replayed || replay.ID != owner.ID || replay.DeviceID != owner.DeviceID || replay.State != "cleanup_pending" { t.Fatal("late provisioning retry replaced archived allocation", replay, err) } diff --git a/services/core/tests/integration/admin_session_archive_worker_http_test.go b/services/core/tests/integration/admin_session_archive_worker_http_test.go index 00f830760..883ea0d85 100644 --- a/services/core/tests/integration/admin_session_archive_worker_http_test.go +++ b/services/core/tests/integration/admin_session_archive_worker_http_test.go @@ -41,7 +41,7 @@ func TestAdminSessionArchiveWorkerHTTPPostgres(t *testing.T) { provider := &lifecycleProvider{resources: map[string]sandbox.Info{}} deployments := deploymentService(t, s) providerConfig := func(setup deployment.Setup) *execution.RuntimeProvider { - return &execution.RuntimeProvider{InstallationID: setup.InstallationID, ProviderKind: setup.Provider, Generation: setup.Generation, Mode: setup.Mode, AdmissionPaused: setup.AdmissionPaused, CoreURL: "https://core.example/api/v1", BackendFingerprint: setup.BackendFingerprint, Provider: provider} + return &execution.RuntimeProvider{InstallationID: setup.InstallationID, ProviderKind: setup.Provider, Generation: setup.Generation, Mode: setup.Mode, AdmissionPaused: setup.AdmissionPaused, CoreURL: "https://core.example/api/v1", SandboxLink: "wss://core.example/api/v1/sandbox-link", BackendFingerprint: setup.BackendFingerprint, Provider: provider} } configuration := execution.NewDeferredRuntimeProvider(installation, func(ctx context.Context) (*execution.RuntimeProvider, error) { setup, err := deployments.Setup(ctx) diff --git a/services/core/tests/integration/archive_cancellation_test.go b/services/core/tests/integration/archive_cancellation_test.go index fd9445838..445f70a9f 100644 --- a/services/core/tests/integration/archive_cancellation_test.go +++ b/services/core/tests/integration/archive_cancellation_test.go @@ -63,7 +63,7 @@ func TestArchiveWaitingCancellationReceipts(t *testing.T) { t.Fatal(err) } secret := uuid.NewString() - owner, err := leased.Deployment.ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: project.TenantID, EnvironmentID: session.Environment.ID}, installation, runtimedevice.HashCredential(secret)) + owner, err := leased.Deployment.ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: project.TenantID, EnvironmentID: session.Environment.ID}, installation, runtimedevice.HashCredential(secret), runtimedevice.HashCredential(secret)) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/credential_matrix_http_test.go b/services/core/tests/integration/credential_matrix_http_test.go index d424fc7d4..3659428e6 100644 --- a/services/core/tests/integration/credential_matrix_http_test.go +++ b/services/core/tests/integration/credential_matrix_http_test.go @@ -95,9 +95,9 @@ func TestCredentialNamespaceMatrix(t *testing.T) { if err != nil || setup.Provider == "" { return nil, err } - return &execution.RuntimeProvider{InstallationID: setup.InstallationID, ProviderKind: setup.Provider, BackendFingerprint: setup.BackendFingerprint, CoreURL: "https://core.example/api/v1", Provider: provider}, nil + return &execution.RuntimeProvider{InstallationID: setup.InstallationID, ProviderKind: setup.Provider, BackendFingerprint: setup.BackendFingerprint, CoreURL: "https://core.example/api/v1", SandboxLink: "wss://core.example/api/v1/sandbox-link", Provider: provider}, nil }, func(_ context.Context, setup deployment.Setup) (execution.PreparedRuntimeDeployment, error) { - return execution.PreparedRuntimeDeployment{Config: &execution.RuntimeProvider{InstallationID: setup.InstallationID, ProviderKind: setup.Provider, Mode: setup.Mode, AdmissionPaused: setup.AdmissionPaused, CoreURL: "https://core.example/api/v1", BackendFingerprint: setup.BackendFingerprint, Provider: provider}}, nil + return execution.PreparedRuntimeDeployment{Config: &execution.RuntimeProvider{InstallationID: setup.InstallationID, ProviderKind: setup.Provider, Mode: setup.Mode, AdmissionPaused: setup.AdmissionPaused, CoreURL: "https://core.example/api/v1", SandboxLink: "wss://core.example/api/v1/sandbox-link", BackendFingerprint: setup.BackendFingerprint, Provider: provider}}, nil }) worker := startWorker(t, ctx, s, &execution.Dispatcher{Registry: runtimegateway.NewRegistry(), ManagedRuntimes: runtimes}) var stop sync.Once diff --git a/services/core/tests/integration/device_bootstrap_binding_test.go b/services/core/tests/integration/device_bootstrap_binding_test.go index 597001774..089c6240c 100644 --- a/services/core/tests/integration/device_bootstrap_binding_test.go +++ b/services/core/tests/integration/device_bootstrap_binding_test.go @@ -36,7 +36,7 @@ func TestDeviceCredentialCarriesPersistedAllocationNode(t *testing.T) { if err != nil { t.Fatal(err) } - allocation, err := deploymentExecution(t, writer).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, d.InstallationID, runtimedevice.HashCredential(bearer)) + allocation, err := deploymentExecution(t, writer).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, d.InstallationID, runtimedevice.HashCredential(bearer), runtimedevice.HashCredential(bearer)) if err != nil { t.Fatal(err) } @@ -66,7 +66,7 @@ func TestDeviceCredentialWithoutManagedNodeRetainsPublicRouteIdentity(t *testing t.Fatal(err) } _, environment := localEnvironment(t, s, tenant) - allocation, err := deploymentExecution(t, executionWriter(t, s)).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, uuid.NewString(), runtimedevice.HashCredential("allocation-token")) + allocation, err := deploymentExecution(t, executionWriter(t, s)).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, uuid.NewString(), runtimedevice.HashCredential("allocation-token"), runtimedevice.HashCredential("allocation-token")) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/link_authority_test.go b/services/core/tests/integration/link_authority_test.go index 8686bb7d7..f675bbe39 100644 --- a/services/core/tests/integration/link_authority_test.go +++ b/services/core/tests/integration/link_authority_test.go @@ -457,18 +457,19 @@ func TestLinkAuthorityDestroyedAllocation(t *testing.T) { tenant, session, environment := managedSession(t, s) key := uuid.NewString() srv := startLinkRoute(t, s) + provider := &lifecycleProvider{resources: map[string]sandbox.Info{}} w := startWorker(t, t.Context(), s, &execution.Dispatcher{Registry: runtimegateway.NewRegistry(), Links: srv.Relay, ManagedRuntimes: &execution.RuntimeProvider{ - CoreURL: "http://core.invalid/api/v1", InstallationID: key, BackendFingerprint: strings.Repeat("a", 64), Provider: &lifecycleProvider{resources: map[string]sandbox.Info{}}}}) + CoreURL: "https://core.invalid/api/v1", SandboxLink: "wss://core.invalid/api/v1/sandbox-link", InstallationID: key, BackendFingerprint: strings.Repeat("a", 64), Provider: provider}}) t.Cleanup(func() { ctx, cancel := context.WithCancel(context.Background()); cancel(); _ = w.Run(ctx) }) owner, err := w.ProvisionEnvironment(t.Context(), tenant, environment.ID, key) if err != nil { t.Fatal(err) } - credential := []byte(uuid.NewString()) - if _, err := s.pool.Exec(t.Context(), "UPDATE runtime_allocations SET serve_credential_hash = $2 WHERE id = $1", owner.ID, serveHash(credential)); err != nil { - t.Fatal(err) + // The sandbox Serves with the input provisioning minted for it. + if provider.serve.Resource != (sandboxbootstrap.Resource{TenantID: tenant, EnvironmentID: environment.ID, Kind: "allocation", ID: owner.ID, Generation: 1}) || provider.serve.LinkURL != "wss://core.invalid/api/v1/sandbox-link" { + t.Fatalf("Create input: %+v %s", provider.serve.Resource, provider.serve.LinkURL) } - p := startLinkServe(t, srv, credential, sandboxbootstrap.Resource{TenantID: tenant, EnvironmentID: environment.ID, Kind: "allocation", ID: owner.ID, Generation: 1}.Ref()) + p := startLinkServe(t, srv, []byte(provider.serve.Credential), provider.serve.Resource.Ref()) within(t, p.connected) if err := sessionService(t, s).DeleteSession(t.Context(), sessions.DeleteSessionCommand{TenantID: tenant, SessionID: session.ID}); err != nil { t.Fatal(err) diff --git a/services/core/tests/integration/root_fixture_test.go b/services/core/tests/integration/root_fixture_test.go index 2fde53550..5a2896eec 100644 --- a/services/core/tests/integration/root_fixture_test.go +++ b/services/core/tests/integration/root_fixture_test.go @@ -31,8 +31,8 @@ type Store struct { } // defaultPlacement is the placement rules cmd/server builds on the built-in -// providers and an unset public URL. -var defaultPlacement, _ = placement.NewRules(providers.Builtin(), "") +// providers and an HTTPS public URL. +var defaultPlacement, _ = placement.NewRules(providers.Builtin(), "https://core.example") // New is the fixture on pool without a credential key, under defaultPlacement. func New(pool *pgxpool.Pool) *Store { diff --git a/services/core/tests/integration/runtime_adoption_test.go b/services/core/tests/integration/runtime_adoption_test.go index 76ae44516..aecb0526c 100644 --- a/services/core/tests/integration/runtime_adoption_test.go +++ b/services/core/tests/integration/runtime_adoption_test.go @@ -18,7 +18,7 @@ func nodelessAllocationFixture(t *testing.T) (*Store, *Store, deployment.Process deploymentConfigure(t, w, &d) tenant := uuid.NewString() _, e := localEnvironment(t, s, tenant) - a, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: e.ID}, d.InstallationID, runtimedevice.HashCredential("runtime")) + a, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: e.ID}, d.InstallationID, runtimedevice.HashCredential("runtime"), runtimedevice.HashCredential("runtime")) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/runtime_allocations_test.go b/services/core/tests/integration/runtime_allocations_test.go index 14ffb4994..165f31b85 100644 --- a/services/core/tests/integration/runtime_allocations_test.go +++ b/services/core/tests/integration/runtime_allocations_test.go @@ -20,7 +20,7 @@ func TestRuntimeAllocationAtomicOwnershipAndRecovery(t *testing.T) { tenant, provider := uuid.NewString(), uuid.NewString() session, environment := localEnvironment(t, s, tenant) secret := uuid.NewString() - owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, provider, runtimedevice.HashCredential(secret)) + owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, provider, runtimedevice.HashCredential(secret), runtimedevice.HashCredential(secret)) if err != nil || owner.Replayed || owner.State != "creating" || owner.CreateSettled { t.Fatalf("reservation: %+v %v", owner, err) } @@ -28,10 +28,10 @@ func TestRuntimeAllocationAtomicOwnershipAndRecovery(t *testing.T) { if err != nil || bound.ID != owner.DeviceID || bound.EnvironmentID != environment.ID { t.Fatalf("binding not committed with allocation: %+v %v", bound, err) } - if _, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: uuid.NewString(), EnvironmentID: environment.ID}, provider, runtimedevice.HashCredential(secret)); !errors.Is(err, sessions.ErrNotFound) { + if _, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: uuid.NewString(), EnvironmentID: environment.ID}, provider, runtimedevice.HashCredential(secret), runtimedevice.HashCredential(secret)); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("foreign allocation accepted: %v", err) } - if _, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, uuid.NewString(), runtimedevice.HashCredential(secret)); !errors.Is(err, deployment.ErrAllocationConflict) { + if _, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, uuid.NewString(), runtimedevice.HashCredential(secret), runtimedevice.HashCredential(secret)); !errors.Is(err, deployment.ErrAllocationConflict) { t.Fatalf("provider target changed: %v", err) } awaitRelease := pgtest.ObserveExecutionLeaseRelease(t, w.pool) @@ -44,7 +44,7 @@ func TestRuntimeAllocationAtomicOwnershipAndRecovery(t *testing.T) { } reopened, _ := testStore(t) next := executionWriter(t, reopened) - retry, err := deploymentExecution(t, next).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, provider, runtimedevice.HashCredential(uuid.NewString())) + retry, err := deploymentExecution(t, next).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, provider, runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())) if err != nil || !retry.Replayed || retry.ID != owner.ID || retry.DeviceID != owner.DeviceID { t.Fatalf("restart replaced unknown allocation: %+v %v", retry, err) } @@ -110,7 +110,7 @@ func TestRuntimeAllocationOneWinnerAndRollback(t *testing.T) { wg.Add(1) go func() { defer wg.Done() - value, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, provider, runtimedevice.HashCredential(uuid.NewString())) + value, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, provider, runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())) results <- value errs <- err }() @@ -146,7 +146,7 @@ func TestRuntimeAllocationOneWinnerAndRollback(t *testing.T) { t.Cleanup(func() { _, _ = pool.Exec(context.Background(), "ALTER TABLE runtime_allocations DROP CONSTRAINT "+constraint) }) - if _, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: fail.ID}, provider, runtimedevice.HashCredential(uuid.NewString())); err == nil { + if _, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: fail.ID}, provider, runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())); err == nil { t.Fatal("injected insert failure succeeded") } var count int @@ -160,7 +160,7 @@ func TestRuntimeAllocationExpiryAndRevocation(t *testing.T) { w := executionWriter(t, s) tenant := uuid.NewString() _, environment := localEnvironment(t, s, tenant) - owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, uuid.NewString(), runtimedevice.HashCredential(uuid.NewString())) + owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, uuid.NewString(), runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())) if err != nil { t.Fatal(err) } @@ -186,7 +186,7 @@ func TestRuntimeAllocationExpiryAndRevocation(t *testing.T) { if _, err := deploymentExecution(t, w).ReleaseAllocation(t.Context(), owner); err != nil { t.Fatal(err) } - got, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, owner.ProviderKey, runtimedevice.HashCredential(uuid.NewString())) + got, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, owner.ProviderKey, runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())) if err != nil || !got.Replayed || got.State != "released" || got.KeptAt.After(time.Now()) { t.Fatalf("cleanup permitted replacement: %+v %v", got, err) } diff --git a/services/core/tests/integration/runtime_compute_lifecycle_test.go b/services/core/tests/integration/runtime_compute_lifecycle_test.go index 10eec20b5..55b2a199e 100644 --- a/services/core/tests/integration/runtime_compute_lifecycle_test.go +++ b/services/core/tests/integration/runtime_compute_lifecycle_test.go @@ -293,7 +293,7 @@ func newComputeLifecycleFixture(t *testing.T, maxActive, maxRetained int) *compu func (f *computeLifecycleFixture) start() { t := f.t t.Helper() - dispatcher := &execution.Dispatcher{Registry: f.provider.registry, ManagedRuntimes: &execution.RuntimeProvider{CoreURL: "http://core.invalid/api/v1", InstallationID: f.key, BackendFingerprint: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Provider: f.provider, Suspension: &f.policy}} + dispatcher := &execution.Dispatcher{Registry: f.provider.registry, ManagedRuntimes: &execution.RuntimeProvider{CoreURL: "https://core.invalid/api/v1", SandboxLink: "wss://core.invalid/api/v1/sandbox-link", InstallationID: f.key, BackendFingerprint: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Provider: f.provider, Suspension: &f.policy}} // Closing the previous Worker's connection can return before PostgreSQL drops its advisory lock. deadline := time.Now().Add(2 * time.Second) var w *execution.Worker diff --git a/services/core/tests/integration/runtime_connection_test.go b/services/core/tests/integration/runtime_connection_test.go index 4d3108f1d..1f90e0bcf 100644 --- a/services/core/tests/integration/runtime_connection_test.go +++ b/services/core/tests/integration/runtime_connection_test.go @@ -33,10 +33,18 @@ func TestManagedRuntimeConnectionTracksAuthenticatedSocket(t *testing.T) { server.Config.Handler = handler server.Start() t.Cleanup(func() { server.Close(); runtime.CloseConnections(registry) }) + origin, err := deployment.NewPublicOrigin(server.URL) + if err != nil { + t.Fatal(err) + } + link, err := origin.SandboxLink() + if err != nil { + t.Fatal(err) + } p := &lifecycleProvider{resources: map[string]sandbox.Info{}} key := uuid.NewString() start := func() *execution.Worker { - w := startWorker(t, t.Context(), s, &execution.Dispatcher{Registry: registry, ManagedRuntimes: &execution.RuntimeProvider{CoreURL: server.URL + "/api/v1", InstallationID: key, BackendFingerprint: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Provider: p}}) + w := startWorker(t, t.Context(), s, &execution.Dispatcher{Registry: registry, ManagedRuntimes: &execution.RuntimeProvider{CoreURL: origin.RuntimeAPI(), SandboxLink: link, InstallationID: key, BackendFingerprint: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Provider: p}}) return w } stop := func(w *execution.Worker) { diff --git a/services/core/tests/integration/runtime_deployment_test.go b/services/core/tests/integration/runtime_deployment_test.go index 28d6bb24c..88f8ecaa9 100644 --- a/services/core/tests/integration/runtime_deployment_test.go +++ b/services/core/tests/integration/runtime_deployment_test.go @@ -74,7 +74,7 @@ func TestRuntimeDeploymentUnknownAllocationsBlockAdoptionAndSwitch(t *testing.T) old := deploymentSelection() tenant := uuid.NewString() session, environment := localEnvironment(t, s, tenant) - owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, old.InstallationID, runtimedevice.HashCredential(uuid.NewString())) + owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, old.InstallationID, runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())) if err != nil { t.Fatal(err) } @@ -101,7 +101,7 @@ func TestRuntimeDeploymentUnknownAllocationsBlockAdoptionAndSwitch(t *testing.T) } deploymentConfigure(t, w, &old) _, environment = localEnvironment(t, s, tenant) - owner, err = deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, old.InstallationID, runtimedevice.HashCredential(uuid.NewString())) + owner, err = deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, old.InstallationID, runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())) if err != nil { t.Fatal(err) } @@ -115,7 +115,7 @@ func TestRuntimeDeploymentUnknownAllocationsBlockAdoptionAndSwitch(t *testing.T) if err := deploymentExecution(t, w).ConfigureProcess(t.Context(), nil); err == nil { t.Fatal("removing adapter orphaned unknown creation") } - replay, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, old.InstallationID, runtimedevice.HashCredential(uuid.NewString())) + replay, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, old.InstallationID, runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())) if err != nil || !replay.Replayed || replay.ID != owner.ID { t.Fatal("maintenance blocked receipt replay", replay, err) } @@ -149,7 +149,7 @@ func TestRuntimeDeploymentMaintenancePreservesCreationRetriesAndOtherPlacements( if err := pool.QueryRow(t.Context(), "SELECT count(*) FROM sessions WHERE tenant_id=$1", tenant).Scan(&count); err != nil || count != 1 { t.Fatal("rejection left partial Session", count, err) } - if _, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: existing.Environment.ID}, old.InstallationID, runtimedevice.HashCredential(uuid.NewString())); !errors.Is(err, placement.ErrAdmissionClosed) { + if _, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: existing.Environment.ID}, old.InstallationID, runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())); !errors.Is(err, placement.ErrAdmissionClosed) { t.Fatal("maintenance reserved new allocation", err) } for _, kind := range []string{"none", "self_hosted"} { @@ -161,10 +161,10 @@ func TestRuntimeDeploymentMaintenancePreservesCreationRetriesAndOtherPlacements( } old.AdmissionPaused = false deploymentConfigure(t, w, &old) - if _, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: existing.Environment.ID}, uuid.NewString(), runtimedevice.HashCredential(uuid.NewString())); !errors.Is(err, placement.ErrAdmissionClosed) { + if _, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: existing.Environment.ID}, uuid.NewString(), runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())); !errors.Is(err, placement.ErrAdmissionClosed) { t.Fatal("wrong installation reserved resource", err) } - if _, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: existing.Environment.ID}, old.InstallationID, runtimedevice.HashCredential(uuid.NewString())); err != nil { + if _, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: existing.Environment.ID}, old.InstallationID, runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())); err != nil { t.Fatal("resume did not reopen allocation", err) } } @@ -216,7 +216,7 @@ func TestRuntimeDeploymentRetainedResourcesBlockSwitchWithoutMutation(t *testing deploymentConfigure(t, w, &old) tenant := uuid.NewString() _, environment := localEnvironment(t, s, tenant) - owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, old.InstallationID, runtimedevice.HashCredential(uuid.NewString())) + owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, old.InstallationID, runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())) if err != nil { t.Fatal(err) } @@ -275,7 +275,7 @@ func TestRuntimeDeploymentAllocationBeforeMaintenanceRetainsOwnership(t *testing } allocated := make(chan error, 1) go func() { - _, err := deploymentExecution(t, w).ReserveAllocation(ctx, deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, config.InstallationID, runtimedevice.HashCredential(uuid.NewString())) + _, err := deploymentExecution(t, w).ReserveAllocation(ctx, deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, config.InstallationID, runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())) allocated <- err }() runtimeSuspensionWaitBlocked(t, ctx, pool, blocker, allocated) diff --git a/services/core/tests/integration/runtime_deployment_worker_test.go b/services/core/tests/integration/runtime_deployment_worker_test.go index 42ce2fae8..ee6d47003 100644 --- a/services/core/tests/integration/runtime_deployment_worker_test.go +++ b/services/core/tests/integration/runtime_deployment_worker_test.go @@ -23,7 +23,7 @@ func TestManagedDeploymentStartupRejectsSwitchBeforeBackendAccess(t *testing.T) } stop() replacement := &lifecycleProvider{resources: map[string]sandbox.Info{}} - config := &execution.RuntimeProvider{CoreURL: "http://core.invalid/api/v1", InstallationID: uuid.NewString(), BackendFingerprint: strings.Repeat("b", 64), Provider: replacement, AdmissionPaused: true} + config := &execution.RuntimeProvider{CoreURL: "https://core.invalid/api/v1", SandboxLink: "wss://core.invalid/api/v1/sandbox-link", InstallationID: uuid.NewString(), BackendFingerprint: strings.Repeat("b", 64), Provider: replacement, AdmissionPaused: true} start := func(config *execution.RuntimeProvider) error { _, err := startWorkerErr(t.Context(), s, &execution.Dispatcher{Registry: runtimegateway.NewRegistry(), ManagedRuntimes: config}) return err diff --git a/services/core/tests/integration/runtime_environment_terminal_test.go b/services/core/tests/integration/runtime_environment_terminal_test.go index 2851301a3..de934bb71 100644 --- a/services/core/tests/integration/runtime_environment_terminal_test.go +++ b/services/core/tests/integration/runtime_environment_terminal_test.go @@ -26,7 +26,7 @@ func TestManagedEnvironmentTerminationSettlesInputAndPreservesIdentity(t *testin } reservation := initialEnvironmentReservation(t, s, pool, tenant, session.ID) writer := executionWriter(t, s) - owner, err := deploymentExecution(t, writer).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, uuid.NewString(), runtimedevice.HashCredential(uuid.NewString())) + owner, err := deploymentExecution(t, writer).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, uuid.NewString(), runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())) if err != nil { t.Fatal(err) } @@ -121,7 +121,7 @@ func TestManagedEnvironmentFailureRollsBackWithSessionEvent(t *testing.T) { t.Fatal(err) } writer := executionWriter(t, s) - owner, err := deploymentExecution(t, writer).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, uuid.NewString(), runtimedevice.HashCredential(uuid.NewString())) + owner, err := deploymentExecution(t, writer).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, uuid.NewString(), runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/runtime_idle_clock_test.go b/services/core/tests/integration/runtime_idle_clock_test.go index b792c00e5..6c4b35c50 100644 --- a/services/core/tests/integration/runtime_idle_clock_test.go +++ b/services/core/tests/integration/runtime_idle_clock_test.go @@ -29,7 +29,7 @@ func managedIdleClockFixture(t *testing.T) (*Store, *Store, deployment.Allocatio if err != nil { t.Fatal(err) } - owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, d.InstallationID, runtimedevice.HashCredential("runtime")) + owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, d.InstallationID, runtimedevice.HashCredential("runtime"), runtimedevice.HashCredential("runtime")) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/runtime_lifecycle_nodes_test.go b/services/core/tests/integration/runtime_lifecycle_nodes_test.go index a02c00452..eba5579d3 100644 --- a/services/core/tests/integration/runtime_lifecycle_nodes_test.go +++ b/services/core/tests/integration/runtime_lifecycle_nodes_test.go @@ -40,7 +40,7 @@ func lifecycleTestSession(t *testing.T, s *Store, node string) (string, sessions func lifecycleTestAllocation(t *testing.T, s, w *Store, d deployment.ProcessDeployment, node string) deployment.Allocation { t.Helper() tenant, session := lifecycleTestSession(t, s, node) - allocation, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, d.InstallationID, runtimedevice.HashCredential(uuid.NewString())) + allocation, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, d.InstallationID, runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())) if err != nil { t.Fatal(err) } @@ -139,7 +139,7 @@ func TestRuntimeLifecycleNodeInventoryAndRouting(t *testing.T) { if _, err := deploymentService(t, w).LifecycleNode(t.Context(), uuid.NewString(), environment); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("tenant boundary", err) } - owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment}, d.InstallationID, runtimedevice.HashCredential("runtime")) + owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment}, d.InstallationID, runtimedevice.HashCredential("runtime"), runtimedevice.HashCredential("runtime")) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/runtime_lifecycle_test.go b/services/core/tests/integration/runtime_lifecycle_test.go index f03f85e0a..5774617cb 100644 --- a/services/core/tests/integration/runtime_lifecycle_test.go +++ b/services/core/tests/integration/runtime_lifecycle_test.go @@ -12,6 +12,7 @@ import ( "github.com/google/uuid" + "github.com/MiniMax-AI/OpenAgentCore/internal/sandboxbootstrap" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/execution" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" @@ -28,6 +29,7 @@ type lifecycleProvider struct { loseCreate, absent, unavailable bool credentialHash string credential string + serve sandboxbootstrap.Input } func (p *lifecycleProvider) Create(_ context.Context, b sandbox.Bootstrap) (sandbox.Info, error) { @@ -36,6 +38,7 @@ func (p *lifecycleProvider) Create(_ context.Context, b sandbox.Bootstrap) (sand p.creates++ p.credentialHash = runtimedevice.HashCredential(b.Credential) p.credential = b.Credential + p.serve = b.SandboxIO i := sandbox.Info{Reference: b.Reference, ProviderID: b.AllocationID, State: "running", BootstrapComplete: true} if !p.absent { p.resources[b.AllocationID] = i @@ -88,7 +91,7 @@ func managedWorkerMode(t *testing.T, s *Store, key string, p sandbox.SandboxProv t.Cleanup(server.Close) peer.setRuntimeGateway(t, "ws"+strings.TrimPrefix(server.URL, "http"), registry) } - w := startWorker(t, t.Context(), s, &execution.Dispatcher{Registry: registry, ManagedRuntimes: &execution.RuntimeProvider{CoreURL: "http://core.invalid/api/v1", InstallationID: key, BackendFingerprint: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Provider: p, AdmissionPaused: maintenance}}) + w := startWorker(t, t.Context(), s, &execution.Dispatcher{Registry: registry, ManagedRuntimes: &execution.RuntimeProvider{CoreURL: "https://core.invalid/api/v1", SandboxLink: "wss://core.invalid/api/v1/sandbox-link", InstallationID: key, BackendFingerprint: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Provider: p, AdmissionPaused: maintenance}}) if len(run) > 0 && run[0] { ctx, cancel := context.WithCancel(t.Context()) done := make(chan error, 1) diff --git a/services/core/tests/integration/runtime_node_generations_test.go b/services/core/tests/integration/runtime_node_generations_test.go index f491d98cf..1a77287dc 100644 --- a/services/core/tests/integration/runtime_node_generations_test.go +++ b/services/core/tests/integration/runtime_node_generations_test.go @@ -114,7 +114,7 @@ func TestNodeGenerationsCapacityFallbackAndImmutablePending(t *testing.T) { t.Fatal("late readiness moved pin or erased serving readiness", n) } } - owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: pending.Environment.ID}, first.InstallationID, runtimedevice.HashCredential("runtime")) + owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: pending.Environment.ID}, first.InstallationID, runtimedevice.HashCredential("runtime"), runtimedevice.HashCredential("runtime")) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/runtime_node_lifecycle_fixture_test.go b/services/core/tests/integration/runtime_node_lifecycle_fixture_test.go index d581b19f6..270a1cef4 100644 --- a/services/core/tests/integration/runtime_node_lifecycle_fixture_test.go +++ b/services/core/tests/integration/runtime_node_lifecycle_fixture_test.go @@ -128,7 +128,7 @@ func newNodeIsolationFixture(t *testing.T, mode string) *nodeIsolationFixture { // Keep restored compute awake throughout the isolation assertions. // The suspension setup explicitly dates its activity two minutes in the past. policy := &execution.RuntimeSuspensionPolicy{IdleTimeout: time.Minute, Retention: time.Hour, MaxActive: 100, MaxRetained: 100} - f.worker = startWorker(t, t.Context(), s, &execution.Dispatcher{Registry: registry, ManagedRuntimes: &execution.RuntimeProvider{CoreURL: "http://core.invalid/api/v1", InstallationID: f.key, BackendFingerprint: strings.Repeat("a", 64), Provider: p, ProviderKind: "microsandbox", LocalNodeID: f.nodeA, LocalCredentialSHA256: runtimedevice.HashCredential("local-credential"), LocalMaxActive: 100, LocalMaxRetained: 100, Suspension: policy}}) + f.worker = startWorker(t, t.Context(), s, &execution.Dispatcher{Registry: registry, ManagedRuntimes: &execution.RuntimeProvider{CoreURL: "https://core.invalid/api/v1", SandboxLink: "wss://core.invalid/api/v1/sandbox-link", InstallationID: f.key, BackendFingerprint: strings.Repeat("a", 64), Provider: p, ProviderKind: "microsandbox", LocalNodeID: f.nodeA, LocalCredentialSHA256: runtimedevice.HashCredential("local-credential"), LocalMaxActive: 100, LocalMaxRetained: 100, Suspension: policy}}) spec := SandboxDeploymentTestSpec("microsandbox") raw, _ := json.Marshal(spec) if _, err := pool.Exec(t.Context(), "UPDATE runtime_deployment SET specification=$1", raw); err != nil { @@ -150,7 +150,7 @@ func (f *nodeIsolationFixture) enroll(id string) { if err != nil { f.t.Fatal(err) } - _, err = f.nodes.Enroll(f.t.Context(), token, deployment.Enrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("microsandbox").Digest("microsandbox"), NodeID: id, Credential: strings.Repeat("x", 64), Name: id, Provider: "microsandbox", BackendFingerprint: strings.Repeat("b", 64)}) + _, err = f.nodes.Enroll(f.t.Context(), token, deployment.Enrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("microsandbox").Digest("microsandbox"), NodeID: id, Credential: strings.Repeat("x", 64), Name: id, Provider: "microsandbox", BackendFingerprint: strings.Repeat("b", 64), CoreURL: f.store.placement.PublicURL()}) if err != nil { f.t.Fatal(err) } diff --git a/services/core/tests/integration/runtime_nodes_test.go b/services/core/tests/integration/runtime_nodes_test.go index cf11c0a42..efbcd91d5 100644 --- a/services/core/tests/integration/runtime_nodes_test.go +++ b/services/core/tests/integration/runtime_nodes_test.go @@ -246,7 +246,7 @@ func TestRuntimeNodesRetention(t *testing.T) { if err != nil { t.Fatal(err) } - retained, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: first.Environment.ID}, next.InstallationID, runtimedevice.HashCredential("runtime")) + retained, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: first.Environment.ID}, next.InstallationID, runtimedevice.HashCredential("runtime"), runtimedevice.HashCredential("runtime")) if err != nil { t.Fatal(err) } @@ -302,7 +302,7 @@ func TestRuntimeNodesRestoreAndCreationShareCapacity(t *testing.T) { if err != nil { t.Fatal(err) } - allocation, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, d.InstallationID, runtimedevice.HashCredential("runtime")) + allocation, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, d.InstallationID, runtimedevice.HashCredential("runtime"), runtimedevice.HashCredential("runtime")) if err != nil { t.Fatal(err) } @@ -360,7 +360,7 @@ func TestRuntimeNodesLongOfflineRetainsExactAllocation(t *testing.T) { if err != nil { t.Fatal(err) } - owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, d.InstallationID, runtimedevice.HashCredential("runtime")) + owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, d.InstallationID, runtimedevice.HashCredential("runtime"), runtimedevice.HashCredential("runtime")) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/runtime_observation_test.go b/services/core/tests/integration/runtime_observation_test.go index 0c178075c..5d2f9ed05 100644 --- a/services/core/tests/integration/runtime_observation_test.go +++ b/services/core/tests/integration/runtime_observation_test.go @@ -16,7 +16,7 @@ func TestRuntimeNodeObservationRetainsResourcesAndFencesStaleResults(t *testing. if err != nil { t.Fatal(err) } - owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, d.InstallationID, runtimedevice.HashCredential("runtime")) + owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, d.InstallationID, runtimedevice.HashCredential("runtime"), runtimedevice.HashCredential("runtime")) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/runtime_pending_test.go b/services/core/tests/integration/runtime_pending_test.go index 8245750f5..fd52739bc 100644 --- a/services/core/tests/integration/runtime_pending_test.go +++ b/services/core/tests/integration/runtime_pending_test.go @@ -27,7 +27,7 @@ func TestManagedRuntimeAutomaticBootstrapRecoversCommittedSessions(t *testing.T) key := uuid.NewString() p := &lifecycleProvider{resources: map[string]sandbox.Info{}} start := func() *execution.Worker { - w := startWorker(t, t.Context(), s, &execution.Dispatcher{Registry: runtimegateway.NewRegistry(), ManagedRuntimes: &execution.RuntimeProvider{CoreURL: "http://core.invalid/api/v1", InstallationID: key, BackendFingerprint: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Provider: p}}) + w := startWorker(t, t.Context(), s, &execution.Dispatcher{Registry: runtimegateway.NewRegistry(), ManagedRuntimes: &execution.RuntimeProvider{CoreURL: "https://core.invalid/api/v1", SandboxLink: "wss://core.invalid/api/v1/sandbox-link", InstallationID: key, BackendFingerprint: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Provider: p}}) return w } stop := func(w *execution.Worker) { diff --git a/services/core/tests/integration/runtime_suspension_test.go b/services/core/tests/integration/runtime_suspension_test.go index 402338582..d1c309424 100644 --- a/services/core/tests/integration/runtime_suspension_test.go +++ b/services/core/tests/integration/runtime_suspension_test.go @@ -21,7 +21,7 @@ func runtimeSuspensionFixture(t *testing.T) (*Store, *Store, *pgxpool.Pool, depl w := executionWriter(t, s) tenant := uuid.NewString() _, environment := localEnvironment(t, s, tenant) - owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, uuid.NewString(), runtimedevice.HashCredential(uuid.NewString())) + owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, uuid.NewString(), runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())) if err != nil { t.Fatal(err) } @@ -276,7 +276,7 @@ func TestRuntimeSuspensionCountsUncertainCapacityUntilReleased(t *testing.T) { for _, item := range cases { tenant := uuid.NewString() _, env := localEnvironment(t, s, tenant) - owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: env.ID}, provider, runtimedevice.HashCredential(uuid.NewString())) + owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: env.ID}, provider, runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())) if err != nil { t.Fatal(err) } @@ -451,7 +451,7 @@ func TestRuntimeComputePhaseChangedAtInNodeAllocations(t *testing.T) { if err != nil { t.Fatal(err) } - allocation, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, d.InstallationID, runtimedevice.HashCredential("runtime")) + allocation, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, d.InstallationID, runtimedevice.HashCredential("runtime"), runtimedevice.HashCredential("runtime")) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/runtime_wake_hint_integration_test.go b/services/core/tests/integration/runtime_wake_hint_integration_test.go index 0d29a6720..6912a2d19 100644 --- a/services/core/tests/integration/runtime_wake_hint_integration_test.go +++ b/services/core/tests/integration/runtime_wake_hint_integration_test.go @@ -76,7 +76,7 @@ func newWakeHintIntegration(t *testing.T) *wakeHintIntegration { worker := startWorker(t, t.Context(), f.store, &execution.Dispatcher{ Registry: f.provider.registry, ManagedRuntimes: &execution.RuntimeProvider{ - CoreURL: "http://core.invalid/api/v1", InstallationID: f.key, + CoreURL: "https://core.invalid/api/v1", SandboxLink: "wss://core.invalid/api/v1/sandbox-link", InstallationID: f.key, BackendFingerprint: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Provider: provider, Suspension: &f.policy, }, diff --git a/services/core/tests/integration/sandbox_deployment_resources_test.go b/services/core/tests/integration/sandbox_deployment_resources_test.go index f4588ac29..bf0c71a12 100644 --- a/services/core/tests/integration/sandbox_deployment_resources_test.go +++ b/services/core/tests/integration/sandbox_deployment_resources_test.go @@ -32,7 +32,7 @@ func TestSandboxDeploymentMutationViewsIncludeActualResources(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, installation, runtimedevice.HashCredential(uuid.NewString())); err != nil { + if _, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, installation, runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())); err != nil { t.Fatal(err) } if _, err := s.CreateSession(t.Context(), tenant, managerSessionInput(uuid.NewString())); err != nil { diff --git a/services/core/tests/integration/sandbox_deployment_switch_test.go b/services/core/tests/integration/sandbox_deployment_switch_test.go index 7c88d88c0..3ee738766 100644 --- a/services/core/tests/integration/sandbox_deployment_switch_test.go +++ b/services/core/tests/integration/sandbox_deployment_switch_test.go @@ -115,7 +115,7 @@ func TestSandboxDirectDeploymentOwnershipAndCleanSwitch(t *testing.T) { if err != nil || len(nodes) != 1 || nodes[0] != "" { t.Fatal("cloud lifecycle requires node", nodes, err) } - owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment}, id, runtimedevice.HashCredential(uuid.NewString())) + owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment}, id, runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())) if err != nil || owner.NodeID != "" { t.Fatal(owner, err) } @@ -179,7 +179,7 @@ func TestSandboxSwitchRetiresNodesAndEnrollment(t *testing.T) { if err != nil { t.Fatal(err) } - node := deployment.Enrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("docker").Digest("docker"), NodeID: uuid.NewString(), Name: "Machine", Provider: "docker", Credential: strings.Repeat("c", 64), BackendFingerprint: strings.Repeat("b", 64)} + node := deployment.Enrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("docker").Digest("docker"), NodeID: uuid.NewString(), Name: "Machine", Provider: "docker", Credential: strings.Repeat("c", 64), BackendFingerprint: strings.Repeat("b", 64), CoreURL: s.placement.PublicURL()} if _, err := nodes.Enroll(t.Context(), token, node); err != nil { t.Fatal(err) } @@ -299,7 +299,7 @@ func TestSandboxSwitchPreservesReleasedAllocationAndItemHistory(t *testing.T) { if err != nil { t.Fatal(err) } - owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, installation, runtimedevice.HashCredential(uuid.NewString())) + owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, installation, runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())) if err != nil { t.Fatal(err) } @@ -385,7 +385,7 @@ func TestUnspecifiedNodeDeploymentRejectedWithoutMutation(t *testing.T) { if err != nil { t.Fatal(err) } - node := deployment.Enrollment{DeploymentGeneration: 1, SpecificationDigest: spec.Digest("docker"), NodeID: uuid.NewString(), Name: "Legacy", Provider: "docker", Credential: strings.Repeat("l", 64), BackendFingerprint: strings.Repeat("b", 64)} + node := deployment.Enrollment{DeploymentGeneration: 1, SpecificationDigest: spec.Digest("docker"), NodeID: uuid.NewString(), Name: "Legacy", Provider: "docker", Credential: strings.Repeat("l", 64), BackendFingerprint: strings.Repeat("b", 64), CoreURL: s.placement.PublicURL()} if _, err := nodes.Enroll(t.Context(), token, node); err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/sandbox_deployment_switch_worker_test.go b/services/core/tests/integration/sandbox_deployment_switch_worker_test.go index e43dd7ae3..e34f7e4a6 100644 --- a/services/core/tests/integration/sandbox_deployment_switch_worker_test.go +++ b/services/core/tests/integration/sandbox_deployment_switch_worker_test.go @@ -35,13 +35,13 @@ func TestSandboxWorkerSwitchesAndRecoversFailedActivation(t *testing.T) { if err != nil || setup.Provider == "" { return nil, err } - return &execution.RuntimeProvider{InstallationID: id, ProviderKind: setup.Provider, Generation: setup.Generation, Mode: setup.Mode, AdmissionPaused: setup.AdmissionPaused, CoreURL: "https://core.example/api/v1", BackendFingerprint: setup.BackendFingerprint, Provider: p}, nil + return &execution.RuntimeProvider{InstallationID: id, ProviderKind: setup.Provider, Generation: setup.Generation, Mode: setup.Mode, AdmissionPaused: setup.AdmissionPaused, CoreURL: "https://core.example/api/v1", SandboxLink: "wss://core.example/api/v1/sandbox-link", BackendFingerprint: setup.BackendFingerprint, Provider: p}, nil }, func(ctx context.Context, setup deployment.Setup) (execution.PreparedRuntimeDeployment, error) { preparations.Add(1) if fail.Load() { return execution.PreparedRuntimeDeployment{}, errors.New("fixture provider unavailable") } - return execution.PreparedRuntimeDeployment{Config: &execution.RuntimeProvider{InstallationID: setup.InstallationID, ProviderKind: setup.Provider, Mode: setup.Mode, AdmissionPaused: setup.AdmissionPaused, CoreURL: "https://core.example/api/v1", BackendFingerprint: setup.BackendFingerprint, Provider: p}}, nil + return execution.PreparedRuntimeDeployment{Config: &execution.RuntimeProvider{InstallationID: setup.InstallationID, ProviderKind: setup.Provider, Mode: setup.Mode, AdmissionPaused: setup.AdmissionPaused, CoreURL: "https://core.example/api/v1", SandboxLink: "wss://core.example/api/v1/sandbox-link", BackendFingerprint: setup.BackendFingerprint, Provider: p}}, nil }) w := startWorker(t, t.Context(), s, &execution.Dispatcher{Registry: runtimegateway.NewRegistry(), ManagedRuntimes: configuration}) ctx, cancel := context.WithCancel(t.Context()) @@ -75,7 +75,7 @@ func TestSandboxWorkerSwitchesAndRecoversFailedActivation(t *testing.T) { if err != nil { t.Fatal(err) } - node := deployment.Enrollment{NodeID: uuid.NewString(), Name: "retained candidate fixture", Provider: "docker", Credential: strings.Repeat("n", 64), BackendFingerprint: strings.Repeat("b", 64), DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("docker").Digest("docker")} + node := deployment.Enrollment{NodeID: uuid.NewString(), Name: "retained candidate fixture", Provider: "docker", Credential: strings.Repeat("n", 64), BackendFingerprint: strings.Repeat("b", 64), DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("docker").Digest("docker"), CoreURL: s.placement.PublicURL()} if _, err := deployments.Enroll(t.Context(), enrollment, node); err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/sandbox_deployment_worker_test.go b/services/core/tests/integration/sandbox_deployment_worker_test.go index 95538e64a..ee6ea8908 100644 --- a/services/core/tests/integration/sandbox_deployment_worker_test.go +++ b/services/core/tests/integration/sandbox_deployment_worker_test.go @@ -27,10 +27,10 @@ func TestSandboxDeploymentWorkerActivatesWithoutRestart(t *testing.T) { if err != nil || setup.Provider == "" { return nil, err } - return &execution.RuntimeProvider{InstallationID: setup.InstallationID, ProviderKind: setup.Provider, BackendFingerprint: setup.BackendFingerprint, CoreURL: "https://core.example/api/v1", Provider: p}, nil + return &execution.RuntimeProvider{InstallationID: setup.InstallationID, ProviderKind: setup.Provider, BackendFingerprint: setup.BackendFingerprint, CoreURL: "https://core.example/api/v1", SandboxLink: "wss://core.example/api/v1/sandbox-link", Provider: p}, nil }, func(ctx context.Context, setup deployment.Setup) (execution.PreparedRuntimeDeployment, error) { - return execution.PreparedRuntimeDeployment{Config: &execution.RuntimeProvider{InstallationID: setup.InstallationID, ProviderKind: setup.Provider, Mode: setup.Mode, AdmissionPaused: setup.AdmissionPaused, CoreURL: "https://core.example/api/v1", BackendFingerprint: setup.BackendFingerprint, Provider: p}}, nil + return execution.PreparedRuntimeDeployment{Config: &execution.RuntimeProvider{InstallationID: setup.InstallationID, ProviderKind: setup.Provider, Mode: setup.Mode, AdmissionPaused: setup.AdmissionPaused, CoreURL: "https://core.example/api/v1", SandboxLink: "wss://core.example/api/v1/sandbox-link", BackendFingerprint: setup.BackendFingerprint, Provider: p}}, nil }) start := func() (*execution.Worker, func()) { t.Helper() @@ -58,7 +58,7 @@ func TestSandboxDeploymentWorkerActivatesWithoutRestart(t *testing.T) { t.Fatal(err) } nodeID := uuid.NewString() - if _, err := deployments.Enroll(t.Context(), token, deployment.Enrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("docker").Digest("docker"), NodeID: nodeID, Name: "Remote", Provider: "docker", Credential: strings.Repeat("x", 64), BackendFingerprint: strings.Repeat("b", 64)}); err != nil { + if _, err := deployments.Enroll(t.Context(), token, deployment.Enrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("docker").Digest("docker"), NodeID: nodeID, Name: "Remote", Provider: "docker", Credential: strings.Repeat("x", 64), BackendFingerprint: strings.Repeat("b", 64), CoreURL: s.placement.PublicURL()}); err != nil { t.Fatal(err) } connect := func() { diff --git a/services/core/tests/integration/sandbox_reset_test.go b/services/core/tests/integration/sandbox_reset_test.go index 43316c0e0..0737f66b0 100644 --- a/services/core/tests/integration/sandbox_reset_test.go +++ b/services/core/tests/integration/sandbox_reset_test.go @@ -254,7 +254,7 @@ func TestSandboxResetSnapshotCountsOfflineOwnershipOnce(t *testing.T) { s, w, process := managerFixture(t, 10, 10) // Reuse the real placement fixture, then adopt its selection as Web-managed. runtimeSuspensionSQL(t, s.pool, `UPDATE runtime_deployment SET web_managed=true,local_node_id=NULL`) - runtimeSuspensionSQL(t, s.pool, `UPDATE runtime_nodes SET deployment_generation=1,specification_digest=$1`, SandboxDeploymentTestSpec("docker").Digest("docker")) + runtimeSuspensionSQL(t, s.pool, `UPDATE runtime_nodes SET deployment_generation=1,specification_digest=$1,core_url=$2`, SandboxDeploymentTestSpec("docker").Digest("docker"), s.placement.PublicURL()) _, pending := managedArchiveSession(t, s, managerSessionInput(uuid.NewString())) tenant, suspended := managedArchiveSession(t, s, managerSessionInput(uuid.NewString())) allocation := archiveAllocation(t, w, tenant, suspended, process.InstallationID) diff --git a/services/core/tests/integration/sandbox_specification_lifecycle_test.go b/services/core/tests/integration/sandbox_specification_lifecycle_test.go index 19e9e2513..0cab3e846 100644 --- a/services/core/tests/integration/sandbox_specification_lifecycle_test.go +++ b/services/core/tests/integration/sandbox_specification_lifecycle_test.go @@ -74,7 +74,7 @@ func TestSandboxSpecificationBootstrapReadDoesNotConsumeEnrollment(t *testing.T) if _, err := nodes.NodeConfiguration(t.Context(), "", "invalid-token", 0); !errors.Is(err, deployment.ErrNodeCredential) { t.Fatal("unauthenticated configuration read", err) } - node := deployment.Enrollment{NodeID: uuid.NewString(), Name: "bootstrap", Credential: strings.Repeat("n", 64), Provider: "docker", BackendFingerprint: strings.Repeat("b", 64), DeploymentGeneration: view.Generation, SpecificationDigest: view.SpecificationDigest} + node := deployment.Enrollment{NodeID: uuid.NewString(), Name: "bootstrap", Credential: strings.Repeat("n", 64), Provider: "docker", BackendFingerprint: strings.Repeat("b", 64), DeploymentGeneration: view.Generation, SpecificationDigest: view.SpecificationDigest, CoreURL: s.placement.PublicURL()} for _, change := range []func(*deployment.Enrollment){ func(n *deployment.Enrollment) { n.DeploymentGeneration++ }, func(n *deployment.Enrollment) { n.SpecificationDigest = strings.Repeat("c", 64) }, @@ -138,7 +138,7 @@ func TestSandboxSpecificationChangesPreserveEveryRetainedResource(t *testing.T) } var owner deployment.Allocation if state != "pending" { - owner, err = deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, view.InstallationID, runtimedevice.HashCredential(uuid.NewString())) + owner, err = deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, view.InstallationID, runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())) if err != nil { t.Fatal(err) } @@ -265,7 +265,7 @@ func TestSandboxSpecificationAllocationRaceWithMaintenance(t *testing.T) { for _, session := range created { go func() { <-start - owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, view.InstallationID, runtimedevice.HashCredential(uuid.NewString())) + owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, view.InstallationID, runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())) results <- result{session, owner, err} }() } @@ -283,7 +283,7 @@ func TestSandboxSpecificationAllocationRaceWithMaintenance(t *testing.T) { result := <-results if result.err == nil { allocated++ - retry, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: result.session.Environment.ID}, view.InstallationID, runtimedevice.HashCredential(uuid.NewString())) + retry, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: result.session.Environment.ID}, view.InstallationID, runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())) if err != nil || retry.ID != result.owner.ID || !retry.Replayed { t.Fatal("maintenance changed an admitted allocation retry", err) } @@ -291,7 +291,7 @@ func TestSandboxSpecificationAllocationRaceWithMaintenance(t *testing.T) { if !errors.Is(result.err, placement.ErrResetAdmission) { t.Fatal("allocation race failed outside admission", result.err) } - if _, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: result.session.Environment.ID}, view.InstallationID, runtimedevice.HashCredential(uuid.NewString())); !errors.Is(err, placement.ErrResetAdmission) { + if _, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: result.session.Environment.ID}, view.InstallationID, runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())); !errors.Is(err, placement.ErrResetAdmission) { t.Fatal("fresh allocation passed committed maintenance", err) } } diff --git a/services/core/tests/integration/session_diagnostics_test.go b/services/core/tests/integration/session_diagnostics_test.go index 4c3996829..5526bfa3d 100644 --- a/services/core/tests/integration/session_diagnostics_test.go +++ b/services/core/tests/integration/session_diagnostics_test.go @@ -207,7 +207,7 @@ func TestDiagnosticProvisioningDetailAtomicAndPrivate(t *testing.T) { t.Fatal(err) } writer := executionWriter(t, s) - owner, err := deploymentExecution(t, writer).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, uuid.NewString(), runtimedevice.HashCredential(uuid.NewString())) + owner, err := deploymentExecution(t, writer).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, uuid.NewString(), runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/session_execution_configuration_test.go b/services/core/tests/integration/session_execution_configuration_test.go index 3f6f5f45a..c57b44886 100644 --- a/services/core/tests/integration/session_execution_configuration_test.go +++ b/services/core/tests/integration/session_execution_configuration_test.go @@ -261,7 +261,7 @@ func TestSessionExecutionConfigurationSurvivesSuspendResume(t *testing.T) { if err != nil { t.Fatal(err) } - owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, uuid.NewString(), runtimedevice.HashCredential(uuid.NewString())) + owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, uuid.NewString(), runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())) if err != nil { t.Fatal(err) } diff --git a/services/core/tools/e2b-provider/README.md b/services/core/tools/e2b-provider/README.md index 34bb368cb..553ad29f8 100644 --- a/services/core/tools/e2b-provider/README.md +++ b/services/core/tools/e2b-provider/README.md @@ -44,7 +44,7 @@ A helper holds its allocation's lock until the SDK operation returns, even after 2. Record the sandbox ID and connection material, check the sandbox domain, then read the sandbox by ID and check its ownership metadata, template and resources before writing any credential. A mismatch records a settled rejection and returns `CreateSettled` with the error. 3. Check that `/opt/oac-e2b/managed_init.py` is readable, write the managed bootstrap input to `/root/.oac/e2b/managed-bootstrap.json` and run `managed_init.py` as root. -`managed_init.py` prepares the image as the [application-managed startup](../../deploy/e2b/README.md#startup-and-security-boundary) does, writes the [Runtime bootstrap](../../../../docs/runtime-bootstrap.md) file to `/home/runtime/runtime-bootstrap.json` (mode 0600, owned by UID 1000), sets the Environment, Session and network variables and starts `oac-daemon connect --profile default --bootstrap-file /home/runtime/runtime-bootstrap.json` as UID/GID 1000. It records process handoff in `/root/.oac/e2b/managed-ready.json` and refuses to run again once any launch record exists. `BootstrapComplete` becomes true when a later inspection reads that record with the expected identity; it does not prove enrollment or native readiness. +`managed_init.py` prepares the image as the [application-managed startup](../../deploy/e2b/README.md#startup-and-security-boundary) does, writes the [Runtime bootstrap](../../../../docs/runtime-bootstrap.md) file to `/home/runtime/runtime-bootstrap.json` (mode 0600, owned by UID 1000), sets the Environment, Session and network variables and starts `oac-daemon connect --profile default --bootstrap-file /home/runtime/runtime-bootstrap.json` as UID/GID 1000. It writes the [Sandbox bootstrap](../../../../docs/sandbox-bootstrap.md) file to `/home/runtime/sandbox-io-bootstrap.json` the same way and starts `oac-sandbox-io --bootstrap-file /home/runtime/sandbox-io-bootstrap.json` as UID/GID 1000 with an empty environment. It records process handoff in `/root/.oac/e2b/managed-ready.json` and refuses to run again once any launch record exists. `BootstrapComplete` becomes true when a later inspection reads that record with the expected identity; it does not prove enrollment or native readiness. An unknown Create is never repeated. A Create whose connection material was lost can be discovered and destroyed but cannot resume bootstrap, and an unconfirmed startup requires reclaiming the whole allocation. diff --git a/services/core/tools/e2b-provider/helper_contract_generated.py b/services/core/tools/e2b-provider/helper_contract_generated.py index ebd7fadf2..cccd7d2e4 100644 --- a/services/core/tools/e2b-provider/helper_contract_generated.py +++ b/services/core/tools/e2b-provider/helper_contract_generated.py @@ -1,7 +1,7 @@ # Code generated by contractgen; DO NOT EDIT. """Adapter-private wire declarations. No SDK or repository dependency.""" ERROR_CODES = ["","invalid","ownership","exists","not_found","command_unconfirmed","unconfirmed","template_invalid","team_mismatch","unauthorized"] -MANAGED_BOOTSTRAP_FIELDS = ["TenantID","EnvironmentID","AllocationID","SessionID","DeviceID","NetworkAccess","AllowedDomains","InstallationID","RuntimeBootstrap"] +MANAGED_BOOTSTRAP_FIELDS = ["TenantID","EnvironmentID","AllocationID","SessionID","DeviceID","NetworkAccess","AllowedDomains","SandboxIO","InstallationID","RuntimeBootstrap"] MANAGED_IDENTITY_FIELDS = ["TenantID","EnvironmentID","AllocationID","SessionID","DeviceID","InstallationID"] MAX_COMMAND_INPUT = 52428832 MAX_CREDENTIAL_REFERENCES = 32 diff --git a/services/core/tools/e2b-provider/provider_test.py b/services/core/tools/e2b-provider/provider_test.py index 74d708547..316af0e70 100644 --- a/services/core/tools/e2b-provider/provider_test.py +++ b/services/core/tools/e2b-provider/provider_test.py @@ -28,7 +28,12 @@ def setUp(self): 'Reference': self.reference, 'Deadline': (datetime.now(timezone.utc) + timedelta(seconds=30)).isoformat(), 'Bootstrap': dict(self.reference, SessionID=str(uuid4()), DeviceID=str(uuid4()), CoreURL='https://core.example/api/v1', Credential='private-runtime-secret', - NetworkAccess='enabled', AllowedDomains=[])} + NetworkAccess='enabled', AllowedDomains=[], + SandboxIO={'version': 1, 'link_url': 'wss://core.example/api/v1/sandbox-link', + 'credential': 'private-serve-secret', + 'resource': {'tenant_id': self.reference['TenantID'], + 'environment_id': self.reference['EnvironmentID'], 'kind': 'allocation', + 'id': self.reference['AllocationID'], 'generation': 1}})} self.request['RuntimeBootstrap'] = { 'version': 1, 'core_url': self.request['Bootstrap']['CoreURL'], 'device_id': self.request['Bootstrap']['DeviceID'], @@ -68,6 +73,7 @@ def test_create_recover_and_never_replay(self): self.assertTrue(result['Info']['CreateSettled']) startup = json.loads(self.cloud.files.write.call_args.args[1]) self.assertEqual(startup['RuntimeBootstrap'], self.request['RuntimeBootstrap']) + self.assertEqual(startup['SandboxIO'], self.request['Bootstrap']['SandboxIO']) self.assertNotIn('CoreURL', startup) self.assertNotIn('Credential', startup) self.assertEqual(self.call('create')['ErrorCode'], 'exists') @@ -80,6 +86,7 @@ def test_create_recover_and_never_replay(self): serialized = json.dumps(self.record()) self.assertNotIn(self.config['APIKey'], serialized) self.assertNotIn(self.request['Bootstrap']['Credential'], serialized) + self.assertNotIn(self.request['Bootstrap']['SandboxIO']['credential'], serialized) self.assertEqual(self.record()['connection']['envd_access_token'], 'private-envd-secret') self.api.connect.assert_not_called() diff --git a/services/core/tools/e2b-provider/testdata/contract.json b/services/core/tools/e2b-provider/testdata/contract.json index aa79da367..8806245e5 100644 --- a/services/core/tools/e2b-provider/testdata/contract.json +++ b/services/core/tools/e2b-provider/testdata/contract.json @@ -1,57 +1,58 @@ [ -{"kind":"managed","name":"disabled","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"disabled","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":true}, -{"kind":"managed","name":"enabled","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"enabled","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":true}, -{"kind":"managed","name":"invalid-AllowedDomains","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":"example.com","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"enabled","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, -{"kind":"managed","name":"invalid-AllowedDomains","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":[1],"DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"enabled","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, -{"kind":"managed","name":"invalid-DeviceID","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"DeviceID":null,"EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"enabled","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, -{"kind":"managed","name":"invalid-DeviceID","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"DeviceID":"invalid","EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"enabled","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, -{"kind":"managed","name":"invalid-Extra","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","Extra":true,"InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"enabled","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, -{"kind":"managed","name":"invalid-NetworkAccess","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":null,"RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, -{"kind":"managed","name":"invalid-NetworkAccess","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"unknown","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, -{"kind":"managed","name":"invalid-NetworkAccess","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":true,"RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, -{"kind":"managed","name":"missing-AllocationID","payload":{"AllowedDomains":null,"DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"enabled","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, -{"kind":"managed","name":"missing-AllowedDomains","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"enabled","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, -{"kind":"managed","name":"missing-DeviceID","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"enabled","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, -{"kind":"managed","name":"missing-EnvironmentID","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"DeviceID":"55555555-5555-4555-8555-555555555555","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"enabled","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, -{"kind":"managed","name":"missing-InstallationID","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, -{"kind":"managed","name":"missing-NetworkAccess","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, -{"kind":"managed","name":"missing-RuntimeBootstrap","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"enabled","SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, -{"kind":"managed","name":"missing-SessionID","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"enabled","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, -{"kind":"managed","name":"missing-TenantID","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"enabled","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SessionID":"44444444-4444-4444-8444-444444444444"},"valid":false}, -{"kind":"managed","name":"restricted","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":["example.com"],"DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"restricted","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":true}, -{"kind":"request","name":"command","payload":{"Version":1,"Operation":"command","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":true}, -{"kind":"request","name":"config-null","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":null,"Deadline":"2099-01-01T00:00:00Z","Operation":"create","Reference":{"AllocationID":"33333333-3333-4333-8333-333333333333","EnvironmentID":"22222222-2222-4222-8222-222222222222","TenantID":"11111111-1111-4111-8111-111111111111"},"RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":1},"valid":false}, -{"kind":"request","name":"create","payload":{"Version":1,"Operation":"create","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":true}, -{"kind":"request","name":"duplicate-observation","payload":{"Version":1,"Operation":"observe","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"References":[{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"}],"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":false}, -{"kind":"request","name":"extra","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":{"APIKey":"","APIURL":"","Binary":"","Domain":"","InstallationID":"66666666-6666-4666-8666-666666666666","Resources":null,"StateDir":"","Template":"","TimeoutSeconds":0},"Deadline":"2099-01-01T00:00:00Z","Extra":true,"Operation":"create","Reference":{"AllocationID":"33333333-3333-4333-8333-333333333333","EnvironmentID":"22222222-2222-4222-8222-222222222222","TenantID":"11111111-1111-4111-8111-111111111111"},"RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":1},"valid":false}, -{"kind":"request","name":"inspect","payload":{"Version":1,"Operation":"inspect","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":true}, -{"kind":"request","name":"kill","payload":{"Version":1,"Operation":"kill","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":true}, -{"kind":"request","name":"list_builds","payload":{"Version":1,"Operation":"list_builds","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":true}, -{"kind":"request","name":"list_templates","payload":{"Version":1,"Operation":"list_templates","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":true}, -{"kind":"request","name":"observe","payload":{"Version":1,"Operation":"observe","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"References":[{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"}],"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":true}, -{"kind":"request","name":"observe-count-0","payload":{"Version":1,"Operation":"observe","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":false}, -{"kind":"request","name":"observe-count-100","payload":{"Version":1,"Operation":"observe","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"References":[{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000001-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000002-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000003-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000004-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000005-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000006-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000007-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000008-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000009-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000010-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000011-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000012-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000013-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000014-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000015-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000016-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000017-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000018-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000019-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000020-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000021-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000022-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000023-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000024-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000025-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000026-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000027-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000028-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000029-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000002a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000002b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000002c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000002d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000002e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000002f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000030-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000031-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000032-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000033-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000034-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000035-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000036-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000037-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000038-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000039-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000003a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000003b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000003c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000003d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000003e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000003f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000040-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000041-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000042-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000043-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000044-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000045-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000046-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000047-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000048-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000049-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000004a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000004b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000004c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000004d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000004e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000004f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000050-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000051-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000052-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000053-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000054-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000055-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000056-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000057-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000058-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000059-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000005a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000005b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000005c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000005d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000005e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000005f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000060-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000061-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000062-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000063-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000064-3333-4333-8333-333333333333"}],"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":true}, -{"kind":"request","name":"observe-count-101","payload":{"Version":1,"Operation":"observe","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"References":[{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000001-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000002-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000003-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000004-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000005-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000006-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000007-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000008-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000009-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000010-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000011-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000012-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000013-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000014-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000015-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000016-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000017-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000018-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000019-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000020-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000021-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000022-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000023-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000024-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000025-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000026-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000027-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000028-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000029-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000002a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000002b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000002c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000002d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000002e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000002f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000030-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000031-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000032-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000033-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000034-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000035-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000036-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000037-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000038-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000039-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000003a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000003b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000003c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000003d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000003e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000003f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000040-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000041-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000042-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000043-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000044-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000045-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000046-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000047-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000048-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000049-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000004a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000004b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000004c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000004d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000004e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000004f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000050-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000051-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000052-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000053-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000054-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000055-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000056-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000057-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000058-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000059-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000005a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000005b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000005c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000005d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000005e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000005f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000060-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000061-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000062-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000063-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000064-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000065-3333-4333-8333-333333333333"}],"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":false}, -{"kind":"request","name":"observe-references-type","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":{"APIKey":"","APIURL":"","Binary":"","Domain":"","InstallationID":"66666666-6666-4666-8666-666666666666","Resources":null,"StateDir":"","Template":"","TimeoutSeconds":0},"Deadline":"2099-01-01T00:00:00Z","Operation":"observe","Reference":{"AllocationID":"33333333-3333-4333-8333-333333333333","EnvironmentID":"22222222-2222-4222-8222-222222222222","TenantID":"11111111-1111-4111-8111-111111111111"},"References":{},"RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":1},"valid":false}, -{"kind":"request","name":"observe-references-type","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":{"APIKey":"","APIURL":"","Binary":"","Domain":"","InstallationID":"66666666-6666-4666-8666-666666666666","Resources":null,"StateDir":"","Template":"","TimeoutSeconds":0},"Deadline":"2099-01-01T00:00:00Z","Operation":"observe","Reference":{"AllocationID":"33333333-3333-4333-8333-333333333333","EnvironmentID":"22222222-2222-4222-8222-222222222222","TenantID":"11111111-1111-4111-8111-111111111111"},"References":"invalid","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":1},"valid":false}, -{"kind":"request","name":"observe-references-type","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":{"APIKey":"","APIURL":"","Binary":"","Domain":"","InstallationID":"66666666-6666-4666-8666-666666666666","Resources":null,"StateDir":"","Template":"","TimeoutSeconds":0},"Deadline":"2099-01-01T00:00:00Z","Operation":"observe","Reference":{"AllocationID":"33333333-3333-4333-8333-333333333333","EnvironmentID":"22222222-2222-4222-8222-222222222222","TenantID":"11111111-1111-4111-8111-111111111111"},"References":[null],"RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":1},"valid":false}, -{"kind":"request","name":"operation-null","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":{"APIKey":"","APIURL":"","Binary":"","Domain":"","InstallationID":"66666666-6666-4666-8666-666666666666","Resources":null,"StateDir":"","Template":"","TimeoutSeconds":0},"Deadline":"2099-01-01T00:00:00Z","Operation":null,"Reference":{"AllocationID":"33333333-3333-4333-8333-333333333333","EnvironmentID":"22222222-2222-4222-8222-222222222222","TenantID":"11111111-1111-4111-8111-111111111111"},"RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":1},"valid":false}, -{"kind":"request","name":"operation-unknown","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":{"APIKey":"","APIURL":"","Binary":"","Domain":"","InstallationID":"66666666-6666-4666-8666-666666666666","Resources":null,"StateDir":"","Template":"","TimeoutSeconds":0},"Deadline":"2099-01-01T00:00:00Z","Operation":"unsupported","Reference":{"AllocationID":"33333333-3333-4333-8333-333333333333","EnvironmentID":"22222222-2222-4222-8222-222222222222","TenantID":"11111111-1111-4111-8111-111111111111"},"RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":1},"valid":false}, -{"kind":"request","name":"reference-null","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":{"APIKey":"","APIURL":"","Binary":"","Domain":"","InstallationID":"66666666-6666-4666-8666-666666666666","Resources":null,"StateDir":"","Template":"","TimeoutSeconds":0},"Deadline":"2099-01-01T00:00:00Z","Operation":"create","Reference":null,"RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":1},"valid":false}, -{"kind":"request","name":"reference-type","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":{"APIKey":"","APIURL":"","Binary":"","Domain":"","InstallationID":"66666666-6666-4666-8666-666666666666","Resources":null,"StateDir":"","Template":"","TimeoutSeconds":0},"Deadline":"2099-01-01T00:00:00Z","Operation":"create","Reference":"invalid","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":1},"valid":false}, -{"kind":"request","name":"renew","payload":{"Version":1,"Operation":"renew","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":true}, -{"kind":"request","name":"validate_deployment","payload":{"Version":1,"Operation":"validate_deployment","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":true}, -{"kind":"request","name":"verify_credential","payload":{"Version":1,"Operation":"verify_credential","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"References":[{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"}],"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":true}, -{"kind":"request","name":"verify_credential-count-0","payload":{"Version":1,"Operation":"verify_credential","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":true}, -{"kind":"request","name":"verify_credential-count-32","payload":{"Version":1,"Operation":"verify_credential","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"References":[{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000001-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000002-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000003-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000004-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000005-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000006-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000007-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000008-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000009-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000010-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000011-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000012-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000013-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000014-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000015-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000016-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000017-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000018-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000019-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000020-3333-4333-8333-333333333333"}],"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":true}, -{"kind":"request","name":"verify_credential-count-33","payload":{"Version":1,"Operation":"verify_credential","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"References":[{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000001-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000002-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000003-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000004-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000005-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000006-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000007-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000008-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000009-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000010-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000011-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000012-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000013-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000014-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000015-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000016-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000017-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000018-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000019-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000020-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000021-3333-4333-8333-333333333333"}],"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":false}, -{"kind":"request","name":"verify_credential-references-type","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":{"APIKey":"","APIURL":"","Binary":"","Domain":"","InstallationID":"66666666-6666-4666-8666-666666666666","Resources":null,"StateDir":"","Template":"","TimeoutSeconds":0},"Deadline":"2099-01-01T00:00:00Z","Operation":"verify_credential","Reference":{"AllocationID":"33333333-3333-4333-8333-333333333333","EnvironmentID":"22222222-2222-4222-8222-222222222222","TenantID":"11111111-1111-4111-8111-111111111111"},"References":{},"RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":1},"valid":false}, -{"kind":"request","name":"verify_credential-references-type","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":{"APIKey":"","APIURL":"","Binary":"","Domain":"","InstallationID":"66666666-6666-4666-8666-666666666666","Resources":null,"StateDir":"","Template":"","TimeoutSeconds":0},"Deadline":"2099-01-01T00:00:00Z","Operation":"verify_credential","Reference":{"AllocationID":"33333333-3333-4333-8333-333333333333","EnvironmentID":"22222222-2222-4222-8222-222222222222","TenantID":"11111111-1111-4111-8111-111111111111"},"References":"invalid","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":1},"valid":false}, -{"kind":"request","name":"verify_credential-references-type","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":{"APIKey":"","APIURL":"","Binary":"","Domain":"","InstallationID":"66666666-6666-4666-8666-666666666666","Resources":null,"StateDir":"","Template":"","TimeoutSeconds":0},"Deadline":"2099-01-01T00:00:00Z","Operation":"verify_credential","Reference":{"AllocationID":"33333333-3333-4333-8333-333333333333","EnvironmentID":"22222222-2222-4222-8222-222222222222","TenantID":"11111111-1111-4111-8111-111111111111"},"References":[null],"RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":1},"valid":false}, -{"kind":"request","name":"version-bool","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":{"APIKey":"","APIURL":"","Binary":"","Domain":"","InstallationID":"66666666-6666-4666-8666-666666666666","Resources":null,"StateDir":"","Template":"","TimeoutSeconds":0},"Deadline":"2099-01-01T00:00:00Z","Operation":"create","Reference":{"AllocationID":"33333333-3333-4333-8333-333333333333","EnvironmentID":"22222222-2222-4222-8222-222222222222","TenantID":"11111111-1111-4111-8111-111111111111"},"RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":true},"valid":false}, -{"kind":"request","name":"version-null","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":{"APIKey":"","APIURL":"","Binary":"","Domain":"","InstallationID":"66666666-6666-4666-8666-666666666666","Resources":null,"StateDir":"","Template":"","TimeoutSeconds":0},"Deadline":"2099-01-01T00:00:00Z","Operation":"create","Reference":{"AllocationID":"33333333-3333-4333-8333-333333333333","EnvironmentID":"22222222-2222-4222-8222-222222222222","TenantID":"11111111-1111-4111-8111-111111111111"},"RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":null},"valid":false}, -{"kind":"request","name":"version-string","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":{"APIKey":"","APIURL":"","Binary":"","Domain":"","InstallationID":"66666666-6666-4666-8666-666666666666","Resources":null,"StateDir":"","Template":"","TimeoutSeconds":0},"Deadline":"2099-01-01T00:00:00Z","Operation":"create","Reference":{"AllocationID":"33333333-3333-4333-8333-333333333333","EnvironmentID":"22222222-2222-4222-8222-222222222222","TenantID":"11111111-1111-4111-8111-111111111111"},"RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":"1"},"valid":false}, -{"kind":"request","name":"version-unknown","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":{"APIKey":"","APIURL":"","Binary":"","Domain":"","InstallationID":"66666666-6666-4666-8666-666666666666","Resources":null,"StateDir":"","Template":"","TimeoutSeconds":0},"Deadline":"2099-01-01T00:00:00Z","Operation":"create","Reference":{"AllocationID":"33333333-3333-4333-8333-333333333333","EnvironmentID":"22222222-2222-4222-8222-222222222222","TenantID":"11111111-1111-4111-8111-111111111111"},"RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":2},"valid":false}, +{"kind":"managed","name":"disabled","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"disabled","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":true}, +{"kind":"managed","name":"enabled","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"enabled","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":true}, +{"kind":"managed","name":"invalid-AllowedDomains","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":"example.com","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"enabled","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, +{"kind":"managed","name":"invalid-AllowedDomains","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":[1],"DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"enabled","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, +{"kind":"managed","name":"invalid-DeviceID","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"DeviceID":null,"EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"enabled","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, +{"kind":"managed","name":"invalid-DeviceID","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"DeviceID":"invalid","EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"enabled","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, +{"kind":"managed","name":"invalid-Extra","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","Extra":true,"InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"enabled","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, +{"kind":"managed","name":"invalid-NetworkAccess","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":null,"RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, +{"kind":"managed","name":"invalid-NetworkAccess","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"unknown","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, +{"kind":"managed","name":"invalid-NetworkAccess","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":true,"RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, +{"kind":"managed","name":"missing-AllocationID","payload":{"AllowedDomains":null,"DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"enabled","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, +{"kind":"managed","name":"missing-AllowedDomains","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"enabled","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, +{"kind":"managed","name":"missing-DeviceID","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"enabled","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, +{"kind":"managed","name":"missing-EnvironmentID","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"DeviceID":"55555555-5555-4555-8555-555555555555","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"enabled","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, +{"kind":"managed","name":"missing-InstallationID","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, +{"kind":"managed","name":"missing-NetworkAccess","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, +{"kind":"managed","name":"missing-RuntimeBootstrap","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"enabled","SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, +{"kind":"managed","name":"missing-SandboxIO","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"enabled","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, +{"kind":"managed","name":"missing-SessionID","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"enabled","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"TenantID":"11111111-1111-4111-8111-111111111111"},"valid":false}, +{"kind":"managed","name":"missing-TenantID","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"enabled","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444"},"valid":false}, +{"kind":"managed","name":"restricted","payload":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":["example.com"],"DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","InstallationID":"66666666-6666-4666-8666-666666666666","NetworkAccess":"restricted","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"valid":true}, +{"kind":"request","name":"command","payload":{"Version":1,"Operation":"command","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null,"SandboxIO":{"version":1,"link_url":"wss://core.example/api/v1/sandbox-link","credential":"fixture-serve-only","resource":{"tenant_id":"11111111-1111-4111-8111-111111111111","environment_id":"22222222-2222-4222-8222-222222222222","kind":"allocation","id":"33333333-3333-4333-8333-333333333333","generation":1}}},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":true}, +{"kind":"request","name":"config-null","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":null,"Deadline":"2099-01-01T00:00:00Z","Operation":"create","Reference":{"AllocationID":"33333333-3333-4333-8333-333333333333","EnvironmentID":"22222222-2222-4222-8222-222222222222","TenantID":"11111111-1111-4111-8111-111111111111"},"RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":1},"valid":false}, +{"kind":"request","name":"create","payload":{"Version":1,"Operation":"create","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null,"SandboxIO":{"version":1,"link_url":"wss://core.example/api/v1/sandbox-link","credential":"fixture-serve-only","resource":{"tenant_id":"11111111-1111-4111-8111-111111111111","environment_id":"22222222-2222-4222-8222-222222222222","kind":"allocation","id":"33333333-3333-4333-8333-333333333333","generation":1}}},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":true}, +{"kind":"request","name":"duplicate-observation","payload":{"Version":1,"Operation":"observe","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"References":[{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"}],"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null,"SandboxIO":{"version":1,"link_url":"wss://core.example/api/v1/sandbox-link","credential":"fixture-serve-only","resource":{"tenant_id":"11111111-1111-4111-8111-111111111111","environment_id":"22222222-2222-4222-8222-222222222222","kind":"allocation","id":"33333333-3333-4333-8333-333333333333","generation":1}}},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":false}, +{"kind":"request","name":"extra","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":{"APIKey":"","APIURL":"","Binary":"","Domain":"","InstallationID":"66666666-6666-4666-8666-666666666666","Resources":null,"StateDir":"","Template":"","TimeoutSeconds":0},"Deadline":"2099-01-01T00:00:00Z","Extra":true,"Operation":"create","Reference":{"AllocationID":"33333333-3333-4333-8333-333333333333","EnvironmentID":"22222222-2222-4222-8222-222222222222","TenantID":"11111111-1111-4111-8111-111111111111"},"RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":1},"valid":false}, +{"kind":"request","name":"inspect","payload":{"Version":1,"Operation":"inspect","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null,"SandboxIO":{"version":1,"link_url":"wss://core.example/api/v1/sandbox-link","credential":"fixture-serve-only","resource":{"tenant_id":"11111111-1111-4111-8111-111111111111","environment_id":"22222222-2222-4222-8222-222222222222","kind":"allocation","id":"33333333-3333-4333-8333-333333333333","generation":1}}},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":true}, +{"kind":"request","name":"kill","payload":{"Version":1,"Operation":"kill","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null,"SandboxIO":{"version":1,"link_url":"wss://core.example/api/v1/sandbox-link","credential":"fixture-serve-only","resource":{"tenant_id":"11111111-1111-4111-8111-111111111111","environment_id":"22222222-2222-4222-8222-222222222222","kind":"allocation","id":"33333333-3333-4333-8333-333333333333","generation":1}}},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":true}, +{"kind":"request","name":"list_builds","payload":{"Version":1,"Operation":"list_builds","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null,"SandboxIO":{"version":1,"link_url":"wss://core.example/api/v1/sandbox-link","credential":"fixture-serve-only","resource":{"tenant_id":"11111111-1111-4111-8111-111111111111","environment_id":"22222222-2222-4222-8222-222222222222","kind":"allocation","id":"33333333-3333-4333-8333-333333333333","generation":1}}},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":true}, +{"kind":"request","name":"list_templates","payload":{"Version":1,"Operation":"list_templates","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null,"SandboxIO":{"version":1,"link_url":"wss://core.example/api/v1/sandbox-link","credential":"fixture-serve-only","resource":{"tenant_id":"11111111-1111-4111-8111-111111111111","environment_id":"22222222-2222-4222-8222-222222222222","kind":"allocation","id":"33333333-3333-4333-8333-333333333333","generation":1}}},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":true}, +{"kind":"request","name":"observe","payload":{"Version":1,"Operation":"observe","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"References":[{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"}],"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null,"SandboxIO":{"version":1,"link_url":"wss://core.example/api/v1/sandbox-link","credential":"fixture-serve-only","resource":{"tenant_id":"11111111-1111-4111-8111-111111111111","environment_id":"22222222-2222-4222-8222-222222222222","kind":"allocation","id":"33333333-3333-4333-8333-333333333333","generation":1}}},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":true}, +{"kind":"request","name":"observe-count-0","payload":{"Version":1,"Operation":"observe","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null,"SandboxIO":{"version":1,"link_url":"wss://core.example/api/v1/sandbox-link","credential":"fixture-serve-only","resource":{"tenant_id":"11111111-1111-4111-8111-111111111111","environment_id":"22222222-2222-4222-8222-222222222222","kind":"allocation","id":"33333333-3333-4333-8333-333333333333","generation":1}}},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":false}, +{"kind":"request","name":"observe-count-100","payload":{"Version":1,"Operation":"observe","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"References":[{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000001-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000002-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000003-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000004-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000005-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000006-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000007-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000008-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000009-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000010-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000011-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000012-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000013-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000014-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000015-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000016-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000017-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000018-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000019-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000020-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000021-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000022-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000023-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000024-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000025-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000026-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000027-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000028-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000029-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000002a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000002b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000002c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000002d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000002e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000002f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000030-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000031-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000032-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000033-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000034-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000035-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000036-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000037-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000038-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000039-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000003a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000003b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000003c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000003d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000003e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000003f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000040-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000041-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000042-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000043-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000044-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000045-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000046-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000047-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000048-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000049-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000004a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000004b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000004c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000004d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000004e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000004f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000050-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000051-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000052-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000053-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000054-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000055-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000056-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000057-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000058-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000059-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000005a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000005b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000005c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000005d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000005e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000005f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000060-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000061-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000062-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000063-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000064-3333-4333-8333-333333333333"}],"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null,"SandboxIO":{"version":1,"link_url":"wss://core.example/api/v1/sandbox-link","credential":"fixture-serve-only","resource":{"tenant_id":"11111111-1111-4111-8111-111111111111","environment_id":"22222222-2222-4222-8222-222222222222","kind":"allocation","id":"33333333-3333-4333-8333-333333333333","generation":1}}},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":true}, +{"kind":"request","name":"observe-count-101","payload":{"Version":1,"Operation":"observe","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"References":[{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000001-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000002-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000003-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000004-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000005-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000006-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000007-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000008-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000009-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000010-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000011-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000012-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000013-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000014-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000015-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000016-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000017-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000018-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000019-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000020-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000021-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000022-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000023-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000024-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000025-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000026-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000027-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000028-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000029-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000002a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000002b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000002c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000002d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000002e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000002f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000030-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000031-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000032-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000033-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000034-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000035-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000036-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000037-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000038-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000039-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000003a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000003b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000003c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000003d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000003e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000003f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000040-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000041-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000042-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000043-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000044-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000045-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000046-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000047-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000048-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000049-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000004a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000004b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000004c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000004d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000004e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000004f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000050-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000051-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000052-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000053-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000054-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000055-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000056-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000057-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000058-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000059-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000005a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000005b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000005c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000005d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000005e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000005f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000060-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000061-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000062-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000063-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000064-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000065-3333-4333-8333-333333333333"}],"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null,"SandboxIO":{"version":1,"link_url":"wss://core.example/api/v1/sandbox-link","credential":"fixture-serve-only","resource":{"tenant_id":"11111111-1111-4111-8111-111111111111","environment_id":"22222222-2222-4222-8222-222222222222","kind":"allocation","id":"33333333-3333-4333-8333-333333333333","generation":1}}},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":false}, +{"kind":"request","name":"observe-references-type","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":{"APIKey":"","APIURL":"","Binary":"","Domain":"","InstallationID":"66666666-6666-4666-8666-666666666666","Resources":null,"StateDir":"","Template":"","TimeoutSeconds":0},"Deadline":"2099-01-01T00:00:00Z","Operation":"observe","Reference":{"AllocationID":"33333333-3333-4333-8333-333333333333","EnvironmentID":"22222222-2222-4222-8222-222222222222","TenantID":"11111111-1111-4111-8111-111111111111"},"References":{},"RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":1},"valid":false}, +{"kind":"request","name":"observe-references-type","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":{"APIKey":"","APIURL":"","Binary":"","Domain":"","InstallationID":"66666666-6666-4666-8666-666666666666","Resources":null,"StateDir":"","Template":"","TimeoutSeconds":0},"Deadline":"2099-01-01T00:00:00Z","Operation":"observe","Reference":{"AllocationID":"33333333-3333-4333-8333-333333333333","EnvironmentID":"22222222-2222-4222-8222-222222222222","TenantID":"11111111-1111-4111-8111-111111111111"},"References":"invalid","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":1},"valid":false}, +{"kind":"request","name":"observe-references-type","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":{"APIKey":"","APIURL":"","Binary":"","Domain":"","InstallationID":"66666666-6666-4666-8666-666666666666","Resources":null,"StateDir":"","Template":"","TimeoutSeconds":0},"Deadline":"2099-01-01T00:00:00Z","Operation":"observe","Reference":{"AllocationID":"33333333-3333-4333-8333-333333333333","EnvironmentID":"22222222-2222-4222-8222-222222222222","TenantID":"11111111-1111-4111-8111-111111111111"},"References":[null],"RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":1},"valid":false}, +{"kind":"request","name":"operation-null","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":{"APIKey":"","APIURL":"","Binary":"","Domain":"","InstallationID":"66666666-6666-4666-8666-666666666666","Resources":null,"StateDir":"","Template":"","TimeoutSeconds":0},"Deadline":"2099-01-01T00:00:00Z","Operation":null,"Reference":{"AllocationID":"33333333-3333-4333-8333-333333333333","EnvironmentID":"22222222-2222-4222-8222-222222222222","TenantID":"11111111-1111-4111-8111-111111111111"},"RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":1},"valid":false}, +{"kind":"request","name":"operation-unknown","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":{"APIKey":"","APIURL":"","Binary":"","Domain":"","InstallationID":"66666666-6666-4666-8666-666666666666","Resources":null,"StateDir":"","Template":"","TimeoutSeconds":0},"Deadline":"2099-01-01T00:00:00Z","Operation":"unsupported","Reference":{"AllocationID":"33333333-3333-4333-8333-333333333333","EnvironmentID":"22222222-2222-4222-8222-222222222222","TenantID":"11111111-1111-4111-8111-111111111111"},"RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":1},"valid":false}, +{"kind":"request","name":"reference-null","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":{"APIKey":"","APIURL":"","Binary":"","Domain":"","InstallationID":"66666666-6666-4666-8666-666666666666","Resources":null,"StateDir":"","Template":"","TimeoutSeconds":0},"Deadline":"2099-01-01T00:00:00Z","Operation":"create","Reference":null,"RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":1},"valid":false}, +{"kind":"request","name":"reference-type","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":{"APIKey":"","APIURL":"","Binary":"","Domain":"","InstallationID":"66666666-6666-4666-8666-666666666666","Resources":null,"StateDir":"","Template":"","TimeoutSeconds":0},"Deadline":"2099-01-01T00:00:00Z","Operation":"create","Reference":"invalid","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":1},"valid":false}, +{"kind":"request","name":"renew","payload":{"Version":1,"Operation":"renew","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null,"SandboxIO":{"version":1,"link_url":"wss://core.example/api/v1/sandbox-link","credential":"fixture-serve-only","resource":{"tenant_id":"11111111-1111-4111-8111-111111111111","environment_id":"22222222-2222-4222-8222-222222222222","kind":"allocation","id":"33333333-3333-4333-8333-333333333333","generation":1}}},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":true}, +{"kind":"request","name":"validate_deployment","payload":{"Version":1,"Operation":"validate_deployment","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null,"SandboxIO":{"version":1,"link_url":"wss://core.example/api/v1/sandbox-link","credential":"fixture-serve-only","resource":{"tenant_id":"11111111-1111-4111-8111-111111111111","environment_id":"22222222-2222-4222-8222-222222222222","kind":"allocation","id":"33333333-3333-4333-8333-333333333333","generation":1}}},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":true}, +{"kind":"request","name":"verify_credential","payload":{"Version":1,"Operation":"verify_credential","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"References":[{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"}],"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null,"SandboxIO":{"version":1,"link_url":"wss://core.example/api/v1/sandbox-link","credential":"fixture-serve-only","resource":{"tenant_id":"11111111-1111-4111-8111-111111111111","environment_id":"22222222-2222-4222-8222-222222222222","kind":"allocation","id":"33333333-3333-4333-8333-333333333333","generation":1}}},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":true}, +{"kind":"request","name":"verify_credential-count-0","payload":{"Version":1,"Operation":"verify_credential","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null,"SandboxIO":{"version":1,"link_url":"wss://core.example/api/v1/sandbox-link","credential":"fixture-serve-only","resource":{"tenant_id":"11111111-1111-4111-8111-111111111111","environment_id":"22222222-2222-4222-8222-222222222222","kind":"allocation","id":"33333333-3333-4333-8333-333333333333","generation":1}}},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":true}, +{"kind":"request","name":"verify_credential-count-32","payload":{"Version":1,"Operation":"verify_credential","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"References":[{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000001-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000002-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000003-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000004-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000005-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000006-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000007-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000008-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000009-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000010-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000011-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000012-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000013-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000014-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000015-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000016-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000017-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000018-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000019-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000020-3333-4333-8333-333333333333"}],"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null,"SandboxIO":{"version":1,"link_url":"wss://core.example/api/v1/sandbox-link","credential":"fixture-serve-only","resource":{"tenant_id":"11111111-1111-4111-8111-111111111111","environment_id":"22222222-2222-4222-8222-222222222222","kind":"allocation","id":"33333333-3333-4333-8333-333333333333","generation":1}}},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":true}, +{"kind":"request","name":"verify_credential-count-33","payload":{"Version":1,"Operation":"verify_credential","Config":{"Binary":"","StateDir":"","InstallationID":"66666666-6666-4666-8666-666666666666","APIKey":"","Template":"","APIURL":"","Domain":"","TimeoutSeconds":0,"Resources":null},"Reference":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333"},"References":[{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000001-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000002-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000003-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000004-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000005-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000006-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000007-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000008-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000009-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000000f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000010-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000011-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000012-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000013-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000014-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000015-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000016-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000017-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000018-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000019-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001a-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001b-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001c-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001d-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001e-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"0000001f-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000020-3333-4333-8333-333333333333"},{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"00000021-3333-4333-8333-333333333333"}],"Bootstrap":{"TenantID":"11111111-1111-4111-8111-111111111111","EnvironmentID":"22222222-2222-4222-8222-222222222222","AllocationID":"33333333-3333-4333-8333-333333333333","SessionID":"44444444-4444-4444-8444-444444444444","DeviceID":"55555555-5555-4555-8555-555555555555","CoreURL":"https://core.example/api/v1","Credential":"fixture-only","NetworkAccess":"enabled","AllowedDomains":null,"SandboxIO":{"version":1,"link_url":"wss://core.example/api/v1/sandbox-link","credential":"fixture-serve-only","resource":{"tenant_id":"11111111-1111-4111-8111-111111111111","environment_id":"22222222-2222-4222-8222-222222222222","kind":"allocation","id":"33333333-3333-4333-8333-333333333333","generation":1}}},"RuntimeBootstrap":{"version":1,"core_url":"https://core.example/api/v1","device_id":"55555555-5555-4555-8555-555555555555","credential":"fixture-only"},"Deadline":"2099-01-01T00:00:00Z"},"valid":false}, +{"kind":"request","name":"verify_credential-references-type","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":{"APIKey":"","APIURL":"","Binary":"","Domain":"","InstallationID":"66666666-6666-4666-8666-666666666666","Resources":null,"StateDir":"","Template":"","TimeoutSeconds":0},"Deadline":"2099-01-01T00:00:00Z","Operation":"verify_credential","Reference":{"AllocationID":"33333333-3333-4333-8333-333333333333","EnvironmentID":"22222222-2222-4222-8222-222222222222","TenantID":"11111111-1111-4111-8111-111111111111"},"References":{},"RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":1},"valid":false}, +{"kind":"request","name":"verify_credential-references-type","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":{"APIKey":"","APIURL":"","Binary":"","Domain":"","InstallationID":"66666666-6666-4666-8666-666666666666","Resources":null,"StateDir":"","Template":"","TimeoutSeconds":0},"Deadline":"2099-01-01T00:00:00Z","Operation":"verify_credential","Reference":{"AllocationID":"33333333-3333-4333-8333-333333333333","EnvironmentID":"22222222-2222-4222-8222-222222222222","TenantID":"11111111-1111-4111-8111-111111111111"},"References":"invalid","RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":1},"valid":false}, +{"kind":"request","name":"verify_credential-references-type","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":{"APIKey":"","APIURL":"","Binary":"","Domain":"","InstallationID":"66666666-6666-4666-8666-666666666666","Resources":null,"StateDir":"","Template":"","TimeoutSeconds":0},"Deadline":"2099-01-01T00:00:00Z","Operation":"verify_credential","Reference":{"AllocationID":"33333333-3333-4333-8333-333333333333","EnvironmentID":"22222222-2222-4222-8222-222222222222","TenantID":"11111111-1111-4111-8111-111111111111"},"References":[null],"RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":1},"valid":false}, +{"kind":"request","name":"version-bool","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":{"APIKey":"","APIURL":"","Binary":"","Domain":"","InstallationID":"66666666-6666-4666-8666-666666666666","Resources":null,"StateDir":"","Template":"","TimeoutSeconds":0},"Deadline":"2099-01-01T00:00:00Z","Operation":"create","Reference":{"AllocationID":"33333333-3333-4333-8333-333333333333","EnvironmentID":"22222222-2222-4222-8222-222222222222","TenantID":"11111111-1111-4111-8111-111111111111"},"RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":true},"valid":false}, +{"kind":"request","name":"version-null","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":{"APIKey":"","APIURL":"","Binary":"","Domain":"","InstallationID":"66666666-6666-4666-8666-666666666666","Resources":null,"StateDir":"","Template":"","TimeoutSeconds":0},"Deadline":"2099-01-01T00:00:00Z","Operation":"create","Reference":{"AllocationID":"33333333-3333-4333-8333-333333333333","EnvironmentID":"22222222-2222-4222-8222-222222222222","TenantID":"11111111-1111-4111-8111-111111111111"},"RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":null},"valid":false}, +{"kind":"request","name":"version-string","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":{"APIKey":"","APIURL":"","Binary":"","Domain":"","InstallationID":"66666666-6666-4666-8666-666666666666","Resources":null,"StateDir":"","Template":"","TimeoutSeconds":0},"Deadline":"2099-01-01T00:00:00Z","Operation":"create","Reference":{"AllocationID":"33333333-3333-4333-8333-333333333333","EnvironmentID":"22222222-2222-4222-8222-222222222222","TenantID":"11111111-1111-4111-8111-111111111111"},"RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":"1"},"valid":false}, +{"kind":"request","name":"version-unknown","payload":{"Bootstrap":{"AllocationID":"33333333-3333-4333-8333-333333333333","AllowedDomains":null,"CoreURL":"https://core.example/api/v1","Credential":"fixture-only","DeviceID":"55555555-5555-4555-8555-555555555555","EnvironmentID":"22222222-2222-4222-8222-222222222222","NetworkAccess":"enabled","SandboxIO":{"credential":"fixture-serve-only","link_url":"wss://core.example/api/v1/sandbox-link","resource":{"environment_id":"22222222-2222-4222-8222-222222222222","generation":1,"id":"33333333-3333-4333-8333-333333333333","kind":"allocation","tenant_id":"11111111-1111-4111-8111-111111111111"},"version":1},"SessionID":"44444444-4444-4444-8444-444444444444","TenantID":"11111111-1111-4111-8111-111111111111"},"Config":{"APIKey":"","APIURL":"","Binary":"","Domain":"","InstallationID":"66666666-6666-4666-8666-666666666666","Resources":null,"StateDir":"","Template":"","TimeoutSeconds":0},"Deadline":"2099-01-01T00:00:00Z","Operation":"create","Reference":{"AllocationID":"33333333-3333-4333-8333-333333333333","EnvironmentID":"22222222-2222-4222-8222-222222222222","TenantID":"11111111-1111-4111-8111-111111111111"},"RuntimeBootstrap":{"core_url":"https://core.example/api/v1","credential":"fixture-only","device_id":"55555555-5555-4555-8555-555555555555","version":1},"Version":2},"valid":false}, {"kind":"response","name":"error-","payload":{"Version":1,"ErrorCode":""},"valid":true}, {"kind":"response","name":"error-command_unconfirmed","payload":{"Version":1,"ErrorCode":"command_unconfirmed"},"valid":true}, {"kind":"response","name":"error-exists","payload":{"Version":1,"ErrorCode":"exists"},"valid":true}, diff --git a/services/core/tools/microsandbox-provider/README.md b/services/core/tools/microsandbox-provider/README.md index 554b4fad8..7d1d3d3ac 100644 --- a/services/core/tools/microsandbox-provider/README.md +++ b/services/core/tools/microsandbox-provider/README.md @@ -20,7 +20,7 @@ Create names the VM from a hash of the installation and allocation reference plu Workspace, staging and outputs share the `/environment` filesystem, which keeps the Runtime's cross-device and link checks intact; the layered root filesystem can report different device IDs for a directory and its upper-layer files, so it holds no workspace data. Full snapshots and sandbox removal capture, restore and reclaim this disk; there is no host path, external mount or separate storage lifecycle. -VM creation does not run the image's entry point. The bootstrap runs as root through confidential standard input, with a two-minute limit. It creates the Runtime directories and the private control directory `/run/oac` (mode 0700, owned by UID 1000), writes the [Runtime bootstrap](../../../../docs/runtime-bootstrap.md) file to `/home/runtime/runtime-bootstrap.json`, bind-mounts `/environment/workspace` at `/workspace` and starts `oac-daemon connect --bootstrap-file` in the background as UID/GID 1000. `OAC_RUNTIME_DAEMON_SUSPEND_PID_FILE=/run/oac/daemon-suspend.json` enables the daemon's park and wake control. The `io.oac.bootstrap` label then changes from `pending` to `complete` through the SDK's next-start modification policy, because v0.7.2 cannot update the labels of a running VM. That label confirms only these writes and the launch, not authentication or native readiness. +VM creation does not run the image's entry point. The bootstrap runs as root through confidential standard input, with a two-minute limit. It creates the Runtime directories and the private control directory `/run/oac` (mode 0700, owned by UID 1000), writes the [Runtime bootstrap](../../../../docs/runtime-bootstrap.md) file to `/home/runtime/runtime-bootstrap.json`, bind-mounts `/environment/workspace` at `/workspace` and starts `oac-daemon connect --bootstrap-file` in the background as UID/GID 1000. It writes the [Sandbox bootstrap](../../../../docs/sandbox-bootstrap.md) file to `/home/runtime/sandbox-io-bootstrap.json` (mode 0600, owned by UID 1000) and starts `oac-sandbox-io --bootstrap-file` with that path, in the background as UID/GID 1000 with an empty environment. `OAC_RUNTIME_DAEMON_SUSPEND_PID_FILE=/run/oac/daemon-suspend.json` enables the daemon's park and wake control. The `io.oac.bootstrap` label then changes from `pending` to `complete` through the SDK's next-start modification policy, because v0.7.2 cannot update the labels of a running VM. That label confirms only these writes and the launch, not authentication or native readiness. A helper response carries `CreateSettled` with a configuration rejection only after native Create has completed, the first inspection has verified the exact compute ID and ownership, and the resource check has rejected the VM before bootstrap started. The adapter keeps the original error and validates the compute identity before passing the proof to Core. Ordinary inspection, uncertain Create outcomes, timeouts and ownership failures never produce it, and missing compute alone never proves that Create settled. diff --git a/services/core/tools/microsandbox-provider/bootstrap.go b/services/core/tools/microsandbox-provider/bootstrap.go index ad1af890a..d74ef9921 100644 --- a/services/core/tools/microsandbox-provider/bootstrap.go +++ b/services/core/tools/microsandbox-provider/bootstrap.go @@ -13,8 +13,9 @@ import ( sdk "github.com/superradcompany/microsandbox/sdk/go" ) -// Runtime reads the shared launch input; its private auth storage stays opaque. -// All credential bytes enter the guest on stdin before any native work is admitted. +// Runtime and the Sandbox I/O service read their launch inputs; Runtime's +// private auth storage stays opaque. All credential bytes enter the guest on +// stdin before any native work is admitted. const bootstrapScript = ` import ctypes,json,os,stat,subprocess,sys b=json.load(sys.stdin) @@ -22,11 +23,15 @@ for p in ['/home/runtime','/home/runtime/.oac','/environment','/environment/work os.makedirs(p,mode=0o700,exist_ok=True) if not stat.S_ISDIR(os.lstat(p).st_mode): raise RuntimeError('invalid bootstrap directory') os.chmod(p,0o700);os.chown(p,1000,1000) +def private(p,v): + fd=os.open(p,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600) + with os.fdopen(fd,'w') as f: + json.dump(v,f) + f.flush();os.fsync(f.fileno());os.fchown(f.fileno(),1000,1000) p='/home/runtime/runtime-bootstrap.json' -fd=os.open(p,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600) -with os.fdopen(fd,'w') as f: - json.dump(b,f) - f.flush();os.fsync(f.fileno());os.fchown(f.fileno(),1000,1000) +private(p,b['Runtime']) +s='/home/runtime/sandbox-io-bootstrap.json' +private(s,b['SandboxIO']) if not stat.S_ISDIR(os.lstat('/workspace').st_mode): raise RuntimeError('invalid workspace alias') libc=ctypes.CDLL(None,use_errno=True) if libc.mount(b'/environment/workspace',b'/workspace',None,4096,None)!=0: @@ -36,6 +41,9 @@ def runtime_user(): subprocess.run(['/usr/local/bin/oac-daemon','connect','--profile','default','--bootstrap-file',p,'-b'], stdin=subprocess.DEVNULL,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL, cwd='/environment/workspace',preexec_fn=runtime_user,check=True) +subprocess.Popen(['/usr/local/bin/oac-sandbox-io','--bootstrap-file',s],env={},start_new_session=True, + stdin=subprocess.DEVNULL,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL, + cwd='/environment/workspace',preexec_fn=runtime_user) ` func (b backend) create(ctx context.Context) (wire.Response, error) { @@ -79,7 +87,15 @@ func (b backend) create(ctx context.Context) (wire.Response, error) { if e != nil { return qualified, e } - data, e := bootstrap.RuntimeConnection().Marshal() + connection, e := bootstrap.RuntimeConnection().Marshal() + if e != nil { + return wire.Response{}, e + } + serve, e := bootstrap.SandboxIO.Marshal() + if e != nil { + return wire.Response{}, e + } + data, e := json.Marshal(struct{ Runtime, SandboxIO json.RawMessage }{connection, serve}) if e != nil { return wire.Response{}, e } diff --git a/services/core/tools/microsandbox-provider/go.mod b/services/core/tools/microsandbox-provider/go.mod index 25d0423c5..1483c8963 100644 --- a/services/core/tools/microsandbox-provider/go.mod +++ b/services/core/tools/microsandbox-provider/go.mod @@ -9,6 +9,9 @@ require ( require ( github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/libp2p/go-buffer-pool v0.0.2 // indirect + github.com/libp2p/go-yamux/v5 v5.1.0 // indirect golang.org/x/sync v0.22.0 // indirect ) diff --git a/services/core/tools/microsandbox-provider/go.sum b/services/core/tools/microsandbox-provider/go.sum index 77cb4e04c..28c8c14f3 100644 --- a/services/core/tools/microsandbox-provider/go.sum +++ b/services/core/tools/microsandbox-provider/go.sum @@ -1,6 +1,62 @@ +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= +github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/libp2p/go-buffer-pool v0.0.2 h1:QNK2iAFa8gjAe1SPz6mHSMuCcjs+X1wlHzeOSqcmlfs= +github.com/libp2p/go-buffer-pool v0.0.2/go.mod h1:MvaB6xw5vOrDl8rYZGLFdKAuk/hRoRZd1Vi32+RXyFM= +github.com/libp2p/go-yamux/v5 v5.1.0 h1:8Qlxj4E9JGJAQVW6+uj2o7mqkqsIVlSUGmTWhlXzoHE= +github.com/libp2p/go-yamux/v5 v5.1.0/go.mod h1:tgIQ07ObtRR/I0IWsFOyQIL9/dR5UXgc2s8xKmNZv1o= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/moby/api v1.56.0 h1:GQzua3NA599ASSIICx0iFgiJeO9YkdDARvQsm23ZZuQ= +github.com/moby/moby/api v1.56.0/go.mod h1:sZ+THbVWkjOmBPPfbnzdD/G1LuIexWhqlSHHPTDQ1Uk= +github.com/moby/moby/client v0.6.0 h1:AJjEB21QPbXSXjDsZorFBoDZPhMrfbpaPLgSMAW9Bgs= +github.com/moby/moby/client v0.6.0/go.mod h1:OCo00wNRyA3m4lmJ228W3JbyCN4ZNNYjpOXiJydBdcQ= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/superradcompany/microsandbox/sdk/go v0.7.2 h1:aO70srBRgu50rNZTtzQTOO3xxPSthkZLkq3kjWvmsqg= github.com/superradcompany/microsandbox/sdk/go v0.7.2/go.mod h1:p7Tm/p9zkO7sHO3N8A1fiTVeLB2w/fQoKdYlpN/5neA= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 156220baf981aa5071398f2b715252dcddfce925 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 7 Oct 2026 19:00:23 +0000 Subject: [PATCH 2/8] Require an https non-loopback public URL for every hosted sandbox Every hosted sandbox runs outside Core's network namespace and dials the sandbox Link, so selection and admission reject a loopback or plain-http public URL for every provider with ErrPublicURLUnreachable. The rule no longer depends on a per-provider declaration, so the PublicOrigin configuration requirement is deleted. --- apps/web/e2e/fixture-console.mjs | 4 +- apps/web/e2e/public-url.spec.ts | 4 +- .../features/sandbox/SandboxSetupWizard.tsx | 2 +- apps/web/src/i18n/locales/en/core-errors.ts | 2 +- .../web/src/i18n/locales/zh-CN/core-errors.ts | 2 +- apps/web/src/lib/locale-strings.ts | 2 +- contracts/agents-api/core-errors.md | 2 +- contracts/agents-api/core.openapi.yaml | 2 +- contracts/agents-api/sandbox-deployment.md | 2 +- contracts/agents-api/zh/core-errors.md | 4 +- contracts/agents-api/zh/sandbox-deployment.md | 4 +- docs/configuration.md | 2 +- docs/getting-started/install.md | 2 +- docs/sandbox-provider.md | 2 +- docs/web/console-api-usage.md | 2 +- docs/zh/configuration.md | 4 +- docs/zh/getting-started/install.md | 4 +- docs/zh/sandbox-provider.md | 4 +- docs/zh/web/console-api-usage.md | 4 +- .../agents-client/src/sandbox-client.test.ts | 4 +- packages/agents-client/src/sandbox-client.ts | 2 +- .../internal/api/sandbox_deployment_setup.go | 2 +- .../deployment/placement/placement.go | 28 ++++---- .../deployment/placement/placement_test.go | 68 ++++++++----------- services/core/internal/deployment/service.go | 3 +- .../core/internal/deployment/service_test.go | 18 ++--- .../core/internal/sandbox/configuration.go | 5 +- .../internal/sandbox/e2b/configuration.go | 2 +- .../sandbox/providers/configuration.go | 12 +--- .../providers/configuration_flow_test.go | 2 +- .../sandbox/providers/configuration_test.go | 18 +++-- .../providers/registration_configuration.go | 4 +- .../registration_configuration_test.go | 29 +++----- .../sandbox/providers/registration_test.go | 6 -- .../core/internal/sessions/creation_test.go | 2 - 35 files changed, 108 insertions(+), 151 deletions(-) diff --git a/apps/web/e2e/fixture-console.mjs b/apps/web/e2e/fixture-console.mjs index 1145cd52d..c3883c869 100644 --- a/apps/web/e2e/fixture-console.mjs +++ b/apps/web/e2e/fixture-console.mjs @@ -387,8 +387,8 @@ async function sandboxRoute(request, response, path, url) { 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. - if (e2b && state.installation === "local") return error(response, 409, "E2B sandboxes reach Core over the internet. Set an HTTPS public URL that is not loopback (public_url in config.json, OAC_PUBLIC_URL for Core).", "sandbox_configuration_error"); + // As Core (ErrPublicURLUnreachable): sandboxes reach Core from outside its host, which a loopback public_url cannot serve. + if (state.installation === "local") return error(response, 409, "Sandboxes reach Core from outside its host. Set an HTTPS public URL that is not loopback (public_url in config.json, OAC_PUBLIC_URL for Core).", "sandbox_configuration_error"); // Synthetic classifier outcomes only; never persist or echo submitted keys. if (e2b) { if (!input.configuration?.template || (initialize && !input.credential?.api_key) || (Object.hasOwn(input, "credential") && !input.credential?.api_key)) return error(response, 400, "The E2B API key was rejected.", "sandbox_credential_invalid"); diff --git a/apps/web/e2e/public-url.spec.ts b/apps/web/e2e/public-url.spec.ts index 2b3a36a27..ec409eaf6 100644 --- a/apps/web/e2e/public-url.spec.ts +++ b/apps/web/e2e/public-url.spec.ts @@ -28,10 +28,10 @@ test("explains an E2B rejection in the wizard, with a link to domain setup", asy await selectFixtureE2BBuild(page); await page.getByRole("button", { name: "Next" }).click(); const address = page.getByRole("definition").filter({ hasText: "http://127.0.0.1:8091" }); - await expect(address).toContainText("Set a public address before connecting remote nodes"); + await expect(address).toContainText("Set an HTTPS public address before saving"); await page.getByRole("button", { name: "Save configuration" }).click(); const rejection = page.locator(".wizard-rejection"); - await expect(rejection).toContainText("E2B sandboxes need a public HTTPS address."); + await expect(rejection).toContainText("Sandboxes need an HTTPS public address that is not loopback."); await expect(rejection.getByRole("button", { name: "Managed in System" })).toBeVisible(); // Nothing was saved and nothing is uncertain: no dialog, and the wizard stays on its review. await expect(page.getByRole("dialog")).toHaveCount(0); diff --git a/apps/web/src/features/sandbox/SandboxSetupWizard.tsx b/apps/web/src/features/sandbox/SandboxSetupWizard.tsx index 79a29f2d7..c0306d156 100644 --- a/apps/web/src/features/sandbox/SandboxSetupWizard.tsx +++ b/apps/web/src/features/sandbox/SandboxSetupWizard.tsx @@ -379,7 +379,7 @@ export function SandboxSetupWizard({ coreUrl, expectedGeneration, current, disab
{address ? {address} : "—"} {t("Managed in System")} - {installation.data?.local_only ? {t("Set a public address before connecting remote nodes; E2B sandboxes need an HTTPS one.")} : null} + {installation.data?.local_only ? {t("Set an HTTPS public address before saving; sandboxes and remote nodes can't reach this one.")} : null}
diff --git a/apps/web/src/i18n/locales/en/core-errors.ts b/apps/web/src/i18n/locales/en/core-errors.ts index 1ffbc659e..3e7c7ccee 100644 --- a/apps/web/src/i18n/locales/en/core-errors.ts +++ b/apps/web/src/i18n/locales/en/core-errors.ts @@ -32,7 +32,7 @@ export const coreErrors = { "sandbox_credential_invalid": "The E2B API key was rejected. The saved configuration is unchanged.", "sandbox_configuration_invalid": "Select a ready immutable E2B template build with matching resources.", "sandbox_verification_unconfirmed": "E2B verification could not be confirmed. Refresh before submitting again.", - "sandbox_configuration_error": "E2B sandboxes need a public HTTPS address. Set OAC_PUBLIC_URL to an HTTPS origin.", + "sandbox_configuration_error": "Sandboxes need an HTTPS public address that is not loopback. Set OAC_PUBLIC_URL to an HTTPS origin.", "sandbox_deployment_conflict": "The sandbox deployment cannot change in its current state. Refresh and check its reset and resource state.", "sandbox_specification_mismatch": "The saved sandbox specification does not match the deployment. Refresh to check the configuration.", "sandbox_operation_unsupported": "The selected sandbox provider does not support this operation.", diff --git a/apps/web/src/i18n/locales/zh-CN/core-errors.ts b/apps/web/src/i18n/locales/zh-CN/core-errors.ts index 0c42e7c88..384f54c21 100644 --- a/apps/web/src/i18n/locales/zh-CN/core-errors.ts +++ b/apps/web/src/i18n/locales/zh-CN/core-errors.ts @@ -31,7 +31,7 @@ export const coreErrors = { "sandbox_credential_invalid": "E2B API 密钥被拒绝。已保存的配置未改变。", "sandbox_configuration_invalid": "请选择已就绪且资源匹配的不可变 E2B 模板构建。", "sandbox_verification_unconfirmed": "无法确认 E2B 验证结果。请刷新后再提交。", - "sandbox_configuration_error": "E2B 沙箱需要可从互联网访问的 HTTPS 地址,请把 OAC_PUBLIC_URL 设为一个 HTTPS 源地址。", + "sandbox_configuration_error": "沙箱需要非回环的 HTTPS 公开地址,请把 OAC_PUBLIC_URL 设为一个 HTTPS 源地址。", "sandbox_deployment_conflict": "沙箱部署在当前状态下无法更改。请刷新并检查重置和资源状态。", "sandbox_specification_mismatch": "已保存的沙箱规格与部署不一致。请刷新检查配置。", "sandbox_operation_unsupported": "所选沙箱提供商不支持此操作。", diff --git a/apps/web/src/lib/locale-strings.ts b/apps/web/src/lib/locale-strings.ts index 880588c9c..d4a7f8050 100644 --- a/apps/web/src/lib/locale-strings.ts +++ b/apps/web/src/lib/locale-strings.ts @@ -425,7 +425,7 @@ export const chinese = { "Change the sandbox configuration": "修改沙箱配置", "The address nodes and sandboxes use to reach Core.": "节点和沙箱访问 Core 使用的地址。", "Managed in System": "在系统中管理", - "Set a public address before connecting remote nodes; E2B sandboxes need an HTTPS one.": "连接远程节点前,请先设置公开地址;E2B 沙箱需要 HTTPS 地址。", + "Set an HTTPS public address before saving; sandboxes and remote nodes can't reach this one.": "保存前请设置 HTTPS 公开地址;沙箱和远程节点无法访问当前地址。", "Enter the E2B key again to save.": "请重新输入 E2B key 后再保存。", "Enter the key": "输入 key", "{{name}} is still bound to an old Core address. Remove it and add it again.": "{{name}} 仍绑定在旧的 Core 地址上,需要移除后重新添加。", diff --git a/contracts/agents-api/core-errors.md b/contracts/agents-api/core-errors.md index bffa4481a..d731dec8a 100644 --- a/contracts/agents-api/core-errors.md +++ b/contracts/agents-api/core-errors.md @@ -86,7 +86,7 @@ These codes have null `param` and no `details`. [Sandbox deployment](./sandbox-d | 409 | `executor_credential_exists` | The executor credential ID already exists; rotate it to replace the secret | | 409 | `sandbox_not_configured` | The sandbox deployment is not configured | | 409 or 503 | `sandbox_reset_in_progress` | A sandbox reset is in progress | -| 409 | `sandbox_configuration_error` | The installation cannot serve the selected provider, such as E2B while the public URL is loopback | +| 409 | `sandbox_configuration_error` | The installation cannot serve the selected provider, such as any provider while the public URL is loopback or not https | | 409 | `sandbox_deployment_conflict` | The sandbox deployment cannot change in its current state | | 409 | `sandbox_specification_mismatch` | The saved deployment specification is no longer valid for its provider | | 409 | `runtime_node_in_use` | The node still holds allocations, snapshots, reservations or pending cleanup | diff --git a/contracts/agents-api/core.openapi.yaml b/contracts/agents-api/core.openapi.yaml index c04c1097c..6686ba285 100644 --- a/contracts/agents-api/core.openapi.yaml +++ b/contracts/agents-api/core.openapi.yaml @@ -6222,7 +6222,7 @@ paths: post: consumes: - application/json - 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. + 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. Every provider returns 409 sandbox_configuration_error while the public URL is loopback or not https. 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. parameters: - description: Deployment selection in: body diff --git a/contracts/agents-api/sandbox-deployment.md b/contracts/agents-api/sandbox-deployment.md index dbee59e5c..eea486ebc 100644 --- a/contracts/agents-api/sandbox-deployment.md +++ b/contracts/agents-api/sandbox-deployment.md @@ -39,7 +39,7 @@ POST and PUT take the same complete selection and require `expected_generation` | `configuration` | The provider's public selectors. E2B: the immutable `template` build and the optional paired `api_url` and `domain`. Docker and microsandbox accept only `{}` or omission | | `credential` | The provider's write-only credential. E2B: `{api_key}`, required at first setup and omitted on PUT to keep the current key; a null or empty key is invalid. Docker and microsandbox reject it | -The request has no Core address. Core derives the deployment's `core_url` from the installation public URL (`public_url` in `config.json`, `OAC_PUBLIC_URL` for Core): the origin nodes and sandbox guests use to reach Core. A request that contains `core_url` is rejected with 400 `invalid_request` like any other unknown member. E2B guests reach Core from E2B's cloud, so an E2B selection is rejected with 409 `sandbox_configuration_error` while the public URL is loopback. Every hosted sandbox dials the [sandbox Link](../../docs/configuration.md#changing-the-public-url), so any selection is rejected the same way while the public URL is http on a host that is not loopback. Docker and microsandbox selections accept a loopback public URL, which serves only local development because a guest's loopback address does not reach its host. Changing the public URL is an installation change: nodes enrolled with the old address receive no new sandboxes and must be removed and added again. +The request has no Core address. Core derives the deployment's `core_url` from the installation public URL (`public_url` in `config.json`, `OAC_PUBLIC_URL` for Core): the origin nodes and sandbox guests use to reach Core. A request that contains `core_url` is rejected with 400 `invalid_request` like any other unknown member. Every hosted sandbox runs outside Core's network namespace and dials the [sandbox Link](../../docs/configuration.md#changing-the-public-url), so every selection is rejected with 409 `sandbox_configuration_error` until the public URL is https on a host that is not loopback: a loopback host names the sandbox's own namespace, and an http origin elsewhere has no Link. Changing the public URL is an installation change: nodes enrolled with the old address receive no new sandboxes and must be removed and added again. ### Resources diff --git a/contracts/agents-api/zh/core-errors.md b/contracts/agents-api/zh/core-errors.md index 02cfd5bc3..851754d2f 100644 --- a/contracts/agents-api/zh/core-errors.md +++ b/contracts/agents-api/zh/core-errors.md @@ -1,7 +1,7 @@ --- title: "Core 管理错误" source: contracts/agents-api/core-errors.md -source_hash: 50b9624c14d3d600f991fcc9da741b6c4c4722831568c28e849a574bb34a4b06 +source_hash: 56fd21c7a7be2ddbc7a3c4163473d03fd6d72de05ba79f57d0389ae768f5d534 --- `/core/v1` 上的错误使用此封装结构。`message` 是安全的英文文本;`code` 和 `param` 可以为 null。客户端依据稳定的 `code` 和可选的 `param` 进行处理,对未知代码显示 `message`,绝不解析消息,也绝不自动重试被拒绝的写操作。 @@ -88,7 +88,7 @@ Web 的控制台服务器在 `/core` 路径上发生自身故障时使用此封 | 409 | `executor_credential_exists` | 该执行器凭证 ID 已存在;要替换密钥,请轮换它 | | 409 | `sandbox_not_configured` | 沙箱部署尚未配置 | | 409 或 503 | `sandbox_reset_in_progress` | 沙箱正在重置 | -| 409 | `sandbox_configuration_error` | 当前安装无法支持所选提供商,例如公开 URL 为 loopback 时选择 E2B | +| 409 | `sandbox_configuration_error` | 当前安装无法支持所选提供商,例如公开 URL 为 loopback 或不是 https 时的任何提供商 | | 409 | `sandbox_deployment_conflict` | 沙箱部署在当前状态下无法更改 | | 409 | `sandbox_specification_mismatch` | 已保存的部署规格对其提供商不再有效 | | 409 | `runtime_node_in_use` | 节点仍有资源分配、快照、预留资源或待清理项 | diff --git a/contracts/agents-api/zh/sandbox-deployment.md b/contracts/agents-api/zh/sandbox-deployment.md index 7b5620408..2ab6371d9 100644 --- a/contracts/agents-api/zh/sandbox-deployment.md +++ b/contracts/agents-api/zh/sandbox-deployment.md @@ -1,7 +1,7 @@ --- title: "沙箱部署" source: contracts/agents-api/sandbox-deployment.md -source_hash: 4e75b037063e5d83f9c0528b5dae4e0284c7c3e2ab9c88a699bd334f2e388576 +source_hash: b1285a55dc2dc836f5a8b97f1a7c9283181dcd35d0344f03f9c1e79dccad095c --- 沙箱部署为 Core 管理的 `openai_hosted` 执行选择 Sandbox Provider、每个沙箱的资源以及不可变的 Runtime 发行版。PostgreSQL 为每个安装维护一个当前有效选择;Web 和 Core API 写入同一配置。节点文件保存其已安装副本和特定于主机的路径,且不能覆盖其资源或 Runtime。该选择独立于 Harness;部署可以保持未配置状态,既无节点,也不接受托管准入。 @@ -41,7 +41,7 @@ POST 和 PUT 接受相同的完整选择,并要求提供先前 GET 返回的 ` | `configuration` | 提供商的公开选择器。E2B:不可变的 `template` 构建以及可选且配套的 `api_url` 和 `domain`。Docker 和 microsandbox 仅接受 `{}` 或省略 | | `credential` | 提供商的只写凭据。E2B:`{api_key}`,首次设置时必填,在 PUT 中省略以保留当前密钥;null 或空密钥无效。Docker 和 microsandbox 拒绝该字段 | -请求中没有 Core 地址。Core 根据安装公开 URL(`config.json` 中的 `public_url`,Core 对应 `OAC_PUBLIC_URL`)派生部署的 `core_url`:这是节点和沙箱客户机访问 Core 时使用的源地址。包含 `core_url` 的请求会像包含任何其他未知成员一样被拒绝,并返回 400 `invalid_request`。E2B 客户机从 E2B 云访问 Core,因此当公开 URL 为回环地址时,E2B 选择会被拒绝,并返回 409 `sandbox_configuration_error`。每个托管沙箱都要连接[沙箱 Link](../../../docs/zh/configuration.md#changing-the-public-url),因此当公开 URL 是非回环主机上的 http 地址时,任何选择都会以同样方式被拒绝。Docker 和 microsandbox 选择接受回环公开 URL,但这仅适用于本地开发,因为客户机的回环地址无法访问其主机。更改公开 URL 属于安装变更:使用旧地址注册的节点不会收到新沙箱,必须移除后重新添加。 +请求中没有 Core 地址。Core 根据安装公开 URL(`config.json` 中的 `public_url`,Core 对应 `OAC_PUBLIC_URL`)派生部署的 `core_url`:这是节点和沙箱客户机访问 Core 时使用的源地址。包含 `core_url` 的请求会像包含任何其他未知成员一样被拒绝,并返回 400 `invalid_request`。每个托管沙箱都运行在 Core 的网络命名空间之外,并连接[沙箱 Link](../../../docs/zh/configuration.md#changing-the-public-url),因此在公开 URL 改为非回环主机上的 https 地址之前,任何选择都会被拒绝,并返回 409 `sandbox_configuration_error`:回环主机指向沙箱自身的命名空间,而其他主机上的 http 源地址没有 Link。更改公开 URL 属于安装变更:使用旧地址注册的节点不会收到新沙箱,必须移除后重新添加。 ### 资源 {#resources} diff --git a/docs/configuration.md b/docs/configuration.md index 2e67248b0..ae09a0fba 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -31,7 +31,7 @@ Use `docker compose ps` to check the services. See [stop and restart](./getting- `OAC_PUBLIC_URL` is the one origin that applications, nodes, sandboxes and self-hosted executors use. Core derives the daemon WebSocket URL, the sandbox Link URL, the self-hosted `remote_url` and each sandbox's connection address from it. It is an http or https origin: the address browsers and nodes use. The installation serves Web over HTTP on `OAC_WEB_PORT`; a reverse proxy or hosting platform terminates HTTPS when you put one in front. -Sandboxes and agent hosts dial the [sandbox Link](./sandbox-link-protocol.md) at `wss:///api/v1/sandbox-link` when the origin is https. An http origin on `localhost` or a loopback address gives `ws:///api/v1/sandbox-link`, which only peers in Core's own network namespace can reach. An http origin on any other host gives no Link URL: until the origin is https, nothing can use the Link and Core selects and admits no hosted sandbox. +Sandboxes and agent hosts dial the [sandbox Link](./sandbox-link-protocol.md) at `wss:///api/v1/sandbox-link` when the origin is https. An http origin on `localhost` or a loopback address gives `ws:///api/v1/sandbox-link`, which only peers in Core's own network namespace, such as a colocated agent host, can reach. An http origin on any other host gives no Link URL. Hosted sandboxes run outside Core's network namespace, so Core selects and admits them only while the origin is https on a host that is not loopback. To change it, point the reverse proxy at the new address first, then edit `OAC_PUBLIC_URL` and run `oac apply`. Afterwards: diff --git a/docs/getting-started/install.md b/docs/getting-started/install.md index e254ba35a..aad1ccfcc 100644 --- a/docs/getting-started/install.md +++ b/docs/getting-started/install.md @@ -75,7 +75,7 @@ For insufficient space or quota, free space on the filesystem named by the error ## Configure the public address {#configure-the-domain-and-https} -Applications, nodes and sandboxes reach Core at one address, the public URL. HTTP is enough on the local network. When you expose Core beyond it, put a reverse proxy in front and set the public URL to the HTTPS origin it serves. E2B guests reach Core from the internet, so they need a public URL that is not loopback. +Applications, nodes and sandboxes reach Core at one address, the public URL. HTTP is enough for applications and nodes on the local network. Hosted sandboxes need an HTTPS public URL on a host that is not loopback: put a reverse proxy in front and set the public URL to the HTTPS origin it serves. 1. Point your reverse proxy at Web. 2. Set `OAC_PUBLIC_URL` to the HTTPS origin it serves, then run `oac apply`. See [changing the public URL](../configuration.md#changing-the-public-url). diff --git a/docs/sandbox-provider.md b/docs/sandbox-provider.md index 3f5838a8a..66edd5675 100644 --- a/docs/sandbox-provider.md +++ b/docs/sandbox-provider.md @@ -128,7 +128,7 @@ A new provider takes these steps: - The Runtime input policy either accepts the pinned Runtime or gives the adapter's fixed reason for rejecting it, never both. - Checkpoint support requires node mode and positive idle and retention defaults that fit Runtime durations; a provider without checkpoint support configures no suspension defaults. -The configuration adapter must be non-nil, including its concrete value. Every `ConfigurationRequirements` field needs an explicit valid decision: `Credential` and `PublicOrigin` are `Required` or `NotRequired`, and `Discovery` uses the shared supported or unsupported declaration with a safe reason. A new requirement field or discovery method needs an explicit validation update and never inherits an existing decision. Configuration discovery is distinct from resource selection discovery, and requiring a credential does not promise the `VerifyCredential` operation. These checks establish complete registration, not correct native SDK behavior; constructor and adapter contract tests still apply. +The configuration adapter must be non-nil, including its concrete value. Every `ConfigurationRequirements` field needs an explicit valid decision: `Credential` is `Required` or `NotRequired`, and `Discovery` uses the shared supported or unsupported declaration with a safe reason. A new requirement field or discovery method needs an explicit validation update and never inherits an existing decision. Configuration discovery is distinct from resource selection discovery, and requiring a credential does not promise the `VerifyCredential` operation. These checks establish complete registration, not correct native SDK behavior; constructor and adapter contract tests still apply. ### Configuration storage and construction diff --git a/docs/web/console-api-usage.md b/docs/web/console-api-usage.md index b81088cf4..9db0c5509 100644 --- a/docs/web/console-api-usage.md +++ b/docs/web/console-api-usage.md @@ -95,7 +95,7 @@ The list carries each harness's configuration, so the console does not read `GET | Operation | Route | Console use | | --- | --- | --- | -| Deployment | `GET`, `POST`, `PUT /core/v1/sandbox/deployment` | Read the provider, the read-only `core_url` (`OAC_PUBLIC_URL`, shown in the setup review and never sent), reset state, installation ID and specification; a 409 `sandbox_configuration_error` (E2B with a loopback `public_url`) shows the shared client's fixed safe address-configuration message in the setup wizard, with Managed in System leading to System, and leaves nothing to confirm; initialize the deployment with `resources` and the Docker or microsandbox `runtime` release, or with the E2B account and no `resources` (Core adopts the template build's CPU and memory); change its settings with the expected generation. E2B's `metadata.template_build` (status, CPU, memory, disk) shows on System, the Sandbox configuration summary and Sandbox metrics, and sizes each sandbox when `specification.resources` is missing; microsandbox's `suspension` (idle and retention seconds) shows on System and the Nodes summary | +| Deployment | `GET`, `POST`, `PUT /core/v1/sandbox/deployment` | Read the provider, the read-only `core_url` (`OAC_PUBLIC_URL`, shown in the setup review and never sent), reset state, installation ID and specification; a 409 `sandbox_configuration_error` (any provider while `public_url` is loopback or not https) shows the shared client's fixed safe address-configuration message in the setup wizard, with Managed in System leading to System, and leaves nothing to confirm; initialize the deployment with `resources` and the Docker or microsandbox `runtime` release, or with the E2B account and no `resources` (Core adopts the template build's CPU and memory); change its settings with the expected generation. E2B's `metadata.template_build` (status, CPU, memory, disk) shows on System, the Sandbox configuration summary and Sandbox metrics, and sizes each sandbox when `specification.resources` is missing; microsandbox's `suspension` (idle and retention seconds) shows on System and the Nodes summary | | E2B discovery | `POST /core/v1/sandbox/providers/e2b/discovery` | The setup wizard lists the templates the entered E2B key can see, then the selected template's ready builds. The key travels only in these request bodies and the deployment write | | Reset | `POST`, `DELETE /core/v1/sandbox/deployment/reset` | Explicitly clear hosted resources, or cancel the remaining clear at the observed generation; show Core's remaining and offline projection | | Nodes | `GET /core/v1/sandbox/nodes` | Nodes page; fleet on Overview; node capacity on Sandbox metrics. An online node's `diagnostic` (`docker_unavailable`, `docker_limits_unsupported`, `runtime_image_unavailable`, `kvm_unavailable`, `microsandbox_artifacts_unavailable`, `capacity_insufficient`, `provider_unavailable`; any other value reads as `provider_unavailable`) marks it degraded and names the reason and fix in the help tip beside its status on each of these and on the node's page. A node whose `core_url` (the address it enrolled with) differs from the deployment's `core_url` is named on the Nodes page as bound to an old address, to be removed and added again, and its status there and on its page reads Old address instead of its health; an empty `core_url` (a node Core did not enroll) is unknown, not old. **Add node** follows only the node whose `enrollment_id` equals its command's | diff --git a/docs/zh/configuration.md b/docs/zh/configuration.md index 3866a92cb..cbf32b9ac 100644 --- a/docs/zh/configuration.md +++ b/docs/zh/configuration.md @@ -1,7 +1,7 @@ --- title: "配置参考" source: docs/configuration.md -source_hash: 1ef873d0104c482bef9b947a945cefb5141889ffcba2007506e0407c986bf8bb +source_hash: 104a52b9f4f2840326a0a06d30f508435cf0606d5d83c1aee4cdb7473877ebab --- Core 安装的每项设置都恰好只有一个归属位置,分属以下三类: @@ -33,7 +33,7 @@ Web 的 **System** 页面显示该安装的地址、默认模型和沙箱配置 `OAC_PUBLIC_URL` 是应用、节点、沙箱和自托管执行器使用的唯一源地址。Core 从中派生守护进程 WebSocket URL、沙箱 Link URL、自托管 `remote_url` 和每个沙箱的连接地址。它是 http 或 https 源地址,也就是浏览器和节点使用的地址。安装通过 `OAC_WEB_PORT` 以 HTTP 提供 Web;前面有反向代理或托管平台时,由它们终止 HTTPS。 -源地址为 https 时,沙箱和 agent host 通过 `wss:///api/v1/sandbox-link` 连接[沙箱 Link](./sandbox-link-protocol.md)。`localhost` 或回环地址上的 http 源地址得到 `ws:///api/v1/sandbox-link`,只有 Core 自身网络命名空间内的 peer 能访问。其他主机上的 http 源地址没有 Link URL:在源地址改为 https 之前,任何组件都无法使用 Link,Core 也不会选择或准入任何托管沙箱。 +源地址为 https 时,沙箱和 agent host 通过 `wss:///api/v1/sandbox-link` 连接[沙箱 Link](./sandbox-link-protocol.md)。`localhost` 或回环地址上的 http 源地址得到 `ws:///api/v1/sandbox-link`,只有 Core 自身网络命名空间内的 peer(例如同机的 agent host)能访问。其他主机上的 http 源地址没有 Link URL。托管沙箱运行在 Core 的网络命名空间之外,因此只有源地址是非回环主机上的 https 地址时,Core 才会选择和准入托管沙箱。 要更改它,先把反向代理指向新地址,然后编辑 `OAC_PUBLIC_URL` 并运行 `oac apply`。之后: diff --git a/docs/zh/getting-started/install.md b/docs/zh/getting-started/install.md index 8e755c993..d2cf1f2d1 100644 --- a/docs/zh/getting-started/install.md +++ b/docs/zh/getting-started/install.md @@ -1,7 +1,7 @@ --- title: "安装 Core 和 Web" source: docs/getting-started/install.md -source_hash: 282d54e2c234a8e0d1d3161cf9f2bb6f0946fd60c2878d5a1c4eaf67bd6955a0 +source_hash: 4a340f0b9ebaf1c1477373c0a4279fcd3548340b2cdbef3b43d233e5872cef44 --- 一条命令即可在 Linux、macOS 或 Windows 上安装 Core、Web 控制台和 PostgreSQL。用 Core 密钥登录 Web,设置默认模型并签发 Project API 密钥。应用使用这些密钥调用 Core。Session 在你添加的节点上的沙箱中运行,也可以在 E2B 上运行。 @@ -77,7 +77,7 @@ curl -fsSL https://github.com/MiniMax-AI/OpenAgentCore/releases/latest/download/ ## 配置公开地址 {#configure-the-domain-and-https} -应用、节点和沙箱通过同一个地址访问 Core,即公开 URL。局域网上用 HTTP 即可。对外暴露时,在前面放反向代理,并把公开 URL 设为它提供的 HTTPS 源地址。E2B 客户机从互联网访问 Core,因此需要非回环的公开 URL。 +应用、节点和沙箱通过同一个地址访问 Core,即公开 URL。局域网上的应用和节点用 HTTP 即可。托管沙箱需要非回环主机上的 HTTPS 公开 URL:在前面放反向代理,并把公开 URL 设为它提供的 HTTPS 源地址。 1. 把反向代理指向 Web。 2. 把 `OAC_PUBLIC_URL` 设为反向代理提供的 HTTPS 源地址,然后运行 `oac apply`。见[修改公开 URL](../configuration.md#changing-the-public-url)。 diff --git a/docs/zh/sandbox-provider.md b/docs/zh/sandbox-provider.md index c8af7d82a..7004b681b 100644 --- a/docs/zh/sandbox-provider.md +++ b/docs/zh/sandbox-provider.md @@ -1,7 +1,7 @@ --- title: "添加 Sandbox Provider" source: docs/sandbox-provider.md -source_hash: 729f9dd34c2810e08347a57a192c0509619960faefec1a1876f87fa7a7bf0367 +source_hash: c6bcdf8b09e317b9e2dd0e6afc3b872918b8756144982908f9b90953686b09cc --- **Sandbox Provider** 为 Core 管理的 Environment 提供 Runtime daemon 运行所需的外层计算资源,以及启动 daemon 的有界引导流程。本指南说明如何添加 Provider,并作为 Core 驱动 Provider 的参考。接口为 [`SandboxProvider`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/internal/sandbox/sandbox_provider.go)。 @@ -130,7 +130,7 @@ Checkpoint 支持增加 `Compute` generation、name、ID 和 `SnapshotIdentity` - Runtime input policy 要么接受固定 Runtime,要么给出 adapter 拒绝它的固定原因,不能两者兼有。 - Checkpoint 支持要求 node mode 和适合 Runtime duration 的正 idle、retention 默认值;不支持 checkpoint 的 provider 不配置 suspension 默认值。 -configuration adapter 必须非 nil,包括其具体值。每个 `ConfigurationRequirements` 字段都需要明确有效的决定:`Credential` 和 `PublicOrigin` 为 `Required` 或 `NotRequired`,`Discovery` 使用共享 supported 或 unsupported 声明并携带安全 reason。新增 requirement field 或 discovery method 需要明确更新验证,不继承已有决定。configuration discovery 与 resource selection discovery 不同,要求凭据也不承诺支持 `VerifyCredential` 操作。这些检查证明注册完整,不证明原生 SDK 行为正确;constructor 和 adapter 契约测试仍然适用。 +configuration adapter 必须非 nil,包括其具体值。每个 `ConfigurationRequirements` 字段都需要明确有效的决定:`Credential` 为 `Required` 或 `NotRequired`,`Discovery` 使用共享 supported 或 unsupported 声明并携带安全 reason。新增 requirement field 或 discovery method 需要明确更新验证,不继承已有决定。configuration discovery 与 resource selection discovery 不同,要求凭据也不承诺支持 `VerifyCredential` 操作。这些检查证明注册完整,不证明原生 SDK 行为正确;constructor 和 adapter 契约测试仍然适用。 ### 配置存储与构造 {#configuration-storage-and-construction} diff --git a/docs/zh/web/console-api-usage.md b/docs/zh/web/console-api-usage.md index a6037092a..c6c053c83 100644 --- a/docs/zh/web/console-api-usage.md +++ b/docs/zh/web/console-api-usage.md @@ -1,7 +1,7 @@ --- title: "控制台 API 使用" source: docs/web/console-api-usage.md -source_hash: 50edc63c79976e9aad9590604a0e361aae178144bc8a6009989b2da5d031408d +source_hash: 6278225ab39366f8cad392969669a6b5e25a1d4e48b116ac31c53fd5167404d1 --- 本页列出各控制台页面读取和写入的 Core 路由,以及控制台如何限定读取范围。[administrator API contract](../../../contracts/agents-api/zh/admin-api.md) 定义了路由、响应结构、分页和审计记录;[API namespaces and credentials](../api/index.md) 定义了本文使用的术语。 @@ -97,7 +97,7 @@ source_hash: 50edc63c79976e9aad9590604a0e361aae178144bc8a6009989b2da5d031408d | 操作 | 路由 | 控制台用途 | | --- | --- | --- | -| 部署 | `GET`、`POST`、`PUT /core/v1/sandbox/deployment` | 读取提供商、只读 `core_url`(即 `OAC_PUBLIC_URL`,会显示在设置审核中且绝不发送)、重置状态、安装 ID 和规范;409 `sandbox_configuration_error`(E2B 搭配回环地址形式的 `public_url`)会在设置向导中显示共享客户端固定的安全地址配置消息,并通过 Managed in System 前往 System,且无需确认;使用 `resources` 以及 Docker 或 microsandbox 的 `runtime` release 初始化部署,或者使用 E2B 账户且不提供 `resources`(Core 采用模板构建的 CPU 和内存);使用预期的 generation 更改设置。E2B 的 `metadata.template_build`(状态、CPU、内存、磁盘)会显示在 System、Sandbox 配置摘要和 Sandbox metrics 中;当缺少 `specification.resources` 时,它还会确定每个 Sandbox 的大小;microsandbox 的 `suspension`(空闲和保留秒数)会显示在 System 和 Nodes 摘要中 | +| 部署 | `GET`、`POST`、`PUT /core/v1/sandbox/deployment` | 读取提供商、只读 `core_url`(即 `OAC_PUBLIC_URL`,会显示在设置审核中且绝不发送)、重置状态、安装 ID 和规范;409 `sandbox_configuration_error`(`public_url` 为回环地址或不是 https 时的任何提供商)会在设置向导中显示共享客户端固定的安全地址配置消息,并通过 Managed in System 前往 System,且无需确认;使用 `resources` 以及 Docker 或 microsandbox 的 `runtime` release 初始化部署,或者使用 E2B 账户且不提供 `resources`(Core 采用模板构建的 CPU 和内存);使用预期的 generation 更改设置。E2B 的 `metadata.template_build`(状态、CPU、内存、磁盘)会显示在 System、Sandbox 配置摘要和 Sandbox metrics 中;当缺少 `specification.resources` 时,它还会确定每个 Sandbox 的大小;microsandbox 的 `suspension`(空闲和保留秒数)会显示在 System 和 Nodes 摘要中 | | E2B 发现 | `POST /core/v1/sandbox/providers/e2b/discovery` | 设置向导先列出输入的 E2B 密钥可见的模板,再列出所选模板的可用构建。该密钥只会通过这些请求体和部署写入请求传输 | | 重置 | `POST`、`DELETE /core/v1/sandbox/deployment/reset` | 显式清除托管资源,或在观测到的 generation 处取消剩余清除;显示 Core 的剩余资源和离线预测 | | Nodes | `GET /core/v1/sandbox/nodes` | Nodes 页面;Overview 上的机群;Sandbox metrics 中的节点容量。在线节点的 `diagnostic`(`docker_unavailable`、`docker_limits_unsupported`、`runtime_image_unavailable`、`kvm_unavailable`、`microsandbox_artifacts_unavailable`、`capacity_insufficient`、`provider_unavailable`;任何其他值均读取为 `provider_unavailable`)会将其标记为降级,并在上述每个页面及节点页面中,紧邻状态的帮助提示里说明原因和修复方法。如果节点的 `core_url`(其注册时使用的地址)与部署的 `core_url` 不同,Nodes 页面会将其标记为绑定到旧地址,需要移除后重新添加;此时它在该页面和节点页面中的状态会显示 Old address,而不是健康状态;如果 `core_url` 为空(Core 未注册该节点),则状态为未知,而不是旧地址。**Add node** 仅跟踪 `enrollment_id` 与其命令所含 `enrollment_id` 相等的节点 | diff --git a/packages/agents-client/src/sandbox-client.test.ts b/packages/agents-client/src/sandbox-client.test.ts index 7567263b4..52fca2bf7 100644 --- a/packages/agents-client/src/sandbox-client.test.ts +++ b/packages/agents-client/src/sandbox-client.test.ts @@ -269,7 +269,7 @@ describe("hosted provider configuration", () => { expect(fetch).toHaveBeenCalledTimes(2); }); it("projects public-URL rejection to fixed copy even when the upstream message reflects a key", async () => { - const rejection = { message: "E2B sandboxes reach Core over the internet. Set an HTTPS public URL that is not loopback.", code: "sandbox_configuration_error", param: null, type: "reflected" }; + const rejection = { message: "Sandboxes reach Core from outside its host. Set an HTTPS public URL that is not loopback.", code: "sandbox_configuration_error", param: null, type: "reflected" }; const fetch = vi.fn() .mockResolvedValueOnce(response({ error: rejection }, 409)) .mockResolvedValueOnce(response({ error: { ...rejection, message: rejection.message + e2b.api_key } }, 409)); @@ -381,7 +381,7 @@ it("keeps omitted-key public-URL errors actionable without reflecting a stored k const fetch = vi.fn().mockResolvedValue(response({ error: { code: "sandbox_configuration_error", message: `arbitrary upstream ${secret}`, param: secret, details: { credential: secret } } }, 409)); const client = new SandboxAdminClient({ fetch }); const error = await client.updateDeployment({ provider: "e2b", expected_generation: 1, configuration: { template: e2bDeployment.configuration.template } }).catch(error => error); - expect(error).toMatchObject({ status: 409, code: "sandbox_configuration_error", param: null, message: "E2B sandboxes reach Core over the internet. Set an HTTPS public URL that is not loopback." }); + expect(error).toMatchObject({ status: 409, code: "sandbox_configuration_error", param: null, message: "Sandboxes reach Core from outside its host. Set an HTTPS public URL that is not loopback." }); expect(JSON.stringify(error)).not.toContain(secret); expect(error.message).not.toContain(secret); expect(error.details).toBeUndefined(); diff --git a/packages/agents-client/src/sandbox-client.ts b/packages/agents-client/src/sandbox-client.ts index 256ee7001..962fa54ac 100644 --- a/packages/agents-client/src/sandbox-client.ts +++ b/packages/agents-client/src/sandbox-client.ts @@ -402,7 +402,7 @@ export class SandboxAdminClient { // This code has one fixed Core meaning. Never forward its raw message or // param: an omitted key cannot be used to detect a reflected stored key. if (error instanceof AgentCoreError && error.status === 409 && error.code === "sandbox_configuration_error") { - throw new AgentCoreError("E2B sandboxes reach Core over the internet. Set an HTTPS public URL that is not loopback.", 409, "sandbox_configuration_error", null); + throw new AgentCoreError("Sandboxes reach Core from outside its host. Set an HTTPS public URL that is not loopback.", 409, "sandbox_configuration_error", null); } // Any other credential-bearing rejection may reflect the key in any error field. throw new AgentCoreError("Sandbox configuration could not be confirmed. Refresh before submitting again.", error instanceof AgentCoreError ? error.status : 0, sandboxConfigurationUnconfirmed); diff --git a/services/core/internal/api/sandbox_deployment_setup.go b/services/core/internal/api/sandbox_deployment_setup.go index b58295306..376b7d397 100644 --- a/services/core/internal/api/sandbox_deployment_setup.go +++ b/services/core/internal/api/sandbox_deployment_setup.go @@ -51,7 +51,7 @@ type DeploymentReset interface { } // @Summary Initialize the deployment sandbox provider -// @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. +// @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. Every provider returns 409 sandbox_configuration_error while the public URL is loopback or not https. 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. // @Tags Sandbox Manager // @Produce json // @Security DeploymentAdminAuth diff --git a/services/core/internal/deployment/placement/placement.go b/services/core/internal/deployment/placement/placement.go index 433e6f6b8..024cdc494 100644 --- a/services/core/internal/deployment/placement/placement.go +++ b/services/core/internal/deployment/placement/placement.go @@ -11,6 +11,7 @@ import ( "fmt" "net" "net/url" + "strings" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" ) @@ -23,8 +24,8 @@ var ( // admit: paused for maintenance, without a valid specification, or for // another installation. ErrAdmissionClosed = errors.New("environment is no longer available") - // ErrPublicURLUnreachable rejects selection and admission when the provider - // requires a reachable public origin and the installation is loopback. + // ErrPublicURLUnreachable rejects hosted selection and admission while the + // installation public URL is loopback or not https. ErrPublicURLUnreachable = errors.New("This sandbox provider needs a reachable HTTPS public URL before they can connect to Core.") // ErrNodesPreparing rejects placement while no node serves the target // generation and at least one is preparing it. @@ -37,9 +38,6 @@ var ( // Declarations are the provider declarations placement reads. // *providers.Registry satisfies it. type Declarations interface { - // RequiresPublicOrigin reports whether the provider's guests must reach - // Core at a public origin. - RequiresPublicOrigin(provider string) (bool, error) // ValidateSpecification rejects a specification the provider cannot run. ValidateSpecification(provider string, spec sandbox.DeploymentSpec) error } @@ -116,14 +114,12 @@ type Restore struct { GenerationReady bool } -// CheckPublicOrigin rejects a provider that requires a reachable public -// origin while the installation public URL is loopback. -func (r *Rules) CheckPublicOrigin(provider string) error { - required, err := r.declarations.RequiresPublicOrigin(provider) - if err != nil { - return err - } - if required && LoopbackOrigin(r.publicURL) { +// CheckPublicOrigin rejects hosted sandboxes while the installation public URL +// is loopback or not https. Every hosted sandbox runs outside Core's network +// namespace and dials the sandbox Link: a loopback host names the sandbox's +// own namespace, and an http origin elsewhere has no Link. +func (r *Rules) CheckPublicOrigin() error { + if !strings.HasPrefix(r.publicURL, "https://") || LoopbackOrigin(r.publicURL) { return ErrPublicURLUnreachable } return nil @@ -163,10 +159,10 @@ func (r *Rules) DecidePlacement(d Deployment, nodes []Node) (*Placement, error) if d.Resetting { return nil, ErrResetAdmission } - // A changed installation address cannot admit guests that require a - // public origin. Existing owned resources remain available for cleanup. + // A changed installation address cannot admit hosted sandboxes. Existing + // owned resources remain available for cleanup. if d.Provider != "" { - if err := r.CheckPublicOrigin(d.Provider); err != nil { + if err := r.CheckPublicOrigin(); err != nil { return nil, err } } diff --git a/services/core/internal/deployment/placement/placement_test.go b/services/core/internal/deployment/placement/placement_test.go index ff7212cde..640bf4c45 100644 --- a/services/core/internal/deployment/placement/placement_test.go +++ b/services/core/internal/deployment/placement/placement_test.go @@ -8,7 +8,6 @@ import ( "github.com/google/uuid" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/providers" ) const publicURL = "https://core.example" @@ -17,17 +16,9 @@ const publicURL = "https://core.example" // test left nil fails the test. type fakeDeclarations struct { t *testing.T - requiresPublicOrigin func(provider string) (bool, error) validateSpecification func(provider string, spec sandbox.DeploymentSpec) error } -func (f *fakeDeclarations) RequiresPublicOrigin(provider string) (bool, error) { - if f.requiresPublicOrigin == nil { - f.t.Fatalf("unexpected RequiresPublicOrigin(%q)", provider) - } - return f.requiresPublicOrigin(provider) -} - func (f *fakeDeclarations) ValidateSpecification(provider string, spec sandbox.DeploymentSpec) error { if f.validateSpecification == nil { f.t.Fatalf("unexpected ValidateSpecification(%q)", provider) @@ -55,20 +46,20 @@ func TestNewRulesRequiresDeclarations(t *testing.T) { } } -// The built-in registry satisfies the declarations the rules read. -func TestRegistryDeclaresPublicOrigin(t *testing.T) { - loopback := rules(t, providers.Builtin(), "http://127.0.0.1:8091") - if err := loopback.CheckPublicOrigin("e2b"); !errors.Is(err, ErrPublicURLUnreachable) { - t.Fatalf("CheckPublicOrigin(e2b) on loopback = %v", err) - } - if err := loopback.CheckPublicOrigin("docker"); err != nil { - t.Fatalf("CheckPublicOrigin(docker) on loopback = %v", err) - } - if err := rules(t, providers.Builtin(), publicURL).CheckPublicOrigin("e2b"); err != nil { - t.Fatalf("CheckPublicOrigin(e2b) on a public URL = %v", err) - } - if err := loopback.CheckPublicOrigin("unknown"); err == nil { - t.Fatal("CheckPublicOrigin accepted an unknown provider") +// Hosted sandboxes need an https public URL on a host that is not loopback. +func TestCheckPublicOrigin(t *testing.T) { + for url, want := range map[string]error{ + publicURL: nil, + "http://127.0.0.1:8091": ErrPublicURLUnreachable, + "http://localhost:8091": ErrPublicURLUnreachable, + "http://10.0.0.5:8091": ErrPublicURLUnreachable, + "https://localhost": ErrPublicURLUnreachable, + "https://127.0.0.1:8443": ErrPublicURLUnreachable, + "": ErrPublicURLUnreachable, + } { + if err := rules(t, &fakeDeclarations{t: t}, url).CheckPublicOrigin(); err != want { + t.Errorf("CheckPublicOrigin on %q = %v", url, err) + } } } @@ -111,9 +102,6 @@ func TestCheckAdmission(t *testing.T) { } func TestDecidePlacement(t *testing.T) { - origin := func(required bool) *fakeDeclarations { - return &fakeDeclarations{t: t, requiresPublicOrigin: func(string) (bool, error) { return required, nil }} - } ready := func(id string, g uint64, active int64) Node { return Node{ID: id, Online: true, ServingReady: true, ReadyGeneration: generation(g), Active: active, MaxActive: 4, Retained: active, MaxRetained: 4, CoreURL: publicURL} } @@ -137,20 +125,20 @@ func TestDecidePlacement(t *testing.T) { want *Placement err error }{ - "reset": {origin(false), publicURL, with(func(d *Deployment) { d.Resetting = true }), nil, nil, ErrResetAdmission}, - "loopback public origin": {origin(true), "http://localhost:8091", nodes, []Node{ready("a", 1, 0)}, nil, ErrPublicURLUnreachable}, - "direct": {origin(false), publicURL, with(func(d *Deployment) { d.Mode = "direct" }), nil, nil, nil}, - "direct paused": {origin(false), publicURL, with(func(d *Deployment) { d.Mode, d.AdmissionPaused = "direct", true }), nil, nil, ErrNodeUnavailable}, - "no provider": {&fakeDeclarations{t: t}, publicURL, Deployment{}, nil, nil, nil}, - "no provider on Web": {&fakeDeclarations{t: t}, publicURL, Deployment{WebManaged: true}, nil, nil, ErrNodeUnavailable}, - "paused": {origin(false), publicURL, with(func(d *Deployment) { d.AdmissionPaused = true }), []Node{ready("a", 1, 0)}, nil, ErrNodeUnavailable}, - "no nodes": {origin(false), publicURL, nodes, nil, nil, ErrNodeUnavailable}, - "only ineligible nodes": {origin(false), publicURL, nodes, []Node{offline, unready, full, retainedFull, elsewhere, {ID: "never", Online: true, ServingReady: true, MaxActive: 1, MaxRetained: 1, CoreURL: publicURL}}, nil, ErrNodeUnavailable}, - "preparing": {origin(false), publicURL, nodes, []Node{offline, preparing}, nil, ErrNodesPreparing}, - "highest generation": {origin(false), publicURL, nodes, []Node{ready("old", 1, 0), ready("new", 2, 3), preparing}, &Placement{NodeID: "new", Generation: 2}, nil}, - "fewest active": {origin(false), publicURL, nodes, []Node{ready("busy", 2, 3), ready("idle", 2, 1)}, &Placement{NodeID: "idle", Generation: 2}, nil}, - "other address off Web": {origin(false), publicURL, with(func(d *Deployment) { d.WebManaged = false }), []Node{elsewhere}, &Placement{NodeID: "elsewhere", Generation: 9}, nil}, - "public origin on public URL": {origin(true), publicURL, nodes, []Node{ready("a", 1, 0)}, &Placement{NodeID: "a", Generation: 1}, nil}, + "reset": {&fakeDeclarations{t: t}, publicURL, with(func(d *Deployment) { d.Resetting = true }), nil, nil, ErrResetAdmission}, + "loopback public URL": {&fakeDeclarations{t: t}, "http://localhost:8091", nodes, []Node{ready("a", 1, 0)}, nil, ErrPublicURLUnreachable}, + "loopback public URL direct": {&fakeDeclarations{t: t}, "http://localhost:8091", with(func(d *Deployment) { d.Mode = "direct" }), nil, nil, ErrPublicURLUnreachable}, + "direct": {&fakeDeclarations{t: t}, publicURL, with(func(d *Deployment) { d.Mode = "direct" }), nil, nil, nil}, + "direct paused": {&fakeDeclarations{t: t}, publicURL, with(func(d *Deployment) { d.Mode, d.AdmissionPaused = "direct", true }), nil, nil, ErrNodeUnavailable}, + "no provider": {&fakeDeclarations{t: t}, publicURL, Deployment{}, nil, nil, nil}, + "no provider on Web": {&fakeDeclarations{t: t}, publicURL, Deployment{WebManaged: true}, nil, nil, ErrNodeUnavailable}, + "paused": {&fakeDeclarations{t: t}, publicURL, with(func(d *Deployment) { d.AdmissionPaused = true }), []Node{ready("a", 1, 0)}, nil, ErrNodeUnavailable}, + "no nodes": {&fakeDeclarations{t: t}, publicURL, nodes, nil, nil, ErrNodeUnavailable}, + "only ineligible nodes": {&fakeDeclarations{t: t}, publicURL, nodes, []Node{offline, unready, full, retainedFull, elsewhere, {ID: "never", Online: true, ServingReady: true, MaxActive: 1, MaxRetained: 1, CoreURL: publicURL}}, nil, ErrNodeUnavailable}, + "preparing": {&fakeDeclarations{t: t}, publicURL, nodes, []Node{offline, preparing}, nil, ErrNodesPreparing}, + "highest generation": {&fakeDeclarations{t: t}, publicURL, nodes, []Node{ready("old", 1, 0), ready("new", 2, 3), preparing}, &Placement{NodeID: "new", Generation: 2}, nil}, + "fewest active": {&fakeDeclarations{t: t}, publicURL, nodes, []Node{ready("busy", 2, 3), ready("idle", 2, 1)}, &Placement{NodeID: "idle", Generation: 2}, nil}, + "other address off Web": {&fakeDeclarations{t: t}, publicURL, with(func(d *Deployment) { d.WebManaged = false }), []Node{elsewhere}, &Placement{NodeID: "elsewhere", Generation: 9}, nil}, } { got, err := rules(t, test.declarations, test.url).DecidePlacement(test.d, test.nodes) if !errors.Is(err, test.err) || (test.err == nil) != (err == nil) || (got == nil) != (test.want == nil) || (got != nil && *got != *test.want) { diff --git a/services/core/internal/deployment/service.go b/services/core/internal/deployment/service.go index 673a41dac..71c2bf6a3 100644 --- a/services/core/internal/deployment/service.go +++ b/services/core/internal/deployment/service.go @@ -249,8 +249,7 @@ func (s *Service) SetupForSelection(installationID string, input sandbox.Selecti if err != nil { return Setup{}, configurationError(err) } - // Adapters declare whether their guests require a public Core origin. - if err := s.rules.CheckPublicOrigin(input.Provider); err != nil { + if err := s.rules.CheckPublicOrigin(); err != nil { return Setup{}, err } result := Setup{InstallationID: installationID, Provider: input.Provider, Mode: description.Mode, Specification: normalized.DeploymentSpec, Configuration: normalized.Configuration, BackendFingerprint: description.BackendFingerprint} diff --git a/services/core/internal/deployment/service_test.go b/services/core/internal/deployment/service_test.go index 9fc5964aa..eccd70402 100644 --- a/services/core/internal/deployment/service_test.go +++ b/services/core/internal/deployment/service_test.go @@ -97,23 +97,25 @@ func TestNewServiceAndOperationsRejectNilDependencies(t *testing.T) { } } -// A provider whose guests connect to Core from outside the host cannot use a -// loopback public URL. The check writes nothing: the fakes allow no call. +// Hosted sandboxes run outside Core's network namespace, so every provider +// needs an https public URL on a host that is not loopback. The check writes nothing: the fakes allow no call. func TestSetupForSelectionRequiresAReachablePublicURL(t *testing.T) { id := uuid.NewString() e2bSelection := sandbox.Selection{Provider: "e2b", Configuration: &e2b.DeploymentConfiguration{APIKey: "synthetic-key", Template: "runtime:" + uuid.NewString()}} - for _, publicURL := range []string{"http://127.0.0.1:8091", "http://localhost:8091", "http://[::1]:8091"} { - if _, err := newService(t, &fakeStorage{t: t}, &fakeReader{t: t}, publicURL).SetupForSelection(id, e2bSelection); !errors.Is(err, placement.ErrPublicURLUnreachable) { - t.Errorf("E2B accepted the loopback public URL %s: %v", publicURL, err) + docker := sandbox.Selection{Provider: "docker", DeploymentSpec: testSpecification("docker")} + for _, selection := range []sandbox.Selection{e2bSelection, docker} { + for _, publicURL := range []string{"http://127.0.0.1:8091", "http://localhost:8091", "http://[::1]:8091", "http://10.0.0.5:8091", "https://localhost"} { + if _, err := newService(t, &fakeStorage{t: t}, &fakeReader{t: t}, publicURL).SetupForSelection(id, selection); !errors.Is(err, placement.ErrPublicURLUnreachable) { + t.Errorf("%s accepted the public URL %s: %v", selection.Provider, publicURL, err) + } } } setup, err := newService(t, &fakeStorage{t: t}, &fakeReader{t: t}, testPublicURL).SetupForSelection(id, e2bSelection) if err != nil || setup.Provider != "e2b" || setup.Mode != "direct" || setup.InstallationID != id || !setup.UsesCredential { t.Fatalf("E2B with a public URL = %+v, %v", setup, err) } - docker := sandbox.Selection{Provider: "docker", DeploymentSpec: testSpecification("docker")} - if setup, err := newService(t, &fakeStorage{t: t}, &fakeReader{t: t}, "http://127.0.0.1:8091").SetupForSelection(id, docker); err != nil || setup.Mode != "nodes" || setup.UsesCredential { - t.Fatalf("Docker with a loopback public URL = %+v, %v", setup, err) + if setup, err := newService(t, &fakeStorage{t: t}, &fakeReader{t: t}, testPublicURL).SetupForSelection(id, docker); err != nil || setup.Mode != "nodes" || setup.UsesCredential { + t.Fatalf("Docker with a public URL = %+v, %v", setup, err) } } diff --git a/services/core/internal/sandbox/configuration.go b/services/core/internal/sandbox/configuration.go index e0fbc5cc6..e1fe0c931 100644 --- a/services/core/internal/sandbox/configuration.go +++ b/services/core/internal/sandbox/configuration.go @@ -19,9 +19,8 @@ const ( ) type ConfigurationRequirements struct { - Credential Requirement - PublicOrigin Requirement - Discovery providercontract.Support + Credential Requirement + Discovery providercontract.Support } // Configuration is an adapter-owned typed value, never a request or response DTO. diff --git a/services/core/internal/sandbox/e2b/configuration.go b/services/core/internal/sandbox/e2b/configuration.go index 752fd60c7..1fe8eed37 100644 --- a/services/core/internal/sandbox/e2b/configuration.go +++ b/services/core/internal/sandbox/e2b/configuration.go @@ -32,7 +32,7 @@ func configuration(s sandbox.Selection) *DeploymentConfiguration { return c } func (ConfigurationAdapter) Requirements() sandbox.ConfigurationRequirements { - return sandbox.ConfigurationRequirements{Credential: sandbox.Required, PublicOrigin: sandbox.Required, Discovery: providercontract.Support{State: providercontract.Supported}} + return sandbox.ConfigurationRequirements{Credential: sandbox.Required, Discovery: providercontract.Support{State: providercontract.Supported}} } func (ConfigurationAdapter) DecodeInput(public, secret json.RawMessage) (sandbox.Configuration, error) { var p publicConfiguration diff --git a/services/core/internal/sandbox/providers/configuration.go b/services/core/internal/sandbox/providers/configuration.go index 910a0cb4c..7f7b5ff3e 100644 --- a/services/core/internal/sandbox/providers/configuration.go +++ b/services/core/internal/sandbox/providers/configuration.go @@ -46,16 +46,6 @@ func (r *Registry) UsesCredential(kind string) (bool, error) { } return required(a.Configuration.Requirements().Credential) } -func (r *Registry) RequiresPublicOrigin(kind string) (bool, error) { - a, err := r.Lookup(kind) - if err != nil { - return false, err - } - if a.Configuration == nil { - return false, providercontract.ErrContract - } - return required(a.Configuration.Requirements().PublicOrigin) -} func required(value sandbox.Requirement) (bool, error) { switch value { case sandbox.Required: @@ -123,7 +113,7 @@ type nodeConfigurationAdapter struct { } func (nodeConfigurationAdapter) Requirements() sandbox.ConfigurationRequirements { - return sandbox.ConfigurationRequirements{Credential: sandbox.NotRequired, PublicOrigin: sandbox.NotRequired, Discovery: providercontract.Support{State: providercontract.Unsupported, Reason: "node_configuration_has_no_catalog"}} + return sandbox.ConfigurationRequirements{Credential: sandbox.NotRequired, Discovery: providercontract.Support{State: providercontract.Unsupported, Reason: "node_configuration_has_no_catalog"}} } func (nodeConfigurationAdapter) DecodeInput(public, secret json.RawMessage) (sandbox.Configuration, error) { if len(secret) > 0 || sandbox.DecodeConfigurationObject(public, &struct{}{}) != nil { diff --git a/services/core/internal/sandbox/providers/configuration_flow_test.go b/services/core/internal/sandbox/providers/configuration_flow_test.go index 4012e48ef..5c8163f12 100644 --- a/services/core/internal/sandbox/providers/configuration_flow_test.go +++ b/services/core/internal/sandbox/providers/configuration_flow_test.go @@ -32,7 +32,7 @@ func (regionalConfiguration) ReplacesCredential() bool { return false } type regionalCodec struct{} func (regionalCodec) Requirements() sandbox.ConfigurationRequirements { - return sandbox.ConfigurationRequirements{Credential: sandbox.NotRequired, PublicOrigin: sandbox.NotRequired, Discovery: providercontract.Support{State: providercontract.Unsupported, Reason: "node_configuration_has_no_catalog"}} + return sandbox.ConfigurationRequirements{Credential: sandbox.NotRequired, Discovery: providercontract.Support{State: providercontract.Unsupported, Reason: "node_configuration_has_no_catalog"}} } func (regionalCodec) WithCredential(sandbox.Configuration, sandbox.Configuration) (sandbox.Configuration, error) { return nil, &providercontract.UnsupportedError{Operation: "WithCredential", Reason: "credentials_not_required"} diff --git a/services/core/internal/sandbox/providers/configuration_test.go b/services/core/internal/sandbox/providers/configuration_test.go index 43588e61a..d427f74a5 100644 --- a/services/core/internal/sandbox/providers/configuration_test.go +++ b/services/core/internal/sandbox/providers/configuration_test.go @@ -40,16 +40,14 @@ func TestNodeConfigurationExplicitUnsupportedAndStrictEmptyInput(t *testing.T) { func TestConfigurationRequirementsDoNotTurnLookupFailuresIntoFalse(t *testing.T) { registry := Builtin() - for _, check := range []func(string) (bool, error){registry.UsesCredential, registry.RequiresPublicOrigin} { - if _, err := check("missing-configuration-provider"); err == nil { - t.Fatal("unknown provider treated as not required") - } - if yes, err := check("docker"); err != nil || yes { - t.Fatal("explicit not-required rejected", err) - } - if yes, err := check("e2b"); err != nil || !yes { - t.Fatal("explicit required lost", err) - } + if _, err := registry.UsesCredential("missing-configuration-provider"); err == nil { + t.Fatal("unknown provider treated as not required") + } + if yes, err := registry.UsesCredential("docker"); err != nil || yes { + t.Fatal("explicit not-required rejected", err) + } + if yes, err := registry.UsesCredential("e2b"); err != nil || !yes { + t.Fatal("explicit required lost", err) } if _, err := required(""); !errors.Is(err, providercontract.ErrContract) { t.Fatal("missing requirement treated as false", err) diff --git a/services/core/internal/sandbox/providers/registration_configuration.go b/services/core/internal/sandbox/providers/registration_configuration.go index 3d8a5dd54..9d91b2ec1 100644 --- a/services/core/internal/sandbox/providers/registration_configuration.go +++ b/services/core/internal/sandbox/providers/registration_configuration.go @@ -49,12 +49,12 @@ func validateConfigurationDiscoveryInterface(discovery reflect.Type) error { // Check field names as well as values so new requirements cannot bypass the // gate. This owns only configuration requirements, not resource operations. func validateConfigurationRequirements(value reflect.Value) error { - if value.Kind() != reflect.Struct || value.NumField() != 3 { + if value.Kind() != reflect.Struct || value.NumField() != 2 { return configurationRegistrationError() } for i := 0; i < value.NumField(); i++ { switch value.Type().Field(i).Name { - case "Credential", "PublicOrigin": + case "Credential": requirement, ok := value.Field(i).Interface().(sandbox.Requirement) if !ok || (requirement != sandbox.Required && requirement != sandbox.NotRequired) { return configurationRegistrationError() diff --git a/services/core/internal/sandbox/providers/registration_configuration_test.go b/services/core/internal/sandbox/providers/registration_configuration_test.go index 7fdcda590..a82019876 100644 --- a/services/core/internal/sandbox/providers/registration_configuration_test.go +++ b/services/core/internal/sandbox/providers/registration_configuration_test.go @@ -61,7 +61,6 @@ func TestConfigurationRequirementsRejectInvalidDeclarations(t *testing.T) { registry := Builtin() for _, change := range []func(*sandbox.ConfigurationRequirements){ func(r *sandbox.ConfigurationRequirements) { r.Credential = "automatic" }, - func(r *sandbox.ConfigurationRequirements) { r.PublicOrigin = "private" }, func(r *sandbox.ConfigurationRequirements) { r.Discovery.State = "unknown" }, func(r *sandbox.ConfigurationRequirements) { r.Discovery.Reason = "" }, func(r *sandbox.ConfigurationRequirements) { r.Discovery.Reason = "https://private:key@host" }, @@ -83,23 +82,17 @@ func TestConfigurationRequirementsDoNotInventDependencies(t *testing.T) { // Required credentials are input policy. VerifyCredential is a separate // resource operation that may be Unsupported for this provider. for _, credential := range []sandbox.Requirement{sandbox.Required, sandbox.NotRequired} { - for _, public := range []sandbox.Requirement{sandbox.Required, sandbox.NotRequired} { - a := registry.adapters["docker"] - requirements := a.Configuration.Requirements() - requirements.Credential, requirements.PublicOrigin = credential, public - a.Configuration = registrationConfiguration{requirements: requirements} - if err := ValidateRegistration(a); err != nil { - t.Fatal(err) - } - registry.adapters[kind] = a - gotCredential, err := registry.UsesCredential(kind) - if err != nil || gotCredential != (credential == sandbox.Required) { - t.Fatalf("credential %s: value=%v error=%v", credential, gotCredential, err) - } - gotPublic, err := registry.RequiresPublicOrigin(kind) - if err != nil || gotPublic != (public == sandbox.Required) { - t.Fatalf("public origin %s: value=%v error=%v", public, gotPublic, err) - } + a := registry.adapters["docker"] + requirements := a.Configuration.Requirements() + requirements.Credential = credential + a.Configuration = registrationConfiguration{requirements: requirements} + if err := ValidateRegistration(a); err != nil { + t.Fatal(err) + } + registry.adapters[kind] = a + gotCredential, err := registry.UsesCredential(kind) + if err != nil || gotCredential != (credential == sandbox.Required) { + t.Fatalf("credential %s: value=%v error=%v", credential, gotCredential, err) } } } diff --git a/services/core/internal/sandbox/providers/registration_test.go b/services/core/internal/sandbox/providers/registration_test.go index 93eb5304b..e75406f08 100644 --- a/services/core/internal/sandbox/providers/registration_test.go +++ b/services/core/internal/sandbox/providers/registration_test.go @@ -54,11 +54,6 @@ func TestRegistrationRejectsBeforeCallbacksOrConstruction(t *testing.T) { r.Credential = "private-token" a.Configuration = registrationConfiguration{requirements: r} }}, - {"invalid public origin requirement", func(a *Adapter) { - r := a.Configuration.Requirements() - r.PublicOrigin = "private-token" - a.Configuration = registrationConfiguration{requirements: r} - }}, {"missing operations", func(a *Adapter) { a.Operations = nil }}, {"incomplete operations", func(a *Adapter) { a.Operations = func() providercontract.Operations { return nil } }}, @@ -81,7 +76,6 @@ func TestRegistrationRejectsBeforeCallbacksOrConstruction(t *testing.T) { }{ {"lookup", func() error { _, err := registry.Lookup(kind); return err }}, {"credential requirement", func() error { _, err := registry.UsesCredential(kind); return err }}, - {"public origin requirement", func() error { _, err := registry.RequiresPublicOrigin(kind); return err }}, {"normalize", func() error { _, err := registry.Normalize(selection); return err }}, {"specification", func() error { return registry.ValidateSpecification(kind, selection.DeploymentSpec) }}, {"resources", func() error { return registry.ValidateResources(kind, selection.Resources) }}, diff --git a/services/core/internal/sessions/creation_test.go b/services/core/internal/sessions/creation_test.go index 86c16990c..467f5f561 100644 --- a/services/core/internal/sessions/creation_test.go +++ b/services/core/internal/sessions/creation_test.go @@ -146,8 +146,6 @@ func unavailable(string) (string, error) { return "", credentialcrypto.ErrUnavai // declarations accept every provider and specification. type declarations struct{} -func (declarations) RequiresPublicOrigin(string) (bool, error) { return false, nil } - func (declarations) ValidateSpecification(string, sandbox.DeploymentSpec) error { return nil } var ( From a7181f11787ca41213021c5ee9933c83e7be485e Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 7 Oct 2026 19:00:56 +0000 Subject: [PATCH 3/8] Verify Serve credentials with the hash that minted them The Link authority hashed a presented Serve credential with a raw SHA-256 while allocation reservation stored runtimedevice.HashCredential. Both sides now use HashCredential, as Attach already does. --- services/core/internal/runtimegateway/link.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/services/core/internal/runtimegateway/link.go b/services/core/internal/runtimegateway/link.go index 08f502ea6..37481b580 100644 --- a/services/core/internal/runtimegateway/link.go +++ b/services/core/internal/runtimegateway/link.go @@ -6,7 +6,6 @@ import ( "crypto/sha256" "crypto/subtle" "encoding/binary" - "encoding/hex" "fmt" "net/netip" "time" @@ -58,8 +57,8 @@ func (l *LinkAuthority) AuthenticateServe(ctx context.Context, hello sandboxlink if err != nil { return sandboxlink.ServePeer{}, err } - digest := sha256.Sum256(hello.Credential) - if !found || subtle.ConstantTimeCompare([]byte(hex.EncodeToString(digest[:])), []byte(authority.CredentialHash)) != 1 { + presented := runtimedevice.HashCredential(string(hello.Credential)) + if !found || subtle.ConstantTimeCompare([]byte(presented), []byte(authority.CredentialHash)) != 1 { return sandboxlink.ServePeer{}, sandboxlink.Fail(sandboxlink.AuthenticationFailed) } current := authority.Resource.Ref() From ce269b3eb70f415bf4456891541e6f2189b16436 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 7 Oct 2026 19:01:49 +0000 Subject: [PATCH 4/8] Start oac-sandbox-io from the Docker container's own command Docker started oac-sandbox-io with an exec after ContainerStart, so a running container whose exec never ran was reported BootstrapComplete. The container command now starts the service in the background from the private files written before start and execs the daemon, so ContainerStart is the last mutating step. --- docs/sandbox-provider.md | 4 ++-- docs/zh/sandbox-provider.md | 6 +++--- .../core/internal/sandbox/docker/provider.go | 18 +++++++++--------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/sandbox-provider.md b/docs/sandbox-provider.md index 66edd5675..6ab6045c9 100644 --- a/docs/sandbox-provider.md +++ b/docs/sandbox-provider.md @@ -170,7 +170,7 @@ Terminal cleanup atomically revokes the device's authority, records the Environm ### Sandbox I/O service -Every hosted sandbox also runs `oac-sandbox-io`, which Serves the allocation over the [Sandbox link](./sandbox-link-protocol.md). `Bootstrap.SandboxIO` is its [Sandbox bootstrap](./sandbox-bootstrap.md) input: the Link URL derived from the [public URL](./configuration.md#changing-the-public-url), the allocation's Serve credential, and the allocation with its Serve generation as resource. Core validates the whole `Bootstrap` once, with `Bootstrap.Validate`, before `Create`, and adapters deliver it as given. `Create` writes `SandboxIO` to a private file, `/home/runtime/sandbox-io-bootstrap.json` (mode 0600, UID 1000) in the reference adapters, and starts `oac-sandbox-io --bootstrap-file` with that path as the daemon's account, beside the daemon. The input never travels in an argument or environment variable. Every Runtime image ships `/usr/local/bin/oac-sandbox-io`. Allocation cleanup revokes the resource at the relay before it calls `Kill`. +Every hosted sandbox also runs `oac-sandbox-io`, which Serves the allocation over the [Sandbox link](./sandbox-link-protocol.md). `Bootstrap.SandboxIO` is its [Sandbox bootstrap](./sandbox-bootstrap.md) input: the Link URL derived from the [public URL](./configuration.md#changing-the-public-url), the allocation's Serve credential, and the allocation with its Serve generation as resource. Core validates the whole `Bootstrap` once, with `Bootstrap.Validate`, before `Create`, and adapters deliver it as given. `Create` writes `SandboxIO` to a private file, `/home/runtime/sandbox-io-bootstrap.json` (mode 0600, UID 1000) in the reference adapters, and starts `oac-sandbox-io --bootstrap-file` with that path as the daemon's account, beside the daemon. `BootstrapComplete` implies that both processes were started. The input never travels in an argument or environment variable. Every Runtime image ships `/usr/local/bin/oac-sandbox-io`. Allocation cleanup revokes the resource at the relay before it calls `Kill`. ### Per-node lifecycle workers @@ -210,7 +210,7 @@ Run `make check-sandbox-provider-contract` while developing. It runs the shared Node tests separately cover disconnect and reconnect fencing and cleanup after a lost Create response. Helper protocols and the [sandbox node protocol](../contracts/agents-api/node-generation-protocol.md) require an exact version match; direct in-process interfaces have no separate wire version. -Native acceptance proves what fixtures cannot: creation, lease behavior, owned partial cleanup, declared isolation and limits, and snapshots where supported. The opt-in Docker lifecycle, recovery, Serve and node transport tests use `AGENTS_RUNTIME_DOCKER_TEST_IMAGE`, an image digest whose entry point sleeps and that contains `/usr/local/bin/oac-sandbox-io`; the SDK helpers use `make check-e2b-provider` and `make check-microsandbox-provider`. Mocked compute proves neither reclamation nor isolation. +Native acceptance proves what fixtures cannot: creation, lease behavior, owned partial cleanup, declared isolation and limits, and snapshots where supported. The opt-in Docker lifecycle, recovery, Serve and node transport tests use `AGENTS_RUNTIME_DOCKER_TEST_IMAGE`, an image digest with `/bin/sh` and a `/usr/local/bin/oac-daemon` that keeps running; the Serve test adds this tree's `oac-sandbox-io` to an image it derives; the SDK helpers use `make check-e2b-provider` and `make check-microsandbox-provider`. Mocked compute proves neither reclamation nor isolation. ## Reference adapters diff --git a/docs/zh/sandbox-provider.md b/docs/zh/sandbox-provider.md index 7004b681b..a050c7a6f 100644 --- a/docs/zh/sandbox-provider.md +++ b/docs/zh/sandbox-provider.md @@ -1,7 +1,7 @@ --- title: "添加 Sandbox Provider" source: docs/sandbox-provider.md -source_hash: c6bcdf8b09e317b9e2dd0e6afc3b872918b8756144982908f9b90953686b09cc +source_hash: 8745eff0e19b6c033f8d00edfc3b45a91826b53374948b64b48218c9f089d2d9 --- **Sandbox Provider** 为 Core 管理的 Environment 提供 Runtime daemon 运行所需的外层计算资源,以及启动 daemon 的有界引导流程。本指南说明如何添加 Provider,并作为 Core 驱动 Provider 的参考。接口为 [`SandboxProvider`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/internal/sandbox/sandbox_provider.go)。 @@ -172,7 +172,7 @@ allocation、专用 daemon credential digest、Serve credential digest 和精确 ### Sandbox I/O 服务 {#sandbox-io-service} -每个托管 sandbox 还会运行 `oac-sandbox-io`,它通过[沙箱 Link](./sandbox-link-protocol.md) Serve 该 allocation。`Bootstrap.SandboxIO` 是它的[沙箱引导](./sandbox-bootstrap.md)输入:从[公开 URL](./configuration.md#changing-the-public-url) 派生的 Link URL、allocation 的 Serve credential,以及作为 resource 的 allocation 及其 Serve generation。Core 在 `Create` 前用 `Bootstrap.Validate` 对整个 `Bootstrap` 校验一次,adapter 原样交付。`Create` 将 `SandboxIO` 写入私有文件(参考 adapter 中为 `/home/runtime/sandbox-io-bootstrap.json`,mode 0600、UID 1000),并以 daemon 的账户在 daemon 旁边启动 `oac-sandbox-io --bootstrap-file`,参数为该路径。该输入从不通过命令参数或环境变量传递。每个 Runtime 镜像都包含 `/usr/local/bin/oac-sandbox-io`。allocation cleanup 在调用 `Kill` 前先在 relay 撤销该 resource。 +每个托管 sandbox 还会运行 `oac-sandbox-io`,它通过[沙箱 Link](./sandbox-link-protocol.md) Serve 该 allocation。`Bootstrap.SandboxIO` 是它的[沙箱引导](./sandbox-bootstrap.md)输入:从[公开 URL](./configuration.md#changing-the-public-url) 派生的 Link URL、allocation 的 Serve credential,以及作为 resource 的 allocation 及其 Serve generation。Core 在 `Create` 前用 `Bootstrap.Validate` 对整个 `Bootstrap` 校验一次,adapter 原样交付。`Create` 将 `SandboxIO` 写入私有文件(参考 adapter 中为 `/home/runtime/sandbox-io-bootstrap.json`,mode 0600、UID 1000),并以 daemon 的账户在 daemon 旁边启动 `oac-sandbox-io --bootstrap-file`,参数为该路径。`BootstrapComplete` 意味着两个进程都已启动。该输入从不通过命令参数或环境变量传递。每个 Runtime 镜像都包含 `/usr/local/bin/oac-sandbox-io`。allocation cleanup 在调用 `Kill` 前先在 relay 撤销该 resource。 ### 每节点生命周期 worker {#per-node-lifecycle-workers} @@ -212,7 +212,7 @@ installer 与 Go adapter 的 E2B template 和 endpoint validator 消费共享 [s node 测试单独覆盖 disconnect、reconnect fencing,以及 Create response 丢失后的 cleanup。helper protocol 和 [sandbox node 协议](../../contracts/agents-api/zh/node-generation-protocol.md)要求精确版本匹配;直接进程内接口没有独立 wire version。 -原生验收证明 fixture 无法证明的事实:creation、lease 行为、所属 partial cleanup、声明的 isolation 与 limit,以及支持时的 snapshot。显式启用的 Docker lifecycle、recovery、Serve 和 node transport 测试使用 `AGENTS_RUNTIME_DOCKER_TEST_IMAGE`,即一个 entry point 为 sleep 且包含 `/usr/local/bin/oac-sandbox-io` 的镜像 digest;SDK helper 使用 `make check-e2b-provider` 和 `make check-microsandbox-provider`。mock compute 不能证明 reclamation 或 isolation。 +原生验收证明 fixture 无法证明的事实:creation、lease 行为、所属 partial cleanup、声明的 isolation 与 limit,以及支持时的 snapshot。显式启用的 Docker lifecycle、recovery、Serve 和 node transport 测试使用 `AGENTS_RUNTIME_DOCKER_TEST_IMAGE`,即一个包含 `/bin/sh` 和持续运行的 `/usr/local/bin/oac-daemon` 的镜像 digest;Serve 测试会把本源码树的 `oac-sandbox-io` 加入它派生的镜像;SDK helper 使用 `make check-e2b-provider` 和 `make check-microsandbox-provider`。mock compute 不能证明 reclamation 或 isolation。 ## 参考 adapter {#reference-adapters} diff --git a/services/core/internal/sandbox/docker/provider.go b/services/core/internal/sandbox/docker/provider.go index 8cfb8dfde..1e696481b 100644 --- a/services/core/internal/sandbox/docker/provider.go +++ b/services/core/internal/sandbox/docker/provider.go @@ -147,7 +147,10 @@ func (p *Provider) Create(ctx context.Context, b sandbox.Bootstrap) (sandbox.Inf } } options := runtimeContainerOptions(p.config, name, p.labels(b.Reference), []string{"OAC_RUNTIME_ENVIRONMENT_ID=" + b.EnvironmentID, "OAC_RUNTIME_SESSION_ID=" + b.SessionID, "OAC_RUNTIME_NETWORK_ACCESS=" + policy.Access, "OAC_RUNTIME_ALLOWED_DOMAINS=" + string(domains)}) - options.Config.Cmd = []string{"connect", "--profile", "default", "--bootstrap-file", "/home/runtime/runtime-bootstrap.json"} + // The container's own command starts both processes from the files + // bootstrap writes before start, so ContainerStart is the last mutating + // step and a running container has started the Sandbox I/O service. + options.Config.Entrypoint = []string{"/bin/sh", "-c", launch} v, e := p.client.ContainerCreate(ctx, options) if errdefs.IsConflict(e) { return info, sandbox.ErrExists @@ -177,17 +180,14 @@ func (p *Provider) Create(ctx context.Context, b sandbox.Bootstrap) (sandbox.Inf if _, e = p.client.ContainerStart(ctx, v.ID, client.ContainerStartOptions{}); e != nil { return info, e } - // The Sandbox I/O service runs beside the daemon, as the same account. - exec, e := p.client.ExecCreate(ctx, v.ID, client.ExecCreateOptions{User: "1000:1000", Cmd: []string{"/usr/local/bin/oac-sandbox-io", "--bootstrap-file", "/home/runtime/sandbox-io-bootstrap.json"}}) - if e != nil { - return info, e - } - if _, e = p.client.ExecStart(ctx, exec.ID, client.ExecStartOptions{Detach: true}); e != nil { - return info, e - } return p.GetInfo(ctx, b.Reference) } +// launch starts the Sandbox I/O service in the background, then replaces the +// shell with the daemon, which stays the container's main process. +const launch = "/usr/local/bin/oac-sandbox-io --bootstrap-file /home/runtime/sandbox-io-bootstrap.json & " + + "exec /usr/local/bin/oac-daemon connect --profile default --bootstrap-file /home/runtime/runtime-bootstrap.json" + // Kill is idempotent only for absence, not for errors or foreign ownership. It // checks all resources before removing any and confirms removal of named volumes. func (p *Provider) Kill(ctx context.Context, r sandbox.Reference) error { From a4ac67a8e4e8cc0baa1a9d5474327941b3e4052b Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 7 Oct 2026 19:03:42 +0000 Subject: [PATCH 5/8] Test this tree's oac-sandbox-io in the Docker Serve test The opt-in Serve test builds oac-sandbox-io from this tree and copies it into the image it derives, so a local run tests the current binary rather than whatever the fixture image ships. --- .../internal/sandbox/docker/provider_test.go | 5 +-- .../internal/sandbox/docker/serve_test.go | 31 +++++++++++++++---- .../internal/sandbox/node/docker_live_test.go | 7 +++-- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/services/core/internal/sandbox/docker/provider_test.go b/services/core/internal/sandbox/docker/provider_test.go index aa0e2ecb8..bdfac0e94 100644 --- a/services/core/internal/sandbox/docker/provider_test.go +++ b/services/core/internal/sandbox/docker/provider_test.go @@ -37,8 +37,9 @@ func TestProviderRejectsUnsafeOperatorConfiguration(t *testing.T) { } } -// This optional Docker mechanism test uses a pinned fixture image whose entrypoint -// is sleep. It is not native/model acceptance; the real Runtime has separate checks. +// This optional Docker mechanism test uses a pinned fixture image whose +// oac-daemon only sleeps. It is not native/model acceptance; the real Runtime +// has separate checks. func TestDockerProviderLifecycle(t *testing.T) { image := os.Getenv("AGENTS_RUNTIME_DOCKER_TEST_IMAGE") if image == "" { diff --git a/services/core/internal/sandbox/docker/serve_test.go b/services/core/internal/sandbox/docker/serve_test.go index 3e32f3deb..f4cc50d07 100644 --- a/services/core/internal/sandbox/docker/serve_test.go +++ b/services/core/internal/sandbox/docker/serve_test.go @@ -11,6 +11,8 @@ import ( "net" "net/http/httptest" "os" + "os/exec" + "path/filepath" "slices" "strconv" "testing" @@ -60,15 +62,32 @@ func TestDockerSandboxServesItsAllocation(t *testing.T) { server.StartTLS() t.Cleanup(server.Close) port := strconv.Itoa(listener.Addr().(*net.TCPAddr).Port) - // Derive a labeled image whose only trust anchor is the test certificate. + // Derive a labeled image that runs this tree's oac-sandbox-io and whose + // only trust anchor is the test certificate. + binary := filepath.Join(t.TempDir(), "oac-sandbox-io") + build := exec.CommandContext(t.Context(), "go", "build", "-trimpath", "-o", binary, "github.com/MiniMax-AI/OpenAgentCore/apps/sandboxio/cmd/oac-sandbox-io") + build.Env = append(os.Environ(), "CGO_ENABLED=0", "GOOS=linux") + if output, err := build.CombinedOutput(); err != nil { + t.Fatalf("build oac-sandbox-io: %v\n%s", err, output) + } + serveBinary, err := os.ReadFile(binary) + if err != nil { + t.Fatal(err) + } ca := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: server.Certificate().Raw}) var content bytes.Buffer archive := tar.NewWriter(&content) - if err := archive.WriteHeader(&tar.Header{Name: "etc/ssl/certs/ca-certificates.crt", Mode: 0o644, Size: int64(len(ca)), Typeflag: tar.TypeReg}); err != nil { - t.Fatal(err) - } - if _, err := archive.Write(ca); err != nil { - t.Fatal(err) + for _, file := range []struct { + name string + mode int64 + data []byte + }{{"etc/ssl/certs/ca-certificates.crt", 0o644, ca}, {"usr/local/bin/oac-sandbox-io", 0o555, serveBinary}} { + if err := archive.WriteHeader(&tar.Header{Name: file.name, Mode: file.mode, Size: int64(len(file.data)), Typeflag: tar.TypeReg}); err != nil { + t.Fatal(err) + } + if _, err := archive.Write(file.data); err != nil { + t.Fatal(err) + } } if err := archive.Close(); err != nil { t.Fatal(err) diff --git a/services/core/internal/sandbox/node/docker_live_test.go b/services/core/internal/sandbox/node/docker_live_test.go index 6dc295325..099c7df0a 100644 --- a/services/core/internal/sandbox/node/docker_live_test.go +++ b/services/core/internal/sandbox/node/docker_live_test.go @@ -16,9 +16,10 @@ import ( "github.com/moby/moby/client" ) -// This uses the same pinned sleep-entrypoint image as the Docker mechanism -// tests. It exercises real Docker resources through the node transport, not a -// native harness/model workflow. No provider credentials are required. +// This uses the same pinned fixture image, whose oac-daemon only sleeps, as +// the Docker mechanism tests. It exercises real Docker resources through the +// node transport, not a native harness/model workflow. No provider +// credentials are required. func TestDockerNodeTransportLifecycle(t *testing.T) { image := os.Getenv("AGENTS_RUNTIME_DOCKER_TEST_IMAGE") if image == "" { From aaf0a4eb9990e550fca2dc0ee888f105d4553cec Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 7 Oct 2026 19:04:13 +0000 Subject: [PATCH 6/8] Test that microsandbox delivers both launch inputs One unit test sends a Create through Core's adapter, decodes the request as the helper does, and checks that it carries SandboxIO and that the helper's stdin payload holds exactly Runtime and SandboxIO. --- .../tools/microsandbox-provider/bootstrap.go | 24 ++++--- .../microsandbox-provider/bootstrap_test.go | 63 +++++++++++++++++++ 2 files changed, 78 insertions(+), 9 deletions(-) create mode 100644 services/core/tools/microsandbox-provider/bootstrap_test.go diff --git a/services/core/tools/microsandbox-provider/bootstrap.go b/services/core/tools/microsandbox-provider/bootstrap.go index d74ef9921..6d3f9364f 100644 --- a/services/core/tools/microsandbox-provider/bootstrap.go +++ b/services/core/tools/microsandbox-provider/bootstrap.go @@ -87,15 +87,7 @@ func (b backend) create(ctx context.Context) (wire.Response, error) { if e != nil { return qualified, e } - connection, e := bootstrap.RuntimeConnection().Marshal() - if e != nil { - return wire.Response{}, e - } - serve, e := bootstrap.SandboxIO.Marshal() - if e != nil { - return wire.Response{}, e - } - data, e := json.Marshal(struct{ Runtime, SandboxIO json.RawMessage }{connection, serve}) + data, e := launchInputs(bootstrap) if e != nil { return wire.Response{}, e } @@ -121,6 +113,20 @@ func (b backend) create(ctx context.Context) (wire.Response, error) { return wire.Response{State: &state}, e } +// launchInputs is the bootstrap script's stdin: the daemon's connection and +// the Sandbox I/O service's input. +func launchInputs(b sandbox.Bootstrap) ([]byte, error) { + connection, err := b.RuntimeConnection().Marshal() + if err != nil { + return nil, err + } + serve, err := b.SandboxIO.Marshal() + if err != nil { + return nil, err + } + return json.Marshal(struct{ Runtime, SandboxIO json.RawMessage }{connection, serve}) +} + // Only the initial post-Create inspection uses this proof. Native creation has // returned successfully, and no bootstrap command has started. Ordinary inspect // and unknown Create outcomes cannot acquire settlement through this path. diff --git a/services/core/tools/microsandbox-provider/bootstrap_test.go b/services/core/tools/microsandbox-provider/bootstrap_test.go new file mode 100644 index 000000000..34e71aa72 --- /dev/null +++ b/services/core/tools/microsandbox-provider/bootstrap_test.go @@ -0,0 +1,63 @@ +//go:build linux + +package main + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "strings" + "testing" + "time" + + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/contracttest" + wire "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/microsandbox" +) + +type captureCaller func(wire.Request) + +func (f captureCaller) Call(_ context.Context, q wire.Request) (wire.Response, error) { + f(q) + return wire.Response{}, errors.New("captured") +} + +// Core's adapter sends SandboxIO to the helper, and the helper hands the +// guest both launch inputs on stdin. +func TestCreateDeliversBothLaunchInputs(t *testing.T) { + config := wire.Config{ + InstallationID: "11111111-1111-4111-8111-111111111111", HelperPath: "/helper", RuntimeHome: "/private/msb", RuntimePath: "/private/bin/msb", FirmwarePath: "/private/lib/libkrunfw.so", + RuntimeSHA256: strings.Repeat("a", 64), FirmwareSHA256: strings.Repeat("b", 64), Image: "registry/runtime@sha256:" + strings.Repeat("c", 64), + MemoryMiB: 2048, CPUs: 2, RootDiskMiB: 4096, EnvironmentDiskMiB: 2048, Network: wire.NetworkPolicy{DefaultEgress: "deny", DefaultIngress: "deny"}, + } + b := contracttest.Bootstrap(sandbox.Reference{TenantID: "22222222-2222-4222-8222-222222222222", EnvironmentID: "33333333-3333-4333-8333-333333333333", AllocationID: "44444444-4444-4444-8444-444444444444"}) + var sent []byte + p, err := wire.NewWithCaller(config, captureCaller(func(q wire.Request) { sent, _ = json.Marshal(q) })) + if err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithTimeout(t.Context(), time.Second) + defer cancel() + _, _ = p.Create(ctx, b) + // Decode as serve does. + decoder := json.NewDecoder(bytes.NewReader(sent)) + decoder.DisallowUnknownFields() + var q wire.Request + if err := decoder.Decode(&q); err != nil || wire.ValidateRequest(q) != nil || q.Bootstrap == nil || q.Bootstrap.SandboxIO != b.SandboxIO { + t.Fatal("the helper request lost the Sandbox I/O input", err) + } + payload, err := launchInputs(*q.Bootstrap) + if err != nil { + t.Fatal(err) + } + runtime, _ := b.RuntimeConnection().Marshal() + serveInput, _ := b.SandboxIO.Marshal() + var got map[string]json.RawMessage + if err := json.Unmarshal(payload, &got); err != nil || len(got) != 2 || !bytes.Equal(got["Runtime"], runtime) || !bytes.Equal(got["SandboxIO"], serveInput) { + t.Fatalf("stdin payload has %v", err) + } + if !strings.Contains(bootstrapScript, "b['Runtime']") || !strings.Contains(bootstrapScript, "b['SandboxIO']") { + t.Fatal("the bootstrap script does not read both launch inputs") + } +} From 4368730b90cf7d5dc2e510fe004b81495c6bc8c7 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 7 Oct 2026 19:04:13 +0000 Subject: [PATCH 7/8] Test the rejections of Bootstrap.Validate One table test covers the gate Core applies before Create, including a SandboxIO resource that names another allocation, tenant or Environment. --- .../internal/sandbox/sandbox_provider_test.go | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 services/core/internal/sandbox/sandbox_provider_test.go diff --git a/services/core/internal/sandbox/sandbox_provider_test.go b/services/core/internal/sandbox/sandbox_provider_test.go new file mode 100644 index 000000000..f5a0f34f4 --- /dev/null +++ b/services/core/internal/sandbox/sandbox_provider_test.go @@ -0,0 +1,41 @@ +package sandbox_test + +import ( + "errors" + "strings" + "testing" + + "github.com/google/uuid" + + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/contracttest" +) + +// Bootstrap.Validate is the one gate Core applies before Create. +func TestBootstrapValidateRejections(t *testing.T) { + ref := sandbox.Reference{TenantID: uuid.NewString(), EnvironmentID: uuid.NewString(), AllocationID: uuid.NewString()} + if err := contracttest.Bootstrap(ref).Validate(); err != nil { + t.Fatal("valid bootstrap rejected:", err) + } + for name, change := range map[string]func(*sandbox.Bootstrap){ + "noncanonical tenant": func(b *sandbox.Bootstrap) { b.TenantID = strings.ToUpper(b.TenantID) }, + "nil session": func(b *sandbox.Bootstrap) { b.SessionID = uuid.Nil.String() }, + "missing device": func(b *sandbox.Bootstrap) { b.DeviceID = "" }, + "Core URL off the API base": func(b *sandbox.Bootstrap) { b.CoreURL = "https://core.example" }, + "empty Runtime credential": func(b *sandbox.Bootstrap) { b.Credential = "" }, + "unknown network access": func(b *sandbox.Bootstrap) { b.NetworkAccess = "sometimes" }, + "plain ws Link off loopback": func(b *sandbox.Bootstrap) { b.SandboxIO.LinkURL = "ws://core.example/api/v1/sandbox-link" }, + "empty Serve credential": func(b *sandbox.Bootstrap) { b.SandboxIO.Credential = "" }, + "zero generation": func(b *sandbox.Bootstrap) { b.SandboxIO.Resource.Generation = 0 }, + "enrollment resource": func(b *sandbox.Bootstrap) { b.SandboxIO.Resource.Kind = "enrollment" }, + "another allocation": func(b *sandbox.Bootstrap) { b.SandboxIO.Resource.ID = uuid.NewString() }, + "another tenant": func(b *sandbox.Bootstrap) { b.SandboxIO.Resource.TenantID = uuid.NewString() }, + "another Environment": func(b *sandbox.Bootstrap) { b.SandboxIO.Resource.EnvironmentID = uuid.NewString() }, + } { + b := contracttest.Bootstrap(ref) + change(&b) + if err := b.Validate(); !errors.Is(err, sandbox.ErrInvalid) { + t.Errorf("%s: Validate = %v", name, err) + } + } +} From e623b580dc1f9c0a9c70eaef593d3fc14093b514 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 7 Oct 2026 19:04:42 +0000 Subject: [PATCH 8/8] Drop the remaining E2B-only public URL wording The install options guide and Web comments still tied the HTTPS public URL requirement to E2B; it now applies to every sandbox backend. --- apps/web/e2e/console.ts | 2 +- apps/web/src/features/sandbox/SandboxSetupWizard.tsx | 2 +- apps/web/src/lib/sandbox-labels.ts | 2 +- docs/getting-started/install-options.md | 2 +- docs/zh/getting-started/install-options.md | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/web/e2e/console.ts b/apps/web/e2e/console.ts index 1e50de4db..603d38da2 100644 --- a/apps/web/e2e/console.ts +++ b/apps/web/e2e/console.ts @@ -11,7 +11,7 @@ export const FIXTURE_CORE_KEY = "fixture-core-key-3f9a2c71"; * Fixture state options: `fresh` is a new install (no project, Session or Runtime), * `sandbox` the sandbox deployment, `nodes: "none"` a deployment no node has joined, and * `installation` how config.json's public_url is set: "public" (HTTPS, the default), "local" - * (loopback: only the Core machine reaches the API, and E2B is rejected) or "stale" (public, + * (loopback: only the Core machine reaches the API, and every sandbox selection is rejected) or "stale" (public, * with a node enrolled with an earlier address), `credentials: "none"` a Core without a * credential encryption key, which cannot store a model provider's key, and * `installers: "none"` a console without its node installation payload, so it serves neither diff --git a/apps/web/src/features/sandbox/SandboxSetupWizard.tsx b/apps/web/src/features/sandbox/SandboxSetupWizard.tsx index c0306d156..3dc2460f5 100644 --- a/apps/web/src/features/sandbox/SandboxSetupWizard.tsx +++ b/apps/web/src/features/sandbox/SandboxSetupWizard.tsx @@ -146,7 +146,7 @@ export function SandboxSetupWizard({ coreUrl, expectedGeneration, current, disab const [dockerConfirmed, setDockerConfirmed] = useState(current?.provider === "docker"); const [confirmingDocker, setConfirmingDocker] = useState(false); const keepMicrosandbox = useRef(null); - // Core's reason for rejecting the saved configuration, such as E2B with a loopback public_url. + // Core's reason for rejecting the saved configuration, such as a loopback public_url. const [rejection, setRejection] = useState(null); const [fieldRejection, setFieldRejection] = useState(null); const fieldError = (param: string) => coreFieldError(fieldRejection, param, tCommon); diff --git a/apps/web/src/lib/sandbox-labels.ts b/apps/web/src/lib/sandbox-labels.ts index 0995dd7aa..289793d89 100644 --- a/apps/web/src/lib/sandbox-labels.ts +++ b/apps/web/src/lib/sandbox-labels.ts @@ -42,7 +42,7 @@ export function sandboxWriteUncertain(error: unknown): boolean { /** * Core's own reason when it rejects a deployment configuration it cannot serve, - * such as E2B with a loopback public_url; null for any other failure. Nothing + * such as a loopback public_url; null for any other failure. Nothing * was saved, so the administrator corrects the cause and saves again. */ export function sandboxConfigurationRejection(error: unknown, locale: Locale = "en"): string | null { diff --git a/docs/getting-started/install-options.md b/docs/getting-started/install-options.md index 3a626ae1f..107d92160 100644 --- a/docs/getting-started/install-options.md +++ b/docs/getting-started/install-options.md @@ -66,7 +66,7 @@ Several installations can share a machine when they use distinct installation di ## Sandbox backend -The installer saves no sandbox backend. After signing in, open **System** → **Manage sandbox configuration** and choose Docker, microsandbox or E2B; Web proposes the Standard size in [`standard-sizes.json`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/web/src/features/sandbox/standard-sizes.json). The choice is stored in Core's database. To change it later, [reset the deployment](./nodes.md#change-the-sandbox-configuration). Docker shares each node's kernel with its sandboxes, and its node service account is [root-equivalent](./nodes.md#what-the-installer-sets-up). E2B needs a public HTTPS URL that is not loopback, because E2B's sandboxes call Core from E2B's cloud. Prepare an E2B template with the [E2B guide](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/deploy/e2b/README.md). +The installer saves no sandbox backend. After signing in, open **System** → **Manage sandbox configuration** and choose Docker, microsandbox or E2B; Web proposes the Standard size in [`standard-sizes.json`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/web/src/features/sandbox/standard-sizes.json). The choice is stored in Core's database. To change it later, [reset the deployment](./nodes.md#change-the-sandbox-configuration). Docker shares each node's kernel with its sandboxes, and its node service account is [root-equivalent](./nodes.md#what-the-installer-sets-up). Every backend needs a public HTTPS URL that is not loopback, because sandboxes call Core from outside its host. Prepare an E2B template with the [E2B guide](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/deploy/e2b/README.md). ## Listeners and access diff --git a/docs/zh/getting-started/install-options.md b/docs/zh/getting-started/install-options.md index bd5cd674c..c4f9979ca 100644 --- a/docs/zh/getting-started/install-options.md +++ b/docs/zh/getting-started/install-options.md @@ -1,7 +1,7 @@ --- title: "安装选项" source: docs/getting-started/install-options.md -source_hash: 6acbb4205e95aa5ad2f36fbfb3656a785b8d420c2ff754b81ae9cc7e07f10482 +source_hash: acf47c13134900271924d6d26b62488998cd331df7ad1196f4ce940c0bf7457c --- [默认安装](install.md)无需任何选项。本页介绍安装选项、Compose 部署和反向代理配置。 @@ -68,7 +68,7 @@ docker compose exec web oac-web core-key ## 沙箱后端 {#sandbox-backend} -安装程序不保存沙箱后端。登录后,打开 **System** → **Manage sandbox configuration**,选择 Docker、microsandbox 或 E2B;Web 会按 [`standard-sizes.json`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/web/src/features/sandbox/standard-sizes.json) 推荐 Standard 尺寸。该选择保存在 Core 的数据库中。以后要更改,请[重置部署](nodes.md#change-the-sandbox-configuration)。Docker 沙箱与每个节点共用该节点的内核,其节点服务账户[等效于 root](nodes.md#what-the-installer-sets-up)。E2B 需要一个非回环的公共 HTTPS URL,因为 E2B 沙箱会从 E2B 云端调用 Core。按照 [E2B 指南](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/deploy/e2b/README.md)准备模板。 +安装程序不保存沙箱后端。登录后,打开 **System** → **Manage sandbox configuration**,选择 Docker、microsandbox 或 E2B;Web 会按 [`standard-sizes.json`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/apps/web/src/features/sandbox/standard-sizes.json) 推荐 Standard 尺寸。该选择保存在 Core 的数据库中。以后要更改,请[重置部署](nodes.md#change-the-sandbox-configuration)。Docker 沙箱与每个节点共用该节点的内核,其节点服务账户[等效于 root](nodes.md#what-the-installer-sets-up)。每种后端都需要一个非回环的公共 HTTPS URL,因为沙箱会从 Core 主机之外调用 Core。按照 [E2B 指南](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/deploy/e2b/README.md)准备模板。 ## 监听器与访问 {#listeners-and-access}