From 631e36a23a6febf20381ff12e7e9b2f88e0fee4b Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Fri, 23 Oct 2026 18:38:08 +0000 Subject: [PATCH 1/3] Remove the unused workspace Observe operation - Delete Observe from workspacefs.Control; Core never called it through the interface. - Keep the NFS Filesystem.Observe method, which Resolve calls directly. - Drop the test fixtures' Observe methods. - Update the workspace provider protocol and its Chinese translation. --- docs/workspace-provider.md | 9 ++++----- docs/zh/workspace-provider.md | 11 +++++------ services/core/internal/workspacefs/workspacefs.go | 2 -- services/core/internal/workspaces/execution_test.go | 3 --- .../core/tests/integration/workspace_fixture_test.go | 3 --- 5 files changed, 9 insertions(+), 19 deletions(-) diff --git a/docs/workspace-provider.md b/docs/workspace-provider.md index 2cb4b95ba..a4dc9f1e4 100644 --- a/docs/workspace-provider.md +++ b/docs/workspace-provider.md @@ -2,7 +2,7 @@ title: Workspace filesystem protocol --- -The workspace filesystem protocol connects Core and sandbox nodes to the adapter that owns durable Environment storage. Core creates, observes and deletes storage objects; a node resolves an object to a host directory that its [Sandbox Provider protocol](sandbox-provider.md) attaches to compute. It is for authors of filesystem adapters and Sandbox Providers. +The workspace filesystem protocol connects Core and sandbox nodes to the adapter that owns durable Environment storage. Core creates and deletes storage objects; a node resolves an object to a host directory that its [Sandbox Provider protocol](sandbox-provider.md) attaches to compute. It is for authors of filesystem adapters and Sandbox Providers. | | | | --- | --- | @@ -21,7 +21,7 @@ Core selects and validates the combination of a Sandbox Provider and a filesyste ## Implement an adapter 1. Define `Parameters` and the optional credential, and implement `Normalize` ([Identity and configuration](#identity-and-configuration)). -2. Implement `Declaration`, `Check`, `Create`, `Observe` and `Delete` in `Control`, and `Resolve` in `Resolver` ([Operations and outcomes](#operations-and-outcomes)). +2. Implement `Declaration`, `Check`, `Create` and `Delete` in `Control`, and `Resolve` in `Resolver` ([Operations and outcomes](#operations-and-outcomes)). 3. Declare the attachment kind, user xattr support and quota enforcement ([Capability validation](#capability-validation)). 4. Keep one writer per object and delete only on Core's authorization ([Lifetime and filesystem scope](#lifetime-and-filesystem-scope)). @@ -50,14 +50,13 @@ Core selects and validates the combination of a Sandbox Provider and a filesyste An adapter registers one implementation of `Adapter` with three separate, mandatory roles. `Normalize(parameters, credential)` validates and canonicalizes parameters and encodes the credential input without I/O; a missing required credential or a credential the parameters do not use is `ErrInvalid`, and an encoded credential is valid input that encodes to itself, so Core can re-select a retained configuration with its stored credential. `Control(ConfigurationRecord)` constructs the control adapter in Core with the decrypted credential. `Resolver(Configuration)` constructs the node resolver from public configuration only. Construction performs no filesystem or network operation. -Every control adapter implements `Declaration`, `Check`, `Create`, `Observe` and `Delete`. Every node attachment adapter implements `Resolve`. There is no optional side interface, implicit adapter substitution or nil implementation fallback. +Every control adapter implements `Declaration`, `Check`, `Create` and `Delete`. Every node attachment adapter implements `Resolve`. There is no optional side interface, implicit adapter substitution or nil implementation fallback. | Operation | Required meaning | | --- | --- | | `Declaration()` | Describe the selected configuration's verified attachment, user xattr and quota behavior. | | `Check(ctx)` | Validate configuration and availability without creating an object. | | `Create(ctx, reference, capacityMiB)` | Create the owned object or converge on its existing attachment without overwriting data; retries and concurrent calls use the same immutable reference and admitted capacity. A positive capacity is enforced, and verified on every retry, before success. | -| `Observe(ctx, reference)` | Return an existing object's bound attachment without creating, repairing or preparing it. | | `Delete(ctx, reference)` | Converge on terminal deletion of the verified owned object after Core has authorized deletion and confirmed compute stop. | | `Resolve(ctx, binding)` | Validate native ownership and resolve the attachment for local compute. | @@ -72,7 +71,7 @@ Errors support `errors.Is`. Adapters preserve these outcomes when wrapping error | `ErrNotFound` | The object is missing | | `ErrUnsupported` | The combination is unsupported | -A missing observation does not prove an earlier mutation settled. A timeout or canceled context ends the caller's wait; it does not prove native stop or rollback. Core retains ownership on unknown outcomes. It may retry `Create` or `Delete` with the same persisted immutable reference and configuration, including after `ErrUnconfirmed`; it must never substitute a new object identity to retry an uncertain operation. +A missing object does not prove an earlier mutation settled. A timeout or canceled context ends the caller's wait; it does not prove native stop or rollback. Core retains ownership on unknown outcomes. It may retry `Create` or `Delete` with the same persisted immutable reference and configuration, including after `ErrUnconfirmed`; it must never substitute a new object identity to retry an uncertain operation. Same-reference retry and concurrent convergence are mandatory adapter guarantees. `Create` preserves any existing object data and returns the attachment for that owned object; it never reinitializes a live object. Repeated and concurrent `Delete` calls converge on the same terminal deletion. Once terminal deletion is recorded, concurrent, delayed or retried `Create` calls must not resurrect the object, and `Resolve` must not expose it. Retained deletion metadata and further cleanup of settled in-flight operations are permitted. These guarantees apply to storage lifecycle operations; they do not establish mutation settlement at timeout or distributed compute fencing. diff --git a/docs/zh/workspace-provider.md b/docs/zh/workspace-provider.md index 524801276..204746617 100644 --- a/docs/zh/workspace-provider.md +++ b/docs/zh/workspace-provider.md @@ -1,10 +1,10 @@ --- title: 工作区文件系统协议 source: docs/workspace-provider.md -source_hash: 2afa36a287c1b4268fef271f18ae094f74d1fc61d8c841bfbe21e9dbabd86cd6 +source_hash: 06a170d6966d003c9294254c2f2e851d26916731c5c227013d272fc86eb454eb --- -工作区文件系统协议连接 Core 和沙箱节点与拥有 Environment 持久存储的适配器。Core 创建、观察和删除存储对象;节点将对象解析为主机目录,由其[沙箱提供商协议](sandbox-provider.md)挂载到计算资源。本文面向文件系统适配器和沙箱提供商的作者。 +工作区文件系统协议连接 Core 和沙箱节点与拥有 Environment 持久存储的适配器。Core 创建和删除存储对象;节点将对象解析为主机目录,由其[沙箱提供商协议](sandbox-provider.md)挂载到计算资源。本文面向文件系统适配器和沙箱提供商的作者。 | | | | --- | --- | @@ -23,7 +23,7 @@ Core 在分配任一资源前,选择并验证沙箱提供商与文件系统适 ## 实现适配器 {#implement-an-adapter} 1. 定义 `Parameters` 和可选凭据,并实现 `Normalize`([标识与配置](#identity-and-configuration))。 -2. 在 `Control` 中实现 `Declaration`、`Check`、`Create`、`Observe` 和 `Delete`,在 `Resolver` 中实现 `Resolve`([操作与结果](#operations-and-outcomes))。 +2. 在 `Control` 中实现 `Declaration`、`Check`、`Create` 和 `Delete`,在 `Resolver` 中实现 `Resolve`([操作与结果](#operations-and-outcomes))。 3. 声明挂载类型、用户扩展属性支持和配额强制执行([能力验证](#capability-validation))。 4. 每个对象保持单一写入方,仅在 Core 授权时删除([生命周期与文件系统范围](#lifetime-and-filesystem-scope))。 @@ -51,14 +51,13 @@ Core 在分配任一资源前,选择并验证沙箱提供商与文件系统适 适配器注册一个 `Adapter` 实现,其三个角色相互独立且均为必需。`Normalize(parameters, credential)` 在不执行 I/O 的情况下验证并规范化参数、编码凭据输入;缺少必需凭据或提供参数不使用的凭据均为 `ErrInvalid`;编码后的凭据是有效输入且编码结果不变,因此 Core 可以使用已存储的凭据重新选择保留的配置。`Control(ConfigurationRecord)` 在 Core 中使用解密后的凭据构造控制适配器。`Resolver(Configuration)` 仅根据公开配置构造节点解析器。构造过程不执行文件系统或网络操作。 -每个控制适配器实现 `Declaration`、`Check`、`Create`、`Observe` 和 `Delete`;每个节点挂载适配器实现 `Resolve`。不得使用可选旁路接口、隐式替换适配器或 nil 实现回退。 +每个控制适配器实现 `Declaration`、`Check`、`Create` 和 `Delete`;每个节点挂载适配器实现 `Resolve`。不得使用可选旁路接口、隐式替换适配器或 nil 实现回退。 | 操作 | 必需语义 | | --- | --- | | `Declaration()` | 描述所选配置已经验证的挂载、用户扩展属性和配额行为。 | | `Check(ctx)` | 验证配置和可用性,不创建对象。 | | `Create(ctx, reference, capacityMiB)` | 创建归属对象或收敛到其现有挂载凭据,不覆盖数据;重试与并发调用使用同一不可变引用和获准容量。正容量须在成功返回前实际强制执行,并在每次重试时重新验证。 | -| `Observe(ctx, reference)` | 返回现有对象的绑定挂载凭据,不创建、修复或准备对象。 | | `Delete(ctx, reference)` | Core 授权删除且确认计算资源停止后,收敛到已验证归属对象的终态删除。 | | `Resolve(ctx, binding)` | 验证原生归属并为本地计算解析挂载。 | @@ -73,7 +72,7 @@ Core 在分配任一资源前,选择并验证沙箱提供商与文件系统适 | `ErrNotFound` | 对象不存在 | | `ErrUnsupported` | 不支持的组合 | -观察到不存在不证明先前变更已结束。超时或上下文取消只结束调用方的等待,不证明原生操作停止或回滚。结果未知时 Core 保留归属。Core 可以使用同一持久化不可变引用和配置重试 `Create` 或 `Delete`,包括收到 `ErrUnconfirmed` 后;绝不能通过替换新对象标识重试结果不确定的操作。 +对象不存在不证明先前变更已结束。超时或上下文取消只结束调用方的等待,不证明原生操作停止或回滚。结果未知时 Core 保留归属。Core 可以使用同一持久化不可变引用和配置重试 `Create` 或 `Delete`,包括收到 `ErrUnconfirmed` 后;绝不能通过替换新对象标识重试结果不确定的操作。 同一引用的重试与并发收敛是适配器必须提供的保证。`Create` 保留现有对象数据并返回该归属对象的挂载凭据,绝不重新初始化存活对象。重复或并发的 `Delete` 调用收敛到同一终态删除。一旦记录终态删除,并发、延迟或重试的 `Create` 均不得使对象复活,`Resolve` 也不得暴露该对象。允许保留删除元数据,并在先前进行中的操作结束后继续清理。这些保证适用于存储生命周期操作,不表示超时时变更已结束,也不提供分布式计算隔离。 diff --git a/services/core/internal/workspacefs/workspacefs.go b/services/core/internal/workspacefs/workspacefs.go index 18dacc6bd..5f9507122 100644 --- a/services/core/internal/workspacefs/workspacefs.go +++ b/services/core/internal/workspacefs/workspacefs.go @@ -102,8 +102,6 @@ type Control interface { // Declaration.CapacityQuota; the object's allocated bytes and inodes are then // bounded by it before Create returns, and a retry verifies that bound. Create(ctx context.Context, reference Reference, capacityMiB uint32) (Attachment, error) - // Observe returns ErrNotFound for absence, which does not settle a mutation. - Observe(context.Context, Reference) (Attachment, error) // Delete retries and concurrent calls for the same Reference converge on // terminal deletion. Concurrent or late Create cannot resurrect the object. Delete(context.Context, Reference) error diff --git a/services/core/internal/workspaces/execution_test.go b/services/core/internal/workspaces/execution_test.go index 5c04a1c6c..30144f6db 100644 --- a/services/core/internal/workspaces/execution_test.go +++ b/services/core/internal/workspaces/execution_test.go @@ -125,9 +125,6 @@ func (c *workspaceControl) Create(_ context.Context, r workspacefs.Reference, ca } return workspacefs.Attachment{Reference: r, ConfigurationID: c.store.record.Configuration.ID, Kind: workspacefs.AttachmentHostDirectory, Native: []byte(`{}`)}, c.createErr } -func (c *workspaceControl) Observe(ctx context.Context, r workspacefs.Reference) (workspacefs.Attachment, error) { - return c.Create(ctx, r, 0) -} func (c *workspaceControl) Delete(context.Context, workspacefs.Reference) error { c.deletes++ return c.deleteErr diff --git a/services/core/tests/integration/workspace_fixture_test.go b/services/core/tests/integration/workspace_fixture_test.go index 37c7bc87a..835226dd9 100644 --- a/services/core/tests/integration/workspace_fixture_test.go +++ b/services/core/tests/integration/workspace_fixture_test.go @@ -57,9 +57,6 @@ func (fixtureControl) Declaration() workspacefs.Declaration { } func (fixtureControl) Check(context.Context) error { return nil } func (c fixtureControl) Create(_ context.Context, r workspacefs.Reference, _ uint32) (workspacefs.Attachment, error) { - return c.Observe(context.Background(), r) -} -func (c fixtureControl) Observe(_ context.Context, r workspacefs.Reference) (workspacefs.Attachment, error) { return workspacefs.Attachment{Reference: r, ConfigurationID: c.configuration, Kind: workspacefs.AttachmentHostDirectory, Native: json.RawMessage(`{}`)}, nil } func (fixtureControl) Delete(context.Context, workspacefs.Reference) error { return nil } From d264132cf3f7686c6632d6a1cfc70ac757991721 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Fri, 23 Oct 2026 18:38:08 +0000 Subject: [PATCH 2/3] Delete unused Core Go code - Remove the unreachable responseHeadersWithErrors and microsandbox Labels. - Remove domain Reader interfaces used only in assertions, and narrow sessions.Reader and its families to the methods called through it. - Remove interface methods never called through their interface in deployment, vaults and skills. - Remove the device WorkspaceID chain, the bootstrap workspace_id and protocol_version fields, and the unread heartbeat ts, active_requests and daemon_version fields (Runtime protocol 0.22.0). - Remove write-only fields: NewAllocation.EnvironmentID, SweepResult times, EnrolledRuntimeBinding.SessionID, EnvironmentFileWrite.EnvironmentID and SessionID; move TurnEvent into its only test. - Remove the request_timeout provider observation category and the system provisioning step. - Drop the public_execution_turns view and the request_timeout error-code constraint value in a forward migration. - Correct the Core errors anchor and the Core implementation notes. --- apps/daemon/internal/cli/connect.go | 3 -- apps/daemon/internal/cli/connect_suspend.go | 2 +- apps/daemon/internal/transport/bootstrap.go | 2 - .../internal/transport/bootstrap_test.go | 5 +-- apps/daemon/internal/transport/ws.go | 2 +- apps/daemon/internal/transport/ws_test.go | 4 +- apps/web/src/i18n/locales/en/diagnostics.ts | 4 +- .../web/src/i18n/locales/zh-CN/diagnostics.ts | 4 +- contracts/agents-api/core-errors.md | 3 +- contracts/agents-api/core.openapi.yaml | 4 -- contracts/agents-api/machine-api.md | 2 +- contracts/agents-api/model-execution.md | 2 +- contracts/agents-api/session-diagnostics.md | 2 +- contracts/agents-api/zh/core-errors.md | 5 +-- contracts/agents-api/zh/machine-api.md | 4 +- contracts/agents-api/zh/model-execution.md | 4 +- .../agents-api/zh/session-diagnostics.md | 4 +- docs/runtime-protocol.md | 4 +- docs/zh/runtime-protocol.md | 6 +-- internal/agentdaemon/proto/engine_failure.go | 2 +- internal/agentdaemon/proto/inbound.go | 3 -- internal/agentdaemon/proto/version.go | 2 +- .../agents-client/src/generated/core-api.ts | 6 +-- .../src/session-diagnostics.test.ts | 2 +- .../agents-client/src/session-diagnostics.ts | 8 ++-- services/core/IMPLEMENTATION.md | 10 ++--- .../core/cmd/server/daemon_bootstrap_test.go | 2 +- services/core/internal/adminaudit/reader.go | 7 ---- services/core/internal/agents/storage.go | 11 ------ .../core/internal/api/core_errors_test.go | 4 +- services/core/internal/api/routing.go | 6 +-- services/core/internal/api/routing_test.go | 4 +- .../core/internal/api/session_diagnostics.go | 2 +- .../api/turn_diagnostic_failure_test.go | 2 +- .../core/internal/api/write_audit_test.go | 4 +- .../deployment_provider_observations.sql | 2 +- .../deployment_provider_observations.sql.go | 2 +- services/core/internal/db/sqlc/models.go | 14 ------- .../core/internal/deployment/allocation.go | 2 +- .../core/internal/deployment/allocations.go | 2 +- services/core/internal/deployment/storage.go | 7 ---- .../internal/environmenttemplates/storage.go | 9 ----- services/core/internal/files/storage.go | 13 ------- .../modelconfiguration/observation.go | 3 +- .../internal/modelconfiguration/storage.go | 5 --- .../testdata/observation_cases.json | 1 - .../persistence/postgres/agentpg/store.go | 1 - .../persistence/postgres/auditpg/store.go | 7 ---- .../persistence/postgres/filepg/filepg.go | 1 - .../postgres/modelconfigurationpg/store.go | 1 - .../postgres/projectpg/projectpg.go | 1 - .../postgres/sessionpg/artifacts.go | 1 - .../persistence/postgres/sessionpg/devices.go | 5 +-- .../sessionpg/execution_file_writes.go | 12 +++--- .../postgres/sessionpg/item_reads.go | 2 - .../postgres/sessionpg/subagent_reads.go | 2 - .../persistence/postgres/templatepg/store.go | 1 - services/core/internal/projects/storage.go | 18 --------- .../core/internal/runtimedevice/credential.go | 1 - services/core/internal/runtimedevice/state.go | 4 -- .../runtimeenrollment/connection_test.go | 2 +- services/core/internal/runtimegateway/auth.go | 2 - .../core/internal/runtimegateway/auth_test.go | 3 +- .../runtimegateway/bootstrap_url_test.go | 2 +- .../runtimegateway/cancellation_test.go | 6 +-- .../runtimegateway/capabilities_test.go | 4 +- .../core/internal/runtimegateway/handler.go | 4 +- .../internal/runtimegateway/handler_test.go | 2 +- .../runtimegateway/preparation_test.go | 4 +- .../internal/runtimegateway/registry_test.go | 14 +++---- .../runtimegateway/runtime_prepare_test.go | 20 +++++----- .../core/internal/runtimegateway/session.go | 7 +--- .../internal/runtimegateway/session_test.go | 39 +++++++++---------- .../runtimegateway/subscription_test.go | 4 +- .../runtimegateway/suspension_test.go | 6 +-- .../core/internal/runtimegateway/wire_test.go | 2 +- .../workspace_directory_test.go | 4 +- .../runtimegateway/workspace_export_test.go | 6 +-- .../runtimegateway/workspace_read_test.go | 12 +++--- .../runtimegateway/workspace_write_test.go | 4 +- services/core/internal/runtimeobs/sampler.go | 6 +-- .../core/internal/runtimeobs/sampler_test.go | 2 - .../internal/sandbox/microsandbox/identity.go | 3 -- services/core/internal/sessions/admin.go | 16 -------- services/core/internal/sessions/artifacts.go | 19 --------- services/core/internal/sessions/devices.go | 8 +--- .../core/internal/sessions/devices_test.go | 2 +- .../core/internal/sessions/environment.go | 11 +++--- .../sessions/execution_file_writes_test.go | 4 +- .../internal/sessions/executor_credentials.go | 17 -------- services/core/internal/sessions/items.go | 11 ------ services/core/internal/sessions/reader.go | 17 ++++---- services/core/internal/sessions/reads.go | 23 ----------- services/core/internal/sessions/subagents.go | 25 ------------ services/core/internal/sessions/turn.go | 12 ------ services/core/internal/skills/storage.go | 1 - services/core/internal/vaults/storage.go | 1 - services/core/internal/writeaudit/reader.go | 8 ---- .../000004_remove_unused_schema.sql | 4 ++ .../core/tests/integration/devices_test.go | 2 +- .../core/tests/integration/fixtures_test.go | 4 +- .../projects_public_fixture_test.go | 2 +- .../public_handler_fixture_test.go | 4 +- .../integration/turn_journal_reads_test.go | 14 +++++-- 104 files changed, 170 insertions(+), 451 deletions(-) create mode 100644 services/core/migrations/000004_remove_unused_schema.sql diff --git a/apps/daemon/internal/cli/connect.go b/apps/daemon/internal/cli/connect.go index e17ac4fba..6185d546e 100644 --- a/apps/daemon/internal/cli/connect.go +++ b/apps/daemon/internal/cli/connect.go @@ -345,9 +345,6 @@ func pumpConn(parentCtx context.Context, conn *transport.Conn, registry *agent.R conn.StartHeartbeats(parentCtx, boot.HeartbeatInterval(), func() proto.HeartbeatPayload { kinds := advertisedKinds(registry, local.CanExport()) return proto.HeartbeatPayload{ - Timestamp: time.Now().Unix(), - ActiveRequests: router.ActiveRuns(), - DaemonVersion: Version, SupportedAgentKinds: kinds, } }, obslog.Bg().With("component", "heartbeat")) diff --git a/apps/daemon/internal/cli/connect_suspend.go b/apps/daemon/internal/cli/connect_suspend.go index d1b4dd8a0..12068bb07 100644 --- a/apps/daemon/internal/cli/connect_suspend.go +++ b/apps/daemon/internal/cli/connect_suspend.go @@ -153,7 +153,7 @@ func (s *suspendedRouter) reconnectSuspension(ctx context.Context, dial transpor func (s *suspendedRouter) heartbeats(ctx context.Context, conn *transport.Conn, boot *transport.BootstrapResponse, discovery agentCLIDiscovery) { conn.StartHeartbeats(ctx, boot.HeartbeatInterval(), func() proto.HeartbeatPayload { kinds := advertisedKinds(s.registry, s.local.CanExport()) - return proto.HeartbeatPayload{Timestamp: time.Now().Unix(), ActiveRequests: s.router.ActiveRuns(), DaemonVersion: Version, SupportedAgentKinds: kinds} + return proto.HeartbeatPayload{SupportedAgentKinds: kinds} }, obslog.Bg()) } diff --git a/apps/daemon/internal/transport/bootstrap.go b/apps/daemon/internal/transport/bootstrap.go index b684fee5d..c40e76470 100644 --- a/apps/daemon/internal/transport/bootstrap.go +++ b/apps/daemon/internal/transport/bootstrap.go @@ -25,10 +25,8 @@ import ( // BootstrapResponse mirrors gateway/handler.go's Bootstrap response. type BootstrapResponse struct { DeviceID string `json:"device_id"` - WorkspaceID string `json:"workspace_id"` WSURL string `json:"ws_url"` HeartbeatSeconds int `json:"heartbeat_seconds"` - ProtocolVersion string `json:"protocol_version"` } // HeartbeatInterval defends against the server returning 0 — a zero diff --git a/apps/daemon/internal/transport/bootstrap_test.go b/apps/daemon/internal/transport/bootstrap_test.go index 1b8467f4f..66575b941 100644 --- a/apps/daemon/internal/transport/bootstrap_test.go +++ b/apps/daemon/internal/transport/bootstrap_test.go @@ -32,10 +32,8 @@ func TestBootstrapSendsBearerAndDeviceID(t *testing.T) { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]any{ "device_id": "rt_abc", - "workspace_id": "ws_xyz", "ws_url": "wss://example/agent-daemon/ws", "heartbeat_seconds": 15, - "protocol_version": "0.3.0", }) })) defer srv.Close() @@ -44,7 +42,7 @@ func TestBootstrapSendsBearerAndDeviceID(t *testing.T) { if err != nil { t.Fatalf("Bootstrap: %v", err) } - if resp.DeviceID != "rt_abc" || resp.WorkspaceID != "ws_xyz" { + if resp.DeviceID != "rt_abc" { t.Errorf("unexpected response: %+v", resp) } if resp.WSURL != "wss://example/agent-daemon/ws" { @@ -68,7 +66,6 @@ func TestBootstrapHeartbeatIntervalDefaultsToFifteen(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode(map[string]any{ "device_id": "rt", - "workspace_id": "ws", "ws_url": "", "heartbeat_seconds": 0, // server forgot to set it }) diff --git a/apps/daemon/internal/transport/ws.go b/apps/daemon/internal/transport/ws.go index 974078bc7..39d165a88 100644 --- a/apps/daemon/internal/transport/ws.go +++ b/apps/daemon/internal/transport/ws.go @@ -186,7 +186,7 @@ func (c *Conn) Close() error { // StartHeartbeats kicks off a ticker that calls payloadFn every // interval and Sends the resulting HeartbeatPayload. Returns // immediately. Caller-controlled because the heartbeat carries fields -// (active_requests, supported_agent_kinds) only the agent layer knows. +// (supported_agent_kinds) only the agent layer knows. // Nil logger falls back to log.Bg(). func (c *Conn) StartHeartbeats(parentCtx context.Context, interval time.Duration, payloadFn func() proto.HeartbeatPayload, logger *slog.Logger) { if interval <= 0 || payloadFn == nil { diff --git a/apps/daemon/internal/transport/ws_test.go b/apps/daemon/internal/transport/ws_test.go index a031e8527..37d676501 100644 --- a/apps/daemon/internal/transport/ws_test.go +++ b/apps/daemon/internal/transport/ws_test.go @@ -264,7 +264,7 @@ func TestStartHeartbeatsTicks(t *testing.T) { var calls atomic.Int32 conn.StartHeartbeats(context.Background(), 30*time.Millisecond, func() proto.HeartbeatPayload { calls.Add(1) - return proto.HeartbeatPayload{Timestamp: time.Now().Unix(), DaemonVersion: "0.0.0-dev"} + return proto.HeartbeatPayload{} }, nil) deadline := time.Now().Add(time.Second) @@ -306,7 +306,7 @@ func TestStartHeartbeatsSendsImmediately(t *testing.T) { defer cancel() conn.StartHeartbeats(hbCtx, time.Hour, func() proto.HeartbeatPayload { calls.Add(1) - return proto.HeartbeatPayload{Timestamp: time.Now().Unix(), DaemonVersion: "0.0.0-dev"} + return proto.HeartbeatPayload{} }, nil) deadline := time.Now().Add(2 * time.Second) diff --git a/apps/web/src/i18n/locales/en/diagnostics.ts b/apps/web/src/i18n/locales/en/diagnostics.ts index 6ce80d5a7..89b8f3741 100644 --- a/apps/web/src/i18n/locales/en/diagnostics.ts +++ b/apps/web/src/i18n/locales/en/diagnostics.ts @@ -17,7 +17,6 @@ export const diagnostics = { server_error: "The model provider reported a server error.", invalid_request: "The model provider rejected the request.", resource_not_found: "The model provider could not find the model.", - request_timeout: "The model provider request timed out.", context_length_exceeded: "The input exceeds the model's context limit.", cyber_policy: "The model provider rejected the input under its safety policy.", connection_failed: "The connection to the model provider failed.", @@ -44,7 +43,6 @@ export const diagnostics = { server_error: "Retry later.", invalid_request: "Check the model and parameters, starting with the model on the Session page. Without a model provider set on the Session or its saved Agent, a hosted sandbox Session uses the default in Settings → Models and credentials.", resource_not_found: "Check the model on the Session page. Without a model provider set on the Session or its saved Agent, a hosted sandbox Session uses the default in Settings → Models and credentials.", - request_timeout: "Retry later.", context_length_exceeded: "Start a new Session or shorten the input.", cyber_policy: "Change the input before sending it again, or start a new Session.", connection_failed: "Check the Base URL of the Session's model provider. Without one set on the Session or its saved Agent, a hosted sandbox Session uses the default in Settings → Models and credentials.", @@ -61,7 +59,7 @@ export const diagnostics = { exitCode: "Exit code {{code}}.", httpStatus: "HTTP {{status}}.", unreachableHost: "Then check that the URL's host resolves and is reachable from the Session's sandbox, or from the host of a self-hosted Session.", - step: { setup: "setup", python: "Python dependencies", npm: "npm dependencies", system: "system packages", file: "file preparation", skill: "Skill preparation" }, + step: { setup: "setup", python: "Python dependencies", npm: "npm dependencies", file: "file preparation", skill: "Skill preparation" }, unavailable: "Failure details unavailable", unavailableHelp: "The diagnostics read failed or no longer matches this status. Refresh to check again.", checking: "Checking failure details", diff --git a/apps/web/src/i18n/locales/zh-CN/diagnostics.ts b/apps/web/src/i18n/locales/zh-CN/diagnostics.ts index 130ac4caf..252f679a3 100644 --- a/apps/web/src/i18n/locales/zh-CN/diagnostics.ts +++ b/apps/web/src/i18n/locales/zh-CN/diagnostics.ts @@ -17,7 +17,6 @@ export const diagnostics = { server_error: "模型提供商发生了服务器错误。", invalid_request: "模型提供商拒绝了请求。", resource_not_found: "模型提供商找不到该模型。", - request_timeout: "模型提供商请求超时。", context_length_exceeded: "输入超出模型的上下文上限。", cyber_policy: "模型提供商因安全策略拒绝了输入。", connection_failed: "与模型提供商的连接失败。", @@ -44,7 +43,6 @@ export const diagnostics = { server_error: "请稍后重试。", invalid_request: "请检查模型和参数,先看 Session 页显示的模型。Session 及其保存的 Agent 都未设置模型提供商时,托管沙箱 Session 使用“设置 → 模型与凭据”中的默认配置。", resource_not_found: "请检查 Session 页显示的模型。Session 及其保存的 Agent 都未设置模型提供商时,托管沙箱 Session 使用“设置 → 模型与凭据”中的默认配置。", - request_timeout: "请稍后重试。", context_length_exceeded: "请新建 Session 或缩短输入。", cyber_policy: "请修改输入后再发送,或新建 Session。", connection_failed: "请检查该 Session 所用模型提供商的 Base URL。Session 及其保存的 Agent 都未设置模型提供商时,托管沙箱 Session 使用“设置 → 模型与凭据”中的默认配置。", @@ -61,7 +59,7 @@ export const diagnostics = { exitCode: "退出码 {{code}}。", httpStatus: "HTTP {{status}}。", unreachableHost: "再确认 Session 的沙箱(自托管 Session 则为其主机)能解析并访问该 URL 的主机。", - step: { setup: "设置", python: "Python 依赖", npm: "npm 依赖", system: "系统软件包", file: "文件准备", skill: "Skill 准备" }, + step: { setup: "设置", python: "Python 依赖", npm: "npm 依赖", file: "文件准备", skill: "Skill 准备" }, unavailable: "失败详情不可用", unavailableHelp: "诊断读取失败或与当前状态不一致,请刷新后重试。", checking: "正在读取失败详情", diff --git a/contracts/agents-api/core-errors.md b/contracts/agents-api/core-errors.md index 549bb2396..821de8edf 100644 --- a/contracts/agents-api/core-errors.md +++ b/contracts/agents-api/core-errors.md @@ -38,7 +38,7 @@ These have null `param` and no `details`. A Core `401 invalid_admin_key` therefo ## Sandbox provider verification -A `POST` or `PUT /core/v1/sandbox/deployment` ([sandbox deployment](./sandbox-deployment.md#reset)) whose provider verifies a credential or configuration, as E2B does, fails with these fixed errors. None returns provider text, a template name, a key or a resource count. +A `POST` or `PUT /core/v1/sandbox/deployment` ([sandbox deployment](./sandbox-deployment.md#e2b-key-replacement)) whose provider verifies a credential or configuration, as E2B does, fails with these fixed errors. None returns provider text, a template name, a key or a resource count. | HTTP | Code | Meaning | `param` | | --- | --- | --- | --- | @@ -118,7 +118,6 @@ The [Session diagnostics](./session-diagnostics.md) routes return these categori | `server_error` | Native server failure classification | | `invalid_request` | Native request rejection | | `resource_not_found` | Native resource/model not found | -| `request_timeout` | Reserved neutral timeout category; no current adapter producer | | `context_length_exceeded` | Native context limit classification | | `cyber_policy` | Native cyber policy rejection | | `connection_failed` | Native connection failure; params contain `http_status`, an integer in 100–599 or null | diff --git a/contracts/agents-api/core.openapi.yaml b/contracts/agents-api/core.openapi.yaml index 4d5d77ce9..182963b6d 100644 --- a/contracts/agents-api/core.openapi.yaml +++ b/contracts/agents-api/core.openapi.yaml @@ -373,7 +373,6 @@ definitions: - server_error - invalid_request - resource_not_found - - request_timeout - context_length_exceeded - cyber_policy - connection_failed @@ -832,7 +831,6 @@ definitions: - server_error - invalid_request - resource_not_found - - request_timeout - context_length_exceeded - cyber_policy - connection_failed @@ -1748,7 +1746,6 @@ definitions: - server_overloaded - server_error - resource_not_found - - request_timeout - invalid_request type: string x-enum-varnames: @@ -1759,7 +1756,6 @@ definitions: - ProviderServerOverloaded - ProviderServerError - ProviderResourceNotFound - - ProviderRequestTimeout - ProviderInvalidRequest modelprovider.Protocol: enum: diff --git a/contracts/agents-api/machine-api.md b/contracts/agents-api/machine-api.md index 7c95e5c85..81e7916d0 100644 --- a/contracts/agents-api/machine-api.md +++ b/contracts/agents-api/machine-api.md @@ -101,7 +101,7 @@ 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. +`POST /api/v1/agent-daemon/bootstrap` with the daemon credential and `{"device_id": "…"}` returns `device_id`, `ws_url` (derived from `OAC_PUBLIC_URL`, never from request headers) and `heartbeat_seconds`. The daemon then dials `ws_url` as the [Core–Runtime protocol](../../docs/runtime-protocol.md#ownership-and-connection) describes. 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. diff --git a/contracts/agents-api/model-execution.md b/contracts/agents-api/model-execution.md index 159fd06af..7be69e624 100644 --- a/contracts/agents-api/model-execution.md +++ b/contracts/agents-api/model-execution.md @@ -141,7 +141,7 @@ Session creation decrypts the default of the resolved Harness and freezes it in The Harness and default-model reads include nullable `last_used_at`, `last_error_code` and `last_error_at`; PUT resets all three, even for the same bundle. - Completed root Turns record use only when their Session froze the exact current default revision. -- Failed root Turns record only the native provider codes `authentication_error`, `connection_failed`, `rate_limit_exceeded`, `usage_limit_exceeded`, `server_overloaded`, `server_error`, `resource_not_found`, `request_timeout` and `invalid_request`. Input-policy, Core or Runtime, cancelled and waiting outcomes do not count. +- Failed root Turns record only the native provider codes `authentication_error`, `connection_failed`, `rate_limit_exceeded`, `usage_limit_exceeded`, `server_overloaded`, `server_error`, `resource_not_found` and `invalid_request`. Input-policy, Core or Runtime, cancelled and waiting outcomes do not count. - A success keeps the earlier error; comparing the timestamps is a display convention. - Errors are throttled for 30 seconds regardless of code, and ordinary successes for 30 seconds; the first success after an error records recovery at once. For an unchanged revision this allows at most three effective writes in any 30-second window of database time. - Each observation has a one-second budget, including pool and row-lock acquisition, and cannot change the committed Turn. A crash, failure or throttle can omit the latest observation. diff --git a/contracts/agents-api/session-diagnostics.md b/contracts/agents-api/session-diagnostics.md index d29bf9883..0b77ab561 100644 --- a/contracts/agents-api/session-diagnostics.md +++ b/contracts/agents-api/session-diagnostics.md @@ -25,7 +25,7 @@ The object is `core.session_diagnostics`, with `session_id`, the official Sessio A hosted provisioning failure takes precedence over input activity, and input activity over the latest root Turn; the public Session response uses the same precedence. Private outcome text, native messages, provider bodies, command text, paths and credentials are never included. Unknown outcome codes become `internal_error`; no prefix matching or reason-text parsing is used. -Provisioning parameters come from the confirmed structured receipt, persisted in the failure transaction that settles the Environment and its input and records the events. `step` is `setup`, `python`, `npm`, `system`, `file`, `skill` or null. `index` is a nonnegative JSON-safe integer for setup only, otherwise null. `exit_code` is 1 to 255 for script steps, otherwise null. Details that were not recorded remain null. These private fields do not change the public failure reason or SSE. +Provisioning parameters come from the confirmed structured receipt, persisted in the failure transaction that settles the Environment and its input and records the events. `step` is `setup`, `python`, `npm`, `file`, `skill` or null. `index` is a nonnegative JSON-safe integer for setup only, otherwise null. `exit_code` is 1 to 255 for script steps, otherwise null. Details that were not recorded remain null. These private fields do not change the public failure reason or SSE. ## Turn snapshot and Item receipt timing diff --git a/contracts/agents-api/zh/core-errors.md b/contracts/agents-api/zh/core-errors.md index 2dfadfb04..8a869a4af 100644 --- a/contracts/agents-api/zh/core-errors.md +++ b/contracts/agents-api/zh/core-errors.md @@ -1,7 +1,7 @@ --- title: "Core 管理错误" source: contracts/agents-api/core-errors.md -source_hash: 9229fb99268c1b3f5a97a4c97e762faaf0e45bada46e6b1f82d28798e090d205 +source_hash: 1532078430d8e8a5570e652db60a99b282d57897ca6f3360cd0b1a94a948d02d --- `/core/v1` 上的错误使用此封装结构。`message` 是安全的英文文本;`code` 和 `param` 可以为 null。客户端依据稳定的 `code` 和可选的 `param` 进行处理,对未知代码显示 `message`,绝不解析消息,也绝不自动重试被拒绝的写操作。 @@ -40,7 +40,7 @@ Web 的控制台服务器在 `/core` 路径上发生自身故障时使用此封 ## 沙箱提供商验证 {#sandbox-provider-verification} -当 `POST` 或 `PUT /core/v1/sandbox/deployment`([sandbox deployment](sandbox-deployment.md#reset))的提供商像 E2B 一样验证凭据或配置时,请求会因以下固定错误而失败。这些错误均不会返回提供商文本、模板名称、密钥或资源数量。 +当 `POST` 或 `PUT /core/v1/sandbox/deployment`([sandbox deployment](sandbox-deployment.md#e2b-key-replacement))的提供商像 E2B 一样验证凭据或配置时,请求会因以下固定错误而失败。这些错误均不会返回提供商文本、模板名称、密钥或资源数量。 | HTTP | 代码 | 含义 | `param` | | --- | --- | --- | --- | @@ -120,7 +120,6 @@ Web 的控制台服务器在 `/core` 路径上发生自身故障时使用此封 | `server_error` | 原生服务器故障分类 | | `invalid_request` | 原生请求被拒绝 | | `resource_not_found` | 未找到原生资源或模型 | -| `request_timeout` | 保留的中性超时类别;当前没有适配器生成该类别 | | `context_length_exceeded` | 原生上下文限制分类 | | `cyber_policy` | 原生网络安全策略拒绝 | | `connection_failed` | 原生连接故障;params 包含 `http_status`,其值为 100–599 范围内的整数或 null | diff --git a/contracts/agents-api/zh/machine-api.md b/contracts/agents-api/zh/machine-api.md index 40bb4c23a..8f3a10748 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: f3e9a3cd478fcc16f466cf749e49ace8cd49bd8769e9698f6e7a0edacad3c5f7 +source_hash: f0b384850366cbbbe9a868c018963bd446b052432ed1617285f8e8bcf7c1569c --- 机器通过 `/api/v1` 调用 Core:包括沙箱节点、Runtime daemon 和自托管安装程序。各路由仅接受所列凭据,不接受 Core 密钥或项目 API 密钥;控制台登录也不授予此处权限。反向代理将 `/api/v1` 直接发送给 Core;Web 不提供这些路由。 @@ -103,7 +103,7 @@ 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`。 +`POST /api/v1/agent-daemon/bootstrap` 携带 daemon 凭据及 `{"device_id": "…"}`,返回 `device_id`、`ws_url`(从 `OAC_PUBLIC_URL` 推导,不使用请求头)和 `heartbeat_seconds`。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`。 diff --git a/contracts/agents-api/zh/model-execution.md b/contracts/agents-api/zh/model-execution.md index 3436cd82d..cdfde9b40 100644 --- a/contracts/agents-api/zh/model-execution.md +++ b/contracts/agents-api/zh/model-execution.md @@ -1,7 +1,7 @@ --- title: "模型执行" source: contracts/agents-api/model-execution.md -source_hash: 1ceb0a7799a57aa1e53b6354b0c4c6acd1002e328430c7a98dd0713f9f7a334b +source_hash: 266d585b72187dfcb7d9b3b328b9a941079bc26495f4e4b991d936f604229bf1 --- 每个 Session 都运行一个 Harness,并使用一个模型提供商。Core 通过三个固定版本上游协议未定义的 Core 扩展来选择它们:`x_agents_core.harness` 选择 Harness,`x_agents_core.model_provider` 提供端点和密钥,`x_agents_core.harness_config` 携带原生模型参数。Core 没有提供商目录、模型别名解析或产品权限模型;除 Session 和已保存 Agent 配置包外,唯一存储的配置包是每个 Harness 的一个 [deployment default](#deployment-defaults)。本文档定义 Harness—模型提供商协议:[`internal/modelprovider/config.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/modelprovider/config.go) 负责验证冻结的提供商连接,每个 Harness 则通过 [`internal/harnessconfig/harness.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/harnessconfig/harness.go) 声明其协议和原生参数。 @@ -143,7 +143,7 @@ PUT 要求提供 `model` 和完整的 `model_provider`;`harness_config` 默认 Harness 和默认模型读取结果包含可空的 `last_used_at`、`last_error_code` 和 `last_error_at`;即使配置包未发生变化,PUT 也会重置这三项。 - 只有当 Session 冻结的正是当前默认修订版时,已完成的根 Turn 才会记录使用情况。 -- 失败的根 Turn 仅记录原生提供商错误码 `authentication_error`、`connection_failed`、`rate_limit_exceeded`、`usage_limit_exceeded`、`server_overloaded`、`server_error`、`resource_not_found`、`request_timeout` 和 `invalid_request`。输入策略、Core 或 Runtime 导致的失败,以及 cancelled 和 waiting 结果均不计入。 +- 失败的根 Turn 仅记录原生提供商错误码 `authentication_error`、`connection_failed`、`rate_limit_exceeded`、`usage_limit_exceeded`、`server_overloaded`、`server_error`、`resource_not_found` 和 `invalid_request`。输入策略、Core 或 Runtime 导致的失败,以及 cancelled 和 waiting 结果均不计入。 - 成功不会清除较早的错误;比较时间戳只是一种显示约定。 - 无论错误码是什么,错误观测都会节流 30 秒,常规成功观测也会节流 30 秒;错误后的首次成功会立即记录恢复。对于未变化的修订版,这意味着数据库时间的任意 30 秒窗口内最多允许三次有效写入。 - 每次观测的预算为 1 秒,包括获取连接池资源和行锁的时间,并且无法更改已提交的 Turn。崩溃、失败或节流都可能导致最新观测缺失。 diff --git a/contracts/agents-api/zh/session-diagnostics.md b/contracts/agents-api/zh/session-diagnostics.md index 995c53217..2af4df4a9 100644 --- a/contracts/agents-api/zh/session-diagnostics.md +++ b/contracts/agents-api/zh/session-diagnostics.md @@ -1,7 +1,7 @@ --- title: "Session 诊断" source: contracts/agents-api/session-diagnostics.md -source_hash: 7ed290070a66339d3a427f1f1735a21915c70c874ce4456b37206b11998a28d7 +source_hash: 705486548640818b9b17065f31e7b0f3aede38ae4e66d93d5483c5669d9e0613 --- 管理员通过两个需要 Core 密钥的路由读取 Session 或 Turn 失败的原因,它们报告失败类别和 Item 接收时间。 @@ -27,7 +27,7 @@ source_hash: 7ed290070a66339d3a427f1f1735a21915c70c874ce4456b37206b11998a28d7 托管配置失败优先于输入活动,输入活动优先于最新根 Turn;公开 Session 响应使用相同优先级。不会包含私有结果文本、原生消息、提供商响应体、命令文本、路径或凭据。未知结果代码变为 `internal_error`;不使用前缀匹配或原因文本解析。 -配置参数来自已确认的结构化回执,在结算 Environment 及其输入并记录事件的失败事务中持久化。`step` 为 `setup`、`python`、`npm`、`system`、`file`、`skill` 或 null。`index` 仅对 setup 为 JSON 安全的非负整数,其他情况为 null。`exit_code` 对脚本步骤为 1 至 255,其他情况为 null。未记录的详情保持 null。这些私有字段不会改变公开失败原因或 SSE。 +配置参数来自已确认的结构化回执,在结算 Environment 及其输入并记录事件的失败事务中持久化。`step` 为 `setup`、`python`、`npm`、`file`、`skill` 或 null。`index` 仅对 setup 为 JSON 安全的非负整数,其他情况为 null。`exit_code` 对脚本步骤为 1 至 255,其他情况为 null。未记录的详情保持 null。这些私有字段不会改变公开失败原因或 SSE。 ## Turn 快照与 Item 回执时间 {#turn-snapshot-and-item-receipt-timing} diff --git a/docs/runtime-protocol.md b/docs/runtime-protocol.md index 0b4a4effe..e1ef8b873 100644 --- a/docs/runtime-protocol.md +++ b/docs/runtime-protocol.md @@ -49,7 +49,7 @@ Each physical connection has fresh routing, admission handles and transfer state ## Versioning -The wire version is [`proto.Version`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/version.go), independent of the Runtime build version that heartbeats report. Core accepts only an exact match, including the patch component. A mismatch returns HTTP 426 `incompatible_version` before any dispatch; the daemon treats it as permanent and stops reconnecting. Deploy matching peers together; a change to a payload or its semantics changes `proto.Version`. +The wire version is [`proto.Version`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/version.go), independent of the Runtime build version. Core accepts only an exact match, including the patch component. A mismatch returns HTTP 426 `incompatible_version` before any dispatch; the daemon treats it as permanent and stops reconnecting. Deploy matching peers together; a change to a payload or its semantics changes `proto.Version`. ## Capability declarations @@ -292,7 +292,7 @@ Retry only an operation whose own contract makes retry safe. A retired preparati An adapter may add `code` and `http_status` to a Run's `error` frame. They are optional neutral metadata, not a terminal event or a public error contract; the frame's text, Usage, `done`, native Session identity and cancellation receipts keep their order and meaning. -The accepted codes are `authentication_error`, `rate_limit_exceeded`, `usage_limit_exceeded`, `server_overloaded`, `server_error`, `invalid_request`, `resource_not_found`, `request_timeout`, `context_length_exceeded`, `cyber_policy` and `connection_failed` ([`engine_failure.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/engine_failure.go)). Only `connection_failed` keeps `http_status`, and only an integer from 100 to 599; every other status is discarded. A missing, malformed or unknown value leaves the error unclassified without discarding Usage or `done`. +The accepted codes are `authentication_error`, `rate_limit_exceeded`, `usage_limit_exceeded`, `server_overloaded`, `server_error`, `invalid_request`, `resource_not_found`, `context_length_exceeded`, `cyber_policy` and `connection_failed` ([`engine_failure.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/engine_failure.go)). Only `connection_failed` keeps `http_status`, and only an integer from 100 to 599; every other status is discarded. A missing, malformed or unknown value leaves the error unclassified without discarding Usage or `done`. Core stores accepted values in the Turn outcome as `engine_error_code` and `engine_http_status`. A Run may carry more than one `error` frame, such as an adapter's classified failure followed by the Runtime's own settlement failure; a frame without an accepted code leaves the stored classification in place, and a later accepted code replaces it. The classification is subordinate to the terminal status and Core's `error_code`: it cannot turn a completed or cancelled Turn into a failure, hide an incomplete event stream, or override a persistence or cancellation-receipt failure. Normal delivery and terminal journal draining use the same extraction. [Session diagnostics](../contracts/agents-api/session-diagnostics.md) expose the category only for a failed Turn whose Core error is `engine_failed`. The [Codex](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/deploy/codex/README.md) and [Claude Code](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/deploy/claude/README.md) adapter guides give each Harness's mapping. An adapter classifies only from structured error data: the Harness's own error variant or status, or a model provider's JSON error body that the Harness passes through, whose own `error.code` or `error.type` may name an accepted code. It never classifies from error prose. diff --git a/docs/zh/runtime-protocol.md b/docs/zh/runtime-protocol.md index dea8a70f8..793533784 100644 --- a/docs/zh/runtime-protocol.md +++ b/docs/zh/runtime-protocol.md @@ -1,7 +1,7 @@ --- title: "Core–Runtime 协议" source: docs/runtime-protocol.md -source_hash: f9a66794bab8aca97b88712a67451271ea96d825f35b20288f94c98d42523f1a +source_hash: e41baeed3c6b990b6da8823fc792d54393b2e247f0d96811f98b4bfccf1da5aa --- Core–Runtime 协议连接 Core 与 Runtime daemon,定义 daemon 连接上消息的含义和顺序。托管和自托管 Runtime 使用同一协议。 @@ -51,7 +51,7 @@ sequenceDiagram ## 版本 {#versioning} -wire 版本为 [`proto.Version`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/version.go),独立于 heartbeat 报告的 Runtime 构建版本。Core 仅接受精确匹配,包括 patch 部分。不匹配时,在任何 dispatch 前返回 HTTP 426 `incompatible_version`;daemon 将其视为永久错误并停止重连。应一起部署版本匹配的两端;payload 或其语义的任何变更都会改变 `proto.Version`。 +wire 版本为 [`proto.Version`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/version.go),独立于 Runtime 构建版本。Core 仅接受精确匹配,包括 patch 部分。不匹配时,在任何 dispatch 前返回 HTTP 426 `incompatible_version`;daemon 将其视为永久错误并停止重连。应一起部署版本匹配的两端;payload 或其语义的任何变更都会改变 `proto.Version`。 ## 能力声明 {#capability-declarations} @@ -294,7 +294,7 @@ transport 与 execution 结果分开。Core 唯一 Run 订阅入口是 `Subscrib adapter 可以给 Run 的 `error` frame 添加 `code` 和 `http_status`。它们是可选的中性元数据,不是终结事件或公开错误契约;frame 文本、Usage、`done`、原生 Session 身份和取消回执保持原顺序与含义。 -接受的 code 为 `authentication_error`、`rate_limit_exceeded`、`usage_limit_exceeded`、`server_overloaded`、`server_error`、`invalid_request`、`resource_not_found`、`request_timeout`、`context_length_exceeded`、`cyber_policy` 和 `connection_failed`([`engine_failure.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/engine_failure.go))。仅 `connection_failed` 保留 `http_status`,且只能是 100 到 599 的整数;其他 status 都丢弃。值缺失、格式错误或未知时保持未分类错误,不丢弃 Usage 或 `done`。 +接受的 code 为 `authentication_error`、`rate_limit_exceeded`、`usage_limit_exceeded`、`server_overloaded`、`server_error`、`invalid_request`、`resource_not_found`、`context_length_exceeded`、`cyber_policy` 和 `connection_failed`([`engine_failure.go`](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/internal/agentdaemon/proto/engine_failure.go))。仅 `connection_failed` 保留 `http_status`,且只能是 100 到 599 的整数;其他 status 都丢弃。值缺失、格式错误或未知时保持未分类错误,不丢弃 Usage 或 `done`。 Core 在 Turn outcome 中将接受的值保存为 `engine_error_code` 和 `engine_http_status`。一个 Run 可以有多个 `error` frame,例如 adapter 的分类失败之后跟着 Runtime 自己的结算失败;没有接受 code 的 frame 不改变已保存的分类,后来接受的 code 会替换它。分类从属于终结状态和 Core 的 `error_code`:不能将已完成或已取消 Turn 变成失败、隐藏不完整事件流,或覆盖持久化或取消回执失败。正常 delivery 和 terminal journal draining 使用同一提取逻辑。[Session 诊断](../../contracts/agents-api/zh/session-diagnostics.md)仅为 Core error 为 `engine_failed` 的失败 Turn 暴露类别。[Codex](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/deploy/codex/README.md) 和 [Claude Code](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/services/core/deploy/claude/README.md) adapter 指南给出各 Harness 的映射。adapter 只依据结构化错误数据分类:Harness 自身的错误类型或状态,或 Harness 原样转交的模型提供商 JSON 错误 body,其 `error.code` 或 `error.type` 可以给出接受的 code。adapter 不依据错误文字分类。 diff --git a/internal/agentdaemon/proto/engine_failure.go b/internal/agentdaemon/proto/engine_failure.go index 03574f499..1d6f29551 100644 --- a/internal/agentdaemon/proto/engine_failure.go +++ b/internal/agentdaemon/proto/engine_failure.go @@ -6,7 +6,7 @@ import "encoding/json" // metadata from newer or malformed peers never changes terminal/usage decoding. func NormalizeEngineFailure(code string, status *int) (string, *int) { switch code { - case "authentication_error", "rate_limit_exceeded", "usage_limit_exceeded", "server_overloaded", "server_error", "invalid_request", "resource_not_found", "request_timeout", "context_length_exceeded", "cyber_policy": + case "authentication_error", "rate_limit_exceeded", "usage_limit_exceeded", "server_overloaded", "server_error", "invalid_request", "resource_not_found", "context_length_exceeded", "cyber_policy": return code, nil case "connection_failed": if status != nil && *status >= 100 && *status <= 599 { diff --git a/internal/agentdaemon/proto/inbound.go b/internal/agentdaemon/proto/inbound.go index 957355d4c..995943674 100644 --- a/internal/agentdaemon/proto/inbound.go +++ b/internal/agentdaemon/proto/inbound.go @@ -203,8 +203,5 @@ type SupportedAgentKind struct { // HeartbeatPayload advertises only explicit engine descriptors. Missing // supported_agent_kinds establishes no engine availability or capabilities. type HeartbeatPayload struct { - Timestamp int64 `json:"ts"` - ActiveRequests int `json:"active_requests"` - DaemonVersion string `json:"daemon_version,omitempty"` SupportedAgentKinds []SupportedAgentKind `json:"supported_agent_kinds,omitempty"` } diff --git a/internal/agentdaemon/proto/version.go b/internal/agentdaemon/proto/version.go index 0a1fc835c..d24c19dd7 100644 --- a/internal/agentdaemon/proto/version.go +++ b/internal/agentdaemon/proto/version.go @@ -2,7 +2,7 @@ package proto // Version identifies the complete Core–Runtime wire contract. Change it when // removing or changing a payload or its semantics; deploy both endpoints together. -const Version = "0.21.0" +const Version = "0.22.0" // VersionCompatible accepts only this contract. Patch drift, prerelease suffixes // and malformed versions do not select an implicit compatibility path. diff --git a/packages/agents-client/src/generated/core-api.ts b/packages/agents-client/src/generated/core-api.ts index 60aff68c4..f8d1b5c52 100644 --- a/packages/agents-client/src/generated/core-api.ts +++ b/packages/agents-client/src/generated/core-api.ts @@ -224,7 +224,7 @@ export interface DiagnosticFailure { params: Record; } export const diagnosticFailureFields = ["code", "failed_at", "params"] as const; -export const diagnosticFailureCodeValues = ["harness_error", "model_provider_required", "runtime_unavailable", "runtime_disconnected", "runtime_preparation_failed", "execution_interrupted", "delivery_unconfirmed", "input_rejected", "executor_protocol_error", "core_storage_failed", "internal_error", "environment_connection_timeout", "environment_unavailable", "environment_provisioning_failed", "authentication_error", "rate_limit_exceeded", "usage_limit_exceeded", "server_overloaded", "server_error", "invalid_request", "resource_not_found", "request_timeout", "context_length_exceeded", "cyber_policy", "connection_failed"] as const; +export const diagnosticFailureCodeValues = ["harness_error", "model_provider_required", "runtime_unavailable", "runtime_disconnected", "runtime_preparation_failed", "execution_interrupted", "delivery_unconfirmed", "input_rejected", "executor_protocol_error", "core_storage_failed", "internal_error", "environment_connection_timeout", "environment_unavailable", "environment_provisioning_failed", "authentication_error", "rate_limit_exceeded", "usage_limit_exceeded", "server_overloaded", "server_error", "invalid_request", "resource_not_found", "context_length_exceeded", "cyber_policy", "connection_failed"] as const; export type DiagnosticFailureCode = (typeof diagnosticFailureCodeValues)[number]; export const diagnosticSourceValues = ["turn", "environment", "environment_input"] as const; export type DiagnosticSource = (typeof diagnosticSourceValues)[number]; @@ -561,7 +561,7 @@ export interface ProjectsPage { has_more: boolean; } export const projectsPageFields = ["data", "has_more"] as const; -export const providerErrorCodeValues = ["authentication_error", "connection_failed", "rate_limit_exceeded", "usage_limit_exceeded", "server_overloaded", "server_error", "resource_not_found", "request_timeout", "invalid_request"] as const; +export const providerErrorCodeValues = ["authentication_error", "connection_failed", "rate_limit_exceeded", "usage_limit_exceeded", "server_overloaded", "server_error", "resource_not_found", "invalid_request"] as const; export type ProviderErrorCode = (typeof providerErrorCodeValues)[number]; export interface Range { end: string; @@ -871,7 +871,7 @@ export interface SessionDiagnosticFailure { } export const sessionDiagnosticFailureFields = ["code", "failed_at", "params", "source", "turn_id"] as const; export const sessionDiagnosticFailureRequired = ["code", "failed_at", "params", "source"] as const; -export const sessionDiagnosticFailureCodeValues = ["harness_error", "model_provider_required", "runtime_unavailable", "runtime_disconnected", "runtime_preparation_failed", "execution_interrupted", "delivery_unconfirmed", "input_rejected", "executor_protocol_error", "core_storage_failed", "internal_error", "environment_connection_timeout", "environment_unavailable", "environment_provisioning_failed", "authentication_error", "rate_limit_exceeded", "usage_limit_exceeded", "server_overloaded", "server_error", "invalid_request", "resource_not_found", "request_timeout", "context_length_exceeded", "cyber_policy", "connection_failed"] as const; +export const sessionDiagnosticFailureCodeValues = ["harness_error", "model_provider_required", "runtime_unavailable", "runtime_disconnected", "runtime_preparation_failed", "execution_interrupted", "delivery_unconfirmed", "input_rejected", "executor_protocol_error", "core_storage_failed", "internal_error", "environment_connection_timeout", "environment_unavailable", "environment_provisioning_failed", "authentication_error", "rate_limit_exceeded", "usage_limit_exceeded", "server_overloaded", "server_error", "invalid_request", "resource_not_found", "context_length_exceeded", "cyber_policy", "connection_failed"] as const; export type SessionDiagnosticFailureCode = (typeof sessionDiagnosticFailureCodeValues)[number]; export interface SessionDiagnostics { failure: SessionDiagnosticFailure | null; diff --git a/packages/agents-client/src/session-diagnostics.test.ts b/packages/agents-client/src/session-diagnostics.test.ts index d8ef7b405..bfb0fd176 100644 --- a/packages/agents-client/src/session-diagnostics.test.ts +++ b/packages/agents-client/src/session-diagnostics.test.ts @@ -59,7 +59,7 @@ it.each([ expect(() => projectSessionDiagnostics(value, id)).toThrow(); }); -it.each(["authentication_error", "rate_limit_exceeded", "usage_limit_exceeded", "server_overloaded", "server_error", "invalid_request", "resource_not_found", "request_timeout", "context_length_exceeded", "cyber_policy", "connection_failed"])("projects finite native category %s", (code) => { +it.each(["authentication_error", "rate_limit_exceeded", "usage_limit_exceeded", "server_overloaded", "server_error", "invalid_request", "resource_not_found", "context_length_exceeded", "cyber_policy", "connection_failed"])("projects finite native category %s", (code) => { const params = code === "connection_failed" ? { http_status: 503 } : {}; const failed = { ...failure, code, params }; expect(projectTurnDiagnostics({ ...snapshot, failure: failed }, id, turn).failure).toEqual(failed); diff --git a/packages/agents-client/src/session-diagnostics.ts b/packages/agents-client/src/session-diagnostics.ts index 27ed6925d..0dff746ff 100644 --- a/packages/agents-client/src/session-diagnostics.ts +++ b/packages/agents-client/src/session-diagnostics.ts @@ -12,7 +12,7 @@ import { // ItemDiagnosticTiming holds Core receipt intervals, distinct from the public tool-reported duration_ms. export type { DiagnosticFailureCode, ItemDiagnosticTiming }; // The schema types params as a free-form object; each code that carries params has one of these shapes. -export type ProvisioningFailureParams = { step: "setup" | "python" | "npm" | "system" | "file" | "skill" | null; index: number | null; exit_code: number | null }; +export type ProvisioningFailureParams = { step: "setup" | "python" | "npm" | "file" | "skill" | null; index: number | null; exit_code: number | null }; export type ConnectionFailureParams = { http_status: number | null }; export type DiagnosticFailure = Omit & { params: Record | ProvisioningFailureParams | ConnectionFailureParams }; export type SessionDiagnosticFailure = DiagnosticFailure & Pick; @@ -20,7 +20,7 @@ export type SessionDiagnostics = Omit & { export type TurnDiagnostics = Omit & { failure: DiagnosticFailure | null }; // The schema's one code enum covers every failure source; each source produces only its subset. -const turnCodes: readonly DiagnosticFailureCode[] = ["authentication_error", "rate_limit_exceeded", "usage_limit_exceeded", "server_overloaded", "server_error", "invalid_request", "resource_not_found", "request_timeout", "context_length_exceeded", "cyber_policy", "connection_failed", "harness_error", "model_provider_required", "runtime_unavailable", "runtime_disconnected", "runtime_preparation_failed", "execution_interrupted", "delivery_unconfirmed", "input_rejected", "executor_protocol_error", "core_storage_failed", "internal_error"]; +const turnCodes: readonly DiagnosticFailureCode[] = ["authentication_error", "rate_limit_exceeded", "usage_limit_exceeded", "server_overloaded", "server_error", "invalid_request", "resource_not_found", "context_length_exceeded", "cyber_policy", "connection_failed", "harness_error", "model_provider_required", "runtime_unavailable", "runtime_disconnected", "runtime_preparation_failed", "execution_interrupted", "delivery_unconfirmed", "input_rejected", "executor_protocol_error", "core_storage_failed", "internal_error"]; const inputCodes: readonly DiagnosticFailureCode[] = ["environment_connection_timeout", "environment_unavailable", "model_provider_required", "internal_error"]; const environmentCodes: readonly DiagnosticFailureCode[] = ["environment_provisioning_failed"]; const fields = (...values: string[]) => new Set(values); @@ -33,9 +33,9 @@ function failure(value: unknown, codes: readonly DiagnosticFailureCode[], sessio let params: DiagnosticFailure["params"] = {}; if (value.code === "environment_provisioning_failed") { const p = value.params; - if (!exactFields(p, fields("step", "index", "exit_code")) || (p.step !== null && (typeof p.step !== "string" || !["setup", "python", "npm", "system", "file", "skill"].includes(p.step))) || + if (!exactFields(p, fields("step", "index", "exit_code")) || (p.step !== null && (typeof p.step !== "string" || !["setup", "python", "npm", "file", "skill"].includes(p.step))) || (p.index !== null && (!Number.isSafeInteger(p.index) || (p.index as number) < 0 || p.step !== "setup")) || - (p.exit_code !== null && (!Number.isSafeInteger(p.exit_code) || (p.exit_code as number) < 1 || (p.exit_code as number) > 255 || !["setup", "python", "npm", "system"].includes(p.step ?? "")))) return invalidAdminResponse(); + (p.exit_code !== null && (!Number.isSafeInteger(p.exit_code) || (p.exit_code as number) < 1 || (p.exit_code as number) > 255 || !["setup", "python", "npm"].includes(p.step ?? "")))) return invalidAdminResponse(); params = { step: p.step as ProvisioningFailureParams["step"], index: p.index as number | null, exit_code: p.exit_code as number | null }; } else if (value.code === "connection_failed") { const p = value.params; diff --git a/services/core/IMPLEMENTATION.md b/services/core/IMPLEMENTATION.md index fe3a73ffb..1059547a8 100644 --- a/services/core/IMPLEMENTATION.md +++ b/services/core/IMPLEMENTATION.md @@ -38,7 +38,7 @@ Domain owners, each with its PostgreSQL adapter under `internal/persistence/post - `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. Session creation uses those rules and the `placementpg` participants only for durable admission inside the creation transaction; it does not reserve compute. The leased execution manager owns the common placement entry point for first compute and replacement compute through `deployment.ExecutionOperations.EnsurePlacement`. 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`. - `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 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), 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, a Project's credential state, 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 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 @@ -104,7 +104,7 @@ The sandbox node Hub's global mutex protects only in-memory connection state. Au ## Sessions, Turns and input -Turn writes serialize on the tenant-scoped Session row. Input requests are ordered batches committed under the same lock: a retry key identifies the complete batch, a changed length, order or content conflicts, and a failed transaction leaves no partial input or cancellation. A single-event request keeps its identity at batch position zero. Internal admission limits are 64 events and 512 KiB of payload per request; the API still validates the upstream event schema. Cancellation keeps its first target, including an idle no-op, so a retry never stops later work. Terminal states and outcomes are never overwritten. +Turn writes serialize on the tenant-scoped Session row. Input requests are ordered batches committed under the same lock: a retry key identifies the complete batch, a changed length, order or content conflicts, and a failed transaction leaves no partial input or cancellation. A single-event request keeps its identity at batch position zero. The [Session events protocol](../../contracts/agents-api/sessions-events.md) owns the admission limits; the API still validates the upstream event schema. Cancellation keeps its first target, including an idle no-op, so a retry never stops later work. Terminal states and outcomes are never overwritten. Session deletion takes its decision and commits the `deleted_at` marker under the tenant Session lock that orders Turn and input admission, so either an admission commits first and deletion conflicts, or admission observes the deletion. Admission checks visibility under that lock before its retry lookup. Internal Turn, receipt, finalization and restart reconciliation keep access to deleted Sessions so that work settles under the execution lease; queued work of a deleted Session is never claimed, and Runtime cleanup still cancels pending work. Deletion never revokes a shared device or removes a saved Agent. Deleted Session records are retained. @@ -122,13 +122,13 @@ Reusable Agents are tenant-scoped rows independent of Session snapshots and engi Saved execution defaults keep a model-provider bundle whole at every replacement boundary: endpoint, key, protocol and limits are never inherited separately. Agent JSON holds only the safe provider fields and an output-only configured flag; the complete bundle is encrypted separately with a tenant and Agent binding and its own purpose, and configuration and secret changes commit together under the Agent row lock. Model-only edits need no key. Merged harness, protocol and limits are validated without reading keys. Session creation reads safe defaults and ciphertext in one snapshot, and a complete Session override does not decrypt the inherited bundle. The resolved bundle is frozen in an encrypted Session-owned row, and dispatch fails closed when that snapshot is missing or cannot be decrypted; later Agent edits, default changes, restarts and suspension never resolve it again. -`modelconfiguration` owns each Harness's deployment default: its service validates a replacement through the Harness declaration, and `modelconfigurationpg` seals the complete bundle to the Harness and stores it under a private revision UUID generated on every PUT, identical replacements included, with its audit row in the same transaction. Session creation resolves the opened bundle and its revision together and freezes them; retries and older Sessions never gain or replace revision metadata. After a successful root terminal commit, the Dispatcher's required `modelconfiguration.Observer` runs one independent pool operation with at most one second to update the matching current revision. Only completed Turns and native provider failures with `engine_failed` count; cancelled work, Core or Runtime errors and input-policy classifications never do. `ShouldObserveProvider` skips the round trip for outcomes that cannot count, and the SQL stays authoritative: it verifies the tenant, root Turn and committed outcome. Both check the same shared cases. The metadata-only transaction sets statement and lock timeouts within the remaining budget and issues one UPDATE that locks only the default and samples database time after the lock. Errors throttle for 30 seconds, ordinary successes throttle for 30 seconds with one immediate recovery write after each accepted error, and an unchanged revision has at most three effective writes in any 30-second window of nondecreasing database time. Observations never change `updated_at`, readiness or execution truth, and can be lost or stale; there is no queue, retry, probe or backfill. +`modelconfiguration` owns each Harness's deployment default: its service validates a replacement through the Harness declaration, and `modelconfigurationpg` seals the complete bundle to the Harness and stores it under a private revision UUID generated on every PUT, identical replacements included, with its audit row in the same transaction. Session creation resolves the opened bundle and its revision together and freezes them; retries and older Sessions never gain or replace revision metadata. After a successful root terminal commit, the Dispatcher's required `modelconfiguration.Observer` runs one independent pool operation with at most one second to update the matching current revision. Only completed Turns and native provider failures with `engine_failed` count; cancelled work, Core or Runtime errors and input-policy classifications never do. `ShouldObserveProvider` skips the round trip for outcomes that cannot count, and the SQL stays authoritative: it verifies the tenant, root Turn and committed outcome. Both check the same shared cases. The metadata-only transaction sets statement and lock timeouts within the remaining budget and issues one UPDATE that locks only the default and samples database time after the lock. The throttling and loss rules are in [Deployment default observations](../../contracts/agents-api/model-execution.md#deployment-default-observations). Session execution-configuration reads use a separate immutable safe projection written with its provenance in the Session's creation transaction. It reads no ciphertext, never recomputes sources from current Agents or defaults, never touches activity or wakes a sandbox, and does not affect retry identity. Provider input validation uses the adapter rules in `internal/harnessconfig`: one internal registry for protocol and token-limit validation, while Core owns credential environment and endpoint admission policy. -- Saved Agents parse tools with the saved-form parser, which keeps every pinned `web_search` mode. Session admission re-resolves the effective tools with the execution parser, which admits only disabled search; Worker device selection and the final preclaim also refuse a non-disabled search control. +- Saved Agents parse tools with the saved-form parser, which keeps every pinned `web_search` mode. ## Vaults and credentials @@ -162,7 +162,7 @@ Accepted public MCP credential profiles are declared centrally by the service, s Core replaces a Turn's usage with each complete valid token breakdown, which `sessions.MeasuredUsage` decides, and keeps the last committed measurement when execution is interrupted. It never infers tokens from context occupancy or costs and never parses native raw payloads. When `done` also carries usage that was already reported, the counters are not added again. -Subagent observations use the common types in `internal/agentdaemon/proto/subagents.go`. `sessions` projects them and `sessionpg` assigns their public IDs, under the Session lock of the leased execution journal; native names, history parsing and outcome proof stay in the adapters. Child Turns have their own table, separate from Core's queue, and Session Turn reads and the Session event stream carry root work only. The migration-defined `public_execution_turns` view has no public reader; do not reintroduce mixed Session Turn pages. Repeated effects are idempotent, root output freezes first, and child Items are delivered before their terminal Turn snapshot. +Subagent observations use the common types in `internal/agentdaemon/proto/subagents.go`. `sessions` projects them and `sessionpg` assigns their public IDs, under the Session lock of the leased execution journal; native names, history parsing and outcome proof stay in the adapters. Child Turns have their own table, separate from Core's queue, and Session Turn reads and the Session event stream carry root work only. Do not reintroduce mixed Session Turn pages. Repeated effects are idempotent, root output freezes first, and child Items are delivered before their terminal Turn snapshot. Function calls are stored per tenant, Session and Turn with immutable public and executor call identities and arguments. Result admission and application receipts serialize on the Session lock with cancellation and terminal transitions. `sessions.AdmitFunctionResult` keeps the complete caller-validated result, including omitted versus null error and output and ordered text and image parts; identical retries return the saved decision and changed results conflict. Recording a call through `sessions.ExecutionOperations` moves the Turn to `waiting`, and the last application receipt resumes it. A function result may join a message or cancel batch: its explicit Turn and call identity select an existing call, admission never creates a Turn for it, and the result and its input retry record commit in one transaction. Targets resolve only after the tenant Session lookup; an unknown call or a call of another Turn is 400 `invalid_request_error`. The input cursor skips function results, whose own receipts decide application. A Session with functions waits for a device that declares `function_tools`, and Core delivers each saved result once per live dispatch, appending a non-null error as a final text part because the native result has no error field. diff --git a/services/core/cmd/server/daemon_bootstrap_test.go b/services/core/cmd/server/daemon_bootstrap_test.go index 372df38ae..b914403ce 100644 --- a/services/core/cmd/server/daemon_bootstrap_test.go +++ b/services/core/cmd/server/daemon_bootstrap_test.go @@ -38,7 +38,7 @@ func TestBootstrapAddressUsesPublicOrigin(t *testing.T) { 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 { + if len(body) != 3 { t.Fatal("bootstrap exposed private allocation metadata") } }) diff --git a/services/core/internal/adminaudit/reader.go b/services/core/internal/adminaudit/reader.go index 921e68e57..0f09937de 100644 --- a/services/core/internal/adminaudit/reader.go +++ b/services/core/internal/adminaudit/reader.go @@ -1,7 +1,6 @@ package adminaudit import ( - "context" "errors" "time" @@ -12,12 +11,6 @@ import ( // or cursor. var ErrInvalidQuery = errors.New("invalid administrator audit query") -// Reader reads committed administrator mutations as safe metadata, never -// request bodies or secrets. -type Reader interface { - ListAdminAudit(ctx context.Context, filter Filter) (Page, error) -} - // Filter selects committed administrator mutations, newest first. A zero Limit // is the default page size. type Filter struct { diff --git a/services/core/internal/agents/storage.go b/services/core/internal/agents/storage.go index ad88085a9..345690f6c 100644 --- a/services/core/internal/agents/storage.go +++ b/services/core/internal/agents/storage.go @@ -48,14 +48,3 @@ type Revision struct { // ModelProvider nil keeps the saved bundle. ModelProvider *ModelProviderChange } - -// Reader reads saved Agents of one tenant. An agent ID or list cursor that -// cannot name an Agent is a missing Agent: ErrNotFound. -type Reader interface { - GetAgent(ctx context.Context, tenantID, agentID string) (Agent, error) - ListAgents(context.Context, ListQuery) (Page, error) - // GetAgentWithModelProvider reads the Agent and its opened model provider - // bundle from one snapshot. The bundle is nil when the Agent has none; a - // bundle that fails to open is an internal error. - GetAgentWithModelProvider(ctx context.Context, tenantID, agentID string) (Agent, *v1.ModelProviderInput, error) -} diff --git a/services/core/internal/api/core_errors_test.go b/services/core/internal/api/core_errors_test.go index 2cd2f555c..0473ab004 100644 --- a/services/core/internal/api/core_errors_test.go +++ b/services/core/internal/api/core_errors_test.go @@ -123,9 +123,9 @@ func TestCoreMarkerPreservesObservationAndStreamingWrappers(t *testing.T) { })) observer := func(code string) { observed = append(observed, code) } if markerOutside { - handler = coreErrorResponses(responseHeadersWithErrors(handler, observer)) + handler = coreErrorResponses(responseHeadersWithLog(handler, observer, nil)) } else { - handler = responseHeadersWithErrors(coreErrorResponses(handler), observer) + handler = responseHeadersWithLog(coreErrorResponses(handler), observer, nil) } handler.ServeHTTP(out, httptest.NewRequest("GET", "/core/v1/test", nil)) if !out.Flushed || !out.deadline.Equal(time.Unix(1234, 0)) || out.Header().Get("Openai-Processing-Ms") == "" || !reflect.DeepEqual(observed, []string{"execution_unavailable"}) || !strings.Contains(out.Body.String(), `"details":{"reason":"draining"}`) { diff --git a/services/core/internal/api/routing.go b/services/core/internal/api/routing.go index cd595f7b2..0f1ce8d66 100644 --- a/services/core/internal/api/routing.go +++ b/services/core/internal/api/routing.go @@ -115,7 +115,7 @@ func cleanPath(p string) string { return cleaned } -// responseHeadersWithErrors adds the observed official response headers to +// responseHeadersWithLog adds the observed official response headers to // every response of this handler, including errors, 401, 404, 405 and SSE // streams (HP-23/HP-24): a fresh random X-Request-Id, attached to the request // log context next to the trace carrier, OpenAI-Version, OpenAI-Processing-Ms @@ -123,10 +123,6 @@ func cleanPath(p string) string { // Cache-Control extensions remain. Organization and project headers are not // reported: Core's project scope is configured, not account-derived. report // observes each emitted API error code. -func responseHeadersWithErrors(next http.Handler, report func(string)) http.Handler { - return responseHeadersWithLog(next, report, nil) -} - func responseHeadersWithLog(next http.Handler, report func(string), logs *requestLogs) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { id := newRequestID() diff --git a/services/core/internal/api/routing_test.go b/services/core/internal/api/routing_test.go index 4bef5001b..8547c403a 100644 --- a/services/core/internal/api/routing_test.go +++ b/services/core/internal/api/routing_test.go @@ -617,7 +617,7 @@ func TestAgentsResponseHeaders(t *testing.T) { // The ID is attached to the request log context, and a stream that flushes // before writing still reports its processing time. var logged string - stream := responseHeadersWithErrors(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + stream := responseHeadersWithLog(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { logged, _ = log.RequestIDFromContext(r.Context()) w.Header().Set("Content-Type", "text/event-stream") controller := http.NewResponseController(w) @@ -628,7 +628,7 @@ func TestAgentsResponseHeaders(t *testing.T) { t.Error(err) } _, _ = io.WriteString(w, ": connected\n\n") - }), func(string) {}) + }), func(string) {}, nil) server := httptest.NewServer(stream) defer server.Close() response, err := server.Client().Get(server.URL) diff --git a/services/core/internal/api/session_diagnostics.go b/services/core/internal/api/session_diagnostics.go index dd5ad70d3..098f7299c 100644 --- a/services/core/internal/api/session_diagnostics.go +++ b/services/core/internal/api/session_diagnostics.go @@ -19,7 +19,7 @@ const ( ) type DiagnosticFailure struct { - Code string `json:"code" binding:"required" enums:"harness_error,model_provider_required,runtime_unavailable,runtime_disconnected,runtime_preparation_failed,execution_interrupted,delivery_unconfirmed,input_rejected,executor_protocol_error,core_storage_failed,internal_error,environment_connection_timeout,environment_unavailable,environment_provisioning_failed,authentication_error,rate_limit_exceeded,usage_limit_exceeded,server_overloaded,server_error,invalid_request,resource_not_found,request_timeout,context_length_exceeded,cyber_policy,connection_failed"` + Code string `json:"code" binding:"required" enums:"harness_error,model_provider_required,runtime_unavailable,runtime_disconnected,runtime_preparation_failed,execution_interrupted,delivery_unconfirmed,input_rejected,executor_protocol_error,core_storage_failed,internal_error,environment_connection_timeout,environment_unavailable,environment_provisioning_failed,authentication_error,rate_limit_exceeded,usage_limit_exceeded,server_overloaded,server_error,invalid_request,resource_not_found,context_length_exceeded,cyber_policy,connection_failed"` Params CoreErrorDetails `json:"params" swaggertype:"object" binding:"required"` FailedAt *time.Time `json:"failed_at" extensions:"x-nullable" binding:"required"` } diff --git a/services/core/internal/api/turn_diagnostic_failure_test.go b/services/core/internal/api/turn_diagnostic_failure_test.go index d5b3b20a7..81a6320ef 100644 --- a/services/core/internal/api/turn_diagnostic_failure_test.go +++ b/services/core/internal/api/turn_diagnostic_failure_test.go @@ -95,7 +95,7 @@ func TestDiagnosticFailureWhitelist(t *testing.T) { } func TestDiagnosticNativeClassification(t *testing.T) { - codes := []string{"authentication_error", "rate_limit_exceeded", "usage_limit_exceeded", "server_overloaded", "server_error", "invalid_request", "resource_not_found", "request_timeout", "context_length_exceeded", "cyber_policy", "connection_failed"} + codes := []string{"authentication_error", "rate_limit_exceeded", "usage_limit_exceeded", "server_overloaded", "server_error", "invalid_request", "resource_not_found", "context_length_exceeded", "cyber_policy", "connection_failed"} catalog, err := os.ReadFile("../../../../contracts/agents-api/core-errors.md") if err != nil { t.Fatal(err) diff --git a/services/core/internal/api/write_audit_test.go b/services/core/internal/api/write_audit_test.go index 41ccf5e2f..29bfaa7ff 100644 --- a/services/core/internal/api/write_audit_test.go +++ b/services/core/internal/api/write_audit_test.go @@ -101,10 +101,10 @@ func TestAuthenticatedWriteProvenance(t *testing.T) { h := &Handler{Dependencies: deps} var source writeaudit.Source var got bool - handler := responseHeadersWithErrors(log.HTTPMiddleware(h.authenticateCaller(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handler := responseHeadersWithLog(log.HTTPMiddleware(h.authenticateCaller(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { source, got = writeaudit.FromContext(r.Context()) w.WriteHeader(204) - }), true)), func(string) {}) + }), true)), func(string) {}, nil) for _, test := range []struct { method, path string present bool diff --git a/services/core/internal/db/queries/deployment_provider_observations.sql b/services/core/internal/db/queries/deployment_provider_observations.sql index e69823bde..b98269973 100644 --- a/services/core/internal/db/queries/deployment_provider_observations.sql +++ b/services/core/internal/db/queries/deployment_provider_observations.sql @@ -16,7 +16,7 @@ WITH locked AS MATERIALIZED ( AND t.outcome->>'engine_error_code' IN ( 'authentication_error', 'connection_failed', 'rate_limit_exceeded', 'usage_limit_exceeded', 'server_overloaded', 'server_error', - 'resource_not_found', 'request_timeout', 'invalid_request'))) + 'resource_not_found', 'invalid_request'))) FOR UPDATE OF d ), observed AS MATERIALIZED ( SELECT harness, revision, clock_timestamp() AS received_at FROM locked diff --git a/services/core/internal/db/sqlc/deployment_provider_observations.sql.go b/services/core/internal/db/sqlc/deployment_provider_observations.sql.go index 9f9c0b5a9..affc37f2e 100644 --- a/services/core/internal/db/sqlc/deployment_provider_observations.sql.go +++ b/services/core/internal/db/sqlc/deployment_provider_observations.sql.go @@ -27,7 +27,7 @@ WITH locked AS MATERIALIZED ( AND t.outcome->>'engine_error_code' IN ( 'authentication_error', 'connection_failed', 'rate_limit_exceeded', 'usage_limit_exceeded', 'server_overloaded', 'server_error', - 'resource_not_found', 'request_timeout', 'invalid_request'))) + 'resource_not_found', 'invalid_request'))) FOR UPDATE OF d ), observed AS MATERIALIZED ( SELECT harness, revision, clock_timestamp() AS received_at FROM locked diff --git a/services/core/internal/db/sqlc/models.go b/services/core/internal/db/sqlc/models.go index 0b29eb728..74ac8e5fa 100644 --- a/services/core/internal/db/sqlc/models.go +++ b/services/core/internal/db/sqlc/models.go @@ -210,20 +210,6 @@ type ProjectApiKey struct { ProjectID pgtype.UUID `json:"project_id"` } -type PublicExecutionTurn struct { - ID pgtype.UUID `json:"id"` - SessionID pgtype.UUID `json:"session_id"` - Status string `json:"status"` - CreatedAt pgtype.Timestamptz `json:"created_at"` - StartedAt pgtype.Timestamptz `json:"started_at"` - CompletedAt pgtype.Timestamptz `json:"completed_at"` - CancelRequestedAt pgtype.Timestamptz `json:"cancel_requested_at"` - Outcome []byte `json:"outcome"` - TokenUsage []byte `json:"token_usage"` - ArtifactCaptureStarted bool `json:"artifact_capture_started"` - SubagentID pgtype.UUID `json:"subagent_id"` -} - type RuntimeAllocation struct { ID pgtype.UUID `json:"id"` EnvironmentID pgtype.UUID `json:"environment_id"` diff --git a/services/core/internal/deployment/allocation.go b/services/core/internal/deployment/allocation.go index d578e4269..2095fa67c 100644 --- a/services/core/internal/deployment/allocation.go +++ b/services/core/internal/deployment/allocation.go @@ -64,7 +64,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, DeviceID, 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 69f8dd0dc..e0ebb2cd4 100644 --- a/services/core/internal/deployment/allocations.go +++ b/services/core/internal/deployment/allocations.go @@ -75,7 +75,7 @@ func (e *ExecutionOperations) ReserveAllocation(ctx context.Context, key Allocat if environment.Status == "failed" || environment.Status == "expired" { return ErrInvalidInput } - allocation := NewAllocation{ID: uuid.NewString(), EnvironmentID: environment.ID, DeviceID: uuid.NewString(), ProviderKey: installation, Generation: d.Generation} + allocation := NewAllocation{ID: uuid.NewString(), DeviceID: uuid.NewString(), ProviderKey: installation, Generation: d.Generation} if d.Mode == string(sandbox.DeploymentNodes) { reserved, err := tx.LoadReserved() if err != nil { diff --git a/services/core/internal/deployment/storage.go b/services/core/internal/deployment/storage.go index 0cd4169e2..851461108 100644 --- a/services/core/internal/deployment/storage.go +++ b/services/core/internal/deployment/storage.go @@ -26,10 +26,6 @@ type Storage interface { // DisconnectNode clears the node's connection when it is still this one. It // waits for the node row first, so an uncertain connect cannot outlive it. DisconnectNode(ctx context.Context, nodeID, connectionID string, epoch uint64) error - // SampleHostHistory copies each fresh authenticated heartbeat into the - // host history, at the Runtime sampler cadence, and returns the number of - // samples. Neither reads nor offline nodes fill gaps. - SampleHostHistory(ctx context.Context) (int64, error) // WithActivity runs apply in one transaction that locks the Session of // the tenant's Environment and then prunes the Session's journal. It // commits only when both succeed. A missing Environment, or one of a @@ -253,9 +249,6 @@ type Reader interface { // the given Session ID, in ID order, whose allocation, if any, is // unreleased. limit is 1 to 100. It reads only and renews nothing. ObservationSessions(ctx context.Context, after string, limit int) (ObservationSessionPage, error) - // NodeAllocations returns up to 1000 unreleased allocations of the node, - // oldest first. A missing node is ErrNotFound. - NodeAllocations(ctx context.Context, nodeID string) ([]NodeAllocation, error) // NodeOnline reports whether the node is connected; a missing node is // offline. NodeOnline(ctx context.Context, nodeID string) (bool, error) diff --git a/services/core/internal/environmenttemplates/storage.go b/services/core/internal/environmenttemplates/storage.go index d5b4e6eda..68a2fb4b7 100644 --- a/services/core/internal/environmenttemplates/storage.go +++ b/services/core/internal/environmenttemplates/storage.go @@ -10,12 +10,3 @@ type Storage interface { Update(ctx context.Context, tenantID, templateID string, in Input) (Template, error) Delete(ctx context.Context, tenantID, templateID string) (string, error) } - -// Reader reads a tenant's Templates. Get and List return safe metadata without -// the credential key. Resolve decrypts the configuration for Session creation. -// An unknown Template or cursor returns ErrNotFound. -type Reader interface { - Get(ctx context.Context, tenantID, templateID string) (Template, error) - List(ctx context.Context, tenantID string, query ListQuery) (Page, error) - Resolve(ctx context.Context, tenantID, templateID string) (Resolved, error) -} diff --git a/services/core/internal/files/storage.go b/services/core/internal/files/storage.go index 8b495a3bc..9ba57c717 100644 --- a/services/core/internal/files/storage.go +++ b/services/core/internal/files/storage.go @@ -14,16 +14,3 @@ type Storage interface { // Delete removes the File and its content, or returns ErrNotFound. Delete(ctx context.Context, tenantID, fileID string) error } - -// Reader reads a tenant's Files. -type Reader interface { - // Get returns the File's metadata, or ErrNotFound. - Get(ctx context.Context, tenantID, fileID string) (File, error) - // List returns one page. An After cursor that names no File of the tenant - // returns ErrNotFound. - List(ctx context.Context, tenantID string, query ListQuery) (Page, error) - // Read passes consume the File's metadata and content from one snapshot, so - // a concurrent Delete never truncates a read it admitted. It returns - // consume's error unchanged. - Read(ctx context.Context, tenantID, fileID string, consume func(File, io.Reader) error) error -} diff --git a/services/core/internal/modelconfiguration/observation.go b/services/core/internal/modelconfiguration/observation.go index fc9e48afc..d642d4610 100644 --- a/services/core/internal/modelconfiguration/observation.go +++ b/services/core/internal/modelconfiguration/observation.go @@ -12,7 +12,6 @@ const ( ProviderServerOverloaded ProviderErrorCode = "server_overloaded" ProviderServerError ProviderErrorCode = "server_error" ProviderResourceNotFound ProviderErrorCode = "resource_not_found" - ProviderRequestTimeout ProviderErrorCode = "request_timeout" ProviderInvalidRequest ProviderErrorCode = "invalid_request" ) @@ -26,7 +25,7 @@ type Observation struct { // itself. Context-length and cyber-policy failures describe the request. The // observation statement's fence lists the same codes; testdata holds the cases // both are checked against. -var providerErrorCodes = []ProviderErrorCode{ProviderAuthenticationError, ProviderConnectionFailed, ProviderRateLimitExceeded, ProviderUsageLimitExceeded, ProviderServerOverloaded, ProviderServerError, ProviderResourceNotFound, ProviderRequestTimeout, ProviderInvalidRequest} +var providerErrorCodes = []ProviderErrorCode{ProviderAuthenticationError, ProviderConnectionFailed, ProviderRateLimitExceeded, ProviderUsageLimitExceeded, ProviderServerOverloaded, ProviderServerError, ProviderResourceNotFound, ProviderInvalidRequest} // ShouldObserveProvider reports whether a committed root Turn outcome says // something about its model provider: every completed Turn, and a failed Turn diff --git a/services/core/internal/modelconfiguration/storage.go b/services/core/internal/modelconfiguration/storage.go index 49622417f..792b332cf 100644 --- a/services/core/internal/modelconfiguration/storage.go +++ b/services/core/internal/modelconfiguration/storage.go @@ -18,11 +18,6 @@ type Storage interface { LoadBundle(ctx context.Context, harness string) (Bundle, error) } -// Reader lists the configured defaults without opening any bundle. -type Reader interface { - List(ctx context.Context) ([]Configuration, error) -} - // Observer records a committed root Turn's outcome against the deployment // default revision its Session froze. It runs after the Turn's commit, outside // the execution lease, and the statement itself re-checks the committed diff --git a/services/core/internal/modelconfiguration/testdata/observation_cases.json b/services/core/internal/modelconfiguration/testdata/observation_cases.json index 0794fd47d..057a597dc 100644 --- a/services/core/internal/modelconfiguration/testdata/observation_cases.json +++ b/services/core/internal/modelconfiguration/testdata/observation_cases.json @@ -7,7 +7,6 @@ {"status": "failed", "error_code": "engine_failed", "engine_error_code": "server_overloaded", "observed": true}, {"status": "failed", "error_code": "engine_failed", "engine_error_code": "server_error", "observed": true}, {"status": "failed", "error_code": "engine_failed", "engine_error_code": "resource_not_found", "observed": true}, - {"status": "failed", "error_code": "engine_failed", "engine_error_code": "request_timeout", "observed": true}, {"status": "failed", "error_code": "engine_failed", "engine_error_code": "invalid_request", "observed": true}, {"status": "failed", "error_code": "engine_failed", "engine_error_code": "context_length_exceeded", "observed": false}, {"status": "failed", "error_code": "engine_failed", "engine_error_code": "cyber_policy", "observed": false}, diff --git a/services/core/internal/persistence/postgres/agentpg/store.go b/services/core/internal/persistence/postgres/agentpg/store.go index a0a7dba24..0be45661c 100644 --- a/services/core/internal/persistence/postgres/agentpg/store.go +++ b/services/core/internal/persistence/postgres/agentpg/store.go @@ -35,7 +35,6 @@ func New(pool *pgunit.Pool, cipher *credentialcrypto.Cipher) *Store { var ( _ agents.Storage = (*Store)(nil) - _ agents.Reader = (*Store)(nil) ) func (s *Store) CreateAgent(ctx context.Context, input agents.NewAgent) (agents.Agent, error) { diff --git a/services/core/internal/persistence/postgres/auditpg/store.go b/services/core/internal/persistence/postgres/auditpg/store.go index e74ccd372..b6742694d 100644 --- a/services/core/internal/persistence/postgres/auditpg/store.go +++ b/services/core/internal/persistence/postgres/auditpg/store.go @@ -9,9 +9,7 @@ import ( "github.com/jackc/pgx/v5/pgtype" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/adminaudit" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/persistence/postgres/pgunit" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/writeaudit" ) // Store reads the audit records and applies write audit retention. @@ -19,11 +17,6 @@ type Store struct{ pool *pgunit.Pool } func New(pool *pgunit.Pool) *Store { return &Store{pool: pool} } -var ( - _ writeaudit.Reader = (*Store)(nil) - _ adminaudit.Reader = (*Store)(nil) -) - // A page cursor names the last row of its page and is bound to the digest of // the query that produced it, so it cannot resume another tenant's or filter's // list. diff --git a/services/core/internal/persistence/postgres/filepg/filepg.go b/services/core/internal/persistence/postgres/filepg/filepg.go index 09a488b87..4e7bf4d5b 100644 --- a/services/core/internal/persistence/postgres/filepg/filepg.go +++ b/services/core/internal/persistence/postgres/filepg/filepg.go @@ -23,7 +23,6 @@ func New(pool *pgunit.Pool) *Store { return &Store{pool: pool} } var ( _ files.Storage = (*Store)(nil) - _ files.Reader = (*Store)(nil) ) // Create writes the content into a large object and stores the File and its diff --git a/services/core/internal/persistence/postgres/modelconfigurationpg/store.go b/services/core/internal/persistence/postgres/modelconfigurationpg/store.go index fd17b9724..0486d76ef 100644 --- a/services/core/internal/persistence/postgres/modelconfigurationpg/store.go +++ b/services/core/internal/persistence/postgres/modelconfigurationpg/store.go @@ -39,7 +39,6 @@ type Store struct { var ( _ modelconfiguration.Storage = (*Store)(nil) - _ modelconfiguration.Reader = (*Store)(nil) _ modelconfiguration.Observer = (*Store)(nil) ) diff --git a/services/core/internal/persistence/postgres/projectpg/projectpg.go b/services/core/internal/persistence/postgres/projectpg/projectpg.go index b290b6f99..62832022c 100644 --- a/services/core/internal/persistence/postgres/projectpg/projectpg.go +++ b/services/core/internal/persistence/postgres/projectpg/projectpg.go @@ -26,7 +26,6 @@ type Store struct { var ( _ projects.Storage = (*Store)(nil) - _ projects.Reader = (*Store)(nil) ) func New(pool *pgunit.Pool) *Store { return &Store{pool: pool} } diff --git a/services/core/internal/persistence/postgres/sessionpg/artifacts.go b/services/core/internal/persistence/postgres/sessionpg/artifacts.go index a43074fee..c98e1548f 100644 --- a/services/core/internal/persistence/postgres/sessionpg/artifacts.go +++ b/services/core/internal/persistence/postgres/sessionpg/artifacts.go @@ -17,7 +17,6 @@ import ( ) var ( - _ sessions.ArtifactReader = (*Store)(nil) _ sessions.ArtifactStorage = (*Store)(nil) ) diff --git a/services/core/internal/persistence/postgres/sessionpg/devices.go b/services/core/internal/persistence/postgres/sessionpg/devices.go index 188a45ef0..e8c915fd1 100644 --- a/services/core/internal/persistence/postgres/sessionpg/devices.go +++ b/services/core/internal/persistence/postgres/sessionpg/devices.go @@ -103,8 +103,7 @@ func loadSessionDevice(ctx context.Context, q *sqlc.Queries, tenant, session pgt } // GetDeviceCredential reads a device's credential for the Runtime gateway. A -// malformed or unknown device has no credential. The standalone service -// assigns no product WorkspaceID. +// malformed or unknown device has no credential. func (s *Store) GetDeviceCredential(ctx context.Context, device string) (runtimedevice.Credential, bool, error) { id, err := pgunit.ParseID(device) if err != nil { @@ -155,7 +154,7 @@ func (s *Store) ListEnrolledRuntimeBindings(ctx context.Context) ([]sessions.Enr for _, row := range rows { result = append(result, sessions.EnrolledRuntimeBinding{ DeviceID: optionalID(row.DeviceID), TenantID: optionalID(row.TenantID), - EnvironmentID: optionalID(row.EnvironmentID), SessionID: optionalID(row.SessionID), + EnvironmentID: optionalID(row.EnvironmentID), }) } return result, nil diff --git a/services/core/internal/persistence/postgres/sessionpg/execution_file_writes.go b/services/core/internal/persistence/postgres/sessionpg/execution_file_writes.go index b6b8a252c..41b946de4 100644 --- a/services/core/internal/persistence/postgres/sessionpg/execution_file_writes.go +++ b/services/core/internal/persistence/postgres/sessionpg/execution_file_writes.go @@ -96,7 +96,7 @@ func (t *fileWriteTx) LoadFileWrite(ctx context.Context, write string) (sessions if err != nil { return sessions.EnvironmentFileWrite{}, false, err } - return fileWriteFromRow(row.EnvironmentFileWrite, row.SessionID), true, nil + return fileWriteFromRow(row.EnvironmentFileWrite), true, nil } func (t *fileWriteTx) loadFileWrite(ctx context.Context, write string) (sqlc.GetEnvironmentFileWriteRow, error) { @@ -132,7 +132,7 @@ func (t *fileWriteTx) CreateFileWrite(ctx context.Context, key sessions.FileWrit if err != nil { return sessions.EnvironmentFileWrite{}, err } - return fileWriteFromRow(row, t.session), nil + return fileWriteFromRow(row), nil } func (t *fileWriteTx) SettleFileWrite(ctx context.Context, write, state string) (sessions.EnvironmentFileWrite, error) { @@ -144,7 +144,7 @@ func (t *fileWriteTx) SettleFileWrite(ctx context.Context, write, state string) if err != nil { return sessions.EnvironmentFileWrite{}, err } - return fileWriteFromRow(row, t.session), nil + return fileWriteFromRow(row), nil } func (t *fileWriteTx) RecordFileWriteAudit(ctx context.Context, write string) error { @@ -162,10 +162,10 @@ func (t *fileWriteTx) RecordFileWriteAudit(ctx context.Context, write string) er return auditpg.RecordWriteAudit(writeaudit.WithSource(ctx, source), t.q, optionalID(t.tenant), writeaudit.ActionUploadFile, writeaudit.ResourceEnvironment, optionalID(t.environment), optionalID(t.session)) } -func fileWriteFromRow(row sqlc.EnvironmentFileWrite, session pgtype.UUID) sessions.EnvironmentFileWrite { +func fileWriteFromRow(row sqlc.EnvironmentFileWrite) sessions.EnvironmentFileWrite { result := sessions.EnvironmentFileWrite{ - Identity: sessions.FileWriteIdentity{ID: optionalID(row.ID), DeviceID: optionalID(row.DeviceID), RequestSHA256: row.RequestSha256}, - EnvironmentID: optionalID(row.EnvironmentID), SessionID: optionalID(session), State: row.State, CreatedAt: row.CreatedAt.Time, + Identity: sessions.FileWriteIdentity{ID: optionalID(row.ID), DeviceID: optionalID(row.DeviceID), RequestSHA256: row.RequestSha256}, + State: row.State, CreatedAt: row.CreatedAt.Time, } if row.SettledAt.Valid { settled := row.SettledAt.Time diff --git a/services/core/internal/persistence/postgres/sessionpg/item_reads.go b/services/core/internal/persistence/postgres/sessionpg/item_reads.go index 6156742dc..24abe280a 100644 --- a/services/core/internal/persistence/postgres/sessionpg/item_reads.go +++ b/services/core/internal/persistence/postgres/sessionpg/item_reads.go @@ -14,8 +14,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) -var _ sessions.ItemReader = (*Store)(nil) - func (s *Store) ListItems(ctx context.Context, tenantID, sessionID, cursor string, limit int, ascending bool) (sessions.ItemPage, error) { if limit < 1 || limit > 100 { return sessions.ItemPage{}, sessions.ErrInvalidInput diff --git a/services/core/internal/persistence/postgres/sessionpg/subagent_reads.go b/services/core/internal/persistence/postgres/sessionpg/subagent_reads.go index 87e089788..ce3bd4605 100644 --- a/services/core/internal/persistence/postgres/sessionpg/subagent_reads.go +++ b/services/core/internal/persistence/postgres/sessionpg/subagent_reads.go @@ -15,8 +15,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) -var _ sessions.SubagentReader = (*Store)(nil) - func (s *Store) GetSubagent(ctx context.Context, tenantID, sessionID, subagentID string) (v1.Subagent, error) { var result v1.Subagent err := s.withPublicSession(ctx, tenantID, sessionID, func(ctx context.Context, q *sqlc.Queries, session pgtype.UUID) error { diff --git a/services/core/internal/persistence/postgres/templatepg/store.go b/services/core/internal/persistence/postgres/templatepg/store.go index 953324da9..2dacf7fff 100644 --- a/services/core/internal/persistence/postgres/templatepg/store.go +++ b/services/core/internal/persistence/postgres/templatepg/store.go @@ -33,7 +33,6 @@ type Store struct { var ( _ environmenttemplates.Storage = (*Store)(nil) - _ environmenttemplates.Reader = (*Store)(nil) ) // New returns a Store that seals and opens confidential configuration with diff --git a/services/core/internal/projects/storage.go b/services/core/internal/projects/storage.go index ad2b4d542..8499e10f1 100644 --- a/services/core/internal/projects/storage.go +++ b/services/core/internal/projects/storage.go @@ -56,21 +56,3 @@ type NewAPIKey struct { ID, Name, Prefix string Digest [sha256.Size]byte } - -// Reader answers Project and key queries. -type Reader interface { - // GetProject returns ErrNotFound for a missing Project or malformed ID. - GetProject(ctx context.Context, id string) (Binding, error) - // ListProjects returns ErrNotFound when query.After names no Project. - ListProjects(ctx context.Context, query ListQuery) (Page, error) - // ListAPIKeys returns every key of the Project, including revoked keys. It - // returns ErrNotFound for a missing Project or when query.After names no key - // of this Project. - ListAPIKeys(ctx context.Context, projectID string, query ListQuery) (KeyPage, error) - // ResolveAPIKey returns the active key with this secret digest, in an - // active Project, or ErrNotFound. - ResolveAPIKey(ctx context.Context, digest [sha256.Size]byte) (KeyBinding, error) - // APIKeyDigestExists reports whether any key, including a revoked key or a - // key of an archived Project, has this secret digest. - APIKeyDigestExists(ctx context.Context, digest [sha256.Size]byte) (bool, error) -} diff --git a/services/core/internal/runtimedevice/credential.go b/services/core/internal/runtimedevice/credential.go index b108f00d8..dd8fd0449 100644 --- a/services/core/internal/runtimedevice/credential.go +++ b/services/core/internal/runtimedevice/credential.go @@ -14,7 +14,6 @@ const RuntimeTypeAgentDaemon = "agent_daemon" // CredentialHash is never sent over the daemon protocol. type Credential struct { ID string - WorkspaceID string Name string Type string CredentialHash string diff --git a/services/core/internal/runtimedevice/state.go b/services/core/internal/runtimedevice/state.go index 931dde4ae..57de85485 100644 --- a/services/core/internal/runtimedevice/state.go +++ b/services/core/internal/runtimedevice/state.go @@ -4,7 +4,6 @@ package runtimedevice // HeartbeatStatus is the post-heartbeat liveness the runner uses to // detect state changes. type HeartbeatStatus struct { - Liveness string // Deleted is true when the heartbeat UPDATE matched zero rows, // meaning the runtime was soft-deleted (or never existed). The // gateway uses this to send a permanent WS close frame so the @@ -59,8 +58,5 @@ type Heartbeat struct { RuntimeID string // CredentialHash comes from gateway authentication, never a daemon frame. CredentialHash string - DaemonVersion string - ActiveRequests int - HeartbeatTimestamp int64 SupportedAgentKinds []SupportedAgentKind } diff --git a/services/core/internal/runtimeenrollment/connection_test.go b/services/core/internal/runtimeenrollment/connection_test.go index 4db7b557e..36eec4a0a 100644 --- a/services/core/internal/runtimeenrollment/connection_test.go +++ b/services/core/internal/runtimeenrollment/connection_test.go @@ -108,7 +108,7 @@ func (s *liveConnectionStore) GetDeviceCredential(context.Context, string) (runt if s.deviceRevokedAtRecheck && s.authCalls >= 2 { return runtimedevice.Credential{}, false, nil } - return runtimedevice.Credential{ID: "device", WorkspaceID: "tenant", Type: runtimedevice.RuntimeTypeAgentDaemon, CredentialHash: s.digest}, true, nil + return runtimedevice.Credential{ID: "device", Type: runtimedevice.RuntimeTypeAgentDaemon, CredentialHash: s.digest}, true, nil } func TestRuntimeConnectedCurrentAuthorityAfterPeer(t *testing.T) { for _, name := range []string{"connected", "rotated before read", "revoked after peer", "device revoked after peer", "retired after peer", "store error after peer", "device store error after peer", "closed"} { diff --git a/services/core/internal/runtimegateway/auth.go b/services/core/internal/runtimegateway/auth.go index 40fa443f5..d8052e43d 100644 --- a/services/core/internal/runtimegateway/auth.go +++ b/services/core/internal/runtimegateway/auth.go @@ -31,7 +31,6 @@ 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 @@ -81,7 +80,6 @@ func (a *Authenticator) AuthenticateBearer(ctx context.Context, deviceID, bearer } return AuthenticatedRuntime{ DeviceID: rt.ID, - WorkspaceID: rt.WorkspaceID, Name: rt.Name, RuntimeNodeID: rt.RuntimeNodeID, RuntimeAllocationID: rt.RuntimeAllocationID, diff --git a/services/core/internal/runtimegateway/auth_test.go b/services/core/internal/runtimegateway/auth_test.go index 0754f2081..631db7d71 100644 --- a/services/core/internal/runtimegateway/auth_test.go +++ b/services/core/internal/runtimegateway/auth_test.go @@ -80,7 +80,6 @@ func TestAuthenticator_RejectsBadCredential(t *testing.T) { func TestAuthenticator_AcceptsValidCredential(t *testing.T) { row := runtimedevice.Credential{ ID: "dev-1", - WorkspaceID: "wks-1", Name: "alice-mac", Type: runtimedevice.RuntimeTypeAgentDaemon, CredentialHash: runtimedevice.HashCredential("real-tok"), @@ -90,7 +89,7 @@ func TestAuthenticator_AcceptsValidCredential(t *testing.T) { if err != nil { t.Fatalf("expected success, got %v", err) } - if got.DeviceID != "dev-1" || got.WorkspaceID != "wks-1" || got.Name != "alice-mac" { + if got.DeviceID != "dev-1" || got.Name != "alice-mac" { t.Fatalf("unexpected AuthenticatedRuntime: %+v", got) } } diff --git a/services/core/internal/runtimegateway/bootstrap_url_test.go b/services/core/internal/runtimegateway/bootstrap_url_test.go index 7b32c8d82..98c6b8f53 100644 --- a/services/core/internal/runtimegateway/bootstrap_url_test.go +++ b/services/core/internal/runtimegateway/bootstrap_url_test.go @@ -13,7 +13,7 @@ func TestBootstrapPublishesURLOnlyAfterAuthentication(t *testing.T) { const publicURL = "wss://core.example/api/v1/agent-daemon/ws" for _, valid := range []bool{true, false} { h := NewHandler(HandlerConfig{Registry: NewRegistry(), PublicWSURL: publicURL, - Authenticator: NewAuthenticator(&stubRuntimeStore{ok: true, row: runtimedevice.Credential{ID: "device", WorkspaceID: "tenant", Type: runtimedevice.RuntimeTypeAgentDaemon, CredentialHash: runtimedevice.HashCredential(token)}})}) + Authenticator: NewAuthenticator(&stubRuntimeStore{ok: true, row: runtimedevice.Credential{ID: "device", Type: runtimedevice.RuntimeTypeAgentDaemon, CredentialHash: runtimedevice.HashCredential(token)}})}) r := httptest.NewRequest(http.MethodPost, "/agent-daemon/bootstrap", strings.NewReader(`{"device_id":"device"}`)) bearer, status := "wrong", http.StatusUnauthorized if valid { diff --git a/services/core/internal/runtimegateway/cancellation_test.go b/services/core/internal/runtimegateway/cancellation_test.go index 09f8d3eac..04030406a 100644 --- a/services/core/internal/runtimegateway/cancellation_test.go +++ b/services/core/internal/runtimegateway/cancellation_test.go @@ -31,7 +31,7 @@ func (r *receiptStore) ArchivedCancellationReceipt(_ context.Context, id, hash s func TestArchivedReceiptTracksDeliveryBeyondDoneAndRejectsNewWork(t *testing.T) { conn := newFakeConn() - peer := newTestSession(conn, "device", "", "", NewRegistry(), nil) + peer := newTestSession(conn, "device", "", NewRegistry(), nil) defer peer.Close("test complete") peer.credentialHash = "original-hash" receipt := runtimedevice.ArchivedCancellationReceipt{RunID: "run", Deadline: time.Now().Add(time.Second)} @@ -94,7 +94,7 @@ func (c *blockedControlConn) WriteControl(kind int, data []byte, _ time.Time) er func TestCloseWithCodeUsesConcurrentControlWriter(t *testing.T) { conn := &blockedControlConn{fakeConn: newFakeConn(), entered: make(chan struct{}), resume: make(chan struct{}), wrote: make(chan struct{})} - peer := newTestSession(conn, "device", "", "", NewRegistry(), nil) + peer := newTestSession(conn, "device", "", NewRegistry(), nil) peer.Start() env, _ := proto.NewEnvelope(proto.TypePromptCancel, "run", nil) if err := peer.Send(t.Context(), env); err != nil { @@ -123,7 +123,7 @@ func TestCloseWithCodeUsesConcurrentControlWriter(t *testing.T) { } func TestArchivedReceiptDeadlineDoesNotRenew(t *testing.T) { - peer := newTestSession(newFakeConn(), "device", "", "", NewRegistry(), nil) + peer := newTestSession(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)} diff --git a/services/core/internal/runtimegateway/capabilities_test.go b/services/core/internal/runtimegateway/capabilities_test.go index 54baa1774..05e8cac2a 100644 --- a/services/core/internal/runtimegateway/capabilities_test.go +++ b/services/core/internal/runtimegateway/capabilities_test.go @@ -31,7 +31,7 @@ func TestCapabilityProjectionCoversEveryField(t *testing.T) { } func TestInvalidHeartbeatDiscardsPreviousDeclarationAndClosesTransport(t *testing.T) { - session := newTestSession(newFakeConn(), "runtime", "workspace", proto.Version, NewRegistry(), nil) + session := newTestSession(newFakeConn(), "runtime", proto.Version, NewRegistry(), nil) valid, err := proto.NewEnvelope(proto.TypeHeartbeat, "", proto.HeartbeatPayload{SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "fixture", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}}}) if err != nil { t.Fatal(err) @@ -51,7 +51,7 @@ func TestInvalidHeartbeatDiscardsPreviousDeclarationAndClosesTransport(t *testin func TestFirstHeartbeatAnnouncesAgentKinds(t *testing.T) { registry := NewRegistry() - session := newTestSession(newFakeConn(), "runtime", "workspace", proto.Version, registry, nil) + session := newTestSession(newFakeConn(), "runtime", proto.Version, registry, nil) heartbeat, err := proto.NewEnvelope(proto.TypeHeartbeat, "", proto.HeartbeatPayload{SupportedAgentKinds: []proto.SupportedAgentKind{{Kind: "fixture", Available: true, Capabilities: prototest.Capabilities(proto.AgentKindCapabilities{})}}}) if err != nil { t.Fatal(err) diff --git a/services/core/internal/runtimegateway/handler.go b/services/core/internal/runtimegateway/handler.go index ac5f3a5f7..8f5ff6822 100644 --- a/services/core/internal/runtimegateway/handler.go +++ b/services/core/internal/runtimegateway/handler.go @@ -127,7 +127,7 @@ func (h *Handler) WS(w http.ResponseWriter, r *http.Request) { h.cfg.Log("agentdaemon gateway: heartbeat on connect: %v", hbErr) } } - sess := NewSession(conn, auth.DeviceID, auth.WorkspaceID, version, h.cfg.Registry, h.cfg.Log) + sess := NewSession(conn, auth.DeviceID, version, h.cfg.Registry, h.cfg.Log) sess.heartbeat = h.cfg.Heartbeat sess.archivedCancellations = h.cfg.ArchivedCancellations sess.credentialHash = runtimedevice.HashCredential(token) @@ -176,10 +176,8 @@ func (h *Handler) Bootstrap(w http.ResponseWriter, r *http.Request) { } resp := map[string]any{ "device_id": auth.DeviceID, - "workspace_id": auth.WorkspaceID, "ws_url": h.cfg.PublicWSURL, "heartbeat_seconds": int(h.cfg.HeartbeatInterval.Seconds()), - "protocol_version": proto.Version, } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) diff --git a/services/core/internal/runtimegateway/handler_test.go b/services/core/internal/runtimegateway/handler_test.go index 59b853cc8..be6ffb87b 100644 --- a/services/core/internal/runtimegateway/handler_test.go +++ b/services/core/internal/runtimegateway/handler_test.go @@ -31,7 +31,7 @@ func TestWebSocketRequiresAuthorizationBearer(t *testing.T) { handler := NewHandler(HandlerConfig{ Registry: registry, Authenticator: NewAuthenticator(&stubRuntimeStore{ok: true, row: runtimedevice.Credential{ - ID: "device", WorkspaceID: "tenant", Type: runtimedevice.RuntimeTypeAgentDaemon, + ID: "device", Type: runtimedevice.RuntimeTypeAgentDaemon, CredentialHash: runtimedevice.HashCredential(credential), }}), }) diff --git a/services/core/internal/runtimegateway/preparation_test.go b/services/core/internal/runtimegateway/preparation_test.go index 421faf5a8..aaea2352d 100644 --- a/services/core/internal/runtimegateway/preparation_test.go +++ b/services/core/internal/runtimegateway/preparation_test.go @@ -10,7 +10,7 @@ import ( func TestPreparationSubscriptionHasNoRunIdentityAndOrdersRevisions(t *testing.T) { registry := NewRegistry() - s := newTestSession(newFakeConn(), "device", "tenant", "test", registry, nil) + s := newTestSession(newFakeConn(), "device", "test", registry, nil) defer s.Close("test") sub, err := s.SubscribePreparation("request") if err != nil { @@ -48,7 +48,7 @@ func TestPreparationSubscriptionHasNoRunIdentityAndOrdersRevisions(t *testing.T) func TestPreparationCloseAndOverflowDoNotInventRunEvents(t *testing.T) { for _, overflow := range []bool{false, true} { - s := newTestSession(newFakeConn(), "device", "tenant", "test", NewRegistry(), nil) + s := newTestSession(newFakeConn(), "device", "test", NewRegistry(), nil) sub, err := s.SubscribePreparation("request") if err != nil { t.Fatal(err) diff --git a/services/core/internal/runtimegateway/registry_test.go b/services/core/internal/runtimegateway/registry_test.go index 34a55af4b..a0846acbc 100644 --- a/services/core/internal/runtimegateway/registry_test.go +++ b/services/core/internal/runtimegateway/registry_test.go @@ -10,7 +10,7 @@ import ( func TestWaitForDevice_FastPath(t *testing.T) { reg := NewRegistry() - sess := newTestSession(newFakeConn(), "dev-warm", "wks-1", "0.1.0", reg, nil) + sess := newTestSession(newFakeConn(), "dev-warm", "0.1.0", reg, nil) reg.Register(sess) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) @@ -27,7 +27,7 @@ func TestWaitForDevice_FastPath(t *testing.T) { func TestWaitForDevice_SignalledByRegister(t *testing.T) { reg := NewRegistry() - sess := newTestSession(newFakeConn(), "dev-cold", "wks-1", "0.1.0", reg, nil) + sess := newTestSession(newFakeConn(), "dev-cold", "0.1.0", reg, nil) ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() @@ -114,7 +114,7 @@ func TestWaitForDevice_EmptyDeviceID(t *testing.T) { // the post-race shape. func TestWaitForDevice_DoubleCheckAfterLock(t *testing.T) { reg := NewRegistry() - sess := newTestSession(newFakeConn(), "dev-race", "wks-1", "0.1.0", reg, nil) + sess := newTestSession(newFakeConn(), "dev-race", "0.1.0", reg, nil) reg.Register(sess) got, err := reg.WaitForDevice(context.Background(), "dev-race", 100*time.Millisecond) @@ -137,7 +137,7 @@ func TestWaitForDevice_DoubleCheckAfterLock(t *testing.T) { // unblock with the same session. func TestWaitForDevice_MultipleWaitersSameDevice(t *testing.T) { reg := NewRegistry() - sess := newTestSession(newFakeConn(), "dev-shared", "wks-1", "0.1.0", reg, nil) + sess := newTestSession(newFakeConn(), "dev-shared", "0.1.0", reg, nil) var wg sync.WaitGroup var mu sync.Mutex @@ -181,7 +181,7 @@ func TestWatchDeviceImmediateDeferredAndUnwatch(t *testing.T) { if len(r.PendingWaiters("late")) != 1 { t.Fatal("rearming duplicated watch") } - s := newTestSession(newFakeConn(), "late", "workspace", "0.1.0", r, nil) + s := newTestSession(newFakeConn(), "late", "0.1.0", r, nil) r.Register(s) if got := <-ch; got != s { t.Fatal("wrong registration") @@ -196,7 +196,7 @@ func TestWatchDeviceImmediateDeferredAndUnwatch(t *testing.T) { r.WatchDevice("removed", ch) r.UnwatchDevice("removed", ch) r.UnwatchDevice("removed", ch) - r.Register(newTestSession(newFakeConn(), "removed", "workspace", "0.1.0", r, nil)) + r.Register(newTestSession(newFakeConn(), "removed", "0.1.0", r, nil)) select { case <-ch: t.Fatal("removed watch fired") @@ -211,7 +211,7 @@ func TestWatchDeviceSharedChannelCoalescesWithoutBlockingRegistration(t *testing r.WatchDevice(id, ch) } for _, id := range []string{"a", "b"} { - r.Register(newTestSession(newFakeConn(), id, "workspace", "0.1.0", r, nil)) + r.Register(newTestSession(newFakeConn(), id, "0.1.0", r, nil)) } if len(ch) != 1 || len(r.PendingWaiters("a")) != 0 || len(r.PendingWaiters("b")) != 0 { t.Fatal("coalesced registrations retained waiter state") diff --git a/services/core/internal/runtimegateway/runtime_prepare_test.go b/services/core/internal/runtimegateway/runtime_prepare_test.go index 3402a2d87..d19cf2917 100644 --- a/services/core/internal/runtimegateway/runtime_prepare_test.go +++ b/services/core/internal/runtimegateway/runtime_prepare_test.go @@ -67,7 +67,7 @@ func noCapabilityFrame(t *testing.T, s *Session) { } func TestCapabilitiesTransfersMoreThanFrameLimitAndCorrelates(t *testing.T) { - s := newTestSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "test", nil, nil) defer s.Close("test") data := bytes.Repeat([]byte{0, 255, 3}, 2<<20) id := uuid.NewString() @@ -115,7 +115,7 @@ func TestCapabilitiesTransfersMoreThanFrameLimitAndCorrelates(t *testing.T) { } func TestCapabilitiesFinalizeTransfersNoArchive(t *testing.T) { - s := newTestSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "test", nil, nil) defer s.Close("test") request := proto.RuntimePreparePayload{BudgetMS: 300000, EnvironmentID: uuid.NewString(), SessionID: uuid.NewString(), Action: "finalize", Sources: &agentcapabilities.Input{}} id := uuid.NewString() @@ -137,7 +137,7 @@ func TestCapabilitiesFinalizeTransfersNoArchive(t *testing.T) { } func TestCapabilitiesRefusesConflictingArchiveBeforeSending(t *testing.T) { - s := newTestSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "test", nil, nil) defer s.Close("test") for _, mutate := range []func(*proto.RuntimePreparePayload){ func(p *proto.RuntimePreparePayload) { p.SHA256 = "incorrect" }, @@ -163,7 +163,7 @@ func TestCapabilitiesStopsAtTerminalOrMalformedReceipt(t *testing.T) { "unsafe code": {Outcome: "rejected", ErrorCode: "private detail"}, } { t.Run(name, func(t *testing.T) { - s := newTestSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "test", nil, nil) defer s.Close("test") id := uuid.NewString() done := beginCapabilities(s, t.Context(), id, skillPreparation(), []byte("data")) @@ -183,7 +183,7 @@ func TestCapabilitiesStopsAtTerminalOrMalformedReceipt(t *testing.T) { } func TestCapabilitiesRejectsWrongChunkReceipt(t *testing.T) { - s := newTestSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "test", nil, nil) defer s.Close("test") id := uuid.NewString() done := beginCapabilities(s, t.Context(), id, skillPreparation(), []byte("data")) @@ -201,7 +201,7 @@ func TestCapabilitiesRejectsWrongChunkReceipt(t *testing.T) { func TestCapabilitiesConnectionOwnershipAndUnknownInterruption(t *testing.T) { for _, closeConnection := range []bool{false, true} { t.Run(map[bool]string{false: "deadline", true: "disconnect"}[closeConnection], func(t *testing.T) { - s := newTestSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "test", nil, nil) defer s.Close("test") ctx, cancel := context.WithTimeout(t.Context(), 150*time.Millisecond) defer cancel() @@ -238,7 +238,7 @@ func TestCapabilitiesConnectionOwnershipAndUnknownInterruption(t *testing.T) { func TestRuntimeInitialFileChunking(t *testing.T) { for _, size := range []int{0, (4 << 20) + 1} { t.Run(fmt.Sprint(size), func(t *testing.T) { - s := newTestSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "test", nil, nil) defer s.Close("test") request := proto.RuntimePreparePayload{BudgetMS: 300000, EnvironmentID: uuid.NewString(), SessionID: uuid.NewString(), Action: "file", File: &proto.RuntimeInitialFile{Path: "/workspace/project/file"}} data := bytes.Repeat([]byte("z"), size) @@ -288,7 +288,7 @@ func TestRuntimeInitialFileChunking(t *testing.T) { func TestRuntimeInitializationNoDataAndExitReceipt(t *testing.T) { for _, exit := range []int{0, 42, 256} { t.Run(fmt.Sprint(exit), func(t *testing.T) { - s := newTestSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "test", nil, nil) defer s.Close("test") request := proto.RuntimePreparePayload{BudgetMS: 300000, EnvironmentID: uuid.NewString(), SessionID: uuid.NewString(), Action: "initialize", Initialization: &proto.RuntimeInitialization{Action: "setup", Command: "echo test"}} if _, err := s.PrepareRuntime(t.Context(), uuid.NewString(), request, []byte("forbidden")); err == nil { @@ -333,7 +333,7 @@ func TestRuntimePreparationSeparatesTransferAndApplyBudgets(t *testing.T) { for _, commit := range []bool{false, true} { t.Run(fmt.Sprint(commit), func(t *testing.T) { synctest.Test(t, func(t *testing.T) { - s := newTestSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "test", nil, nil) defer s.Close("test") request := proto.RuntimePreparePayload{BudgetMS: 300000, EnvironmentID: uuid.NewString(), SessionID: uuid.NewString(), Action: "initialize", Initialization: &proto.RuntimeInitialization{Action: "configure"}} id := uuid.NewString() @@ -374,7 +374,7 @@ func TestRuntimePreparationSeparatesTransferAndApplyBudgets(t *testing.T) { func TestRuntimePreparationCommittedApplyStopsWaitingOnDisconnectOrCallerDeadline(t *testing.T) { for _, disconnect := range []bool{false, true} { t.Run(fmt.Sprint(disconnect), func(t *testing.T) { - s := newTestSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "test", nil, nil) defer s.Close("test") ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond) defer cancel() diff --git a/services/core/internal/runtimegateway/session.go b/services/core/internal/runtimegateway/session.go index a2e48df53..a1004b4bf 100644 --- a/services/core/internal/runtimegateway/session.go +++ b/services/core/internal/runtimegateway/session.go @@ -73,7 +73,6 @@ type SessionLogger func(format string, args ...any) // violating gorilla's "single writer" requirement. type Session struct { DeviceID string - WorkspaceID string DaemonVersion string ConnectedAt time.Time @@ -137,11 +136,10 @@ type Session struct { // NewSession wires a freshly-upgraded WS connection into a Session. The session // does NOT start its goroutines automatically — Start runs once the handler is // ready so the session can't race with response writes. -func NewSession(conn WSConn, deviceID, workspaceID, daemonVersion string, reg *Registry, log SessionLogger) *Session { +func NewSession(conn WSConn, deviceID, daemonVersion string, reg *Registry, log SessionLogger) *Session { now := time.Now() return &Session{ DeviceID: deviceID, - WorkspaceID: workspaceID, DaemonVersion: daemonVersion, ConnectedAt: now, conn: conn, @@ -411,9 +409,6 @@ func (s *Session) handleHeartbeat(env proto.Envelope) { status, err := s.heartbeat.TouchAgentDaemonHeartbeat(ctx, runtimedevice.Heartbeat{ RuntimeID: s.DeviceID, CredentialHash: s.credentialHash, - DaemonVersion: p.DaemonVersion, - ActiveRequests: p.ActiveRequests, - HeartbeatTimestamp: p.Timestamp, SupportedAgentKinds: kinds, }) if err != nil { diff --git a/services/core/internal/runtimegateway/session_test.go b/services/core/internal/runtimegateway/session_test.go index 32cb3f7d0..15cb6f889 100644 --- a/services/core/internal/runtimegateway/session_test.go +++ b/services/core/internal/runtimegateway/session_test.go @@ -15,14 +15,14 @@ import ( ) // newTestSession builds a Session over a test connection, defaulting the log and Registry. -func newTestSession(conn WSConn, deviceID, workspaceID, daemonVersion string, reg *Registry, log SessionLogger) *Session { +func newTestSession(conn WSConn, deviceID, daemonVersion string, reg *Registry, log SessionLogger) *Session { if log == nil { log = func(string, ...any) {} } if reg == nil { reg = NewRegistry() } - return NewSession(conn, deviceID, workspaceID, daemonVersion, reg, log) + return NewSession(conn, deviceID, daemonVersion, reg, log) } // fakeConn is the WSConn implementation used by session + registry @@ -129,7 +129,7 @@ func (f *fakeHeartbeatStore) TouchRuntimeHeartbeat(_ context.Context, runtimeID f.mu.Lock() f.runtime = append(f.runtime, runtimeID) f.mu.Unlock() - return runtimedevice.HeartbeatStatus{Liveness: "online"}, nil + return runtimedevice.HeartbeatStatus{}, nil } func (f *fakeHeartbeatStore) TouchAgentDaemonHeartbeat(_ context.Context, input runtimedevice.Heartbeat) (runtimedevice.HeartbeatStatus, error) { @@ -140,7 +140,7 @@ func (f *fakeHeartbeatStore) TouchAgentDaemonHeartbeat(_ context.Context, input case f.daemonCh <- input: default: } - return runtimedevice.HeartbeatStatus{Liveness: "online"}, nil + return runtimedevice.HeartbeatStatus{}, nil } func (f *fakeHeartbeatStore) waitDaemonHeartbeat(t *testing.T) runtimedevice.Heartbeat { @@ -158,7 +158,7 @@ func (f *fakeHeartbeatStore) waitDaemonHeartbeat(t *testing.T) runtimedevice.Hea func TestRegistry_RegisterAndLookup(t *testing.T) { reg := NewRegistry() - sess := newTestSession(newFakeConn(), "dev-1", "wks-1", "0.1.0", reg, nil) + sess := newTestSession(newFakeConn(), "dev-1", "0.1.0", reg, nil) if prev := reg.Register(sess); prev != nil { t.Fatalf("first Register returned non-nil prev") } @@ -170,11 +170,11 @@ func TestRegistry_RegisterAndLookup(t *testing.T) { func TestRegistry_RegisterReplacesAndEvictsRuns(t *testing.T) { reg := NewRegistry() - old := newTestSession(newFakeConn(), "dev-1", "wks-1", "0.1.0", reg, nil) + old := newTestSession(newFakeConn(), "dev-1", "0.1.0", reg, nil) reg.Register(old) reg.AttachRun("run-1", old) - new := newTestSession(newFakeConn(), "dev-1", "wks-1", "0.1.0", reg, nil) + new := newTestSession(newFakeConn(), "dev-1", "0.1.0", reg, nil) prev := reg.Register(new) if prev != old { t.Fatalf("expected old session as displaced previous, got %p", prev) @@ -188,9 +188,9 @@ func TestRegistry_RegisterReplacesAndEvictsRuns(t *testing.T) { func TestRegistry_DeregisterPreservesNewer(t *testing.T) { reg := NewRegistry() - old := newTestSession(newFakeConn(), "dev-1", "wks-1", "0.1.0", reg, nil) + old := newTestSession(newFakeConn(), "dev-1", "0.1.0", reg, nil) reg.Register(old) - new := newTestSession(newFakeConn(), "dev-1", "wks-1", "0.1.0", reg, nil) + new := newTestSession(newFakeConn(), "dev-1", "0.1.0", reg, nil) reg.Register(new) // A stale Deregister from the old session (e.g. its read loop // wakes after preemption) must NOT remove the new session. @@ -206,7 +206,7 @@ func TestRegistry_DeregisterPreservesNewer(t *testing.T) { func TestSession_DispatchDeliversToSubscriber(t *testing.T) { reg := NewRegistry() conn := newFakeConn() - sess := newTestSession(conn, "dev-1", "wks-1", "0.1.0", reg, nil) + sess := newTestSession(conn, "dev-1", "0.1.0", reg, nil) sess.Start() defer sess.Close("test done") @@ -232,7 +232,7 @@ func TestSession_DispatchDeliversToSubscriber(t *testing.T) { func TestSession_DoneFrameAutoUnsubscribes(t *testing.T) { reg := NewRegistry() conn := newFakeConn() - sess := newTestSession(conn, "dev-1", "wks-1", "0.1.0", reg, nil) + sess := newTestSession(conn, "dev-1", "0.1.0", reg, nil) sess.Start() defer sess.Close("test done") @@ -265,7 +265,7 @@ func TestSession_DoneFrameAutoUnsubscribes(t *testing.T) { } func TestSession_CloseReportsUnknownWithoutExecutionEvents(t *testing.T) { - sess := newTestSession(newFakeConn(), "device", "tenant", proto.Version, NewRegistry(), nil) + sess := newTestSession(newFakeConn(), "device", proto.Version, NewRegistry(), nil) sub, err := sess.SubscribeDurable("run") if err != nil { t.Fatal(err) @@ -280,7 +280,7 @@ func TestSession_CloseReportsUnknownWithoutExecutionEvents(t *testing.T) { } func TestSession_NoCapabilitiesBeforeHeartbeat(t *testing.T) { - sess := newTestSession(newFakeConn(), "device", "tenant", proto.Version, NewRegistry(), nil) + sess := newTestSession(newFakeConn(), "device", proto.Version, NewRegistry(), nil) defer sess.Close("test done") for _, kind := range []string{"fake_alpha", "codex", "contract"} { if info, found, known := sess.AgentKindStatus(kind); found || known || info.Available { @@ -291,7 +291,7 @@ func TestSession_NoCapabilitiesBeforeHeartbeat(t *testing.T) { func TestSession_SendOnClosedReturnsError(t *testing.T) { reg := NewRegistry() - sess := newTestSession(newFakeConn(), "dev-1", "wks-1", "0.1.0", reg, nil) + sess := newTestSession(newFakeConn(), "dev-1", "0.1.0", reg, nil) sess.Close("never started") env, _ := proto.NewEnvelope(proto.TypePromptCancel, "run-1", nil) err := sess.Send(context.Background(), env) @@ -303,7 +303,7 @@ func TestSession_SendOnClosedReturnsError(t *testing.T) { func TestSession_SendWritesToWire(t *testing.T) { reg := NewRegistry() conn := newFakeConn() - sess := newTestSession(conn, "dev-1", "wks-1", "0.1.0", reg, nil) + sess := newTestSession(conn, "dev-1", "0.1.0", reg, nil) sess.Start() defer sess.Close("test done") @@ -332,15 +332,12 @@ func TestSession_HeartbeatPersistsSupportedAgentKinds(t *testing.T) { reg := NewRegistry() conn := newFakeConn() heartbeat := newFakeHeartbeatStore() - sess := newTestSession(conn, "dev-1", "wks-1", "0.1.0", reg, nil) + sess := newTestSession(conn, "dev-1", "0.1.0", reg, nil) sess.heartbeat = heartbeat sess.Start() defer sess.Close("test done") env, _ := proto.NewEnvelope(proto.TypeHeartbeat, "", proto.HeartbeatPayload{ - Timestamp: 1710000000, - ActiveRequests: 2, - DaemonVersion: "0.2.0-test", SupportedAgentKinds: []proto.SupportedAgentKind{ { Kind: "fake_beta", @@ -371,7 +368,7 @@ func TestSession_HeartbeatPersistsSupportedAgentKinds(t *testing.T) { conn.Feed(raw) got := heartbeat.waitDaemonHeartbeat(t) - if got.RuntimeID != "dev-1" || got.DaemonVersion != "0.2.0-test" || got.ActiveRequests != 2 || got.HeartbeatTimestamp != 1710000000 { + if got.RuntimeID != "dev-1" { t.Fatalf("heartbeat metadata not preserved: %+v", got) } if len(got.SupportedAgentKinds) != 3 { @@ -405,7 +402,7 @@ func TestSession_HeartbeatDoesNotInferCapabilities(t *testing.T) { reg := NewRegistry() conn := newFakeConn() heartbeat := newFakeHeartbeatStore() - sess := newTestSession(conn, "device", "tenant", proto.Version, reg, nil) + sess := newTestSession(conn, "device", proto.Version, reg, nil) sess.heartbeat = heartbeat sess.Start() defer sess.Close("test done") diff --git a/services/core/internal/runtimegateway/subscription_test.go b/services/core/internal/runtimegateway/subscription_test.go index 2cc9177b4..289b25c8e 100644 --- a/services/core/internal/runtimegateway/subscription_test.go +++ b/services/core/internal/runtimegateway/subscription_test.go @@ -9,7 +9,7 @@ import ( ) func TestDurableSubscriptionOverflowIsExplicitAndIsolated(t *testing.T) { - s := newTestSession(newFakeConn(), "device", "tenant", proto.Version, NewRegistry(), nil) + s := newTestSession(newFakeConn(), "device", proto.Version, NewRegistry(), nil) defer s.Close("test finished") sub, err := s.SubscribeDurable("slow") if err != nil { @@ -35,7 +35,7 @@ func TestDurableSubscriptionOverflowIsExplicitAndIsolated(t *testing.T) { func TestSubscriptionCloseAndDispatchAreSerialized(t *testing.T) { for range 100 { - s := newTestSession(newFakeConn(), "device", "tenant", proto.Version, NewRegistry(), nil) + s := newTestSession(newFakeConn(), "device", proto.Version, NewRegistry(), nil) sub, _ := s.SubscribeDurable("run") var wg sync.WaitGroup wg.Add(3) diff --git a/services/core/internal/runtimegateway/suspension_test.go b/services/core/internal/runtimegateway/suspension_test.go index f66eb052c..f4c724bbf 100644 --- a/services/core/internal/runtimegateway/suspension_test.go +++ b/services/core/internal/runtimegateway/suspension_test.go @@ -9,7 +9,7 @@ import ( func TestSuspendAcknowledgementSurvivesImmediateConnectionClose(t *testing.T) { for range 100 { - s := newTestSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "test", nil, nil) done := make(chan error, 1) go func() { result, err := s.SuspendControl(t.Context(), proto.TypeEnvironmentQuiesce, proto.EnvironmentSuspendPayload{EnvironmentID: "env", SuspendID: "attempt"}) @@ -33,7 +33,7 @@ func TestSuspendControlRejectsForeignIdentityAndDoesNotReplay(t *testing.T) { {EnvironmentID: "env", SuspendID: "old", Accepted: true}, {EnvironmentID: "env", SuspendID: "attempt", Accepted: true, ErrorCode: "resource_busy"}, } { - s := newTestSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "test", nil, nil) done := make(chan error, 1) go func() { _, err := s.SuspendControl(t.Context(), proto.TypeEnvironmentResume, proto.EnvironmentSuspendPayload{EnvironmentID: "env", SuspendID: "attempt"}) @@ -54,7 +54,7 @@ func TestSuspendControlRejectsForeignIdentityAndDoesNotReplay(t *testing.T) { } } func TestSuspendObserverCancellationRetainsUnknownOutcome(t *testing.T) { - s := newTestSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "test", nil, nil) defer s.Close("test") ctx, cancel := context.WithCancel(t.Context()) done := make(chan error, 1) diff --git a/services/core/internal/runtimegateway/wire_test.go b/services/core/internal/runtimegateway/wire_test.go index 7b09c5e9b..5fb6f3e72 100644 --- a/services/core/internal/runtimegateway/wire_test.go +++ b/services/core/internal/runtimegateway/wire_test.go @@ -28,7 +28,7 @@ const wait = 3 * time.Second type credentialStore struct{} func (credentialStore) GetDeviceCredential(context.Context, string) (runtimedevice.Credential, bool, error) { - return runtimedevice.Credential{ID: prototest.DeviceID, WorkspaceID: "tenant", Type: runtimedevice.RuntimeTypeAgentDaemon, CredentialHash: runtimedevice.HashCredential(prototest.Credential)}, true, nil + return runtimedevice.Credential{ID: prototest.DeviceID, Type: runtimedevice.RuntimeTypeAgentDaemon, CredentialHash: runtimedevice.HashCredential(prototest.Credential)}, true, nil } func newGateway(t *testing.T) (*runtimegateway.Registry, string) { diff --git a/services/core/internal/runtimegateway/workspace_directory_test.go b/services/core/internal/runtimegateway/workspace_directory_test.go index 05704e9cb..c9ad4f7d7 100644 --- a/services/core/internal/runtimegateway/workspace_directory_test.go +++ b/services/core/internal/runtimegateway/workspace_directory_test.go @@ -10,7 +10,7 @@ import ( ) func TestWorkspaceDirectorySharesReadCorrelationAndFrameBound(t *testing.T) { - s := newTestSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "test", nil, nil) defer s.Close("test") request := proto.WorkspaceReadPayload{Handle: "prepared", EnvironmentID: "environment", MaxEntries: proto.WorkspaceDirectoryMaxEntries} done := make(chan error, 1) @@ -75,7 +75,7 @@ func TestWorkspaceDirectoryRequiresExplicitWireTruncation(t *testing.T) { {"truncated", `{"entries":[{"name":"dir","kind":"directory","size_bytes":null}],"truncated":true}`, true}, } { t.Run(tc.name, func(t *testing.T) { - s := newTestSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "test", nil, nil) defer s.Close("test") done := make(chan error, 1) go func() { diff --git a/services/core/internal/runtimegateway/workspace_export_test.go b/services/core/internal/runtimegateway/workspace_export_test.go index 595b23bf0..9938f300f 100644 --- a/services/core/internal/runtimegateway/workspace_export_test.go +++ b/services/core/internal/runtimegateway/workspace_export_test.go @@ -21,7 +21,7 @@ func exportReply(t *testing.T, s *Session, id string, result proto.WorkspaceExpo } func TestWorkspaceExportPullsOnlyAfterConsumedAndRequiresCompletion(t *testing.T) { - s := newTestSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "test", nil, nil) defer s.Close("test") consumed, resume := make(chan struct{}), make(chan struct{}) done := make(chan error, 1) @@ -74,7 +74,7 @@ func TestWorkspaceExportRejectsInvalidChunksAndNativeFailure(t *testing.T) { {Outcome: "completed", Data: []byte("x")}, {Outcome: "failed", ErrorCode: "export_failed"}, } { - s := newTestSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "test", nil, nil) done := make(chan error, 1) go func() { done <- s.ExportWorkspaceOutputs(t.Context(), proto.WorkspaceExportPayload{Handle: "prepared", EnvironmentID: "env"}, func(r io.Reader) error { _, err := io.Copy(io.Discard, r); return err }) @@ -95,7 +95,7 @@ func TestWorkspaceExportRejectsInvalidChunksAndNativeFailure(t *testing.T) { func TestWorkspaceExportDisconnectAndCancellationSettleRead(t *testing.T) { for _, disconnect := range []bool{false, true} { - s := newTestSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "test", nil, nil) ctx, cancel := context.WithCancel(t.Context()) done := make(chan error, 1) go func() { diff --git a/services/core/internal/runtimegateway/workspace_read_test.go b/services/core/internal/runtimegateway/workspace_read_test.go index ed8089822..49de66acf 100644 --- a/services/core/internal/runtimegateway/workspace_read_test.go +++ b/services/core/internal/runtimegateway/workspace_read_test.go @@ -17,7 +17,7 @@ func workspaceReadRequest() proto.WorkspaceReadPayload { } func TestWorkspaceReadCorrelatesOneBoundedResult(t *testing.T) { - s := newTestSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "test", nil, nil) defer s.Close("test") done := make(chan error, 1) data := bytes.Repeat([]byte{0, 127, 255, 3}, proto.WorkspaceReadMaxBytes/4) @@ -54,7 +54,7 @@ func TestWorkspaceReadRejectsIncompleteOrContradictoryReplies(t *testing.T) { {Outcome: "unknown", ErrorCode: "read_unconfirmed", Data: []byte("partial")}, {Outcome: "rejected", ErrorCode: "native-private-secret"}, } { - s := newTestSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "test", nil, nil) done := make(chan error, 1) go func() { _, err := s.ReadWorkspaceFile(t.Context(), workspaceReadRequest()); done <- err }() request := <-s.sendCh @@ -82,7 +82,7 @@ func TestWorkspaceDirectoryAcceptsNotDirectoryOnlyForDirectoryReads(t *testing.T {true, proto.WorkspaceReadResultPayload{Outcome: "completed", CloseAcknowledged: true, ErrorCode: proto.WorkspaceReadNotDirectory, Directory: &proto.WorkspaceDirectoryResult{Entries: []proto.WorkspaceDirectoryEntry{}}}, false}, {false, proto.WorkspaceReadResultPayload{Outcome: "rejected", ErrorCode: proto.WorkspaceReadNotDirectory}, false}, } { - s := newTestSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "test", nil, nil) done := make(chan error, 1) go func() { var err error @@ -104,7 +104,7 @@ func TestWorkspaceDirectoryAcceptsNotDirectoryOnlyForDirectoryReads(t *testing.T } func TestWorkspaceReadObserverCancellationDoesNotSendCancelOrRetry(t *testing.T) { - s := newTestSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "test", nil, nil) defer s.Close("test") ctx, cancel := context.WithCancel(t.Context()) done := make(chan error, 1) @@ -130,7 +130,7 @@ func TestWorkspaceReadObserverCancellationDoesNotSendCancelOrRetry(t *testing.T) } func TestWorkspaceReadCapacityAndConnectionLoss(t *testing.T) { - s := newTestSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "test", nil, nil) done := make(chan error, 4) for i := 0; i < 4; i++ { go func() { _, err := s.ReadWorkspaceFile(t.Context(), workspaceReadRequest()); done <- err }() @@ -154,7 +154,7 @@ func TestWorkspaceReadCapacityAndConnectionLoss(t *testing.T) { func TestWorkspaceReadRejectsOversizedRequestsBeforeQueueing(t *testing.T) { for _, field := range []string{"path", "handle", "environment", "escaped"} { t.Run(field, func(t *testing.T) { - s := newTestSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "test", nil, nil) defer s.Close("test") request := workspaceReadRequest() oversized := strings.Repeat("x", int(ReadLimit)) diff --git a/services/core/internal/runtimegateway/workspace_write_test.go b/services/core/internal/runtimegateway/workspace_write_test.go index 7bd10a4f7..99dab5b33 100644 --- a/services/core/internal/runtimegateway/workspace_write_test.go +++ b/services/core/internal/runtimegateway/workspace_write_test.go @@ -13,7 +13,7 @@ import ( ) func TestWorkspaceWriteChunksAndCorrelatesReceipt(t *testing.T) { - s := newTestSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "test", nil, nil) defer s.Close("test") data := bytes.Repeat([]byte{0, 255, 3}, 400000) id := uuid.NewString() @@ -67,7 +67,7 @@ func TestWorkspaceWriteChunksAndCorrelatesReceipt(t *testing.T) { } func TestWorkspaceWriteDoesNotCommitAfterLostObservation(t *testing.T) { - s := newTestSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "test", nil, nil) defer s.Close("test") ctx, cancel := context.WithCancel(t.Context()) done := make(chan error, 1) diff --git a/services/core/internal/runtimeobs/sampler.go b/services/core/internal/runtimeobs/sampler.go index a89384344..75b67a261 100644 --- a/services/core/internal/runtimeobs/sampler.go +++ b/services/core/internal/runtimeobs/sampler.go @@ -45,7 +45,6 @@ type SamplerOptions struct { // SweepResult is deliberately low-cardinality. It reports collection coverage // without exposing tenant, Session, allocation, provider-native, or error text. type SweepResult struct { - StartedAt, CompletedAt time.Time Listed, Observed, Failed int Complete bool } @@ -55,7 +54,6 @@ type Sampler struct { observer HistoryObserver owner OwnershipChecker options SamplerOptions - now func() time.Time } func NewSampler(lister SessionLister, observer HistoryObserver, owner OwnershipChecker, options SamplerOptions) (*Sampler, error) { @@ -80,14 +78,12 @@ func NewSampler(lister SessionLister, observer HistoryObserver, owner OwnershipC if options.SourceTimeout < time.Millisecond || options.SourceTimeout > 30*time.Second { return nil, errors.New("Runtime history sampler source timeout is out of range") } - return &Sampler{lister: lister, observer: observer, owner: owner, options: options, now: time.Now}, nil + return &Sampler{lister: lister, observer: observer, owner: owner, options: options}, nil } // Sweep performs one full keyset sweep. A failed sweep is isolated from // execution; the caller repeats sweeps without overlap. func (s *Sampler) Sweep(ctx context.Context) (result SweepResult) { - result.StartedAt = s.now() - defer func() { result.CompletedAt = s.now() }() sweepCtx, cancel := context.WithCancel(ctx) defer cancel() if err := s.checkOwnership(sweepCtx); err != nil { diff --git a/services/core/internal/runtimeobs/sampler_test.go b/services/core/internal/runtimeobs/sampler_test.go index da14b77b4..711ad7aee 100644 --- a/services/core/internal/runtimeobs/sampler_test.go +++ b/services/core/internal/runtimeobs/sampler_test.go @@ -129,8 +129,6 @@ func TestSamplerSweepsEveryPageAndIsolatesSessionFailures(t *testing.T) { if err != nil { t.Fatal(err) } - now := time.Date(2026, 9, 23, 4, 0, 0, 0, time.UTC) - sampler.now = func() time.Time { now = now.Add(time.Second); return now } result := sampler.Sweep(t.Context()) if !result.Complete || result.Listed != 3 || result.Observed != 2 || result.Failed != 1 { t.Fatalf("unexpected sweep result: %+v", result) diff --git a/services/core/internal/sandbox/microsandbox/identity.go b/services/core/internal/sandbox/microsandbox/identity.go index ddcc8d185..82c3b01cc 100644 --- a/services/core/internal/sandbox/microsandbox/identity.go +++ b/services/core/internal/sandbox/microsandbox/identity.go @@ -53,9 +53,6 @@ func Name(c Config, r sandbox.Reference, g uint64) string { func SnapshotReference(c Config, r sandbox.Reference, operation string) string { return Name(c, r, 0) + ":s-" + operation } -func Labels(c Config, r sandbox.Reference) map[string]string { - return map[string]string{"io.oac.installation": c.InstallationID, "io.oac.tenant": r.TenantID, "io.oac.environment": r.EnvironmentID, "io.oac.allocation": r.AllocationID} -} func ValidateCompute(c Config, r sandbox.Reference, v Compute) error { if !ValidReference(r) || v.Name != Name(c, r, v.Generation) { return sandbox.ErrInvalid diff --git a/services/core/internal/sessions/admin.go b/services/core/internal/sessions/admin.go index ab9ae4f58..a2e22dab5 100644 --- a/services/core/internal/sessions/admin.go +++ b/services/core/internal/sessions/admin.go @@ -1,7 +1,6 @@ package sessions import ( - "context" "time" ) @@ -28,18 +27,3 @@ type AdminAssetCounts struct { Vaults int64 `json:"vaults" binding:"required"` Credentials int64 `json:"credentials" binding:"required"` } - -// AdminReader reads the administrator's cross-Project Session views. -type AdminReader interface { - // ListAdminRuntimeTargets pages the live Sessions of the tenants by - // creation time, then ID, ascending or descending, after the Session - // after. A limit outside 1 to 100 or a malformed tenant is - // ErrInvalidInput; an after Session that is malformed, deleted or - // outside the tenants is ErrNotFound. - ListAdminRuntimeTargets(ctx context.Context, tenants []string, after string, limit int, ascending bool) (AdminRuntimeTargetPage, error) - // ReadAdminSummary counts the tenant's assets and visits each live Session - // the filter selects, with GetSession's activity and the ID of the key that - // created it, nil when none is recorded, from one read-only snapshot. A - // malformed tenant is ErrInvalidInput; an error from visit ends the read. - ReadAdminSummary(ctx context.Context, tenant string, filter AdminSummaryFilter, visit func(Session, *string) error) (AdminAssetCounts, error) -} diff --git a/services/core/internal/sessions/artifacts.go b/services/core/internal/sessions/artifacts.go index f7d6178cf..1d9040af4 100644 --- a/services/core/internal/sessions/artifacts.go +++ b/services/core/internal/sessions/artifacts.go @@ -41,25 +41,6 @@ type ArtifactPage struct { NextCursor string } -// ArtifactReader reads a Session's published Artifacts, independently of its -// Environment's availability. -type ArtifactReader interface { - // GetSessionArtifact returns a published Artifact of the visible Session, - // or ErrNotFound. - GetSessionArtifact(ctx context.Context, tenantID, sessionID, artifactID string) (Artifact, error) - // ListSessionArtifacts returns one page of the visible Session's published - // Artifacts in publication-time and ID order. A non-empty environmentID - // keeps the Artifacts that Environment produced; a malformed one matches - // nothing. A limit outside 1..100 is ErrInvalidInput. A cursor that is not - // a published Artifact of the Session, including a malformed one, is - // ErrArtifactCursor. - ListSessionArtifacts(ctx context.Context, tenantID, sessionID, environmentID, cursor string, limit int, ascending bool) (ArtifactPage, error) - // ReadSessionArtifact passes consume a published Artifact and its content - // from one snapshot, so a concurrent deletion never truncates a read it - // admitted. It returns consume's error unchanged. - ReadSessionArtifact(ctx context.Context, tenantID, sessionID, artifactID string, consume func(Artifact, io.Reader) error) error -} - // ArtifactStorage persists a Session's Artifacts. type ArtifactStorage interface { // WithArtifactStaging runs stage in one transaction that commits only when diff --git a/services/core/internal/sessions/devices.go b/services/core/internal/sessions/devices.go index 1a488b93c..85a102e37 100644 --- a/services/core/internal/sessions/devices.go +++ b/services/core/internal/sessions/devices.go @@ -41,7 +41,7 @@ type RuntimeEnrollment struct { // allocation or any promise of live authorization. The Worker rechecks the // socket's key. type EnrolledRuntimeBinding struct { - DeviceID, TenantID, EnvironmentID, SessionID string + DeviceID, TenantID, EnvironmentID string } // EnrollmentAuthority is what an executor credential authorizes when it @@ -65,10 +65,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) // ListEnrolledRuntimeBindings lists the enrolled user-managed Runtimes of // live Environments. ListEnrolledRuntimeBindings(ctx context.Context) ([]EnrolledRuntimeBinding, error) @@ -159,7 +155,7 @@ func (s *Service) TouchAgentDaemonHeartbeat(ctx context.Context, heartbeat runti // belongs to the gateway Registry. Stored declarations describe capabilities, // not current connectivity. func heartbeatStatus(current bool) runtimedevice.HeartbeatStatus { - return runtimedevice.HeartbeatStatus{Liveness: "online", Deleted: !current} + return runtimedevice.HeartbeatStatus{Deleted: !current} } // EnrollRuntime binds a user-managed Runtime to one self_hosted Environment diff --git a/services/core/internal/sessions/devices_test.go b/services/core/internal/sessions/devices_test.go index 6fa241645..0194ccda9 100644 --- a/services/core/internal/sessions/devices_test.go +++ b/services/core/internal/sessions/devices_test.go @@ -173,7 +173,7 @@ func TestDeviceUseCases(t *testing.T) { storage := &fakeStorage{t: t, touchDevice: returns(current), touchAuthenticatedDevice: returns(current)} service := deviceService(t, storage) runtime, err := service.TouchRuntimeHeartbeat(t.Context(), "device") - if err != nil || runtime != (runtimedevice.HeartbeatStatus{Liveness: "online", Deleted: !current}) { + if err != nil || runtime != (runtimedevice.HeartbeatStatus{Deleted: !current}) { t.Fatalf("runtime heartbeat %+v, %v", runtime, err) } daemon, err := service.TouchAgentDaemonHeartbeat(t.Context(), runtimedevice.Heartbeat{RuntimeID: "device", CredentialHash: credentialDigest}) diff --git a/services/core/internal/sessions/environment.go b/services/core/internal/sessions/environment.go index 0172dcda1..d0465df7e 100644 --- a/services/core/internal/sessions/environment.go +++ b/services/core/internal/sessions/environment.go @@ -130,12 +130,11 @@ func (k FileWriteIdentity) Valid() bool { } type EnvironmentFileWrite struct { - Identity FileWriteIdentity - EnvironmentID, SessionID string - State string - CreatedAt time.Time - SettledAt *time.Time - Replayed bool + Identity FileWriteIdentity + State string + CreatedAt time.Time + SettledAt *time.Time + Replayed bool } // ValidateRetainedHistory checks the selected external filesystem and Harness combination. diff --git a/services/core/internal/sessions/execution_file_writes_test.go b/services/core/internal/sessions/execution_file_writes_test.go index 9fab5574f..e6f14259e 100644 --- a/services/core/internal/sessions/execution_file_writes_test.go +++ b/services/core/internal/sessions/execution_file_writes_test.go @@ -52,7 +52,7 @@ var testWriteKey = FileWriteIdentity{ID: testFileWrite, DeviceID: testDevice, Re func TestReserveEnvironmentFileWrite(t *testing.T) { reserve := "WithFileWriteReservation " + testTenant + " " + testEnvironment admission := []string{reserve, "LoadFileWrite " + testFileWrite, "LoadEnvironment", "LoadSessionDevice", "LoadComputeSuspension", "LoadPendingFileWrite", "LoadActiveTurn", "LoadPendingInput"} - pending := EnvironmentFileWrite{Identity: testWriteKey, EnvironmentID: testEnvironment, SessionID: testSession, State: "pending"} + pending := EnvironmentFileWrite{Identity: testWriteKey, State: "pending"} bound := loads(ExecutionDevice{ID: testDevice, EnvironmentID: testEnvironment}, true) admitted := func(tx fakeTx) fakeTx { tx.loadFileWrite = loads(EnvironmentFileWrite{}, false) @@ -116,7 +116,7 @@ func TestSettleEnvironmentFileWrite(t *testing.T) { load := "LoadFileWrite " + testFileWrite settledAt := time.Unix(1, 0) write := func(state string) EnvironmentFileWrite { - return EnvironmentFileWrite{Identity: testWriteKey, EnvironmentID: testEnvironment, SessionID: testSession, State: state, SettledAt: &settledAt} + return EnvironmentFileWrite{Identity: testWriteKey, State: state, SettledAt: &settledAt} } for _, test := range []struct { name string diff --git a/services/core/internal/sessions/executor_credentials.go b/services/core/internal/sessions/executor_credentials.go index d7581791f..25f272e65 100644 --- a/services/core/internal/sessions/executor_credentials.go +++ b/services/core/internal/sessions/executor_credentials.go @@ -13,23 +13,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/projects" ) -// ExecutorCredentialReader authenticates Environment executor credentials and -// reads a Project's credentials for an Environment. -type ExecutorCredentialReader interface { - // AuthenticateEnvironmentExecutor checks, in one database snapshot, that - // the credential whose SHA-256 digest is given is current for the - // Environment and belongs to its Session's creator, and returns the - // Session's tenant. Any other credential is ErrNotFound. - AuthenticateEnvironmentExecutor(ctx context.Context, environment, digest string) (string, error) - // ProjectExecutorCredentialState reads, in one snapshot, the connection - // facts of the tenant's self_hosted Environment and the metadata of the - // credentials restricted to it that the Project's principal holds, oldest - // first. An invalid principal is ErrInvalidInput; an Environment of a - // publicly deleted Session, another type or another tenant is ErrNotFound. - // The snapshot ends before any live observation of the connection. - ProjectExecutorCredentialState(ctx context.Context, project identity.Principal, environment string) (ExecutorCredentialState, error) -} - type IssuedExecutorCredential struct { KeyID string `json:"key_id" binding:"required"` EnvironmentID string `json:"environment_id,omitempty"` diff --git a/services/core/internal/sessions/items.go b/services/core/internal/sessions/items.go index f6447c639..d24e15f44 100644 --- a/services/core/internal/sessions/items.go +++ b/services/core/internal/sessions/items.go @@ -1,8 +1,6 @@ package sessions import ( - "context" - v1 "github.com/MiniMax-AI/OpenAgentCore/contracts/agents-api/v1" ) @@ -11,12 +9,3 @@ type ItemPage struct { Items []v1.Item HasMore bool } - -// ItemReader reads a Session's root Items. -type ItemReader interface { - // ListItems returns one page of the visible Session's root Items in - // first-observation order, ascending or descending. A limit outside 1..100 - // is ErrInvalidInput. A cursor that is not an Item of the Session, - // including a malformed one, is ErrItemCursor. - ListItems(ctx context.Context, tenantID, sessionID, cursor string, limit int, ascending bool) (ItemPage, error) -} diff --git a/services/core/internal/sessions/reader.go b/services/core/internal/sessions/reader.go index 59e24ba32..ccfed52b4 100644 --- a/services/core/internal/sessions/reader.go +++ b/services/core/internal/sessions/reader.go @@ -1,18 +1,19 @@ package sessions -// Reader reads Sessions and their resources, Environment and devices, and the -// administrator's cross-Project Session views, one family per line. Callers -// use it directly; no use case forwards a read. +import "context" + +// Reader reads Sessions and their resources and Environment, and devices. +// Callers use it directly; no use case forwards a read. type Reader interface { - AdminReader - ArtifactReader + // AuthenticateEnvironmentExecutor checks, in one database snapshot, that + // the credential whose SHA-256 digest is given is current for the + // Environment and belongs to its Session's creator, and returns the + // Session's tenant. Any other credential is ErrNotFound. + AuthenticateEnvironmentExecutor(ctx context.Context, environment, digest string) (string, error) DeviceReader EnvironmentReader - ExecutorCredentialReader InputReader - ItemReader ModelExecutionReader SessionReader - SubagentReader TurnReader } diff --git a/services/core/internal/sessions/reads.go b/services/core/internal/sessions/reads.go index 016220a83..3db0dccff 100644 --- a/services/core/internal/sessions/reads.go +++ b/services/core/internal/sessions/reads.go @@ -22,25 +22,6 @@ type SessionReader interface { // GetSession returns the tenant's visible Session with its Environment, // input activity and latest Turn, read from one snapshot, or ErrNotFound. GetSession(ctx context.Context, tenantID, sessionID string) (Session, error) - // ListSessions returns one page of the tenant's visible Sessions in - // creation-time and ID order; a non-nil agentID keeps that root Agent's - // Sessions. The cursor is the last returned Session ID and must belong to - // the tenant; it grants no additional access. A limit outside 1..100 is - // ErrInvalidInput. - ListSessions(ctx context.Context, tenantID, cursor string, limit int, ascending bool, agentID *string) (Page, error) - // SessionStreamSnapshot reads the projection GetSession returns and the - // committed Session event cursor from the same snapshot. - SessionStreamSnapshot(ctx context.Context, tenantID, sessionID string) (Session, int64, error) - // SessionEventCursor returns the sequence of the visible Session's latest - // public change, or ErrNotFound. - SessionEventCursor(ctx context.Context, tenantID, sessionID string) (int64, error) - // ListSessionEvents returns the retained public changes after the - // sequence, in order. A negative after is ErrInvalidInput; a pruned change - // in between is ErrStreamGap. - ListSessionEvents(ctx context.Context, tenantID, sessionID string, after int64) ([]SessionChange, error) - // GetTurnDiagnosticsSnapshot reads a root Turn and the timing of up to - // 1000 of its root Items with the Session in one read-only snapshot. - GetTurnDiagnosticsSnapshot(ctx context.Context, tenantID, sessionID, turnID string) (TurnDiagnosticsSnapshot, error) // GetSessionExecutionConfiguration reads only the safe committed // configuration. It never loads provider ciphertext, current defaults or // runtime health. @@ -51,10 +32,6 @@ type SessionReader interface { // keeps the official rule. A missing Session reads as null, so callers // resolve the Session first. MeasuredSessionUsage(ctx context.Context, tenantID, sessionID string) (json.RawMessage, error) - // GetManagedSessionArchive reads the resource disposal of the tenant's - // hosted Session and never contacts compute. A missing Session is - // ErrNotFound; one that is not hosted is ErrInvalidInput. - GetManagedSessionArchive(ctx context.Context, tenantID, sessionID string) (ManagedArchive, error) } // ItemDiagnosticTiming records Core database receipt and settlement, never native diff --git a/services/core/internal/sessions/subagents.go b/services/core/internal/sessions/subagents.go index 458263d9e..11a4fb492 100644 --- a/services/core/internal/sessions/subagents.go +++ b/services/core/internal/sessions/subagents.go @@ -11,31 +11,6 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/items" ) -// SubagentReader reads the Subagents a visible Session observed, with their -// Turns and Items. A Subagent, Turn or Item outside the named Session or -// Subagent is ErrNotFound. A list limit outside 1..100 is ErrInvalidInput. A -// Subagent or Subagent Turn cursor outside its list, including a malformed -// one, is ErrResourceCursor; an Item cursor is ErrItemCursor. -type SubagentReader interface { - // GetSubagent returns one Subagent of the Session, including a nested or - // closed one. - GetSubagent(ctx context.Context, tenantID, sessionID, subagentID string) (v1.Subagent, error) - // ListSubagents returns one page of the Session's Subagents in opening - // order. - ListSubagents(ctx context.Context, tenantID, sessionID, cursor string, limit int, ascending bool) (v1.SubagentList, error) - // ListSubagentItems returns one page of the Subagent's Items. - ListSubagentItems(ctx context.Context, tenantID, sessionID, subagentID, cursor string, limit int, ascending bool) (v1.ItemList, error) - // GetSubagentTurn returns one of the Subagent's Turns. Its agent_id is the - // Session's Agent ID and its subagent_id names the child. - GetSubagentTurn(ctx context.Context, tenantID, sessionID, subagentID, turnID string) (v1.Turn, error) - // ListSubagentTurns returns one page of the Subagent's Turns in creation - // order. - ListSubagentTurns(ctx context.Context, tenantID, sessionID, subagentID, cursor string, limit int, ascending bool) (v1.TurnList, error) - // ListSubagentTurnItems returns one page of the Items of one of the - // Subagent's Turns. An empty Turn ID is ErrInvalidInput. - ListSubagentTurnItems(ctx context.Context, tenantID, sessionID, subagentID, turnID, cursor string, limit int, ascending bool) (v1.ItemList, error) -} - // SubagentIdentity binds a native Subagent to the Session at the journal entry // that first observed it. It is an execution binding, not a public Subagent. type SubagentIdentity struct { diff --git a/services/core/internal/sessions/turn.go b/services/core/internal/sessions/turn.go index 9e902048a..9f62421af 100644 --- a/services/core/internal/sessions/turn.go +++ b/services/core/internal/sessions/turn.go @@ -67,13 +67,6 @@ type TurnPage struct { NextCursor string } -type TurnEvent struct { - Ordinal int32 - Kind string - Payload json.RawMessage - CreatedAt time.Time -} - type ExecutionEvent struct { Kind string `json:"kind"` Payload json.RawMessage `json:"payload"` @@ -102,11 +95,6 @@ type TurnReader interface { // is ErrInvalidInput; a malformed Session or Turn ID, a Subagent Turn ID // and a missing Turn are ErrNotFound. GetTurn(ctx context.Context, tenant, session, turn string) (Turn, error) - // ListTurns pages the root Turns of the tenant's Session that was not - // publicly deleted by creation time and ID, after the Turn cursor names. - // A page size outside 1..100 is ErrInvalidInput; a missing Session or - // cursor Turn is ErrNotFound. - ListTurns(ctx context.Context, tenant, session, cursor string, limit int, ascending bool) (TurnPage, error) // ListExecutionWork lists, in ID order after the Turn after names, up to // 100 root Turns in statuses across tenants. With connectedDevices not nil, it // lists only the Turns of tenants that own one of those devices, unrevoked. diff --git a/services/core/internal/skills/storage.go b/services/core/internal/skills/storage.go index 065fb0a7c..a4447e92e 100644 --- a/services/core/internal/skills/storage.go +++ b/services/core/internal/skills/storage.go @@ -63,7 +63,6 @@ type Reader interface { // Skills lists one page of the tenant's Skills in creation order, then by // ID; a nil After starts at the first Skill. Skills(ctx context.Context, tenantID string, page SkillPageQuery) (Page, error) - Version(ctx context.Context, tenantID string, skillID uuid.UUID, version int64) (Version, error) // VersionByID finds a version anywhere in the tenant, so a list cursor can // tell another Skill's version from a missing one. VersionByID(ctx context.Context, tenantID string, id uuid.UUID) (Version, error) diff --git a/services/core/internal/vaults/storage.go b/services/core/internal/vaults/storage.go index af97af86c..eae8030d8 100644 --- a/services/core/internal/vaults/storage.go +++ b/services/core/internal/vaults/storage.go @@ -7,7 +7,6 @@ import "context" // of ListCredentials, which is ErrNotFound; a missing or foreign resource is // ErrNotFound. type Reader interface { - GetVault(ctx context.Context, tenantID, vaultID string) (Vault, error) ListVaults(ctx context.Context, tenantID string, query PageQuery) (VaultPage, error) GetCredential(ctx context.Context, tenantID, vaultID, credentialID string) (Credential, error) // ListCredentials resolves the parent Vault first: an inaccessible parent diff --git a/services/core/internal/writeaudit/reader.go b/services/core/internal/writeaudit/reader.go index 0b8629e8b..94f82be14 100644 --- a/services/core/internal/writeaudit/reader.go +++ b/services/core/internal/writeaudit/reader.go @@ -1,7 +1,6 @@ package writeaudit import ( - "context" "errors" "time" ) @@ -10,13 +9,6 @@ import ( // page size or cursor. var ErrInvalidQuery = errors.New("invalid write audit query") -// Reader reads write provenance as safe read models, never request bodies or -// credentials. -type Reader interface { - GetResourceOwners(ctx context.Context, tenantID, resourceType string, resourceIDs []string) ([]ResourceOwner, error) - ListWriteOperations(ctx context.Context, tenantID string, filter Filter) (Page, error) -} - // APIKey is the recorded identity of the key that made a write, with its // current revocation time. type APIKey struct { diff --git a/services/core/migrations/000004_remove_unused_schema.sql b/services/core/migrations/000004_remove_unused_schema.sql new file mode 100644 index 000000000..98c3ec3c4 --- /dev/null +++ b/services/core/migrations/000004_remove_unused_schema.sql @@ -0,0 +1,4 @@ +-- +goose Up +DROP VIEW public_execution_turns; +ALTER TABLE deployment_model_providers DROP CONSTRAINT deployment_provider_error_code; +ALTER TABLE deployment_model_providers ADD CONSTRAINT deployment_provider_error_code CHECK ((last_error_code = ANY (ARRAY['authentication_error'::text, 'connection_failed'::text, 'rate_limit_exceeded'::text, 'usage_limit_exceeded'::text, 'server_overloaded'::text, 'server_error'::text, 'resource_not_found'::text, 'invalid_request'::text]))); diff --git a/services/core/tests/integration/devices_test.go b/services/core/tests/integration/devices_test.go index 385c4571f..c4f7b36f7 100644 --- a/services/core/tests/integration/devices_test.go +++ b/services/core/tests/integration/devices_test.go @@ -143,7 +143,7 @@ func TestStandaloneGatewayUsesExecutionCredentials(t *testing.T) { err = json.NewDecoder(response.Body).Decode(&data) response.Body.Close() if token == secret { - if err != nil || response.StatusCode != http.StatusOK || data["workspace_id"] != "" || data["ws_url"] != wsURL { + if err != nil || response.StatusCode != http.StatusOK || data["ws_url"] != wsURL { t.Fatalf("standalone bootstrap: %d %v %v", response.StatusCode, data, err) } } else if response.StatusCode != http.StatusUnauthorized { diff --git a/services/core/tests/integration/fixtures_test.go b/services/core/tests/integration/fixtures_test.go index c3bb5605b..692bb7413 100644 --- a/services/core/tests/integration/fixtures_test.go +++ b/services/core/tests/integration/fixtures_test.go @@ -94,8 +94,8 @@ func FixtureFileWrite(ctx context.Context, pool *pgxpool.Pool, tenantID, environ } write := row.EnvironmentFileWrite return sessions.EnvironmentFileWrite{ - Identity: sessions.FileWriteIdentity{ID: uuid.UUID(write.ID.Bytes).String(), DeviceID: uuid.UUID(write.DeviceID.Bytes).String(), RequestSHA256: write.RequestSha256}, - EnvironmentID: uuid.UUID(write.EnvironmentID.Bytes).String(), SessionID: uuid.UUID(row.SessionID.Bytes).String(), State: write.State, CreatedAt: write.CreatedAt.Time, + Identity: sessions.FileWriteIdentity{ID: uuid.UUID(write.ID.Bytes).String(), DeviceID: uuid.UUID(write.DeviceID.Bytes).String(), RequestSHA256: write.RequestSha256}, + State: write.State, CreatedAt: write.CreatedAt.Time, }, nil } diff --git a/services/core/tests/integration/projects_public_fixture_test.go b/services/core/tests/integration/projects_public_fixture_test.go index 50f1265e0..bbf8f33a6 100644 --- a/services/core/tests/integration/projects_public_fixture_test.go +++ b/services/core/tests/integration/projects_public_fixture_test.go @@ -24,7 +24,7 @@ func fixtureProjects(t testing.TB, s *Store) (*projectpg.Store, *projects.Servic // fixtureProjectsReader reads Projects from the database and resolves Project // keys from the test's fixture keys. type fixtureProjectsReader struct { - projects.Reader + *projectpg.Store keys fixtureKeyResolver } diff --git a/services/core/tests/integration/public_handler_fixture_test.go b/services/core/tests/integration/public_handler_fixture_test.go index f7d8c0a6d..e815d9ff6 100644 --- a/services/core/tests/integration/public_handler_fixture_test.go +++ b/services/core/tests/integration/public_handler_fixture_test.go @@ -80,7 +80,7 @@ func publicHandler(t testing.TB, s *Store, keys fixtureKeyResolver, engine strin deployments := deploymentService(t, s) deps := api.Dependencies{ Engine: engine, CoreKeys: admin, InstallationBindings: deployments, - Projects: projectService, ProjectsReader: fixtureProjectsReader{Reader: projectStore, keys: keys}, + Projects: projectService, ProjectsReader: fixtureProjectsReader{Store: projectStore, keys: keys}, ModelProviders: modelConfigurationService, ModelProvidersReader: modelConfigurationStore, Vaults: vaultService, VaultsReader: vaultStore, Files: fileService, FilesReader: fileStore, @@ -131,7 +131,7 @@ func fixtureFiles(t testing.TB, s *Store) (*filepg.Store, *files.Service) { // storeKeys resolves Project keys from the database, as production does. func storeKeys(*Store) func(*api.Dependencies) { - return func(d *api.Dependencies) { d.ProjectsReader = d.ProjectsReader.(fixtureProjectsReader).Reader } + return func(d *api.Dependencies) { d.ProjectsReader = d.ProjectsReader.(fixtureProjectsReader).Store } } // withCoreKeys replaces the Core key. diff --git a/services/core/tests/integration/turn_journal_reads_test.go b/services/core/tests/integration/turn_journal_reads_test.go index 1bb705a24..4d34c39ff 100644 --- a/services/core/tests/integration/turn_journal_reads_test.go +++ b/services/core/tests/integration/turn_journal_reads_test.go @@ -2,6 +2,7 @@ package integration import ( "context" + "encoding/json" "errors" "time" @@ -13,9 +14,16 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/sessions" ) +type turnEvent struct { + Ordinal int32 + Kind string + Payload json.RawMessage + CreatedAt time.Time +} + // ListTurnEvents reads a Turn's journal after the given ordinal. Only tests // read the journal back. -func (s *Store) ListTurnEvents(ctx context.Context, tenantID, sessionID, turnID string, after int32, limit int) ([]sessions.TurnEvent, error) { +func (s *Store) ListTurnEvents(ctx context.Context, tenantID, sessionID, turnID string, after int32, limit int) ([]turnEvent, error) { p, err := sessionpg.TurnLookup(tenantID, sessionID, turnID) if err != nil { return nil, err @@ -30,9 +38,9 @@ func (s *Store) ListTurnEvents(ctx context.Context, tenantID, sessionID, turnID if err != nil { return nil, err } - events := make([]sessions.TurnEvent, 0, len(rows)) + events := make([]turnEvent, 0, len(rows)) for _, row := range rows { - events = append(events, sessions.TurnEvent{Ordinal: row.Ordinal, Kind: row.Kind, Payload: row.Payload, CreatedAt: row.CreatedAt.Time}) + events = append(events, turnEvent{Ordinal: row.Ordinal, Kind: row.Kind, Payload: row.Payload, CreatedAt: row.CreatedAt.Time}) } return events, nil } From 5d73560c0225186702b0d327c7914ce79e8514c3 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Fri, 23 Oct 2026 18:52:41 +0000 Subject: [PATCH 3/3] Keep the microsandbox labels the helper sets --- services/core/internal/sandbox/microsandbox/identity.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/core/internal/sandbox/microsandbox/identity.go b/services/core/internal/sandbox/microsandbox/identity.go index 82c3b01cc..ddcc8d185 100644 --- a/services/core/internal/sandbox/microsandbox/identity.go +++ b/services/core/internal/sandbox/microsandbox/identity.go @@ -53,6 +53,9 @@ func Name(c Config, r sandbox.Reference, g uint64) string { func SnapshotReference(c Config, r sandbox.Reference, operation string) string { return Name(c, r, 0) + ":s-" + operation } +func Labels(c Config, r sandbox.Reference) map[string]string { + return map[string]string{"io.oac.installation": c.InstallationID, "io.oac.tenant": r.TenantID, "io.oac.environment": r.EnvironmentID, "io.oac.allocation": r.AllocationID} +} func ValidateCompute(c Config, r sandbox.Reference, v Compute) error { if !ValidReference(r) || v.Name != Name(c, r, v.Generation) { return sandbox.ErrInvalid