diff --git a/.github/workflows/api-acceptance.yml b/.github/workflows/api-acceptance.yml index ee010ed18..d60aca75f 100644 --- a/.github/workflows/api-acceptance.yml +++ b/.github/workflows/api-acceptance.yml @@ -43,7 +43,6 @@ jobs: - name: Build standalone commands run: | OAC_DEV_CORE_BUILD_DIR="$RUNNER_TEMP/oac-core-build" make build-core - "$RUNNER_TEMP/oac-core-build/oac-core-device" --help "$RUNNER_TEMP/oac-core-build/oac-core-environment-key" --help - uses: actions/setup-python@v6 with: diff --git a/apps/web/e2e/data/routes.mjs b/apps/web/e2e/data/routes.mjs index 124a7966c..538ab5fbb 100644 --- a/apps/web/e2e/data/routes.mjs +++ b/apps/web/e2e/data/routes.mjs @@ -101,11 +101,11 @@ export function buildDemo(now = Math.floor(Date.now() / 1000), publicUrl = "http }); const observations = sessions.map((session) => { const base = { id: session.id, object: "agent.runtime_observation", session_id: session.id, resolved_at: now }; - if (session.environment.type === "none") return { ...base, environment_id: null, mode: "none", provider_type: null, instance: { kind: "none", allocation_id: null, device_id: null, connection_generation: null }, lifecycle_state: null, status: "unsupported", reason: "runtime_mode_not_observable", allocation_created_at: null, observed_at: null, started_at: null, cpu: null, memory: null }; - if (session.environment.type === "self_hosted") return { ...base, environment_id: session.environment.id, mode: "self_hosted", provider_type: null, instance: { kind: "self_hosted_connection", allocation_id: null, device_id: null, connection_generation: null }, lifecycle_state: null, status: "unsupported", reason: "runtime_mode_not_observable", allocation_created_at: null, observed_at: null, started_at: null, cpu: null, memory: null }; + if (session.environment.type === "none") return { ...base, environment_id: null, mode: "none", provider_type: null, instance: { kind: "none", allocation_id: null, connection_generation: null }, lifecycle_state: null, status: "unsupported", reason: "runtime_mode_not_observable", allocation_created_at: null, observed_at: null, started_at: null, cpu: null, memory: null }; + if (session.environment.type === "self_hosted") return { ...base, environment_id: session.environment.id, mode: "self_hosted", provider_type: null, instance: { kind: "self_hosted_connection", allocation_id: null, connection_generation: null }, lifecycle_state: null, status: "unsupported", reason: "runtime_mode_not_observable", allocation_created_at: null, observed_at: null, started_at: null, cpu: null, memory: null }; const allocation = allocations.find((entry) => entry.session_id === session.id); const sleeping = allocation.compute_phase === "suspended"; - return { ...base, environment_id: session.environment.id, mode: "openai_hosted", provider_type: "docker", instance: { kind: "managed_allocation", allocation_id: allocation.id, device_id: null, connection_generation: null }, lifecycle_state: sleeping ? "sleeping" : "active", status: "observed", reason: null, allocation_created_at: session.created_at, observed_at: now - 2, started_at: session.created_at, cpu: sleeping ? null : { usage_seconds_total: 600 + Math.floor(rand() * 4000), capacity_cores: 2, usage_cores: Number((rand() * 1.6).toFixed(2)), utilization_ratio: null }, memory: sleeping ? null : { usage_bytes: Math.floor((0.4 + rand() * 1.4) * 2 ** 30), limit_bytes: 2 * 2 ** 30 } }; + return { ...base, environment_id: session.environment.id, mode: "openai_hosted", provider_type: "docker", instance: { kind: "managed_allocation", allocation_id: allocation.id, connection_generation: null }, lifecycle_state: sleeping ? "sleeping" : "active", status: "observed", reason: null, allocation_created_at: session.created_at, observed_at: now - 2, started_at: session.created_at, cpu: sleeping ? null : { usage_seconds_total: 600 + Math.floor(rand() * 4000), capacity_cores: 2, usage_cores: Number((rand() * 1.6).toFixed(2)), utilization_ratio: null }, memory: sleeping ? null : { usage_bytes: Math.floor((0.4 + rand() * 1.4) * 2 ** 30), limit_bytes: 2 * 2 ** 30 } }; }); // Added after everything else is generated, so the seeded data above does not change. agents.push(...providerAgentDefinitions.map((definition, index) => ({ diff --git a/apps/web/src/features/dashboard/runtime-trends.test.ts b/apps/web/src/features/dashboard/runtime-trends.test.ts index 643703797..481a3d2fc 100644 --- a/apps/web/src/features/dashboard/runtime-trends.test.ts +++ b/apps/web/src/features/dashboard/runtime-trends.test.ts @@ -70,7 +70,6 @@ function snapshot(at: number, options: { instance: { kind: "managed_allocation", allocation_id: options.allocationId ?? "33333333-3333-4333-8333-333333333333", - device_id: null, connection_generation: null, }, lifecycle_state: "active", @@ -115,7 +114,7 @@ describe("Runtime live-window trends", () => { const pending = snapshot(120_000); pending.observations = [{ ...pending.observations[0]!, - instance: { kind: "managed_allocation", allocation_id: null, device_id: null, connection_generation: null }, + instance: { kind: "managed_allocation", allocation_id: null, connection_generation: null }, lifecycle_state: "pending", status: "unavailable", reason: "allocation_pending", diff --git a/apps/web/src/features/metrics/sandbox-runtime.test.ts b/apps/web/src/features/metrics/sandbox-runtime.test.ts index a398e72cc..f1a89c255 100644 --- a/apps/web/src/features/metrics/sandbox-runtime.test.ts +++ b/apps/web/src/features/metrics/sandbox-runtime.test.ts @@ -5,7 +5,7 @@ import type { SandboxAllocation } from "@oac/agents-client"; import { hostedObservation, node, session } from "../overview/test-fixtures"; import { hostedRuntimeRows, hostedRuntimeUsage, loadHostedRuntimes, matchesRuntime, runtimeSnapshot } from "./sandbox-runtime"; -const none = { ...hostedObservation("plain", "p1"), mode: "none", instance: { kind: "none", allocation_id: null, device_id: null, connection_generation: null }, lifecycle_state: null, status: "unsupported", reason: "runtime_mode_not_observable", cpu: null, memory: null } as unknown as ReturnType; +const none = { ...hostedObservation("plain", "p1"), mode: "none", instance: { kind: "none", allocation_id: null, connection_generation: null }, lifecycle_state: null, status: "unsupported", reason: "runtime_mode_not_observable", cpu: null, memory: null } as unknown as ReturnType; describe("loadHostedRuntimes", () => { it("keeps hosted observations, reads their Sessions through their project and bounds the reads", async () => { diff --git a/apps/web/src/features/overview/test-fixtures.ts b/apps/web/src/features/overview/test-fixtures.ts index e11dfcea3..b5ba2412c 100644 --- a/apps/web/src/features/overview/test-fixtures.ts +++ b/apps/web/src/features/overview/test-fixtures.ts @@ -49,7 +49,7 @@ export function hostedObservation(sessionId: string, projectId: string, override return { id: sessionId, object: "agent.runtime_observation", session_id: sessionId, resolved_at: 1_000, environment_id: `env_${sessionId}`, mode: "openai_hosted", provider_type: "docker", - instance: { kind: "managed_allocation", allocation_id: `alloc_${sessionId}`, device_id: null, connection_generation: null }, + instance: { kind: "managed_allocation", allocation_id: `alloc_${sessionId}`, connection_generation: null }, lifecycle_state: "active", status: "observed", reason: null, allocation_created_at: 100, observed_at: 1_000, started_at: 400, cpu: { usage_seconds_total: 10, capacity_cores: 2, usage_cores: 0.5, utilization_ratio: null }, memory: { usage_bytes: 100, limit_bytes: 400 }, diff --git a/contracts/agents-api/core.openapi.yaml b/contracts/agents-api/core.openapi.yaml index ef3544112..f42491d65 100644 --- a/contracts/agents-api/core.openapi.yaml +++ b/contracts/agents-api/core.openapi.yaml @@ -2816,10 +2816,6 @@ definitions: format: uuid type: string x-nullable: true - device_id: - format: uuid - type: string - x-nullable: true kind: enum: - managed_allocation @@ -2829,7 +2825,6 @@ definitions: required: - allocation_id - connection_generation - - device_id - kind type: object v1.RuntimeMemoryObservation: diff --git a/contracts/agents-api/machine-api.md b/contracts/agents-api/machine-api.md index 2eecb098c..7c69dc02a 100644 --- a/contracts/agents-api/machine-api.md +++ b/contracts/agents-api/machine-api.md @@ -16,14 +16,13 @@ Machines call Core under `/api/v1`: sandbox nodes, Runtime daemons, the Sandbox | `POST agent-daemon/installation`, `POST agent-daemon/installation/claim` | Self-hosted installer | Installation grant | [Installation grant](./environment-executor-credentials.md#installation-grant) | | `POST agent-daemon/enroll` | Self-hosted daemon | Executor credential | [Enroll a self-hosted daemon](#enroll-a-self-hosted-daemon) | | `GET agent-daemon/connection?environment_id=` | Self-hosted installer | Executor credential | [Private connection confirmation](./environment-executor-credentials.md#private-connection-confirmation) | -| `POST agent-daemon/bootstrap` | Runtime daemon | Daemon credential | [Daemon bootstrap](#daemon-bootstrap) | -| `GET agent-daemon/device-status?device_id=` | Runtime daemon | Daemon credential | [Device status](#device-status) | -| WebSocket `GET agent-daemon/ws?device_id=&version=` | Runtime daemon | Daemon credential | [Core–Runtime protocol](../../docs/runtime-protocol.md) | +| `POST agent-daemon/bootstrap` | Agent-host Runtime | Agent-host credential | [Daemon bootstrap](#daemon-bootstrap) | +| WebSocket `GET agent-daemon/ws?device_id=&version=` | Agent-host Runtime | Agent-host credential | [Core–Runtime protocol](../../docs/runtime-protocol.md) | | WebSocket `GET sandbox-link` | Sandbox I/O service (serve peer) and agent-host Runtime (attach peer) | The resource's Serve credential or the agent host's Runtime credential, in the Link Hello | [Sandbox link protocol](../../docs/sandbox-link-protocol.md) | Every credential travels in an `Authorization: Bearer` header, except on `sandbox-link`, where each peer sends it in its Link Hello after the upgrade. No credential travels in a URL. Core derives the [Link URL](../../docs/configuration.md#changing-the-public-url) from `OAC_PUBLIC_URL`. -The generated [`runtime.openapi.yaml`](./runtime.openapi.yaml) describes only the sandbox-node configuration, enroll and identity routes, the two installation routes and the `sandbox-link` upgrade, whose messages the Sandbox link protocol defines. The `sandbox-node/connect` and `agent-daemon/ws` WebSockets and the daemon bootstrap, device-status, enroll and connection routes are served outside the API router and have no generated schema; this document and the linked contracts are their only definition. +The generated [`runtime.openapi.yaml`](./runtime.openapi.yaml) describes only the sandbox-node configuration, enroll and identity routes, the two installation routes and the `sandbox-link` upgrade, whose messages the Sandbox link protocol defines. The `sandbox-node/connect` and `agent-daemon/ws` WebSockets and the daemon bootstrap, enroll and connection routes are served outside the API router and have no generated schema; this document and the linked contracts are their only definition. ## Credentials @@ -33,23 +32,10 @@ The generated [`runtime.openapi.yaml`](./runtime.openapi.yaml) describes only th | Node credential | The node itself: it generates a secret of 32 to 256 characters without whitespace and registers it at enrollment | `sandbox-node/configuration` with `X-OAC-Node-ID`, `sandbox-node/identity`, `sandbox-node/connect` | | Installation grant | The `x_agents_core.installation` command of a `self_hosted` Session; short-lived | `agent-daemon/installation` and its `claim` | | Executor credential | The installation claim, or the Core-key [executor credential routes](./environment-executor-credentials.md) | `agent-daemon/enroll` and `agent-daemon/connection`; after enrollment it is also the Serve credential of the Environment's enrollment on `sandbox-link` | -| Operator device profile | `oac-core-device`, run by an operator with database access | `agent-daemon/bootstrap`, `device-status` and `ws` | +| Agent-host credential | Installation initialization writes the [agent-host identity](../../docs/configuration.md#agent-host-container); Core registers it at startup | `agent-daemon/bootstrap`, `agent-daemon/ws` and `sandbox-link` as an attach peer | Core keeps only a SHA-256 digest of each token and credential it stores; installation grants are signed and not stored. Credentials are not interchangeable: each works only on its own routes. -### Operator device profile - -`oac-core-device` provisions a Runtime device profile directly in the database: - -```sh -umask 077 -mkdir -p ~/.oac/daemon/default -OAC_DATABASE_URL=... oac-core-device --tenant --name 'engine host' --url https://core.example > ~/.oac/daemon/default/auth.json -oac-daemon connect --profile default -``` - -`--tenant` is the Project's execution tenant UUID and `--url` Core's origin without a path. The command prints the profile once: `server_url` (the origin plus `/api/v1`), `runtime_id` (the device ID), `runner_credential` and `device_name`. Use a new profile rather than overwriting another device's file, and copy it privately to the same path on a remote host. `oac-core-device --tenant --revoke ` revokes the device: new connections are refused at once, and an open connection closes at its next heartbeat. Core binds Sessions only to the deployment's agent host ([Session assignments](../../docs/runtime-protocol.md#session-assignments)), so a device of this profile runs no Session. - ## Node routes ### Read the node configuration @@ -100,13 +86,9 @@ The credential is checked before any deployment state, so a rejected credential, ### Daemon bootstrap -`POST /api/v1/agent-daemon/bootstrap` with the daemon credential and `{"device_id": "…"}` returns `device_id`, `workspace_id`, `ws_url` (derived from `OAC_PUBLIC_URL`, never from request headers), `heartbeat_seconds` and `protocol_version`. The daemon then dials `ws_url` as the [Core–Runtime protocol](../../docs/runtime-protocol.md#ownership-and-connection) describes. - -### Device status - -`GET /api/v1/agent-daemon/device-status?device_id=` with the daemon credential returns `device_id` and `online`, which says whether the device has a live connection to Core. +`POST /api/v1/agent-daemon/bootstrap` with the agent-host credential and `{"device_id": "…"}` returns `device_id`, `workspace_id` (an empty string for the deployment-scoped host), `ws_url` (derived from `OAC_PUBLIC_URL`, never from request headers), `heartbeat_seconds` and `protocol_version`. The daemon then dials `ws_url` as the [Core–Runtime protocol](../../docs/runtime-protocol.md#ownership-and-connection) describes. -The bootstrap, device-status and WebSocket routes share one error body, `{"error": code, "detail": text}`: 400 `missing_params`, `missing_device_id` or `bad_json`; 401 `missing_bearer`, `unknown_device` or `bad_credential`; 403 `wrong_runtime_type`; 500 `internal`; and on the WebSocket 426 `incompatible_version` when `version` is not Core's exact Runtime protocol version. +The bootstrap and WebSocket routes share one error body, `{"error": code, "detail": text}`: 400 `missing_params`, `missing_device_id` or `bad_json`; 401 `missing_bearer`, `unknown_device` or `bad_credential`; 403 `wrong_runtime_type`; 500 `internal`; and on the WebSocket 426 `incompatible_version` when `version` is not Core's exact Runtime protocol version. ### Enroll a self-hosted daemon diff --git a/contracts/agents-api/runtime-observability-api.md b/contracts/agents-api/runtime-observability-api.md index b4bc701d1..79f8ee991 100644 --- a/contracts/agents-api/runtime-observability-api.md +++ b/contracts/agents-api/runtime-observability-api.md @@ -39,7 +39,6 @@ The list has one row for every Session of every Project that is not deleted, inc "instance": { "kind": "managed_allocation", "allocation_id": "d23ab94e-e40b-45bd-93a2-444f1f74642b", - "device_id": "2e434f4f-76aa-4e54-a707-4757036d90ef", "connection_generation": null }, "lifecycle_state": "active", @@ -115,7 +114,6 @@ This returns one `RuntimeObservation`, without `disk`. It accepts no query param | --- | --- | | `kind` | `managed_allocation`, `self_hosted_connection` or `none`. | | `allocation_id` | The managed allocation, which identifies the compute of a managed Session; null otherwise. | -| `device_id` | The Runtime device bound to the managed allocation, when there is one; null otherwise. | | `connection_generation` | Always null: Core does not observe self-hosted connections. | ### `cpu` diff --git a/contracts/agents-api/v1/runtime_observations.go b/contracts/agents-api/v1/runtime_observations.go index 8e9fef69e..5ceaf3521 100644 --- a/contracts/agents-api/v1/runtime_observations.go +++ b/contracts/agents-api/v1/runtime_observations.go @@ -22,7 +22,6 @@ type RuntimeObservation struct { type RuntimeInstance struct { Kind string `json:"kind" enums:"managed_allocation,self_hosted_connection,none" binding:"required"` AllocationID *string `json:"allocation_id" extensions:"x-nullable" binding:"required" format:"uuid"` - DeviceID *string `json:"device_id" extensions:"x-nullable" binding:"required" format:"uuid"` ConnectionGeneration *string `json:"connection_generation" extensions:"x-nullable" binding:"required" format:"uuid"` } diff --git a/contracts/agents-api/zh/machine-api.md b/contracts/agents-api/zh/machine-api.md index ea68d0c94..a3152de69 100644 --- a/contracts/agents-api/zh/machine-api.md +++ b/contracts/agents-api/zh/machine-api.md @@ -1,7 +1,7 @@ --- title: "机器连接 API" source: contracts/agents-api/machine-api.md -source_hash: 873d25773a865039ae1f1f8983d7b26e3bc772fad7301b36fc03a3ffd7084186 +source_hash: f42768f959577708b0f4bc1db63d42498e032a8668416ae8f62736d184efc543 --- 机器通过 `/api/v1` 调用 Core:包括沙箱节点、Runtime daemon、Sandbox I/O 服务和自托管安装器。各路由仅接受所列凭据,不接受 Core 密钥或 Project API 密钥;控制台登录也不授予此处权限。反向代理将 `/api/v1` 直接发送给 Core;Web 不提供这些路由。 @@ -18,14 +18,13 @@ source_hash: 873d25773a865039ae1f1f8983d7b26e3bc772fad7301b36fc03a3ffd7084186 | `POST agent-daemon/installation`, `POST agent-daemon/installation/claim` | 自托管安装器 | 安装授权 | [安装授权](environment-executor-credentials.md#installation-grant) | | `POST agent-daemon/enroll` | 自托管 daemon | 执行器凭据 | [登记自托管 daemon](#enroll-a-self-hosted-daemon) | | `GET agent-daemon/connection?environment_id=` | 自托管安装器 | 执行器凭据 | [私有连接确认](environment-executor-credentials.md#private-connection-confirmation) | -| `POST agent-daemon/bootstrap` | Runtime daemon | daemon 凭据 | [daemon 引导](#daemon-bootstrap) | -| `GET agent-daemon/device-status?device_id=` | Runtime daemon | daemon 凭据 | [设备状态](#device-status) | -| WebSocket `GET agent-daemon/ws?device_id=&version=` | Runtime daemon | daemon 凭据 | [Core–Runtime 协议](../../../docs/zh/runtime-protocol.md) | +| `POST agent-daemon/bootstrap` | Agent-host Runtime | Agent-host 凭据 | [daemon 引导](#daemon-bootstrap) | +| WebSocket `GET agent-daemon/ws?device_id=&version=` | Agent-host Runtime | Agent-host 凭据 | [Core–Runtime 协议](../../../docs/zh/runtime-protocol.md) | | WebSocket `GET sandbox-link` | Sandbox I/O 服务(serve peer)和 agent-host Runtime(attach peer) | 资源的 Serve 凭据或 agent host 的 Runtime 凭据,在 Link Hello 中发送 | [Sandbox link 协议](../../../docs/zh/sandbox-link-protocol.md) | 所有凭据通过 `Authorization: Bearer` 头传输;`sandbox-link` 例外,各 peer 在升级之后的 Link Hello 中发送凭据。凭据从不放入 URL。Core 从 `OAC_PUBLIC_URL` 派生 [Link URL](../../../docs/zh/configuration.md#changing-the-public-url)。 -生成的 [`runtime.openapi.yaml`](../runtime.openapi.yaml) 仅描述 sandbox-node 配置、登记、身份路由、两个安装路由和 `sandbox-link` 升级;该升级上的消息由 Sandbox link 协议定义。`sandbox-node/connect` 和 `agent-daemon/ws` 两个 WebSocket 及 daemon 引导、设备状态、登记和连接路由在 API 路由器外提供,无生成 schema;本文及所链接契约是它们唯一的定义。 +生成的 [`runtime.openapi.yaml`](../runtime.openapi.yaml) 仅描述 sandbox-node 配置、登记、身份路由、两个安装路由和 `sandbox-link` 升级;该升级上的消息由 Sandbox link 协议定义。`sandbox-node/connect` 和 `agent-daemon/ws` 两个 WebSocket 及 daemon 引导、登记和连接路由在 API 路由器外提供,无生成 schema;本文及所链接契约是它们唯一的定义。 ## 凭据 {#credentials} @@ -35,23 +34,10 @@ source_hash: 873d25773a865039ae1f1f8983d7b26e3bc772fad7301b36fc03a3ffd7084186 | 节点凭据 | 节点自身:生成 32 至 256 个无空白字符的密钥,在登记时注册 | 带 `X-OAC-Node-ID` 的 `sandbox-node/configuration`、`sandbox-node/identity`、`sandbox-node/connect` | | 安装授权 | `self_hosted` Session 的 `x_agents_core.installation` 命令;短期有效 | `agent-daemon/installation` 及其 `claim` | | 执行器凭据 | 安装领取,或 Core 密钥[执行器凭据路由](environment-executor-credentials.md) | `agent-daemon/enroll` 和 `agent-daemon/connection`;登记后也作为该 Environment 的 enrollment 在 `sandbox-link` 上的 Serve 凭据 | -| 操作者设备配置 | 具有数据库访问权限的操作者运行 `oac-core-device` | `agent-daemon/bootstrap`、`device-status` 和 `ws` | +| Agent-host 凭据 | 安装初始化写入 [agent-host 身份](../../../docs/zh/configuration.md#agent-host-container);Core 在启动时注册它 | `agent-daemon/bootstrap`、`agent-daemon/ws` 和作为 attach peer 的 `sandbox-link` | Core 对存储的每个 token 和凭据仅保留 SHA-256 摘要;安装授权经签名但不存储。凭据不可互换:各自仅适用于自身路由。 -### 操作者设备配置 {#operator-device-profile} - -`oac-core-device` 直接在数据库中创建 Runtime 设备配置: - -```sh -umask 077 -mkdir -p ~/.oac/daemon/default -OAC_DATABASE_URL=... oac-core-device --tenant --name 'engine host' --url https://core.example > ~/.oac/daemon/default/auth.json -oac-daemon connect --profile default -``` - -`--tenant` 为 Project 执行租户 UUID,`--url` 为不带路径的 Core origin。命令打印配置一次:`server_url`(origin 加 `/api/v1`)、`runtime_id`(设备 ID)、`runner_credential` 和 `device_name`。使用新配置,不覆盖其他设备文件;私密复制到远程主机相同路径。`oac-core-device --tenant --revoke ` 撤销设备:立即拒绝新连接,已有连接在下一次心跳关闭。Core 只把 Session 绑定到部署的 agent host([Session 分配](../../../docs/zh/runtime-protocol.md#session-assignments)),因此此配置的设备不运行任何 Session。 - ## 节点路由 {#node-routes} ### 读取节点配置 {#read-the-node-configuration} @@ -102,13 +88,9 @@ Core 在一个事务中检查 token 有效、部署已初始化且为节点型 ### daemon 引导 {#daemon-bootstrap} -`POST /api/v1/agent-daemon/bootstrap` 携带 daemon 凭据及 `{"device_id": "…"}`,返回 `device_id`、`workspace_id`、`ws_url`(从 `OAC_PUBLIC_URL` 推导,不使用请求头)、`heartbeat_seconds` 和 `protocol_version`。daemon 随后按 [Core–Runtime 协议](../../../docs/zh/runtime-protocol.md#ownership-and-connection)连接 `ws_url`。 - -### 设备状态 {#device-status} - -`GET /api/v1/agent-daemon/device-status?device_id=` 携带 daemon 凭据,返回 `device_id` 和 `online`,后者表示设备当前是否与 Core 保持活动连接。 +`POST /api/v1/agent-daemon/bootstrap` 携带 agent-host 凭据及 `{"device_id": "…"}`,返回 `device_id`、`workspace_id`(部署范围的主机为空字符串)、`ws_url`(从 `OAC_PUBLIC_URL` 推导,不使用请求头)、`heartbeat_seconds` 和 `protocol_version`。daemon 随后按 [Core–Runtime 协议](../../../docs/zh/runtime-protocol.md#ownership-and-connection)连接 `ws_url`。 -引导、设备状态和 WebSocket 路由共享错误体 `{"error": code, "detail": text}`:400 `missing_params`、`missing_device_id` 或 `bad_json`;401 `missing_bearer`、`unknown_device` 或 `bad_credential`;403 `wrong_runtime_type`;500 `internal`;WebSocket 的 `version` 不等于 Core 精确 Runtime 协议版本时返回 426 `incompatible_version`。 +引导和 WebSocket 路由共享错误体 `{"error": code, "detail": text}`:400 `missing_params`、`missing_device_id` 或 `bad_json`;401 `missing_bearer`、`unknown_device` 或 `bad_credential`;403 `wrong_runtime_type`;500 `internal`;WebSocket 的 `version` 不等于 Core 精确 Runtime 协议版本时返回 426 `incompatible_version`。 ### 登记自托管 daemon {#enroll-a-self-hosted-daemon} diff --git a/contracts/agents-api/zh/runtime-observability-api.md b/contracts/agents-api/zh/runtime-observability-api.md index 660c4a7d9..6e7ac50e4 100644 --- a/contracts/agents-api/zh/runtime-observability-api.md +++ b/contracts/agents-api/zh/runtime-observability-api.md @@ -1,7 +1,7 @@ --- title: "Runtime 遥测 API" source: contracts/agents-api/runtime-observability-api.md -source_hash: 05ee25e01c2a8e9ce0f85c325a4a0e0e2f11eb76861e54740efa8975d9c5a999 +source_hash: 1c52fa06b312330585b304300b61993d2021105196d58aff7ee0daacafd744b8 --- Core 通过 `/core/v1` 下的只读管理员路由报告托管 Runtime 和沙箱节点所使用的信息:当前 Runtime 观测值、单个 Session 的已存储 Runtime 历史记录,以及沙箱节点的主机观测值和历史记录。读取操作绝不创建、唤醒、续期或更改计算资源,也绝不向历史记录添加样本。[Runtime observability](runtime-observability.md) 定义了 Core 如何采集和保留这些值;[Console API usage](../../../docs/zh/web/console-api-usage.md) 列出了读取这些值的 Web 页面。 @@ -41,7 +41,6 @@ Authorization: Bearer "instance": { "kind": "managed_allocation", "allocation_id": "d23ab94e-e40b-45bd-93a2-444f1f74642b", - "device_id": "2e434f4f-76aa-4e54-a707-4757036d90ef", "connection_generation": null }, "lifecycle_state": "active", @@ -117,7 +116,6 @@ Authorization: Bearer | --- | --- | | `kind` | `managed_allocation`、`self_hosted_connection` 或 `none`。 | | `allocation_id` | 用于标识托管 Session 计算资源的托管分配;其他情况下为 null。 | -| `device_id` | 存在时,为绑定到托管分配的 Runtime 设备;其他情况下为 null。 | | `connection_generation` | 始终为 null:Core 不观测自托管连接。 | ### `cpu` {#cpu} diff --git a/deploy/distribution/Dockerfile b/deploy/distribution/Dockerfile index a20af95ce..cd7805c0f 100644 --- a/deploy/distribution/Dockerfile +++ b/deploy/distribution/Dockerfile @@ -5,7 +5,7 @@ FROM debian:bookworm-slim@sha256:3783cc01769c7b2b1b83a5c5ad96c815348e28ed7da68e2 RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates libgcc-s1 \ && rm -rf /var/lib/apt/lists/* -COPY --chmod=0555 bin/oac-core bin/oac-core-device bin/oac-core-environment-key bin/oac /usr/local/bin/ +COPY --chmod=0555 bin/oac-core bin/oac-core-environment-key bin/oac /usr/local/bin/ COPY e2b/ /opt/oac/e2b/ # Only the small version/checksum catalog, never native execution archives. diff --git a/deploy/install.dev.sh b/deploy/install.dev.sh index 85043829d..584751182 100755 --- a/deploy/install.dev.sh +++ b/deploy/install.dev.sh @@ -67,7 +67,6 @@ go_build() { ( cd "$repo_root" go_build services/core/cmd/server "$build/core/bin/oac-core" - go_build services/core/cmd/device "$build/core/bin/oac-core-device" go_build services/core/cmd/environment-key "$build/core/bin/oac-core-environment-key" go_build services/core/cmd/oac "$build/core/bin/oac" go_build services/web "$build/web/oac-web" diff --git a/docs/api/public-agent-api.md b/docs/api/public-agent-api.md index b6aabd446..9930a5f4a 100644 --- a/docs/api/public-agent-api.md +++ b/docs/api/public-agent-api.md @@ -249,7 +249,7 @@ Returns 201 with the Session: | --- | --- | --- | | `openai_hosted` | A sandbox Core creates on a node or E2B; the administrator provides the capacity | Optional `network`, `packages`, `files`, `skills`, `plugins`, `env`, `capability_directories`, `setup_commands`, or a template | | `self_hosted` | Your own Linux machine | Requires an absolute `workspace_directory`. Skills, packages, files or a template go in `x_agents_core.environment`. The response carries install commands in `x_agents_core.installation`; see [self-hosted execution](../getting-started/self-hosted.md) | -| `none` | A device connection an operator registered, with no workspace | `input` required | +| `none` | The deployment's agent host, with no workspace | `input` required | A new `openai_hosted` Session reads `idle` while Core prepares its sandbox; its first Turn starts when the Environment is ready. The [Environment contract](../../contracts/agents-api/environments.md) owns placement, expiry and preparation. diff --git a/docs/maintainers.md b/docs/maintainers.md index 8c663d296..8ce15b217 100644 --- a/docs/maintainers.md +++ b/docs/maintainers.md @@ -122,7 +122,7 @@ The helper is written to `~/.oac/build/microsandbox-provider/oac-microsandbox-pr ### Standalone Core builds -`make build-core` builds `oac-core`, `oac-core-device`, `oac-core-environment-key` `oac-node` and `oac` into `${OAC_DEV_HOME:-$HOME/.oac}/build/oac-core` (`OAC_DEV_CORE_BUILD_DIR` selects another absolute directory). The build copies only the source set listed in `scripts/build-core.sh` (the Core service, its contracts, the shared packages it needs and the root Go module files) into a temporary context and builds with CGO disabled, read-only modules and trimmed paths. It needs no Node, Docker or other application. When Core gains a shared dependency, add that package to the list; never copy the whole repository to make it compile. +`make build-core` builds `oac-core`, `oac-core-environment-key`, `oac-node` and `oac` into `${OAC_DEV_HOME:-$HOME/.oac}/build/oac-core` (`OAC_DEV_CORE_BUILD_DIR` selects another absolute directory). The build copies only the source set listed in `scripts/build-core.sh` (the Core service, its contracts, the shared packages it needs and the root Go module files) into a temporary context and builds with CGO disabled, read-only modules and trimmed paths. It needs no Node, Docker or other application. When Core gains a shared dependency, add that package to the list; never copy the whole repository to make it compile. `make docker-build-core` builds the image `oac-core:dev` (`OAC_DEV_CORE_IMAGE` selects another name) from those five commands and the E2B helper. The base is the digest-pinned `debian:bookworm-slim` with CA certificates and the glibc runtime the helper needs; the default user is UID/GID 65532 and Core listens on `:8091`. This local target builds Linux amd64; the [distribution build](#build-a-distribution) builds both architectures. Changes to the image or its build need `make check-core-container` in addition to the relevant source checks: it runs the official-client suite against the image with a read-only root filesystem and needs Linux Docker, a non-root user, and the [test database and pinned SDK](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/README.md#official-client-verification) of the service checks (`OAC_TEST_DATABASE_URL` naming an `oac_*_tests` database with the migrations applied, and `OAC_TEST_OFFICIAL_SDK_PYTHON`). diff --git a/docs/sandbox-provider.md b/docs/sandbox-provider.md index 6df4efab4..3cbb75237 100644 --- a/docs/sandbox-provider.md +++ b/docs/sandbox-provider.md @@ -156,13 +156,13 @@ Node readiness binds to the exact generation, the current connection and the own ### Allocation lifecycle -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. +The allocation and its Serve credential digest 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. +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 Worker observes Environment connections through the authenticated [Link resource](./sandbox-link-protocol.md), independently of allocation provisioning. Between Turns, Core checks that connected, observed compute is still its Session's running allocation; the check changes nothing and never revives a cleanup request. Running compute never expires: explicit deletion and the snapshot retention authorize its cleanup. A stopped or missing container never authorizes discarding retained workspace or history. Disabling the provider stops new hosted admission and bootstrap but never blocks cancellation, function results or input retry outcomes of existing Sessions. -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`. +Terminal cleanup atomically withdraws the allocation's Serve authority, releases the Session's agent-host assignment while retaining its home, 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`. ### `oac-sandbox-io` diff --git a/docs/zh/api/public-agent-api.md b/docs/zh/api/public-agent-api.md index 754be3740..db9931f46 100644 --- a/docs/zh/api/public-agent-api.md +++ b/docs/zh/api/public-agent-api.md @@ -1,7 +1,7 @@ --- title: "Agents API 指南" source: docs/api/public-agent-api.md -source_hash: d5cb941564c90dc90cf38c9bf9c839485aed5239b24d93e8bcabcd104c0a0df7 +source_hash: 4c5342136dda13c416f096568c2f5961819808ffddfaafb3ab0ab6557344c28b --- Core 在 `/v1` 提供 [OpenAI Agents API](https://platform.openai.com/docs/api-reference)。可以使用官方 OpenAI SDK 或普通 HTTP。本指南针对每项常见操作同时展示这两种方式,并说明 Core 与 OpenAI 存在差异的地方。 @@ -251,7 +251,7 @@ oac "/agents/sessions" -H "Idempotency-Key: $(uuidgen)" -d '{ | --- | --- | --- | | `openai_hosted` | Core 在某个节点或 E2B 上创建的沙箱;容量由管理员提供 | 可选 `network`、`packages`、`files`、`skills`、`plugins`、`env`、`capability_directories`、`setup_commands`,也可指定模板 | | `self_hosted` | 你自己的 Linux 计算机 | 要求提供绝对路径 `workspace_directory`。Skills、软件包、文件或模板应放在 `x_agents_core.environment` 中。响应会在 `x_agents_core.installation` 中携带安装命令;请参阅 [self-hosted execution](../getting-started/self-hosted.md) | -| `none` | 由操作员注册的设备连接,无工作区 | `input` 为必填 | +| `none` | 部署的 agent host,没有工作区 | `input` 为必填 | 新建的 `openai_hosted` Session 在 Core 准备沙箱期间会读取到 `idle`;Environment 准备就绪后,其首个 Turn 才会启动。[Environment contract](../../../contracts/agents-api/zh/environments.md) 负责部署位置、过期和准备过程。 diff --git a/docs/zh/maintainers.md b/docs/zh/maintainers.md index e4dca3fe3..0caf181d3 100644 --- a/docs/zh/maintainers.md +++ b/docs/zh/maintainers.md @@ -1,7 +1,7 @@ --- title: "构建并发布 OpenAgentCore" source: docs/maintainers.md -source_hash: 4446c6769f46b6379e6ee078b8cfa2450190321cff34faaa2525e72e47cef350 +source_hash: d9f683219343883082b468e7d931d6a07ced420af3f7a1cc3adba777a1afd275 --- 本指南面向负责构建和发布 OpenAgentCore 的维护者。要安装 Core 和 Web,请使用 [安装指南](getting-started/install.md)。安装器代码遵循的规则见 [部署](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/README.md) 和 [节点安装器](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/deploy/node/README.md);必需检查见 [CONTRIBUTING](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/CONTRIBUTING.md#required-checks)。 @@ -124,7 +124,7 @@ make check-microsandbox-provider ### 独立 Core 构建 {#standalone-core-builds} -`make build-core` 会将 `oac-core`、`oac-core-device`、`oac-core-environment-key`、`oac-node` 和 `oac` 构建到 `${OAC_DEV_HOME:-$HOME/.oac}/build/oac-core`(`OAC_DEV_CORE_BUILD_DIR` 可选择其他绝对目录)。构建过程仅将 `scripts/build-core.sh` 中列出的源文件集(Core 服务、其契约、所需的共享软件包以及根 Go 模块文件)复制到临时上下文,并使用禁用 CGO、只读模块和裁剪路径的方式构建。它不需要 Node、Docker 或其他应用程序。Core 新增共享依赖时,请将该软件包加入列表;绝不能复制整个仓库来使其完成编译。 +`make build-core` 会将 `oac-core`、`oac-core-environment-key`、`oac-node` 和 `oac` 构建到 `${OAC_DEV_HOME:-$HOME/.oac}/build/oac-core`(`OAC_DEV_CORE_BUILD_DIR` 可选择其他绝对目录)。构建过程仅将 `scripts/build-core.sh` 中列出的源文件集(Core 服务、其契约、所需的共享软件包以及根 Go 模块文件)复制到临时上下文,并使用禁用 CGO、只读模块和裁剪路径的方式构建。它不需要 Node、Docker 或其他应用程序。Core 新增共享依赖时,请将该软件包加入列表;绝不能复制整个仓库来使其完成编译。 `make docker-build-core` 会根据这五个命令和 E2B 辅助程序构建 `oac-core:dev` 镜像(`OAC_DEV_CORE_IMAGE` 可选择其他名称)。基础镜像是通过摘要固定的 `debian:bookworm-slim`,包含 CA 证书以及辅助程序所需的 glibc 运行时;默认用户的 UID/GID 为 65532,Core 监听 `:8091`。此本地构建目标生成 Linux amd64 镜像;[分发构建](#build-a-distribution)生成两种架构的镜像。对镜像或其构建进行更改时,除了相关的源代码检查外,还必须运行 `make check-core-container`:它会在只读根文件系统上针对该镜像运行官方客户端测试套件,并且需要 Linux Docker、非 root 用户,以及服务检查中的[测试数据库和固定版本 SDK](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/README.md#official-client-verification)(`OAC_TEST_DATABASE_URL` 指向一个已应用迁移的 `oac_*_tests` 数据库,并设置 `OAC_TEST_OFFICIAL_SDK_PYTHON`)。 diff --git a/docs/zh/sandbox-provider.md b/docs/zh/sandbox-provider.md index 6bb27e917..efa88666b 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: 2dd362d329e39d15c3b6ddfa727429ab282c037015dabcece938d6daaeffaa99 +source_hash: 9caf2d1e9f5e5ba07d9727051765f1726682bfcc65a6459bde41b7eeb306fc0a --- **Sandbox Provider** 为 Core 管理的 Environment 提供计算资源,以及在其中启动 [Sandbox I/O 服务](#oac-sandbox-io)的有界引导流程;该服务是 Provider 启动的唯一进程。本指南说明如何添加 Provider,并作为 Core 驱动 Provider 的参考。接口为 [`SandboxProvider`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/internal/sandbox/sandbox_provider.go)。 @@ -158,13 +158,13 @@ Node readiness 绑定到精确 generation、当前连接和 owner epoch。持久 ### Allocation 生命周期 {#allocation-lifecycle} -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 推断。 +allocation 及其 Serve credential digest 在 `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 的认证连接观测。 +配置 provider 后,Worker 扫描已提交且没有 allocation 的 pending hosted Environment,涵盖空闲 Session 创建以及 commit 与 bootstrap 之间中断后的恢复;已有 allocation 不重新进入此路径。scan 有界,由 lifecycle owner 串行化,不需要调用方操作。没有 Turn 的初始预约让 Session 保持空闲,daemon 连接不被当作原生 readiness。Worker 通过已认证的 [Link resource](sandbox-link-protocol.md) 观测 Environment 连接,与 allocation provisioning 独立。 Core 在 Turn 之间检查已连接且已观察的计算资源仍是其 Session 正在运行的 allocation;该检查不做任何修改,也不复活 cleanup 请求。正在运行的计算资源不会到期:显式删除和 snapshot retention 授权其清理。停止或缺失 container 不授权丢弃保留工作区或历史。禁用 provider 停止新 hosted admission 与 bootstrap,但不阻止现有 Session 的取消、function result 或 input retry outcome。 -终结清理原子撤销 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`。 +终结清理原子撤销 allocation 的 Serve authority、释放 Session 的 agent-host assignment 并保留 home、记录 Environment 失败或到期、结算 pending input 并请求取消,然后才调用 `Kill`;原 input deadline 与 retry outcome 保留。临时 provider outage、未知 Create result 和停止的计算资源不证明永久失败。公开 Session 删除后 Core 保留 allocation,仅在所属 compute 与 volume 清理完成且原 Create 已结算的证明成立后标记 released;未知创建即使观察到不存在也保留 cleanup ownership,有界 scan 继续捕捉延迟资源,不再调用 `Create`。 ### `oac-sandbox-io` {#oac-sandbox-io} diff --git a/packages/agents-client/src/admin-client.test.ts b/packages/agents-client/src/admin-client.test.ts index e4a0d6496..38d52560a 100644 --- a/packages/agents-client/src/admin-client.test.ts +++ b/packages/agents-client/src/admin-client.test.ts @@ -435,7 +435,7 @@ describe("AdminClient project monitoring", () => { const observation = { id: sessionId, object: "agent.runtime_observation", session_id: sessionId, environment_id: resourceId, mode: "openai_hosted", provider_type: "docker", - instance: { kind: "managed_allocation", allocation_id: resourceId, device_id: keyId, connection_generation: null }, + instance: { kind: "managed_allocation", allocation_id: resourceId, connection_generation: null }, lifecycle_state: "active", status: "observed", reason: null, allocation_created_at: 10, resolved_at: 30, observed_at: 20, started_at: 10, cpu: { usage_seconds_total: 0, capacity_cores: 2, usage_cores: null, utilization_ratio: null }, @@ -458,7 +458,7 @@ describe("AdminClient project monitoring", () => { const observation = { id: sessionId, object: "agent.runtime_observation", session_id: sessionId, environment_id: resourceId, mode: "openai_hosted", provider_type: "e2b", - instance: { kind: "managed_allocation", allocation_id: resourceId, device_id: keyId, connection_generation: null }, + instance: { kind: "managed_allocation", allocation_id: resourceId, connection_generation: null }, lifecycle_state: "active", status: "observed", reason: null, allocation_created_at: 10, resolved_at: 30, observed_at: 20, started_at: 10, cpu: { usage_seconds_total: null, capacity_cores: 2, usage_cores: null, utilization_ratio: 0.1955 }, diff --git a/packages/agents-client/src/client.test.ts b/packages/agents-client/src/client.test.ts index 99c737350..c6bc99f97 100644 --- a/packages/agents-client/src/client.test.ts +++ b/packages/agents-client/src/client.test.ts @@ -110,7 +110,6 @@ const runtimeProjectId = "33333333-3333-4333-8333-333333333333"; const runtimeSessionId = "11111111-1111-4111-8111-111111111111"; const runtimeEnvironmentId = "22222222-2222-4222-8222-222222222222"; const runtimeAllocationId = "33333333-3333-4333-8333-333333333333"; -const runtimeDeviceId = "44444444-4444-4444-8444-444444444444"; function runtimeObservation(overrides: Record = {}): Record { return { @@ -123,7 +122,6 @@ function runtimeObservation(overrides: Record = {}): Record { environment_id: null, mode: "none", provider_type: null, - instance: { kind: "none", allocation_id: null, device_id: null, connection_generation: null }, + instance: { kind: "none", allocation_id: null, connection_generation: null }, lifecycle_state: null, status: "unsupported", reason: "runtime_mode_not_observable", diff --git a/packages/agents-client/src/client.ts b/packages/agents-client/src/client.ts index 5beb09c3a..5a350810a 100644 --- a/packages/agents-client/src/client.ts +++ b/packages/agents-client/src/client.ts @@ -889,13 +889,11 @@ export function projectRuntimeObservation(value: unknown, expectedSessionId?: st ) return invalidRuntimeObservation(); const allocationId = value.instance.allocation_id === null ? null : canonicalUuid(value.instance.allocation_id); - const deviceId = value.instance.device_id === null ? null : canonicalUuid(value.instance.device_id); const connectionGeneration = value.instance.connection_generation === null ? null : canonicalUuid(value.instance.connection_generation); if ( (value.instance.allocation_id !== null && allocationId === null) || - (value.instance.device_id !== null && deviceId === null) || (value.instance.connection_generation !== null && connectionGeneration === null) ) return invalidRuntimeObservation(); @@ -908,7 +906,7 @@ export function projectRuntimeObservation(value: unknown, expectedSessionId?: st if ( (isNone && ( value.instance.kind !== "none" || environmentId !== null || value.provider_type !== null || - allocationId !== null || deviceId !== null || connectionGeneration !== null || allocationCreatedAt !== null || + allocationId !== null || connectionGeneration !== null || allocationCreatedAt !== null || value.lifecycle_state !== null )) || (isSelfHosted && ( @@ -918,7 +916,7 @@ export function projectRuntimeObservation(value: unknown, expectedSessionId?: st (isManaged && ( value.instance.kind !== "managed_allocation" || environmentId === null || connectionGeneration !== null || !isOneOf(runtimeObservationLifecycleStateValues, value.lifecycle_state) || - (allocationId === null && (deviceId !== null || allocationCreatedAt !== null)) + (allocationId === null && allocationCreatedAt !== null) )) ) return invalidRuntimeObservation(); @@ -977,7 +975,7 @@ export function projectRuntimeObservation(value: unknown, expectedSessionId?: st id, object: "agent.runtime_observation", session_id: sessionId, environment_id: environmentId, mode: value.mode, provider_type: value.provider_type, instance: { kind: value.instance.kind as RuntimeObservation["instance"]["kind"], - allocation_id: allocationId, device_id: deviceId, connection_generation: connectionGeneration, + allocation_id: allocationId, connection_generation: connectionGeneration, }, status: value.status, reason: value.reason as RuntimeObservation["reason"], lifecycle_state: value.lifecycle_state as RuntimeObservation["lifecycle_state"], diff --git a/packages/agents-client/src/generated/core-api.ts b/packages/agents-client/src/generated/core-api.ts index e99cf3d5c..f6dab94ae 100644 --- a/packages/agents-client/src/generated/core-api.ts +++ b/packages/agents-client/src/generated/core-api.ts @@ -716,10 +716,9 @@ export const runtimeHistoryTokenUsagePointFields = ["end", "input_tokens", "outp export interface RuntimeInstance { allocation_id: string | null; connection_generation: string | null; - device_id: string | null; kind: RuntimeInstanceKind; } -export const runtimeInstanceFields = ["allocation_id", "connection_generation", "device_id", "kind"] as const; +export const runtimeInstanceFields = ["allocation_id", "connection_generation", "kind"] as const; export const runtimeInstanceKindValues = ["managed_allocation", "self_hosted_connection", "none"] as const; export type RuntimeInstanceKind = (typeof runtimeInstanceKindValues)[number]; export interface RuntimeMemoryObservation { diff --git a/packages/agents-client/src/types.ts b/packages/agents-client/src/types.ts index c9e056007..4981a8c3e 100644 --- a/packages/agents-client/src/types.ts +++ b/packages/agents-client/src/types.ts @@ -804,7 +804,6 @@ export interface RuntimeObservedObservation extends RuntimeObservationBase { instance: { kind: "managed_allocation"; allocation_id: string; - device_id: string | null; connection_generation: null; }; lifecycle_state: RuntimeObservationLifecycleState; @@ -824,7 +823,6 @@ export interface RuntimeUnavailableObservation extends RuntimeObservationBase { instance: { kind: "managed_allocation"; allocation_id: string | null; - device_id: string | null; connection_generation: null; }; lifecycle_state: RuntimeObservationLifecycleState; @@ -841,7 +839,7 @@ export interface RuntimeNoneObservation extends RuntimeObservationBase { environment_id: null; mode: "none"; provider_type: null; - instance: { kind: "none"; allocation_id: null; device_id: null; connection_generation: null }; + instance: { kind: "none"; allocation_id: null; connection_generation: null }; lifecycle_state: null; status: "unsupported"; reason: "runtime_mode_not_observable"; @@ -859,7 +857,6 @@ export interface RuntimeSelfHostedObservation extends RuntimeObservationBase { instance: { kind: "self_hosted_connection"; allocation_id: null; - device_id: string | null; connection_generation: string | null; }; lifecycle_state: null; diff --git a/scripts/build-core.sh b/scripts/build-core.sh index 04163c3e1..e3afd488b 100755 --- a/scripts/build-core.sh +++ b/scripts/build-core.sh @@ -33,7 +33,7 @@ tar -C "$repo_root" -cf - \ ( cd "$build_context" export GOWORK=off CGO_ENABLED=0 - for command in server device environment-key sandbox-node oac; do + for command in server environment-key sandbox-node oac; do artifact="oac-core-$command" if [[ "$command" == server ]]; then artifact=oac-core; fi if [[ "$command" == sandbox-node ]]; then artifact=oac-node; fi @@ -45,7 +45,7 @@ tar -C "$repo_root" -cf - \ # Publish only after every command builds successfully. mkdir -p "$output_dir" -for artifact in oac-core oac-core-device oac-core-environment-key oac-node oac; do +for artifact in oac-core oac-core-environment-key oac-node oac; do mv -f "$build_context/bin/$artifact" "$output_dir/$artifact" done printf 'Core commands: %s\n' "$output_dir" diff --git a/scripts/compose-smoke.py b/scripts/compose-smoke.py index 83548d7a7..5706079ee 100644 --- a/scripts/compose-smoke.py +++ b/scripts/compose-smoke.py @@ -66,7 +66,7 @@ def go_build(package, output, build_revision=revision): contexts = {name: directory / ('image-' + name) for name in ('core', 'web', 'ingress', 'agent-host')} core = contexts['core'] - for name, package in (('oac-core', 'server'), ('oac-core-device', 'device'), + for name, package in (('oac-core', 'server'), ('oac-core-environment-key', 'environment-key'), ('oac', 'oac')): go_build('services/core/cmd/' + package, core / 'bin' / name) (core / 'e2b').mkdir() diff --git a/services/core/IMPLEMENTATION.md b/services/core/IMPLEMENTATION.md index eefc3d054..433cf678f 100644 --- a/services/core/IMPLEMENTATION.md +++ b/services/core/IMPLEMENTATION.md @@ -18,9 +18,9 @@ One error is shared across domains, with one `api` helper: `textvalue.ErrUnstora Shared vocabulary has one owner each, and domains use it rather than copy it. `internal/environmentconfig` owns Environment setup, Skills, Plugins and initial files with their validation and public metadata; `Setup.Validate` checks requested configuration, where a Skill may be an unresolved reference, and `Setup.ValidateInstalled` checks frozen, installable configuration. `internal/skills` owns `ParseVersion`, the canonical positive decimal Skill version. `internal/metadata` owns the metadata rules: `Validate` for the pair, key and value limits and U+0000, `ValidateStorable` for U+0000 alone, and `Encode` with its 64 KiB bound. `internal/jsonobject` owns `Normalize`, the stable encoding of stored JSON objects that snapshots and retry identities compare. -`internal/sessions` owns the Session vocabulary: Sessions, Turns, inputs, Environments and their provisioning failures, function calls, Item and Artifact reads, executor credentials, and the errors Session operations return, which `api` maps in `writeSessionsError`. It also owns the Session change vocabulary and its decisions: the public changes that report Turn and Session transitions, what a Turn that ends settles, measured Turn usage and the Session activity a change reports. Sequences that several Session operations share are `sessions` procedures, each over the transaction interface it declares: `CancelTurn` and `CancelWork` cancel work, `TrackInputActivity` reports the input activity a write changes, `FailEnvironment` and `TerminateEnvironment` settle an Environment that failed or whose managed compute ended, `CreateEnvironmentDevice` creates and binds a hosted Environment's dedicated Runtime device, `CheckComputeAdmission`, `CheckFileWriteGate` and `CheckInputStart` gate new work, `AdmitInputs` admits a validated input batch in order, each input steering the active Turn, starting a Turn, requesting a cancellation or joining a function result to its call's Turn, `AdmitFunctionResult` saves a submitted function result for its Turn's call, `LoadRequiredActions` lists the calls a Turn waits on, and `TransitionTurn` moves a root Turn's status as its current status allows and settles a Turn that ends. `AppendTurnEvents` records a batch of a Turn's execution observations, which `NewJournalBatch` validates, and `AppendTurnEvent` records its terminal outcome; both project the new entries. `ProjectSource` projects one journal entry or admitted input into Items, the root Turn's usage and Subagents, and `ProjectInput` projects an admitted input. `ValidTransition` decides the Turn status transitions, and the Subagent projection decides each Subagent's identity, lifecycle, child Turn and child Item changes. `sessions.ExecutionOperations` runs the Session writes only the execution owner makes; each function-call and Turn operation loads the Turn in one leased Session transaction, decides in `sessions`, then applies in `sessionpg`, and `AppendTurnEvents` records and projects a Turn's journal batch in one leased Session transaction. `internal/items` owns public Items: it projects observations, merges them into stored Items and builds the ordered events that report each Item change. `internal/persistence/postgres/sessionpg` loads the facts those decisions read and applies them inside the caller's Session transaction, under the Session lock: it allocates event sequence positions, event IDs, Item positions, output indexes and Subagent IDs, writes the change journal, each Turn's execution journal and its counters, Items, Turn usage, Subagent bindings, child Turns and child Items, and Artifact settlement, and prunes the change journal. It decides nothing. `WithSession` runs a Session operation's transaction on the pool or the lease: it locks the Session, applies the operation, then prunes the change journal. `BindSession` binds one Session to a transaction's queries as a `SessionTx`, which implements the procedures' interfaces. +`internal/sessions` owns the Session vocabulary: Sessions, Turns, inputs, Environments and their provisioning failures, function calls, Item and Artifact reads, executor credentials, and the errors Session operations return, which `api` maps in `writeSessionsError`. It also owns the Session change vocabulary and its decisions: the public changes that report Turn and Session transitions, what a Turn that ends settles, measured Turn usage and the Session activity a change reports. Sequences that several Session operations share are `sessions` procedures, each over the transaction interface it declares: `CancelTurn` and `CancelWork` cancel work, `TrackInputActivity` reports the input activity a write changes, `FailEnvironment` and `TerminateEnvironment` settle an Environment that failed or whose managed compute ended, `CheckComputeAdmission`, `CheckFileWriteGate` and `CheckInputStart` gate new work, `AdmitInputs` admits a validated input batch in order, each input steering the active Turn, starting a Turn, requesting a cancellation or joining a function result to its call's Turn, `AdmitFunctionResult` saves a submitted function result for its Turn's call, `LoadRequiredActions` lists the calls a Turn waits on, and `TransitionTurn` moves a root Turn's status as its current status allows and settles a Turn that ends. `AppendTurnEvents` records a batch of a Turn's execution observations, which `NewJournalBatch` validates, and `AppendTurnEvent` records its terminal outcome; both project the new entries. `ProjectSource` projects one journal entry or admitted input into Items, the root Turn's usage and Subagents, and `ProjectInput` projects an admitted input. `ValidTransition` decides the Turn status transitions, and the Subagent projection decides each Subagent's identity, lifecycle, child Turn and child Item changes. `sessions.ExecutionOperations` runs the Session writes only the execution owner makes; each function-call and Turn operation loads the Turn in one leased Session transaction, decides in `sessions`, then applies in `sessionpg`, and `AppendTurnEvents` records and projects a Turn's journal batch in one leased Session transaction. `internal/items` owns public Items: it projects observations, merges them into stored Items and builds the ordered events that report each Item change. `internal/persistence/postgres/sessionpg` loads the facts those decisions read and applies them inside the caller's Session transaction, under the Session lock: it allocates event sequence positions, event IDs, Item positions, output indexes and Subagent IDs, writes the change journal, each Turn's execution journal and its counters, Items, Turn usage, Subagent bindings, child Turns and child Items, and Artifact settlement, and prunes the change journal. It decides nothing. `WithSession` runs a Session operation's transaction on the pool or the lease: it locks the Session, applies the operation, then prunes the change journal. `BindSession` binds one Session to a transaction's queries as a `SessionTx`, which implements the procedures' interfaces. -Other adapters call only these `sessionpg` participants, on their own transaction's queries: `LockSession`, which orders a write against the Session's Turns; `BindSession`, which runs a Session procedure, such as `CreateEnvironmentDevice` or `TransitionTurn`, in that transaction; and `LoadEnvironment`, which reads the tenant's Environment of a live Session. In turn, Session creation's transaction in `sessionpg` calls the `placementpg`, `skillpg` and `filepg` participants and `auditpg.RecordWriteAudit` on its own queries. `tests/fixtures` is test tooling that composes procedures over a pooled `WithSession`, which is why `WithSession` stays exported; no production code may do this. `deploymentpg` may import `sessionpg`, and `sessionpg` never imports `deploymentpg`; `placementpg`, which loads the placement facts and reserves placements, sits below both, so either may call it and it imports neither. Likewise `deployment` may compose the `sessions` procedures, and `sessions` never imports `deployment`. +Other adapters call only these `sessionpg` participants, on their own transaction's queries: `LockSession`, which orders a write against the Session's Turns; `BindSession`, which runs a Session procedure, such as `TransitionTurn`, in that transaction; and `LoadEnvironment`, which reads the tenant's Environment of a live Session. In turn, Session creation's transaction in `sessionpg` calls the `placementpg`, `skillpg` and `filepg` participants and `auditpg.RecordWriteAudit` on its own queries. `tests/fixtures` is test tooling that composes procedures over a pooled `WithSession`, which is why `WithSession` stays exported; no production code may do this. `deploymentpg` may import `sessionpg`, and `sessionpg` never imports `deploymentpg`; `placementpg`, which loads the placement facts and reserves placements, sits below both, so either may call it and it imports neither. Likewise `deployment` may compose the `sessions` procedures, and `sessions` never imports `deployment`. Adapters' execution repositories require a `*pgunit.Lease` at construction, and their public repositories expose no execution operation. @@ -35,10 +35,10 @@ Domain owners, each with its PostgreSQL adapter under `internal/persistence/post - `environmenttemplates` (`templatepg`): Environment Templates, their validation and default network, their sealed setup, initial files, Skills and Plugins, and the resolved Template that Session creation composes into its Environment. - `modelconfiguration` (`modelconfigurationpg`): each Harness's deployment default model configuration and its last-use observations. - `skills` (`skillpg`): Skills and their immutable versions: archive checks, the default and latest pointers, version selection and deletion, and each version's sealed archive. Session creation freezes selected versions inside its own transaction: `skillpg.LockSkills` locks the selected Skills in ID order, `skillpg.ReadVersionForFreeze` opens and verifies a version, and `sessions` selects each version with the `skills` rules. -- `deployment` and its subpackage `deployment/placement` (`deploymentpg`, `placementpg`): the sandbox deployment and its nodes: provider configuration and the sealed credential, the specification and retained generations, setup, update and switch, node enrollment, identity and authentication, generation configuration, capacity, presence, status, host history, reset, and the counts of nodes and sandboxes bound to the public address; and hosted runtime allocations: reservation with the dedicated device, compute ownership and settlement, observation diagnostics, activity, compute phases, wake receipts and cleanup, with the reads that schedule, discover and authorize them. It interprets Sandbox Provider declarations through the `providers.Registry` it is given, whose lookups return typed errors. `cmd/server` builds that registry and calls it only to build a direct Provider and to discover a Provider's configuration; it takes each setup's mode and declared operations from the `Setup` that `deployment` returns. `deployment/placement` owns hosted admission and placement: `cmd/server` builds one `placement.Rules` from the registry and the public URL, both fixed while Core runs, and gives it to `deployment.Service` and `sessions.Service`; its pure decisions admit a hosted Session, choose its node, and admit an allocation's reserved node and a restore on it, and its errors keep one status, code and message through `writeSandboxError`. `placementpg` loads the facts those decisions read and applies them on the caller's transaction-bound queries; it has no Store or transaction runner. The only other caller is Session creation: `sessions` decides admission and placement with those rules over the `placementpg` participants inside the creation transaction. Deployment changes and allocation writes run through `deployment.ExecutionOperations` on `deploymentpg.NewExecution(lease, …)`, which the Worker receives as `execution.Owner.Deployment`. Each allocation write locks the owning Session, decides in `deployment`, applies in `deploymentpg` and prunes the Session's change journal in the same leased transaction; cleanup settles the Session through the `sessions` procedures on a `sessionpg.SessionTx` bound to that transaction. The Worker reads the deployment, prepares a selection's setup and records live-compute activity through the pooled `deployment.Service` in `execution.Dispatcher.Deployment`, and lists the Sessions a reset still has to archive, schedules node lifecycles and reads allocations through the pooled `deployment.Reader` in `execution.Dispatcher.DeploymentReader`; a pooled read carries no lease, and the leased write that follows it rechecks the allocation's owner. Node management and reads use the pooled `deploymentpg.Store`, which `cmd/server` also reads the owner epoch from and runs the host-history sampler on. Provider calls run outside transactions, and the final transaction rechecks the expected generation. Session archive crosses the Session and the deployment, so it is a `deployment.ExecutionOperations` operation: `ArchiveSession`, which the administrator's archive route calls through `api.Execution.SessionArchive`, and `ArchiveResetSession`, which a reset calls, lock the Session through `deploymentpg`'s `WithSessionArchive`, check the deployment's generation and the running reset in `deployment`, then expire the Environment and cancel its work through the `sessions` procedures, revoke the allocation's device and request its cleanup, and record the audit entry in one leased transaction. `deployment.ObservationResolver` resolves a Session's Runtime observation target for `runtimeobs` from `sessions.SessionReader` and `deployment.Reader`. +- `deployment` and its subpackage `deployment/placement` (`deploymentpg`, `placementpg`): the sandbox deployment and its nodes: provider configuration and the sealed credential, the specification and retained generations, setup, update and switch, node enrollment, identity and authentication, generation configuration, capacity, presence, status, host history, reset, and the counts of nodes and sandboxes bound to the public address; and hosted runtime allocations: reservation with sandbox Serve authority, compute ownership and settlement, observation diagnostics, activity, compute phases, wake receipts and cleanup, with the reads that schedule, discover and authorize them. It interprets Sandbox Provider declarations through the `providers.Registry` it is given, whose lookups return typed errors. `cmd/server` builds that registry and calls it only to build a direct Provider and to discover a Provider's configuration; it takes each setup's mode and declared operations from the `Setup` that `deployment` returns. `deployment/placement` owns hosted admission and placement: `cmd/server` builds one `placement.Rules` from the registry and the public URL, both fixed while Core runs, and gives it to `deployment.Service` and `sessions.Service`; its pure decisions admit a hosted Session, choose its node, and admit an allocation's reserved node and a restore on it, and its errors keep one status, code and message through `writeSandboxError`. `placementpg` loads the facts those decisions read and applies them on the caller's transaction-bound queries; it has no Store or transaction runner. The only other caller is Session creation: `sessions` decides admission and placement with those rules over the `placementpg` participants inside the creation transaction. Deployment changes and allocation writes run through `deployment.ExecutionOperations` on `deploymentpg.NewExecution(lease, …)`, which the Worker receives as `execution.Owner.Deployment`. Each allocation write locks the owning Session, decides in `deployment`, applies in `deploymentpg` and prunes the Session's change journal in the same leased transaction; cleanup settles the Session through the `sessions` procedures on a `sessionpg.SessionTx` bound to that transaction. The Worker reads the deployment, prepares a selection's setup and records live-compute activity through the pooled `deployment.Service` in `execution.Dispatcher.Deployment`, and lists the Sessions a reset still has to archive, schedules node lifecycles and reads allocations through the pooled `deployment.Reader` in `execution.Dispatcher.DeploymentReader`; a pooled read carries no lease, and the leased write that follows it rechecks the allocation's owner. Node management and reads use the pooled `deploymentpg.Store`, which `cmd/server` also reads the owner epoch from and runs the host-history sampler on. Provider calls run outside transactions, and the final transaction rechecks the expected generation. Session archive crosses the Session and the deployment, so it is a `deployment.ExecutionOperations` operation: `ArchiveSession`, which the administrator's archive route calls through `api.Execution.SessionArchive`, and `ArchiveResetSession`, which a reset calls, lock the Session through `deploymentpg`'s `WithSessionArchive`, check the deployment's generation and the running reset in `deployment`, then expire the Environment and cancel its work through the `sessions` procedures, release its agent-host assignment while retaining its home and request allocation cleanup, and record the audit entry in one leased transaction. `deployment.ObservationResolver` resolves a Session's Runtime observation target for `runtimeobs` from `sessions.SessionReader` and `deployment.Reader`. - `coremetrics` (`coremetricspg`): the Core metrics PostgreSQL holds: the root Turn queue counts, the root Turn history, read from one read-only snapshot, and the database size. `cmd/server`'s Core metrics source adds them to the process, pool, Worker and daemon registry measurements. - `runtimehistory` (`runtimehistorypg`): Runtime history samples: the periodic export, scoped reads and retention, which also prunes node-host samples. `runtimehistory.Service` scopes a read to the Session's hosted Environment through `sessions.EnvironmentReader`. -- `sessions` (`sessionpg`): Session use cases and reads. The pooled `sessions.Service` runs the use cases on `sessionpg.Store`, which implements `sessions.Storage`, and plain reads use `sessions.Reader`, which `sessionpg.Store` also implements, directly. These cover Sessions (their creation, reads, the change journal and stream snapshot, diagnostics, the frozen execution configuration, measured usage and archive state, metadata updates, deletion and the public write audit), Turns (their reads and the execution work scan), input admission (a public input batch, Environment input reservation and the expiry of one reservation, with the reads of a Turn's admitted inputs, a reservation and the Environment input work scan), the model provider a Session froze, which `sessionpg.Store` opens with the credential key, root Items and Subagents (their reads), Session Artifacts (their reads, deletion and the staging of a Turn's export), Environments (their reads, the initialization list and the frozen setup and initial files, which `sessionpg.Store` opens with the credential key), devices (their reads, which include a Session's execution binding and the execution device list, their creation, revocation, heartbeats and Runtime enrollment, and the archived cancellation receipt the daemon gateway reads), the administrator's views across Projects (a Project's asset counts and Sessions for the summary, and the Sessions whose Runtime the administrator observes), executor credentials (authentication and a Project's credential state through `sessions.ExecutorCredentialReader`, and issuance, rotation and revocation; the Core-key Project operations record their audit entry in the same transaction) and native installation authorization and claims, whose tokens `sessionpg.Store` signs with the credential key. Session creation runs in one pooled `sessions.CreationTx`: `sessions` validates the request, computes its retry identity, with the provider key fingerprinted by `sessionpg.Store` under the credential key, and orders the upsert, hosted admission, the Skill freeze, the sealed model provider, execution configuration, initial files and setup, the Environment, placement and the initial input; `FindSessionCreation` finds an earlier creation by its recorded intent. `cmd/server` builds one `sessionpg.Store` with the credential key and the Service with its `placement.Rules`, and wires the Service and the Store into `execution.Dispatcher.Sessions` and `SessionsReader`, into the api fields, into Runtime enrollment and into the daemon gateway; the Worker creates Sessions through the Service after its execution checks, waking the scheduler only after the commit, and stages Artifacts through it; `cmd/environment-key` builds one without the key, and the Service without placement rules, for its credential commands. Turn transitions, execution completion, which alone publishes or discards a Turn's staged Artifacts, the start of Artifact capture, function calls and their application receipts, the Turn execution journal, Environment initialization, connection observations and their reconciliation, Session device binding, file-write reservation and settlement, and the promotion, failure and bulk expiry of Environment input reservations run through `sessions.ExecutionOperations` on `sessionpg.NewExecution(lease)`, which the Worker receives as `execution.Owner.Sessions`. +- `sessions` (`sessionpg`): Session use cases and reads. The pooled `sessions.Service` runs the use cases on `sessionpg.Store`, which implements `sessions.Storage`, and plain reads use `sessions.Reader`, which `sessionpg.Store` also implements, directly. These cover Sessions (their creation, reads, the change journal and stream snapshot, diagnostics, the frozen execution configuration, measured usage and archive state, metadata updates, deletion and the public write audit), Turns (their reads and the execution work scan), input admission (a public input batch, Environment input reservation and the expiry of one reservation, with the reads of a Turn's admitted inputs, a reservation and the Environment input work scan), the model provider a Session froze, which `sessionpg.Store` opens with the credential key, root Items and Subagents (their reads), Session Artifacts (their reads, deletion and the staging of a Turn's export), Environments (their reads, the initialization list and the frozen setup and initial files, which `sessionpg.Store` opens with the credential key), agent-host identities (their reads, which include a Session's execution binding and the execution device list, registration, authentication and heartbeats), sandbox enrollment, the administrator's views across Projects (a Project's asset counts and Sessions for the summary, and the Sessions whose Runtime the administrator observes), executor credentials (authentication and a Project's credential state through `sessions.ExecutorCredentialReader`, and issuance, rotation and revocation; the Core-key Project operations record their audit entry in the same transaction) and native installation authorization and claims, whose tokens `sessionpg.Store` signs with the credential key. Session creation runs in one pooled `sessions.CreationTx`: `sessions` validates the request, computes its retry identity, with the provider key fingerprinted by `sessionpg.Store` under the credential key, and orders the upsert, hosted admission, the Skill freeze, the sealed model provider, execution configuration, initial files and setup, the Environment, placement and the initial input; `FindSessionCreation` finds an earlier creation by its recorded intent. `cmd/server` builds one `sessionpg.Store` with the credential key and the Service with its `placement.Rules`, and wires the Service and the Store into `execution.Dispatcher.Sessions` and `SessionsReader`, into the api fields, into Runtime enrollment and into the daemon gateway; the Worker creates Sessions through the Service after its execution checks, waking the scheduler only after the commit, and stages Artifacts through it; `cmd/environment-key` builds one without the key, and the Service without placement rules, for its credential commands. Turn transitions, execution completion, which alone publishes or discards a Turn's staged Artifacts, the start of Artifact capture, function calls and their application receipts, the Turn execution journal, Environment initialization, connection observations and their reconciliation, Session device binding, file-write reservation and settlement, and the promotion, failure and bulk expiry of Environment input reservations run through `sessions.ExecutionOperations` on `sessionpg.NewExecution(lease)`, which the Worker receives as `execution.Owner.Sessions`. ## Request handling @@ -90,9 +90,9 @@ The Worker scans pending inputs with the same scheduling slots, Session locks, d ## Runtime connections -`services/core/internal/runtimegateway` is Core's daemon connection implementation; its persistence interfaces use `services/core/internal/runtimedevice`, and the frames and validators live in the shared `internal/agentdaemon/proto`. It is a single-process registry: connectivity comes from the live registry, never a persisted online flag, and `last_seen_at` is diagnostic only. A Session binds only to an agent host of no tenant or of its own tenant, and the binding is immutable; for a Session with an Environment, `BindSessionDevice` also requires a live `sandbox_resources` row under the Session lock. Revocation denies new connections and binding reads at once, and an open connection closes at its next heartbeat. +`services/core/internal/runtimegateway` is Core's daemon connection implementation; its persistence interfaces use `services/core/internal/runtimedevice`, and the frames and validators live in the shared `internal/agentdaemon/proto`. It is a single-process registry: connectivity comes from the live registry, never a persisted online flag, and `last_seen_at` is diagnostic only. A Session binds only to a deployment-scoped agent host, and the binding is immutable; for a Session with an Environment, `BindSessionDevice` also requires a live `sandbox_resources` row under the Session lock. Revocation denies new connections and binding reads at once, and an open connection closes at its next heartbeat. -`runtimegateway.LinkAuthority` is Core's [Link](../../docs/sandbox-link-protocol.md) `Authority`. `cmd/server` builds it over `sessionpg.Store` and gives it to one `relay.New`, which the API serves at `/api/v1/sandbox-link`; the Worker revokes through that relay as `execution.Dispatcher.Links`. Every Hello, Open and renewal rereads the database. A Serve credential authenticates only its own resource while the `sandbox_resources` view marks it live, at that resource's current generation: an allocation in `creating` or `running` by its `serve_credential_hash`, or a `sandbox_enrollments` row by its executor key while the key would still authenticate for the Environment. Rotating the key advances the generation of each of its enrollments and the epoch of their Sessions' bound assignments in the same statement, so Opens and renewals for the old generation are refused, its attachments end within one lease and the next bind supersedes the old epoch. A key without an Environment restriction may hold several enrollments, and its holder is trusted for every Environment the key authenticates for: it may Serve any of them, replacing that enrollment's serve peer. Only a device marked `agent_host` Attaches, and its `credential_revision` is the peer's `Revision`. The deployment's agent host has no tenant: `cmd/server` registers it at startup from `OAC_AGENT_HOST_IDENTITY_FILE`, which advances the revision only for a new credential and never lifts a revocation. An attach grant is the assignment ID, epoch and resource generation followed by their keyed digest under the credential key, so Core stores none. It opens a service only while its assignment is the Session's bound assignment at that epoch, held by the peer's Runtime, and its generation is current. File gets the `world` export, Network gets every destination while the Session's network access is enabled and none otherwise, and each lease lasts one minute. Cleanup of an allocation and a release first commit, which withdraws their authority, then revoke the resource at the relay, and only then destroy the compute or send the release. Every other end of Serve authority, such as a credential revocation or rotation, an Environment expiry or a Session deletion, reaches the relay through the Worker's connection pass: it keeps each live resource's last generation in memory and revokes only on a transition, the previous generation when the generation advances and the last one when the resource leaves the view, because each revocation advances the relay's epoch. The relay and that memory are process-local, so a restarted Core starts both empty. +`runtimegateway.LinkAuthority` is Core's [Link](../../docs/sandbox-link-protocol.md) `Authority`. `cmd/server` builds it over `sessionpg.Store` and gives it to one `relay.New`, which the API serves at `/api/v1/sandbox-link`; the Worker revokes through that relay as `execution.Dispatcher.Links`. Every Hello, Open and renewal rereads the database. A Serve credential authenticates only its own resource while the `sandbox_resources` view marks it live, at that resource's current generation: an allocation in `creating` or `running` by its `serve_credential_hash`, or a `sandbox_enrollments` row by its executor key while the key would still authenticate for the Environment. Rotating the key advances the generation of each of its enrollments and the epoch of their Sessions' bound assignments in the same statement, so Opens and renewals for the old generation are refused, its attachments end within one lease and the next bind supersedes the old epoch. A key without an Environment restriction may hold several enrollments, and its holder is trusted for every Environment the key authenticates for: it may Serve any of them, replacing that enrollment's serve peer. Every device is a deployment-scoped agent host and may Attach; its `credential_revision` is the peer's `Revision`. `cmd/server` registers it at startup from `OAC_AGENT_HOST_IDENTITY_FILE`, which advances the revision only for a new credential and never lifts a revocation. An attach grant is the assignment ID, epoch and resource generation followed by their keyed digest under the credential key, so Core stores none. It opens a service only while its assignment is the Session's bound assignment at that epoch, held by the peer's Runtime, and its generation is current. File gets the `world` export, Network gets every destination while the Session's network access is enabled and none otherwise, and each lease lasts one minute. Cleanup of an allocation and a release first commit, which withdraws their authority, then revoke the resource at the relay, and only then destroy the compute or send the release. Every other end of Serve authority, such as a credential revocation or rotation, an Environment expiry or a Session deletion, reaches the relay through the Worker's connection pass: it keeps each live resource's last generation in memory and revokes only on a transition, the previous generation when the generation advances and the last one when the resource leaves the view, because each revocation advances the relay's epoch. The relay and that memory are process-local, so a restarted Core starts both empty. A self-hosted machine enrolls as its Environment's Link resource: enrollment authorizes the executor key and, under the Session lock, inserts the `sandbox_enrollments` row, where the first key wins; it creates no device and binds nothing. Its connection status is that resource Serving while the enrolled key keeps its authority (`runtimeenrollment.RuntimeConnected`), the same rule as a hosted Environment's. diff --git a/services/core/README.md b/services/core/README.md index 486030ff7..f901a5461 100644 --- a/services/core/README.md +++ b/services/core/README.md @@ -7,12 +7,11 @@ | Package | Executable | Purpose | | --- | --- | --- | | `cmd/server` | `oac-core` | The HTTP service and execution Worker. It applies the embedded database migrations before serving | -| `cmd/device` | `oac-core-device` | Provisions or revokes an [operator device profile](../../contracts/agents-api/machine-api.md#operator-device-profile) for `environment: none` engine hosts | | `cmd/environment-key` | `oac-core-environment-key` | The [break-glass executor credential command](../../contracts/agents-api/environment-executor-credentials.md#break-glass-command) | | `cmd/sandbox-node` | `oac-node` | The sandbox node program; see the [nodes guide](../../docs/getting-started/nodes.md) | | `cmd/specification-contract` | None | Regenerates the deployment contract projections of the node installer and the TypeScript client | -`make build-core` builds the four executables into `~/.oac/build/oac-core`; [Standalone Core builds](../../docs/maintainers.md#standalone-core-builds) describes the build and its options. `make build-daemon` builds `oac-daemon`. +`make build-core` builds `oac-core`, `oac-core-environment-key`, `oac-node` and `oac` into `~/.oac/build/oac-core`; [Standalone Core builds](../../docs/maintainers.md#standalone-core-builds) describes the build and its options. `make build-daemon` builds `oac-daemon`. ## Database @@ -46,7 +45,7 @@ Core uses its own PostgreSQL database and account and shares no tables with an a ``` 4. Create a Project and an API key with the Core key, as in [Script the Core API](../../docs/getting-started/operations.md#script-the-core-api), and call `/v1` with the key as in the [quickstart](../../docs/getting-started/quickstart.md). -5. To run `environment: none` Sessions, provision an [operator device profile](../../contracts/agents-api/machine-api.md#operator-device-profile) for the Project's tenant and start `oac-daemon connect --profile default` on a host with a Harness installed. Self-hosted Sessions use the [self-hosted guide](../../docs/getting-started/self-hosted.md) instead. +5. Start the [agent host](../../docs/configuration.md#agent-host-container) with the same identity file and Core origin. Core assigns Sessions to this deployment-scoped host; `environment: none` needs no sandbox. Self-hosted Sessions also need the sandbox connection described in the [self-hosted guide](../../docs/getting-started/self-hosted.md). ## Tests diff --git a/services/core/cmd/device/main.go b/services/core/cmd/device/main.go deleted file mode 100644 index 692325600..000000000 --- a/services/core/cmd/device/main.go +++ /dev/null @@ -1,83 +0,0 @@ -// Command device provisions or revokes an execution device using operator DB access. -package main - -import ( - "context" - "crypto/rand" - "encoding/base64" - "encoding/json" - "errors" - "flag" - "net/url" - "os" - "strings" - "time" - - "github.com/jackc/pgx/v5/pgxpool" - - "github.com/MiniMax-AI/OpenAgentCore/internal/obs/log" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/databaseurl" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/sessionpg" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" -) - -func main() { - if err := run(); err != nil { - log.Bg().Error("execution device provisioning failed", "error", err) - os.Exit(1) - } -} - -func run() error { - tenant := flag.String("tenant", "", "execution tenant UUID") - name := flag.String("name", "", "device label") - serverURL := flag.String("url", "", "Agents API HTTP base URL") - revoke := flag.String("revoke", "", "revoke this device UUID instead of provisioning") - flag.Parse() - dsn, err := databaseurl.FromEnvironment() - if err != nil { - return err - } - if dsn == "" || *tenant == "" || flag.NArg() != 0 { - return errors.New("OAC_DATABASE_URL and --tenant are required") - } - if *revoke == "" { - u, err := url.Parse(*serverURL) - if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Hostname() == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" || (u.Path != "" && u.Path != "/") { - return errors.New("--url must be an absolute http(s) base URL without a path or credentials") - } - } - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) - defer cancel() - pool, err := pgxpool.New(ctx, dsn) - if err != nil { - return errors.New("invalid execution database configuration") - } - defer pool.Close() - // Device provisioning opens no frozen Session data, so it needs no - // credential key. - s, err := sessions.NewService(sessionpg.New(pgunit.NewPool(pool), nil), nil) - if err != nil { - return err - } - if *revoke != "" { - return s.RevokeDevice(ctx, *tenant, *revoke) - } - secret := make([]byte, 32) - if _, err := rand.Read(secret); err != nil { - return err - } - credential := base64.RawURLEncoding.EncodeToString(secret) - registered, err := s.CreateDevice(ctx, *tenant, *name, runtimedevice.HashCredential(credential)) - if err != nil { - return err - } - // Emit the existing daemon profile shape. Operators redirect this secret to a - // mode-0600 auth.json under ~/.oac; it is never included in diagnostic logs. - return json.NewEncoder(os.Stdout).Encode(map[string]string{ - "server_url": strings.TrimRight(*serverURL, "/") + "/api/v1", "runtime_id": registered.ID, - "runner_credential": credential, "device_name": registered.Name, - }) -} diff --git a/services/core/cmd/server/daemon_bootstrap_test.go b/services/core/cmd/server/daemon_bootstrap_test.go index 372df38ae..998b7e3e0 100644 --- a/services/core/cmd/server/daemon_bootstrap_test.go +++ b/services/core/cmd/server/daemon_bootstrap_test.go @@ -12,35 +12,28 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" ) -type bootstrapCredentialStore struct { - nodeID string - allocationID string -} +type bootstrapCredentialStore struct{} -func (s bootstrapCredentialStore) GetDeviceCredential(context.Context, string) (runtimedevice.Credential, bool, error) { +func (bootstrapCredentialStore) GetDeviceCredential(context.Context, string) (runtimedevice.Credential, bool, error) { return runtimedevice.Credential{ID: "runtime", Type: runtimedevice.RuntimeTypeAgentDaemon, - CredentialHash: runtimedevice.HashCredential("synthetic-token"), RuntimeNodeID: s.nodeID, RuntimeAllocationID: s.allocationID}, true, nil + CredentialHash: runtimedevice.HashCredential("synthetic-token")}, true, nil } func TestBootstrapAddressUsesPublicOrigin(t *testing.T) { const publicURL = "wss://public.example/api/v1/agent-daemon/ws" - for _, node := range []string{"local-node", "remote-node", ""} { - t.Run(node, func(t *testing.T) { - h := runtimegateway.NewHandler(runtimegateway.HandlerConfig{Registry: runtimegateway.NewRegistry(), PublicWSURL: publicURL, - Authenticator: runtimegateway.NewAuthenticator(bootstrapCredentialStore{nodeID: node})}) - request := httptest.NewRequest(http.MethodPost, "https://forged.example/api/v1/agent-daemon/bootstrap", - strings.NewReader(`{"device_id":"runtime","node_id":"local-node","runtime_node_id":"local-node"}`)) - request.Header.Set("Authorization", "Bearer synthetic-token") - request.Header.Set("X-Forwarded-Host", "forged-proxy.example") - response := httptest.NewRecorder() - h.Bootstrap(response, request) - var body map[string]any - if response.Code != http.StatusOK || json.Unmarshal(response.Body.Bytes(), &body) != nil || body["ws_url"] != publicURL { - t.Fatalf("bootstrap status=%d body=%s", response.Code, response.Body.String()) - } - if len(body) != 5 { - t.Fatal("bootstrap exposed private allocation metadata") - } - }) + h := runtimegateway.NewHandler(runtimegateway.HandlerConfig{Registry: runtimegateway.NewRegistry(), PublicWSURL: publicURL, + Authenticator: runtimegateway.NewAuthenticator(bootstrapCredentialStore{})}) + request := httptest.NewRequest(http.MethodPost, "https://forged.example/api/v1/agent-daemon/bootstrap", + strings.NewReader(`{"device_id":"runtime","node_id":"local-node","runtime_node_id":"local-node"}`)) + request.Header.Set("Authorization", "Bearer synthetic-token") + request.Header.Set("X-Forwarded-Host", "forged-proxy.example") + response := httptest.NewRecorder() + h.Bootstrap(response, request) + var body map[string]any + if response.Code != http.StatusOK || json.Unmarshal(response.Body.Bytes(), &body) != nil || body["ws_url"] != publicURL { + t.Fatalf("bootstrap status=%d body=%s", response.Code, response.Body.String()) + } + if len(body) != 5 { + t.Fatal("bootstrap exposed private allocation metadata") } } diff --git a/services/core/cmd/server/main.go b/services/core/cmd/server/main.go index aa4e5899f..062ae3564 100644 --- a/services/core/cmd/server/main.go +++ b/services/core/cmd/server/main.go @@ -211,7 +211,7 @@ func run(config processconfig.Config) error { } executorURL := config.PublicOrigin.DaemonWebSocket() links := runtimegateway.NewLinkAuthority(sessionStore) - daemonHandler, registry, err := runtime.NewGateway(sessionStore, sessionService, sessionStore, links, executorURL) + daemonHandler, registry, err := runtime.NewGateway(sessionStore, sessionService, links, executorURL) if err != nil { return err } diff --git a/services/core/internal/api/runtime_observations.go b/services/core/internal/api/runtime_observations.go index 2d8e94606..90ce99da9 100644 --- a/services/core/internal/api/runtime_observations.go +++ b/services/core/internal/api/runtime_observations.go @@ -103,18 +103,12 @@ func runtimeObservationResponse(observation runtimeobs.Observation) (v1.RuntimeO if observation.Target.Instance.AllocationID != "" { result.Instance.AllocationID = &observation.Target.Instance.AllocationID } - if observation.Target.Instance.DeviceID != "" { - result.Instance.DeviceID = &observation.Target.Instance.DeviceID - } if !observation.Target.Instance.AllocationCreatedAt.IsZero() { created := observation.Target.Instance.AllocationCreatedAt.Unix() result.AllocationCreatedAt = &created } case runtimeobs.ModeSelfHosted: result.Instance.Kind = "self_hosted_connection" - if observation.Target.Instance.DeviceID != "" { - result.Instance.DeviceID = &observation.Target.Instance.DeviceID - } if observation.Target.Instance.ConnectionGeneration != "" { result.Instance.ConnectionGeneration = &observation.Target.Instance.ConnectionGeneration } diff --git a/services/core/internal/api/runtime_observations_test.go b/services/core/internal/api/runtime_observations_test.go index 227db91ac..8d68833ee 100644 --- a/services/core/internal/api/runtime_observations_test.go +++ b/services/core/internal/api/runtime_observations_test.go @@ -109,7 +109,7 @@ func TestRuntimeObservationResponsePreservesObservedZero(t *testing.T) { now := time.Date(2026, 9, 22, 8, 0, 0, 0, time.UTC) sessionID, environmentID := uuid.NewString(), uuid.NewString() value, err := runtimeObservationResponse(runtimeobs.Observation{ - Target: runtimeobs.Target{SessionID: sessionID, EnvironmentID: environmentID, Mode: runtimeobs.ModeManaged, Instance: runtimeobs.Instance{AllocationID: uuid.NewString(), DeviceID: uuid.NewString(), AllocationState: "running", ComputePhase: "running", AllocationCreatedAt: now.Add(-time.Hour)}}, + Target: runtimeobs.Target{SessionID: sessionID, EnvironmentID: environmentID, Mode: runtimeobs.ModeManaged, Instance: runtimeobs.Instance{AllocationID: uuid.NewString(), AllocationState: "running", ComputePhase: "running", AllocationCreatedAt: now.Add(-time.Hour)}}, Status: runtimeobs.StatusObserved, ProviderType: "docker", ResolvedAt: now, Sample: &runtimeobs.Sample{ObservedAt: now, CPUUsageSecondsTotal: &zeroCPU, MemoryUsageBytes: &zeroMemory}, }) @@ -147,7 +147,7 @@ func TestRuntimeObservationResponseRejectsTimesOutsidePublicContract(t *testing. base := runtimeobs.Observation{ Target: runtimeobs.Target{ SessionID: uuid.NewString(), EnvironmentID: uuid.NewString(), Mode: runtimeobs.ModeManaged, - Instance: runtimeobs.Instance{AllocationID: uuid.NewString(), DeviceID: uuid.NewString(), AllocationCreatedAt: now.Add(-time.Hour)}, + Instance: runtimeobs.Instance{AllocationID: uuid.NewString(), AllocationCreatedAt: now.Add(-time.Hour)}, }, Status: runtimeobs.StatusObserved, ResolvedAt: now, Sample: &runtimeobs.Sample{ObservedAt: now, StartedAt: timePointer(now.Add(-time.Minute))}, diff --git a/services/core/internal/db/queries/devices.sql b/services/core/internal/db/queries/devices.sql index 203e2ac37..50f10afab 100644 --- a/services/core/internal/db/queries/devices.sql +++ b/services/core/internal/db/queries/devices.sql @@ -1,42 +1,22 @@ --- name: CreateDevice :one -INSERT INTO devices (id, tenant_id, name, credential_hash) -VALUES ($1, $2, $3, $4) RETURNING id; - -- name: GetAgentHost :one --- An agent host that may run the tenant's Sessions: the deployment's own or --- one of the tenant's. +-- A deployment agent host that may run Sessions. SELECT id, name FROM devices -WHERE id = $2 AND agent_host AND revoked_at IS NULL AND (tenant_id IS NULL OR tenant_id = $1); +WHERE id = $1 AND revoked_at IS NULL; -- name: GetDeviceCredential :one -SELECT d.id, d.name, d.credential_hash, COALESCE(a.node_id::text, '')::text AS runtime_node_id, COALESCE(a.id::text, '')::text AS runtime_allocation_id -FROM runtime_device_authority d -LEFT JOIN runtime_allocations a ON a.device_id = d.id -WHERE d.id = $1; - --- name: RevokeDevice :execrows -UPDATE devices SET revoked_at = COALESCE(revoked_at, clock_timestamp()), archive_cancel_turn_id = NULL -WHERE tenant_id = $1 AND id = $2; - --- name: SettleRevokedRuntimeReleases :exec --- No Runtime is left to act on a revoked device's releases, so revocation --- settles them. It runs after the revocation in the same transaction: the --- revocation holds the device row, which a release locks before it reads the --- device's authority, so every release is either seen here or sees the --- revocation. -UPDATE session_runtime_assignments SET applied_epoch = epoch -WHERE runtime_id = $1 AND desired_state = 'released'; +SELECT id, name, credential_hash FROM devices +WHERE id = $1 AND revoked_at IS NULL; -- name: TouchDevice :execrows UPDATE devices SET last_seen_at = clock_timestamp() -WHERE devices.id = $1 AND EXISTS (SELECT 1 FROM runtime_device_authority a WHERE a.id = devices.id); +WHERE id = $1 AND revoked_at IS NULL; -- name: BindSessionDevice :one -- Binds the Session to an agent host. A Session with an Environment binds -- only while its Environment has a live Link resource, through which the -- agent host reaches the sandbox. INSERT INTO session_runtime_assignments (session_id, runtime_id) -SELECT s.id, d.id FROM sessions s JOIN devices d ON d.agent_host AND (d.tenant_id IS NULL OR d.tenant_id = s.tenant_id) +SELECT s.id, d.id FROM sessions s CROSS JOIN devices d WHERE s.tenant_id = $1 AND s.id = $2 AND d.id = $3 AND d.revoked_at IS NULL AND (NOT EXISTS (SELECT 1 FROM environments e WHERE e.session_id = s.id) OR EXISTS ( SELECT 1 FROM environments e JOIN sandbox_resources r ON r.environment_id = e.id @@ -47,14 +27,12 @@ WHERE session_runtime_assignments.runtime_id = EXCLUDED.runtime_id AND session_r RETURNING runtime_id; -- name: GetSessionDevice :one --- The Session's bound Runtime: a device of its tenant or the deployment's --- agent host, with the Session's Environment, which the assignment binds. +-- The Session's bound agent host and Environment. SELECT d.id, d.name, e.id AS session_environment_id, b.assignment_id, b.epoch FROM session_runtime_assignments b JOIN sessions s ON s.id = b.session_id -JOIN devices d ON d.id = b.runtime_id AND (d.tenant_id = s.tenant_id OR (d.agent_host AND d.tenant_id IS NULL)) +JOIN devices d ON d.id = b.runtime_id LEFT JOIN environments e ON e.session_id = s.id -WHERE s.tenant_id = $1 AND s.id = $2 AND d.revoked_at IS NULL AND b.desired_state = 'bound' -AND EXISTS (SELECT 1 FROM runtime_device_authority a WHERE a.id = d.id); +WHERE s.tenant_id = $1 AND s.id = $2 AND d.revoked_at IS NULL AND b.desired_state = 'bound'; -- name: GetSessionExecutionBinding :one -- The bound Runtime as GetSessionDevice reads it, with the native session. @@ -62,17 +40,15 @@ SELECT d.id, d.name, b.native_session_id, e.id AS session_environment_id, b.assi EXISTS (SELECT 1 FROM turns t WHERE t.session_id = s.id AND t.started_at IS NOT NULL) AS has_started_turn FROM session_runtime_assignments b JOIN sessions s ON s.id = b.session_id -JOIN devices d ON d.id = b.runtime_id AND (d.tenant_id = s.tenant_id OR (d.agent_host AND d.tenant_id IS NULL)) +JOIN devices d ON d.id = b.runtime_id LEFT JOIN environments e ON e.session_id = s.id -WHERE s.tenant_id = $1 AND s.id = $2 AND d.revoked_at IS NULL AND b.desired_state = 'bound' -AND EXISTS (SELECT 1 FROM runtime_device_authority a WHERE a.id = d.id); +WHERE s.tenant_id = $1 AND s.id = $2 AND d.revoked_at IS NULL AND b.desired_state = 'bound'; -- name: RememberNativeSession :execrows UPDATE session_runtime_assignments SET native_session_id = $2 WHERE session_id = $1; -- name: LockAssignmentRuntime :exec --- Locks the device row of the Session's Runtime before a release reads its --- authority; see SettleRevokedRuntimeReleases. +-- Locks the agent host before a release reads its authority. SELECT 1 FROM session_runtime_assignments b JOIN devices d ON d.id = b.runtime_id WHERE b.session_id = $1 FOR SHARE OF d; @@ -82,7 +58,7 @@ WHERE b.session_id = $1 FOR SHARE OF d; -- Runtime can act on it. UPDATE session_runtime_assignments b SET desired_state = 'released', epoch = b.epoch + 1, remove_home = b.remove_home OR sqlc.arg(remove_home)::boolean, - applied_epoch = CASE WHEN EXISTS (SELECT 1 FROM runtime_device_authority a WHERE a.id = b.runtime_id) THEN b.applied_epoch ELSE b.epoch + 1 END + applied_epoch = CASE WHEN EXISTS (SELECT 1 FROM devices d WHERE d.id = b.runtime_id AND d.revoked_at IS NULL) THEN b.applied_epoch ELSE b.epoch + 1 END WHERE b.session_id = sqlc.arg(session_id) AND (b.desired_state = 'bound' OR (sqlc.arg(remove_home)::boolean AND NOT b.remove_home)); -- name: ListPendingAssignmentReleases :many diff --git a/services/core/internal/db/queries/local_environment_devices.sql b/services/core/internal/db/queries/local_environment_devices.sql deleted file mode 100644 index 75d50bfc5..000000000 --- a/services/core/internal/db/queries/local_environment_devices.sql +++ /dev/null @@ -1,8 +0,0 @@ --- name: CreateEnvironmentDevice :one -INSERT INTO devices (id, tenant_id, name, credential_hash, environment_id) -SELECT sqlc.arg(id), s.tenant_id, sqlc.arg(name), sqlc.arg(credential_hash), e.id -FROM environments e JOIN sessions s ON s.id = e.session_id -WHERE s.tenant_id = sqlc.arg(tenant_id) AND s.id = sqlc.arg(session_id) AND e.id = sqlc.arg(environment_id) -AND s.deleted_at IS NULL AND s.configuration->'environment'->>'type' = 'openai_hosted' -ON CONFLICT (environment_id) DO NOTHING -RETURNING id; diff --git a/services/core/internal/db/queries/runtime_allocations.sql b/services/core/internal/db/queries/runtime_allocations.sql index 5ceed124e..c8fe97188 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, serve_credential_hash) -VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *; +INSERT INTO runtime_allocations (id, environment_id, provider_key, node_id, deployment_generation, serve_credential_hash) +VALUES ($1, $2, $3, $4, $5, $6) RETURNING *; -- name: GetRuntimeAllocation :one SELECT sqlc.embed(a), e.session_id, s.tenant_id, s.deleted_at, (a.compute_phase NOT IN ('disabled', 'running') AND a.compute_retained_until IS NOT NULL AND a.compute_retained_until <= clock_timestamp())::boolean AS expired diff --git a/services/core/internal/db/queries/runtime_cancellation.sql b/services/core/internal/db/queries/runtime_cancellation.sql deleted file mode 100644 index 600b246b7..000000000 --- a/services/core/internal/db/queries/runtime_cancellation.sql +++ /dev/null @@ -1,32 +0,0 @@ --- name: GetArchivedCancellationReceipt :one -SELECT t.id, t.cancel_requested_at -FROM devices d -JOIN runtime_allocations a ON a.device_id = d.id -JOIN environments e ON e.id = a.environment_id AND e.id = d.environment_id -JOIN sessions s ON s.id = e.session_id AND s.tenant_id = d.tenant_id -JOIN turns t ON t.session_id = s.id AND t.id = d.archive_cancel_turn_id -WHERE d.id = sqlc.arg(device_id) - AND d.credential_hash = sqlc.arg(credential_hash) - AND d.executor_key_id IS NULL AND d.revoked_at IS NOT NULL - AND a.state = 'cleanup_pending' AND a.released_at IS NULL - AND e.status = 'expired' AND s.deleted_at IS NULL - AND s.configuration->'environment'->>'type' = 'openai_hosted' - AND t.status IN ('in_progress', 'waiting') - AND t.id::text = ANY(sqlc.arg(run_ids)::text[]) - AND t.cancel_requested_at IS NOT NULL - AND d.revoked_at >= t.cancel_requested_at - AND t.cancel_requested_at > clock_timestamp() - sqlc.arg(limit_seconds)::int * interval '1 second'; - --- name: RevokeArchivedRuntimeDevice :execrows -UPDATE devices d -SET archive_cancel_turn_id = CASE WHEN d.revoked_at IS NULL THEN ( - SELECT t.id FROM turns t - WHERE t.session_id = sqlc.arg(session_id) - AND t.status IN ('in_progress', 'waiting') AND t.cancel_requested_at IS NOT NULL -) ELSE d.archive_cancel_turn_id END, - revoked_at = COALESCE(d.revoked_at, clock_timestamp()) -WHERE d.tenant_id = sqlc.arg(tenant_id) AND d.id = sqlc.arg(device_id); - --- name: RevokeRuntimeCleanupDevice :execrows -UPDATE devices SET revoked_at = COALESCE(revoked_at, clock_timestamp()) -WHERE tenant_id = sqlc.arg(tenant_id) AND id = sqlc.arg(device_id); diff --git a/services/core/internal/db/queries/runtime_enrollment.sql b/services/core/internal/db/queries/runtime_enrollment.sql index 0f79c31e9..15c96b256 100644 --- a/services/core/internal/db/queries/runtime_enrollment.sql +++ b/services/core/internal/db/queries/runtime_enrollment.sql @@ -23,7 +23,4 @@ RETURNING id, generation; -- name: TouchAuthenticatedDevice :execrows UPDATE devices SET last_seen_at = clock_timestamp() -WHERE devices.id = sqlc.arg(id) AND EXISTS ( - SELECT 1 FROM runtime_device_authority a - WHERE a.id = devices.id AND a.credential_hash = sqlc.arg(credential_hash) -); +WHERE id = sqlc.arg(id) AND revoked_at IS NULL AND credential_hash = sqlc.arg(credential_hash); diff --git a/services/core/internal/db/queries/sandbox_link.sql b/services/core/internal/db/queries/sandbox_link.sql index 14f52a9df..a71f6742f 100644 --- a/services/core/internal/db/queries/sandbox_link.sql +++ b/services/core/internal/db/queries/sandbox_link.sql @@ -18,14 +18,14 @@ SELECT kind, id, generation, credential_hash FROM sandbox_resources WHERE tenant_id = $1 AND environment_id = $2 AND live; -- name: GetAgentHostCredential :one -SELECT COALESCE(credential_hash, '')::text AS credential_hash, credential_revision FROM devices -WHERE id = $1 AND agent_host AND revoked_at IS NULL; +SELECT credential_hash, credential_revision FROM devices +WHERE id = $1 AND revoked_at IS NULL; -- name: GetLinkAssignment :one -- The assignment with its Runtime's Attach authority, the live Link resource -- of its Session's Environment and that Environment's network access. SELECT b.session_id, b.runtime_id, b.epoch, b.desired_state = 'bound' AS bound, - (d.agent_host AND d.revoked_at IS NULL)::boolean AS agent_host, d.credential_revision, + (d.revoked_at IS NULL)::boolean AS agent_host, d.credential_revision, r.tenant_id AS resource_tenant_id, r.environment_id AS resource_environment_id, r.kind AS resource_kind, r.id AS resource_id, r.generation AS resource_generation, (COALESCE(s.configuration->'environment'->'network'->>'access', 'enabled') = 'enabled')::boolean AS network_enabled @@ -39,11 +39,9 @@ WHERE b.assignment_id = $1; -- name: RegisterAgentHost :one -- The deployment's agent host, with no tenant, Environment or executor key. -- A new credential advances the revision, which fences the Links the old one --- authenticated; a revocation stays. No row means the ID belongs to a device --- that is not an agent host. -INSERT INTO devices (id, name, credential_hash, agent_host) -VALUES ($1, 'agent-host', sqlc.arg(credential_hash), true) +-- authenticated; a revocation stays. +INSERT INTO devices (id, name, credential_hash) +VALUES ($1, 'agent-host', sqlc.arg(credential_hash)) ON CONFLICT (id) DO UPDATE SET credential_hash = EXCLUDED.credential_hash, credential_revision = devices.credential_revision + (devices.credential_hash IS DISTINCT FROM EXCLUDED.credential_hash)::int -WHERE devices.agent_host RETURNING id; diff --git a/services/core/internal/db/queries/scheduling.sql b/services/core/internal/db/queries/scheduling.sql index 2539433a0..44eaebe10 100644 --- a/services/core/internal/db/queries/scheduling.sql +++ b/services/core/internal/db/queries/scheduling.sql @@ -7,8 +7,7 @@ FROM turns t JOIN sessions s ON s.id = t.session_id WHERE t.status = ANY(sqlc.arg(statuses)::text[]) AND t.id > sqlc.arg(after_id)::uuid AND (s.deleted_at IS NULL OR t.status <> 'queued') AND (NOT sqlc.arg(connected_only)::boolean OR EXISTS ( - SELECT 1 FROM devices d WHERE d.agent_host AND (d.tenant_id IS NULL OR d.tenant_id = s.tenant_id) - AND d.revoked_at IS NULL AND d.id = ANY(sqlc.arg(connected_devices)::uuid[]) + SELECT 1 FROM devices d WHERE d.revoked_at IS NULL AND d.id = ANY(sqlc.arg(connected_devices)::uuid[]) )) ORDER BY t.id LIMIT 100; @@ -20,16 +19,15 @@ LEFT JOIN session_runtime_assignments b ON b.session_id = s.id WHERE r.state = 'pending' AND r.deadline > clock_timestamp() AND r.id > sqlc.arg(after_id)::uuid AND s.deleted_at IS NULL AND EXISTS ( - SELECT 1 FROM devices d WHERE d.agent_host AND (d.tenant_id IS NULL OR d.tenant_id = s.tenant_id) - AND d.revoked_at IS NULL AND d.id = ANY(sqlc.arg(connected_devices)::uuid[]) + SELECT 1 FROM devices d WHERE d.revoked_at IS NULL AND d.id = ANY(sqlc.arg(connected_devices)::uuid[]) AND (b.runtime_id IS NULL OR d.id = b.runtime_id) ) ORDER BY r.id LIMIT 100; -- name: ListAgentHosts :many --- The agent hosts that may run the tenant's Sessions; see GetAgentHost. +-- The deployment's available agent hosts. SELECT id, name FROM devices -WHERE agent_host AND revoked_at IS NULL AND (tenant_id IS NULL OR tenant_id = $1) +WHERE revoked_at IS NULL ORDER BY id; -- name: GetLatestSessionTurn :one diff --git a/services/core/internal/db/sqlc/devices.sql.go b/services/core/internal/db/sqlc/devices.sql.go index df0bffa03..4346fc26e 100644 --- a/services/core/internal/db/sqlc/devices.sql.go +++ b/services/core/internal/db/sqlc/devices.sql.go @@ -32,7 +32,7 @@ func (q *Queries) AcknowledgeAssignmentRelease(ctx context.Context, arg Acknowle const bindSessionDevice = `-- name: BindSessionDevice :one INSERT INTO session_runtime_assignments (session_id, runtime_id) -SELECT s.id, d.id FROM sessions s JOIN devices d ON d.agent_host AND (d.tenant_id IS NULL OR d.tenant_id = s.tenant_id) +SELECT s.id, d.id FROM sessions s CROSS JOIN devices d WHERE s.tenant_id = $1 AND s.id = $2 AND d.id = $3 AND d.revoked_at IS NULL AND (NOT EXISTS (SELECT 1 FROM environments e WHERE e.session_id = s.id) OR EXISTS ( SELECT 1 FROM environments e JOIN sandbox_resources r ON r.environment_id = e.id @@ -59,89 +59,48 @@ func (q *Queries) BindSessionDevice(ctx context.Context, arg BindSessionDevicePa return runtime_id, err } -const createDevice = `-- name: CreateDevice :one -INSERT INTO devices (id, tenant_id, name, credential_hash) -VALUES ($1, $2, $3, $4) RETURNING id -` - -type CreateDeviceParams struct { - ID pgtype.UUID `json:"id"` - TenantID pgtype.UUID `json:"tenant_id"` - Name string `json:"name"` - CredentialHash pgtype.Text `json:"credential_hash"` -} - -func (q *Queries) CreateDevice(ctx context.Context, arg CreateDeviceParams) (pgtype.UUID, error) { - row := q.db.QueryRow(ctx, createDevice, - arg.ID, - arg.TenantID, - arg.Name, - arg.CredentialHash, - ) - var id pgtype.UUID - err := row.Scan(&id) - return id, err -} - const getAgentHost = `-- name: GetAgentHost :one SELECT id, name FROM devices -WHERE id = $2 AND agent_host AND revoked_at IS NULL AND (tenant_id IS NULL OR tenant_id = $1) +WHERE id = $1 AND revoked_at IS NULL ` -type GetAgentHostParams struct { - TenantID pgtype.UUID `json:"tenant_id"` - ID pgtype.UUID `json:"id"` -} - type GetAgentHostRow struct { ID pgtype.UUID `json:"id"` Name string `json:"name"` } -// An agent host that may run the tenant's Sessions: the deployment's own or -// one of the tenant's. -func (q *Queries) GetAgentHost(ctx context.Context, arg GetAgentHostParams) (GetAgentHostRow, error) { - row := q.db.QueryRow(ctx, getAgentHost, arg.TenantID, arg.ID) +// A deployment agent host that may run Sessions. +func (q *Queries) GetAgentHost(ctx context.Context, id pgtype.UUID) (GetAgentHostRow, error) { + row := q.db.QueryRow(ctx, getAgentHost, id) var i GetAgentHostRow err := row.Scan(&i.ID, &i.Name) return i, err } const getDeviceCredential = `-- name: GetDeviceCredential :one -SELECT d.id, d.name, d.credential_hash, COALESCE(a.node_id::text, '')::text AS runtime_node_id, COALESCE(a.id::text, '')::text AS runtime_allocation_id -FROM runtime_device_authority d -LEFT JOIN runtime_allocations a ON a.device_id = d.id -WHERE d.id = $1 +SELECT id, name, credential_hash FROM devices +WHERE id = $1 AND revoked_at IS NULL ` type GetDeviceCredentialRow struct { - ID pgtype.UUID `json:"id"` - Name string `json:"name"` - CredentialHash string `json:"credential_hash"` - RuntimeNodeID string `json:"runtime_node_id"` - RuntimeAllocationID string `json:"runtime_allocation_id"` + ID pgtype.UUID `json:"id"` + Name string `json:"name"` + CredentialHash string `json:"credential_hash"` } func (q *Queries) GetDeviceCredential(ctx context.Context, id pgtype.UUID) (GetDeviceCredentialRow, error) { row := q.db.QueryRow(ctx, getDeviceCredential, id) var i GetDeviceCredentialRow - err := row.Scan( - &i.ID, - &i.Name, - &i.CredentialHash, - &i.RuntimeNodeID, - &i.RuntimeAllocationID, - ) + err := row.Scan(&i.ID, &i.Name, &i.CredentialHash) return i, err } const getSessionDevice = `-- name: GetSessionDevice :one SELECT d.id, d.name, e.id AS session_environment_id, b.assignment_id, b.epoch FROM session_runtime_assignments b JOIN sessions s ON s.id = b.session_id -JOIN devices d ON d.id = b.runtime_id AND (d.tenant_id = s.tenant_id OR (d.agent_host AND d.tenant_id IS NULL)) +JOIN devices d ON d.id = b.runtime_id LEFT JOIN environments e ON e.session_id = s.id WHERE s.tenant_id = $1 AND s.id = $2 AND d.revoked_at IS NULL AND b.desired_state = 'bound' -AND EXISTS (SELECT 1 FROM runtime_device_authority a WHERE a.id = d.id) ` type GetSessionDeviceParams struct { @@ -157,8 +116,7 @@ type GetSessionDeviceRow struct { Epoch int64 `json:"epoch"` } -// The Session's bound Runtime: a device of its tenant or the deployment's -// agent host, with the Session's Environment, which the assignment binds. +// The Session's bound agent host and Environment. func (q *Queries) GetSessionDevice(ctx context.Context, arg GetSessionDeviceParams) (GetSessionDeviceRow, error) { row := q.db.QueryRow(ctx, getSessionDevice, arg.TenantID, arg.ID) var i GetSessionDeviceRow @@ -177,10 +135,9 @@ SELECT d.id, d.name, b.native_session_id, e.id AS session_environment_id, b.assi EXISTS (SELECT 1 FROM turns t WHERE t.session_id = s.id AND t.started_at IS NOT NULL) AS has_started_turn FROM session_runtime_assignments b JOIN sessions s ON s.id = b.session_id -JOIN devices d ON d.id = b.runtime_id AND (d.tenant_id = s.tenant_id OR (d.agent_host AND d.tenant_id IS NULL)) +JOIN devices d ON d.id = b.runtime_id LEFT JOIN environments e ON e.session_id = s.id WHERE s.tenant_id = $1 AND s.id = $2 AND d.revoked_at IS NULL AND b.desired_state = 'bound' -AND EXISTS (SELECT 1 FROM runtime_device_authority a WHERE a.id = d.id) ` type GetSessionExecutionBindingParams struct { @@ -276,8 +233,7 @@ SELECT 1 FROM session_runtime_assignments b JOIN devices d ON d.id = b.runtime_i WHERE b.session_id = $1 FOR SHARE OF d ` -// Locks the device row of the Session's Runtime before a release reads its -// authority; see SettleRevokedRuntimeReleases. +// Locks the agent host before a release reads its authority. func (q *Queries) LockAssignmentRuntime(ctx context.Context, sessionID pgtype.UUID) error { _, err := q.db.Exec(ctx, lockAssignmentRuntime, sessionID) return err @@ -286,7 +242,7 @@ func (q *Queries) LockAssignmentRuntime(ctx context.Context, sessionID pgtype.UU const releaseSessionAssignment = `-- name: ReleaseSessionAssignment :exec UPDATE session_runtime_assignments b SET desired_state = 'released', epoch = b.epoch + 1, remove_home = b.remove_home OR $1::boolean, - applied_epoch = CASE WHEN EXISTS (SELECT 1 FROM runtime_device_authority a WHERE a.id = b.runtime_id) THEN b.applied_epoch ELSE b.epoch + 1 END + applied_epoch = CASE WHEN EXISTS (SELECT 1 FROM devices d WHERE d.id = b.runtime_id AND d.revoked_at IS NULL) THEN b.applied_epoch ELSE b.epoch + 1 END WHERE b.session_id = $2 AND (b.desired_state = 'bound' OR ($1::boolean AND NOT b.remove_home)) ` @@ -320,42 +276,9 @@ func (q *Queries) RememberNativeSession(ctx context.Context, arg RememberNativeS return result.RowsAffected(), nil } -const revokeDevice = `-- name: RevokeDevice :execrows -UPDATE devices SET revoked_at = COALESCE(revoked_at, clock_timestamp()), archive_cancel_turn_id = NULL -WHERE tenant_id = $1 AND id = $2 -` - -type RevokeDeviceParams struct { - TenantID pgtype.UUID `json:"tenant_id"` - ID pgtype.UUID `json:"id"` -} - -func (q *Queries) RevokeDevice(ctx context.Context, arg RevokeDeviceParams) (int64, error) { - result, err := q.db.Exec(ctx, revokeDevice, arg.TenantID, arg.ID) - if err != nil { - return 0, err - } - return result.RowsAffected(), nil -} - -const settleRevokedRuntimeReleases = `-- name: SettleRevokedRuntimeReleases :exec -UPDATE session_runtime_assignments SET applied_epoch = epoch -WHERE runtime_id = $1 AND desired_state = 'released' -` - -// No Runtime is left to act on a revoked device's releases, so revocation -// settles them. It runs after the revocation in the same transaction: the -// revocation holds the device row, which a release locks before it reads the -// device's authority, so every release is either seen here or sees the -// revocation. -func (q *Queries) SettleRevokedRuntimeReleases(ctx context.Context, runtimeID pgtype.UUID) error { - _, err := q.db.Exec(ctx, settleRevokedRuntimeReleases, runtimeID) - return err -} - const touchDevice = `-- name: TouchDevice :execrows UPDATE devices SET last_seen_at = clock_timestamp() -WHERE devices.id = $1 AND EXISTS (SELECT 1 FROM runtime_device_authority a WHERE a.id = devices.id) +WHERE id = $1 AND revoked_at IS NULL ` func (q *Queries) TouchDevice(ctx context.Context, id pgtype.UUID) (int64, error) { diff --git a/services/core/internal/db/sqlc/local_environment_devices.sql.go b/services/core/internal/db/sqlc/local_environment_devices.sql.go deleted file mode 100644 index 72c89655c..000000000 --- a/services/core/internal/db/sqlc/local_environment_devices.sql.go +++ /dev/null @@ -1,45 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.29.0 -// source: local_environment_devices.sql - -package sqlc - -import ( - "context" - - "github.com/jackc/pgx/v5/pgtype" -) - -const createEnvironmentDevice = `-- name: CreateEnvironmentDevice :one -INSERT INTO devices (id, tenant_id, name, credential_hash, environment_id) -SELECT $1, s.tenant_id, $2, $3, e.id -FROM environments e JOIN sessions s ON s.id = e.session_id -WHERE s.tenant_id = $4 AND s.id = $5 AND e.id = $6 -AND s.deleted_at IS NULL AND s.configuration->'environment'->>'type' = 'openai_hosted' -ON CONFLICT (environment_id) DO NOTHING -RETURNING id -` - -type CreateEnvironmentDeviceParams struct { - ID pgtype.UUID `json:"id"` - Name string `json:"name"` - CredentialHash pgtype.Text `json:"credential_hash"` - TenantID pgtype.UUID `json:"tenant_id"` - SessionID pgtype.UUID `json:"session_id"` - EnvironmentID pgtype.UUID `json:"environment_id"` -} - -func (q *Queries) CreateEnvironmentDevice(ctx context.Context, arg CreateEnvironmentDeviceParams) (pgtype.UUID, error) { - row := q.db.QueryRow(ctx, createEnvironmentDevice, - arg.ID, - arg.Name, - arg.CredentialHash, - arg.TenantID, - arg.SessionID, - arg.EnvironmentID, - ) - var id pgtype.UUID - err := row.Scan(&id) - return id, err -} diff --git a/services/core/internal/db/sqlc/models.go b/services/core/internal/db/sqlc/models.go index e3af2d78e..5793329c9 100644 --- a/services/core/internal/db/sqlc/models.go +++ b/services/core/internal/db/sqlc/models.go @@ -54,18 +54,13 @@ type DeploymentModelProvider struct { } type Device struct { - ID pgtype.UUID `json:"id"` - TenantID pgtype.UUID `json:"tenant_id"` - Name string `json:"name"` - CredentialHash pgtype.Text `json:"credential_hash"` - CreatedAt pgtype.Timestamptz `json:"created_at"` - LastSeenAt pgtype.Timestamptz `json:"last_seen_at"` - RevokedAt pgtype.Timestamptz `json:"revoked_at"` - EnvironmentID pgtype.UUID `json:"environment_id"` - ExecutorKeyID pgtype.UUID `json:"executor_key_id"` - ArchiveCancelTurnID pgtype.UUID `json:"archive_cancel_turn_id"` - AgentHost bool `json:"agent_host"` - CredentialRevision int64 `json:"credential_revision"` + ID pgtype.UUID `json:"id"` + Name string `json:"name"` + CredentialHash string `json:"credential_hash"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + LastSeenAt pgtype.Timestamptz `json:"last_seen_at"` + RevokedAt pgtype.Timestamptz `json:"revoked_at"` + CredentialRevision int64 `json:"credential_revision"` } type Environment struct { @@ -218,7 +213,6 @@ type PublicExecutionTurn struct { type RuntimeAllocation struct { ID pgtype.UUID `json:"id"` EnvironmentID pgtype.UUID `json:"environment_id"` - DeviceID pgtype.UUID `json:"device_id"` ProviderKey pgtype.UUID `json:"provider_key"` State string `json:"state"` CreateSettled bool `json:"create_settled"` @@ -267,14 +261,6 @@ type RuntimeDeploymentGeneration struct { ProviderMetadata []byte `json:"provider_metadata"` } -type RuntimeDeviceAuthority struct { - ID pgtype.UUID `json:"id"` - TenantID pgtype.UUID `json:"tenant_id"` - Name string `json:"name"` - EnvironmentID pgtype.UUID `json:"environment_id"` - CredentialHash string `json:"credential_hash"` -} - type RuntimeHistorySample struct { TenantID pgtype.UUID `json:"tenant_id"` SessionID pgtype.UUID `json:"session_id"` diff --git a/services/core/internal/db/sqlc/runtime_allocations.sql.go b/services/core/internal/db/sqlc/runtime_allocations.sql.go index 70103e304..f8072a567 100644 --- a/services/core/internal/db/sqlc/runtime_allocations.sql.go +++ b/services/core/internal/db/sqlc/runtime_allocations.sql.go @@ -12,14 +12,13 @@ import ( ) const createRuntimeAllocation = `-- name: CreateRuntimeAllocation :one -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, 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, provider_key, node_id, deployment_generation, serve_credential_hash) +VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, environment_id, provider_key, state, create_settled, created_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 { ID pgtype.UUID `json:"id"` EnvironmentID pgtype.UUID `json:"environment_id"` - DeviceID pgtype.UUID `json:"device_id"` ProviderKey pgtype.UUID `json:"provider_key"` NodeID pgtype.UUID `json:"node_id"` DeploymentGeneration pgtype.Int8 `json:"deployment_generation"` @@ -30,7 +29,6 @@ func (q *Queries) CreateRuntimeAllocation(ctx context.Context, arg CreateRuntime row := q.db.QueryRow(ctx, createRuntimeAllocation, arg.ID, arg.EnvironmentID, - arg.DeviceID, arg.ProviderKey, arg.NodeID, arg.DeploymentGeneration, @@ -40,7 +38,6 @@ func (q *Queries) CreateRuntimeAllocation(ctx context.Context, arg CreateRuntime err := row.Scan( &i.ID, &i.EnvironmentID, - &i.DeviceID, &i.ProviderKey, &i.State, &i.CreateSettled, @@ -63,7 +60,7 @@ func (q *Queries) CreateRuntimeAllocation(ctx context.Context, arg CreateRuntime } const getRuntimeAllocation = `-- name: GetRuntimeAllocation :one -SELECT a.id, a.environment_id, a.device_id, a.provider_key, a.state, a.create_settled, a.created_at, a.released_at, a.compute_phase, a.compute_revision, a.compute_state, a.compute_activity_at, a.compute_wake_requested, a.compute_retained_until, a.node_id, a.observation_error, a.compute_phase_changed_at, a.deployment_generation, a.serve_credential_hash, a.serve_generation, e.session_id, s.tenant_id, s.deleted_at, (a.compute_phase NOT IN ('disabled', 'running') AND a.compute_retained_until IS NOT NULL AND a.compute_retained_until <= clock_timestamp())::boolean AS expired +SELECT a.id, a.environment_id, a.provider_key, a.state, a.create_settled, a.created_at, a.released_at, a.compute_phase, a.compute_revision, a.compute_state, a.compute_activity_at, a.compute_wake_requested, a.compute_retained_until, a.node_id, a.observation_error, a.compute_phase_changed_at, a.deployment_generation, a.serve_credential_hash, a.serve_generation, e.session_id, s.tenant_id, s.deleted_at, (a.compute_phase NOT IN ('disabled', 'running') AND a.compute_retained_until IS NOT NULL AND a.compute_retained_until <= clock_timestamp())::boolean AS expired FROM runtime_allocations a JOIN environments e ON e.id = a.environment_id JOIN sessions s ON s.id = e.session_id @@ -89,7 +86,6 @@ func (q *Queries) GetRuntimeAllocation(ctx context.Context, arg GetRuntimeAlloca err := row.Scan( &i.RuntimeAllocation.ID, &i.RuntimeAllocation.EnvironmentID, - &i.RuntimeAllocation.DeviceID, &i.RuntimeAllocation.ProviderKey, &i.RuntimeAllocation.State, &i.RuntimeAllocation.CreateSettled, @@ -116,7 +112,7 @@ func (q *Queries) GetRuntimeAllocation(ctx context.Context, arg GetRuntimeAlloca } const listRuntimeAllocations = `-- name: ListRuntimeAllocations :many -SELECT a.id, a.environment_id, a.device_id, a.provider_key, a.state, a.create_settled, a.created_at, a.released_at, a.compute_phase, a.compute_revision, a.compute_state, a.compute_activity_at, a.compute_wake_requested, a.compute_retained_until, a.node_id, a.observation_error, a.compute_phase_changed_at, a.deployment_generation, a.serve_credential_hash, a.serve_generation, e.session_id, s.tenant_id, s.deleted_at, (a.compute_phase NOT IN ('disabled', 'running') AND a.compute_retained_until IS NOT NULL AND a.compute_retained_until <= clock_timestamp())::boolean AS expired +SELECT a.id, a.environment_id, a.provider_key, a.state, a.create_settled, a.created_at, a.released_at, a.compute_phase, a.compute_revision, a.compute_state, a.compute_activity_at, a.compute_wake_requested, a.compute_retained_until, a.node_id, a.observation_error, a.compute_phase_changed_at, a.deployment_generation, a.serve_credential_hash, a.serve_generation, e.session_id, s.tenant_id, s.deleted_at, (a.compute_phase NOT IN ('disabled', 'running') AND a.compute_retained_until IS NOT NULL AND a.compute_retained_until <= clock_timestamp())::boolean AS expired FROM runtime_allocations a JOIN environments e ON e.id = a.environment_id JOIN sessions s ON s.id = e.session_id @@ -144,7 +140,6 @@ func (q *Queries) ListRuntimeAllocations(ctx context.Context, id pgtype.UUID) ([ if err := rows.Scan( &i.RuntimeAllocation.ID, &i.RuntimeAllocation.EnvironmentID, - &i.RuntimeAllocation.DeviceID, &i.RuntimeAllocation.ProviderKey, &i.RuntimeAllocation.State, &i.RuntimeAllocation.CreateSettled, @@ -223,7 +218,7 @@ func (q *Queries) ListRuntimeObservationSessions(ctx context.Context, arg ListRu const observeRuntimeRunning = `-- name: ObserveRuntimeRunning :one UPDATE runtime_allocations SET state = 'running', create_settled = true WHERE id = $1 AND state IN ('creating', 'running') -RETURNING id, environment_id, device_id, provider_key, state, create_settled, created_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 +RETURNING id, environment_id, provider_key, state, create_settled, created_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 ` func (q *Queries) ObserveRuntimeRunning(ctx context.Context, id pgtype.UUID) (RuntimeAllocation, error) { @@ -232,7 +227,6 @@ func (q *Queries) ObserveRuntimeRunning(ctx context.Context, id pgtype.UUID) (Ru err := row.Scan( &i.ID, &i.EnvironmentID, - &i.DeviceID, &i.ProviderKey, &i.State, &i.CreateSettled, @@ -256,7 +250,7 @@ func (q *Queries) ObserveRuntimeRunning(ctx context.Context, id pgtype.UUID) (Ru const releaseRuntimeAllocation = `-- name: ReleaseRuntimeAllocation :one UPDATE runtime_allocations SET state = 'released', released_at = clock_timestamp() -WHERE id = $1 AND state = 'cleanup_pending' AND create_settled RETURNING id, environment_id, device_id, provider_key, state, create_settled, created_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 +WHERE id = $1 AND state = 'cleanup_pending' AND create_settled RETURNING id, environment_id, provider_key, state, create_settled, created_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 ` func (q *Queries) ReleaseRuntimeAllocation(ctx context.Context, id pgtype.UUID) (RuntimeAllocation, error) { @@ -265,7 +259,6 @@ func (q *Queries) ReleaseRuntimeAllocation(ctx context.Context, id pgtype.UUID) err := row.Scan( &i.ID, &i.EnvironmentID, - &i.DeviceID, &i.ProviderKey, &i.State, &i.CreateSettled, @@ -289,7 +282,7 @@ func (q *Queries) ReleaseRuntimeAllocation(ctx context.Context, id pgtype.UUID) const requestRuntimeCleanup = `-- name: RequestRuntimeCleanup :one UPDATE runtime_allocations SET state = 'cleanup_pending' -WHERE id = $1 AND state <> 'released' RETURNING id, environment_id, device_id, provider_key, state, create_settled, created_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 +WHERE id = $1 AND state <> 'released' RETURNING id, environment_id, provider_key, state, create_settled, created_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 ` func (q *Queries) RequestRuntimeCleanup(ctx context.Context, id pgtype.UUID) (RuntimeAllocation, error) { @@ -298,7 +291,6 @@ func (q *Queries) RequestRuntimeCleanup(ctx context.Context, id pgtype.UUID) (Ru err := row.Scan( &i.ID, &i.EnvironmentID, - &i.DeviceID, &i.ProviderKey, &i.State, &i.CreateSettled, @@ -322,7 +314,7 @@ func (q *Queries) RequestRuntimeCleanup(ctx context.Context, id pgtype.UUID) (Ru const settleRuntimeCreation = `-- name: SettleRuntimeCreation :one UPDATE runtime_allocations SET create_settled = true -WHERE id = $1 AND state <> 'released' RETURNING id, environment_id, device_id, provider_key, state, create_settled, created_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 +WHERE id = $1 AND state <> 'released' RETURNING id, environment_id, provider_key, state, create_settled, created_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 ` func (q *Queries) SettleRuntimeCreation(ctx context.Context, id pgtype.UUID) (RuntimeAllocation, error) { @@ -331,7 +323,6 @@ func (q *Queries) SettleRuntimeCreation(ctx context.Context, id pgtype.UUID) (Ru err := row.Scan( &i.ID, &i.EnvironmentID, - &i.DeviceID, &i.ProviderKey, &i.State, &i.CreateSettled, diff --git a/services/core/internal/db/sqlc/runtime_cancellation.sql.go b/services/core/internal/db/sqlc/runtime_cancellation.sql.go deleted file mode 100644 index dbe9b5ba2..000000000 --- a/services/core/internal/db/sqlc/runtime_cancellation.sql.go +++ /dev/null @@ -1,99 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.29.0 -// source: runtime_cancellation.sql - -package sqlc - -import ( - "context" - - "github.com/jackc/pgx/v5/pgtype" -) - -const getArchivedCancellationReceipt = `-- name: GetArchivedCancellationReceipt :one -SELECT t.id, t.cancel_requested_at -FROM devices d -JOIN runtime_allocations a ON a.device_id = d.id -JOIN environments e ON e.id = a.environment_id AND e.id = d.environment_id -JOIN sessions s ON s.id = e.session_id AND s.tenant_id = d.tenant_id -JOIN turns t ON t.session_id = s.id AND t.id = d.archive_cancel_turn_id -WHERE d.id = $1 - AND d.credential_hash = $2 - AND d.executor_key_id IS NULL AND d.revoked_at IS NOT NULL - AND a.state = 'cleanup_pending' AND a.released_at IS NULL - AND e.status = 'expired' AND s.deleted_at IS NULL - AND s.configuration->'environment'->>'type' = 'openai_hosted' - AND t.status IN ('in_progress', 'waiting') - AND t.id::text = ANY($3::text[]) - AND t.cancel_requested_at IS NOT NULL - AND d.revoked_at >= t.cancel_requested_at - AND t.cancel_requested_at > clock_timestamp() - $4::int * interval '1 second' -` - -type GetArchivedCancellationReceiptParams struct { - DeviceID pgtype.UUID `json:"device_id"` - CredentialHash pgtype.Text `json:"credential_hash"` - RunIds []string `json:"run_ids"` - LimitSeconds int32 `json:"limit_seconds"` -} - -type GetArchivedCancellationReceiptRow struct { - ID pgtype.UUID `json:"id"` - CancelRequestedAt pgtype.Timestamptz `json:"cancel_requested_at"` -} - -func (q *Queries) GetArchivedCancellationReceipt(ctx context.Context, arg GetArchivedCancellationReceiptParams) (GetArchivedCancellationReceiptRow, error) { - row := q.db.QueryRow(ctx, getArchivedCancellationReceipt, - arg.DeviceID, - arg.CredentialHash, - arg.RunIds, - arg.LimitSeconds, - ) - var i GetArchivedCancellationReceiptRow - err := row.Scan(&i.ID, &i.CancelRequestedAt) - return i, err -} - -const revokeArchivedRuntimeDevice = `-- name: RevokeArchivedRuntimeDevice :execrows -UPDATE devices d -SET archive_cancel_turn_id = CASE WHEN d.revoked_at IS NULL THEN ( - SELECT t.id FROM turns t - WHERE t.session_id = $1 - AND t.status IN ('in_progress', 'waiting') AND t.cancel_requested_at IS NOT NULL -) ELSE d.archive_cancel_turn_id END, - revoked_at = COALESCE(d.revoked_at, clock_timestamp()) -WHERE d.tenant_id = $2 AND d.id = $3 -` - -type RevokeArchivedRuntimeDeviceParams struct { - SessionID pgtype.UUID `json:"session_id"` - TenantID pgtype.UUID `json:"tenant_id"` - DeviceID pgtype.UUID `json:"device_id"` -} - -func (q *Queries) RevokeArchivedRuntimeDevice(ctx context.Context, arg RevokeArchivedRuntimeDeviceParams) (int64, error) { - result, err := q.db.Exec(ctx, revokeArchivedRuntimeDevice, arg.SessionID, arg.TenantID, arg.DeviceID) - if err != nil { - return 0, err - } - return result.RowsAffected(), nil -} - -const revokeRuntimeCleanupDevice = `-- name: RevokeRuntimeCleanupDevice :execrows -UPDATE devices SET revoked_at = COALESCE(revoked_at, clock_timestamp()) -WHERE tenant_id = $1 AND id = $2 -` - -type RevokeRuntimeCleanupDeviceParams struct { - TenantID pgtype.UUID `json:"tenant_id"` - DeviceID pgtype.UUID `json:"device_id"` -} - -func (q *Queries) RevokeRuntimeCleanupDevice(ctx context.Context, arg RevokeRuntimeCleanupDeviceParams) (int64, error) { - result, err := q.db.Exec(ctx, revokeRuntimeCleanupDevice, arg.TenantID, arg.DeviceID) - if err != nil { - return 0, err - } - return result.RowsAffected(), nil -} diff --git a/services/core/internal/db/sqlc/runtime_enrollment.sql.go b/services/core/internal/db/sqlc/runtime_enrollment.sql.go index 7f0ef4495..0a9ee778e 100644 --- a/services/core/internal/db/sqlc/runtime_enrollment.sql.go +++ b/services/core/internal/db/sqlc/runtime_enrollment.sql.go @@ -69,10 +69,7 @@ func (q *Queries) EnrollSandbox(ctx context.Context, arg EnrollSandboxParams) (E const touchAuthenticatedDevice = `-- name: TouchAuthenticatedDevice :execrows UPDATE devices SET last_seen_at = clock_timestamp() -WHERE devices.id = $1 AND EXISTS ( - SELECT 1 FROM runtime_device_authority a - WHERE a.id = devices.id AND a.credential_hash = $2 -) +WHERE id = $1 AND revoked_at IS NULL AND credential_hash = $2 ` type TouchAuthenticatedDeviceParams struct { diff --git a/services/core/internal/db/sqlc/runtime_lifecycle_nodes.sql.go b/services/core/internal/db/sqlc/runtime_lifecycle_nodes.sql.go index 529f4d765..d08d3d1fc 100644 --- a/services/core/internal/db/sqlc/runtime_lifecycle_nodes.sql.go +++ b/services/core/internal/db/sqlc/runtime_lifecycle_nodes.sql.go @@ -50,7 +50,7 @@ func (q *Queries) GetRuntimeLifecyclePlacement(ctx context.Context, arg GetRunti } const listRuntimeAllocationsForNode = `-- name: ListRuntimeAllocationsForNode :many -SELECT a.id, a.environment_id, a.device_id, a.provider_key, a.state, a.create_settled, a.created_at, a.released_at, a.compute_phase, a.compute_revision, a.compute_state, a.compute_activity_at, a.compute_wake_requested, a.compute_retained_until, a.node_id, a.observation_error, a.compute_phase_changed_at, a.deployment_generation, a.serve_credential_hash, a.serve_generation, e.session_id, s.tenant_id, s.deleted_at, (a.compute_phase NOT IN ('disabled', 'running') AND a.compute_retained_until IS NOT NULL AND a.compute_retained_until <= clock_timestamp())::boolean AS expired +SELECT a.id, a.environment_id, a.provider_key, a.state, a.create_settled, a.created_at, a.released_at, a.compute_phase, a.compute_revision, a.compute_state, a.compute_activity_at, a.compute_wake_requested, a.compute_retained_until, a.node_id, a.observation_error, a.compute_phase_changed_at, a.deployment_generation, a.serve_credential_hash, a.serve_generation, e.session_id, s.tenant_id, s.deleted_at, (a.compute_phase NOT IN ('disabled', 'running') AND a.compute_retained_until IS NOT NULL AND a.compute_retained_until <= clock_timestamp())::boolean AS expired FROM runtime_allocations a JOIN environments e ON e.id=a.environment_id JOIN sessions s ON s.id=e.session_id @@ -84,7 +84,6 @@ func (q *Queries) ListRuntimeAllocationsForNode(ctx context.Context, arg ListRun if err := rows.Scan( &i.RuntimeAllocation.ID, &i.RuntimeAllocation.EnvironmentID, - &i.RuntimeAllocation.DeviceID, &i.RuntimeAllocation.ProviderKey, &i.RuntimeAllocation.State, &i.RuntimeAllocation.CreateSettled, diff --git a/services/core/internal/db/sqlc/runtime_suspension.sql.go b/services/core/internal/db/sqlc/runtime_suspension.sql.go index 913891c03..ae0a96f83 100644 --- a/services/core/internal/db/sqlc/runtime_suspension.sql.go +++ b/services/core/internal/db/sqlc/runtime_suspension.sql.go @@ -113,7 +113,7 @@ SET compute_phase_changed_at = CASE WHEN compute_phase = $1::text THEN compute_p WHERE runtime_allocations.id = $4 AND compute_revision = $5 AND state = 'running' AND EXISTS (SELECT 1 FROM environments e WHERE e.id = runtime_allocations.environment_id AND e.initialization = 'complete') AND (compute_phase IN ('disabled','running') OR compute_retained_until > clock_timestamp()) -RETURNING id, environment_id, device_id, provider_key, state, create_settled, created_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 +RETURNING id, environment_id, provider_key, state, create_settled, created_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 SetRuntimeComputeParams struct { @@ -136,7 +136,6 @@ func (q *Queries) SetRuntimeCompute(ctx context.Context, arg SetRuntimeComputePa err := row.Scan( &i.ID, &i.EnvironmentID, - &i.DeviceID, &i.ProviderKey, &i.State, &i.CreateSettled, diff --git a/services/core/internal/db/sqlc/sandbox_link.sql.go b/services/core/internal/db/sqlc/sandbox_link.sql.go index a997df574..ab0ae31ad 100644 --- a/services/core/internal/db/sqlc/sandbox_link.sql.go +++ b/services/core/internal/db/sqlc/sandbox_link.sql.go @@ -12,8 +12,8 @@ import ( ) const getAgentHostCredential = `-- name: GetAgentHostCredential :one -SELECT COALESCE(credential_hash, '')::text AS credential_hash, credential_revision FROM devices -WHERE id = $1 AND agent_host AND revoked_at IS NULL +SELECT credential_hash, credential_revision FROM devices +WHERE id = $1 AND revoked_at IS NULL ` type GetAgentHostCredentialRow struct { @@ -61,7 +61,7 @@ func (q *Queries) GetEnvironmentResource(ctx context.Context, arg GetEnvironment const getLinkAssignment = `-- name: GetLinkAssignment :one SELECT b.session_id, b.runtime_id, b.epoch, b.desired_state = 'bound' AS bound, - (d.agent_host AND d.revoked_at IS NULL)::boolean AS agent_host, d.credential_revision, + (d.revoked_at IS NULL)::boolean AS agent_host, d.credential_revision, r.tenant_id AS resource_tenant_id, r.environment_id AS resource_environment_id, r.kind AS resource_kind, r.id AS resource_id, r.generation AS resource_generation, (COALESCE(s.configuration->'environment'->'network'->>'access', 'enabled') = 'enabled')::boolean AS network_enabled @@ -183,23 +183,21 @@ func (q *Queries) ListLiveSandboxResources(ctx context.Context) ([]ListLiveSandb } const registerAgentHost = `-- name: RegisterAgentHost :one -INSERT INTO devices (id, name, credential_hash, agent_host) -VALUES ($1, 'agent-host', $2, true) +INSERT INTO devices (id, name, credential_hash) +VALUES ($1, 'agent-host', $2) ON CONFLICT (id) DO UPDATE SET credential_hash = EXCLUDED.credential_hash, credential_revision = devices.credential_revision + (devices.credential_hash IS DISTINCT FROM EXCLUDED.credential_hash)::int -WHERE devices.agent_host RETURNING id ` type RegisterAgentHostParams struct { ID pgtype.UUID `json:"id"` - CredentialHash pgtype.Text `json:"credential_hash"` + CredentialHash string `json:"credential_hash"` } // The deployment's agent host, with no tenant, Environment or executor key. // A new credential advances the revision, which fences the Links the old one -// authenticated; a revocation stays. No row means the ID belongs to a device -// that is not an agent host. +// authenticated; a revocation stays. func (q *Queries) RegisterAgentHost(ctx context.Context, arg RegisterAgentHostParams) (pgtype.UUID, error) { row := q.db.QueryRow(ctx, registerAgentHost, arg.ID, arg.CredentialHash) var id pgtype.UUID diff --git a/services/core/internal/db/sqlc/scheduling.sql.go b/services/core/internal/db/sqlc/scheduling.sql.go index ad31ebbdf..ea8b5b16c 100644 --- a/services/core/internal/db/sqlc/scheduling.sql.go +++ b/services/core/internal/db/sqlc/scheduling.sql.go @@ -37,7 +37,7 @@ func (q *Queries) GetLatestSessionTurn(ctx context.Context, sessionID pgtype.UUI const listAgentHosts = `-- name: ListAgentHosts :many SELECT id, name FROM devices -WHERE agent_host AND revoked_at IS NULL AND (tenant_id IS NULL OR tenant_id = $1) +WHERE revoked_at IS NULL ORDER BY id ` @@ -46,9 +46,9 @@ type ListAgentHostsRow struct { Name string `json:"name"` } -// The agent hosts that may run the tenant's Sessions; see GetAgentHost. -func (q *Queries) ListAgentHosts(ctx context.Context, tenantID pgtype.UUID) ([]ListAgentHostsRow, error) { - rows, err := q.db.Query(ctx, listAgentHosts, tenantID) +// The deployment's available agent hosts. +func (q *Queries) ListAgentHosts(ctx context.Context) ([]ListAgentHostsRow, error) { + rows, err := q.db.Query(ctx, listAgentHosts) if err != nil { return nil, err } @@ -75,8 +75,7 @@ LEFT JOIN session_runtime_assignments b ON b.session_id = s.id WHERE r.state = 'pending' AND r.deadline > clock_timestamp() AND r.id > $1::uuid AND s.deleted_at IS NULL AND EXISTS ( - SELECT 1 FROM devices d WHERE d.agent_host AND (d.tenant_id IS NULL OR d.tenant_id = s.tenant_id) - AND d.revoked_at IS NULL AND d.id = ANY($2::uuid[]) + SELECT 1 FROM devices d WHERE d.revoked_at IS NULL AND d.id = ANY($2::uuid[]) AND (b.runtime_id IS NULL OR d.id = b.runtime_id) ) ORDER BY r.id LIMIT 100 @@ -119,8 +118,7 @@ FROM turns t JOIN sessions s ON s.id = t.session_id WHERE t.status = ANY($1::text[]) AND t.id > $2::uuid AND (s.deleted_at IS NULL OR t.status <> 'queued') AND (NOT $3::boolean OR EXISTS ( - SELECT 1 FROM devices d WHERE d.agent_host AND (d.tenant_id IS NULL OR d.tenant_id = s.tenant_id) - AND d.revoked_at IS NULL AND d.id = ANY($4::uuid[]) + SELECT 1 FROM devices d WHERE d.revoked_at IS NULL AND d.id = ANY($4::uuid[]) )) ORDER BY t.id LIMIT 100 ` diff --git a/services/core/internal/deployment/allocation.go b/services/core/internal/deployment/allocation.go index 46bdff878..6cab5cb4e 100644 --- a/services/core/internal/deployment/allocation.go +++ b/services/core/internal/deployment/allocation.go @@ -21,7 +21,6 @@ type Allocation struct { ComputeRetainedUntil *time.Time ID, EnvironmentID string SessionID, TenantID string - DeviceID string // ServeGeneration is the generation of the allocation's Link resource. ServeGeneration uint64 // ProviderKey is the installation the allocation was provisioned for. @@ -66,7 +65,7 @@ func (k AllocationKey) parse() (AllocationKey, error) { // NewAllocation is an allocation to store with its dedicated device. type NewAllocation struct { - ID, EnvironmentID, DeviceID, ProviderKey string + ID, EnvironmentID, ProviderKey string // NodeID is empty when no node serves the allocation. NodeID string Generation uint64 diff --git a/services/core/internal/deployment/allocations.go b/services/core/internal/deployment/allocations.go index b6a6e25fe..d825b257b 100644 --- a/services/core/internal/deployment/allocations.go +++ b/services/core/internal/deployment/allocations.go @@ -16,12 +16,12 @@ import ( ) // ReserveAllocation commits the allocation of the tenant's hosted Environment -// and its dedicated device together, before the provider creates compute. +// before the provider creates compute. // installation is the provider key the allocation is provisioned for, and -// 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) { +// serveCredentialHash is the SHA-256 digest of the Link Serve 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, serveCredentialHash string) (Allocation, error) { key, err := key.parse() if err != nil { return Allocation{}, err @@ -29,9 +29,8 @@ func (e *ExecutionOperations) ReserveAllocation(ctx context.Context, key Allocat if installation, err = parseID(installation); err != nil { return Allocation{}, err } - registration, err := sessions.NewDeviceRegistration("managed-runtime", credentialHash) serve, serveErr := hex.DecodeString(serveCredentialHash) - if err != nil || serveErr != nil || len(serve) != sha256.Size { + if serveErr != nil || len(serve) != sha256.Size { return Allocation{}, fmt.Errorf("%w: SHA-256 credential digest required", ErrInvalidInput) } var result Allocation @@ -70,7 +69,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, ServeCredentialHash: hex.EncodeToString(serve)} + allocation := NewAllocation{ID: uuid.NewString(), EnvironmentID: environment.ID, ProviderKey: installation, Generation: d.Generation, ServeCredentialHash: hex.EncodeToString(serve)} if d.Mode == "nodes" { reserved, err := tx.LoadReserved() if err != nil { @@ -81,10 +80,6 @@ func (e *ExecutionOperations) ReserveAllocation(ctx context.Context, key Allocat } allocation.NodeID, allocation.Generation = reserved.NodeID, reserved.Generation } - device := sessions.ExecutionDevice{ID: allocation.DeviceID, Name: registration.Name} - if err := sessions.CreateEnvironmentDevice(ctx, tx, environment.ID, device, registration.CredentialHash); err != nil { - return err - } result, err = tx.InsertAllocation(allocation) return err }) @@ -168,19 +163,19 @@ func (e *ExecutionOperations) cleanup(ctx context.Context, owner Allocation, abs result = current return nil } - if err := tx.RevokeDevice(current); err != nil { + if err := tx.ReleaseAssignment(ctx, false); err != nil { return err } - // The revocation commits with the Session's settlement, which reads + // The release commits with the Session's settlement, which reads // the allocation's expiry after it. - revoked, err := tx.LoadAllocation() + observed, err := tx.LoadAllocation() if err != nil { return err } - if revoked.SessionDeleted { + if observed.SessionDeleted { err = sessions.CancelWork(ctx, tx) } else { - err = sessions.TerminateEnvironment(ctx, tx, revoked.Expired, sessions.ProvisioningFailureReason, nil) + err = sessions.TerminateEnvironment(ctx, tx, observed.Expired, sessions.ProvisioningFailureReason, nil) } if err != nil { return err @@ -300,14 +295,14 @@ func (e *ExecutionOperations) change(ctx context.Context, owner Allocation, live } // checkAllocation returns the stored allocation when it is still the owner's: -// the same allocation, device, installation and node. A live change also +// the same allocation, installation and node. A live change also // requires the Session undeleted. func checkAllocation(tx AllocationTx, owner Allocation, live bool) (Allocation, error) { current, err := tx.LoadAllocation() if err != nil { return Allocation{}, err } - if current.ID != owner.ID || current.DeviceID != owner.DeviceID || current.ProviderKey != owner.ProviderKey || current.NodeID != owner.NodeID { + if current.ID != owner.ID || current.ProviderKey != owner.ProviderKey || current.NodeID != owner.NodeID { return Allocation{}, ErrAllocationConflict } if !live { diff --git a/services/core/internal/deployment/allocations_test.go b/services/core/internal/deployment/allocations_test.go index 11f9d454a..00303daf3 100644 --- a/services/core/internal/deployment/allocations_test.go +++ b/services/core/internal/deployment/allocations_test.go @@ -17,28 +17,12 @@ import ( ) type fakeReservationTx struct { - t testing.TB - loadBoundDevice func() (bool, error) - insertEnvironmentDevice func(string, sessions.ExecutionDevice, string) error - loadEnvironment func() (sessions.Environment, error) - findAllocation func() (Allocation, bool, error) - lockDeployment func() (placement.Deployment, error) - loadReserved func() (placement.Reserved, error) - insertAllocation func(NewAllocation) (Allocation, error) -} - -func (f *fakeReservationTx) LoadBoundDevice(context.Context) (bool, error) { - if f.loadBoundDevice == nil { - unexpected(f.t, "LoadBoundDevice") - } - return f.loadBoundDevice() -} - -func (f *fakeReservationTx) InsertEnvironmentDevice(_ context.Context, environment string, device sessions.ExecutionDevice, credentialHash string) error { - if f.insertEnvironmentDevice == nil { - unexpected(f.t, "InsertEnvironmentDevice") - } - return f.insertEnvironmentDevice(environment, device, credentialHash) + t testing.TB + loadEnvironment func() (sessions.Environment, error) + findAllocation func() (Allocation, bool, error) + lockDeployment func() (placement.Deployment, error) + loadReserved func() (placement.Reserved, error) + insertAllocation func(NewAllocation) (Allocation, error) } func (f *fakeReservationTx) LoadEnvironment(context.Context) (sessions.Environment, error) { @@ -134,7 +118,7 @@ func (f *fakeAllocationTx) RecordObservation(Allocation, string) error { // activity change. type fakeCleanupTx struct { *fakeAllocationTx - revokeDevice func(Allocation) error + releaseAssignment func(bool) error requestCleanup func(Allocation) (Allocation, error) loadEnvironment func() (sessions.Environment, error) loadEnvironmentInput func() (*sessions.EnvironmentInputState, error) @@ -144,11 +128,11 @@ type fakeCleanupTx struct { cancelPendingInput func() error } -func (f *fakeCleanupTx) RevokeDevice(current Allocation) error { - if f.revokeDevice == nil { - unexpected(f.t, "RevokeDevice") +func (f *fakeCleanupTx) ReleaseAssignment(_ context.Context, removeHome bool) error { + if f.releaseAssignment == nil { + unexpected(f.t, "ReleaseAssignment") } - return f.revokeDevice(current) + return f.releaseAssignment(removeHome) } func (f *fakeCleanupTx) RequestCleanup(current Allocation) (Allocation, error) { @@ -276,27 +260,27 @@ 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(), testCredentialHash()) + replayed, err := allocationOperations(t, tx, sessions.LockedSession{}, nil).ReserveAllocation(t.Context(), key, installation, 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(), testCredentialHash()); !errors.Is(err, ErrAllocationConflict) { + if _, err := allocationOperations(t, tx, sessions.LockedSession{}, nil).ReserveAllocation(t.Context(), key, uuid.NewString(), 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(), testCredentialHash()); !errors.Is(err, sessions.ErrNotFound) { + if _, err := allocationOperations(t, deleted, sessions.LockedSession{Deleted: true}, nil).ReserveAllocation(t.Context(), key, installation, 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(), testCredentialHash()); !errors.Is(err, ErrInvalidInput) { + if _, err := allocationOperations(t, selfHosted, sessions.LockedSession{}, nil).ReserveAllocation(t.Context(), key, installation, testCredentialHash()); !errors.Is(err, ErrInvalidInput) { t.Fatal("a self-hosted Environment reserved an allocation", err) } } // A fresh reservation passes admission, takes the node and generation its -// Session reserved and creates the dedicated device with the allocation. +// Session reserved and retains the Serve credential digest. func TestReserveAllocationAdmitsAndTakesTheReservedNode(t *testing.T) { key := AllocationKey{TenantID: uuid.NewString(), EnvironmentID: uuid.NewString()} installation, node := uuid.NewString(), uuid.NewString() @@ -316,39 +300,29 @@ func TestReserveAllocationAdmitsAndTakesTheReservedNode(t *testing.T) { d.Resetting = true return d, nil } - if _, err := allocationOperations(t, resetting, sessions.LockedSession{}, nil).ReserveAllocation(t.Context(), key, installation, testCredentialHash(), testCredentialHash()); !errors.Is(err, placement.ErrResetAdmission) { + if _, err := allocationOperations(t, resetting, sessions.LockedSession{}, nil).ReserveAllocation(t.Context(), key, installation, testCredentialHash()); !errors.Is(err, placement.ErrResetAdmission) { t.Fatal("a resetting deployment 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(), testCredentialHash()); !errors.Is(err, placement.ErrNodeUnavailable) { + if _, err := allocationOperations(t, released, sessions.LockedSession{}, nil).ReserveAllocation(t.Context(), key, installation, testCredentialHash()); !errors.Is(err, placement.ErrNodeUnavailable) { t.Fatal("a released placement reserved an allocation", err) } - var device sessions.ExecutionDevice - var deviceEnvironment string var inserted NewAllocation tx := fresh() tx.loadReserved = func() (placement.Reserved, error) { return placement.Reserved{NodeID: node, Generation: 5, Available: true}, nil } - tx.loadBoundDevice = func() (bool, error) { return false, nil } - tx.insertEnvironmentDevice = func(environment string, d sessions.ExecutionDevice, hash string) error { - if hash != testCredentialHash() { - t.Fatal("device credential", hash) - } - device, deviceEnvironment = d, environment - return nil - } tx.insertAllocation = func(a NewAllocation) (Allocation, error) { inserted = a - return Allocation{ID: a.ID, DeviceID: a.DeviceID, NodeID: a.NodeID}, nil + return Allocation{ID: a.ID, NodeID: a.NodeID}, nil } 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 || deviceEnvironment != key.EnvironmentID || inserted.ServeCredentialHash != serveHash { - t.Fatal("reservation", result, inserted, device, err) + result, err := allocationOperations(t, tx, sessions.LockedSession{}, nil).ReserveAllocation(t.Context(), key, installation, serveHash) + if err != nil || result.Replayed || inserted.NodeID != node || inserted.Generation != 5 || inserted.ProviderKey != installation || inserted.EnvironmentID != key.EnvironmentID || inserted.ServeCredentialHash != serveHash { + t.Fatal("reservation", result, inserted, err) } } @@ -356,12 +330,12 @@ func TestReserveAllocationAdmitsAndTakesTheReservedNode(t *testing.T) { // deleted Session. Any other owner, or compute that no longer runs, is a // conflict. func TestAllocationChangesCheckTheOwner(t *testing.T) { - owner := Allocation{ID: uuid.NewString(), DeviceID: uuid.NewString(), EnvironmentID: uuid.NewString(), TenantID: uuid.NewString(), ProviderKey: uuid.NewString(), State: "running"} + owner := Allocation{ID: uuid.NewString(), EnvironmentID: uuid.NewString(), TenantID: uuid.NewString(), ProviderKey: uuid.NewString(), State: "running"} stored := func(current Allocation) func() (Allocation, error) { return func() (Allocation, error) { return current, nil } } moved := owner - moved.DeviceID = uuid.NewString() + moved.ID = uuid.NewString() if _, err := allocationOperations(t, nil, sessions.LockedSession{}, &fakeAllocationTx{t: t, loadAllocation: stored(moved)}).SettleCreation(t.Context(), owner); !errors.Is(err, ErrAllocationConflict) { t.Fatal("a replaced allocation settled", err) } @@ -419,26 +393,26 @@ func TestSetComputeValidatesBeforeStorage(t *testing.T) { // reaches storage. func TestReserveAllocationValidatesTheCredentialDigests(t *testing.T) { key := AllocationKey{TenantID: uuid.NewString(), EnvironmentID: uuid.NewString()} - 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) { + for _, digest := range []string{"not-a-digest", "", testCredentialHash()[:62]} { + if _, err := allocationOperations(t, nil, sessions.LockedSession{}, nil).ReserveAllocation(t.Context(), key, uuid.NewString(), digest); !errors.Is(err, ErrInvalidInput) { t.Fatal("a malformed digest reserved an allocation", err) } } } -// Cleanup revokes the device, then cancels a deleted Session's work or +// Cleanup releases the assignment, then cancels a deleted Session's work or // terminates a live Session's Environment, then requests cleanup. An absent // creation then settles and releases the allocation, which releases its node // placement with it. -func TestCleanupRevokesSettlesTheSessionThenReleases(t *testing.T) { - owner := Allocation{ID: uuid.NewString(), DeviceID: uuid.NewString(), EnvironmentID: uuid.NewString(), TenantID: uuid.NewString(), ProviderKey: uuid.NewString(), State: "running"} +func TestCleanupReleasesAssignmentSettlesTheSessionThenReleases(t *testing.T) { + owner := Allocation{ID: uuid.NewString(), EnvironmentID: uuid.NewString(), TenantID: uuid.NewString(), ProviderKey: uuid.NewString(), State: "running"} for _, test := range []struct { name string deleted, absent bool want []string }{ - {"deleted Session with absent creation", true, true, []string{"LoadAllocation", "RevokeDevice", "LoadAllocation", "LoadActiveTurn", "CancelPendingInput", "RequestCleanup", "SettleCreation", "Release"}}, - {"live Session with expired compute", false, false, []string{"LoadAllocation", "RevokeDevice", "LoadAllocation", "LoadEnvironment", "LoadEnvironmentInput", "ExpireEnvironment", "FailPendingInput", "LoadActiveTurn", "CancelPendingInput", "LoadEnvironmentInput", "RequestCleanup"}}, + {"deleted Session with absent creation", true, true, []string{"LoadAllocation", "ReleaseAssignment", "LoadAllocation", "LoadActiveTurn", "CancelPendingInput", "RequestCleanup", "SettleCreation", "Release"}}, + {"live Session with expired compute", false, false, []string{"LoadAllocation", "ReleaseAssignment", "LoadAllocation", "LoadEnvironment", "LoadEnvironmentInput", "ExpireEnvironment", "FailPendingInput", "LoadActiveTurn", "CancelPendingInput", "LoadEnvironmentInput", "RequestCleanup"}}, } { t.Run(test.name, func(t *testing.T) { var calls []string @@ -457,7 +431,13 @@ func TestCleanupRevokesSettlesTheSessionThenReleases(t *testing.T) { settleCreation: change("SettleCreation", func(a *Allocation) { a.CreateSettled = true }), release: change("Release", func(a *Allocation) { a.State = "released" }), }, - revokeDevice: func(Allocation) error { calls = append(calls, "RevokeDevice"); return nil }, + releaseAssignment: func(removeHome bool) error { + if removeHome { + t.Fatal("cleanup removed native home") + } + calls = append(calls, "ReleaseAssignment") + return nil + }, requestCleanup: change("RequestCleanup", func(a *Allocation) { a.State = "cleanup_pending" }), loadActiveTurn: func() (sessions.Turn, bool, error) { calls = append(calls, "LoadActiveTurn") diff --git a/services/core/internal/deployment/observation.go b/services/core/internal/deployment/observation.go index b4871624e..cf26da4a2 100644 --- a/services/core/internal/deployment/observation.go +++ b/services/core/internal/deployment/observation.go @@ -85,7 +85,7 @@ func (r *ObservationResolver) Resolve(ctx context.Context, tenantID, sessionID s return runtimeobs.Target{}, errors.New("Runtime allocation does not match resolved ownership") } target.Instance = runtimeobs.Instance{ - AllocationID: allocation.ID, ProviderKey: allocation.ProviderKey, DeviceID: allocation.DeviceID, + AllocationID: allocation.ID, ProviderKey: allocation.ProviderKey, AllocationState: allocation.State, AllocationCreatedAt: allocation.CreatedAt, ComputePhase: allocation.ComputePhase, ProviderState: append(json.RawMessage(nil), allocation.ComputeState...), } diff --git a/services/core/internal/deployment/observation_test.go b/services/core/internal/deployment/observation_test.go index b84bc1c57..27be18e2c 100644 --- a/services/core/internal/deployment/observation_test.go +++ b/services/core/internal/deployment/observation_test.go @@ -56,7 +56,7 @@ func TestObservationResolverBindsManagedSessionEnvironmentAndAllocation(t *testi measured: []byte(`{"input_tokens":120,"input_tokens_details":{"cached_tokens":20},"output_tokens":30,"output_tokens_details":{"reasoning_tokens":10},"total_tokens":150}`), allocation: Allocation{ ID: "allocation", TenantID: "tenant", SessionID: "session", EnvironmentID: "environment", - ProviderKey: "provider", DeviceID: "device", ComputePhase: "running", ComputeState: []byte(`{"current":{"name":"sandbox"}}`), + ProviderKey: "provider", ComputePhase: "running", ComputeState: []byte(`{"current":{"name":"sandbox"}}`), }, }) if err != nil { @@ -66,7 +66,7 @@ func TestObservationResolverBindsManagedSessionEnvironmentAndAllocation(t *testi if err != nil { t.Fatal(err) } - if target.TenantID != "tenant" || target.SessionID != "session" || target.EnvironmentID != "environment" || target.Mode != runtimeobs.ModeManaged || target.Instance.AllocationID != "allocation" || target.Instance.ProviderKey != "provider" || target.Instance.DeviceID != "device" { + if target.TenantID != "tenant" || target.SessionID != "session" || target.EnvironmentID != "environment" || target.Mode != runtimeobs.ModeManaged || target.Instance.AllocationID != "allocation" || target.Instance.ProviderKey != "provider" { t.Fatalf("incorrect managed identity binding: %+v", target) } if string(target.Instance.ProviderState) != `{"current":{"name":"sandbox"}}` { @@ -193,7 +193,7 @@ func TestObservationResolverRejectsMismatchedEnvironmentOwnership(t *testing.T) func TestObservationResolverRejectsMismatchedAllocationOwnership(t *testing.T) { base := Allocation{ ID: "allocation", TenantID: "tenant", SessionID: "session", EnvironmentID: "environment", - ProviderKey: "provider", DeviceID: "device", + ProviderKey: "provider", } for _, mutate := range []func(*Allocation){ func(value *Allocation) { value.TenantID = "other" }, diff --git a/services/core/internal/deployment/session_archive_test.go b/services/core/internal/deployment/session_archive_test.go index 2db8c361c..49bb493a4 100644 --- a/services/core/internal/deployment/session_archive_test.go +++ b/services/core/internal/deployment/session_archive_test.go @@ -130,7 +130,7 @@ func (f *fakeArchiveTx) FindAllocation(environment string) (Allocation, bool, er } func (f *fakeArchiveTx) RequestArchiveCleanup(current Allocation) error { - f.record("RequestArchiveCleanup " + current.DeviceID + " " + current.ID) + f.record("RequestArchiveCleanup " + current.ID) return nil } @@ -227,7 +227,7 @@ func TestArchiveSession(t *testing.T) { managed := Record{InstallationID: "installation", Provider: "docker", Generation: 1} hosted := &sessions.Environment{ID: "environment", Status: "connected", Configuration: json.RawMessage(`{"type":"openai_hosted"}`)} expired := &sessions.Environment{ID: "environment", Status: "expired", Configuration: hosted.Configuration} - live := &Allocation{ID: "allocation", DeviceID: "device", ProviderKey: "installation", State: "running"} + live := &Allocation{ID: "allocation", ProviderKey: "installation", State: "running"} settle := []string{"LoadEnvironmentInput", "LoadActiveTurn", "CancelPendingInput", "LoadEnvironmentInput"} expire := append([]string{"LoadEnvironmentInput", "ExpireEnvironment environment"}, settle[1:]...) head := []string{"LoadDeployment", "LoadEnvironment", "FindAllocation environment"} @@ -247,12 +247,12 @@ func TestArchiveSession(t *testing.T) { calls []string }{ {"live allocation", sessions.LockedSession{}, hosted, live, nil, - join(head, expire, []string{"RequestArchiveCleanup device allocation", "RecordArchiveAudit", "LoadArchive"})}, + join(head, expire, []string{"RequestArchiveCleanup allocation", "RecordArchiveAudit", "LoadArchive"})}, {"no allocation", sessions.LockedSession{}, hosted, nil, nil, join(head, expire, []string{"ReleasePlacement", "RecordArchiveAudit", "LoadArchive"})}, {"released allocation", sessions.LockedSession{}, hosted, &Allocation{ID: "allocation", ProviderKey: "previous", State: "released"}, nil, join(head, expire, []string{"RecordArchiveAudit", "LoadArchive"})}, {"ended Environment", sessions.LockedSession{}, expired, live, nil, - join(head, settle, []string{"RequestArchiveCleanup device allocation", "RecordArchiveAudit", "LoadArchive"})}, + join(head, settle, []string{"RequestArchiveCleanup allocation", "RecordArchiveAudit", "LoadArchive"})}, {"allocation of another installation", sessions.LockedSession{}, hosted, &Allocation{ID: "allocation", ProviderKey: "previous", State: "running"}, ErrConflict, head}, {"deleted Session", sessions.LockedSession{Deleted: true}, hosted, nil, sessions.ErrNotFound, nil}, {"no Environment", sessions.LockedSession{}, nil, nil, sessions.ErrInvalidInput, head[:2]}, diff --git a/services/core/internal/deployment/storage.go b/services/core/internal/deployment/storage.go index e2668abd7..3c1e05ea7 100644 --- a/services/core/internal/deployment/storage.go +++ b/services/core/internal/deployment/storage.go @@ -83,7 +83,6 @@ type AllocationCleanupStorage interface { // ReservationTx is one Session-locked allocation reservation. The Session is // the owner of the Environment the transaction was opened for. type ReservationTx interface { - sessions.EnvironmentDeviceTx // LoadEnvironment reads the Session's Environment. LoadEnvironment(ctx context.Context) (sessions.Environment, error) // FindAllocation returns the Environment's allocation and whether it has @@ -127,9 +126,9 @@ type AllocationTx interface { type AllocationCleanupTx interface { AllocationTx sessions.EnvironmentTerminationTx - // RevokeDevice releases the Session's Runtime assignment without home - // removal, then revokes the allocation's device. - RevokeDevice(current Allocation) error + // ReleaseAssignment releases the Session's Runtime assignment, retaining + // its native home when removeHome is false. + ReleaseAssignment(ctx context.Context, removeHome bool) error // RequestCleanup records that the allocation's resources await cleanup. RequestCleanup(current Allocation) (Allocation, error) } diff --git a/services/core/internal/execution/archive_cancellation_cleanup_test.go b/services/core/internal/execution/archive_cancellation_cleanup_test.go deleted file mode 100644 index 97e90b8bc..000000000 --- a/services/core/internal/execution/archive_cancellation_cleanup_test.go +++ /dev/null @@ -1,209 +0,0 @@ -package execution - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "net/url" - "testing" - "time" - - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgtest" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/projectpg" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/projects" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/providercontract" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox/microsandbox" - - v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/internal/sandboxlink/relay" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/adminaudit" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/identity" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtime" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/google/uuid" - "github.com/gorilla/websocket" -) - -// Provider callbacks inspect the real database at the instant destructive -// cleanup is invoked. They do not create containers or claim native evidence. -type waitingCleanupProvider struct { - sandbox.SandboxProvider - beforeKill func() -} - -func (p waitingCleanupProvider) GetInfo(_ context.Context, r sandbox.Reference) (sandbox.Info, error) { - return sandbox.Info{Reference: r, ProviderID: r.AllocationID, State: "running", BootstrapComplete: true, CreateSettled: true}, nil -} -func (p waitingCleanupProvider) Kill(context.Context, sandbox.Reference) error { - p.beforeKill() - return nil -} - -func (waitingCleanupCheckpoint) ProviderOperations() providercontract.Operations { - return microsandbox.Operations() -} - -type waitingCleanupCheckpoint struct { - sandbox.SandboxProvider - beforeKill func() -} - -func (p waitingCleanupCheckpoint) KillCompute(context.Context, sandbox.Reference, sandbox.Compute) error { - p.beforeKill() - return nil -} - -func TestArchiveWaitingCleanupReceiptBarrier(t *testing.T) { - for _, scenario := range []struct { - name string - checkpoint, delivery bool - }{{"Kill_no_delivery", false, false}, {"KillCompute_no_delivery", true, false}, {"Kill_live_delivery", false, true}, {"KillCompute_live_delivery", true, true}} { - t.Run(scenario.name, func(t *testing.T) { - checkpoint := scenario.checkpoint - leased, deployments, reader, pool := resetManagerDB(t, nil) - installation := initializeE2BDeployment(t, leased) - projectID := uuid.NewString() - audit := adminaudit.WithSource(t.Context(), adminaudit.Source{CredentialID: "fixture-admin", ProjectID: projectID, RequestID: uuid.NewString(), TraceID: uuid.NewString()}) - management, err := projects.NewService(projectpg.New(pgunit.NewPool(pool))) - if err != nil { - t.Fatal(err) - } - project, err := management.CreateProject(audit, projects.CreateProject{ID: projectID, Name: "Cleanup diagnosis"}) - if err != nil { - t.Fatal(err) - } - _, service := testSessions(t, pool, pgtest.CredentialKey(t)) - created, err := service.CreateSession(t.Context(), project.TenantID, sessions.CreateSession{Creator: identity.Subject{Kind: "service_account", ID: "fixture"}, Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"model":"test-model"},"environment":{"type":"openai_hosted","network":{"access":"disabled"}}}`), ModelProvider: &v1.ModelProviderInput{Protocol: "responses", BaseURL: "https://model.fixture.example/v1", APIKey: "fixture-key"}, ModelProviderSource: v1.ModelProviderSourceSession}) - if err != nil { - t.Fatal(err) - } - 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), runtimedevice.HashCredential(secret)) - if err != nil { - t.Fatal(err) - } - owner, err = leased.Deployment.ObserveRunning(t.Context(), owner) - if err != nil { - t.Fatal(err) - } - inputs, err := service.SubmitInputs(t.Context(), project.TenantID, session.ID, "start", []sessions.Input{{Kind: "message", Payload: json.RawMessage(`{"input":[{"role":"user","content":[{"type":"input_text","text":"run"}]}]}`)}}) - if err != nil { - t.Fatal(err) - } - input := inputs[0] - // This fixture isolates lifecycle ordering. - for _, transition := range []sessions.TurnTransition{{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}, {ExpectedStatus: sessions.TurnInProgress, Status: sessions.TurnWaiting}} { - if _, err := leased.Sessions.TransitionTurn(t.Context(), project.TenantID, session.ID, input.TurnID, transition); err != nil { - t.Fatal(err) - } - } - currentCompute := sandbox.Compute{ID: uuid.NewString(), Name: owner.ID + "-g0"} - if checkpoint { - state, _ := json.Marshal(runtimeCompute{Current: currentCompute}) - owner, err = leased.Deployment.SetCompute(t.Context(), owner, "running", state, nil, 0) - if err != nil { - t.Fatal(err) - } - } - registry := runtimegateway.NewRegistry() - if scenario.delivery { - server := httptest.NewUnstartedServer(nil) - wsURL := "ws://" + server.Listener.Addr().String() + "/api/v1/agent-daemon/ws" - credentials, heartbeat := testSessions(t, pool, pgtest.CredentialKey(t)) - handler, liveRegistry, err := runtime.NewGateway(credentials, heartbeat, credentials, runtimegateway.NewLinkAuthority(credentials), wsURL) - if err != nil { - t.Fatal(err) - } - registry = liveRegistry - server.Config.Handler = handler - server.Start() - t.Cleanup(func() { runtime.CloseConnections(registry); server.Close() }) - u, _ := url.Parse(wsURL) - u.RawQuery = url.Values{"device_id": {owner.DeviceID}, "version": {proto.Version}}.Encode() - conn, _, err := websocket.DefaultDialer.Dial(u.String(), http.Header{"Authorization": {"Bearer " + secret}}) - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { conn.Close() }) - var peer *runtimegateway.Session - for end := time.Now().Add(3 * time.Second); ; { - peer, err = registry.LookupDevice(owner.DeviceID) - if err == nil { - break - } - if time.Now().After(end) { - t.Fatal(err) - } - time.Sleep(time.Millisecond) - } - release, err := peer.TrackExecutionDelivery(input.TurnID) - if err != nil { - t.Fatal(err) - } - defer release() - } - if _, err := leased.Deployment.ArchiveSession(audit, project.TenantID, session.ID, 1); err != nil { - t.Fatal(err) - } - owner, err = reader.EnvironmentAllocation(t.Context(), key) - if err != nil { - t.Fatal(err) - } - sessionReader, _ := testSessions(t, pool, pgtest.CredentialKey(t)) - kills := 0 - expectedStatus := sessions.TurnWaiting - provider := waitingCleanupProvider{beforeKill: func() { - kills++ - turn, err := sessionReader.GetTurn(t.Context(), project.TenantID, session.ID, input.TurnID) - if err != nil || turn.Status != expectedStatus || turn.CancelRequestedAt.IsZero() || (turn.CompletedAt.IsZero() != (expectedStatus == sessions.TurnWaiting)) { - t.Fatal("cleanup observed unexpected terminal state", turn, err) - } - allocation, err := reader.EnvironmentAllocation(t.Context(), key) - if err != nil || allocation.State != "cleanup_pending" { - t.Fatal("Kill bypassed durable cleanup ownership", allocation, err) - } - }} - lifecycle := &runtimeLifecycle{sessions: sessionReader, sessionExecution: leased.Sessions, deployment: leased.Deployment, deployments: deployments, reader: reader, lease: leased.Lease, registry: registry, links: relay.New(nil), config: RuntimeProvider{InstallationID: installation, Provider: provider}} - if checkpoint { - lifecycle.config.Provider = waitingCleanupCheckpoint{beforeKill: provider.beforeKill} - } - if err := lifecycle.observe(t.Context(), owner); err != nil { - t.Fatal(err) - } - if scenario.delivery { - if kills != 0 { - t.Fatal("destroyed compute before cancellation committed") - } - pending, err := reader.EnvironmentAllocation(t.Context(), key) - if err != nil || pending.State != "cleanup_pending" { - t.Fatal(pending, err) - } - // Controlled terminal receipt fixture; no native cancellation claim. - if _, err := leased.Sessions.TransitionTurn(t.Context(), project.TenantID, session.ID, input.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnWaiting, Status: sessions.TurnCancelled}); err != nil { - t.Fatal(err) - } - expectedStatus = sessions.TurnCancelled - if err := lifecycle.observe(t.Context(), owner); err != nil { - t.Fatal(err) - } - } - after, err := reader.EnvironmentAllocation(t.Context(), key) - if err != nil || after.State != "released" || kills != 1 { - t.Fatal("cleanup did not release", after, kills, err) - } - turn, err := sessionReader.GetTurn(t.Context(), project.TenantID, session.ID, input.TurnID) - if err != nil || turn.Status != expectedStatus || (turn.CompletedAt.IsZero() != (expectedStatus == sessions.TurnWaiting)) { - t.Fatal("cleanup should not fabricate cancellation", turn, err) - } - }) - } -} diff --git a/services/core/internal/execution/device_authority.go b/services/core/internal/execution/device_authority.go index 4ea9eaca3..081edbf1a 100644 --- a/services/core/internal/execution/device_authority.go +++ b/services/core/internal/execution/device_authority.go @@ -19,11 +19,7 @@ func authorizedRuntimePeer(ctx context.Context, devices sessions.DeviceReader, r return nil, err } if !found || !peer.AuthenticatedWith(credential.CredentialHash) { - // Reject new work even while this exact old delivery drains its receipt. - draining, drainErr := peer.DrainArchivedCancellation(ctx) - if drainErr != nil || !draining { - peer.Close("Runtime authorization changed") - } + peer.Close("Runtime authorization changed") return nil, sessions.ErrNotFound } if peer.IsClosed() { diff --git a/services/core/internal/execution/dispatcher.go b/services/core/internal/execution/dispatcher.go index 5cc9875b5..d9b6304d0 100644 --- a/services/core/internal/execution/dispatcher.go +++ b/services/core/internal/execution/dispatcher.go @@ -120,11 +120,6 @@ func (d *Dispatcher) Run(ctx context.Context, tenantID, sessionID, turnID string if _, err := d.sessionExecution.TransitionTurn(ctx, tenantID, sessionID, turnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnQueued, Status: sessions.TurnInProgress}); err != nil { return sessions.Turn{}, err } - release, err := peer.TrackExecutionDelivery(turnID) - if err != nil { - return d.finishRun(tenantID, sessionID, turnID, snapshot.Agent.Model, Result{ErrorCode: "delivery_unknown", AppliedThrough: through}, sessions.TurnFailed) - } - defer release() result, status := d.deliver(ctx, tenantID, sessionID, peer, req, turnID, text, through, prepared) return d.finishRun(tenantID, sessionID, turnID, snapshot.Agent.Model, result, status) } diff --git a/services/core/internal/execution/prepared_dispatch.go b/services/core/internal/execution/prepared_dispatch.go index 6faa49132..6c7a19683 100644 --- a/services/core/internal/execution/prepared_dispatch.go +++ b/services/core/internal/execution/prepared_dispatch.go @@ -108,12 +108,6 @@ func (d *Dispatcher) RunEnvironmentInput(ctx context.Context, lease Ownership, t } turnID := run.Reservation.Receipts[0].TurnID through := run.Reservation.Receipts[len(run.Reservation.Receipts)-1].Sequence - releaseDelivery, err := peer.TrackExecutionDelivery(turnID) - if err != nil { - run.Turn, err = d.finishRun(tenantID, sessionID, turnID, snapshot.Agent.Model, Result{ErrorCode: "delivery_unknown", AppliedThrough: through}, sessions.TurnFailed) - return run, err - } - defer releaseDelivery() result, status := d.deliver(owner, tenantID, sessionID, peer, req, turnID, messages, through, prepared) result, status = d.captureCompletedArtifacts(owner, peer, session, environment, bound.Device, turnID, result, status) run.Turn, err = d.finishRun(tenantID, sessionID, turnID, snapshot.Agent.Model, result, status) diff --git a/services/core/internal/execution/runtime_lifecycle.go b/services/core/internal/execution/runtime_lifecycle.go index 0d9cb602c..d46f0389f 100644 --- a/services/core/internal/execution/runtime_lifecycle.go +++ b/services/core/internal/execution/runtime_lifecycle.go @@ -172,13 +172,13 @@ func (r *runtimeLifecycle) provision(ctx context.Context, tenant, environment, p return deployment.Allocation{}, sandbox.ErrInvalid } key := deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment} - secret := make([]byte, 64) + secret := make([]byte, 32) if _, err := rand.Read(secret); err != nil { return deployment.Allocation{}, err } - // 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)) + // Only the Serve credential digest is stored. + serve := hex.EncodeToString(secret) + owner, err := r.deployment.ReserveAllocation(ctx, key, providerKey, runtimedevice.HashCredential(serve)) if err != nil { return owner, err } @@ -292,15 +292,6 @@ func (r *runtimeLifecycle) observe(ctx context.Context, owner deployment.Allocat if err != nil { return err } - if peer, err := r.registry.LookupDevice(owner.DeviceID); err == nil { - draining, err := peer.DrainArchivedCancellation(ctx) - if err != nil { - return err - } - if draining { - return nil - } - } } if owner.ComputePhase != "disabled" { return r.observeCompute(ctx, owner) diff --git a/services/core/internal/execution/worker_device.go b/services/core/internal/execution/worker_device.go index 9b6ec58e1..e068a83fc 100644 --- a/services/core/internal/execution/worker_device.go +++ b/services/core/internal/execution/worker_device.go @@ -119,7 +119,7 @@ func (w *Worker) place(ctx context.Context, tenant, session, environment string, if !errors.Is(err, sessions.ErrNotFound) { return false, err } - hosts, err := w.dispatcher.SessionsReader.ListAgentHosts(ctx, tenant) + hosts, err := w.dispatcher.SessionsReader.ListAgentHosts(ctx) if err != nil { return false, err } diff --git a/services/core/internal/persistence/postgres/coremetricspg/store_test.go b/services/core/internal/persistence/postgres/coremetricspg/store_test.go index 4485d4386..3e02ff459 100644 --- a/services/core/internal/persistence/postgres/coremetricspg/store_test.go +++ b/services/core/internal/persistence/postgres/coremetricspg/store_test.go @@ -54,7 +54,7 @@ func TestCoreMetricsSnapshot(t *testing.T) { t.Fatal("empty queue must not invent an age") } connected := uuid.NewString() - if _, err := pool.Exec(t.Context(), `INSERT INTO devices(id,tenant_id,name,credential_hash) VALUES($1,$2,'metrics',$3)`, connected, uuid.NewString(), strings.Repeat("a", 64)); err != nil { + if _, err := pool.Exec(t.Context(), `INSERT INTO devices(id,name,credential_hash) VALUES($1,'metrics',$2)`, connected, strings.Repeat("a", 64)); err != nil { t.Fatal(err) } t.Cleanup(func() { _, _ = pool.Exec(context.Background(), `DELETE FROM devices WHERE id=$1`, connected) }) diff --git a/services/core/internal/persistence/postgres/deploymentpg/allocations.go b/services/core/internal/persistence/postgres/deploymentpg/allocations.go index f8890d02e..e8b14f73c 100644 --- a/services/core/internal/persistence/postgres/deploymentpg/allocations.go +++ b/services/core/internal/persistence/postgres/deploymentpg/allocations.go @@ -24,7 +24,7 @@ func allocation(row sqlc.RuntimeAllocation, session, tenant pgtype.UUID, deleted ComputePhase: row.ComputePhase, ComputeRevision: row.ComputeRevision, ComputeState: row.ComputeState, ComputeActivityAt: row.ComputeActivityAt.Time, ComputeWakeRequested: row.ComputeWakeRequested, ComputeRetainedUntil: timestamp(row.ComputeRetainedUntil), ID: uuidString(row.ID), EnvironmentID: uuidString(row.EnvironmentID), SessionID: uuidString(session), TenantID: uuidString(tenant), - DeviceID: uuidString(row.DeviceID), ProviderKey: uuidString(row.ProviderKey), ServeGeneration: uint64(row.ServeGeneration), State: row.State, CreateSettled: row.CreateSettled, + ProviderKey: uuidString(row.ProviderKey), ServeGeneration: uint64(row.ServeGeneration), State: row.State, CreateSettled: row.CreateSettled, SessionDeleted: deleted.Valid, Expired: expired, CreatedAt: row.CreatedAt.Time, } } @@ -202,10 +202,6 @@ func (t *reservationTx) InsertAllocation(a deployment.NewAllocation) (deployment if err != nil { return deployment.Allocation{}, err } - device, err := parseID(a.DeviceID) - if err != nil { - return deployment.Allocation{}, err - } provider, err := parseID(a.ProviderKey) if err != nil { return deployment.Allocation{}, err @@ -215,7 +211,7 @@ func (t *reservationTx) InsertAllocation(a deployment.NewAllocation) (deployment return deployment.Allocation{}, err } row, err := t.q.CreateRuntimeAllocation(t.ctx, sqlc.CreateRuntimeAllocationParams{ - ID: id, EnvironmentID: t.environment, DeviceID: device, ProviderKey: provider, NodeID: node, + ID: id, EnvironmentID: t.environment, ProviderKey: provider, NodeID: node, DeploymentGeneration: pgtype.Int8{Int64: int64(a.Generation), Valid: true}, ServeCredentialHash: pgtype.Text{String: a.ServeCredentialHash, Valid: true}, }) @@ -321,18 +317,6 @@ type cleanupTx struct { *sessionpg.SessionTx } -func (t *cleanupTx) RevokeDevice(current deployment.Allocation) error { - device, err := parseID(current.DeviceID) - if err != nil { - return err - } - // The release follows the revocation, which leaves no Runtime to deliver it to. - if _, err := t.q.RevokeRuntimeCleanupDevice(t.ctx, sqlc.RevokeRuntimeCleanupDeviceParams{TenantID: t.tenant, DeviceID: device}); err != nil { - return err - } - return t.ReleaseAssignment(t.ctx, false) -} - func (t *cleanupTx) RequestCleanup(current deployment.Allocation) (deployment.Allocation, error) { return t.change(current, t.q.RequestRuntimeCleanup) } diff --git a/services/core/internal/persistence/postgres/deploymentpg/allocations_test.go b/services/core/internal/persistence/postgres/deploymentpg/allocations_test.go index 1496d60a8..c61d3331a 100644 --- a/services/core/internal/persistence/postgres/deploymentpg/allocations_test.go +++ b/services/core/internal/persistence/postgres/deploymentpg/allocations_test.go @@ -14,6 +14,8 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/sessionpg" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) // hostedEnvironment stores a fresh tenant's hosted Session with its pending @@ -31,23 +33,23 @@ func hostedEnvironment(t *testing.T, pool *pgxpool.Pool) deployment.AllocationKe return deployment.AllocationKey{TenantID: tenant.String(), EnvironmentID: environment.String()} } -// credentialHash is the digest of a fresh device credential. +// credentialHash is the digest of a fresh Serve credential. func credentialHash() string { digest := sha256.Sum256([]byte(uuid.NewString())) return hex.EncodeToString(digest[:]) } -// allocationRows reports the Environment's allocation state, device -// revocation and status, and the Session's journal length. -func allocationRows(t *testing.T, f fixture, key deployment.AllocationKey) (allocations int, state string, revoked bool, status string, changes int) { +// allocationRows reports the allocation, assignment release, Environment +// status and Session journal length. +func allocationRows(t *testing.T, f fixture, key deployment.AllocationKey) (allocations int, state string, released bool, status string, changes int) { t.Helper() err := f.pool.QueryRow(t.Context(), `SELECT (SELECT count(*) FROM runtime_allocations WHERE environment_id = e.id), COALESCE((SELECT state FROM runtime_allocations WHERE environment_id = e.id), ''), - COALESCE((SELECT revoked_at IS NOT NULL FROM devices WHERE environment_id = e.id), false), + COALESCE((SELECT desired_state = 'released' FROM session_runtime_assignments WHERE session_id = e.session_id), false), e.status, (SELECT count(*) FROM session_events WHERE session_id = e.session_id) - FROM environments e WHERE e.id = $1`, key.EnvironmentID).Scan(&allocations, &state, &revoked, &status, &changes) + FROM environments e WHERE e.id = $1`, key.EnvironmentID).Scan(&allocations, &state, &released, &status, &changes) if err != nil { t.Fatal(err) } @@ -55,7 +57,7 @@ func allocationRows(t *testing.T, f fixture, key deployment.AllocationKey) (allo } // Concurrent reservations of one Environment serialize on its Session: one -// commits the allocation and its device, the others replay it, and another +// commits the allocation, the others replay it, and another // installation conflicts. func TestConcurrentReservationsCommitOneAllocation(t *testing.T) { f := newFixture(t) @@ -67,13 +69,13 @@ 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(), credentialHash()) + results[i], errs[i] = changes.ReserveAllocation(t.Context(), key, installation, credentialHash()) }) } wg.Wait() fresh := 0 for i, result := range results { - if errs[i] != nil || result.ID != results[0].ID || result.DeviceID != results[0].DeviceID { + if errs[i] != nil || result.ID != results[0].ID { t.Fatal("reservations disagree", result, errs[i]) } if !result.Replayed { @@ -81,13 +83,13 @@ func TestConcurrentReservationsCommitOneAllocation(t *testing.T) { } } var devices int - if err := f.pool.QueryRow(t.Context(), "SELECT count(*) FROM devices WHERE environment_id = $1", key.EnvironmentID).Scan(&devices); err != nil { + if err := f.pool.QueryRow(t.Context(), "SELECT count(*) FROM devices").Scan(&devices); err != nil { t.Fatal(err) } - if allocations, state, _, _, _ := allocationRows(t, f, key); fresh != 1 || allocations != 1 || devices != 1 || state != "creating" { + if allocations, state, _, _, _ := allocationRows(t, f, key); fresh != 1 || allocations != 1 || devices != 0 || state != "creating" { t.Fatal("reservations committed more than one allocation", fresh, allocations, devices, state) } - if _, err := changes.ReserveAllocation(t.Context(), key, uuid.NewString(), credentialHash(), credentialHash()); !errors.Is(err, deployment.ErrAllocationConflict) { + if _, err := changes.ReserveAllocation(t.Context(), key, uuid.NewString(), credentialHash()); !errors.Is(err, deployment.ErrAllocationConflict) { t.Fatal("another installation replayed the allocation", err) } } @@ -100,12 +102,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(), credentialHash()) + owner, err := changes.ReserveAllocation(t.Context(), key, installation, credentialHash()) if err != nil { t.Fatal(err) } unallocated := hostedEnvironment(t, f.pool) - if _, err := closed.ReserveAllocation(t.Context(), unallocated, installation, credentialHash(), credentialHash()); !errors.Is(err, pgunit.ErrLeaseClosed) { + if _, err := closed.ReserveAllocation(t.Context(), unallocated, installation, credentialHash()); !errors.Is(err, pgunit.ErrLeaseClosed) { t.Fatal("reserved without the lease", err) } if allocations, _, _, _, _ := allocationRows(t, f, unallocated); allocations != 0 { @@ -131,20 +133,31 @@ func TestAllocationWritesNeedTheLease(t *testing.T) { } } -// Cleanup revokes the device, settles the Session and requests cleanup in -// one transaction: a failed settlement rolls the revocation back. -func TestFailedCleanupSettlementRollsBackRevocation(t *testing.T) { +// Cleanup releases the assignment, settles the Session and requests cleanup +// in one transaction: a failed settlement rolls the release back. +func TestFailedCleanupSettlementRollsBackAssignmentRelease(t *testing.T) { f := newFixture(t) - changes, _ := f.execution(t) + changes, lease := f.execution(t) installation, _ := f.initialize(t, changes, setupE2BSelection()) key := hostedEnvironment(t, f.pool) - owner, err := changes.ReserveAllocation(t.Context(), key, installation, credentialHash(), credentialHash()) + owner, err := changes.ReserveAllocation(t.Context(), key, installation, credentialHash()) if err != nil { t.Fatal(err) } if owner, err = changes.ObserveRunning(t.Context(), owner); err != nil { t.Fatal(err) } + host := uuid.NewString() + if _, err := f.pool.Exec(t.Context(), `INSERT INTO devices(id, name, credential_hash) VALUES ($1, 'host', $2)`, host, credentialHash()); err != nil { + t.Fatal(err) + } + execution, err := sessions.NewExecutionOperations(sessionpg.NewExecution(lease)) + if err != nil { + t.Fatal(err) + } + if err := execution.BindSessionDevice(t.Context(), owner.TenantID, owner.SessionID, host); err != nil { + t.Fatal(err) + } _, _, _, _, before := allocationRows(t, f, key) if _, err := f.pool.Exec(t.Context(), `CREATE FUNCTION fail_environment_update() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'injected settlement failure'; END $$; CREATE TRIGGER fail_environment_update BEFORE UPDATE ON environments FOR EACH ROW EXECUTE FUNCTION fail_environment_update()`); err != nil { @@ -153,8 +166,8 @@ func TestFailedCleanupSettlementRollsBackRevocation(t *testing.T) { if _, err := changes.RequestCleanup(t.Context(), owner); err == nil { t.Fatal("cleanup committed without the Session settlement") } - if _, state, revoked, status, changed := allocationRows(t, f, key); state != "running" || revoked || status != "pending" || changed != before { - t.Fatal("failed cleanup left a partial commit", state, revoked, status, changed) + if _, state, released, status, changed := allocationRows(t, f, key); state != "running" || released || status != "pending" || changed != before { + t.Fatal("failed cleanup left a partial commit", state, released, status, changed) } if _, err := f.pool.Exec(t.Context(), "DROP TRIGGER fail_environment_update ON environments"); err != nil { t.Fatal(err) @@ -163,19 +176,24 @@ func TestFailedCleanupSettlementRollsBackRevocation(t *testing.T) { if err != nil || pending.State != "cleanup_pending" { t.Fatal(pending, err) } - if _, state, revoked, status, changed := allocationRows(t, f, key); state != "cleanup_pending" || !revoked || status != "failed" || changed <= before { - t.Fatal("cleanup did not commit together", state, revoked, status, changed) + if _, state, released, status, changed := allocationRows(t, f, key); state != "cleanup_pending" || !released || status != "failed" || changed <= before { + t.Fatal("cleanup did not commit together", state, released, status, changed) + } + var removeHome, revoked bool + var epoch int64 + if err := f.pool.QueryRow(t.Context(), `SELECT a.remove_home, a.epoch, d.revoked_at IS NOT NULL FROM session_runtime_assignments a JOIN devices d ON d.id = a.runtime_id WHERE a.session_id = $1`, owner.SessionID).Scan(&removeHome, &epoch, &revoked); err != nil || removeHome || revoked || epoch != 2 { + t.Fatal("cleanup changed host authority or home removal", removeHome, revoked, epoch, err) } } // An allocation write commits only with the Session journal prune: a failed -// prune rolls back a change and a reservation with its device. +// prune rolls back a change and a reservation. func TestFailedPruneRollsBackTheAllocationWrite(t *testing.T) { f := newFixture(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(), credentialHash()) + owner, err := changes.ReserveAllocation(t.Context(), key, installation, credentialHash()) if err != nil { t.Fatal(err) } @@ -190,11 +208,11 @@ 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(), credentialHash()); err == nil || !strings.Contains(err.Error(), "injected prune failure") { + if _, err := changes.ReserveAllocation(t.Context(), unallocated, installation, credentialHash()); err == nil || !strings.Contains(err.Error(), "injected prune failure") { t.Fatal("a reservation committed without the prune", err) } var devices int - if err := f.pool.QueryRow(t.Context(), "SELECT count(*) FROM devices WHERE environment_id = $1", unallocated.EnvironmentID).Scan(&devices); err != nil { + if err := f.pool.QueryRow(t.Context(), "SELECT count(*) FROM devices").Scan(&devices); err != nil { t.Fatal(err) } if allocations, _, _, _, _ := allocationRows(t, f, unallocated); allocations != 0 || devices != 0 { diff --git a/services/core/internal/persistence/postgres/deploymentpg/session_archive.go b/services/core/internal/persistence/postgres/deploymentpg/session_archive.go index b4f3693e2..4c27ec925 100644 --- a/services/core/internal/persistence/postgres/deploymentpg/session_archive.go +++ b/services/core/internal/persistence/postgres/deploymentpg/session_archive.go @@ -61,18 +61,10 @@ func (t *archiveTx) FindAllocation(environment string) (deployment.Allocation, b } func (t *archiveTx) RequestArchiveCleanup(current deployment.Allocation) error { - device, err := parseID(current.DeviceID) - if err != nil { - return err - } id, err := parseID(current.ID) if err != nil { return err } - // The release follows the revocation, which leaves no Runtime to deliver it to. - if _, err := t.q.RevokeArchivedRuntimeDevice(t.ctx, sqlc.RevokeArchivedRuntimeDeviceParams{TenantID: t.tenant, DeviceID: device, SessionID: t.session}); err != nil { - return err - } if err := t.ReleaseAssignment(t.ctx, false); err != nil { return err } diff --git a/services/core/internal/persistence/postgres/sessionpg/binding.go b/services/core/internal/persistence/postgres/sessionpg/binding.go index d73d38122..467990538 100644 --- a/services/core/internal/persistence/postgres/sessionpg/binding.go +++ b/services/core/internal/persistence/postgres/sessionpg/binding.go @@ -33,7 +33,6 @@ var ( _ sessions.EnvironmentTerminationTx = (*SessionTx)(nil) _ sessions.InputStartTx = (*SessionTx)(nil) _ sessions.ComputeAdmissionTx = (*SessionTx)(nil) - _ sessions.EnvironmentDeviceTx = (*SessionTx)(nil) _ sessions.InputProjectionTx = (*SessionTx)(nil) _ sessions.TurnTx = (*SessionTx)(nil) ) @@ -154,37 +153,6 @@ func (t *SessionTx) ExpireEnvironment(ctx context.Context, environment string) e return err } -func (t *SessionTx) LoadBoundDevice(ctx context.Context) (bool, error) { - _, err := t.q.GetSessionDevice(ctx, sqlc.GetSessionDeviceParams{TenantID: t.tenant, ID: t.session}) - if errors.Is(err, pgx.ErrNoRows) { - return false, nil - } - return err == nil, err -} - -// InsertEnvironmentDevice inserts the device only for the Session's own hosted -// Environment. The insert takes no row when the Environment already has a -// device, including one a concurrent transaction inserted first, or cannot -// take one: that is sessions.ErrDeviceBindingConflict. -func (t *SessionTx) InsertEnvironmentDevice(ctx context.Context, environment string, device sessions.ExecutionDevice, credentialHash string) error { - id, err := parseID(device.ID) - if err != nil { - return err - } - environmentID, err := parseID(environment) - if err != nil { - return err - } - _, err = t.q.CreateEnvironmentDevice(ctx, sqlc.CreateEnvironmentDeviceParams{ - ID: id, TenantID: t.tenant, SessionID: t.session, EnvironmentID: environmentID, - Name: device.Name, CredentialHash: pgtype.Text{String: credentialHash, Valid: true}, - }) - if errors.Is(err, pgx.ErrNoRows) { - return sessions.ErrDeviceBindingConflict - } - return err -} - func (t *SessionTx) LoadComputeSuspension(ctx context.Context) (bool, error) { return placementpg.ComputeBlocksAdmission(ctx, t.q, t.session) } diff --git a/services/core/internal/persistence/postgres/sessionpg/devices.go b/services/core/internal/persistence/postgres/sessionpg/devices.go index 5f3d4c04a..dc35b1938 100644 --- a/services/core/internal/persistence/postgres/sessionpg/devices.go +++ b/services/core/internal/persistence/postgres/sessionpg/devices.go @@ -3,7 +3,6 @@ package sessionpg import ( "context" "errors" - "fmt" "github.com/google/uuid" "github.com/jackc/pgx/v5" @@ -63,12 +62,8 @@ func requireInitialized(ctx context.Context, q *sqlc.Queries, lookup Lookup) err return err } -func (s *Store) ListAgentHosts(ctx context.Context, tenant string) ([]sessions.ExecutionDevice, error) { - id, err := parseID(tenant) - if err != nil { - return nil, err - } - rows, err := s.units.Queries().ListAgentHosts(ctx, id) +func (s *Store) ListAgentHosts(ctx context.Context) ([]sessions.ExecutionDevice, error) { + rows, err := s.units.Queries().ListAgentHosts(ctx) if err != nil { return nil, err } @@ -131,66 +126,10 @@ func (s *Store) GetDeviceCredential(ctx context.Context, device string) (runtime } return runtimedevice.Credential{ ID: uuid.UUID(row.ID.Bytes).String(), Name: row.Name, Type: runtimedevice.RuntimeTypeAgentDaemon, - CredentialHash: row.CredentialHash, RuntimeNodeID: row.RuntimeNodeID, RuntimeAllocationID: row.RuntimeAllocationID, + CredentialHash: row.CredentialHash, }, true, nil } -// ArchivedCancellationReceipt is a read-only exception for the exact already -// authenticated delivery. The ordinary credential view remains revoked; this -// cannot authorize bootstrap, reconnect, dispatch, workspace access or renewal. -// A marker records that archive caused the first revocation; timestamps alone -// cannot distinguish an earlier ordinary cancel/revoke followed by archive. -func (s *Store) ArchivedCancellationReceipt(ctx context.Context, device, credentialHash string, runIDs []string) (runtimedevice.ArchivedCancellationReceipt, error) { - if len(runIDs) == 0 || credentialHash == "" { - return runtimedevice.ArchivedCancellationReceipt{}, nil - } - id, err := parseID(device) - if err != nil { - return runtimedevice.ArchivedCancellationReceipt{}, err - } - row, err := s.units.Queries().GetArchivedCancellationReceipt(ctx, sqlc.GetArchivedCancellationReceiptParams{DeviceID: id, CredentialHash: pgtype.Text{String: credentialHash, Valid: true}, RunIds: runIDs, LimitSeconds: int32(runtimedevice.ArchivedCancellationReceiptLimit.Seconds())}) - if errors.Is(err, pgx.ErrNoRows) { - return runtimedevice.ArchivedCancellationReceipt{}, nil - } - if err != nil { - return runtimedevice.ArchivedCancellationReceipt{}, err - } - return runtimedevice.ArchivedCancellationReceipt{RunID: uuid.UUID(row.ID.Bytes).String(), Deadline: row.CancelRequestedAt.Time.Add(runtimedevice.ArchivedCancellationReceiptLimit)}, nil -} - -func (s *Store) CreateDevice(ctx context.Context, tenant string, registration sessions.DeviceRegistration) (sessions.ExecutionDevice, error) { - tenantID, err := parseID(tenant) - if err != nil { - return sessions.ExecutionDevice{}, err - } - id, err := s.units.Queries().CreateDevice(ctx, sqlc.CreateDeviceParams{ - ID: pgtype.UUID{Bytes: uuid.New(), Valid: true}, TenantID: tenantID, - Name: registration.Name, CredentialHash: pgtype.Text{String: registration.CredentialHash, Valid: true}, - }) - if err != nil { - return sessions.ExecutionDevice{}, fmt.Errorf("create execution device: %w", err) - } - return sessions.ExecutionDevice{ID: uuid.UUID(id.Bytes).String(), Name: registration.Name}, nil -} - -func (s *Store) RevokeDevice(ctx context.Context, tenant, device string) error { - lookup, err := ResourceLookup(tenant, device) - if err != nil { - return err - } - return s.units.Transaction(ctx, func(ctx context.Context, tx pgx.Tx) error { - q := sqlc.New(tx) - n, err := q.RevokeDevice(ctx, sqlc.RevokeDeviceParams(lookup)) - if err != nil { - return err - } - if n == 0 { - return sessions.ErrNotFound - } - return q.SettleRevokedRuntimeReleases(ctx, lookup.ID) - }) -} - func (s *Store) TouchDevice(ctx context.Context, device string) (bool, error) { id, err := parseID(device) if err != nil { @@ -276,7 +215,7 @@ func (t *SessionTx) LoadDevice(ctx context.Context, device string) (bool, error) if err != nil { return false, err } - _, err = t.q.GetAgentHost(ctx, sqlc.GetAgentHostParams{TenantID: t.tenant, ID: id}) + _, err = t.q.GetAgentHost(ctx, id) if errors.Is(err, pgx.ErrNoRows) { return false, nil } diff --git a/services/core/internal/persistence/postgres/sessionpg/devices_test.go b/services/core/internal/persistence/postgres/sessionpg/devices_test.go index 4f9da5bfb..86054dc96 100644 --- a/services/core/internal/persistence/postgres/sessionpg/devices_test.go +++ b/services/core/internal/persistence/postgres/sessionpg/devices_test.go @@ -15,12 +15,11 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) -// newAgentHost stores an agent host of tenant, or of none when tenant is not -// valid, and returns its ID. -func newAgentHost(t *testing.T, pool *pgxpool.Pool, tenant pgtype.UUID) string { +// newAgentHost stores a deployment agent host and returns its ID. +func newAgentHost(t *testing.T, pool *pgxpool.Pool) string { t.Helper() id := uuid.NewString() - exec(t, pool, `INSERT INTO devices(id, tenant_id, name, credential_hash, agent_host) VALUES ($1, $2, 'agent host', $3, true)`, id, tenant, strings.Repeat("a", 64)) + exec(t, pool, `INSERT INTO devices(id, name, credential_hash) VALUES ($1, 'agent host', $2)`, id, strings.Repeat("a", 64)) return id } @@ -50,20 +49,15 @@ func TestBindSessionDeviceTranslatesTheBindingOutcome(t *testing.T) { } tenantID, sessionID, environmentID := newEnvironment(t, pool, "self_hosted", "pending") tenant, session := uuidText(tenantID), uuidText(sessionID) - otherTenant, _, _ := newEnvironment(t, pool, "self_hosted", "pending") - device, replacement := newAgentHost(t, pool, pgtype.UUID{}), newAgentHost(t, pool, tenantID) - revoked := newAgentHost(t, pool, pgtype.UUID{}) + device, replacement := newAgentHost(t, pool), newAgentHost(t, pool) + revoked := newAgentHost(t, pool) exec(t, pool, `UPDATE devices SET revoked_at = clock_timestamp() WHERE id = $1`, revoked) - tenantDevice := uuid.NewString() - exec(t, pool, `INSERT INTO devices(id, tenant_id, name, credential_hash) VALUES ($1, $2, 'runtime', $3)`, tenantDevice, tenantID, strings.Repeat("a", 64)) for name, test := range map[string]struct { device string want error }{ - "another tenant's agent host": {newAgentHost(t, pool, otherTenant), sessions.ErrNotFound}, "revoked agent host": {revoked, sessions.ErrNotFound}, - "device that is no agent host": {tenantDevice, sessions.ErrNotFound}, "malformed device": {"device", sessions.ErrInvalidInput}, "Environment without a live resource": {device, sessions.ErrDeviceBindingConflict}, } { diff --git a/services/core/internal/persistence/postgres/sessionpg/execution_journal_test.go b/services/core/internal/persistence/postgres/sessionpg/execution_journal_test.go index 42adfa930..e7985884c 100644 --- a/services/core/internal/persistence/postgres/sessionpg/execution_journal_test.go +++ b/services/core/internal/persistence/postgres/sessionpg/execution_journal_test.go @@ -120,7 +120,7 @@ func TestSubagentProjectionStoresTheSessionRows(t *testing.T) { exec(t, pool, `INSERT INTO sessions(id, tenant_id, engine, idempotency_key, request_hash, configuration) VALUES ($1, $2, 'codex', 'key', 'hash', '{"agent":{"id":"agent_root"}}')`, session, tenant) exec(t, pool, `INSERT INTO turns(id, session_id, status) VALUES ($1, $2, 'in_progress')`, turn, session) - exec(t, pool, `INSERT INTO devices(id, tenant_id, name, credential_hash) VALUES ($1, $2, 'device', repeat('a', 64))`, device, tenant) + exec(t, pool, `INSERT INTO devices(id, name, credential_hash) VALUES ($1, 'device', repeat('a', 64))`, device) exec(t, pool, `INSERT INTO session_runtime_assignments(session_id, runtime_id, native_session_id) VALUES ($1, $2, 'root')`, session, device) encode := func(kind string, value any) sessions.ExecutionEvent { raw, err := json.Marshal(value) diff --git a/services/core/internal/persistence/postgres/sessionpg/link.go b/services/core/internal/persistence/postgres/sessionpg/link.go index fce516440..360e44e35 100644 --- a/services/core/internal/persistence/postgres/sessionpg/link.go +++ b/services/core/internal/persistence/postgres/sessionpg/link.go @@ -3,7 +3,6 @@ package sessionpg import ( "context" "errors" - "fmt" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" @@ -99,10 +98,7 @@ func (s *Store) RegisterAgentHost(ctx context.Context, runtime, credentialHash s if err != nil { return err } - _, err = s.units.Queries().RegisterAgentHost(ctx, sqlc.RegisterAgentHostParams{ID: id, CredentialHash: pgtype.Text{String: credentialHash, Valid: true}}) - if errors.Is(err, pgx.ErrNoRows) { - return fmt.Errorf("agent host %s is registered as another device", runtime) - } + _, err = s.units.Queries().RegisterAgentHost(ctx, sqlc.RegisterAgentHostParams{ID: id, CredentialHash: credentialHash}) return err } diff --git a/services/core/internal/persistence/postgres/sessionpg/session_test.go b/services/core/internal/persistence/postgres/sessionpg/session_test.go index 421a1f1f4..32381dc50 100644 --- a/services/core/internal/persistence/postgres/sessionpg/session_test.go +++ b/services/core/internal/persistence/postgres/sessionpg/session_test.go @@ -4,7 +4,6 @@ import ( "context" "errors" "reflect" - "strings" "testing" "time" @@ -250,47 +249,3 @@ func TestTerminateEnvironmentThroughTheBindingExpires(t *testing.T) { t.Fatalf("change %+v", changes[0]) } } - -func TestCreateEnvironmentDeviceCreatesOneDevice(t *testing.T) { - pool := pgtest.Open(t) - hash := strings.Repeat("a", 64) - create := func(tenant, session, environment pgtype.UUID, device uuid.UUID, commit bool) error { - dedicated := sessions.ExecutionDevice{ID: device.String(), Name: "runtime"} - return pgx.BeginFunc(t.Context(), pool, func(tx pgx.Tx) error { - if err := sessions.CreateEnvironmentDevice(t.Context(), BindSession(sqlc.New(tx), tenant, session), uuid.UUID(environment.Bytes).String(), dedicated, hash); err != nil || commit { - return err - } - return errRollback - }) - } - created := func(session pgtype.UUID) []uuid.UUID { - rows, err := pool.Query(t.Context(), `SELECT d.id FROM devices d JOIN environments e ON e.id = d.environment_id WHERE e.session_id = $1`, session) - if err != nil { - t.Fatal(err) - } - devices, err := pgx.CollectRows(rows, pgx.RowTo[uuid.UUID]) - if err != nil { - t.Fatal(err) - } - return devices - } - - tenant, session, environment := newEnvironment(t, pool, "openai_hosted", "pending") - if err := create(tenant, session, environment, uuid.New(), false); !errors.Is(err, errRollback) || len(created(session)) != 0 { - t.Fatalf("rolled back creation created %v: %v", created(session), err) - } - if err := create(pgID(uuid.New()), session, environment, uuid.New(), true); !errors.Is(err, sessions.ErrDeviceBindingConflict) || len(created(session)) != 0 { - t.Fatalf("other tenant created %v: %v", created(session), err) - } - device := uuid.New() - if err := create(tenant, session, environment, device, true); err != nil || !reflect.DeepEqual(created(session), []uuid.UUID{device}) { - t.Fatalf("created %v: %v", created(session), err) - } - if err := create(tenant, session, environment, uuid.New(), true); !errors.Is(err, sessions.ErrDeviceBindingConflict) { - t.Fatalf("second device: %v", err) - } - tenant, session, environment = newEnvironment(t, pool, "self_hosted", "pending") - if err := create(tenant, session, environment, uuid.New(), true); !errors.Is(err, sessions.ErrDeviceBindingConflict) { - t.Fatalf("self-hosted device: %v", err) - } -} diff --git a/services/core/internal/runtime/gateway.go b/services/core/internal/runtime/gateway.go index 9d048f33d..7058db9ac 100644 --- a/services/core/internal/runtime/gateway.go +++ b/services/core/internal/runtime/gateway.go @@ -10,19 +10,17 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" ) -// NewGateway serves the V1 daemon executor transport for both managed and -// user-managed Runtime. It authenticates devices with credentials, records -// their heartbeats with heartbeat, drains an archived Session's cancellation -// receipts through cancellations and gives agent hosts' binds their Link -// fields from links. Its credentials never grant public Session API access. -func NewGateway(credentials runtimegateway.RuntimeStore, heartbeat runtimegateway.HeartbeatTouch, cancellations runtimegateway.ArchivedCancellationStore, links *runtimegateway.LinkAuthority, publicWSURL string) (http.Handler, *runtimegateway.Registry, error) { - if credentials == nil || heartbeat == nil || cancellations == nil || links == nil { +// NewGateway serves the V1 agent host transport. It authenticates devices with +// credentials, records their heartbeats and gives binds their Link fields from +// links. Its credentials never grant public Session API access. +func NewGateway(credentials runtimegateway.RuntimeStore, heartbeat runtimegateway.HeartbeatTouch, links *runtimegateway.LinkAuthority, publicWSURL string) (http.Handler, *runtimegateway.Registry, error) { + if credentials == nil || heartbeat == nil || links == nil { return nil, nil, errors.New("daemon gateway dependencies are required") } registry := runtimegateway.NewRegistry() h := runtimegateway.NewHandler(runtimegateway.HandlerConfig{ Authenticator: runtimegateway.NewAuthenticator(credentials), Registry: registry, - Heartbeat: heartbeat, ArchivedCancellations: cancellations, Links: links, PublicWSURL: publicWSURL, + Heartbeat: heartbeat, Links: links, PublicWSURL: publicWSURL, }) r := chi.NewRouter() r.Route("/api/v1", func(r chi.Router) { runtimegateway.RegisterRoutes(r, h) }) diff --git a/services/core/internal/runtimedevice/cancellation.go b/services/core/internal/runtimedevice/cancellation.go deleted file mode 100644 index b747cd462..000000000 --- a/services/core/internal/runtimedevice/cancellation.go +++ /dev/null @@ -1,14 +0,0 @@ -package runtimedevice - -import "time" - -// ArchivedCancellationReceiptLimit bounds receipt draining after an administrator -// archive. It is not a model/tool timeout and never restarts on heartbeat or retry. -const ArchivedCancellationReceiptLimit = 20 * time.Second - -// ArchivedCancellationReceipt authorizes only the original delivery's receipt -// drain. It grants neither Runtime credentials nor permission to start work. -type ArchivedCancellationReceipt struct { - RunID string - Deadline time.Time -} diff --git a/services/core/internal/runtimedevice/credential.go b/services/core/internal/runtimedevice/credential.go index 29d5be349..d9e41f4e4 100644 --- a/services/core/internal/runtimedevice/credential.go +++ b/services/core/internal/runtimedevice/credential.go @@ -18,10 +18,6 @@ type Credential struct { Name string Type string CredentialHash string - // RuntimeNodeID is the persisted managed allocation binding, never caller input. - RuntimeNodeID string - // RuntimeAllocationID distinguishes managed cloud compute from self-hosted devices. - RuntimeAllocationID string } // HashCredential preserves the runtime bearer format, including trimming diff --git a/services/core/internal/runtimegateway/auth.go b/services/core/internal/runtimegateway/auth.go index 40fa443f5..48a59a79a 100644 --- a/services/core/internal/runtimegateway/auth.go +++ b/services/core/internal/runtimegateway/auth.go @@ -30,11 +30,9 @@ type RuntimeStore interface { // AuthenticatedRuntime is the result of a successful credential check. type AuthenticatedRuntime struct { - DeviceID string - WorkspaceID string - Name string - RuntimeNodeID string - RuntimeAllocationID string + DeviceID string + WorkspaceID string + Name string } // Authenticator validates the (device_id, token, version) trio that @@ -80,10 +78,8 @@ func (a *Authenticator) AuthenticateBearer(ctx context.Context, deviceID, bearer return AuthenticatedRuntime{}, ErrAuthBadCredential } return AuthenticatedRuntime{ - DeviceID: rt.ID, - WorkspaceID: rt.WorkspaceID, - Name: rt.Name, - RuntimeNodeID: rt.RuntimeNodeID, - RuntimeAllocationID: rt.RuntimeAllocationID, + DeviceID: rt.ID, + WorkspaceID: rt.WorkspaceID, + Name: rt.Name, }, nil } diff --git a/services/core/internal/runtimegateway/cancellation.go b/services/core/internal/runtimegateway/cancellation.go deleted file mode 100644 index 2ee2b13ed..000000000 --- a/services/core/internal/runtimegateway/cancellation.go +++ /dev/null @@ -1,121 +0,0 @@ -package runtimegateway - -import ( - "context" - "errors" - "sync" - "time" - - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" -) - -// ArchivedCancellationStore is deliberately separate from RuntimeStore: a -// receipt opportunity cannot authenticate a new connection or authorize work. -type ArchivedCancellationStore interface { - ArchivedCancellationReceipt(context.Context, string, string, []string) (runtimedevice.ArchivedCancellationReceipt, error) -} - -// TrackExecutionDelivery retains the exact existing Core delivery through its -// terminal database commit. Durable subscriptions may already have seen Done or -// been removed while cancellation ACK/commit is still pending. -func (s *Session) TrackExecutionDelivery(runID string) (func(), error) { - s.receiptMu.Lock() - defer s.receiptMu.Unlock() - if runID == "" || s.IsClosed() || s.receiptDrain.RunID != "" { - return nil, ErrSessionClosed - } - if s.deliveries == nil { - s.deliveries = make(map[string]struct{}) - } - if _, exists := s.deliveries[runID]; exists { - return nil, errors.New("execution delivery already tracked") - } - s.deliveries[runID] = struct{}{} - var once sync.Once - return func() { - once.Do(func() { - s.receiptMu.Lock() - delete(s.deliveries, runID) - closePeer := s.receiptDrain.RunID == runID - s.receiptMu.Unlock() - if closePeer { - s.CloseWithCode(CloseRuntimeDeleted, "archived cancellation settled") - } - }) - }, nil -} - -// DrainArchivedCancellation checks both durable archive identity and this exact -// connection's unfinished delivery. It performs no liveness or credential write. -func (s *Session) DrainArchivedCancellation(ctx context.Context) (bool, error) { - if s.archivedCancellations == nil || s.IsClosed() { - return false, nil - } - s.receiptMu.Lock() - ids := make([]string, 0, len(s.deliveries)) - for id := range s.deliveries { - ids = append(ids, id) - } - s.receiptMu.Unlock() - if len(ids) == 0 { - return false, nil - } - receipt, err := s.archivedCancellations.ArchivedCancellationReceipt(ctx, s.DeviceID, s.credentialHash, ids) - if err != nil || receipt.RunID == "" { - return false, err - } - s.receiptMu.Lock() - defer s.receiptMu.Unlock() - if _, exists := s.deliveries[receipt.RunID]; !exists || s.IsClosed() || !time.Now().Before(receipt.Deadline) { - return false, nil - } - if s.receiptDrain.RunID != "" { - // Once fenced, neither a later cancel nor a changed response extends it. - return s.receiptDrain.RunID == receipt.RunID && s.receiptDrain.Deadline.Equal(receipt.Deadline), nil - } - s.receiptDrain = receipt - s.receiptTimer = time.AfterFunc(time.Until(receipt.Deadline), func() { s.CloseWithCode(CloseRuntimeDeleted, "archived cancellation deadline") }) - return true, nil -} - -func (s *Session) receiptDraining() bool { - s.receiptMu.Lock() - defer s.receiptMu.Unlock() - return s.receiptDrain.RunID != "" -} - -func (s *Session) allowsReceiptFrame(env proto.Envelope, outbound bool) bool { - s.receiptMu.Lock() - defer s.receiptMu.Unlock() - if s.receiptDrain.RunID == "" { - return true - } - if !time.Now().Before(s.receiptDrain.Deadline) { - return false - } - if !outbound && env.Type == proto.TypeHeartbeat { - return true - } - if env.ID != s.receiptDrain.RunID { - return false - } - if outbound { - return env.Type == proto.TypePromptCancel - } - if env.Type == proto.TypeInteractionDecisionAck { - var ack proto.InteractionDecisionAckPayload - return env.DecodePayload(&ack) == nil && ack.DeliveryID == "cancel:"+env.ID - } - // Existing run events must remain lossless, including child observations and - // Done before ACK. Workspace/preparation replies have other correlation IDs. - return true -} - -func (s *Session) stopReceiptTimer() { - s.receiptMu.Lock() - defer s.receiptMu.Unlock() - if s.receiptTimer != nil { - s.receiptTimer.Stop() - } -} diff --git a/services/core/internal/runtimegateway/cancellation_test.go b/services/core/internal/runtimegateway/cancellation_test.go deleted file mode 100644 index a534277b5..000000000 --- a/services/core/internal/runtimegateway/cancellation_test.go +++ /dev/null @@ -1,147 +0,0 @@ -package runtimegateway - -import ( - "context" - "sync" - "testing" - "time" - - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" -) - -type receiptStore struct { - mu sync.Mutex - receipt runtimedevice.ArchivedCancellationReceipt -} - -func (r *receiptStore) ArchivedCancellationReceipt(_ context.Context, id, hash string, runs []string) (runtimedevice.ArchivedCancellationReceipt, error) { - r.mu.Lock() - defer r.mu.Unlock() - if id != "device" || hash != "original-hash" { - return runtimedevice.ArchivedCancellationReceipt{}, nil - } - for _, run := range runs { - if run == r.receipt.RunID { - return r.receipt, nil - } - } - return runtimedevice.ArchivedCancellationReceipt{}, nil -} - -func TestArchivedReceiptTracksDeliveryBeyondDoneAndRejectsNewWork(t *testing.T) { - conn := newFakeConn() - peer := NewSession(conn, "device", "", "", NewRegistry(), nil) - defer peer.Close("test complete") - peer.credentialHash = "original-hash" - receipt := runtimedevice.ArchivedCancellationReceipt{RunID: "run", Deadline: time.Now().Add(time.Second)} - peer.heartbeat = newFakeHeartbeatStore() - peer.archivedCancellations = &receiptStore{receipt: receipt} - if draining, err := peer.DrainArchivedCancellation(t.Context()); err != nil || draining { - t.Fatal("unowned delivery got drain", draining, err) - } - release, err := peer.TrackExecutionDelivery("run") - if err != nil { - t.Fatal(err) - } - subscription, err := peer.SubscribeDurable("run", proto.AssignmentRef{}) - if err != nil { - t.Fatal(err) - } - done, _ := proto.NewEnvelope(proto.TypeDone, "run", proto.DonePayload{}) - peer.dispatch(done) - for range subscription.Events { - } - if draining, err := peer.DrainArchivedCancellation(t.Context()); err != nil || !draining { - t.Fatal("Done discarded in-flight ACK/commit owner", draining, err) - } - if _, err := peer.TrackExecutionDelivery("new-run"); err == nil { - t.Fatal("new delivery admitted while draining") - } - for _, kind := range []string{proto.TypeExecutionStart, proto.TypePromptSteer, proto.TypeExecutionPrepare, proto.TypeRuntimePrepare, proto.TypeWorkspaceWrite, proto.TypeWorkspaceRead} { - env, _ := proto.NewEnvelope(kind, "run", nil) - if err := peer.Send(t.Context(), env); err == nil { - t.Fatal("drain permitted new operation", kind) - } - } - cancel, _ := proto.NewEnvelope(proto.TypePromptCancel, "run", proto.PromptCancelPayload{DeliveryID: "cancel:run"}) - if err := peer.Send(t.Context(), cancel); err != nil { - t.Fatal("drain blocked cancellation", err) - } - release() // The caller has now committed its terminal result. - if !peer.IsClosed() { - t.Fatal("finished delivery retained revoked connection") - } -} - -type blockedControlConn struct { - *fakeConn - entered chan struct{} - resume chan struct{} - wrote chan struct{} - once sync.Once -} - -func (c *blockedControlConn) WriteMessage(kind int, data []byte) error { - c.once.Do(func() { close(c.entered) }) - <-c.resume - defer close(c.wrote) - return c.fakeConn.WriteMessage(kind, data) -} -func (c *blockedControlConn) WriteControl(kind int, data []byte, _ time.Time) error { - return c.fakeConn.WriteMessage(kind, data) -} - -func TestCloseWithCodeUsesConcurrentControlWriter(t *testing.T) { - conn := &blockedControlConn{fakeConn: newFakeConn(), entered: make(chan struct{}), resume: make(chan struct{}), wrote: make(chan struct{})} - peer := NewSession(conn, "device", "", "", NewRegistry(), nil) - peer.Start() - env, _ := proto.NewEnvelope(proto.TypePromptCancel, "run", nil) - if err := peer.Send(t.Context(), env); err != nil { - t.Fatal(err) - } - select { - case <-conn.entered: - case <-time.After(time.Second): - t.Fatal("data writer never entered") - } - finished := make(chan struct{}) - go func() { peer.CloseWithCode(CloseRuntimeDeleted, "retired"); close(finished) }() - select { - case <-finished: - case <-time.After(time.Second): - close(conn.resume) - <-finished - t.Fatal("close used the blocked data writer") - } - close(conn.resume) - select { - case <-conn.wrote: - case <-time.After(time.Second): - t.Fatal("data writer did not exit") - } -} - -func TestArchivedReceiptDeadlineDoesNotRenew(t *testing.T) { - peer := NewSession(newFakeConn(), "device", "", "", NewRegistry(), nil) - defer peer.Close("test complete") - peer.credentialHash = "original-hash" - receipt := runtimedevice.ArchivedCancellationReceipt{RunID: "run", Deadline: time.Now().Add(100 * time.Millisecond)} - peer.heartbeat = newFakeHeartbeatStore() - peer.archivedCancellations = &receiptStore{receipt: receipt} - release, err := peer.TrackExecutionDelivery("run") - if err != nil { - t.Fatal(err) - } - defer release() - for range 10 { - if draining, err := peer.DrainArchivedCancellation(t.Context()); err != nil || !draining { - t.Fatal(draining, err) - } - } - select { - case <-peer.Closed(): - case <-time.After(time.Second): - t.Fatal("receipt retries renewed deadline") - } -} diff --git a/services/core/internal/runtimegateway/handler.go b/services/core/internal/runtimegateway/handler.go index 9c8dbfa0d..9ee2f53a3 100644 --- a/services/core/internal/runtimegateway/handler.go +++ b/services/core/internal/runtimegateway/handler.go @@ -36,10 +36,6 @@ type HandlerConfig struct { // last_heartbeat_at fresh. nil tracks liveness in-process only. Heartbeat HeartbeatTouch - // ArchivedCancellations reads the receipt that an archived Session's - // cancellation still owes a connection's delivery. nil drains nothing. - ArchivedCancellations ArchivedCancellationStore - // Links gives an agent host's binds their Link resource and attach grant. // nil sends binds without them. Links *LinkAuthority @@ -133,7 +129,6 @@ func (h *Handler) WS(w http.ResponseWriter, r *http.Request) { } sess := newSession(conn, auth.DeviceID, auth.WorkspaceID, version, h.cfg.Registry, h.cfg.Log) sess.heartbeat = h.cfg.Heartbeat - sess.archivedCancellations = h.cfg.ArchivedCancellations sess.links = h.cfg.Links sess.credentialHash = runtimedevice.HashCredential(token) h.cfg.Log("agentdaemon gateway: ws upgrade ok, registering device_id=%s waiters=%d", @@ -191,36 +186,6 @@ func (h *Handler) Bootstrap(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(resp) } -// DeviceStatus is a lightweight liveness probe the daemon hits before -// the WS dial. -func (h *Handler) DeviceStatus(w http.ResponseWriter, r *http.Request) { - bearer := bearerFromAuthHeader(r) - if bearer == "" { - writeAuthError(w, http.StatusUnauthorized, "missing_bearer", "") - return - } - deviceID := r.URL.Query().Get("device_id") - if deviceID == "" { - writeAuthError(w, http.StatusBadRequest, "missing_device_id", "device_id query param required") - return - } - auth, err := h.cfg.Authenticator.AuthenticateBearer(r.Context(), deviceID, bearer) - if err != nil { - status, code := mapAuthError(err) - h.cfg.Log("agentdaemon gateway: device-status auth rejected device_id=%s code=%s status=%d err=%v", - deviceID, code, status, err) - writeAuthError(w, status, code, err.Error()) - return - } - _, regErr := h.cfg.Registry.LookupDevice(auth.DeviceID) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(map[string]any{ - "device_id": auth.DeviceID, - "online": regErr == nil, - }) -} - // ---------------------------------------------------------------------- // helpers // ---------------------------------------------------------------------- diff --git a/services/core/internal/runtimegateway/routes.go b/services/core/internal/runtimegateway/routes.go index d0c098c1e..c8f2dcf51 100644 --- a/services/core/internal/runtimegateway/routes.go +++ b/services/core/internal/runtimegateway/routes.go @@ -9,9 +9,8 @@ import ( // // GET /agent-daemon/ws — daemon dial-in (WS upgrade) // POST /agent-daemon/bootstrap — daemon first-call to fetch wsUrl + heartbeat cadence -// GET /agent-daemon/device-status — daemon self-check // -// All three accept the daemon credential described in +// Both accept the daemon credential described in // contracts/agents-api/machine-api.md. func RegisterRoutes(r chi.Router, h *Handler) { if h == nil { @@ -20,6 +19,5 @@ func RegisterRoutes(r chi.Router, h *Handler) { r.Route("/agent-daemon", func(r chi.Router) { r.Get("/ws", h.WS) r.Post("/bootstrap", h.Bootstrap) - r.Get("/device-status", h.DeviceStatus) }) } diff --git a/services/core/internal/runtimegateway/session.go b/services/core/internal/runtimegateway/session.go index eb91a4892..c41d7ad5e 100644 --- a/services/core/internal/runtimegateway/session.go +++ b/services/core/internal/runtimegateway/session.go @@ -85,9 +85,6 @@ type Session struct { // heartbeat persists daemon-advertised capability snapshots. heartbeat HeartbeatTouch credentialHash string - // archivedCancellations reads the receipt an archived Session's - // cancellation owes this connection's delivery. - archivedCancellations ArchivedCancellationStore // links supplies the Link fields of this connection's binds. links *LinkAuthority @@ -135,11 +132,6 @@ type Session struct { // closeOnce guards the shutdown path so concurrent Close calls // collapse into one. - receiptMu sync.Mutex - deliveries map[string]struct{} - receiptDrain runtimedevice.ArchivedCancellationReceipt - receiptTimer *time.Timer - closeOnce sync.Once closed chan struct{} } @@ -250,7 +242,6 @@ func (s *Session) setDeclarations(kinds []proto.SupportedAgentKind, homeRemoval // releases connection ownership. It establishes no execution outcome. Idempotent. func (s *Session) Close(reason string) { s.closeOnce.Do(func() { - s.stopReceiptTimer() close(s.closed) _ = s.conn.Close() // Transport failure must remain distinct from native execution facts. @@ -290,7 +281,7 @@ func (s *Session) CloseWithCode(code int, reason string) { // daemon frame inherits the caller's trace_id. Callers that explicitly // set env.Trace win. func (s *Session) Send(ctx context.Context, env proto.Envelope) error { - if s.IsClosed() || !s.allowsReceiptFrame(env, true) { + if s.IsClosed() { return ErrSessionClosed } if env.Trace == "" { @@ -356,9 +347,6 @@ func (s *Session) writeLoop() { if !ok { return } - if !s.allowsReceiptFrame(env, true) { - continue - } raw, err := json.Marshal(env) if err != nil { s.log("agentdaemon gateway: marshal outbound envelope: %v", err) @@ -390,9 +378,7 @@ func (s *Session) readLoop() { s.log("agentdaemon gateway: read frame: %v", err) return } - if !s.receiptDraining() { - s.markSeen() - } + s.markSeen() var env proto.Envelope if err := json.Unmarshal(raw, &env); err != nil { @@ -430,23 +416,12 @@ func (s *Session) handleHeartbeat(env proto.Envelope) { return } if status.Deleted { - draining, drainErr := s.DrainArchivedCancellation(ctx) - if drainErr == nil && draining { - return - } - s.log("agentdaemon gateway: runtime retired, closing session device=%s", s.DeviceID) - // "retired" rather than "deleted by admin": the row may have - // been soft-deleted by sandbox stale-row cleanup or by an - // actual admin action; the daemon only sees it's no longer - // the current owner. - s.CloseWithCode(CloseRuntimeDeleted, "runtime retired") + s.log("agentdaemon gateway: runtime authorization changed, closing session device=%s", s.DeviceID) + s.CloseWithCode(CloseRuntimeDeleted, "runtime authorization changed") } } func (s *Session) dispatch(env proto.Envelope) { - if !s.allowsReceiptFrame(env, false) { - return - } switch env.Type { case proto.TypeWorkspaceExportResult: s.dispatchWorkspaceExport(env) diff --git a/services/core/internal/runtimeobs/identity.go b/services/core/internal/runtimeobs/identity.go index f317c1d14..bd78c5633 100644 --- a/services/core/internal/runtimeobs/identity.go +++ b/services/core/internal/runtimeobs/identity.go @@ -6,12 +6,10 @@ import ( ) // Instance is one provider-owned Runtime incarnation. AllocationID is present -// for managed compute. DeviceID and ConnectionGeneration are reserved for a -// future authenticated self-hosted telemetry source. +// for managed compute. type Instance struct { AllocationID string ProviderKey string - DeviceID string ConnectionGeneration string AllocationState string AllocationCreatedAt time.Time diff --git a/services/core/internal/sandbox/docker/resources_test.go b/services/core/internal/sandbox/docker/resources_test.go index d31d14f4c..4911716d4 100644 --- a/services/core/internal/sandbox/docker/resources_test.go +++ b/services/core/internal/sandbox/docker/resources_test.go @@ -19,7 +19,7 @@ func TestObserveVerifiesOwnershipThenReadsOneShotStats(t *testing.T) { installationID := uuid.NewString() target := runtimeobs.Target{ TenantID: uuid.NewString(), SessionID: uuid.NewString(), EnvironmentID: uuid.NewString(), Mode: runtimeobs.ModeManaged, - Instance: runtimeobs.Instance{AllocationID: uuid.NewString(), ProviderKey: installationID, DeviceID: uuid.NewString()}, + Instance: runtimeobs.Instance{AllocationID: uuid.NewString(), ProviderKey: installationID}, } observed := time.Now().UTC().Truncate(time.Microsecond) started := observed.Add(-time.Minute) diff --git a/services/core/internal/sessions/artifacts_test.go b/services/core/internal/sessions/artifacts_test.go index f11756353..51f8c1c31 100644 --- a/services/core/internal/sessions/artifacts_test.go +++ b/services/core/internal/sessions/artifacts_test.go @@ -197,16 +197,6 @@ func (s *fakeArtifactStorage) WithInputs(context.Context, string, string, func(c return nil } -func (s *fakeArtifactStorage) CreateDevice(context.Context, string, DeviceRegistration) (ExecutionDevice, error) { - s.t.Fatal("unexpected call to CreateDevice") - return ExecutionDevice{}, nil -} - -func (s *fakeArtifactStorage) RevokeDevice(context.Context, string, string) error { - s.t.Fatal("unexpected call to RevokeDevice") - return nil -} - func (s *fakeArtifactStorage) TouchDevice(context.Context, string) (bool, error) { s.t.Fatal("unexpected call to TouchDevice") return false, nil diff --git a/services/core/internal/sessions/devices.go b/services/core/internal/sessions/devices.go index 91214e807..feb27e22f 100644 --- a/services/core/internal/sessions/devices.go +++ b/services/core/internal/sessions/devices.go @@ -2,33 +2,11 @@ package sessions import ( "context" - "encoding/hex" - "fmt" - "strings" "github.com/MiniMax-AI/OpenAgentCore/internal/sandboxbootstrap" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" ) -// DeviceRegistration is a Runtime device's name and the SHA-256 digest of its -// credential, as NewDeviceRegistration validates them. -type DeviceRegistration struct { - Name string - CredentialHash string -} - -// NewDeviceRegistration validates a device registration: a name of 1 to 256 -// bytes once trimmed, and a SHA-256 credential digest in hex, which it -// normalizes to lowercase. Anything else is ErrInvalidInput. -func NewDeviceRegistration(name, credentialHash string) (DeviceRegistration, error) { - name = strings.TrimSpace(name) - digest, err := hex.DecodeString(credentialHash) - if err != nil || len(digest) != 32 || name == "" || len(name) > 256 { - return DeviceRegistration{}, fmt.Errorf("%w: device name and SHA-256 credential digest required", ErrInvalidInput) - } - return DeviceRegistration{Name: name, CredentialHash: hex.EncodeToString(digest)}, nil -} - // SandboxResource is a live Link resource. Quiesced compute is between a // quiesce and the wake that resumes it. type SandboxResource struct { @@ -50,10 +28,6 @@ type DeviceReader interface { // authenticates a device with, and reports whether the device still has // authority. GetDeviceCredential(ctx context.Context, device string) (runtimedevice.Credential, bool, error) - // ArchivedCancellationReceipt reads the receipt window that archiving a - // Session leaves the device's exact authenticated delivery of one of - // runIDs; without one it is the zero receipt. - ArchivedCancellationReceipt(ctx context.Context, device, credentialHash string, runIDs []string) (runtimedevice.ArchivedCancellationReceipt, error) // ListLiveSandboxResources lists the live Link resources. ListLiveSandboxResources(ctx context.Context) ([]SandboxResource, error) // GetEnvironmentResource reads the live Link resource of the tenant's @@ -64,18 +38,12 @@ type DeviceReader interface { // once its Environment preparation completed; before that, and without an // authorized bound device, it is ErrNotFound. GetSessionExecutionBinding(ctx context.Context, tenant, session string) (ExecutionBinding, error) - // ListAgentHosts lists the unrevoked agent hosts that may run the - // tenant's Sessions, in ID order. - ListAgentHosts(ctx context.Context, tenant string) ([]ExecutionDevice, error) + // ListAgentHosts lists the deployment's unrevoked agent hosts in ID order. + ListAgentHosts(ctx context.Context) ([]ExecutionDevice, error) } // DeviceStorage stores Runtime devices. type DeviceStorage interface { - // CreateDevice stores a new device of the tenant and returns it. - CreateDevice(ctx context.Context, tenant string, registration DeviceRegistration) (ExecutionDevice, error) - // RevokeDevice revokes the tenant's device; an unknown device is - // ErrNotFound. - RevokeDevice(ctx context.Context, tenant, device string) error // TouchDevice records that the device was seen and reports whether it // still has authority. TouchDevice(ctx context.Context, device string) (bool, error) @@ -103,21 +71,6 @@ type EnrollmentTx interface { EnrollSandbox(ctx context.Context, key string) (sandboxbootstrap.Resource, error) } -// CreateDevice provisions a device for an operator. It is not a tenant-facing -// registration API. -func (s *Service) CreateDevice(ctx context.Context, tenant, name, credentialHash string) (ExecutionDevice, error) { - registration, err := NewDeviceRegistration(name, credentialHash) - if err != nil { - return ExecutionDevice{}, err - } - return s.storage.CreateDevice(ctx, tenant, registration) -} - -// RevokeDevice revokes the tenant's device. -func (s *Service) RevokeDevice(ctx context.Context, tenant, device string) error { - return s.storage.RevokeDevice(ctx, tenant, device) -} - // TouchRuntimeHeartbeat records a Runtime connection. A device that lost its // authority reports Deleted. func (s *Service) TouchRuntimeHeartbeat(ctx context.Context, device string) (runtimedevice.HeartbeatStatus, error) { diff --git a/services/core/internal/sessions/devices_test.go b/services/core/internal/sessions/devices_test.go index 9f62d31fd..78b3868a8 100644 --- a/services/core/internal/sessions/devices_test.go +++ b/services/core/internal/sessions/devices_test.go @@ -18,8 +18,6 @@ type fakeStorage struct { t *testing.T calls []string - createDevice func(DeviceRegistration) (ExecutionDevice, error) - revokeDevice func() error touchDevice func() (bool, error) touchAuthenticatedDevice func() (bool, error) // enrollment is the transaction WithEnrollment applies in, with @@ -62,16 +60,6 @@ func (s *fakeStorage) record(name string, set bool, detail ...string) { s.calls = append(s.calls, strings.Join(append([]string{name}, detail...), " ")) } -func (s *fakeStorage) CreateDevice(_ context.Context, tenant string, registration DeviceRegistration) (ExecutionDevice, error) { - s.record("CreateDevice", s.createDevice != nil, tenant, registration.Name, registration.CredentialHash) - return s.createDevice(registration) -} - -func (s *fakeStorage) RevokeDevice(_ context.Context, tenant, device string) error { - s.record("RevokeDevice", s.revokeDevice != nil, tenant, device) - return s.revokeDevice() -} - func (s *fakeStorage) TouchDevice(_ context.Context, device string) (bool, error) { s.record("TouchDevice", s.touchDevice != nil, device) return s.touchDevice() @@ -123,49 +111,7 @@ func deviceService(t *testing.T, storage *fakeStorage) *Service { const credentialDigest = "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" -func TestNewDeviceRegistration(t *testing.T) { - registration, err := NewDeviceRegistration(" runtime ", strings.ToUpper(credentialDigest)) - if err != nil || registration != (DeviceRegistration{Name: "runtime", CredentialHash: credentialDigest}) { - t.Fatalf("registration %+v, %v", registration, err) - } - if _, err := NewDeviceRegistration(strings.Repeat("n", 256), credentialDigest); err != nil { - t.Fatalf("longest name: %v", err) - } - for name, input := range map[string][2]string{ - "blank name": {" ", credentialDigest}, - "long name": {strings.Repeat("n", 257), credentialDigest}, - "not hex": {"runtime", "not-a-digest"}, - "short digest": {"runtime", credentialDigest[:62]}, - "raw credential": {"runtime", "secret"}, - "missing digest": {"runtime", ""}, - "padded digest": {"runtime", " " + credentialDigest}, - "too long digest": {"runtime", credentialDigest + "00"}, - } { - if _, err := NewDeviceRegistration(input[0], input[1]); !errors.Is(err, ErrInvalidInput) { - t.Fatalf("%s: %v", name, err) - } - } -} - func TestDeviceUseCases(t *testing.T) { - storage := &fakeStorage{t: t} - if _, err := deviceService(t, storage).CreateDevice(t.Context(), "tenant", "runtime", "secret"); !errors.Is(err, ErrInvalidInput) { - t.Fatalf("invalid registration: %v", err) - } - created := ExecutionDevice{ID: "device", Name: "runtime"} - storage.createDevice = func(DeviceRegistration) (ExecutionDevice, error) { return created, nil } - storage.revokeDevice = done - if device, err := deviceService(t, storage).CreateDevice(t.Context(), "tenant", " runtime ", strings.ToUpper(credentialDigest)); err != nil || device != created { - t.Fatalf("created %+v, %v", device, err) - } - if err := deviceService(t, storage).RevokeDevice(t.Context(), "tenant", "device"); err != nil { - t.Fatal(err) - } - want := []string{"CreateDevice tenant runtime " + credentialDigest, "RevokeDevice tenant device"} - if strings.Join(storage.calls, "\n") != strings.Join(want, "\n") { - t.Fatalf("calls %q", storage.calls) - } - // A heartbeat reports whether the device, or the credential it was // authenticated with, still has authority. for _, current := range []bool{true, false} { @@ -184,7 +130,7 @@ func TestDeviceUseCases(t *testing.T) { } } failing := func() (bool, error) { return true, errStorage } - storage = &fakeStorage{t: t, touchDevice: failing, touchAuthenticatedDevice: failing} + storage := &fakeStorage{t: t, touchDevice: failing, touchAuthenticatedDevice: failing} if status, err := deviceService(t, storage).TouchRuntimeHeartbeat(t.Context(), "device"); !errors.Is(err, errStorage) || status != (runtimedevice.HeartbeatStatus{}) { t.Fatalf("failed runtime heartbeat %+v, %v", status, err) } diff --git a/services/core/internal/sessions/environment_device.go b/services/core/internal/sessions/environment_device.go deleted file mode 100644 index aab8cf5d3..000000000 --- a/services/core/internal/sessions/environment_device.go +++ /dev/null @@ -1,30 +0,0 @@ -package sessions - -import "context" - -// EnvironmentDeviceTx is the Session transaction CreateEnvironmentDevice runs -// in. -type EnvironmentDeviceTx interface { - // LoadBoundDevice reports whether the Session is bound to a Runtime device - // that still has authority. - LoadBoundDevice(ctx context.Context) (bool, error) - // InsertEnvironmentDevice inserts device, with the credential hash, as the - // dedicated Runtime device of the Session's hosted Environment. An - // Environment that already has a device or cannot take one is - // ErrDeviceBindingConflict. - InsertEnvironmentDevice(ctx context.Context, environment string, device ExecutionDevice, credentialHash string) error -} - -// CreateEnvironmentDevice creates the dedicated Runtime device of the -// Session's hosted Environment. A Session that is already bound to a device -// is ErrDeviceBindingConflict, so an existing credential is never widened. -func CreateEnvironmentDevice(ctx context.Context, tx EnvironmentDeviceTx, environment string, device ExecutionDevice, credentialHash string) error { - bound, err := tx.LoadBoundDevice(ctx) - if err != nil { - return err - } - if bound { - return ErrDeviceBindingConflict - } - return tx.InsertEnvironmentDevice(ctx, environment, device, credentialHash) -} diff --git a/services/core/internal/sessions/environment_device_test.go b/services/core/internal/sessions/environment_device_test.go deleted file mode 100644 index 01a12db12..000000000 --- a/services/core/internal/sessions/environment_device_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package sessions - -import ( - "errors" - "testing" -) - -// A Session that is already bound to a device never gets a second one. -func TestCreateEnvironmentDevice(t *testing.T) { - device := ExecutionDevice{ID: "device", Name: "runtime"} - free := &fakeTx{t: t, loadBoundDevice: returns(false), insertEnvironmentDevice: done} - if err := CreateEnvironmentDevice(t.Context(), free, "environment", device, "hash"); err != nil { - t.Fatal(err) - } - assertCalls(t, free, "LoadBoundDevice", "InsertEnvironmentDevice device runtime environment hash") - - bound := &fakeTx{t: t, loadBoundDevice: returns(true)} - if err := CreateEnvironmentDevice(t.Context(), bound, "environment", device, "hash"); !errors.Is(err, ErrDeviceBindingConflict) { - t.Fatal(err) - } - assertCalls(t, bound, "LoadBoundDevice") -} diff --git a/services/core/internal/sessions/transaction_test.go b/services/core/internal/sessions/transaction_test.go index fc3895cb8..4e89afd17 100644 --- a/services/core/internal/sessions/transaction_test.go +++ b/services/core/internal/sessions/transaction_test.go @@ -38,8 +38,6 @@ type fakeTx struct { expireEnvironment func() error loadComputeSuspension func() (bool, error) loadPendingFileWrite func() (bool, error) - loadBoundDevice func() (bool, error) - insertEnvironmentDevice func() error loadSessionDevice func() (ExecutionDevice, bool, error) claimInitialization func() (bool, error) unclaimInitialization func() (bool, error) @@ -105,7 +103,6 @@ var ( _ EnvironmentTerminationTx = (*fakeTx)(nil) _ InputStartTx = (*fakeTx)(nil) _ ComputeAdmissionTx = (*fakeTx)(nil) - _ EnvironmentDeviceTx = (*fakeTx)(nil) _ InputProjectionTx = (*fakeTx)(nil) _ InitializationTx = (*fakeTx)(nil) _ ConnectionTx = (*fakeTx)(nil) @@ -212,16 +209,6 @@ func (f *fakeTx) LoadPendingFileWrite(context.Context) (bool, error) { return f.loadPendingFileWrite() } -func (f *fakeTx) LoadBoundDevice(context.Context) (bool, error) { - f.record("LoadBoundDevice", f.loadBoundDevice != nil) - return f.loadBoundDevice() -} - -func (f *fakeTx) InsertEnvironmentDevice(_ context.Context, environment string, device ExecutionDevice, credentialHash string) error { - f.record("InsertEnvironmentDevice", f.insertEnvironmentDevice != nil, device.ID, device.Name, environment, credentialHash) - return f.insertEnvironmentDevice() -} - func (f *fakeTx) LoadJournalTurn(_ context.Context, turn string) (JournalTurn, bool, error) { f.record("LoadJournalTurn", f.loadJournalTurn != nil, turn) return f.loadJournalTurn() diff --git a/services/core/migrations/000098_sandbox_link_authority.sql b/services/core/migrations/000098_sandbox_link_authority.sql index 375191145..b1b344f6f 100644 --- a/services/core/migrations/000098_sandbox_link_authority.sql +++ b/services/core/migrations/000098_sandbox_link_authority.sql @@ -1,13 +1,39 @@ -- +goose Up --- Link authority. An allocation's Serve credential serves only its own --- resource at serve_generation. A self_hosted enrollment's resource is served --- with its executor credential while that key is authorized for the --- Environment. Only a marked agent host may Attach; credential_revision fences --- links that a rotated credential authenticated. An agent host may belong to --- no tenant, such as the deployment's own; every other device belongs to one. +-- Agent hosts are deployment identities. Existing assignments cannot be +-- transferred to them without losing native execution continuity. +-- +goose StatementBegin +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 FROM session_runtime_assignments b + JOIN sessions s ON s.id = b.session_id + WHERE b.desired_state = 'bound' AND s.deleted_at IS NULL + ) THEN + RAISE EXCEPTION 'Sessions have bound Runtime assignments; delete those Sessions, then upgrade'; + END IF; +END $$; +-- +goose StatementEnd + +DROP VIEW runtime_device_authority; +-- These UUIDs identify historical native children and immutable write requests, +-- not current execution authority. Preserve them without registry foreign keys. +ALTER TABLE subagent_identities DROP CONSTRAINT subagent_identities_device_id_fkey; +ALTER TABLE environment_file_writes DROP CONSTRAINT environment_file_writes_device_id_fkey; +-- Allocations retain Provider cleanup ownership independently of a Runtime. ALTER TABLE runtime_allocations + DROP COLUMN device_id, ADD COLUMN serve_credential_hash text CHECK (serve_credential_hash ~ '^[0-9a-f]{64}$'), ADD COLUMN serve_generation bigint NOT NULL DEFAULT 1 CHECK (serve_generation > 0); +DELETE FROM session_runtime_assignments; +DELETE FROM devices; +ALTER TABLE devices + DROP CONSTRAINT device_credential_source, + DROP COLUMN tenant_id, + DROP COLUMN environment_id, + DROP COLUMN executor_key_id, + DROP COLUMN archive_cancel_turn_id, + ALTER COLUMN credential_hash SET NOT NULL, + ADD COLUMN credential_revision bigint NOT NULL DEFAULT 1 CHECK (credential_revision > 0); CREATE TABLE sandbox_enrollments ( id uuid PRIMARY KEY, @@ -17,12 +43,6 @@ CREATE TABLE sandbox_enrollments ( created_at timestamptz NOT NULL DEFAULT clock_timestamp() ); -ALTER TABLE devices - ADD COLUMN agent_host boolean NOT NULL DEFAULT false, - ADD COLUMN credential_revision bigint NOT NULL DEFAULT 1 CHECK (credential_revision > 0), - ALTER COLUMN tenant_id DROP NOT NULL, - ADD CONSTRAINT devices_agent_host CHECK (CASE WHEN agent_host THEN environment_id IS NULL AND executor_key_id IS NULL ELSE tenant_id IS NOT NULL END); - -- Every Link resource with its Serve credential hash. A resource is live -- while that credential may Serve it. CREATE VIEW sandbox_resources AS @@ -44,32 +64,62 @@ JOIN environments e ON e.id = n.environment_id JOIN sessions s ON s.id = e.session_id JOIN environment_executor_credentials c ON c.key_id = n.executor_key_id; --- Sessions run on an agent host. A Session bound to an in-sandbox or --- user-managed Runtime cannot move, so the upgrade waits until those Sessions --- are deleted. +-- +goose Down +-- Restoring the old device references cannot invent identities or discard +-- durable history, enrollment authority, uncertain writes or pending cleanup. -- +goose StatementBegin DO $$ BEGIN - IF EXISTS ( - SELECT 1 FROM session_runtime_assignments b - JOIN sessions s ON s.id = b.session_id - JOIN devices d ON d.id = b.runtime_id - WHERE b.desired_state = 'bound' AND s.deleted_at IS NULL AND NOT d.agent_host - ) THEN - RAISE EXCEPTION 'Sessions are bound to a Runtime that is not an agent host; delete those Sessions, then upgrade'; + IF EXISTS (SELECT 1 FROM subagent_identities) + OR EXISTS (SELECT 1 FROM environment_file_writes) + OR EXISTS (SELECT 1 FROM runtime_allocations) + OR EXISTS (SELECT 1 FROM sandbox_enrollments) + OR EXISTS ( + SELECT 1 FROM session_runtime_assignments b JOIN sessions s ON s.id = b.session_id + WHERE s.deleted_at IS NULL OR b.desired_state <> 'released' OR b.applied_epoch <> b.epoch + ) THEN + RAISE EXCEPTION 'Cannot restore device constraints while Runtime history or cleanup is retained'; END IF; END $$; -- +goose StatementEnd - --- +goose Down DROP VIEW sandbox_resources; -DELETE FROM devices WHERE tenant_id IS NULL; +DELETE FROM session_runtime_assignments; +DELETE FROM devices; ALTER TABLE devices - DROP CONSTRAINT devices_agent_host, - ALTER COLUMN tenant_id SET NOT NULL, DROP COLUMN credential_revision, - DROP COLUMN agent_host; + ADD COLUMN tenant_id uuid NOT NULL, + ADD COLUMN environment_id uuid UNIQUE REFERENCES environments(id), + ADD COLUMN executor_key_id uuid REFERENCES environment_executor_credentials(key_id), + ADD COLUMN archive_cancel_turn_id uuid REFERENCES turns(id) ON DELETE SET NULL, + ALTER COLUMN credential_hash DROP NOT NULL, + ADD CONSTRAINT device_credential_source CHECK ( + (executor_key_id IS NULL AND credential_hash IS NOT NULL) + OR (executor_key_id IS NOT NULL AND credential_hash IS NULL AND environment_id IS NOT NULL) + ); +CREATE INDEX devices_tenant_idx ON devices (tenant_id); +ALTER TABLE subagent_identities ADD CONSTRAINT subagent_identities_device_id_fkey FOREIGN KEY (device_id) REFERENCES devices(id); +ALTER TABLE environment_file_writes ADD CONSTRAINT environment_file_writes_device_id_fkey FOREIGN KEY (device_id) REFERENCES devices(id); DROP TABLE sandbox_enrollments; ALTER TABLE runtime_allocations DROP COLUMN serve_generation, - DROP COLUMN serve_credential_hash; + DROP COLUMN serve_credential_hash, + ADD COLUMN device_id uuid NOT NULL UNIQUE REFERENCES devices(id); + +CREATE VIEW runtime_device_authority AS +SELECT d.id, d.tenant_id, d.name, d.environment_id, + COALESCE(c.token_sha256, d.credential_hash) AS credential_hash +FROM devices d +LEFT JOIN environments e ON e.id = d.environment_id +LEFT JOIN sessions s ON s.id = e.session_id AND s.tenant_id = d.tenant_id +LEFT JOIN environment_executor_credentials c ON c.key_id = d.executor_key_id +WHERE d.revoked_at IS NULL + AND (d.environment_id IS NULL OR (s.id IS NOT NULL AND s.deleted_at IS NULL)) + AND (d.executor_key_id IS NULL OR ( + c.revoked_at IS NULL AND c.tenant_id = s.tenant_id + AND c.subject_kind = s.creator_kind AND c.subject_id = s.creator_id + AND (c.environment_id IS NULL OR c.environment_id = e.id) + AND s.configuration->'environment'->>'type' = 'self_hosted' + AND e.status NOT IN ('failed', 'expired') + AND EXISTS (SELECT 1 FROM execution_project_scopes p WHERE p.tenant_id = c.tenant_id) + )); + diff --git a/services/core/tests/integration/admin_session_archive_test.go b/services/core/tests/integration/admin_session_archive_test.go index fce0d8ae3..52cd739fb 100644 --- a/services/core/tests/integration/admin_session_archive_test.go +++ b/services/core/tests/integration/admin_session_archive_test.go @@ -69,7 +69,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()), 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())) if err != nil { t.Fatal(err) } @@ -126,7 +126,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()), 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())); !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) { @@ -167,14 +167,11 @@ func TestManagedSessionArchiveRetainsHistoryAndSettledResources(t *testing.T) { if !reflect.DeepEqual(history, adminMutationSnapshot(t, s, "sessions", "turns", "session_items", "session_artifacts", "source_files", "pg_largeobject", "pg_largeobject_metadata")) { t.Fatal("archive changed persisted history or artifacts") } - if _, ok, err := sessionAdapter(s).GetDeviceCredential(t.Context(), owner.DeviceID); err != nil || ok { - t.Fatal("archive retained runtime authority", err) - } 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()), runtimedevice.HashCredential(uuid.NewString())) - if err != nil || !replay.Replayed || replay.ID != owner.ID || replay.DeviceID != owner.DeviceID || replay.State != "cleanup_pending" { + replay, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, installation, runtimedevice.HashCredential(uuid.NewString())) + if err != nil || !replay.Replayed || replay.ID != owner.ID || replay.State != "cleanup_pending" { t.Fatal("late provisioning retry replaced archived allocation", replay, err) } if _, err := deploymentExecution(t, w).RequestCleanup(t.Context(), owner); err != nil { @@ -245,10 +242,12 @@ func TestManagedSessionArchivePreservesFailuresAndRejectsSelfHosted(t *testing.T s, w, installation := managedArchiveFixture(t) tenant, session := managedArchiveSession(t, s, managerSessionInput(uuid.NewString())) owner := archiveAllocation(t, w, tenant, session, installation) + host := registerAgentHost(t, s) + assignSession(t, s, session.ID, host.ID) if _, err := s.pool.Exec(t.Context(), "UPDATE environments SET initialization='running' WHERE id=$1", owner.EnvironmentID); err != nil { t.Fatal(err) } - if err := sessionExecution(t, w.lease).FailEnvironmentInitialization(t.Context(), sessions.EnvironmentInitialization{EnvironmentID: owner.EnvironmentID, SessionID: owner.SessionID, TenantID: owner.TenantID, DeviceID: owner.DeviceID}, sessions.ProvisioningFailure{Step: sessions.ProvisioningSetupCommand, Index: 0, ExitCode: 2}); err != nil { + if err := sessionExecution(t, w.lease).FailEnvironmentInitialization(t.Context(), sessions.EnvironmentInitialization{EnvironmentID: owner.EnvironmentID, SessionID: owner.SessionID, TenantID: owner.TenantID, DeviceID: host.ID}, sessions.ProvisioningFailure{Step: sessions.ProvisioningSetupCommand, Index: 0, ExitCode: 2}); err != nil { t.Fatal(err) } failed, err := sessionAdapter(s).GetSession(t.Context(), tenant, session.ID) diff --git a/services/core/tests/integration/agent_host_migration_test.go b/services/core/tests/integration/agent_host_migration_test.go index e03f9ceb1..01ddeacb7 100644 --- a/services/core/tests/integration/agent_host_migration_test.go +++ b/services/core/tests/integration/agent_host_migration_test.go @@ -2,41 +2,176 @@ package integration import ( "database/sql" + "encoding/json" "os" "strings" "testing" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" + "github.com/google/uuid" "github.com/jackc/pgx/v5/stdlib" "github.com/pressly/goose/v3" ) -// TestAgentHostMigrationRefusesGuestBoundSessions upgrades a database where a -// Session is bound to a Runtime that is not an agent host: the upgrade refuses -// until that Session is deleted. -func TestAgentHostMigrationRefusesGuestBoundSessions(t *testing.T) { - s, pool := newManagedTestStore(t) - ctx := t.Context() - tenant, session := newTurnSession(t, s) - guest, _ := registerTestDevice(t, s, tenant) - if _, err := pool.Exec(ctx, `INSERT INTO session_runtime_assignments (session_id, runtime_id) VALUES ($1, $2)`, session.ID, guest.ID); err != nil { - t.Fatal(err) - } - db := sql.OpenDB(stdlib.GetConnector(*pool.Config().ConnConfig)) - t.Cleanup(func() { _ = db.Close() }) - provider, err := goose.NewProvider(goose.DialectPostgres, db, os.DirFS("../../migrations"), goose.WithTableName("agents_api_schema_version")) - if err != nil { - t.Fatal(err) - } - if _, err := provider.DownTo(ctx, 97); err != nil { - t.Fatal(err) - } - if _, err := provider.Up(ctx); err == nil || !strings.Contains(err.Error(), "delete those Sessions, then upgrade") { - t.Fatal("the upgrade kept a Session bound to a guest Runtime", err) - } - if _, err := pool.Exec(ctx, `UPDATE sessions SET deleted_at = clock_timestamp() WHERE id = $1`, session.ID); err != nil { - t.Fatal(err) - } - if _, err := provider.Up(ctx); err != nil { - t.Fatal(err) +func TestAgentHostMigration(t *testing.T) { + for _, scenario := range []string{"bound_assignment", "subagent_history", "pending_write", "live_allocation", "enrollment_authority", "settled_session"} { + t.Run(scenario, func(t *testing.T) { + s, pool := newManagedTestStore(t) + ctx := t.Context() + tenant, session := newTurnSession(t, s) + db := sql.OpenDB(stdlib.GetConnector(*pool.Config().ConnConfig)) + t.Cleanup(func() { _ = db.Close() }) + migrations, err := goose.NewProvider(goose.DialectPostgres, db, os.DirFS("../../migrations"), goose.WithTableName("agents_api_schema_version")) + if err != nil { + t.Fatal(err) + } + if _, err := migrations.DownTo(ctx, 97); err != nil { + t.Fatal(err) + } + exec := func(query string, args ...any) { + t.Helper() + if _, err := pool.Exec(ctx, query, args...); err != nil { + t.Fatal(err) + } + } + snapshot := func(table string) string { + t.Helper() + var value string + if err := pool.QueryRow(ctx, "SELECT COALESCE(jsonb_agg(to_jsonb(r) ORDER BY to_jsonb(r)::text), '[]'::jsonb)::text FROM "+table+" r").Scan(&value); err != nil { + t.Fatal(err) + } + return value + } + device, turn, environment := uuid.NewString(), uuid.NewString(), uuid.NewString() + exec(`INSERT INTO devices(id, tenant_id, name, credential_hash) VALUES($1,$2,'guest',$3)`, device, tenant, strings.Repeat("a", 64)) + exec(`INSERT INTO turns(id,session_id,status,started_at,completed_at) VALUES($1,$2,'completed',clock_timestamp(),clock_timestamp())`, turn, session.ID) + exec(`INSERT INTO session_runtime_assignments(session_id,runtime_id,native_session_id) VALUES($1,$2,'native-history')`, session.ID, device) + if scenario == "bound_assignment" { + before := snapshot("devices") + if _, err := migrations.Up(ctx); err == nil || !strings.Contains(err.Error(), "delete those Sessions, then upgrade") { + t.Fatal("upgrade accepted a bound Session", err) + } + if snapshot("devices") != before { + t.Fatal("refused upgrade changed devices") + } + } + exec(`UPDATE sessions SET deleted_at=clock_timestamp() WHERE id=$1`, session.ID) + if scenario == "subagent_history" || scenario == "pending_write" || scenario == "live_allocation" { + exec(`INSERT INTO environments(id,session_id) VALUES($1,$2)`, environment, session.ID) + if scenario == "subagent_history" { + exec(`INSERT INTO turn_events(session_id,turn_id,ordinal,kind,payload) VALUES($1,$2,1,'subagent','{}')`, session.ID, turn) + exec(`INSERT INTO subagent_identities(id,session_id,device_id,engine,native_id,parent_native_id,native_created_at,first_turn_id,first_event_ordinal) VALUES($1,$2,$3,'codex','child','root',1,$4,1)`, uuid.NewString(), session.ID, device, turn) + } + if scenario == "pending_write" { + exec(`INSERT INTO environment_file_writes(id,environment_id,device_id,request_sha256) VALUES($1,$2,$3,$4)`, uuid.NewString(), environment, device, strings.Repeat("b", 64)) + } + if scenario == "live_allocation" { + // The allocation still owns a real resource, even after Session deletion. + exec(`INSERT INTO runtime_allocations(id,environment_id,device_id,provider_key,deployment_generation) VALUES($1,$2,$3,$4,0)`, uuid.NewString(), environment, device, uuid.NewString()) + } + } + histories := map[string]string{} + for _, table := range []string{"sessions", "turns", "turn_events", "subagent_identities", "environment_file_writes"} { + histories[table] = snapshot(table) + } + if _, err := migrations.Up(ctx); err != nil { + t.Fatal(err) + } + for table, before := range histories { + if snapshot(table) != before { + t.Fatalf("upgrade changed %s", table) + } + } + if snapshot("devices") != "[]" || snapshot("session_runtime_assignments") != "[]" { + t.Fatal("guest execution authority survived upgrade") + } + if scenario == "enrollment_authority" { + principal := FixtureExecutorPrincipal(t, s, uuid.NewString()) + selfHosted, err := s.CreateSession(ctx, principal.TenantID, sessions.CreateSession{ + Creator: principal.Subject(), Engine: "codex", IdempotencyKey: uuid.NewString(), + Configuration: json.RawMessage(`{"agent":{"model":"fixture"},"environment":{"type":"self_hosted","workspace_directory":"/workspace"}}`), + }) + if err != nil { + t.Fatal(err) + } + service := sessionService(t, s) + first, err := service.IssueExecutorCredential(ctx, principal, uuid.NewString(), selfHosted.Environment.ID) + if err != nil { + t.Fatal(err) + } + resource, err := service.EnrollRuntime(ctx, selfHosted.Environment.ID, executorDigest(first.Token)) + if err != nil { + t.Fatal(err) + } + before := snapshot("sandbox_enrollments") + if _, err := migrations.DownTo(ctx, 97); err == nil || !strings.Contains(err.Error(), "Cannot restore device constraints") { + t.Fatal("rollback discarded enrollment authority", err) + } + if snapshot("sandbox_enrollments") != before || snapshot("session_runtime_assignments") != "[]" || snapshot("runtime_allocations") != "[]" { + t.Fatal("refused rollback changed unbound enrollment") + } + var columns int + if err := pool.QueryRow(ctx, `SELECT count(*) FROM information_schema.columns WHERE table_schema=current_schema() AND table_name='devices' AND column_name='credential_revision'`).Scan(&columns); err != nil || columns != 1 { + t.Fatal("refused rollback changed schema", columns, err) + } + if again, err := service.EnrollRuntime(ctx, selfHosted.Environment.ID, executorDigest(first.Token)); err != nil || again != resource { + t.Fatal("rollback changed enrolled identity", again, err) + } + return + } + if scenario == "subagent_history" || scenario == "pending_write" || scenario == "live_allocation" { + if scenario == "live_allocation" { + var count int + if err := pool.QueryRow(ctx, `SELECT count(*) FROM runtime_allocations WHERE state='creating' AND NOT create_settled`).Scan(&count); err != nil || count != 1 { + t.Fatal("upgrade discarded Provider cleanup", count, err) + } + } + before := snapshot("runtime_allocations") + if _, err := migrations.DownTo(ctx, 97); err == nil || !strings.Contains(err.Error(), "Cannot restore device constraints") { + t.Fatal("rollback discarded history", err) + } + for table, original := range histories { + if snapshot(table) != original { + t.Fatalf("refused rollback changed %s", table) + } + } + if snapshot("runtime_allocations") != before { + t.Fatal("refused rollback changed allocation") + } + var columns int + if err := pool.QueryRow(ctx, `SELECT count(*) FROM information_schema.columns WHERE table_schema=current_schema() AND table_name='devices' AND column_name='credential_revision'`).Scan(&columns); err != nil || columns != 1 { + t.Fatal("refused rollback changed schema", columns, err) + } + return + } + // A host has run the Session and acknowledged its deletion. The root Turn + // remains history, while the settled execution association can be removed. + host := uuid.NewString() + exec(`INSERT INTO devices(id,name,credential_hash) VALUES($1,'host',$2)`, host, strings.Repeat("c", 64)) + exec(`INSERT INTO session_runtime_assignments(session_id,runtime_id,native_session_id,desired_state,remove_home,epoch,applied_epoch) VALUES($1,$2,'native-host-history','released',true,2,2)`, session.ID, host) + if scenario == "settled_session" { + exec(`UPDATE session_runtime_assignments SET applied_epoch=1 WHERE session_id=$1`, session.ID) + before := snapshot("session_runtime_assignments") + hosts := snapshot("devices") + if _, err := migrations.DownTo(ctx, 97); err == nil || !strings.Contains(err.Error(), "Cannot restore device constraints") { + t.Fatal("rollback discarded pending home cleanup", err) + } + if snapshot("session_runtime_assignments") != before || snapshot("devices") != hosts { + t.Fatal("refused rollback changed pending cleanup") + } + exec(`UPDATE session_runtime_assignments SET applied_epoch=epoch WHERE session_id=$1`, session.ID) + } + if _, err := migrations.DownTo(ctx, 97); err != nil { + t.Fatal("settled Session prevented rollback", err) + } + if _, err := migrations.Up(ctx); err != nil { + t.Fatal("second upgrade", err) + } + for table, before := range histories { + if snapshot(table) != before { + t.Fatalf("round trip changed %s", table) + } + } + }) } } diff --git a/services/core/tests/integration/archive_cancellation_cleanup_test.go b/services/core/tests/integration/archive_cancellation_cleanup_test.go new file mode 100644 index 000000000..a7ff9f6ed --- /dev/null +++ b/services/core/tests/integration/archive_cancellation_cleanup_test.go @@ -0,0 +1,129 @@ +package integration + +import ( + "errors" + "testing" + "time" + + "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sandbox" + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" + "github.com/google/uuid" +) + +func TestArchiveCancellationThenDeleteRemovesHome(t *testing.T) { + h := newDispatchHarnessForSession(t, []byte(hostedLinkSession)) + h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{HomeRemoval: proto.CapabilitySupported, SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: workerEnvironmentCapabilities()}}}) + if _, err := h.s.pool.Exec(t.Context(), "INSERT INTO execution_project_scopes(tenant_id,organization_id,project_id) VALUES($1,'archive-cleanup',$2)", h.tenant, h.tenant); err != nil { + t.Fatal(err) + } + if _, err := h.s.pool.Exec(t.Context(), "INSERT INTO projects(id,name,tenant_id,subject_kind,subject_id) VALUES($1,'Archive cleanup',$1,'service_account',$2)", h.tenant, "project:"+h.tenant); err != nil { + t.Fatal(err) + } + setup, err := deploymentService(t, h.s).Setup(t.Context()) + if err != nil { + t.Fatal(err) + } + provider := &lifecycleProvider{resources: map[string]sandbox.Info{h.resource.ID: { + Reference: sandbox.Reference{TenantID: h.tenant, EnvironmentID: h.resource.EnvironmentID, AllocationID: h.resource.ID}, + ProviderID: h.resource.ID, State: "running", BootstrapComplete: true, CreateSettled: true, + }}} + h.d.ManagedRuntimes = webRuntimes(t, h.s, setup.InstallationID, provider, nil) + owner := h.owner() + worker := startOwnedWorker(t, t.Context(), h.s, h.d, owner) + runWorker(t, worker) + if _, err := sessionService(t, h.s).ReserveEnvironmentInput(t.Context(), h.tenant, h.session.ID, "active", []sessions.Input{messageInput("run")}); err != nil { + t.Fatal(err) + } + frame := h.read(proto.TypeExecutionPrepare) + handle := acknowledgePreparation(h, frame.ID) + h.write(frame.ID, proto.TypePreparationStatus, proto.PreparationStatusPayload{Handle: handle, Revision: 2, State: "ready"}) + started := h.read(proto.TypeExecutionStart) + var start proto.ExecutionStartPayload + if started.DecodePayload(&start) != nil || start.RunID == "" { + t.Fatal("missing active Turn") + } + h.write(frame.ID, proto.TypePreparationStatus, proto.PreparationStatusPayload{Handle: handle, Revision: 3, State: "started", RunID: start.RunID}) + command := sessions.DeleteSessionCommand{TenantID: h.tenant, SessionID: h.session.ID} + if err := sessionService(t, h.s).DeleteSession(t.Context(), command); !errors.Is(err, sessions.ErrNotIdle) { + t.Fatal("active Delete did not refuse", err) + } + if _, err := owner.Deployment.ArchiveSession(adminDeleteContext(t.Context(), h.tenant, uuid.NewString()), h.tenant, h.session.ID, 1); err != nil { + t.Fatal(err) + } + allocation, err := deploymentStore(h.s).EnvironmentAllocation(t.Context(), deployment.AllocationKey{TenantID: h.tenant, EnvironmentID: h.session.Environment.ID}) + if err != nil || (allocation.State != "cleanup_pending" && allocation.State != "released") { + t.Fatal("archive lost cleanup ownership", allocation, err) + } + // Provider cleanup and host cancellation proceed independently. Only the host's + // receipt can settle the Turn, even after the sandbox stops Serving. + if err := worker.ReconcileManagedRuntimes(t.Context()); err != nil { + t.Fatal(err) + } + provider.mu.Lock() + killed := provider.kills > 0 + provider.mu.Unlock() + if !killed { + t.Fatal("provider cleanup did not proceed before cancellation receipt") + } + h.stopServing() + var cancellation proto.PromptCancelPayload + var release proto.Envelope + for cancellation.DeliveryID == "" || release.Type == "" { + var incoming proto.Envelope + _ = h.conn.SetReadDeadline(time.Now().Add(10 * time.Second)) + if err := h.conn.ReadJSON(&incoming); err != nil { + t.Fatal(err) + } + h.observe(incoming) + switch incoming.Type { + case proto.TypePromptCancel: + if incoming.DecodePayload(&cancellation) != nil || cancellation.DeliveryID == "" { + t.Fatal("cancel lost receipt identity") + } + case proto.TypeAssignmentRelease: + var request proto.AssignmentReleasePayload + if incoming.DecodePayload(&request) != nil || request.RemoveHome { + t.Fatal("Archive removed the home", request) + } + release = incoming + } + } + turn, err := sessionAdapter(h.s).GetTurn(t.Context(), h.tenant, h.session.ID, start.RunID) + if err != nil || turn.Status != sessions.TurnInProgress || turn.CancelRequestedAt.IsZero() { + t.Fatal("archive fabricated cancellation settlement", turn, err) + } + if err := sessionService(t, h.s).DeleteSession(t.Context(), command); !errors.Is(err, sessions.ErrNotIdle) { + t.Fatal("Delete accepted cancellation without its receipt", err) + } + h.write(start.RunID, proto.TypeInteractionDecisionAck, proto.InteractionDecisionAckPayload{DeliveryID: cancellation.DeliveryID, Applied: true, Outcome: &proto.DonePayload{}}) + reply, err := release.Reply(proto.TypeAssignmentStatus, proto.AssignmentStatusPayload{State: proto.AssignmentReleased}) + if err != nil || h.conn.WriteJSON(reply) != nil { + t.Fatal("cannot acknowledge Archive release", err) + } + awaitDaemonRemoteCondition(t, t.Context(), 5*time.Second, "cancellation receipt settled", func() bool { + turn, err := sessionAdapter(h.s).GetTurn(t.Context(), h.tenant, h.session.ID, start.RunID) + return err == nil && turn.Status == sessions.TurnCancelled + }) + if err := sessionService(t, h.s).DeleteSession(t.Context(), command); err != nil { + t.Fatal(err) + } + deleted := h.read(proto.TypeAssignmentRelease) + var request proto.AssignmentReleasePayload + if deleted.DecodePayload(&request) != nil || !request.RemoveHome || deleted.Assignment.Epoch <= release.Assignment.Epoch { + t.Fatal("idle Delete did not request home removal", deleted.Assignment, request) + } + reply, err = deleted.Reply(proto.TypeAssignmentStatus, proto.AssignmentStatusPayload{State: proto.AssignmentHomeRemoved}) + if err != nil || h.conn.WriteJSON(reply) != nil { + t.Fatal("cannot acknowledge home removal", err) + } + awaitDaemonRemoteCondition(t, t.Context(), 5*time.Second, "home removal acknowledged", func() bool { + var applied int64 + err := h.s.pool.QueryRow(t.Context(), "SELECT applied_epoch FROM session_runtime_assignments WHERE session_id=$1", h.session.ID).Scan(&applied) + return err == nil && applied == int64(deleted.Assignment.Epoch) + }) + if _, ok, err := sessionAdapter(h.s).GetDeviceCredential(t.Context(), h.device.ID); err != nil || !ok { + t.Fatal("Session cleanup revoked the shared host", err) + } +} diff --git a/services/core/tests/integration/archived_cancellation_migration_test.go b/services/core/tests/integration/archived_cancellation_migration_test.go index 705a695d6..27991b96e 100644 --- a/services/core/tests/integration/archived_cancellation_migration_test.go +++ b/services/core/tests/integration/archived_cancellation_migration_test.go @@ -1,57 +1,49 @@ package integration import ( - "database/sql" - "os" "strings" "testing" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" "github.com/google/uuid" - "github.com/jackc/pgx/v5/stdlib" - "github.com/pressly/goose/v3" ) func TestArchivedCancellationMigrationDoesNotAdoptOldRevocations(t *testing.T) { - s, w, installation := managedArchiveFixture(t) - tenant, session := managedArchiveSession(t, s, managerSessionInput(uuid.NewString())) - owner := archiveAllocation(t, w, tenant, session, installation) - input := submitMessage(t, s, tenant, session.ID, "waiting") - transition(t, w, tenant, session.ID, input.TurnID, sessions.TurnQueued, sessions.TurnInProgress) - transition(t, w, tenant, session.ID, input.TurnID, sessions.TurnInProgress, sessions.TurnWaiting) - if _, err := deploymentExecution(t, w).ArchiveSession(adminDeleteContext(t.Context(), tenant, uuid.NewString()), tenant, session.ID, 1); err != nil { + db, provider := runtimeNamesMigrationSchema(t) + ctx := t.Context() + if _, err := provider.UpTo(ctx, 79); err != nil { t.Fatal(err) } - db := sql.OpenDB(stdlib.GetConnector(*s.pool.Config().ConnConfig)) - defer db.Close() - provider, err := goose.NewProvider(goose.DialectPostgres, db, os.DirFS("../../migrations"), goose.WithTableName("agents_api_schema_version")) - if err != nil { + exec := func(query string, args ...any) { + t.Helper() + if _, err := db.ExecContext(ctx, query, args...); err != nil { + t.Fatal(err) + } + } + tenant, session, environment, device, allocation, turn := uuid.NewString(), uuid.NewString(), uuid.NewString(), uuid.NewString(), uuid.NewString(), uuid.NewString() + exec(`INSERT INTO sessions(id,tenant_id,engine,idempotency_key,request_hash,configuration) VALUES($1,$2,'codex','archive','archive','{}')`, session, tenant) + exec(`INSERT INTO environments(id,session_id) VALUES($1,$2)`, environment, session) + exec(`INSERT INTO devices(id,tenant_id,name,credential_hash,revoked_at) VALUES($1,$2,'revoked',repeat('a',64),clock_timestamp())`, device, tenant) + exec(`INSERT INTO runtime_allocations(id,environment_id,device_id,provider_key) VALUES($1,$2,$3,$4)`, allocation, environment, device, uuid.NewString()) + exec(`INSERT INTO turns(id,session_id,status) VALUES($1,$2,'in_progress')`, turn, session) + if _, err := provider.UpTo(ctx, 80); err != nil { t.Fatal(err) } - if _, err := provider.DownTo(t.Context(), 79); err == nil { - t.Fatal("downgrade discarded unsettled cancellation marker") - } else if !strings.Contains(err.Error(), "Cannot remove archived cancellation receipts while cleanup is unsettled") { - t.Fatal("downgrade failed before checking the cancellation marker", err) - } - // Later migrations can already have been reverted before migration 80 - // refuses. Restore the current schema before using current generated queries. - if _, err := provider.Up(t.Context()); err != nil { - t.Fatal(err) - } - if _, err := deploymentExecution(t, w).SettleCreation(t.Context(), owner); err != nil { - t.Fatal(err) + var marker *string + if err := db.QueryRowContext(ctx, "SELECT archive_cancel_turn_id::text FROM devices WHERE id=$1", device).Scan(&marker); err != nil || marker != nil { + t.Fatal("upgrade adopted historical revoked device", marker, err) } - if _, err := deploymentExecution(t, w).ReleaseAllocation(t.Context(), owner); err != nil { - t.Fatal(err) + exec(`UPDATE devices SET archive_cancel_turn_id=$2 WHERE id=$1`, device, turn) + if _, err := provider.DownTo(ctx, 79); err == nil || !strings.Contains(err.Error(), "Cannot remove archived cancellation receipts while cleanup is unsettled") { + t.Fatal("downgrade discarded unsettled cancellation marker", err) } - if _, err := provider.DownTo(t.Context(), 79); err != nil { + exec(`UPDATE runtime_allocations SET state='released',released_at=clock_timestamp(),create_settled=true WHERE id=$1`, allocation) + if _, err := provider.DownTo(ctx, 79); err != nil { t.Fatal(err) } - if _, err := provider.Up(t.Context()); err != nil { + if _, err := provider.UpTo(ctx, 80); err != nil { t.Fatal(err) } - var marker *string - if err := s.pool.QueryRow(t.Context(), "SELECT archive_cancel_turn_id::text FROM devices WHERE id=$1", owner.DeviceID).Scan(&marker); err != nil || marker != nil { - t.Fatal("upgrade adopted historical revoked device", marker, err) + if err := db.QueryRowContext(ctx, "SELECT archive_cancel_turn_id::text FROM devices WHERE id=$1", device).Scan(&marker); err != nil || marker != nil { + t.Fatal("re-upgrade adopted historical revoked device", marker, err) } } diff --git a/services/core/tests/integration/assignment_release_test.go b/services/core/tests/integration/assignment_release_test.go index f273ccda4..6d38bfeb1 100644 --- a/services/core/tests/integration/assignment_release_test.go +++ b/services/core/tests/integration/assignment_release_test.go @@ -11,9 +11,6 @@ import ( "github.com/gorilla/websocket" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/sessionpg" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) @@ -68,123 +65,20 @@ func TestDeletionReleaseReachesReconnectedRuntime(t *testing.T) { awaitDaemonRemoteCondition(t, t.Context(), 3*time.Second, "release acknowledged", func() bool { return applied() == 2 }) } -// TestRevocationSettlesUndeliverableReleases checks that a release whose -// Runtime was revoked is settled with the revocation instead of staying -// pending: no Runtime is left to act on it. -func TestRevocationSettlesUndeliverableReleases(t *testing.T) { +// A revoked host cannot acknowledge a new release, so deletion settles it. +func TestDeletionSettlesReleaseToRevokedHost(t *testing.T) { h := newDispatchHarness(t) - ctx := t.Context() - second, err := h.s.CreateSession(ctx, h.tenant, WithFixtureModelProvider(sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "second", Configuration: []byte(`{"agent":{"model":"test-model"},"environment":{"type":"none"}}`)})) - if err != nil { - t.Fatal(err) - } - if err := bindSessionDevice(t, h.s, h.tenant, second.ID, h.device.ID); err != nil { - t.Fatal(err) - } - service := sessionService(t, h.s) - settled := func(session string) bool { - var epoch, applied int64 - if err := h.s.pool.QueryRow(ctx, "SELECT epoch, applied_epoch FROM session_runtime_assignments WHERE session_id=$1", session).Scan(&epoch, &applied); err != nil { - t.Fatal(err) - } - return epoch == 2 && applied == 2 - } - if err := service.DeleteSession(ctx, sessions.DeleteSessionCommand{TenantID: h.tenant, SessionID: h.session.ID}); err != nil { + if _, err := h.s.pool.Exec(t.Context(), "UPDATE devices SET revoked_at=clock_timestamp() WHERE id=$1", h.device.ID); err != nil { t.Fatal(err) } - if settled(h.session.ID) { - t.Fatal("a release to an authorized Runtime was settled before delivery") - } - if err := service.RevokeDevice(ctx, h.tenant, h.device.ID); err != nil { + if err := sessionService(t, h.s).DeleteSession(t.Context(), sessions.DeleteSessionCommand{TenantID: h.tenant, SessionID: h.session.ID}); err != nil { t.Fatal(err) } - if !settled(h.session.ID) { - t.Fatal("revocation left the Runtime's release pending") - } - if err := service.DeleteSession(ctx, sessions.DeleteSessionCommand{TenantID: h.tenant, SessionID: second.ID}); err != nil { + var epoch, applied int64 + if err := h.s.pool.QueryRow(t.Context(), "SELECT epoch, applied_epoch FROM session_runtime_assignments WHERE session_id=$1", h.session.ID).Scan(&epoch, &applied); err != nil { t.Fatal(err) } - if !settled(second.ID) { - t.Fatal("a release to a revoked Runtime was left pending") - } -} - -// TestReleaseRacingRevocationIsSettled checks that a release racing the -// revocation of its shared Runtime is settled whichever reaches the device row -// first: the revocation settles the releases it sees, and a release sees the -// revocation. -func TestReleaseRacingRevocationIsSettled(t *testing.T) { - for _, first := range []string{"revocation", "release"} { - t.Run(first, func(t *testing.T) { - h := newDispatchHarness(t) - ctx := t.Context() - h.conn.Close() - awaitDaemonRemoteCondition(t, ctx, 3*time.Second, "Runtime disconnected", func() bool { - _, err := h.registry.LookupDevice(h.device.ID) - return err != nil - }) - // waiting reports whether the named query waits for a lock. - waiting := func(query string) bool { - var n int - if err := h.s.pool.QueryRow(ctx, "SELECT count(*) FROM pg_stat_activity WHERE datname = current_database() AND wait_event_type = 'Lock' AND query LIKE '-- name: ' || $1 || ' %'", query).Scan(&n); err != nil { - t.Fatal(err) - } - return n == 1 - } - service := sessionService(t, h.s) - revoked := make(chan error, 1) - tx, err := h.s.pool.Begin(ctx) - if err != nil { - t.Fatal(err) - } - defer tx.Rollback(context.Background()) - if first == "revocation" { - // The open transaction holds the device row, so the revocation - // starts first and the deletion queues behind it. - if _, err := tx.Exec(ctx, "SELECT 1 FROM devices WHERE id = $1 FOR NO KEY UPDATE", h.device.ID); err != nil { - t.Fatal(err) - } - go func() { revoked <- service.RevokeDevice(ctx, h.tenant, h.device.ID) }() - awaitDaemonRemoteCondition(t, ctx, 3*time.Second, "revocation waits for the device", func() bool { return waiting("RevokeDevice") }) - deleted := make(chan error, 1) - go func() { - deleted <- service.DeleteSession(ctx, sessions.DeleteSessionCommand{TenantID: h.tenant, SessionID: h.session.ID}) - }() - awaitDaemonRemoteCondition(t, ctx, 3*time.Second, "deletion ends or waits for the device", func() bool { - return len(deleted) == 1 || waiting("LockAssignmentRuntime") - }) - if err := tx.Rollback(ctx); err != nil { - t.Fatal(err) - } - if err := <-deleted; err != nil { - t.Fatal(err) - } - } else { - // The open transaction records the release, and the revocation - // runs before it commits. - tenant, _ := pgunit.ParseID(h.tenant) - session, _ := pgunit.ParseID(h.session.ID) - if err := sessionpg.BindSession(sqlc.New(tx), tenant, session).ReleaseAssignment(ctx, true); err != nil { - t.Fatal(err) - } - go func() { revoked <- service.RevokeDevice(ctx, h.tenant, h.device.ID) }() - awaitDaemonRemoteCondition(t, ctx, 3*time.Second, "revocation ends or waits for the device", func() bool { - return len(revoked) == 1 || waiting("RevokeDevice") - }) - if err := tx.Commit(ctx); err != nil { - t.Fatal(err) - } - } - if err := <-revoked; err != nil { - t.Fatal(err) - } - var epoch, applied int64 - if err := h.s.pool.QueryRow(ctx, "SELECT epoch, applied_epoch FROM session_runtime_assignments WHERE session_id=$1", h.session.ID).Scan(&epoch, &applied); err != nil { - t.Fatal(err) - } - if epoch != 2 || applied != 2 { - t.Fatalf("epoch %d, applied %d: the release racing the revocation stayed pending", epoch, applied) - } - }) + if epoch != 2 || applied != 2 { + t.Fatalf("release to revoked host remains pending: epoch=%d applied=%d", epoch, applied) } } diff --git a/services/core/tests/integration/device_bootstrap_binding_test.go b/services/core/tests/integration/device_bootstrap_binding_test.go deleted file mode 100644 index 0cc56f73d..000000000 --- a/services/core/tests/integration/device_bootstrap_binding_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package integration - -import ( - "errors" - "strings" - "testing" - - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/deployment" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" - "github.com/google/uuid" -) - -func TestDeviceCredentialCarriesPersistedAllocationNode(t *testing.T) { - s, writer, d := managerFixture(t, 4, 8) - nodes := deploymentService(t, s) - token, err := EnrollmentTestToken(nodes.CreateEnrollment(t.Context(), deployment.Capacity{MaxActive: 4, MaxRetained: 8})) - if err != nil { - t.Fatal(err) - } - remote := uuid.NewString() - _, err = nodes.Enroll(t.Context(), token, deployment.Enrollment{DeploymentGeneration: 1, SpecificationDigest: SandboxDeploymentTestSpec("docker").Digest("docker"), NodeID: remote, Credential: strings.Repeat("x", 64), - Name: "remote", Provider: "docker", BackendFingerprint: strings.Repeat("b", 64), CoreURL: s.placement.PublicURL()}) - if err != nil { - t.Fatal(err) - } - onlineManagerNode(t, s, remote) - for _, nodeID := range []string{d.NodeID, remote} { - t.Run(nodeID, func(t *testing.T) { - tenant, bearer := uuid.NewString(), uuid.NewString() - session, err := createSessionOnNode(t, s, tenant, managerSessionInput(uuid.NewString()), nodeID) - if err != nil { - t.Fatal(err) - } - environment, err := sessionAdapter(s).GetSessionEnvironment(t.Context(), tenant, session.ID) - 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), runtimedevice.HashCredential(bearer)) - if err != nil { - t.Fatal(err) - } - authenticator := runtimegateway.NewAuthenticator(sessionAdapter(s)) - auth, err := authenticator.AuthenticateBearer(t.Context(), allocation.DeviceID, bearer) - if err != nil || auth.RuntimeNodeID != nodeID { - t.Fatalf("authenticated node=%s want=%s error=%v", auth.RuntimeNodeID, nodeID, err) - } - if _, err := authenticator.AuthenticateBearer(t.Context(), allocation.DeviceID, "wrong-token"); !errors.Is(err, runtimegateway.ErrAuthBadCredential) { - t.Fatal("binding bypassed credential check", err) - } - if err := sessionService(t, s).RevokeDevice(t.Context(), tenant, allocation.DeviceID); err != nil { - t.Fatal(err) - } - if _, err := authenticator.AuthenticateBearer(t.Context(), allocation.DeviceID, bearer); !errors.Is(err, runtimegateway.ErrAuthUnknownDevice) { - t.Fatal("allocation revived revoked credential", err) - } - }) - } -} - -func TestDeviceCredentialWithoutManagedNodeRetainsPublicRouteIdentity(t *testing.T) { - s, installation := configuredStore(t) - tenant := uuid.NewString() - ordinary, err := sessionService(t, s).CreateDevice(t.Context(), tenant, "ordinary", runtimedevice.HashCredential("ordinary-token")) - if err != nil { - 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}, installation, runtimedevice.HashCredential("allocation-token"), runtimedevice.HashCredential("allocation-token")) - if err != nil { - t.Fatal(err) - } - for _, id := range []string{ordinary.ID, allocation.DeviceID} { - credential, found, err := sessionAdapter(s).GetDeviceCredential(t.Context(), id) - if err != nil || !found || credential.RuntimeNodeID != "" { - t.Fatalf("non-node credential acquired allocation route: found=%v node=%s error=%v", found, credential.RuntimeNodeID, err) - } - } -} diff --git a/services/core/tests/integration/devices_test.go b/services/core/tests/integration/devices_test.go index 80c7e31f1..fc4f498a1 100644 --- a/services/core/tests/integration/devices_test.go +++ b/services/core/tests/integration/devices_test.go @@ -12,35 +12,22 @@ import ( "testing" "time" - "github.com/google/uuid" "github.com/gorilla/websocket" "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/sessionpg" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtime" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimegateway" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) -func registerTestDevice(t *testing.T, s *Store, tenant string) (sessions.ExecutionDevice, string) { - t.Helper() - secret := uuid.NewString() + uuid.NewString() - d, err := sessionService(t, s).CreateDevice(context.Background(), tenant, "isolated executor", runtimedevice.HashCredential(secret)) - if err != nil { - t.Fatal(err) - } - return d, secret -} - func TestDeviceBindingIsTenantScopedStableAndDurable(t *testing.T) { s, pool := testStore(t) ctx := context.Background() tenant, session := newTurnSession(t, s) otherTenant, otherSession := newTurnSession(t, s) - a, b := registerAgentHost(t, s, tenant), registerAgentHost(t, s, tenant) - ordinary, _ := registerTestDevice(t, s, tenant) + a, b := registerAgentHost(t, s), registerAgentHost(t, s) // The binds run on an execution lease of their own, which closes before // the pool does. lease, err := pgunit.AcquireLease(ctx, pool) @@ -52,8 +39,8 @@ func TestDeviceBindingIsTenantScopedStableAndDurable(t *testing.T) { if err != nil { t.Fatal(err) } - // Sessions bind only agent hosts, and only within their own tenant. - for _, args := range [][3]string{{tenant, session.ID, ordinary.ID}, {otherTenant, session.ID, a.ID}, {tenant, otherSession.ID, a.ID}} { + // Session ownership remains tenant-scoped even though hosts are deployment-wide. + for _, args := range [][3]string{{otherTenant, session.ID, a.ID}, {tenant, otherSession.ID, a.ID}} { if err := execution.BindSessionDevice(ctx, args[0], args[1], args[2]); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("foreign binding: %v", err) } @@ -94,6 +81,9 @@ func TestDeviceBindingIsTenantScopedStableAndDurable(t *testing.T) { if _, err := sessionAdapter(s).GetSessionDevice(ctx, otherTenant, session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("foreign lookup: %v", err) } + if err := execution.BindSessionDevice(ctx, otherTenant, otherSession.ID, a.ID); err != nil { + t.Fatal("deployment host could not serve another tenant", err) + } if err := lease.Close(ctx); err != nil { t.Fatal(err) } @@ -114,12 +104,12 @@ func TestDeviceBindingIsTenantScopedStableAndDurable(t *testing.T) { func TestStandaloneGatewayUsesExecutionCredentials(t *testing.T) { s, _ := testStore(t) ctx := context.Background() - tenant, _ := newTurnSession(t, s) - a, secret := registerTestDevice(t, s, tenant) - _, foreignSecret := registerTestDevice(t, s, uuid.NewString()) + a := registerAgentHost(t, s) + secret := a.Credential + foreignSecret := registerAgentHost(t, s).Credential server := httptest.NewUnstartedServer(nil) wsURL := "ws://" + server.Listener.Addr().String() + "/api/v1/agent-daemon/ws" - handler, registry, err := runtime.NewGateway(sessionAdapter(s), sessionService(t, s), sessionAdapter(s), runtimegateway.NewLinkAuthority(sessionAdapter(s)), wsURL) + handler, registry, err := runtime.NewGateway(sessionAdapter(s), sessionService(t, s), runtimegateway.NewLinkAuthority(sessionAdapter(s)), wsURL) if err != nil { t.Fatal(err) } @@ -179,7 +169,7 @@ func TestStandaloneGatewayUsesExecutionCredentials(t *testing.T) { if err != nil || current == previous || current.IsClosed() { t.Fatalf("replacement connection missing: %v", err) } - if err := sessionService(t, s).RevokeDevice(ctx, tenant, a.ID); err != nil { + if _, err := s.pool.Exec(ctx, "UPDATE devices SET revoked_at=clock_timestamp() WHERE id=$1", a.ID); err != nil { t.Fatal(err) } if err := second.WriteJSON(map[string]any{"type": proto.TypeHeartbeat, "payload": map[string]any{"version": "test", "home_removal": false}}); err != nil { diff --git a/services/core/tests/integration/dispatch_test.go b/services/core/tests/integration/dispatch_test.go index a17be50cd..c9eb6eca6 100644 --- a/services/core/tests/integration/dispatch_test.go +++ b/services/core/tests/integration/dispatch_test.go @@ -67,9 +67,11 @@ func newDispatchHarnessForSession(t *testing.T, configuration []byte) *dispatchH } `json:"environment"` } _ = json.Unmarshal(configuration, &snapshot) - s, _ := testStore(t) + var s *Store if snapshot.Environment.Type == "openai_hosted" { s, _ = configuredStore(t) + } else { + s, _ = newManagedTestStore(t) } h := &dispatchHarness{t: t, s: s, tenant: uuid.NewString(), environments: map[string]*dispatchHarness{}} ctx := context.Background() @@ -78,7 +80,7 @@ func newDispatchHarnessForSession(t *testing.T, configuration []byte) *dispatchH if err != nil { t.Fatal(err) } - host := registerAgentHost(t, s, h.tenant) + host := registerAgentHost(t, s) h.device, h.credential = sessions.ExecutionDevice{ID: host.ID, Name: "agent host"}, host.Credential secret := h.credential h.link = sandboxlinktest.StartRelay(t, runtimegateway.NewLinkAuthority(sessionAdapter(s))) @@ -92,7 +94,7 @@ func newDispatchHarnessForSession(t *testing.T, configuration []byte) *dispatchH } server := httptest.NewUnstartedServer(nil) wsURL := "ws://" + server.Listener.Addr().String() + "/api/v1/agent-daemon/ws" - server.Config.Handler, h.registry, err = runtime.NewGateway(sessionAdapter(s), sessionService(t, s), sessionAdapter(s), runtimegateway.NewLinkAuthority(sessionAdapter(s)), wsURL) + server.Config.Handler, h.registry, err = runtime.NewGateway(sessionAdapter(s), sessionService(t, s), runtimegateway.NewLinkAuthority(sessionAdapter(s)), wsURL) if err != nil { t.Fatal(err) } @@ -286,7 +288,7 @@ func TestExecutionDispatchSteeringAndNativeContinuity(t *testing.T) { t.Fatal(err) } awaitRelease() - newStore, _ := testStore(t) + newStore := reopenStore(t, h.s) h.s, h.owned = newStore, nil bound, err := sessionAdapter(newStore).GetSessionExecutionBinding(ctx, h.tenant, h.session.ID) if err != nil || bound.NativeSessionID != "native-thread-1" { diff --git a/services/core/tests/integration/environment_admission_test.go b/services/core/tests/integration/environment_admission_test.go index 6a176135f..48ad86aff 100644 --- a/services/core/tests/integration/environment_admission_test.go +++ b/services/core/tests/integration/environment_admission_test.go @@ -67,7 +67,7 @@ func awaitEnvironmentAdmission(t *testing.T, result <-chan environmentAdmissionR func environmentAdmissionPending(t *testing.T, h *dispatchHarness, key string) sessions.EnvironmentInputReservation { t.Helper() - _, pool := testStore(t) + pool := h.s.pool var id string awaitDaemonRemoteCondition(t, t.Context(), 3*time.Second, "input reservation", func() bool { return pool.QueryRow(t.Context(), "SELECT id::text FROM environment_input_reservations WHERE session_id=$1 AND idempotency_key=$2", h.session.ID, key).Scan(&id) == nil @@ -171,7 +171,7 @@ func TestEnvironmentAdmissionSettlementDoesNotCreateTurn(t *testing.T) { defer cancel() response := submitEnvironmentAdmission(ctx, h, worker, "waiting") pending := environmentAdmissionPending(t, h, "waiting") - _, pool := testStore(t) + pool := h.s.pool expected := execution.ErrEnvironmentInputExpired switch name { case "expired": diff --git a/services/core/tests/integration/environment_device_test.go b/services/core/tests/integration/environment_device_test.go index f2b5354af..e5f5989c3 100644 --- a/services/core/tests/integration/environment_device_test.go +++ b/services/core/tests/integration/environment_device_test.go @@ -11,7 +11,7 @@ import ( func TestWorkerEnvironmentSelectsCapableDeviceWithoutMovingBinding(t *testing.T) { h := newDispatchHarness(t) - _, pool := testStore(t) + pool := h.s.pool enableWorkerEnvironment(t, h) pending := unboundWorkerEnvironmentReservation(t, h) bound := workerEnvironmentReservation(t, h) diff --git a/services/core/tests/integration/environment_executor_management_test.go b/services/core/tests/integration/environment_executor_management_test.go index 629b46847..b02b7a4c8 100644 --- a/services/core/tests/integration/environment_executor_management_test.go +++ b/services/core/tests/integration/environment_executor_management_test.go @@ -273,7 +273,7 @@ func TestProjectExecutorConnectionState(t *testing.T) { t.Fatal("internal facts serialize", string(raw), err) } // Existing target visibility is preserved: an expired self-hosted Environment - // is readable but never has runtime_device_authority; deletion removes it. + // is readable but never has device authority; deletion removes it. if _, err = pool.Exec(ctx, "UPDATE environments SET status='expired' WHERE id=$1", env.ID); err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/environment_expiry_dispatch_test.go b/services/core/tests/integration/environment_expiry_dispatch_test.go index bcceb0c32..6d20f3123 100644 --- a/services/core/tests/integration/environment_expiry_dispatch_test.go +++ b/services/core/tests/integration/environment_expiry_dispatch_test.go @@ -15,7 +15,7 @@ func enableEnvironmentExpiryDispatch(h *dispatchHarness) { func TestWorkerEnvironmentExpiryAtFullExecutionCapacity(t *testing.T) { h := newDispatchHarness(t) - _, pool := testStore(t) + pool := h.s.pool enableEnvironmentExpiryDispatch(h) worker, stop := startEnvironmentExpiryWorker(t, h.s, h.d) var requests []proto.Envelope @@ -48,7 +48,7 @@ func TestWorkerEnvironmentExpiryAtFullExecutionCapacity(t *testing.T) { func TestWorkerEnvironmentExpirySkipsBusySessionAndAllowsDispatch(t *testing.T) { h := newDispatchHarness(t) - _, pool := testStore(t) + pool := h.s.pool enableEnvironmentExpiryDispatch(h) lockedTenant, locked := newEnvironmentExpiryReservation(t, h.s) otherTenant, other := newEnvironmentExpiryReservation(t, h.s) diff --git a/services/core/tests/integration/environment_file_writes_test.go b/services/core/tests/integration/environment_file_writes_test.go index c6092ef3d..bec92e397 100644 --- a/services/core/tests/integration/environment_file_writes_test.go +++ b/services/core/tests/integration/environment_file_writes_test.go @@ -27,7 +27,7 @@ func newFileWriteFixture(t *testing.T) fileWriteFixture { lease := executionWriter(t, s).lease tenant := uuid.NewString() session, env := localEnvironment(t, s, tenant) - host := registerAgentHost(t, s, tenant) + host := registerAgentHost(t, s) assignSession(t, s, session.ID, host.ID) return fileWriteFixture{s: s, lease: lease, writer: sessionExecution(t, lease), tenant: tenant, session: session, env: env, key: sessions.FileWriteIdentity{ID: uuid.NewString(), DeviceID: host.ID, RequestSHA256: strings.Repeat("a", 64)}} diff --git a/services/core/tests/integration/environment_initialization_test.go b/services/core/tests/integration/environment_initialization_test.go index 54429021b..8c25f1bd6 100644 --- a/services/core/tests/integration/environment_initialization_test.go +++ b/services/core/tests/integration/environment_initialization_test.go @@ -187,7 +187,7 @@ func TestEnvironmentInitializationRevocationBeforeClaim(t *testing.T) { if _, err := sessionService(t, s).EnrollRuntime(t.Context(), environment.ID, runtimedevice.HashCredential(key.Token)); err != nil { t.Fatal(err) } - host := registerAgentHost(t, s, principal.TenantID) + host := registerAgentHost(t, s) if err := owned.BindSessionDevice(t.Context(), principal.TenantID, session.ID, host.ID); err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/environment_runtime_fixture_test.go b/services/core/tests/integration/environment_runtime_fixture_test.go index 0798a9518..c8e21b731 100644 --- a/services/core/tests/integration/environment_runtime_fixture_test.go +++ b/services/core/tests/integration/environment_runtime_fixture_test.go @@ -38,13 +38,9 @@ func fixtureLinkResource(t *testing.T, s *Store, tenant string, session sessions } return resource, []byte(key.Token) } - device, err := FixtureEnvironmentDevice(t, t.Context(), s.pool, tenant, environment, "sandbox", runtimedevice.HashCredential(uuid.NewString())) - if err != nil { - t.Fatal(err) - } resource := sandboxbootstrap.Resource{TenantID: tenant, EnvironmentID: environment, Kind: "allocation", ID: uuid.NewString(), Generation: 1} serve := []byte(uuid.NewString()) - insertAllocation(t, s, resource, device.ID, serve) + insertAllocation(t, s, resource, serve) return resource, serve } @@ -56,7 +52,7 @@ func connectFixtureRuntime(t *testing.T, h *dispatchHarness, session sessions.Se // The Runtime shares the harness's Core, not its connection or write lock. other := &dispatchHarness{t: h.t, s: h.s, lease: h.lease, owned: h.owned, d: h.d, tenant: h.tenant, session: session, registry: h.registry, url: h.url, admissions: h.admissions, environments: h.environments, link: h.link} - host := registerAgentHost(t, h.s, h.tenant) + host := registerAgentHost(t, h.s) other.device, other.credential = sessions.ExecutionDevice{ID: host.ID, Name: "agent host"}, host.Credential other.resource, other.serve = fixtureLinkResource(t, h.s, h.tenant, session) // The placement precedes Serve, so a running Worker cannot place the @@ -125,7 +121,7 @@ func completeEmptyArtifactExport(t *testing.T, h *dispatchHarness, frames ...<-c func assertNoRuntimeAllocation(t *testing.T, h *dispatchHarness) { t.Helper() - _, pool := testStore(t) + pool := h.s.pool var count int if err := pool.QueryRow(t.Context(), "SELECT count(*) FROM runtime_allocations WHERE environment_id IN (SELECT id FROM environments WHERE session_id=$1)", h.session.ID).Scan(&count); err != nil || count != 0 { t.Fatal("self-hosted fixture allocated managed compute", count, err) diff --git a/services/core/tests/integration/environment_work_test.go b/services/core/tests/integration/environment_work_test.go index 3f6ee16cd..eed7967fb 100644 --- a/services/core/tests/integration/environment_work_test.go +++ b/services/core/tests/integration/environment_work_test.go @@ -10,7 +10,7 @@ import ( func TestEnvironmentInputWorkFiltersAndPagesDevices(t *testing.T) { h := newDispatchHarness(t) - _, pool := testStore(t) + pool := h.s.pool wanted := map[string]string{} for range 103 { pending := workerEnvironmentReservation(t, h) @@ -40,7 +40,7 @@ func TestEnvironmentInputWorkFiltersAndPagesDevices(t *testing.T) { default: runtime := h.environments[pending.SessionID] if state == "revoked" { - if err := sessionService(t, h.s).RevokeDevice(t.Context(), h.tenant, runtime.device.ID); err != nil { + if _, err := h.s.pool.Exec(t.Context(), "UPDATE devices SET revoked_at=clock_timestamp() WHERE id=$1", runtime.device.ID); err != nil { t.Fatal(err) } work, err := sessionAdapter(h.s).ListEnvironmentInputWork(t.Context(), "", []string{runtime.device.ID}) @@ -59,9 +59,11 @@ func TestEnvironmentInputWorkFiltersAndPagesDevices(t *testing.T) { t.Fatal("unconnected work selected", work, err) } } - unboundWorkerEnvironmentReservation(t, &dispatchHarness{s: h.s, tenant: uuid.NewString()}) + otherTenant := uuid.NewString() + other := unboundWorkerEnvironmentReservation(t, &dispatchHarness{s: h.s, tenant: otherTenant}) + wanted[other.ID] = other.SessionID seen, cursor := 0, "" - for _, count := range []int{100, 4, 0} { + for _, count := range []int{100, 5, 0} { devices := []string{h.device.ID} for _, runtime := range h.environments { devices = append(devices, runtime.device.ID) @@ -71,7 +73,11 @@ func TestEnvironmentInputWorkFiltersAndPagesDevices(t *testing.T) { t.Fatal("environment work page", len(work), count, err) } for _, item := range work { - if item.TenantID != h.tenant || item.SessionID != wanted[item.ReservationID] || item.ReservationID <= cursor { + tenant := h.tenant + if item.ReservationID == other.ID { + tenant = otherTenant + } + if item.TenantID != tenant || item.SessionID != wanted[item.ReservationID] || item.ReservationID <= cursor { t.Fatal("wrong scope or pagination", item) } cursor = item.ReservationID diff --git a/services/core/tests/integration/environment_worker_test.go b/services/core/tests/integration/environment_worker_test.go index de3a762ee..b6cfbad55 100644 --- a/services/core/tests/integration/environment_worker_test.go +++ b/services/core/tests/integration/environment_worker_test.go @@ -11,7 +11,7 @@ import ( func TestWorkerEnvironmentSharesCapacityThroughClaimAndCleanup(t *testing.T) { h := newDispatchHarness(t) - _, pool := testStore(t) + pool := h.s.pool enableWorkerEnvironment(t, h) pending := map[string]sessions.EnvironmentInputReservation{} for range 2 { diff --git a/services/core/tests/integration/execution_events_test.go b/services/core/tests/integration/execution_events_test.go index 7fd4676a5..b7b6283f7 100644 --- a/services/core/tests/integration/execution_events_test.go +++ b/services/core/tests/integration/execution_events_test.go @@ -22,7 +22,8 @@ func TestExecutionPersistsLiveAndCancelledPartialOutput(t *testing.T) { h.write(input.TurnID, proto.TypeToolCall, proto.ToolCallPayload{ID: "tool-1", Name: "Bash", Stage: "before", Observation: &proto.ToolObservation{Kind: "command", Command: "pwd", Status: "in_progress"}}) h.write(input.TurnID, proto.TypeToolCall, proto.ToolCallPayload{ID: "tool-1", Name: "Bash", Stage: "after", Observation: &proto.ToolObservation{Kind: "command", Command: "pwd", Status: "completed"}}) h.write(input.TurnID, proto.TypeUsage, proto.UsagePayload{Usage: proto.Usage{InputTokens: 11, OutputTokens: 2}}) - reopened, pool := testStore(t) + reopened := reopenStore(t, h.s) + pool := reopened.pool defer pool.Close() deadline := time.Now().Add(5 * time.Second) for { @@ -80,8 +81,7 @@ func TestExecutionDoesNotCompleteAfterEventPersistenceFailure(t *testing.T) { input := h.message("start", "Output beyond storage budget") result := h.run(ctx, input.TurnID) h.read(testExecutionRequest) - _, pool := testStore(t) - defer pool.Close() + pool := h.s.pool if _, err := pool.Exec(ctx, "UPDATE turns SET event_bytes=33554432 WHERE id=$1", input.TurnID); err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/execution_test.go b/services/core/tests/integration/execution_test.go index a6765d983..1a0b6018b 100644 --- a/services/core/tests/integration/execution_test.go +++ b/services/core/tests/integration/execution_test.go @@ -112,7 +112,7 @@ func TestExecutionLeaseLossFencesAllLifecycleWrites(t *testing.T) { tenant, active := newTurnSession(t, s) input := submitMessage(t, s, tenant, active.ID, "active") transition(t, writer, tenant, active.ID, input.TurnID, sessions.TurnQueued, sessions.TurnInProgress) - host := registerAgentHost(t, s, tenant) + host := registerAgentHost(t, s) err := sessionExecution(t, writer.lease).BindSessionDevice(t.Context(), tenant, active.ID, host.ID) if err != nil { t.Fatal(err) diff --git a/services/core/tests/integration/execution_tools_test.go b/services/core/tests/integration/execution_tools_test.go index 9cd17ef96..7ec8be940 100644 --- a/services/core/tests/integration/execution_tools_test.go +++ b/services/core/tests/integration/execution_tools_test.go @@ -42,7 +42,8 @@ func TestExecutionNegotiatesAndPersistsToolObservations(t *testing.T) { _ = env.DecodePayload(&cancel) h.write(input.TurnID, proto.TypeInteractionDecisionAck, proto.InteractionDecisionAckPayload{DeliveryID: cancel.DeliveryID, Applied: true, Outcome: &proto.DonePayload{}}) h.finished(result, sessions.TurnCancelled) - reopened, pool := testStore(t) + reopened := reopenStore(t, h.s) + pool := reopened.pool defer pool.Close() events, err := reopened.ListTurnEvents(ctx, h.tenant, h.session.ID, input.TurnID, 0, 100) if err != nil { diff --git a/services/core/tests/integration/function_worker_test.go b/services/core/tests/integration/function_worker_test.go index 1069294f0..2ad15255d 100644 --- a/services/core/tests/integration/function_worker_test.go +++ b/services/core/tests/integration/function_worker_test.go @@ -118,7 +118,6 @@ const mcpWorkerConfiguration = `{"agent":{"model":"gpt-5.5","tools":[{"type":"mc func mcpBearerWorkerConfiguration(t *testing.T, h *dispatchHarness) (string, string) { t.Helper() - h.s, _ = testStore(t) _, service, err := fixtureVaults(h.s) 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 7ada0a95d..c2c149444 100644 --- a/services/core/tests/integration/link_authority_test.go +++ b/services/core/tests/integration/link_authority_test.go @@ -56,12 +56,11 @@ func newLinkHarness(t *testing.T, configuration string) *linkHarness { return &linkHarness{dispatchHarness: h} } -// insertAllocation inserts a running allocation of device that is resource -// and Serves with the credential. -func insertAllocation(t *testing.T, s *Store, resource sandboxbootstrap.Resource, device string, serve []byte) { +// insertAllocation inserts a running resource that Serves with the credential. +func insertAllocation(t *testing.T, s *Store, resource sandboxbootstrap.Resource, serve []byte) { t.Helper() - if _, err := s.pool.Exec(t.Context(), `INSERT INTO runtime_allocations(id,environment_id,device_id,provider_key,state,create_settled,deployment_generation,serve_credential_hash) - VALUES($1,$2,$3,$4,'running',true,(SELECT generation FROM runtime_deployment),$5)`, resource.ID, resource.EnvironmentID, device, uuid.NewString(), serveHash(serve)); err != nil { + if _, err := s.pool.Exec(t.Context(), `INSERT INTO runtime_allocations(id,environment_id,provider_key,state,create_settled,deployment_generation,serve_credential_hash) + VALUES($1,$2,(SELECT installation_id FROM runtime_deployment),'running',true,(SELECT generation FROM runtime_deployment),$3)`, resource.ID, resource.EnvironmentID, serveHash(serve)); err != nil { t.Fatal(err) } } @@ -232,7 +231,7 @@ func within[T any](t *testing.T, ch <-chan T) T { } // TestLinkAuthorityAgentHost serves an allocation and opens services on it -// from a marked agent host, and checks that each part of the grant's +// from an agent host, and checks that each part of the grant's // authority is current at every Open and renewal. func TestLinkAuthorityAgentHost(t *testing.T) { l := newLinkHarness(t, hostedLinkSession) @@ -248,14 +247,6 @@ func TestLinkAuthorityAgentHost(t *testing.T) { t.Fatalf("Serve of another resource refused with %v, want %v", got, test.want) } } - operator := uuid.NewString() - unmarked, err := sessionService(t, l.s).CreateDevice(t.Context(), l.tenant, "operator", runtimedevice.HashCredential(operator)) - if err != nil { - t.Fatal(err) - } - if _, err := l.attach(unmarked.ID, []byte(operator)); linkCode(err) != sandboxlink.AuthenticationFailed { - t.Fatal("an unmarked device attached", err) - } link, err := l.attach(l.device.ID, []byte(l.credential)) if err != nil { t.Fatal(err) @@ -466,8 +457,8 @@ func TestLinkAuthorityDestroyedAllocation(t *testing.T) { // TestRegisteredAgentHostAuthenticates registers the agent host from its // identity file as Core's startup does. The Link route and the Runtime gateway -// accept its credential, a second startup changes nothing, and another -// device's ID is never taken over. +// accept its credential, a second startup changes nothing, and rotation fences +// the previous credential. func TestRegisteredAgentHostAuthenticates(t *testing.T) { s, _ := newManagedTestStore(t) dir := t.TempDir() @@ -495,8 +486,7 @@ func TestRegisteredAgentHostAuthenticates(t *testing.T) { } row := func() string { var state string - if err := s.pool.QueryRow(t.Context(), `SELECT row(tenant_id IS NULL, environment_id IS NULL, executor_key_id IS NULL, agent_host, - credential_hash, credential_revision, revoked_at IS NULL, count(*) OVER ())::text FROM devices WHERE id = $1`, runtime).Scan(&state); err != nil { + if err := s.pool.QueryRow(t.Context(), `SELECT row(credential_hash, credential_revision, revoked_at IS NULL, count(*) OVER ())::text FROM devices WHERE id = $1`, runtime).Scan(&state); err != nil { t.Fatal(err) } return state @@ -505,7 +495,7 @@ func TestRegisteredAgentHostAuthenticates(t *testing.T) { t.Fatal(err) } registered := row() - if want := "(t,t,t,t," + runtimedevice.HashCredential(credential) + ",1,t,1)"; registered != want { + if want := "(" + runtimedevice.HashCredential(credential) + ",1,t,1)"; registered != want { t.Fatalf("registered %s, want %s", registered, want) } if _, err := attachLink(t, startLinkRoute(t, s), runtime, []byte(credential)); err != nil { @@ -518,12 +508,15 @@ func TestRegisteredAgentHostAuthenticates(t *testing.T) { t.Fatal("a second registration changed the agent host", err) } - device, err := sessionService(t, s).CreateDevice(t.Context(), uuid.NewString(), "operator", runtimedevice.HashCredential(credential)) - if err != nil { + rotated := uuid.NewString() + if err := sessionAdapter(s).RegisterAgentHost(t.Context(), runtime, runtimedevice.HashCredential(rotated)); err != nil { t.Fatal(err) } - if err := sessionAdapter(s).RegisterAgentHost(t.Context(), device.ID, config.AgentHostCredentialHash); err == nil { - t.Fatal("registration took over a tenant device") + if want := "(" + runtimedevice.HashCredential(rotated) + ",2,t,1)"; row() != want { + t.Fatal("rotation did not advance credential revision") + } + if _, err := runtimegateway.NewAuthenticator(sessionAdapter(s)).AuthenticateBearer(t.Context(), runtime, credential); !errors.Is(err, runtimegateway.ErrAuthBadCredential) { + t.Fatal("rotation retained previous credential", err) } } @@ -538,7 +531,7 @@ func TestInitializationBindsAgentHost(t *testing.T) { t.Run(environment, func(t *testing.T) { // Hosted work is admitted only on a configured deployment. s, _ := configuredStore(t) - tenant, host := uuid.NewString(), registerAgentHost(t, s, "") + tenant, host := uuid.NewString(), registerAgentHost(t, s) session, err := s.CreateSession(t.Context(), tenant, WithFixtureModelProvider(sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), Configuration: json.RawMessage(`{"agent":{"model":"test-model"},"environment":` + environment + `}`), InitialFiles: []environmentconfig.InitialFile{{Type: "inline", Path: "/workspace/input", Data: []byte("frozen")}}})) @@ -548,7 +541,7 @@ func TestInitializationBindsAgentHost(t *testing.T) { resource, serve := fixtureLinkResource(t, s, tenant, session) server := httptest.NewUnstartedServer(nil) endpoint := "ws://" + server.Listener.Addr().String() + "/api/v1/agent-daemon/ws" - handler, registry, err := runtime.NewGateway(sessionAdapter(s), sessionService(t, s), sessionAdapter(s), runtimegateway.NewLinkAuthority(sessionAdapter(s)), endpoint) + handler, registry, err := runtime.NewGateway(sessionAdapter(s), sessionService(t, s), runtimegateway.NewLinkAuthority(sessionAdapter(s)), endpoint) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/list_cursor_public_test.go b/services/core/tests/integration/list_cursor_public_test.go index f6d5dc1e3..dfd0bbf0a 100644 --- a/services/core/tests/integration/list_cursor_public_test.go +++ b/services/core/tests/integration/list_cursor_public_test.go @@ -94,7 +94,7 @@ func seedCursorFixture(t *testing.T, s *Store, leased execution.Owner, skillServ if err != nil { t.Fatal(err) } - host := registerAgentHost(t, s, tenant) + host := registerAgentHost(t, s) if err = leased.Sessions.BindSessionDevice(ctx, tenant, created.ID, host.ID); err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/local_environment_devices_test.go b/services/core/tests/integration/local_environment_devices_test.go deleted file mode 100644 index 7e4565be1..000000000 --- a/services/core/tests/integration/local_environment_devices_test.go +++ /dev/null @@ -1,113 +0,0 @@ -package integration - -import ( - "encoding/json" - "errors" - "slices" - "sync" - "testing" - - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" - "github.com/google/uuid" -) - -func localEnvironment(t *testing.T, s *Store, tenant string) (sessions.Session, sessions.Environment) { - t.Helper() - session, err := s.CreateSession(t.Context(), tenant, sessions.CreateSession{ - Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), - Configuration: json.RawMessage(`{"agent":{"model":"test"},"environment":{"type":"openai_hosted","network":{"access":"disabled"}}}`), - }) - if err != nil { - t.Fatal(err) - } - environment, err := sessionAdapter(s).GetSessionEnvironment(t.Context(), tenant, session.ID) - if err != nil { - t.Fatal(err) - } - return session, environment -} - -func TestEnvironmentDeviceAuthorityAndLifecycle(t *testing.T) { - s, _ := configuredStore(t) - pool := s.pool - tenant, foreignTenant := uuid.NewString(), uuid.NewString() - session, environment := localEnvironment(t, s, tenant) - sibling, _ := localEnvironment(t, s, tenant) - foreign, _ := localEnvironment(t, s, foreignTenant) - digest := runtimedevice.HashCredential(uuid.NewString()) - if _, err := FixtureEnvironmentDevice(t, t.Context(), pool, foreignTenant, environment.ID, "foreign", digest); !errors.Is(err, sessions.ErrNotFound) { - t.Fatalf("foreign provisioning: %v", err) - } - bound, err := FixtureEnvironmentDevice(t, t.Context(), pool, tenant, environment.ID, "dedicated", digest) - if err != nil { - t.Fatalf("provision: %+v %v", bound, err) - } - // Sessions are placed on agent hosts only: the device binds no Session. - execution := sessionExecution(t, executionWriter(t, s).lease) - for _, other := range []sessions.Session{session, sibling, foreign} { - if err := execution.BindSessionDevice(t.Context(), other.TenantID, other.ID, bound.ID); err == nil { - t.Fatal("an Environment device was bound to a Session") - } - } - if _, err := sessionAdapter(s).GetSessionDevice(t.Context(), tenant, session.ID); !errors.Is(err, sessions.ErrNotFound) { - t.Fatalf("provisioning bound the Session: %v", err) - } - hosts, err := sessionAdapter(s).ListAgentHosts(t.Context(), tenant) - if err != nil || slices.ContainsFunc(hosts, func(host sessions.ExecutionDevice) bool { return host.ID == bound.ID }) { - t.Fatalf("an Environment device entered placement: %v %v", hosts, err) - } - if _, ok, err := sessionAdapter(s).GetDeviceCredential(t.Context(), bound.ID); err != nil || !ok { - t.Fatalf("valid credential unavailable: %v", err) - } - if err := sessionService(t, s).DeleteSession(t.Context(), sessions.DeleteSessionCommand{TenantID: tenant, SessionID: session.ID}); err != nil { - t.Fatal(err) - } - if _, ok, err := sessionAdapter(s).GetDeviceCredential(t.Context(), bound.ID); err != nil || ok { - t.Fatalf("deleted Environment still authenticates: %v", err) - } - status, err := sessionService(t, s).TouchRuntimeHeartbeat(t.Context(), bound.ID) - if err != nil || !status.Deleted { - t.Fatalf("deleted Environment heartbeat: %+v %v", status, err) - } -} - -func TestEnvironmentDeviceProvisioningHasOneWinner(t *testing.T) { - s, _ := configuredStore(t) - pool := s.pool - tenant := uuid.NewString() - _, environment := localEnvironment(t, s, tenant) - var wg sync.WaitGroup - results := make(chan error, 6) - for range 6 { - wg.Add(1) - go func() { - defer wg.Done() - _, err := FixtureEnvironmentDevice(t, t.Context(), pool, tenant, environment.ID, "runtime", runtimedevice.HashCredential(uuid.NewString())) - results <- err - }() - } - wg.Wait() - close(results) - winners := 0 - for err := range results { - if err == nil { - winners++ - } else if !errors.Is(err, sessions.ErrDeviceBindingConflict) { - t.Fatal(err) - } - } - if winners != 1 { - t.Fatalf("provisioned %d devices", winners) - } - var device string - if err := pool.QueryRow(t.Context(), "SELECT id::text FROM devices WHERE environment_id = $1", environment.ID).Scan(&device); err != nil { - t.Fatal(err) - } - if err := sessionService(t, s).RevokeDevice(t.Context(), tenant, device); err != nil { - t.Fatal(err) - } - if _, err := FixtureEnvironmentDevice(t, t.Context(), pool, tenant, environment.ID, "replacement", runtimedevice.HashCredential(uuid.NewString())); !errors.Is(err, sessions.ErrDeviceBindingConflict) { - t.Fatalf("silent placement replacement: %v", err) - } -} diff --git a/services/core/tests/integration/local_environment_fixture_test.go b/services/core/tests/integration/local_environment_fixture_test.go new file mode 100644 index 000000000..0f92f9b64 --- /dev/null +++ b/services/core/tests/integration/local_environment_fixture_test.go @@ -0,0 +1,25 @@ +package integration + +import ( + "encoding/json" + "testing" + + "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" + "github.com/google/uuid" +) + +func localEnvironment(t *testing.T, s *Store, tenant string) (sessions.Session, sessions.Environment) { + t.Helper() + session, err := s.CreateSession(t.Context(), tenant, sessions.CreateSession{ + Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: uuid.NewString(), + Configuration: json.RawMessage(`{"agent":{"model":"test"},"environment":{"type":"openai_hosted","network":{"access":"disabled"}}}`), + }) + if err != nil { + t.Fatal(err) + } + environment, err := sessionAdapter(s).GetSessionEnvironment(t.Context(), tenant, session.ID) + if err != nil { + t.Fatal(err) + } + return session, environment +} diff --git a/services/core/tests/integration/native_recovery_test.go b/services/core/tests/integration/native_recovery_test.go index 9d0207f59..b2219f744 100644 --- a/services/core/tests/integration/native_recovery_test.go +++ b/services/core/tests/integration/native_recovery_test.go @@ -11,7 +11,7 @@ func TestSessionExecutionBindingRetainsStartedExecutionRequirement(t *testing.T) s, pool := testStore(t) tenant, session := newTurnSession(t, s) foreign, _ := newTurnSession(t, s) - device := registerAgentHost(t, s, tenant) + device := registerAgentHost(t, s) // The bind runs on an execution lease of its own, which closes before the // pool does. writer := executionWriter(t, s) diff --git a/services/core/tests/integration/prepared_dispatch_failure_test.go b/services/core/tests/integration/prepared_dispatch_failure_test.go index 357a9f224..244f29637 100644 --- a/services/core/tests/integration/prepared_dispatch_failure_test.go +++ b/services/core/tests/integration/prepared_dispatch_failure_test.go @@ -17,7 +17,7 @@ func TestPreparedDispatchSettlesOnlyReadyInput(t *testing.T) { for _, action := range []string{"cancel", "expire", "delete", "prepare-failure", "disconnect"} { t.Run(action, func(t *testing.T) { h, pending := preparedDispatchHarness(t) - _, pool := testStore(t) + pool := h.s.pool result := runPreparedDispatch(h, t.Context(), pending) frame := h.read(proto.TypeExecutionPrepare) handle := acknowledgePreparation(h, frame.ID) diff --git a/services/core/tests/integration/prepared_dispatch_test.go b/services/core/tests/integration/prepared_dispatch_test.go index ca9a7a7a7..278ca675a 100644 --- a/services/core/tests/integration/prepared_dispatch_test.go +++ b/services/core/tests/integration/prepared_dispatch_test.go @@ -135,7 +135,7 @@ func TestPreparedDispatchPromotesOriginalBatchAndPersistsCompletion(t *testing.T func TestPreparedDispatchOwnerOutlivesReservationDeadline(t *testing.T) { h, pending := preparedDispatchHarness(t) - _, pool := testStore(t) + pool := h.s.pool parent, cancel := context.WithCancel(context.Background()) defer cancel() result := runPreparedDispatch(h, parent, pending) diff --git a/services/core/tests/integration/runtime_allocations_test.go b/services/core/tests/integration/runtime_allocations_test.go index 87db5f0fa..a7e400338 100644 --- a/services/core/tests/integration/runtime_allocations_test.go +++ b/services/core/tests/integration/runtime_allocations_test.go @@ -20,17 +20,17 @@ func TestRuntimeAllocationAtomicOwnershipAndRecovery(t *testing.T) { tenant := 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), runtimedevice.HashCredential(secret)) + owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, provider, runtimedevice.HashCredential(secret)) if err != nil || owner.Replayed || owner.State != "creating" || owner.CreateSettled { t.Fatalf("reservation: %+v %v", owner, err) } if _, err := sessionAdapter(s).GetSessionDevice(t.Context(), tenant, session.ID); !errors.Is(err, sessions.ErrNotFound) { t.Fatalf("the reservation bound the Session: %v", err) } - 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) { + 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) { 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), 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)); !errors.Is(err, deployment.ErrAllocationConflict) { t.Fatalf("provider target changed: %v", err) } awaitRelease := pgtest.ObserveExecutionLeaseRelease(t, w.pool) @@ -42,13 +42,13 @@ func TestRuntimeAllocationAtomicOwnershipAndRecovery(t *testing.T) { t.Fatal("lost writer changed allocation") } next := executionWriter(t, New(t, pool)) - 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 { + retry, err := deploymentExecution(t, next).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, provider, runtimedevice.HashCredential(uuid.NewString())) + if err != nil || !retry.Replayed || retry.ID != owner.ID { t.Fatalf("restart replaced unknown allocation: %+v %v", retry, err) } - credential, ok, err := sessionAdapter(s).GetDeviceCredential(t.Context(), owner.DeviceID) - if err != nil || !ok || credential.CredentialHash != runtimedevice.HashCredential(secret) { - t.Fatal("retry rewrote bootstrap credential") + var credential string + if err := pool.QueryRow(t.Context(), "SELECT serve_credential_hash FROM runtime_allocations WHERE id=$1", owner.ID).Scan(&credential); err != nil || credential != runtimedevice.HashCredential(secret) { + t.Fatal("retry rewrote Serve credential", err) } if err := sessionService(t, s).DeleteSession(t.Context(), sessions.DeleteSessionCommand{TenantID: tenant, SessionID: session.ID}); err != nil { t.Fatal(err) @@ -109,7 +109,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()), runtimedevice.HashCredential(uuid.NewString())) + value, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, provider, runtimedevice.HashCredential(uuid.NewString())) results <- value errs <- err }() @@ -136,7 +136,7 @@ func TestRuntimeAllocationOneWinnerAndRollback(t *testing.T) { t.Fatalf("%d fresh Create receipts", winners) } _, fail := localEnvironment(t, s, tenant) - // Reject the final insert after device/binding writes to prove transactional rollback. + // Reject the reservation insert to prove that failed admission creates no allocation. constraint := "fixture_" + uuid.NewString()[:8] _, err := pool.Exec(t.Context(), "ALTER TABLE runtime_allocations ADD CONSTRAINT "+constraint+" CHECK (environment_id <> '"+fail.ID+"'::uuid)") if err != nil { @@ -145,21 +145,23 @@ 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()), 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())); err == nil { t.Fatal("injected insert failure succeeded") } var count int - if err := pool.QueryRow(t.Context(), "SELECT count(*) FROM devices WHERE environment_id=$1", fail.ID).Scan(&count); err != nil || count != 0 { - t.Fatalf("failed reservation left a device: %d %v", count, err) + if err := pool.QueryRow(t.Context(), "SELECT count(*) FROM runtime_allocations WHERE environment_id=$1", fail.ID).Scan(&count); err != nil || count != 0 { + t.Fatalf("failed reservation left an allocation: %d %v", count, err) } } -func TestRuntimeAllocationCleanupRevokesAndKeepsIdentity(t *testing.T) { +func TestRuntimeAllocationCleanupReleasesAssignmentAndKeepsIdentity(t *testing.T) { s, installation := configuredStore(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}, installation, runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())) + session, environment := localEnvironment(t, s, tenant) + host := registerAgentHost(t, s) + assignSession(t, s, session.ID, host.ID) + owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, installation, runtimedevice.HashCredential(uuid.NewString())) if err != nil { t.Fatal(err) } @@ -173,8 +175,13 @@ func TestRuntimeAllocationCleanupRevokesAndKeepsIdentity(t *testing.T) { if _, err := deploymentExecution(t, w).RequestCleanup(t.Context(), owner); err != nil { t.Fatal(err) } - if _, ok, err := sessionAdapter(s).GetDeviceCredential(t.Context(), owner.DeviceID); err != nil || ok { - t.Fatal("cleanup credential still authenticates") + var desired string + var removeHome bool + if err := s.pool.QueryRow(t.Context(), "SELECT desired_state, remove_home FROM session_runtime_assignments WHERE session_id=$1", session.ID).Scan(&desired, &removeHome); err != nil || desired != "released" || removeHome { + t.Fatal("cleanup did not release assignment while retaining home", desired, removeHome, err) + } + if _, ok, err := sessionAdapter(s).GetDeviceCredential(t.Context(), host.ID); err != nil || !ok { + t.Fatal("allocation cleanup revoked shared host", err) } if _, err := deploymentExecution(t, w).CheckRunning(t.Context(), owner); !errors.Is(err, deployment.ErrAllocationConflict) { t.Fatalf("cleanup kept the allocation running: %v", err) @@ -182,7 +189,7 @@ func TestRuntimeAllocationCleanupRevokesAndKeepsIdentity(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()), 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())) if err != nil || !got.Replayed || got.State != "released" { 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 2cd74267b..0f33e84a9 100644 --- a/services/core/tests/integration/runtime_compute_lifecycle_test.go +++ b/services/core/tests/integration/runtime_compute_lifecycle_test.go @@ -53,7 +53,7 @@ type fakeCheckpointProvider struct { func newFakeCheckpointProvider(t *testing.T, s *Store) *fakeCheckpointProvider { t.Helper() p := &fakeCheckpointProvider{t: t, lifecycleProvider: lifecycleProvider{resources: map[string]sandbox.Info{}}, computes: map[string]sandbox.ComputeState{}, snapshots: map[string]sandbox.SnapshotIdentity{}, bootstraps: map[string]sandbox.Bootstrap{}, serving: map[string]*linkServe{}, - host: registerAgentHost(t, s, ""), registry: runtimegateway.NewRegistry(), link: sandboxlinktest.StartRelay(t, runtimegateway.NewLinkAuthority(sessionAdapter(s)))} + host: registerAgentHost(t, s), registry: runtimegateway.NewRegistry(), link: sandboxlinktest.StartRelay(t, runtimegateway.NewLinkAuthority(sessionAdapter(s)))} handler := runtimegateway.NewHandler(runtimegateway.HandlerConfig{Authenticator: runtimegateway.NewAuthenticator(sessionAdapter(s)), Registry: p.registry}) server := httptest.NewServer(http.HandlerFunc(handler.WS)) p.endpoint = "ws" + strings.TrimPrefix(server.URL, "http") @@ -454,7 +454,7 @@ func TestRuntimeComputeLifecycleIdleSuspendAndQueuedSameSessionWake(t *testing.T } queued := f.queued(suspended) awake := f.phase(tenant, env.ID, "running") - if awake.SessionID != session.ID || awake.ID != owner.ID || awake.DeviceID != owner.DeviceID || f.provider.creates != 1 || f.provider.restores != 1 || f.provider.snapshotDeletes != 1 || f.provider.resumes.Load() != 1 { + if awake.SessionID != session.ID || awake.ID != owner.ID || f.provider.creates != 1 || f.provider.restores != 1 || f.provider.snapshotDeletes != 1 || f.provider.resumes.Load() != 1 { t.Fatal("wake replaced Session or replayed allocation") } var completedCount, queuedCount int @@ -588,9 +588,6 @@ func TestRuntimeComputeLifecycleSuspendedDeletionAndExpiryCleanup(t *testing.T) if len(f.provider.computes) != 0 || len(f.provider.snapshots) != 0 || f.provider.snapshotDeletes != 1 { t.Fatal("retained snapshot survived cleanup") } - if _, ok, err := sessionAdapter(f.store).GetDeviceCredential(t.Context(), owner.DeviceID); err != nil || ok { - t.Fatal("cleanup retained daemon authority", err) - } }) } } diff --git a/services/core/tests/integration/runtime_configuration_cleanup_test.go b/services/core/tests/integration/runtime_configuration_cleanup_test.go index e72ff26fe..9c8513bc3 100644 --- a/services/core/tests/integration/runtime_configuration_cleanup_test.go +++ b/services/core/tests/integration/runtime_configuration_cleanup_test.go @@ -138,9 +138,6 @@ func TestManagedRuntimeConfigurationCleanup(t *testing.T) { if err != nil || got.ID != owner.ID || got.State != wantState || got.CreateSettled != test.wantSettled { t.Fatal("cleanup lost ownership or settlement", got, err) } - if _, ok, err := sessionAdapter(s).GetDeviceCredential(t.Context(), owner.DeviceID); err != nil || ok { - t.Fatal("cleanup retained execution authority", err) - } p.mu.Lock() creates, kills, remaining := p.creates, p.kills, len(p.resources) p.mu.Unlock() diff --git a/services/core/tests/integration/runtime_creation_settlement_test.go b/services/core/tests/integration/runtime_creation_settlement_test.go index 211a0d07b..70a7b5884 100644 --- a/services/core/tests/integration/runtime_creation_settlement_test.go +++ b/services/core/tests/integration/runtime_creation_settlement_test.go @@ -61,9 +61,6 @@ func TestManagedRuntimeConfirmedAbsentCreateReleasesAtomically(t *testing.T) { if err != nil || stored.State != "released" || !stored.CreateSettled { t.Fatal("release not durable", err) } - if _, ok, err := sessionAdapter(s).GetDeviceCredential(t.Context(), owner.DeviceID); err != nil || ok { - t.Fatal("released credential retained authority", err) - } value, err := sessionAdapter(s).GetSession(t.Context(), tenant, session.ID) if err != nil || value.Environment.Status != "failed" { t.Fatal("environment not terminated", err) diff --git a/services/core/tests/integration/runtime_deployment_test.go b/services/core/tests/integration/runtime_deployment_test.go index 91977e175..3133ee445 100644 --- a/services/core/tests/integration/runtime_deployment_test.go +++ b/services/core/tests/integration/runtime_deployment_test.go @@ -37,7 +37,7 @@ func TestRuntimeDeploymentResetPreservesCreationRetriesAndOtherPlacements(t *tes if err := s.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}, installation, runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())); !errors.Is(err, placement.ErrResetAdmission) { + if _, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: existing.Environment.ID}, installation, runtimedevice.HashCredential(uuid.NewString())); !errors.Is(err, placement.ErrResetAdmission) { t.Fatal("reset reserved new allocation", err) } for _, kind := range []string{"none", "self_hosted"} { @@ -50,10 +50,10 @@ func TestRuntimeDeploymentResetPreservesCreationRetriesAndOtherPlacements(t *tes if err := deploymentExecution(t, w).CancelReset(ctx, installation, 1); err != nil { t.Fatal(err) } - 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) { + 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) { t.Fatal("wrong installation reserved resource", err) } - if _, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: existing.Environment.ID}, installation, runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())); err != nil { + if _, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: existing.Environment.ID}, installation, runtimedevice.HashCredential(uuid.NewString())); err != nil { t.Fatal("cancelled reset did not reopen allocation", err) } } diff --git a/services/core/tests/integration/runtime_environment_terminal_test.go b/services/core/tests/integration/runtime_environment_terminal_test.go index 874f623e3..e3b2adaff 100644 --- a/services/core/tests/integration/runtime_environment_terminal_test.go +++ b/services/core/tests/integration/runtime_environment_terminal_test.go @@ -27,7 +27,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}, installation, runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())) + owner, err := deploymentExecution(t, writer).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, installation, runtimedevice.HashCredential(uuid.NewString())) if err != nil { t.Fatal(err) } @@ -51,9 +51,6 @@ func TestManagedEnvironmentTerminationSettlesInputAndPreservesIdentity(t *testin if failure := ended.EnvironmentFailure; expired != (failure == nil) || !expired && (failure.Reason != sessions.ProvisioningFailureReason || failure.FailedAt.IsZero()) { t.Fatal("terminal failure projection", failure) } - if _, ok, err := sessionAdapter(s).GetDeviceCredential(t.Context(), owner.DeviceID); err != nil || ok { - t.Fatal("terminal credential remained usable", err) - } failed, err := sessionExecution(t, writer.lease).PromoteEnvironmentInput(t.Context(), tenant, session.ID, reservation.ID) if err != nil || failed.State != sessions.EnvironmentInputFailed || len(failed.Receipts) != 0 || failed.SettledAt == nil || !failed.Deadline.Equal(reservation.Deadline) { t.Fatal("late preparation resurrected failed input", failed, err) @@ -123,7 +120,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}, installation, runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())) + owner, err := deploymentExecution(t, writer).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, installation, runtimedevice.HashCredential(uuid.NewString())) if err != nil { t.Fatal(err) } @@ -148,7 +145,4 @@ func TestManagedEnvironmentFailureRollsBackWithSessionEvent(t *testing.T) { if err != nil || allocation.State != "creating" { t.Fatal("partial allocation transition", err) } - if _, ok, err := sessionAdapter(s).GetDeviceCredential(t.Context(), owner.DeviceID); err != nil || !ok { - t.Fatal("partial credential revocation", err) - } } diff --git a/services/core/tests/integration/runtime_file_admission_test.go b/services/core/tests/integration/runtime_file_admission_test.go index 564e4c8de..68d564338 100644 --- a/services/core/tests/integration/runtime_file_admission_test.go +++ b/services/core/tests/integration/runtime_file_admission_test.go @@ -17,7 +17,7 @@ import ( // Runtime its file writes name. func runtimeFileWriteHost(t *testing.T, s *Store, owner deployment.Allocation) string { t.Helper() - host := registerAgentHost(t, s, owner.TenantID) + host := registerAgentHost(t, s) assignSession(t, s, owner.SessionID, host.ID) return host.ID } diff --git a/services/core/tests/integration/runtime_idle_clock_test.go b/services/core/tests/integration/runtime_idle_clock_test.go index 6c4b35c50..0f7e81827 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"), runtimedevice.HashCredential("runtime")) + owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, d.InstallationID, runtimedevice.HashCredential("runtime")) if err != nil { t.Fatal(err) } @@ -118,7 +118,7 @@ func TestManagedIdleClockIgnoresChildHostSkewAndReplay(t *testing.T) { root := runtimeSuspensionCompleted(t, s.pool, owner) child := uuid.NewString() runtimeSuspensionSQL(t, s.pool, `INSERT INTO turn_events(session_id,turn_id,ordinal,kind,payload) VALUES($1,$2,1,'subagent','{}')`, owner.SessionID, root) - runtimeSuspensionSQL(t, s.pool, `INSERT INTO subagent_identities(id,session_id,device_id,engine,native_id,parent_native_id,native_created_at,first_turn_id,first_event_ordinal,public_visible) VALUES($1,$2,$3,'codex','child','root',1,$4,1,true)`, child, owner.SessionID, owner.DeviceID, root) + runtimeSuspensionSQL(t, s.pool, `INSERT INTO subagent_identities(id,session_id,device_id,engine,native_id,parent_native_id,native_created_at,first_turn_id,first_event_ordinal,public_visible) VALUES($1,$2,$3,'codex','child','root',1,$4,1,true)`, child, owner.SessionID, registerAgentHost(t, s).ID, root) source := runtimeDatabaseTime(t, s).Add(skew).UnixMilli() created := source - 1000 payload, _ := json.Marshal(proto.SubagentTurnPayload{NativeID: "child", TurnID: "remote-turn", Status: sessions.TurnCompleted, CreatedAtMS: created, StartedAtMS: &created, CompletedAtMS: &source}) diff --git a/services/core/tests/integration/runtime_initialization_peer_test.go b/services/core/tests/integration/runtime_initialization_peer_test.go index c2749d8f0..f57fdc599 100644 --- a/services/core/tests/integration/runtime_initialization_peer_test.go +++ b/services/core/tests/integration/runtime_initialization_peer_test.go @@ -45,7 +45,7 @@ type initializationPeer struct { func (p *initializationPeer) setRuntimeGateway(t *testing.T, s *Store, endpoint string, registry *runtimegateway.Registry, link *sandboxlinktest.Server) { p.t, p.endpoint, p.registry, p.link, p.serving = t, endpoint, registry, link, nil if p.host.ID == "" { - p.host = registerAgentHost(t, s, p.tenant) + p.host = registerAgentHost(t, s) } } diff --git a/services/core/tests/integration/runtime_initialization_test.go b/services/core/tests/integration/runtime_initialization_test.go index ca5a908af..1e09a81bf 100644 --- a/services/core/tests/integration/runtime_initialization_test.go +++ b/services/core/tests/integration/runtime_initialization_test.go @@ -85,13 +85,10 @@ func TestEnvironmentInitializationCompletionUnknownAndRestart(t *testing.T) { } return } - owner, err := w.ProvisionEnvironment(t.Context(), tenant, env.ID, key) + _, err = w.ProvisionEnvironment(t.Context(), tenant, env.ID, key) if err != nil { t.Fatal(err) } - if _, ok, err := sessionAdapter(s).GetDeviceCredential(t.Context(), owner.DeviceID); err != nil || !ok { - t.Fatal("preparation blocked authentication", err) - } time.Sleep(350 * time.Millisecond) if initializationState(t, s, tenant, env.ID) != "pending" || p.writes.Load() != 0 { t.Fatal("missing socket consumed initialization") diff --git a/services/core/tests/integration/runtime_lifecycle_nodes_test.go b/services/core/tests/integration/runtime_lifecycle_nodes_test.go index 27a0c1f1a..72f26498a 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 managerNode, 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()), 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())) 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"), runtimedevice.HashCredential("runtime")) + owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment}, d.InstallationID, runtimedevice.HashCredential("runtime")) if err != nil { t.Fatal(err) } @@ -208,7 +208,7 @@ func TestRuntimeLifecycleNodeRejectsMissingOrReleasedPlacement(t *testing.T) { func TestRuntimeLifecycleNodelessLane(t *testing.T) { s, w, installation := managedArchiveFixture(t) _, reserved := localEnvironment(t, s, uuid.NewString()) - a, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: reserved.TenantID, EnvironmentID: reserved.ID}, installation, runtimedevice.HashCredential("runtime"), runtimedevice.HashCredential("runtime")) + a, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: reserved.TenantID, EnvironmentID: reserved.ID}, installation, 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 614511b9c..2e91174f4 100644 --- a/services/core/tests/integration/runtime_lifecycle_test.go +++ b/services/core/tests/integration/runtime_lifecycle_test.go @@ -177,9 +177,6 @@ func TestManagedRuntimeLostCreateRestartAndDeletion(t *testing.T) { if err != nil || clean.State != "released" || p.kills != 1 { t.Fatalf("deleted cleanup: %+v %v", clean, err) } - if _, ok, err := sessionAdapter(s).GetDeviceCredential(t.Context(), owner.DeviceID); err != nil || ok { - t.Fatal("cleanup did not revoke authority") - } } func TestManagedRuntimeUnknownCreationRetainsCleanup(t *testing.T) { @@ -199,9 +196,6 @@ func TestManagedRuntimeUnknownCreationRetainsCleanup(t *testing.T) { if err != nil || got.State != "cleanup_pending" || got.CreateSettled || p.creates != 1 { t.Fatalf("unknown creation forgotten: %+v %v", got, err) } - if _, ok, err := sessionAdapter(s).GetDeviceCredential(t.Context(), owner.DeviceID); err != nil || ok { - t.Fatal("unknown allocation retains execution authority") - } // A late completion is still owned and reclaimed on the next scan. p.resources[owner.ID] = sandbox.Info{Reference: sandbox.Reference{TenantID: tenant, EnvironmentID: env.ID, AllocationID: owner.ID}, ProviderID: owner.ID, State: "running", BootstrapComplete: true} reconcileManagedState(t, w, s, tenant, env.ID, "released") diff --git a/services/core/tests/integration/runtime_node_generations_test.go b/services/core/tests/integration/runtime_node_generations_test.go index 1a77287dc..f491d98cf 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"), runtimedevice.HashCredential("runtime")) + owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: pending.Environment.ID}, first.InstallationID, runtimedevice.HashCredential("runtime")) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/runtime_nodes_test.go b/services/core/tests/integration/runtime_nodes_test.go index dd38b03b6..7086652ca 100644 --- a/services/core/tests/integration/runtime_nodes_test.go +++ b/services/core/tests/integration/runtime_nodes_test.go @@ -244,7 +244,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"), runtimedevice.HashCredential("runtime")) + retained, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: first.Environment.ID}, next.InstallationID, runtimedevice.HashCredential("runtime")) if err != nil { t.Fatal(err) } @@ -286,7 +286,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"), runtimedevice.HashCredential("runtime")) + allocation, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, d.InstallationID, runtimedevice.HashCredential("runtime")) if err != nil { t.Fatal(err) } @@ -344,7 +344,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"), runtimedevice.HashCredential("runtime")) + owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, d.InstallationID, runtimedevice.HashCredential("runtime")) if err != nil { t.Fatal(err) } @@ -364,7 +364,7 @@ func TestRuntimeNodesLongOfflineRetainsExactAllocation(t *testing.T) { } onlineManagerNode(t, s, d.NodeID) resumed, err := deploymentExecution(t, w).ObserveRunning(t.Context(), offline) - if err != nil || resumed.ID != owner.ID || resumed.DeviceID != owner.DeviceID || resumed.NodeID != owner.NodeID { + if err != nil || resumed.ID != owner.ID || resumed.NodeID != owner.NodeID { t.Fatal("reconnect changed instance", resumed, err) } if _, err := deploymentExecution(t, w).CheckRunning(t.Context(), resumed); err != nil { diff --git a/services/core/tests/integration/runtime_observation_test.go b/services/core/tests/integration/runtime_observation_test.go index d92f927c3..5ad2acf2b 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"), runtimedevice.HashCredential("runtime")) + owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, d.InstallationID, 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 72a435f97..03daea275 100644 --- a/services/core/tests/integration/runtime_pending_test.go +++ b/services/core/tests/integration/runtime_pending_test.go @@ -78,7 +78,7 @@ func TestManagedRuntimeAutomaticBootstrapRecoversCommittedSessions(t *testing.T) } for _, owner := range []deployment.Allocation{idleOwner, initialOwner} { got, err := deploymentStore(s).EnvironmentAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: owner.EnvironmentID}) - if err != nil || got.ID != owner.ID || got.DeviceID != owner.DeviceID { + if err != nil || got.ID != owner.ID { t.Fatal("restart replaced allocation identity", got, err) } } diff --git a/services/core/tests/integration/runtime_suspension_concurrency_test.go b/services/core/tests/integration/runtime_suspension_concurrency_test.go index 381ed3e7f..f1605e937 100644 --- a/services/core/tests/integration/runtime_suspension_concurrency_test.go +++ b/services/core/tests/integration/runtime_suspension_concurrency_test.go @@ -135,7 +135,7 @@ func TestRuntimeSuspensionCaptureRechecksNewPendingWork(t *testing.T) { case "input": _, err = tx.Exec(ctx, `INSERT INTO environment_input_reservations(id,session_id,idempotency_key,batch,created_at,deadline) VALUES($1,$2,'pending','[{}]',clock_timestamp(),clock_timestamp()+interval '1 minute')`, uuid.NewString(), owner.SessionID) case "file_write": - _, err = tx.Exec(ctx, `INSERT INTO environment_file_writes(id,environment_id,device_id,request_sha256) VALUES($1,$2,$3,$4)`, uuid.NewString(), owner.EnvironmentID, owner.DeviceID, strings.Repeat("a", 64)) + _, err = tx.Exec(ctx, `INSERT INTO environment_file_writes(id,environment_id,device_id,request_sha256) VALUES($1,$2,$3,$4)`, uuid.NewString(), owner.EnvironmentID, registerAgentHost(t, s).ID, strings.Repeat("a", 64)) case "wake": _, err = tx.Exec(ctx, `UPDATE runtime_allocations SET compute_wake_requested=true,compute_activity_at=clock_timestamp() WHERE id=$1`, owner.ID) } diff --git a/services/core/tests/integration/runtime_suspension_test.go b/services/core/tests/integration/runtime_suspension_test.go index 13b42bb2d..781e0a09f 100644 --- a/services/core/tests/integration/runtime_suspension_test.go +++ b/services/core/tests/integration/runtime_suspension_test.go @@ -22,7 +22,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}, installation, runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())) + owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, installation, runtimedevice.HashCredential(uuid.NewString())) if err != nil { t.Fatal(err) } @@ -68,7 +68,7 @@ func TestRuntimeSuspensionRequiresCompletedIdleAndNoPendingWork(t *testing.T) { cases := []string{"no_completed_turn", "queued", "in_progress", "waiting", "subagent_queued", "subagent_in_progress", "subagent_waiting", "input_reservation", "file_write", "idle"} for _, kind := range cases { t.Run(kind, func(t *testing.T) { - _, w, pool, owner := runtimeSuspensionFixture(t) + s, w, pool, owner := runtimeSuspensionFixture(t) completed := "" if kind != "no_completed_turn" { completed = runtimeSuspensionCompleted(t, pool, owner) @@ -79,12 +79,12 @@ func TestRuntimeSuspensionRequiresCompletedIdleAndNoPendingWork(t *testing.T) { case "subagent_queued", "subagent_in_progress", "subagent_waiting": child := uuid.NewString() runtimeSuspensionSQL(t, pool, `INSERT INTO turn_events(session_id,turn_id,ordinal,kind,payload) VALUES($1,$2,1,'subagent','{}')`, owner.SessionID, completed) - runtimeSuspensionSQL(t, pool, `INSERT INTO subagent_identities(id,session_id,device_id,engine,native_id,parent_native_id,native_created_at,first_turn_id,first_event_ordinal) VALUES($1,$2,$3,'codex','child','root',1,$4,1)`, child, owner.SessionID, owner.DeviceID, completed) + runtimeSuspensionSQL(t, pool, `INSERT INTO subagent_identities(id,session_id,device_id,engine,native_id,parent_native_id,native_created_at,first_turn_id,first_event_ordinal) VALUES($1,$2,$3,'codex','child','root',1,$4,1)`, child, owner.SessionID, registerAgentHost(t, s).ID, completed) runtimeSuspensionSQL(t, pool, `INSERT INTO subagent_turns(id,session_id,subagent_id,native_id,status,created_at) VALUES($1,$2,$3,'child-turn',$4,clock_timestamp())`, uuid.NewString(), owner.SessionID, child, strings.TrimPrefix(kind, "subagent_")) case "input_reservation": runtimeSuspensionSQL(t, pool, `INSERT INTO environment_input_reservations(id,session_id,idempotency_key,batch,created_at,deadline) VALUES($1,$2,'pending','[{}]',clock_timestamp(),clock_timestamp()+interval '1 minute')`, uuid.NewString(), owner.SessionID) case "file_write": - runtimeSuspensionSQL(t, pool, `INSERT INTO environment_file_writes(id,environment_id,device_id,request_sha256) VALUES($1,$2,$3,$4)`, uuid.NewString(), owner.EnvironmentID, owner.DeviceID, strings.Repeat("a", 64)) + runtimeSuspensionSQL(t, pool, `INSERT INTO environment_file_writes(id,environment_id,device_id,request_sha256) VALUES($1,$2,$3,$4)`, uuid.NewString(), owner.EnvironmentID, registerAgentHost(t, s).ID, strings.Repeat("a", 64)) } activity, err := deploymentStore(w).Activity(t.Context(), owner.ID) if err != nil { @@ -347,10 +347,10 @@ func TestRuntimeSuspensionRechecksCompletionAgainstIdleTimeout(t *testing.T) { case "subagent": child := uuid.NewString() runtimeSuspensionSQL(t, pool, `INSERT INTO turn_events(session_id,turn_id,ordinal,kind,payload) VALUES($1,$2,1,'subagent','{}')`, owner.SessionID, turn) - runtimeSuspensionSQL(t, pool, `INSERT INTO subagent_identities(id,session_id,device_id,engine,native_id,parent_native_id,native_created_at,first_turn_id,first_event_ordinal) VALUES($1,$2,$3,'codex','child','root',1,$4,1)`, child, owner.SessionID, owner.DeviceID, turn) + runtimeSuspensionSQL(t, pool, `INSERT INTO subagent_identities(id,session_id,device_id,engine,native_id,parent_native_id,native_created_at,first_turn_id,first_event_ordinal) VALUES($1,$2,$3,'codex','child','root',1,$4,1)`, child, owner.SessionID, registerAgentHost(t, s).ID, turn) runtimeSuspensionSQL(t, pool, `INSERT INTO subagent_turns(id,session_id,subagent_id,native_id,status,created_at,completed_at) VALUES($1,$2,$3,'child-turn','completed',clock_timestamp()-interval '10 minutes',clock_timestamp())`, uuid.NewString(), owner.SessionID, child) default: - runtimeSuspensionSQL(t, pool, `INSERT INTO environment_file_writes(id,environment_id,device_id,request_sha256,state,created_at,settled_at) VALUES($1,$2,$3,$4,$5,clock_timestamp()-interval '10 minutes',clock_timestamp())`, uuid.NewString(), owner.EnvironmentID, owner.DeviceID, strings.Repeat("a", 64), strings.TrimPrefix(kind, "file_")) + runtimeSuspensionSQL(t, pool, `INSERT INTO environment_file_writes(id,environment_id,device_id,request_sha256,state,created_at,settled_at) VALUES($1,$2,$3,$4,$5,clock_timestamp()-interval '10 minutes',clock_timestamp())`, uuid.NewString(), owner.EnvironmentID, registerAgentHost(t, s).ID, strings.Repeat("a", 64), strings.TrimPrefix(kind, "file_")) } until := time.Now().Add(time.Hour) if _, err := deploymentExecution(t, w).SetCompute(t.Context(), owner, "quiescing", json.RawMessage(`{}`), &until, idleTimeout); !errors.Is(err, deployment.ErrAllocationConflict) { @@ -405,7 +405,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"), runtimedevice.HashCredential("runtime")) + allocation, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, d.InstallationID, runtimedevice.HashCredential("runtime")) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/sandbox_deployment_resources_test.go b/services/core/tests/integration/sandbox_deployment_resources_test.go index 8d4bd2e8e..b22495635 100644 --- a/services/core/tests/integration/sandbox_deployment_resources_test.go +++ b/services/core/tests/integration/sandbox_deployment_resources_test.go @@ -25,7 +25,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()), 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())); 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 6bad18655..fdc0ba50f 100644 --- a/services/core/tests/integration/sandbox_deployment_switch_test.go +++ b/services/core/tests/integration/sandbox_deployment_switch_test.go @@ -104,14 +104,10 @@ 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()), runtimedevice.HashCredential(uuid.NewString())) + owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment}, id, runtimedevice.HashCredential(uuid.NewString())) if err != nil || owner.NodeID != "" { t.Fatal(owner, err) } - credential, ok, err := sessionAdapter(s).GetDeviceCredential(t.Context(), owner.DeviceID) - if err != nil || !ok || credential.RuntimeAllocationID != owner.ID || credential.RuntimeNodeID != "" { - t.Fatal("direct bootstrap lost managed identity", err) - } if err := deploymentExecution(t, w).StartReset(SandboxResetTestContext(t.Context()), id, deployment.ResetRequest{Clear: "auto", ExpectedGeneration: 1}); err != nil { t.Fatal(err) } @@ -275,7 +271,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()), 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())) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/sandbox_reset_test.go b/services/core/tests/integration/sandbox_reset_test.go index cc37abdc4..7fd0355c6 100644 --- a/services/core/tests/integration/sandbox_reset_test.go +++ b/services/core/tests/integration/sandbox_reset_test.go @@ -83,10 +83,10 @@ func TestSandboxResetAutoUsesStartedWorkAndLockedRecheck(t *testing.T) { parent, child := uuid.NewString(), uuid.NewString() runtimeSuspensionSQL(t, s.pool, `INSERT INTO turns(id,session_id,status,completed_at) VALUES($1,$2,'completed',clock_timestamp())`, parent, session.ID) runtimeSuspensionSQL(t, s.pool, `INSERT INTO turn_events(session_id,turn_id,ordinal,kind,payload) VALUES($1,$2,1,'subagent','{}')`, session.ID, parent) - runtimeSuspensionSQL(t, s.pool, `INSERT INTO subagent_identities(id,session_id,device_id,engine,native_id,parent_native_id,native_created_at,first_turn_id,first_event_ordinal) VALUES($1,$2,$3,'codex','child','root',1,$4,1)`, child, session.ID, owner.DeviceID, parent) + runtimeSuspensionSQL(t, s.pool, `INSERT INTO subagent_identities(id,session_id,device_id,engine,native_id,parent_native_id,native_created_at,first_turn_id,first_event_ordinal) VALUES($1,$2,$3,'codex','child','root',1,$4,1)`, child, session.ID, registerAgentHost(t, s).ID, parent) runtimeSuspensionSQL(t, s.pool, `INSERT INTO subagent_turns(id,session_id,subagent_id,native_id,status,created_at) VALUES($1,$2,$3,'child-turn',$4,clock_timestamp())`, uuid.NewString(), session.ID, child, strings.TrimPrefix(kind, "subagent_")) case "file_write": - runtimeSuspensionSQL(t, s.pool, `INSERT INTO environment_file_writes(id,environment_id,device_id,request_sha256) VALUES($1,$2,$3,$4)`, uuid.NewString(), session.Environment.ID, owner.DeviceID, strings.Repeat("a", 64)) + runtimeSuspensionSQL(t, s.pool, `INSERT INTO environment_file_writes(id,environment_id,device_id,request_sha256) VALUES($1,$2,$3,$4)`, uuid.NewString(), session.Environment.ID, registerAgentHost(t, s).ID, strings.Repeat("a", 64)) case "suspended": runtimeSuspensionSQL(t, s.pool, `UPDATE runtime_allocations SET compute_phase='suspended', compute_retained_until=clock_timestamp()+interval '1 hour' WHERE id=$1`, owner.ID) } diff --git a/services/core/tests/integration/sandbox_specification_lifecycle_test.go b/services/core/tests/integration/sandbox_specification_lifecycle_test.go index 96b7517f2..6c27b7e03 100644 --- a/services/core/tests/integration/sandbox_specification_lifecycle_test.go +++ b/services/core/tests/integration/sandbox_specification_lifecycle_test.go @@ -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()), 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())) 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()), 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())) 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()), 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())) 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()), 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())); !errors.Is(err, placement.ErrResetAdmission) { t.Fatal("fresh allocation passed committed maintenance", err) } } diff --git a/services/core/tests/integration/session_devices_fixture_test.go b/services/core/tests/integration/session_devices_fixture_test.go index 54acf9726..634c09b97 100644 --- a/services/core/tests/integration/session_devices_fixture_test.go +++ b/services/core/tests/integration/session_devices_fixture_test.go @@ -18,21 +18,13 @@ import ( // deployment's agent host, the Runtime Sessions are placed on. type agentHost struct{ ID, Credential string } -// registerAgentHost registers an agent host. Tests on the shared database pass -// their tenant: a deployment-wide host would let the test's Worker list and -// place every other test's Sessions, so the host serves only that tenant, as -// the test's own deployment. A test on an isolated database passes "". -func registerAgentHost(t testing.TB, s *Store, tenant string) agentHost { +// registerAgentHost registers a deployment-wide agent host. +func registerAgentHost(t testing.TB, s *Store) agentHost { t.Helper() host := agentHost{ID: uuid.NewString(), Credential: uuid.NewString()} if err := sessionAdapter(s).RegisterAgentHost(t.Context(), host.ID, runtimedevice.HashCredential(host.Credential)); err != nil { t.Fatal(err) } - if tenant != "" { - if _, err := s.pool.Exec(t.Context(), `UPDATE devices SET tenant_id = $2 WHERE id = $1`, host.ID, tenant); err != nil { - t.Fatal(err) - } - } return host } diff --git a/services/core/tests/integration/session_diagnostics_test.go b/services/core/tests/integration/session_diagnostics_test.go index 92f0bc80e..9affcfb42 100644 --- a/services/core/tests/integration/session_diagnostics_test.go +++ b/services/core/tests/integration/session_diagnostics_test.go @@ -208,7 +208,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}, installation, runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())) + owner, err := deploymentExecution(t, writer).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: session.Environment.ID}, installation, runtimedevice.HashCredential(uuid.NewString())) if err != nil { t.Fatal(err) } @@ -222,7 +222,9 @@ func TestDiagnosticProvisioningDetailAtomicAndPrivate(t *testing.T) { }) failure := sessions.ProvisioningFailure{Step: sessions.ProvisioningSetupCommand, Index: 2, ExitCode: 7} runtimeSuspensionSQL(t, pool, "UPDATE environments SET initialization='running' WHERE id=$1", owner.EnvironmentID) - preparation := sessions.EnvironmentInitialization{EnvironmentID: owner.EnvironmentID, SessionID: owner.SessionID, TenantID: owner.TenantID, DeviceID: owner.DeviceID} + host := registerAgentHost(t, s) + assignSession(t, s, owner.SessionID, host.ID) + preparation := sessions.EnvironmentInitialization{EnvironmentID: owner.EnvironmentID, SessionID: owner.SessionID, TenantID: owner.TenantID, DeviceID: host.ID} if err = sessionExecution(t, writer.lease).FailEnvironmentInitialization(t.Context(), preparation, failure); err == nil { t.Fatal("failure committed without events") } @@ -317,7 +319,7 @@ func TestDiagnosticRootReadRejectsActualChildTurn(t *testing.T) { root := runtimeSuspensionCompleted(t, s.pool, owner) child, turn := uuid.NewString(), uuid.NewString() runtimeSuspensionSQL(t, s.pool, `INSERT INTO turn_events(session_id,turn_id,ordinal,kind,payload) VALUES($1,$2,1,'subagent','{}')`, owner.SessionID, root) - runtimeSuspensionSQL(t, s.pool, `INSERT INTO subagent_identities(id,session_id,device_id,engine,native_id,parent_native_id,native_created_at,first_turn_id,first_event_ordinal) VALUES($1,$2,$3,'codex','child','root',1,$4,1)`, child, owner.SessionID, owner.DeviceID, root) + runtimeSuspensionSQL(t, s.pool, `INSERT INTO subagent_identities(id,session_id,device_id,engine,native_id,parent_native_id,native_created_at,first_turn_id,first_event_ordinal) VALUES($1,$2,$3,'codex','child','root',1,$4,1)`, child, owner.SessionID, registerAgentHost(t, s).ID, root) runtimeSuspensionSQL(t, s.pool, `INSERT INTO subagent_turns(id,session_id,subagent_id,native_id,status,created_at) VALUES($1,$2,$3,'child-turn','in_progress',clock_timestamp())`, turn, owner.SessionID, child) if _, err := sessionAdapter(w).GetTurnDiagnosticsSnapshot(t.Context(), owner.TenantID, owner.SessionID, turn); !errors.Is(err, sessions.ErrNotFound) { t.Fatal("child Turn became root diagnostics", err) diff --git a/services/core/tests/integration/session_execution_configuration_test.go b/services/core/tests/integration/session_execution_configuration_test.go index 2e1c5b445..82860a5fa 100644 --- a/services/core/tests/integration/session_execution_configuration_test.go +++ b/services/core/tests/integration/session_execution_configuration_test.go @@ -253,7 +253,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}, installation, runtimedevice.HashCredential(uuid.NewString()), runtimedevice.HashCredential(uuid.NewString())) + owner, err := deploymentExecution(t, w).ReserveAllocation(t.Context(), deployment.AllocationKey{TenantID: tenant, EnvironmentID: environment.ID}, installation, runtimedevice.HashCredential(uuid.NewString())) if err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/session_reads_fixture_test.go b/services/core/tests/integration/session_reads_fixture_test.go deleted file mode 100644 index f2cdc8308..000000000 --- a/services/core/tests/integration/session_reads_fixture_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package integration - -import ( - "context" - "testing" - - "github.com/google/uuid" - "github.com/jackc/pgx/v5/pgxpool" - - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/db/sqlc" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/sessionpg" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" -) - -// FixtureEnvironmentDevice creates the per-allocation device of the tenant's -// hosted Environment on pool through the procedure the managed Runtime -// allocation runs, without the allocation. -func FixtureEnvironmentDevice(t testing.TB, ctx context.Context, pool *pgxpool.Pool, tenant, environment, name, credentialHash string) (sessions.ExecutionDevice, error) { - s := New(t, pool) - current, err := sessionAdapter(s).GetEnvironment(ctx, tenant, environment) - if err != nil { - return sessions.ExecutionDevice{}, err - } - registration, err := sessions.NewDeviceRegistration(name, credentialHash) - if err != nil { - return sessions.ExecutionDevice{}, err - } - lookup, err := sessionpg.ResourceLookup(tenant, current.SessionID) - if err != nil { - return sessions.ExecutionDevice{}, err - } - device := sessions.ExecutionDevice{ID: uuid.NewString(), Name: registration.Name} - err = sessionpg.WithSession(ctx, s.pooled, lookup.TenantID, lookup.ID, func(ctx context.Context, q *sqlc.Queries, locked sessions.LockedSession) error { - if err := locked.Public(); err != nil { - return err - } - return sessions.CreateEnvironmentDevice(ctx, sessionpg.BindSession(q, lookup.TenantID, lookup.ID), current.ID, device, registration.CredentialHash) - }) - if err != nil { - return sessions.ExecutionDevice{}, err - } - return device, nil -} diff --git a/services/core/tests/integration/subagent_identities_test.go b/services/core/tests/integration/subagent_identities_test.go index 8537baacd..8c34a35ab 100644 --- a/services/core/tests/integration/subagent_identities_test.go +++ b/services/core/tests/integration/subagent_identities_test.go @@ -25,7 +25,7 @@ func TestSubagentIdentityIsAtomicScopedAndImmutable(t *testing.T) { journal := sessionExecution(t, w.lease) ctx := t.Context() tenant, session := newSubagentSession(t, s) - host := registerAgentHost(t, s, tenant) + host := registerAgentHost(t, s) err := sessionExecution(t, w.lease).BindSessionDevice(ctx, tenant, session.ID, host.ID) if err != nil { t.Fatal(err) diff --git a/services/core/tests/integration/subagent_native_outputs_test.go b/services/core/tests/integration/subagent_native_outputs_test.go index b6116a893..33a7d5411 100644 --- a/services/core/tests/integration/subagent_native_outputs_test.go +++ b/services/core/tests/integration/subagent_native_outputs_test.go @@ -14,7 +14,7 @@ func TestSubagentNativeFunctionResultDoesNotConsumeOutputIndex(t *testing.T) { s, pool := testStore(t) owner := executionWriter(t, s) tenant, session := newSubagentSession(t, s) - host := registerAgentHost(t, s, tenant) + host := registerAgentHost(t, s) err := sessionExecution(t, owner.lease).BindSessionDevice(t.Context(), tenant, session.ID, host.ID) if err != nil { t.Fatal(err) @@ -86,7 +86,7 @@ func TestSubagentCancelledPartialMessageSurvivesHistoryReplay(t *testing.T) { s, _ := testStore(t) owner := executionWriter(t, s) tenant, session := newSubagentSession(t, s) - host := registerAgentHost(t, s, tenant) + host := registerAgentHost(t, s) err := sessionExecution(t, owner.lease).BindSessionDevice(t.Context(), tenant, session.ID, host.ID) if err != nil { t.Fatal(err) diff --git a/services/core/tests/integration/subagent_resources_test.go b/services/core/tests/integration/subagent_resources_test.go index ad2f964b7..8dafc6b78 100644 --- a/services/core/tests/integration/subagent_resources_test.go +++ b/services/core/tests/integration/subagent_resources_test.go @@ -32,7 +32,7 @@ func TestSubagentResourcesNativeOwnershipLifecycleAndRecovery(t *testing.T) { journal := sessionExecution(t, owner.lease) ctx := t.Context() tenant, session := newSubagentSession(t, s) - host := registerAgentHost(t, s, tenant) + host := registerAgentHost(t, s) err := sessionExecution(t, owner.lease).BindSessionDevice(ctx, tenant, session.ID, host.ID) if err != nil { t.Fatal(err) diff --git a/services/core/tests/integration/subagent_visibility_public_test.go b/services/core/tests/integration/subagent_visibility_public_test.go index cf07a75ab..95425f9cf 100644 --- a/services/core/tests/integration/subagent_visibility_public_test.go +++ b/services/core/tests/integration/subagent_visibility_public_test.go @@ -121,7 +121,7 @@ func TestSubagentVisibilityPublic(t *testing.T) { t.Fatal(page, err) } root := page.Turns[0].ID - host := registerAgentHost(t, s, tenant) + host := registerAgentHost(t, s) if err = leased.Sessions.BindSessionDevice(ctx, tenant, session, host.ID); err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/turn_events_test.go b/services/core/tests/integration/turn_events_test.go index 6ab52db0f..0324d81d6 100644 --- a/services/core/tests/integration/turn_events_test.go +++ b/services/core/tests/integration/turn_events_test.go @@ -101,8 +101,7 @@ func TestEventLimitStillAllowsTerminalFailure(t *testing.T) { if err != nil { t.Fatal(err) } - _, pool := testStore(t) - defer pool.Close() + pool := h.s.pool if _, err := pool.Exec(ctx, "UPDATE turns SET event_bytes=33554432 WHERE id=$1", input.TurnID); err != nil { t.Fatal(err) } diff --git a/services/core/tests/integration/worker_input_race_test.go b/services/core/tests/integration/worker_input_race_test.go index b6d926ab5..5adce5a12 100644 --- a/services/core/tests/integration/worker_input_race_test.go +++ b/services/core/tests/integration/worker_input_race_test.go @@ -39,7 +39,7 @@ func TestWorkerInputReadSkipsConcurrentlyCancelledCandidate(t *testing.T) { h := newDispatchHarness(t) candidate := h.message("candidate", "queued") candidateSession := h.session.ID - _, pool := testStore(t) + pool := h.s.pool cfg := pool.Config() mutated := make(chan error, 1) cfg.ConnConfig.Tracer = &beforeInputRead{run: func() { diff --git a/services/core/tests/integration/worker_lease_loss_test.go b/services/core/tests/integration/worker_lease_loss_test.go index 362caa06f..989bdff32 100644 --- a/services/core/tests/integration/worker_lease_loss_test.go +++ b/services/core/tests/integration/worker_lease_loss_test.go @@ -13,7 +13,7 @@ import ( func TestWorkerLeaseLossLeavesUncertainWorkForSuccessor(t *testing.T) { h := newDispatchHarness(t) - _, pool := testStore(t) + pool := h.s.pool h.write("", proto.TypeHeartbeat, proto.HeartbeatPayload{HomeRemoval: proto.CapabilityUnsupported, SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "codex", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{TextVerbosity: proto.CapabilitySupported, EnvironmentNone: proto.CapabilitySupported})}}}) h.session = publicSession(t, h, "active") queued := publicSession(t, h, "queued") diff --git a/services/core/tests/integration/worker_wakeup_test.go b/services/core/tests/integration/worker_wakeup_test.go index 6f03ce2b7..25b4d5c1f 100644 --- a/services/core/tests/integration/worker_wakeup_test.go +++ b/services/core/tests/integration/worker_wakeup_test.go @@ -34,7 +34,7 @@ func TestWorkerSchedulerCommittedAdmissionWakesBeforeMaintenance(t *testing.T) { h := newDispatchHarness(t) enableWorkerEnvironment(t, h) h.session = publicSession(t, h, "wakeup") - _, pool := testStore(t) + pool := h.s.pool trace := &schedulerQueryOrder{first: make(chan string, 1)} config := pool.Config() config.ConnConfig.Tracer = trace