diff --git a/apps/web/src/features/metrics/CoreMetricsPage.tsx b/apps/web/src/features/metrics/CoreMetricsPage.tsx index 2fc0a9297..b0232180e 100644 --- a/apps/web/src/features/metrics/CoreMetricsPage.tsx +++ b/apps/web/src/features/metrics/CoreMetricsPage.tsx @@ -1,6 +1,5 @@ -import { AgentCoreError, type CoreJobStatus, type CoreMetrics, type CoreMetricsRange } from "@oac/agents-client"; +import { type CoreJobStatus, type CoreMetrics, type CoreMetricsRange } from "@oac/agents-client"; import { keepPreviousData, useQuery } from "@tanstack/react-query"; -import { Network } from "lucide-react"; import { useMemo, useState, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; @@ -55,17 +54,14 @@ export function CoreMetricsPage() { const [range, setRange] = useState("1h"); const query = useQuery({ ...coreMetricsQuery(range), placeholderData: keepPreviousData, refetchInterval: REFRESH_MS, refetchIntervalInBackground: false }); const metrics = query.data ?? null; - const missing = query.error instanceof AgentCoreError && (query.error.status === 404 || query.error.status === 501); const error = query.isError ? coreError(query.error, tCommon) : ""; useFailureToast(metrics !== null && query.isError, t("core.stale", { reason: error }), "core-metrics-refresh"); let body: ReactNode; if (!metrics) { - body = missing - ? - : query.isError - ?

{t("core.failed", { reason: error })}

- : ; + body = query.isError + ?

{t("core.failed", { reason: error })}

+ : ; } else { body = ; } diff --git a/apps/web/src/features/skills/skill-operations.ts b/apps/web/src/features/skills/skill-operations.ts index 52fd7a2c1..3df3f118b 100644 --- a/apps/web/src/features/skills/skill-operations.ts +++ b/apps/web/src/features/skills/skill-operations.ts @@ -3,27 +3,12 @@ import { AgentCoreError, type Skill, type SkillVersion } from "@oac/agents-clien import type { ProjectClient } from "../../lib/projects"; import { appendCollectionPage } from "../../lib/collection-pagination"; -/** - * Skill navigation support, from the first Skill list request: - * - `supported`: the list succeeded; - * - `unsupported`: 404 or 405, an older Core without Skills (hide the entry); - * - `error`: anything else (show the entry with a retry). - */ -export type SkillsSupport = "supported" | "unsupported" | "error"; - export const SKILLS_PAGE_SIZE = 20; export function isAbortError(error: unknown): boolean { return (error instanceof DOMException || error instanceof Error) && error.name === "AbortError"; } -export function classifySkillsError(error: unknown): Exclude { - if (error instanceof AgentCoreError) { - if (error.status === 404 || error.status === 405) return "unsupported"; - } - return "error"; -} - /** Core rejects deleting the default while other versions remain (400 invalid_value on `version`). */ export function isDefaultVersionConflict(error: unknown): boolean { return error instanceof AgentCoreError && error.status === 400 && error.code === "invalid_value" && error.param === "version"; diff --git a/apps/web/src/i18n/locales/en/metrics.ts b/apps/web/src/i18n/locales/en/metrics.ts index eee22ec69..5abbf61aa 100644 --- a/apps/web/src/i18n/locales/en/metrics.ts +++ b/apps/web/src/i18n/locales/en/metrics.ts @@ -110,8 +110,6 @@ export const metrics = { loading: "Reading Core metrics…", failed: "Core metrics could not be read. {{reason}}", stale: "Refresh failed; showing the last metrics read. {{reason}}", - missingTitle: "Core does not report its own metrics yet", - missingDescription: "Core needs to provide /core/v1/metrics before execution, database and background jobs can show.", kpiLabel: "Core summary", meta: "{{status}} · {{revision}} · up {{uptime}}", notOwner: "Does not hold the execution lease", diff --git a/apps/web/src/i18n/locales/en/sessions.ts b/apps/web/src/i18n/locales/en/sessions.ts index 00285837d..3384645fe 100644 --- a/apps/web/src/i18n/locales/en/sessions.ts +++ b/apps/web/src/i18n/locales/en/sessions.ts @@ -1,11 +1,6 @@ import { coreHarnessNames } from "@oac/agents-client"; -/** - * Session log and one Session's read-only history (Monitor › Session log). - * `validation` and `actions.errors` serve the retired Session-creation and - * metadata helpers that the orphaned API workbench still imports; remove them - * together with that code. - */ +/** Session log and one Session's read-only history (Monitor › Session log). */ export const sessions = { common: { agent: "Agent", session: "Session", status: "Status", unknown: "Unknown", unavailable: "Unavailable", untitledAgent: "Unnamed Agent", @@ -272,25 +267,4 @@ export const sessions = { toolDurationSentence: "Tool-reported duration: {{duration}}.", toolDuration: "Tool-reported duration: {{duration}}", durationUnknown: "Reported duration is unknown.", durationInvalid: "The reported duration could not be interpreted.", perItemNotProvided: "Per-item timing not provided by Core.", perItemTimingMissing: "Core does not provide per-item timing.", turnDuration: "Turn {{duration}}", turnTimeUnknown: "Turn time unknown", turnTimeNotProvided: "Turn time not provided", payloadAvailable: "Payload available", resultAvailable: "Result available", noPreview: "No preview available", configuredInstructions: "Configured instructions", userMessage: "User message", assistantMessage: "Assistant message", unsupportedItem: "Unsupported Item", agentConfiguration: "Agent configuration", turnNumber: "Turn {{number}}", unassociatedItemsNumber: "Unassociated Items {{number}}", unknownCount: "{{count}} unknown", }, - actions: { - errors: { - metadataConflict: "Metadata changed in OpenAgentCore while you were editing: {{keys}}. Your draft was kept; review the latest values before saving again.", - metadataValidJson: "Metadata must be valid JSON.", metadataObject: "Metadata must be a JSON object.", metadataStringValues: "Every metadata value must be a string.", - metadataTitleField: "Edit title in the Title field, not in additional metadata.", metadataMaxPairs: "Session metadata supports at most 16 pairs, including title.", metadataLength: "Metadata keys must be at most 64 characters and values at most 512 characters.", - invalidRetrieval: "OpenAgentCore returned an invalid Session retrieval response. The Web kept its current durable view and did not send a write.", - updateLabel: "update", deletionLabel: "deletion", notFound: "This Session was not found in OpenAgentCore. The Web kept its current durable view; refresh Sessions before trying again.", - busyDelete: "OpenAgentCore deletes a Session only when it is idle or failed without required actions. This Session still has queued, running or waiting work or pending input, so nothing was changed. Choose Cancel work and delete to cancel it, wait until it is idle and then delete it.", - readLifecycleConflict: "The latest Session could not be retrieved because this compatible Core returned a lifecycle conflict (409), so no update request was sent. Your draft and current durable view were kept.", - writeLifecycleConflict: "This compatible Core rejected the Session {{action}} because it conflicts with the current lifecycle state. The Web did not retry.", - noUpdateSent: "No update request was sent.", unknownWriteSuffix: "The write result is unknown. The Web kept its current durable view and did not retry; refresh before deciding whether to try again.", - coreUnavailable: "OpenAgentCore could not complete the Session {{action}} ({{status}}). {{suffix}}", uncertainStatus: "The Session {{action}} result is uncertain after OpenAgentCore returned {{status}}. The Web kept its current durable view and did not retry; refresh before deciding whether to try again.", - readFailureWithDetail: "{{detail}} No update request was sent; your draft and current durable view were kept.", writeFailureWithDetail: "{{detail}} The Web kept its current durable view and did not retry.", - latestReadFailure: "The latest Session could not be retrieved, so no update request was sent. Your draft and the current durable view were kept.", unknownConnection: "The Session {{action}} result is unknown because the connection ended before Core confirmed it. The Web kept its current durable view and did not retry; refresh before deciding whether to try again.", - invalidUpdateConfirmation: "OpenAgentCore returned an invalid Session update confirmation. The write result is unknown; the Web kept its current durable view and did not retry.", - inputPending: "OpenAgentCore deletes a Session only when it is idle or failed without required actions. This Session has input waiting to start in its Environment, which cannot be cancelled, so nothing was changed. Delete it after that input starts, expires or fails; once it starts, cancel its work first.", - invalidDeletionConfirmation: "OpenAgentCore returned an invalid deletion confirmation. The Web kept the Session; refresh durable state before taking another action.", - cancelRejected: "OpenAgentCore rejected the cancellation ({{status}}): {{detail}}", cancelUnknown: "The cancellation result is unknown because the connection ended before Core confirmed it.", cancelDeleteKept: "{{detail}} The Session was not deleted and the Web did not retry.", - waitReadFailure: "Cancellation was requested, but the Session could not be read while waiting for it to become idle. It was not deleted.", waitTimeout: "Cancellation was requested, but the Session was still {{status}} after {{seconds}} seconds. It was not deleted; try again once it is idle.", - }, - }, } as const; diff --git a/apps/web/src/i18n/locales/zh-CN/metrics.ts b/apps/web/src/i18n/locales/zh-CN/metrics.ts index 01e03a2da..6c37faea6 100644 --- a/apps/web/src/i18n/locales/zh-CN/metrics.ts +++ b/apps/web/src/i18n/locales/zh-CN/metrics.ts @@ -110,8 +110,6 @@ export const metrics = { loading: "正在读取 Core 指标…", failed: "无法读取 Core 指标。{{reason}}", stale: "刷新失败,显示的是上次读取的指标。{{reason}}", - missingTitle: "Core 尚未上报自身指标", - missingDescription: "需要 Core 提供 /core/v1/metrics,才能显示执行、数据库和后台任务。", kpiLabel: "Core 概况", meta: "{{status}} · {{revision}} · 已运行 {{uptime}}", notOwner: "未持有执行归属", diff --git a/apps/web/src/i18n/locales/zh-CN/sessions.ts b/apps/web/src/i18n/locales/zh-CN/sessions.ts index d4135395a..cdec25270 100644 --- a/apps/web/src/i18n/locales/zh-CN/sessions.ts +++ b/apps/web/src/i18n/locales/zh-CN/sessions.ts @@ -272,27 +272,4 @@ export const sessions = { toolDurationSentence: "工具报告耗时:{{duration}}。", toolDuration: "工具报告耗时:{{duration}}", durationUnknown: "报告的耗时未知。", durationInvalid: "无法解析报告的耗时。", perItemNotProvided: "Core 未提供逐 Item 时间。", perItemTimingMissing: "Core 不提供逐 Item 时间。", turnDuration: "Turn {{duration}}", turnTimeUnknown: "Turn 耗时未知", turnTimeNotProvided: "未提供 Turn 耗时", payloadAvailable: "载荷可用", resultAvailable: "结果可用", noPreview: "无可用预览", configuredInstructions: "已配置指令", userMessage: "用户消息", assistantMessage: "助手消息", unsupportedItem: "不支持的 Item", agentConfiguration: "Agent 配置", turnNumber: "Turn {{number}}", unassociatedItemsNumber: "未关联 Item {{number}}", unknownCount: "{{count}} 个未知", }, - actions: { - ...english.actions, - errors: { - ...english.actions.errors, - metadataConflict: "你编辑期间 OpenAgentCore 中的元数据已发生变化:{{keys}}。草稿已保留;请检查最新值后再保存。", - metadataValidJson: "元数据必须是有效的 JSON。", metadataObject: "元数据必须是 JSON 对象。", metadataStringValues: "每个元数据值都必须是字符串。", - metadataTitleField: "请在“标题”字段中编辑标题,不要放在其他元数据中。", metadataMaxPairs: "Session 元数据最多支持 16 个键值对(包括标题)。", metadataLength: "元数据键最多 64 个字符,值最多 512 个字符。", - invalidRetrieval: "OpenAgentCore 返回了无效的 Session 读取响应。Web 已保留当前持久化视图,且未发送写入请求。", - updateLabel: "更新", deletionLabel: "删除", notFound: "OpenAgentCore 中未找到此 Session。Web 已保留当前持久化视图;请刷新 Session 后重试。", - busyDelete: "OpenAgentCore 仅允许删除处于空闲或失败状态且没有必需操作的 Session。此 Session 仍有排队、运行、等待中的工作或待处理输入,因此未做任何更改。请选择“取消工作并删除”,等待 Session 空闲后再删除。", - readLifecycleConflict: "读取最新 Session 时,兼容的 Core 返回了生命周期冲突(409),因此未发送更新请求。草稿和当前持久化视图均已保留。", - writeLifecycleConflict: "兼容的 Core 拒绝了 Session{{action}},因为它与当前生命周期状态冲突。Web 未自动重试。", - noUpdateSent: "未发送更新请求。", unknownWriteSuffix: "写入结果未知。Web 已保留当前持久化视图且未自动重试;请刷新状态后再决定是否重试。", - coreUnavailable: "OpenAgentCore 无法完成 Session{{action}}({{status}})。{{suffix}}", uncertainStatus: "OpenAgentCore 返回 {{status}} 后,Session{{action}}结果不确定。Web 已保留当前持久化视图且未自动重试;请刷新状态后再决定是否重试。", - readFailureWithDetail: "{{detail}} 未发送更新请求;草稿和当前持久化视图均已保留。", writeFailureWithDetail: "{{detail}} Web 已保留当前持久化视图且未自动重试。", - latestReadFailure: "无法读取最新 Session,因此未发送更新请求。草稿和当前持久化视图均已保留。", unknownConnection: "连接在 Core 确认前中断,因此 Session{{action}}结果未知。Web 已保留当前持久化视图且未自动重试;请刷新状态后再决定是否重试。", - invalidUpdateConfirmation: "OpenAgentCore 返回了无效的 Session 更新确认。写入结果未知;Web 已保留当前持久化视图且未自动重试。", - inputPending: "OpenAgentCore 仅允许删除处于空闲或失败状态且没有必需操作的 Session。此 Session 有等待在 Environment 中启动的输入,无法取消,因此未做任何更改。请等待输入启动、过期或失败后删除;输入启动后,请先取消其工作。", - invalidDeletionConfirmation: "OpenAgentCore 返回了无效的删除确认。Web 已保留该 Session;请刷新持久化状态后再操作。", - cancelRejected: "OpenAgentCore 拒绝取消请求({{status}}):{{detail}}", cancelUnknown: "连接在 Core 确认前中断,因此取消结果未知。", cancelDeleteKept: "{{detail}} Session 未删除,Web 也未自动重试。", - waitReadFailure: "已请求取消,但等待 Session 进入空闲状态期间无法读取该 Session,因此未删除。", waitTimeout: "已请求取消,但 {{seconds}} 秒后 Session 仍处于{{status}}状态,因此未删除;请在 Session 空闲后重试。", - }, - }, } as const; diff --git a/apps/web/src/style.css b/apps/web/src/style.css index e74f8903b..00c0f0e3c 100644 --- a/apps/web/src/style.css +++ b/apps/web/src/style.css @@ -567,7 +567,7 @@ html[data-theme="dark"] .brand-mark { color: var(--fg-muted); } -/* Shared ledger primitives and legacy Agent detail styles. */ +/* Shared ledger primitives. */ .ledger { min-height: 0; @@ -712,13 +712,6 @@ html[data-theme="dark"] .brand-mark { /* Sessions — conversation list and 48rem thread. */ -.status-label { - color: var(--fg-muted); - font-size: 12px; - line-height: 16px; - text-transform: capitalize; -} - .trace-workbench { display: flex; min-width: 0; @@ -1288,50 +1281,6 @@ html[data-theme="dark"] .brand-mark { padding-left: 0; } -.thread-content { - display: flex; - width: 100%; - max-width: 768px; - min-height: 100%; - flex-direction: column; - margin: 0 auto; - padding: 24px 16px; -} - -.message-stack { - display: flex; - flex-direction: column; - gap: 20px; -} - -.message-row { - display: flex; - width: 100%; -} - -.message-row.user { - justify-content: flex-end; -} - -.message-body { - display: flex; - min-width: 0; - max-width: 85%; - flex-direction: column; - color: var(--fg); -} - -.message-row.user .message-body { - align-items: flex-end; -} - -.message-byline { - margin-bottom: 4px; - color: var(--fg-muted); - font-size: 12px; - line-height: 16px; -} - .message-copy { color: var(--fg); font-size: 14px; @@ -1645,12 +1594,6 @@ html[data-theme="dark"] .brand-mark { font-size: 13.5px; } -.message-row.user .message-copy { - padding: 8px 12px; - background: var(--surface-muted); - border-radius: 6px; -} - .work-trace { width: 100%; min-width: 0; diff --git a/apps/web/src/styles/app.css b/apps/web/src/styles/app.css index 12894e03f..b46cbfd2f 100644 --- a/apps/web/src/styles/app.css +++ b/apps/web/src/styles/app.css @@ -2,7 +2,7 @@ * The console's single stylesheet entry, in cascade order: * 1. Tailwind's theme variables (no preflight: the console keeps its base rules); * 2. IBM Plex Sans and Beautiful UI's foundation — the design tokens every surface uses; - * 3. the console's legacy base and grammar, whose variables alias those tokens; + * 3. the console's base and grammar, whose variables alias those tokens; * 4. system.css, the component system in Beautiful UI's vocabulary; * 5. Tailwind utilities last, generated only for the registry components. */ diff --git a/apps/web/src/styles/system.css b/apps/web/src/styles/system.css index 70790f706..095c43859 100644 --- a/apps/web/src/styles/system.css +++ b/apps/web/src/styles/system.css @@ -3,7 +3,7 @@ * (src/app/beautifui/foundation.css): cool canvas, white cards drawn by a * hairline ring and a smooth shadow, an ink ramp for text, pill buttons with an * ink primary, filled fields with a hairline ring, dark tooltips, and - * indigo as the only accent. Loaded after the legacy stylesheets, so every + * indigo as the only accent. Loaded after the base stylesheets, so every * shared atom is defined here once; feature stylesheets only arrange them. */ diff --git a/contracts/agents-api/machine-api.md b/contracts/agents-api/machine-api.md index 02499ae4d..75e932e77 100644 --- a/contracts/agents-api/machine-api.md +++ b/contracts/agents-api/machine-api.md @@ -17,12 +17,11 @@ Machines call Core under `/api/v1`: sandbox nodes, Runtime daemons and the self- | `POST agent-daemon/enroll` | Self-hosted daemon | Executor credential | [Enroll a self-hosted daemon](#enroll-a-self-hosted-daemon) | | `GET agent-daemon/connection?environment_id=` | Self-hosted installer | Executor credential | [Private connection confirmation](./environment-executor-credentials.md#private-connection-confirmation) | | `POST agent-daemon/bootstrap` | Runtime daemon | Daemon credential | [Daemon bootstrap](#daemon-bootstrap) | -| `GET agent-daemon/device-status?device_id=` | Runtime daemon | Daemon credential | [Device status](#device-status) | | WebSocket `GET agent-daemon/ws?device_id=&version=` | Runtime daemon | Daemon credential | [Core–Runtime protocol](../../docs/runtime-protocol.md) | Every credential travels in an `Authorization: Bearer` header, never in a URL. -The generated [`runtime.openapi.yaml`](./runtime.openapi.yaml) describes only the sandbox-node configuration, enroll and identity routes and the two installation routes. The two WebSockets and the daemon bootstrap, device-status, enroll and connection routes are served outside the API router and have no generated schema; this document and the linked contracts are their only definition. +The generated [`runtime.openapi.yaml`](./runtime.openapi.yaml) describes only the sandbox-node configuration, enroll and identity routes and the two installation routes. The two WebSockets and the daemon bootstrap, enroll and connection routes are served outside the API router and have no generated schema; this document and the linked contracts are their only definition. ## Credentials @@ -32,8 +31,8 @@ The generated [`runtime.openapi.yaml`](./runtime.openapi.yaml) describes only th | Node credential | The node itself: it generates a secret of 32 to 256 characters without whitespace and registers it at enrollment | `sandbox-node/configuration` with `X-OAC-Node-ID`, `sandbox-node/identity`, `sandbox-node/connect` | | Installation grant | The `x_agents_core.installation` command of a `self_hosted` Session; short-lived | `agent-daemon/installation` and its `claim` | | Executor credential | The installation claim, or the Core-key routes in [Executor credentials](./environment-executor-credentials.md) | `agent-daemon/enroll` and `agent-daemon/connection`; after enrollment it is also the daemon credential of the bound device | -| Daemon credential of a hosted sandbox | Core, for each managed allocation, delivered in the [bootstrap file](../../docs/runtime-bootstrap.md) | `agent-daemon/bootstrap`, `device-status` and `ws` | -| Operator device profile | `oac-core-device`, run by an operator with database access | `agent-daemon/bootstrap`, `device-status` and `ws` | +| Daemon credential of a hosted sandbox | Core, for each managed allocation, delivered in the [bootstrap file](../../docs/runtime-bootstrap.md) | `agent-daemon/bootstrap` and `ws` | +| Operator device profile | `oac-core-device`, run by an operator with database access | `agent-daemon/bootstrap` and `ws` | Core keeps only a SHA-256 digest of each token and credential it stores; installation grants are signed and not stored. Credentials are not interchangeable: each works only on its own routes. @@ -102,11 +101,7 @@ The credential is checked before any deployment state, so a rejected credential, `POST /api/v1/agent-daemon/bootstrap` with the daemon credential and `{"device_id": "…"}` returns `device_id`, `workspace_id`, `ws_url` (derived from `OAC_PUBLIC_URL`, never from request headers), `heartbeat_seconds` and `protocol_version`. The daemon then dials `ws_url` as the [Core–Runtime protocol](../../docs/runtime-protocol.md#ownership-and-connection) describes. -### Device status - -`GET /api/v1/agent-daemon/device-status?device_id=` with the daemon credential returns `device_id`, `online` and `owner`: the current connection owner's `owner_pod_id`, `owner_url`, `generation`, `status` and `lease_expires_at`, or null. - -The bootstrap, device-status and WebSocket routes share one error body, `{"error": code, "detail": text}`: 400 `missing_params`, `missing_device_id` or `bad_json`; 401 `missing_bearer`, `unknown_device` or `bad_credential`; 403 `wrong_runtime_type`; 500 `internal`; and on the WebSocket 426 `incompatible_version` when `version` is not Core's exact Runtime protocol version. +The bootstrap and WebSocket routes share one error body, `{"error": code, "detail": text}`: 400 `missing_params`, `missing_device_id` or `bad_json`; 401 `missing_bearer`, `unknown_device` or `bad_credential`; 403 `wrong_runtime_type`; 500 `internal`; and on the WebSocket 426 `incompatible_version` when `version` is not Core's exact Runtime protocol version. ### Enroll a self-hosted daemon diff --git a/contracts/agents-api/zh/machine-api.md b/contracts/agents-api/zh/machine-api.md index 347843f3c..d59363798 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: fab6f7d3aabb1b883bce7e67c0314e8dff760825693d97a45245ce54ad984959 +source_hash: 637717534ca8fd64eea6f6688461dd6b065b317ae271cd974880524d698c3ae9 --- 机器通过 `/api/v1` 调用 Core:包括沙箱节点、Runtime daemon 和自托管安装程序。各路由仅接受所列凭据,不接受 Core 密钥或项目 API 密钥;控制台登录也不授予此处权限。反向代理将 `/api/v1` 直接发送给 Core;Web 不提供这些路由。 @@ -19,12 +19,11 @@ source_hash: fab6f7d3aabb1b883bce7e67c0314e8dff760825693d97a45245ce54ad984959 | `POST agent-daemon/enroll` | 自托管 daemon | 执行器凭据 | [登记自托管 daemon](#enroll-a-self-hosted-daemon) | | `GET agent-daemon/connection?environment_id=` | 自托管安装程序 | 执行器凭据 | [私有连接确认](environment-executor-credentials.md#private-connection-confirmation) | | `POST agent-daemon/bootstrap` | Runtime daemon | daemon 凭据 | [daemon 引导](#daemon-bootstrap) | -| `GET agent-daemon/device-status?device_id=` | Runtime daemon | daemon 凭据 | [设备状态](#device-status) | | WebSocket `GET agent-daemon/ws?device_id=&version=` | Runtime daemon | daemon 凭据 | [Core–Runtime 协议](../../../docs/zh/runtime-protocol.md) | 所有凭据通过 `Authorization: Bearer` 头传输,不放入 URL。 -生成的 [`runtime.openapi.yaml`](../runtime.openapi.yaml) 仅描述 sandbox-node 配置、登记、身份路由和两个安装路由。两个 WebSocket 及 daemon 引导、设备状态、登记和连接路由在 API 路由器外提供,无生成 schema;本文及所链接契约是它们唯一的定义。 +生成的 [`runtime.openapi.yaml`](../runtime.openapi.yaml) 仅描述 sandbox-node 配置、登记、身份路由和两个安装路由。两个 WebSocket 及 daemon 引导、登记和连接路由在 API 路由器外提供,无生成 schema;本文及所链接契约是它们唯一的定义。 ## 凭据 {#credentials} @@ -34,8 +33,8 @@ source_hash: fab6f7d3aabb1b883bce7e67c0314e8dff760825693d97a45245ce54ad984959 | 节点凭据 | 节点自身:生成 32 至 256 个无空白字符的密钥,在登记时注册 | 带 `X-OAC-Node-ID` 的 `sandbox-node/configuration`、`sandbox-node/identity`、`sandbox-node/connect` | | 安装授权 | `self_hosted` Session 的 `x_agents_core.installation` 命令;短期有效 | `agent-daemon/installation` 及其 `claim` | | 执行器凭据 | 安装领取,或[执行器凭据](environment-executor-credentials.md)中使用 Core 密钥的接口 | `agent-daemon/enroll` 和 `agent-daemon/connection`;登记后也作为绑定设备的 daemon 凭据 | -| 托管沙箱 daemon 凭据 | Core 为每个受管分配签发,通过[引导文件](../../../docs/zh/runtime-bootstrap.md)交付 | `agent-daemon/bootstrap`、`device-status` 和 `ws` | -| 操作者设备配置 | 具有数据库访问权限的操作者运行 `oac-core-device` | `agent-daemon/bootstrap`、`device-status` 和 `ws` | +| 托管沙箱 daemon 凭据 | Core 为每个受管分配签发,通过[引导文件](../../../docs/zh/runtime-bootstrap.md)交付 | `agent-daemon/bootstrap` 和 `ws` | +| 操作者设备配置 | 具有数据库访问权限的操作者运行 `oac-core-device` | `agent-daemon/bootstrap` 和 `ws` | Core 对存储的每个 token 和凭据仅保留 SHA-256 摘要;安装授权经签名但不存储。凭据不可互换:各自仅适用于自身路由。 @@ -104,11 +103,7 @@ Core 在一个事务中检查 token 有效、部署已初始化且为节点型 `POST /api/v1/agent-daemon/bootstrap` 携带 daemon 凭据及 `{"device_id": "…"}`,返回 `device_id`、`workspace_id`、`ws_url`(从 `OAC_PUBLIC_URL` 推导,不使用请求头)、`heartbeat_seconds` 和 `protocol_version`。daemon 随后按 [Core–Runtime 协议](../../../docs/zh/runtime-protocol.md#ownership-and-connection)连接 `ws_url`。 -### 设备状态 {#device-status} - -`GET /api/v1/agent-daemon/device-status?device_id=` 携带 daemon 凭据,返回 `device_id`、`online` 和 `owner`:当前连接所有者的 `owner_pod_id`、`owner_url`、`generation`、`status` 和 `lease_expires_at`,或 null。 - -引导、设备状态和 WebSocket 路由共享错误体 `{"error": code, "detail": text}`:400 `missing_params`、`missing_device_id` 或 `bad_json`;401 `missing_bearer`、`unknown_device` 或 `bad_credential`;403 `wrong_runtime_type`;500 `internal`;WebSocket 的 `version` 不等于 Core 精确 Runtime 协议版本时返回 426 `incompatible_version`。 +引导和 WebSocket 路由共享错误体 `{"error": code, "detail": text}`:400 `missing_params`、`missing_device_id` 或 `bad_json`;401 `missing_bearer`、`unknown_device` 或 `bad_credential`;403 `wrong_runtime_type`;500 `internal`;WebSocket 的 `version` 不等于 Core 精确 Runtime 协议版本时返回 426 `incompatible_version`。 ### 登记自托管 daemon {#enroll-a-self-hosted-daemon} diff --git a/docs.json b/docs.json index 041a5df18..04e0453b1 100644 --- a/docs.json +++ b/docs.json @@ -107,23 +107,5 @@ ] } ] - }, - "redirects": [ - { - "source": "/docs/getting-started/README", - "destination": "/docs/getting-started/index" - }, - { - "source": "/docs/api/README", - "destination": "/docs/api/index" - }, - { - "source": "/docs/web/README", - "destination": "/docs/web/index" - }, - { - "source": "/contracts/agents-api/README", - "destination": "/contracts/agents-api/index" - } - ] + } } diff --git a/docs/runtime-protocol.md b/docs/runtime-protocol.md index 2ee42f6a0..88b713fdb 100644 --- a/docs/runtime-protocol.md +++ b/docs/runtime-protocol.md @@ -45,7 +45,7 @@ sequenceDiagram - The first heartbeat is sent at once, then at the interval that bootstrap returned. Each heartbeat declares `supported_agent_kinds`, their availability and their [capabilities](#capability-declarations). Before the first heartbeat, capabilities are unknown; a kind missing from a heartbeat is not advertised. Neither permits inference. - Heartbeats establish liveness only, never execution progress or a receipt for an earlier message. -Each physical connection has fresh routing, admission handles and transfer state. A newer connection for the same device replaces the previous one: Core fences the owner lease and evicts the old Run routes, and the new connection inherits none of them. A valid credential and connection are never authority to choose another Session or Environment binding. +Each physical connection has fresh routing, admission handles and transfer state. A newer connection for the same device replaces the previous one: Core evicts the old Run routes, and the new connection inherits none of them. A valid credential and connection are never authority to choose another Session or Environment binding. ## Versioning diff --git a/docs/zh/runtime-protocol.md b/docs/zh/runtime-protocol.md index 0540f9b40..3c570fe82 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: 012fe8bd1a7bf086bcb54d026ee0cf1997890b76fdb79fe3a476088c777a6de2 +source_hash: 8764ef0b2f84de45e65f77d66e2f4a76f4e6771543d0ec9b63b04b81a7ee17d4 --- Core–Runtime 协议连接 Core 与 Runtime daemon,定义 daemon 连接上消息的含义和顺序。托管和自托管 Runtime 使用同一协议。 @@ -47,7 +47,7 @@ sequenceDiagram - 立即发送第一个 heartbeat,之后按 bootstrap 返回的间隔发送。每个 heartbeat 声明 `supported_agent_kinds`、其可用性和[能力](#capability-declarations)。第一个 heartbeat 之前能力未知;heartbeat 中缺失的 kind 视为未声明。两者都不允许推断。 - Heartbeat 仅证明存活,不证明执行进度,也不充当此前消息的回执。 -每条物理连接拥有新的路由、admission handle 和传输状态。同一设备的新连接替代旧连接:Core 隔离 owner lease,移除旧 Run 路由,新连接不继承这些状态。有效凭据和连接不授权选择其他 Session 或 Environment 绑定。 +每条物理连接拥有新的路由、admission handle 和传输状态。同一设备的新连接替代旧连接:Core 移除旧 Run 路由,新连接不继承这些状态。有效凭据和连接不授权选择其他 Session 或 Environment 绑定。 ## 版本 {#versioning} diff --git a/example/parsar/README.md b/example/parsar/README.md index a1c0e9b3e..72eead840 100644 --- a/example/parsar/README.md +++ b/example/parsar/README.md @@ -32,7 +32,7 @@ The loopback Node server has two small responsibilities: a fixed allowlist proxy SQLite files live in `~/.oac/data/parsar-example/`. Set the absolute `OAC_EXAMPLE_DATA_DIR` to relocate them. A hash of the Core origin and Project key selects the file; changing either selects a different local catalog. Back up the SQLite file with the server stopped. The Core Project key is not stored in it. Provider keys are stored there. Builds and caches use `${OAC_DEV_HOME:-$HOME/.oac}`. -Use one server process and a dedicated Project for this local single-user example. The server rejects cross-origin writes and keeps the Project key out of browser responses. Session creation freezes a pending request in SQLite before calling Core, and uses the local Session ID as its idempotency key. An uncertain response can be recovered from the Agent's Session list, including after a server restart. Successful creation leaves only the Core Session reference and local title/Agent/runtime relationship; Core owns messages, turns and execution status. The browser consumes public SSE text deltas through a non-buffering proxy, reconnects after interruption, and reconciles with durable history to avoid duplicate final replies. Each send records request-return latency and first nonempty text-delta latency from browser request start. These independent observations remain in sessionStorage across reloads; missing observations are shown as a dash rather than inferred from history. Definite validation rejection removes the pending request so it can be corrected. Use one server process. Deleting a local resource is blocked while another local record references it; Session deletion/archival is outside this example. Skill deletion is a separate explicit Core operation. Earlier templates and instances become Agent configs on startup; they are never presented as actual Sessions. +Use one server process and a dedicated Project for this local single-user example. The server rejects cross-origin writes and keeps the Project key out of browser responses. Session creation freezes a pending request in SQLite before calling Core, and uses the local Session ID as its idempotency key. An uncertain response can be recovered from the Agent's Session list, including after a server restart. Successful creation leaves only the Core Session reference and local title/Agent/runtime relationship; Core owns messages, turns and execution status. The browser consumes public SSE text deltas through a non-buffering proxy, reconnects after interruption, and reconciles with durable history to avoid duplicate final replies. Each send records request-return latency and first nonempty text-delta latency from browser request start. These independent observations remain in sessionStorage across reloads; missing observations are shown as a dash rather than inferred from history. Definite validation rejection removes the pending request so it can be corrected. Use one server process. Deleting a local resource is blocked while another local record references it; Session deletion/archival is outside this example. Skill deletion is a separate explicit Core operation. A few more implementation details: diff --git a/example/parsar/server/product.mjs b/example/parsar/server/product.mjs index 1b3df5f29..f3a9a6d3c 100644 --- a/example/parsar/server/product.mjs +++ b/example/parsar/server/product.mjs @@ -12,34 +12,6 @@ const kinds = new Set([ "sessions", ]); export function productAPI(store, core, fetchImpl = fetch) { - // Preserve earlier example configurations without fabricating execution records. - for (const kind of ["templates", "instances"]) { - for (const row of store.list(kind)) { - if (!store.get("agents", row.id)) { - const { - id, - name, - revision, - model_id, - harness, - instructions, - skill_ids, - mcp_ids, - } = row; - store.put("agents", { - id, - name, - revision, - model_id, - harness, - instructions, - skill_ids, - mcp_ids, - }); - } - store.remove(kind, row.id); - } - } const sessions = sessionAPI(store, core); const publicRow = (kind, row) => { if (kind === "providers") return publicProvider(row); diff --git a/example/parsar/tests/product.test.mjs b/example/parsar/tests/product.test.mjs index 3a1e59055..a891f24be 100644 --- a/example/parsar/tests/product.test.mjs +++ b/example/parsar/tests/product.test.mjs @@ -299,10 +299,10 @@ test("one Agent creates independent Sessions; edits do not mutate prior executio assert.equal(store.get("sessions", pending).request, undefined); assert.equal(receipts.size, 3); }); -test("compatibility validation precedes execution, and old configurations migrate without fake Sessions", async (t) => { +test("compatibility validation precedes execution", async (t) => { const store = openStore(":memory:"); t.after(() => store.close()); - const { api, put, agent, input } = await setup(store, () => { + const { api, put, input } = await setup(store, () => { throw new Error("must not execute"); }); const runtime = await put("runtimes", { name: "Text", environment: "none" }); @@ -318,13 +318,6 @@ test("compatibility validation precedes execution, and old configurations migrat }), /不含凭据/, ); - const legacy = { ...agent, id: randomUUID(), runtime_id: runtime.id }; - store.put("instances", legacy); - productAPI(store, () => {}); - assert.equal(store.get("agents", legacy.id).name, agent.name); - assert.equal(store.get("agents", legacy.id).runtime_id, undefined); - assert.equal(store.list("sessions").length, 0); - assert.equal(store.list("instances").length, 0); assert.notEqual( dataPath({ target: "https://core.example", key: "a" }), dataPath({ target: "https://core.example", key: "b" }), diff --git a/packages/agents-client/src/client.test.ts b/packages/agents-client/src/client.test.ts index 3d4dde42b..621ce525a 100644 --- a/packages/agents-client/src/client.test.ts +++ b/packages/agents-client/src/client.test.ts @@ -2285,7 +2285,7 @@ describe("OpenAIAgentsClient", () => { } }); - it("keeps the three legacy single-event helpers on the public batch wire", async () => { + it("maps the single-event helpers on the public batch wire", async () => { const calls: FetchCall[] = []; const client = new OpenAIAgentsClient({ fetch: recordingFetch(new Response(null, { status: 202 }), calls) }); diff --git a/packages/agents-client/src/sandbox-client.test.ts b/packages/agents-client/src/sandbox-client.test.ts index 8d53d2cfd..5489e6413 100644 --- a/packages/agents-client/src/sandbox-client.test.ts +++ b/packages/agents-client/src/sandbox-client.test.ts @@ -301,7 +301,7 @@ describe("hosted provider configuration", () => { expect((shown as AgentCoreError).errorType).toBeUndefined(); await expect(client.initializeDeployment({ expected_generation: 0, provider: "e2b", configuration: { template: e2b.template }, credential: { api_key: e2b.api_key } })).rejects.toMatchObject({ status: 409, code: "sandbox_configuration_error", message: rejection.message, param: null }); }); - it("keeps legacy E2B ownership reset actionable without reflecting either key", async () => { + it("keeps E2B ownership reset actionable without reflecting either key", async () => { const fetch = vi.fn().mockResolvedValue(response({ error: { code: "sandbox_reset_required", message: "revoked-old-key " + e2b.api_key, param: "revoked-old-key", details: { current_provider: "e2b", requested_provider: "e2b", secret: e2b.api_key }, diff --git a/services/core/cmd/environment-key/main_test.go b/services/core/cmd/environment-key/main_test.go index 54cae3f0b..feebed0c3 100644 --- a/services/core/cmd/environment-key/main_test.go +++ b/services/core/cmd/environment-key/main_test.go @@ -79,7 +79,7 @@ func TestParseOptionsRequiresEveryIdentityFlag(t *testing.T) { }) } if _, err := parseOptions([]string{"--tenant", testTenant, "--environment", testEnvironment}, io.Discard); err == nil { - t.Fatal("legacy exact-Environment arguments were accepted") + t.Fatal("exact-Environment arguments were accepted") } } diff --git a/services/core/cmd/server/managed_generations.go b/services/core/cmd/server/managed_generations.go index b2a87357d..5874c60c0 100644 --- a/services/core/cmd/server/managed_generations.go +++ b/services/core/cmd/server/managed_generations.go @@ -132,8 +132,9 @@ func (s *managedSetup) routeGenerations(candidate execution.PreparedRuntimeDeplo } if err := verify(current, nil); err != nil { if errors.Is(err, sandbox.ErrCredentialRejected) || errors.Is(err, sandbox.ErrCredentialOwnership) { - // A revoked legacy key or a public template outside its team cannot - // anchor ownership. This says nothing about the candidate key's validity. + // A revoked committed key, or a committed template its own key no + // longer owns, cannot anchor ownership. This says nothing about the + // candidate key's validity. return &deployment.ResetRequiredError{CurrentProvider: setup.Provider, RequestedProvider: setup.Provider} } return err diff --git a/services/core/cmd/server/managed_generations_test.go b/services/core/cmd/server/managed_generations_test.go index 7e5855f6b..9a639d41c 100644 --- a/services/core/cmd/server/managed_generations_test.go +++ b/services/core/cmd/server/managed_generations_test.go @@ -101,15 +101,15 @@ print(json.dumps({'Version':1,'Info':info})) } } -// The empty resource set must not let a publicly readable template substitute -// for a team identity. In particular, the old key must be checked as itself. +// A publicly readable template must not substitute for a team identity, and the +// committed key is checked as itself. func TestE2BReplacementRequiresCommittedOwnershipAnchor(t *testing.T) { for _, tc := range []struct { name, committedKey, committedTemplate, candidateKey, candidateTemplate string want error reset bool }{ - {name: "legacy public template cross team", committedKey: "team-a", committedTemplate: "public-b", candidateKey: "team-b", candidateTemplate: "public-b", reset: true}, + {name: "committed template outside its key's team", committedKey: "team-a", committedTemplate: "public-b", candidateKey: "team-b", candidateTemplate: "public-b", reset: true}, {name: "revoked committed key", committedKey: "revoked", committedTemplate: "owned-a", candidateKey: "team-a", candidateTemplate: "owned-a", reset: true}, {name: "unknown committed ownership", committedKey: "unconfirmed", committedTemplate: "owned-a", candidateKey: "team-a", candidateTemplate: "owned-a", want: sandbox.ErrConfigurationUnconfirmed}, {name: "proven different team", committedKey: "team-a", committedTemplate: "owned-a", candidateKey: "team-b", candidateTemplate: "public-b", want: sandbox.ErrCredentialOwnership}, diff --git a/services/core/internal/api/admin_resources_test.go b/services/core/internal/api/admin_resources_test.go index cf323c93c..32c4660f0 100644 --- a/services/core/internal/api/admin_resources_test.go +++ b/services/core/internal/api/admin_resources_test.go @@ -109,7 +109,7 @@ func TestAdminResourcesHaveExplicitTargetWithoutCallerImpersonation(t *testing.T } for _, path := range []string{"/core/v1/project-api-keys/" + key.TokenSHA256, "/core/v1/resource-owners", "/core/v1/write-operations"} { if w := projectKeyHTTP(h, "GET", path, "admin", ""); w.Code != 404 { - t.Fatal("retired route still served", path, w.Code) + t.Fatal("removed route still served", path, w.Code) } } } diff --git a/services/core/internal/api/configuration_validation.go b/services/core/internal/api/configuration_validation.go index 577621a26..2fb0a933f 100644 --- a/services/core/internal/api/configuration_validation.go +++ b/services/core/internal/api/configuration_validation.go @@ -207,8 +207,7 @@ func checkMembers(path string, raw json.RawMessage, members []member) error { // configurationConflict reports the official semantic rejections of a // well-formed configuration: a non-object schema root, a repeated function name // and a repeated web_search or tool_search declaration. Schemas without a string -// root type are unchanged. Session admission also applies it to the resolved -// saved configuration, so records saved before these checks cannot execute. +// root type are unchanged. func configurationConflict(tools []json.RawMessage, formatType string, schema json.RawMessage) error { names, declared := map[string]bool{}, map[string]bool{} for _, raw := range tools { diff --git a/services/core/internal/api/configuration_validation_test.go b/services/core/internal/api/configuration_validation_test.go index 0c4fc121f..06753adbc 100644 --- a/services/core/internal/api/configuration_validation_test.go +++ b/services/core/internal/api/configuration_validation_test.go @@ -279,8 +279,8 @@ func TestAgentConfigurationLocalLimitsKeepCodes(t *testing.T) { // C4 and SES-33: only configuration errors precede the input requirement; // accepted configurations and saved-Agent lookup keep their order. func TestSessionConfigurationValidationOrder(t *testing.T) { - legacy := uuid.NewString() - h, s := configurationHandler(t, map[string]string{legacy: `{"model":"m","tools":[{"type":"web_search","mode":"disabled"}]}`}) + saved := uuid.NewString() + h, s := configurationHandler(t, map[string]string{saved: `{"model":"m","tools":[{"type":"web_search","mode":"disabled"}]}`}) const input = "conversation-only sessions currently require initial input" for _, body := range []string{ `{"agent":{"model":"m","tools":[{"type":"web_search","mode":"live"}]},"environment":{"type":"none"}}`, @@ -288,7 +288,7 @@ func TestSessionConfigurationValidationOrder(t *testing.T) { `{"agent":{"model":"m","reasoning":{"effort":"max"}},"environment":{"type":"none"}}`, `{"agent":{"instructions":"x"},"environment":{"type":"none"}}`, `{"agent_id":"` + uuid.NewString() + `","environment":{"type":"none"}}`, - `{"agent_id":"` + legacy + `","agent":{"tools":[{"type":"tool_search"}]},"environment":{"type":"none"}}`, + `{"agent_id":"` + saved + `","agent":{"tools":[{"type":"tool_search"}]},"environment":{"type":"none"}}`, } { assertConfigurationError(t, credentialRequest(h, http.MethodPost, "/v1/agents/sessions", body), "invalid_request_error", nil, input) } @@ -297,7 +297,7 @@ func TestSessionConfigurationValidationOrder(t *testing.T) { t.Fatal(w.Code, w.Body) } // An invalid inline override is reported before the saved-Agent lookup. - for _, id := range []string{missing, legacy} { + for _, id := range []string{missing, saved} { for _, suffix := range []string{``, `,"input":"x"`} { w := credentialRequest(h, http.MethodPost, "/v1/agents/sessions", `{"agent_id":"`+id+`","agent":{"tools":[{"type":"tool_search"},{"type":"tool_search"}]},"environment":{"type":"none"}`+suffix+`}`) assertConfigurationError(t, w, "invalid_request_error", nil, "duplicate tool_search tool") @@ -308,46 +308,6 @@ func TestSessionConfigurationValidationOrder(t *testing.T) { } } -// Saved records that predate the conflict checks cannot execute. -func TestSessionAdmissionRejectsSavedConfigurationConflicts(t *testing.T) { - saved := func(fields string) string { - return `{"model":"m","multi_agent":{"enabled":false,"max_concurrent_subagents":null},"reasoning":{},"service_tier":"auto","text":{"format":{"type":"text"},"verbosity":"medium"},` + fields + `}` - } - records := map[string]struct{ configuration, message string }{ - uuid.NewString(): {saved(`"tools":[{"type":"function","name":"lookup","description":"","parameters":{"type":"string"},"defer_loading":false}]`), `Invalid schema for function 'lookup': schema must be a JSON Schema of 'type: "object"', got 'type: "string"'.`}, - uuid.NewString(): {saved(`"tools":[{"type":"function","name":"f","description":"","parameters":{},"defer_loading":false},{"type":"function","name":"f","description":"","parameters":{},"defer_loading":false}]`), "duplicate function tool name: f"}, - uuid.NewString(): {saved(`"tools":[{"type":"tool_search"},{"type":"tool_search"}]`), "duplicate tool_search tool"}, - uuid.NewString(): {saved(`"tools":[],"text":{"format":{"type":"json_schema","schema":{"type":"array"}},"verbosity":"medium"}`), `agent.text.format.schema must have top-level type "object"; got "array"`}, - } - agents := map[string]string{} - for id, record := range records { - agents[id] = record.configuration - } - valid := uuid.NewString() - agents[valid] = saved(`"tools":[{"type":"function","name":"f","description":"","parameters":{"type":"object"},"defer_loading":false}]`) - h, s := configurationHandler(t, agents) - for id, record := range records { - w := credentialRequest(h, http.MethodPost, "/v1/agents/sessions", `{"agent_id":"`+id+`","environment":{"type":"none"},"input":"x"}`) - assertConfigurationError(t, w, "invalid_request_error", nil, record.message) - } - if s.writes != 0 { - t.Fatal("rejected configuration reached storage") - } - // A replacement of the conflicting field admits the saved record. - for id, record := range records { - field := `"tools":[]` - if strings.Contains(record.configuration, "json_schema") { - field = `"text":{"format":{"type":"text"}}` - } - if w := credentialRequest(h, http.MethodPost, "/v1/agents/sessions", `{"agent_id":"`+id+`","agent":{`+field+`},"environment":{"type":"none"},"input":"x"}`); w.Code != http.StatusCreated { - t.Fatal(id, w.Code, w.Body) - } - } - if w := credentialRequest(h, http.MethodPost, "/v1/agents/sessions", `{"agent_id":"`+valid+`","environment":{"type":"none"},"input":"x"}`); w.Code != http.StatusCreated { - t.Fatal(w.Code, w.Body) - } -} - // Caller-supplied names and values are repeated only when short and printable // (the Environment Files rule), so every response stays small. func TestAgentConfigurationErrorsBoundEchoedInput(t *testing.T) { diff --git a/services/core/internal/api/routing_test.go b/services/core/internal/api/routing_test.go index 910246b45..4bef5001b 100644 --- a/services/core/internal/api/routing_test.go +++ b/services/core/internal/api/routing_test.go @@ -561,9 +561,9 @@ func TestHeadRequests(t *testing.T) { } } -// Retired Core extensions are unknown /v1 paths; the administrator reads stay +// Core extensions are unknown /v1 paths; the administrator reads stay // under /core/v1. -func TestRetiredV1ExtensionsAreNotFound(t *testing.T) { +func TestV1ExtensionsAreNotFound(t *testing.T) { handler, _, _ := routingFixture(t) session := "/v1/agents/sessions/" + uuid.NewString() unknown := serve(handler, http.MethodGet, session+"/unknown", "", withHeaders(project, beta)) diff --git a/services/core/internal/api/sandbox_manager_test.go b/services/core/internal/api/sandbox_manager_test.go index aff6e084b..ddb395c04 100644 --- a/services/core/internal/api/sandbox_manager_test.go +++ b/services/core/internal/api/sandbox_manager_test.go @@ -96,7 +96,7 @@ func TestSandboxEnrollmentCapacityIsAdministratorOnly(t *testing.T) { t.Fatal(test.path, response.Code, response.Body.String()) } } - // Node routes moved to /api/v1/sandbox-node; the old /core paths are gone. + // Node routes are served under /api/v1/sandbox-node, never under /core. for _, test := range []struct{ method, path string }{{"POST", "/core/v1/sandbox/enroll"}, {"GET", "/core/v1/sandbox/node/identity"}, {"GET", "/core/v1/sandbox/node/configuration"}} { request := httptest.NewRequest(test.method, test.path, nil) request.Header.Set("Authorization", "Bearer administrator") diff --git a/services/core/internal/api/session_agent.go b/services/core/internal/api/session_agent.go index fc6366f0b..aa81f7400 100644 --- a/services/core/internal/api/session_agent.go +++ b/services/core/internal/api/session_agent.go @@ -71,10 +71,6 @@ func resolveSessionAgent(input sessionRequest, saved *v1.SavedAgent) (v1.Agent, } func admitSessionAgent(cfg v1.SavedAgentConfiguration) (v1.Agent, error) { - // Protocol conflicts precede execution limits, including in saved records. - if err := configurationConflict(cfg.Tools, cfg.Text.Format.Type, cfg.Text.Format.Schema); err != nil { - return v1.Agent{}, err - } if strings.TrimSpace(cfg.Model) == "" { return v1.Agent{}, errors.New("Execution currently requires a nonempty model.") } diff --git a/services/core/internal/api/session_model_configuration.go b/services/core/internal/api/session_model_configuration.go index c40c82c18..3abbcd9ba 100644 --- a/services/core/internal/api/session_model_configuration.go +++ b/services/core/internal/api/session_model_configuration.go @@ -74,7 +74,7 @@ func (h *Handler) prepareSessionModelConfiguration(ctx context.Context, input *s source = v1.ExecutionSourceSession } else if saved != nil { source = v1.ExecutionSourceAgent - // Changing the harness also discards the former adapter's parameters. + // Changing the harness also discards the previous adapter's parameters. if _, overridden := input.agentFields["x_agents_core"]; !overridden && saved.XAgentsCore != nil { raw = v1.ResolvedHarnessConfig(saved.XAgentsCore.HarnessConfig) } diff --git a/services/core/internal/deployment/nodes.go b/services/core/internal/deployment/nodes.go index 94c281f5b..8dfe2049d 100644 --- a/services/core/internal/deployment/nodes.go +++ b/services/core/internal/deployment/nodes.go @@ -24,24 +24,16 @@ func (s *Service) ListNodes(ctx context.Context) ([]Node, error) { } out := make([]Node, 0, len(records)) for _, n := range records { - value, err := s.node(n) - if err != nil { - return nil, err - } - out = append(out, value) + out = append(out, nodeView(n)) } return out, nil } -func (s *Service) node(n NodeRecord) (Node, error) { - retained, err := s.registry.RetainedLimit(n.Provider, n.MaxActive, n.MaxRetained) - if err != nil { - return Node{}, err - } +func nodeView(n NodeRecord) Node { health := n.Health health.Host = nil health.ProviderReady = n.Online && n.ServingReady - return Node{Rollout: nodeRollout(n), NodeHealth: health, Running: n.Running, Snapshots: n.Snapshots, ID: n.ID, Name: n.Name, CoreURL: n.CoreURL, EnrollmentID: n.EnrollmentID, Provider: n.Provider, Online: n.Online, LastSeenAt: n.LastSeenAt, MaxActive: n.MaxActive, MaxRetained: retained, Active: n.Active, Reserved: n.Reserved, Retained: n.Retained, CleanupPending: n.CleanupPending, CreatedAt: n.CreatedAt}, nil + return Node{Rollout: nodeRollout(n), NodeHealth: health, Running: n.Running, Snapshots: n.Snapshots, ID: n.ID, Name: n.Name, CoreURL: n.CoreURL, EnrollmentID: n.EnrollmentID, Provider: n.Provider, Online: n.Online, LastSeenAt: n.LastSeenAt, MaxActive: n.MaxActive, MaxRetained: n.MaxRetained, Active: n.Active, Reserved: n.Reserved, Retained: n.Retained, CleanupPending: n.CleanupPending, CreatedAt: n.CreatedAt} } // NodeDetail returns one node with its last host observation and its host @@ -62,11 +54,7 @@ func (s *Service) NodeDetail(ctx context.Context, id, window string) (NodeDetail if err != nil { return NodeDetail{}, err } - node, err := s.node(record) - if err != nil { - return NodeDetail{}, err - } - result := NodeDetail{Node: node, History: HostHistory{ResolutionSeconds: span.ResolutionSeconds, Points: historyPoints(span, samples)}} + result := NodeDetail{Node: nodeView(record), History: HostHistory{ResolutionSeconds: span.ResolutionSeconds, Points: historyPoints(span, samples)}} if record.Health.Host != nil { result.Host = *record.Health.Host } @@ -240,8 +228,8 @@ func (s *Service) Enroll(ctx context.Context, token string, input Enrollment) (N if !consumed { return ErrNodeCredential } - result, err = s.identity(n, d.Provider) - return err + result = identity(n, d.Provider) + return nil }) if err != nil { return NodeIdentity{}, err @@ -249,12 +237,8 @@ func (s *Service) Enroll(ctx context.Context, token string, input Enrollment) (N return result, nil } -func (s *Service) identity(n StoredNode, provider string) (NodeIdentity, error) { - retained, err := s.registry.RetainedLimit(provider, n.MaxActive, n.MaxRetained) - if err != nil { - return NodeIdentity{}, err - } - return NodeIdentity{SpecificationDigest: n.SpecificationDigest, DeploymentGeneration: n.DeploymentGeneration, NodeID: n.ID, InstallationID: n.InstallationID, Provider: provider, BackendFingerprint: n.BackendFingerprint, MaxActive: n.MaxActive, MaxRetained: retained}, nil +func identity(n StoredNode, provider string) NodeIdentity { + return NodeIdentity{SpecificationDigest: n.SpecificationDigest, DeploymentGeneration: n.DeploymentGeneration, NodeID: n.ID, InstallationID: n.InstallationID, Provider: provider, BackendFingerprint: n.BackendFingerprint, MaxActive: n.MaxActive, MaxRetained: n.MaxRetained} } // AuthenticateNode checks a node credential and returns the node's identity. @@ -288,8 +272,8 @@ func (s *Service) AuthenticateNode(ctx context.Context, nodeID, credential strin // Reset retires nodes before another backend lineage can be selected. // Enrollment generation and digest remain immutable identity history; the // current target and per-generation readiness do not replace that history. - result, err = s.identity(n, d.Provider) - return err + result = identity(n, d.Provider) + return nil }) if err != nil { return NodeIdentity{}, err @@ -424,11 +408,7 @@ func (s *Service) NodeConfiguration(ctx context.Context, nodeID, token string, g if err != nil { return err } - limit, err := s.registry.RetainedLimit(d.Provider, active, retained) - if err != nil { - return err - } - result = NodeConfiguration{MaxActive: active, MaxRetained: limit, InstallationID: d.InstallationID, Provider: d.Provider, CoreURL: s.rules.PublicURL(), Generation: selected, Specification: spec, SpecificationDigest: spec.Digest(d.Provider)} + result = NodeConfiguration{MaxActive: active, MaxRetained: retained, InstallationID: d.InstallationID, Provider: d.Provider, CoreURL: s.rules.PublicURL(), Generation: selected, Specification: spec, SpecificationDigest: spec.Digest(d.Provider)} return nil }) if err != nil { diff --git a/services/core/internal/deployment/service_test.go b/services/core/internal/deployment/service_test.go index b7ca120df..1c2c85878 100644 --- a/services/core/internal/deployment/service_test.go +++ b/services/core/internal/deployment/service_test.go @@ -157,16 +157,12 @@ func TestSandboxValidationWrapperPreservesClassification(t *testing.T) { } } -// A stored provider the registry no longer holds fails every read and change -// that consults it. +// A stored provider the registry no longer holds fails every read that consults it. func TestRegistryLookupFailuresPropagate(t *testing.T) { stored := webDeployment(t, uuid.NewString(), "retired", 1) reader := &fakeReader{t: t, snapshot: func(context.Context) (Snapshot, error) { return Snapshot{Record: stored}, nil }, deployment: func(context.Context) (Record, error) { return stored, nil }, - nodes: func(context.Context) ([]NodeRecord, error) { - return []NodeRecord{{ID: uuid.NewString(), Provider: "retired", MaxActive: 1, MaxRetained: 1}}, nil - }, } service := newService(t, &fakeStorage{t: t}, reader, testPublicURL) // View decodes the stored configuration first, and reports that failure as a conflict. @@ -176,9 +172,6 @@ func TestRegistryLookupFailuresPropagate(t *testing.T) { if _, err := service.Setup(t.Context()); !errors.Is(err, providers.ErrUnknownProvider) { t.Errorf("Setup = %v", err) } - if _, err := service.ListNodes(t.Context()); !errors.Is(err, providers.ErrUnknownProvider) { - t.Errorf("ListNodes = %v", err) - } } // Setup carries the mode and operations the provider declares. A stored @@ -198,17 +191,6 @@ func TestSetupReportsDeclarationsAndStoredConfigurationConflicts(t *testing.T) { } } -// Docker never suspends, so its nodes retain at most their active capacity. -func TestListNodesAppliesTheRetainedLimit(t *testing.T) { - reader := &fakeReader{t: t, nodes: func(context.Context) ([]NodeRecord, error) { - return []NodeRecord{{Provider: "docker", MaxActive: 2, MaxRetained: 5}, {Provider: "microsandbox", MaxActive: 2, MaxRetained: 5}}, nil - }} - nodes, err := newService(t, &fakeStorage{t: t}, reader, testPublicURL).ListNodes(t.Context()) - if err != nil || len(nodes) != 2 || nodes[0].MaxRetained != 2 || nodes[1].MaxRetained != 5 { - t.Fatalf("ListNodes = %+v, %v", nodes, err) - } -} - func TestSetupChangeDecisions(t *testing.T) { installation := uuid.NewString() stored := webDeployment(t, installation, "docker", 2) diff --git a/services/core/internal/execution/environment_placement.go b/services/core/internal/execution/environment_placement.go index ed1bcbf95..148e26fc3 100644 --- a/services/core/internal/execution/environment_placement.go +++ b/services/core/internal/execution/environment_placement.go @@ -23,7 +23,7 @@ type environmentPlacement struct { CapabilityDirectories []string `json:"capability_directories"` } -// LocalWorkspaceConfiguration recognizes the qualified stored V1 profile. It +// LocalWorkspaceConfiguration recognizes a stored V1 profile. It // does not provision a Runtime, validate live authority, or admit hosted creation. func LocalWorkspaceConfiguration(configuration json.RawMessage) bool { placement, err := parseEnvironmentPlacement(configuration) diff --git a/services/core/internal/execution/sandbox_generations_test.go b/services/core/internal/execution/sandbox_generations_test.go index 733d28f82..0f4b12432 100644 --- a/services/core/internal/execution/sandbox_generations_test.go +++ b/services/core/internal/execution/sandbox_generations_test.go @@ -67,7 +67,7 @@ func TestE2BReplacementVerifiesTwiceAndNeverPublishesFailedCommit(t *testing.T) request := input request.ExpectedGeneration = 1 audit := adminaudit.WithSource(t.Context(), adminaudit.Source{CredentialID: "test", RequestID: "test", TraceID: "test"}) - for _, failure := range []string{"preliminary", "final", "unanchored legacy or revoked committed key", "unknown ownership", "commit"} { + for _, failure := range []string{"preliminary", "final", "revoked committed key", "unknown ownership", "commit"} { verifyCalls = 0 rejectAt = 0 ctx := audit @@ -76,7 +76,7 @@ func TestE2BReplacementVerifiesTwiceAndNeverPublishesFailedCommit(t *testing.T) rejectAt = 1 case "final": rejectAt = 2 - case "unanchored legacy or revoked committed key": + case "revoked committed key": rejectAt = 1 rejection = &deployment.ResetRequiredError{CurrentProvider: "e2b", RequestedProvider: "e2b"} case "unknown ownership": diff --git a/services/core/internal/items/changes.go b/services/core/internal/items/changes.go index adf6c6fc1..f4b5c8e13 100644 --- a/services/core/internal/items/changes.go +++ b/services/core/internal/items/changes.go @@ -33,9 +33,9 @@ type Change struct { } // NeedsNativeMessage reports whether Observe reads Stored.NativeMessage for u: -// a legacy aggregate applies only when the Turn recorded no native message. +// an aggregate answer applies only when the Turn recorded no native message. func (u Update) NeedsNativeMessage() bool { - return u.LegacyFinal + return u.AggregateFinal } // ResultCall returns the call whose saved result Observe reads for u, a @@ -48,7 +48,7 @@ func (u Update) ResultCall() (string, bool) { // its stored Item. It reports false when the Update is superseded and writes // nothing. func Observe(kind string, update Update, stored Stored) (Change, bool, error) { - if update.LegacyFinal && stored.NativeMessage { + if update.AggregateFinal && stored.NativeMessage { return Change{}, false, nil } item, err := Merge(update, stored.Item) diff --git a/services/core/internal/items/changes_test.go b/services/core/internal/items/changes_test.go index 96f98766a..92c6915f0 100644 --- a/services/core/internal/items/changes_test.go +++ b/services/core/internal/items/changes_test.go @@ -28,11 +28,11 @@ func TestObserveDecidesItemChanges(t *testing.T) { check func(Change) bool }{ { - name: "a legacy aggregate yields to a native message", kind: "execution_completed", + name: "an aggregate answer yields to a native message", kind: "execution_completed", update: project("execution_completed", `{"done":{"content":"final"}}`), stored: Stored{NativeMessage: true}, skip: true, }, { - name: "a legacy aggregate applies without a native message", kind: "execution_completed", + name: "an aggregate answer applies without a native message", kind: "execution_completed", update: project("execution_completed", `{"done":{"content":"final"}}`), check: func(c Change) bool { return c.Item.Status == "completed" && c.Output && c.Delta == nil }, }, @@ -74,15 +74,15 @@ func TestObserveDecidesItemChanges(t *testing.T) { } func TestUpdatesDeclareTheFactsObserveReads(t *testing.T) { - legacy := Update{Item: v1.Item{Type: "message"}, LegacyFinal: true} + aggregate := Update{Item: v1.Item{Type: "message"}, AggregateFinal: true} result := Update{Item: v1.Item{Type: "function_call_output", CallID: "call"}} - if !legacy.NeedsNativeMessage() || result.NeedsNativeMessage() { + if !aggregate.NeedsNativeMessage() || result.NeedsNativeMessage() { t.Fatal("native message need") } if call, ok := result.ResultCall(); !ok || call != "call" { t.Fatal("result call", call, ok) } - if _, ok := legacy.ResultCall(); ok { + if _, ok := aggregate.ResultCall(); ok { t.Fatal("message asked for a function result") } } diff --git a/services/core/internal/items/messages.go b/services/core/internal/items/messages.go index 50c439f45..2c4d0cd5f 100644 --- a/services/core/internal/items/messages.go +++ b/services/core/internal/items/messages.go @@ -14,8 +14,8 @@ type Update struct { Item v1.Item AppendText bool CommandOutputDelta *string - // A legacy aggregate is used only when no native message identity was recorded. - LegacyFinal bool + // An aggregate answer is used only when no native message identity was recorded. + AggregateFinal bool } func Identity(turn, key string) string { @@ -42,7 +42,7 @@ func Project(turn, kind string, sequence int64, raw json.RawMessage) ([]Update, } key := "message:" + p.ItemID if p.ItemID == "" { - key = "legacy-message" + key = "aggregate-message" } return []Update{{Item: message(turn, key, "assistant", p.Delta, "in_progress"), AppendText: true}}, nil case proto.TypeOutputMessage: @@ -63,7 +63,7 @@ func Project(turn, kind string, sequence int64, raw json.RawMessage) ([]Update, } return []Update{{Item: item, AppendText: p.Text == nil}}, nil case proto.TypeDone, "execution_failed": - // Legacy Done may contain adapter diagnostics. Only a successful Turn + // Done may contain adapter diagnostics. Only a successful Turn // confirms aggregate answer text; failures retain observed message deltas. return nil, nil case "cancel_receipt", "execution_completed", "execution_cancelled": @@ -89,7 +89,7 @@ func Project(turn, kind string, sequence int64, raw json.RawMessage) ([]Update, if kind == "execution_completed" { status = "completed" } - return []Update{{Item: message(turn, "legacy-message", "assistant", final.Content, status), LegacyFinal: true}}, nil + return []Update{{Item: message(turn, "aggregate-message", "assistant", final.Content, status), AggregateFinal: true}}, nil case proto.TypeToolCall: return projectTool(turn, raw) case proto.TypeCommandOutput: @@ -107,7 +107,7 @@ func Merge(update Update, previous v1.Item) (v1.Item, error) { if previous.ID == "" { return item, nil } - if previous.Status != "in_progress" && !(update.LegacyFinal && previous.Status == "incomplete") { + if previous.Status != "in_progress" && !(update.AggregateFinal && previous.Status == "incomplete") { return previous, nil } if (item.Type == "function_call") && (item.Arguments == nil || string(encoded(item.Arguments)) == "null") { diff --git a/services/core/internal/items/messages_test.go b/services/core/internal/items/messages_test.go index eb6e8acf2..d7946d4ef 100644 --- a/services/core/internal/items/messages_test.go +++ b/services/core/internal/items/messages_test.go @@ -34,7 +34,7 @@ func TestMessageSnapshotsReplaceDeltasAndDoNotRegress(t *testing.T) { } } -func TestLegacyDoneDoesNotConfirmSuccessfulAnswer(t *testing.T) { +func TestDoneDoesNotConfirmSuccessfulAnswer(t *testing.T) { var previous v1.Item for _, event := range []struct{ kind, body string }{ {"delta", `{"delta":"partial answer"}`}, diff --git a/services/core/internal/persistence/postgres/deploymentpg/nodes_test.go b/services/core/internal/persistence/postgres/deploymentpg/nodes_test.go index 5f58df30f..399e75669 100644 --- a/services/core/internal/persistence/postgres/deploymentpg/nodes_test.go +++ b/services/core/internal/persistence/postgres/deploymentpg/nodes_test.go @@ -81,8 +81,7 @@ func TestEnrollmentApprovedCapacity(t *testing.T) { } } -// Docker never suspends a sandbox, so every read reports its retained limit as its -// active limit, including for a token or node stored before Core applied that rule. +// Docker never suspends a sandbox, so every write stores its retained limit as its active limit. func TestDockerRetainedLimitFollowsActive(t *testing.T) { f := newFixture(t) changes, _ := f.execution(t) @@ -95,40 +94,12 @@ func TestDockerRetainedLimitFollowsActive(t *testing.T) { if identity, err := f.service.Enroll(t.Context(), token.Token, current); err != nil || identity.MaxRetained != 3 { t.Fatal("enrollment kept a separate Docker retained limit", identity, err) } - legacyToken, err := f.service.CreateEnrollment(t.Context(), deployment.Capacity{MaxActive: 2, MaxRetained: 8}) - if err != nil { - t.Fatal(err) - } - nodesExec(t, f.pool, "UPDATE runtime_node_enrollments SET max_retained=8 WHERE token_sha256=$1", nodesTokenDigest(legacyToken.Token)) - if config, err := f.service.NodeConfiguration(t.Context(), "", legacyToken.Token, 0); err != nil || config.MaxRetained != 2 { - t.Fatal("bootstrap reported a legacy Docker retained limit", config, err) - } - legacy := nodesEnrollment(view, "Legacy", "l") - if _, err := f.service.Enroll(t.Context(), legacyToken.Token, legacy); err != nil { - t.Fatal(err) - } - var stored int32 - if err := f.pool.QueryRow(t.Context(), "SELECT max_retained FROM runtime_nodes WHERE id=$1", legacy.NodeID).Scan(&stored); err != nil || stored != 2 { - t.Fatal("enrollment stored a legacy Docker retained limit", stored, err) - } - nodesExec(t, f.pool, "UPDATE runtime_nodes SET max_retained=8 WHERE id=$1", legacy.NodeID) - if identity, err := f.service.AuthenticateNode(t.Context(), legacy.NodeID, legacy.Credential); err != nil || identity.MaxRetained != 2 { - t.Fatal("node identity reported a legacy Docker retained limit", identity, err) - } - if config, err := f.service.NodeConfiguration(t.Context(), legacy.NodeID, legacy.Credential, 0); err != nil || config.MaxRetained != 2 { - t.Fatal("node configuration reported a legacy Docker retained limit", config, err) - } if err := f.service.UpdateNode(t.Context(), current.NodeID, deployment.NodeUpdate{Name: "Docker", MaxActive: 5, MaxRetained: 12}); err != nil { t.Fatal(err) } nodes, err := f.service.ListNodes(t.Context()) - if err != nil || len(nodes) != 2 { - t.Fatal(nodes, err) - } - for _, n := range nodes { - if n.MaxRetained != n.MaxActive || (n.ID == current.NodeID && n.MaxActive != 5) { - t.Fatal("node list kept a separate Docker retained limit", n) - } + if err != nil || len(nodes) != 1 || nodes[0].MaxActive != 5 || nodes[0].MaxRetained != 5 { + t.Fatal("node list kept a separate Docker retained limit", nodes, err) } } diff --git a/services/core/internal/persistence/postgres/sessionpg/sessionpg_test.go b/services/core/internal/persistence/postgres/sessionpg/sessionpg_test.go index b8aca3bd6..d9e6dc7d6 100644 --- a/services/core/internal/persistence/postgres/sessionpg/sessionpg_test.go +++ b/services/core/internal/persistence/postgres/sessionpg/sessionpg_test.go @@ -127,9 +127,9 @@ func TestItemsTakePositionsAndOutputIndexes(t *testing.T) { if err != nil || !reflect.DeepEqual(stored, items.Stored{Item: answer}) { t.Fatalf("stored %+v, %v", stored, err) } - legacy, err := bound.LoadItem(t.Context(), turnID, items.Update{Item: v1.Item{ID: uuid.NewString()}, LegacyFinal: true}) - if err != nil || !legacy.NativeMessage || legacy.Item.ID != "" { - t.Fatalf("legacy aggregate %+v, %v", legacy, err) + aggregate, err := bound.LoadItem(t.Context(), turnID, items.Update{Item: v1.Item{ID: uuid.NewString()}, AggregateFinal: true}) + if err != nil || !aggregate.NativeMessage || aggregate.Item.ID != "" { + t.Fatalf("aggregate answer %+v, %v", aggregate, err) } return nil }) diff --git a/services/core/internal/persistence/postgres/vaultpg/seal_test.go b/services/core/internal/persistence/postgres/vaultpg/seal_test.go index d77172ab8..27ceea575 100644 --- a/services/core/internal/persistence/postgres/vaultpg/seal_test.go +++ b/services/core/internal/persistence/postgres/vaultpg/seal_test.go @@ -67,7 +67,7 @@ func TestCredentialSecretsKeepTheirSealedFormat(t *testing.T) { t.Fatal(err) } } - legacy, err := json.Marshal(storedGrant{Version: 1, Metadata: metadata, AccessToken: "old-access", RefreshToken: "refresh"}) + grant, err := json.Marshal(storedGrant{Version: 1, Metadata: metadata, AccessToken: "old-access", RefreshToken: "refresh"}) if err != nil { t.Fatal(err) } @@ -75,7 +75,7 @@ func TestCredentialSecretsKeepTheirSealedFormat(t *testing.T) { return bearerToken(t.Context(), service, tenant, []string{vault.ID}, oauthBinding(credential)) } write(static, []byte("old-static"), scope(static)) - write(oauth, legacy, scope(oauth)) + write(oauth, grant, scope(oauth)) for _, tc := range []struct { credential vaults.Credential want, fails string @@ -103,7 +103,7 @@ func TestCredentialSecretsKeepTheirSealedFormat(t *testing.T) { wrongID.CredentialID = oauth.ID wrongDestination.Destination += "/other" write(static, []byte("old-static"), wrongID) - write(oauth, legacy, wrongDestination) + write(oauth, grant, wrongDestination) for _, tc := range []struct { credential vaults.Credential message string diff --git a/services/core/internal/runtimedevice/state.go b/services/core/internal/runtimedevice/state.go index 56f7c3eb2..931dde4ae 100644 --- a/services/core/internal/runtimedevice/state.go +++ b/services/core/internal/runtimedevice/state.go @@ -1,51 +1,6 @@ // Package device defines persistence data shared by daemon gateways and their stores. package runtimedevice -import "time" - -const OwnerStatusConnected = "connected" -const OwnerStatusDraining = "draining" -const OwnerStatusExpired = "expired" - -// Owner is the persisted view of the current -// WebSocket owner for one agent_daemon device. Generation is a fencing -// token: renewal/release paths must carry it so stale pods can't act. -type Owner struct { - DeviceID string - WorkspaceID string - OwnerPodID string - OwnerURL string - Generation int64 - Status string - ConnectedAt time.Time - LastSeenAt time.Time - LeaseExpiresAt time.Time - UpdatedAt time.Time -} - -type ClaimOwner struct { - DeviceID string - WorkspaceID string - OwnerPodID string - OwnerURL string - LeaseExpiresAt time.Time - Now time.Time -} - -type RenewOwner struct { - DeviceID string - OwnerPodID string - Generation int64 - LeaseExpiresAt time.Time - Now time.Time -} - -type ReleaseOwner struct { - DeviceID string - OwnerPodID string - Generation int64 -} - // HeartbeatStatus is the post-heartbeat liveness the runner uses to // detect state changes. type HeartbeatStatus struct { diff --git a/services/core/internal/runtimegateway/auth_test.go b/services/core/internal/runtimegateway/auth_test.go index 4aeb25182..0754f2081 100644 --- a/services/core/internal/runtimegateway/auth_test.go +++ b/services/core/internal/runtimegateway/auth_test.go @@ -39,7 +39,7 @@ func TestAuthenticator_RejectsUnknownDevice(t *testing.T) { } func TestAuthenticator_RejectsWrongRuntimeType(t *testing.T) { - // A legacy local runtime row trying to dial in as agent_daemon + // A local runtime row trying to dial in as agent_daemon // must fail — otherwise a paired local credential could open an // agent_daemon WS and bypass the device picker. row := runtimedevice.Credential{ diff --git a/services/core/internal/runtimegateway/cancellation_test.go b/services/core/internal/runtimegateway/cancellation_test.go index cead32b38..09f8d3eac 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 := NewSession(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 := NewSession(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 := NewSession(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 da0a6af11..a6c26c69b 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 := NewSession(newFakeConn(), "runtime", "workspace", proto.Version, NewRegistry(), nil) + session := newTestSession(newFakeConn(), "runtime", "workspace", 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) diff --git a/services/core/internal/runtimegateway/handler.go b/services/core/internal/runtimegateway/handler.go index a22d6df2d..ac5f3a5f7 100644 --- a/services/core/internal/runtimegateway/handler.go +++ b/services/core/internal/runtimegateway/handler.go @@ -45,18 +45,6 @@ type HandlerConfig struct { // the externally-reachable URL. PublicWSURL string - // OwnerStore enables multi-pod WebSocket ownership. When set, every - // successful daemon WS dial-in claims device_id -> owner_pod_id in - // Postgres and receives a generation fencing token. nil preserves - // the legacy single-pod in-memory Registry behavior. - OwnerStore DeviceOwnerStore - OwnerPodID string - OwnerURL string - - // OwnerLeaseTTL controls how long the owner row stays valid without - // a renewing inbound daemon frame. Zero uses the package default. - OwnerLeaseTTL time.Duration - // HeartbeatInterval overrides DefaultHeartbeatInterval. Zero -> default. HeartbeatInterval time.Duration @@ -83,7 +71,6 @@ func NewHandler(cfg HandlerConfig) *Handler { if cfg.HeartbeatInterval <= 0 { cfg.HeartbeatInterval = DefaultHeartbeatInterval } - cfg.OwnerLeaseTTL = normalizeOwnerTTL(cfg.OwnerLeaseTTL) if cfg.Log == nil { cfg.Log = func(string, ...any) {} } @@ -140,37 +127,12 @@ func (h *Handler) WS(w http.ResponseWriter, r *http.Request) { h.cfg.Log("agentdaemon gateway: heartbeat on connect: %v", hbErr) } } - var lease *ownerLease - if h.cfg.OwnerStore != nil { - now := time.Now().UTC() - owner, ownerErr := h.cfg.OwnerStore.ClaimAgentDaemonDeviceOwner(r.Context(), runtimedevice.ClaimOwner{ - DeviceID: auth.DeviceID, - WorkspaceID: auth.WorkspaceID, - OwnerPodID: h.cfg.OwnerPodID, - OwnerURL: h.cfg.OwnerURL, - Now: now, - LeaseExpiresAt: now.Add(h.cfg.OwnerLeaseTTL), - }) - if ownerErr != nil { - h.cfg.Log("agentdaemon gateway: owner claim failed: %v", ownerErr) - _ = conn.Close() - return - } - lease = &ownerLease{ - store: h.cfg.OwnerStore, - deviceID: auth.DeviceID, - ownerPodID: owner.OwnerPodID, - ownerURL: owner.OwnerURL, - generation: owner.Generation, - ttl: h.cfg.OwnerLeaseTTL, - } - } - sess := NewSessionWithOwner(conn, auth.DeviceID, auth.WorkspaceID, version, h.cfg.Registry, h.cfg.Log, lease) + sess := NewSession(conn, auth.DeviceID, auth.WorkspaceID, version, h.cfg.Registry, h.cfg.Log) sess.heartbeat = h.cfg.Heartbeat sess.archivedCancellations = h.cfg.ArchivedCancellations sess.credentialHash = runtimedevice.HashCredential(token) - h.cfg.Log("agentdaemon gateway: ws upgrade ok, registering device_id=%s owner_pod=%s waiters=%d", - auth.DeviceID, h.cfg.OwnerPodID, len(h.cfg.Registry.PendingWaiters(auth.DeviceID))) + h.cfg.Log("agentdaemon gateway: ws upgrade ok, registering device_id=%s waiters=%d", + auth.DeviceID, len(h.cfg.Registry.PendingWaiters(auth.DeviceID))) if prev := h.cfg.Registry.Register(sess); prev != nil { // Latest dial-in wins; close the zombie out-of-band. prev.Close("preempted by newer connection from same device_id") @@ -224,54 +186,6 @@ func (h *Handler) Bootstrap(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(resp) } -// DeviceStatus is a lightweight liveness probe the daemon hits before -// the WS dial. -func (h *Handler) DeviceStatus(w http.ResponseWriter, r *http.Request) { - bearer := bearerFromAuthHeader(r) - if bearer == "" { - writeAuthError(w, http.StatusUnauthorized, "missing_bearer", "") - return - } - deviceID := r.URL.Query().Get("device_id") - if deviceID == "" { - writeAuthError(w, http.StatusBadRequest, "missing_device_id", "device_id query param required") - return - } - auth, err := h.cfg.Authenticator.AuthenticateBearer(r.Context(), deviceID, bearer) - if err != nil { - status, code := mapAuthError(err) - h.cfg.Log("agentdaemon gateway: device-status auth rejected device_id=%s code=%s status=%d err=%v", - deviceID, code, status, err) - writeAuthError(w, status, code, err.Error()) - return - } - _, regErr := h.cfg.Registry.LookupDevice(auth.DeviceID) - online := regErr == nil - var owner map[string]any - if h.cfg.OwnerStore != nil { - if current, ok, ownerErr := h.cfg.OwnerStore.GetAgentDaemonDeviceOwner(r.Context(), auth.DeviceID); ownerErr != nil { - h.cfg.Log("agentdaemon gateway: device-status owner lookup failed: %v", ownerErr) - } else if ok { - leaseOnline := current.Status == runtimedevice.OwnerStatusConnected && current.LeaseExpiresAt.After(time.Now().UTC()) - online = online || leaseOnline - owner = map[string]any{ - "owner_pod_id": current.OwnerPodID, - "owner_url": current.OwnerURL, - "generation": current.Generation, - "status": current.Status, - "lease_expires_at": current.LeaseExpiresAt, - } - } - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _ = json.NewEncoder(w).Encode(map[string]any{ - "device_id": auth.DeviceID, - "online": online, - "owner": owner, - }) -} - // ---------------------------------------------------------------------- // helpers // ---------------------------------------------------------------------- diff --git a/services/core/internal/runtimegateway/owner.go b/services/core/internal/runtimegateway/owner.go deleted file mode 100644 index 9155c5347..000000000 --- a/services/core/internal/runtimegateway/owner.go +++ /dev/null @@ -1,35 +0,0 @@ -package runtimegateway - -import ( - "context" - "time" - - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" -) - -const defaultOwnerLeaseTTL = 90 * time.Second - -// DeviceOwnerStore is the DB-backed owner lease surface used by the -// gateway. -type DeviceOwnerStore interface { - ClaimAgentDaemonDeviceOwner(ctx context.Context, input runtimedevice.ClaimOwner) (runtimedevice.Owner, error) - RenewAgentDaemonDeviceOwner(ctx context.Context, input runtimedevice.RenewOwner) (runtimedevice.Owner, bool, error) - ReleaseAgentDaemonDeviceOwner(ctx context.Context, input runtimedevice.ReleaseOwner) (bool, error) - GetAgentDaemonDeviceOwner(ctx context.Context, deviceID string) (runtimedevice.Owner, bool, error) -} - -type ownerLease struct { - store DeviceOwnerStore - deviceID string - ownerPodID string - ownerURL string - generation int64 - ttl time.Duration -} - -func normalizeOwnerTTL(ttl time.Duration) time.Duration { - if ttl <= 0 { - return defaultOwnerLeaseTTL - } - return ttl -} diff --git a/services/core/internal/runtimegateway/owner_test.go b/services/core/internal/runtimegateway/owner_test.go deleted file mode 100644 index 09ace7043..000000000 --- a/services/core/internal/runtimegateway/owner_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package runtimegateway - -import ( - "context" - "testing" - "time" - - "github.com/MiniMax-AI/OpenAgentCore/internal/agentdaemon/proto" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" -) - -type fakeOwnerStore struct { - renewOK bool - renewCalls int - releaseCalls int - releaseCh chan struct{} - lastRenewGen int64 -} - -func (f *fakeOwnerStore) ClaimAgentDaemonDeviceOwner(context.Context, runtimedevice.ClaimOwner) (runtimedevice.Owner, error) { - return runtimedevice.Owner{}, nil -} - -func (f *fakeOwnerStore) RenewAgentDaemonDeviceOwner(_ context.Context, in runtimedevice.RenewOwner) (runtimedevice.Owner, bool, error) { - f.renewCalls++ - f.lastRenewGen = in.Generation - return runtimedevice.Owner{}, f.renewOK, nil -} - -func (f *fakeOwnerStore) ReleaseAgentDaemonDeviceOwner(context.Context, runtimedevice.ReleaseOwner) (bool, error) { - f.releaseCalls++ - if f.releaseCh != nil { - select { - case <-f.releaseCh: - default: - close(f.releaseCh) - } - } - return true, nil -} - -func (f *fakeOwnerStore) GetAgentDaemonDeviceOwner(context.Context, string) (runtimedevice.Owner, bool, error) { - return runtimedevice.Owner{}, false, nil -} - -func TestSessionOwnerLeaseLostClosesStaleConnection(t *testing.T) { - reg := NewRegistry() - conn := newFakeConn() - owners := &fakeOwnerStore{renewOK: false, releaseCh: make(chan struct{})} - lease := &ownerLease{store: owners, deviceID: "dev-1", ownerPodID: "pod-a", generation: 7, ttl: time.Minute} - sess := NewSessionWithOwner(conn, "dev-1", "wks-1", proto.Version, reg, func(string, ...any) {}, lease) - reg.Register(sess) - sess.Start() - - heartbeat, _ := proto.NewEnvelope(proto.TypeHeartbeat, "", proto.HeartbeatPayload{}) - raw, _ := jsonMarshal(heartbeat) - conn.Feed(raw) - - select { - case <-sess.Closed(): - case <-time.After(2 * time.Second): - t.Fatal("session did not close after owner renew returned false") - } - if owners.renewCalls == 0 || owners.lastRenewGen != 7 { - t.Fatalf("renew not called with generation 7: calls=%d gen=%d", owners.renewCalls, owners.lastRenewGen) - } - select { - case <-owners.releaseCh: - case <-time.After(2 * time.Second): - t.Fatal("release not called on close") - } - if owners.releaseCalls == 0 { - t.Fatal("release count not incremented") - } -} diff --git a/services/core/internal/runtimegateway/preparation_test.go b/services/core/internal/runtimegateway/preparation_test.go index b4383a498..421faf5a8 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 := NewSession(newFakeConn(), "device", "tenant", "test", registry, nil) + s := newTestSession(newFakeConn(), "device", "tenant", "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 := NewSession(newFakeConn(), "device", "tenant", "test", NewRegistry(), nil) + s := newTestSession(newFakeConn(), "device", "tenant", "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 e67e87ccd..34a55af4b 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 := NewSession(newFakeConn(), "dev-warm", "wks-1", "0.1.0", reg, nil) + sess := newTestSession(newFakeConn(), "dev-warm", "wks-1", "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 := NewSession(newFakeConn(), "dev-cold", "wks-1", "0.1.0", reg, nil) + sess := newTestSession(newFakeConn(), "dev-cold", "wks-1", "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 := NewSession(newFakeConn(), "dev-race", "wks-1", "0.1.0", reg, nil) + sess := newTestSession(newFakeConn(), "dev-race", "wks-1", "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 := NewSession(newFakeConn(), "dev-shared", "wks-1", "0.1.0", reg, nil) + sess := newTestSession(newFakeConn(), "dev-shared", "wks-1", "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 := NewSession(newFakeConn(), "late", "workspace", "0.1.0", r, nil) + s := newTestSession(newFakeConn(), "late", "workspace", "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(NewSession(newFakeConn(), "removed", "workspace", "0.1.0", r, nil)) + r.Register(newTestSession(newFakeConn(), "removed", "workspace", "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(NewSession(newFakeConn(), id, "workspace", "0.1.0", r, nil)) + r.Register(newTestSession(newFakeConn(), id, "workspace", "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/routes.go b/services/core/internal/runtimegateway/routes.go index d0c098c1e..c8f2dcf51 100644 --- a/services/core/internal/runtimegateway/routes.go +++ b/services/core/internal/runtimegateway/routes.go @@ -9,9 +9,8 @@ import ( // // GET /agent-daemon/ws — daemon dial-in (WS upgrade) // POST /agent-daemon/bootstrap — daemon first-call to fetch wsUrl + heartbeat cadence -// GET /agent-daemon/device-status — daemon self-check // -// All three accept the daemon credential described in +// Both accept the daemon credential described in // contracts/agents-api/machine-api.md. func RegisterRoutes(r chi.Router, h *Handler) { if h == nil { @@ -20,6 +19,5 @@ func RegisterRoutes(r chi.Router, h *Handler) { r.Route("/agent-daemon", func(r chi.Router) { r.Get("/ws", h.WS) r.Post("/bootstrap", h.Bootstrap) - r.Get("/device-status", h.DeviceStatus) }) } diff --git a/services/core/internal/runtimegateway/runtime_prepare_test.go b/services/core/internal/runtimegateway/runtime_prepare_test.go index f0beea586..3402a2d87 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 := NewSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "tenant", "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 := NewSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "tenant", "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 := NewSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "tenant", "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 := NewSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "tenant", "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 := NewSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "tenant", "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 := NewSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "tenant", "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 := NewSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "tenant", "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 := NewSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "tenant", "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 := NewSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "tenant", "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 := NewSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "tenant", "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 f61e2c396..1ec05993d 100644 --- a/services/core/internal/runtimegateway/session.go +++ b/services/core/internal/runtimegateway/session.go @@ -81,12 +81,6 @@ type Session struct { log SessionLogger reg *Registry - // owner is non-nil in multi-pod mode. It fences this WebSocket - // against the DB owner row so stale connections from an older pod - // cannot keep handling prompts after a reconnect claimed a newer - // generation. - owner *ownerLease - // heartbeat persists daemon-advertised capability snapshots. heartbeat HeartbeatTouch credentialHash string @@ -140,12 +134,10 @@ type Session struct { closed chan struct{} } -// NewSessionWithOwner wires a freshly-upgraded WS connection into a Session -// with an optional DB-backed owner lease. Multi-pod deployments pass the lease -// returned by ClaimAgentDaemonDeviceOwner so heartbeats can fence stale -// connections. 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 NewSessionWithOwner(conn WSConn, deviceID, workspaceID, daemonVersion string, reg *Registry, log SessionLogger, owner *ownerLease) *Session { +// 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 { now := time.Now() return &Session{ DeviceID: deviceID, @@ -155,7 +147,6 @@ func NewSessionWithOwner(conn WSConn, deviceID, workspaceID, daemonVersion strin conn: conn, log: log, reg: reg, - owner: owner, lastSeenAt: now, subs: map[string]*Subscription{}, preparations: map[string]*preparationSubscription{}, @@ -250,7 +241,6 @@ func (s *Session) Close(reason string) { s.closeWorkspaceWrites() s.closeCapabilities() s.closeWorkspaceExports() - s.releaseOwnerLease() }) } @@ -380,9 +370,6 @@ func (s *Session) readLoop() { } if !s.receiptDraining() { s.markSeen() - if !s.renewOwnerLease() { - return - } } var env proto.Envelope @@ -400,48 +387,6 @@ func (s *Session) markSeen() { s.hbMu.Unlock() } -func (s *Session) renewOwnerLease() bool { - if s.owner == nil || s.owner.store == nil { - return true - } - now := time.Now().UTC() - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) - defer cancel() - _, ok, err := s.owner.store.RenewAgentDaemonDeviceOwner(ctx, runtimedevice.RenewOwner{ - DeviceID: s.owner.deviceID, - OwnerPodID: s.owner.ownerPodID, - Generation: s.owner.generation, - Now: now, - LeaseExpiresAt: now.Add(normalizeOwnerTTL(s.owner.ttl)), - }) - if err != nil { - s.log("agentdaemon gateway: owner lease renew failed device=%s generation=%d: %v", s.DeviceID, s.owner.generation, err) - s.Close("owner lease renew failed") - return false - } - if !ok { - s.log("agentdaemon gateway: owner lease lost device=%s generation=%d", s.DeviceID, s.owner.generation) - s.Close("owner lease lost to a newer connection") - return false - } - return true -} - -func (s *Session) releaseOwnerLease() { - if s.owner == nil || s.owner.store == nil { - return - } - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) - defer cancel() - if _, err := s.owner.store.ReleaseAgentDaemonDeviceOwner(ctx, runtimedevice.ReleaseOwner{ - DeviceID: s.owner.deviceID, - OwnerPodID: s.owner.ownerPodID, - Generation: s.owner.generation, - }); err != nil { - s.log("agentdaemon gateway: owner lease release failed device=%s generation=%d: %v", s.DeviceID, s.owner.generation, err) - } -} - func (s *Session) handleHeartbeat(env proto.Envelope) { var p proto.HeartbeatPayload if err := env.DecodePayload(&p); err != nil { diff --git a/services/core/internal/runtimegateway/session_test.go b/services/core/internal/runtimegateway/session_test.go index 86eaf4cd0..32cb3f7d0 100644 --- a/services/core/internal/runtimegateway/session_test.go +++ b/services/core/internal/runtimegateway/session_test.go @@ -14,15 +14,15 @@ import ( "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" ) -// NewSession builds an unowned Session over a test connection. -func NewSession(conn WSConn, deviceID, workspaceID, daemonVersion string, reg *Registry, log SessionLogger) *Session { +// 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 { if log == nil { log = func(string, ...any) {} } if reg == nil { reg = NewRegistry() } - return NewSessionWithOwner(conn, deviceID, workspaceID, daemonVersion, reg, log, nil) + return NewSession(conn, deviceID, workspaceID, daemonVersion, reg, log) } // fakeConn is the WSConn implementation used by session + registry @@ -158,7 +158,7 @@ func (f *fakeHeartbeatStore) waitDaemonHeartbeat(t *testing.T) runtimedevice.Hea func TestRegistry_RegisterAndLookup(t *testing.T) { reg := NewRegistry() - sess := NewSession(newFakeConn(), "dev-1", "wks-1", "0.1.0", reg, nil) + sess := newTestSession(newFakeConn(), "dev-1", "wks-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 := NewSession(newFakeConn(), "dev-1", "wks-1", "0.1.0", reg, nil) + old := newTestSession(newFakeConn(), "dev-1", "wks-1", "0.1.0", reg, nil) reg.Register(old) reg.AttachRun("run-1", old) - new := NewSession(newFakeConn(), "dev-1", "wks-1", "0.1.0", reg, nil) + new := newTestSession(newFakeConn(), "dev-1", "wks-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 := NewSession(newFakeConn(), "dev-1", "wks-1", "0.1.0", reg, nil) + old := newTestSession(newFakeConn(), "dev-1", "wks-1", "0.1.0", reg, nil) reg.Register(old) - new := NewSession(newFakeConn(), "dev-1", "wks-1", "0.1.0", reg, nil) + new := newTestSession(newFakeConn(), "dev-1", "wks-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 := NewSession(conn, "dev-1", "wks-1", "0.1.0", reg, nil) + sess := newTestSession(conn, "dev-1", "wks-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 := NewSession(conn, "dev-1", "wks-1", "0.1.0", reg, nil) + sess := newTestSession(conn, "dev-1", "wks-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 := NewSession(newFakeConn(), "device", "tenant", proto.Version, NewRegistry(), nil) + sess := newTestSession(newFakeConn(), "device", "tenant", 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 := NewSession(newFakeConn(), "device", "tenant", proto.Version, NewRegistry(), nil) + sess := newTestSession(newFakeConn(), "device", "tenant", 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 := NewSession(newFakeConn(), "dev-1", "wks-1", "0.1.0", reg, nil) + sess := newTestSession(newFakeConn(), "dev-1", "wks-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 := NewSession(conn, "dev-1", "wks-1", "0.1.0", reg, nil) + sess := newTestSession(conn, "dev-1", "wks-1", "0.1.0", reg, nil) sess.Start() defer sess.Close("test done") @@ -332,7 +332,7 @@ func TestSession_HeartbeatPersistsSupportedAgentKinds(t *testing.T) { reg := NewRegistry() conn := newFakeConn() heartbeat := newFakeHeartbeatStore() - sess := NewSession(conn, "dev-1", "wks-1", "0.1.0", reg, nil) + sess := newTestSession(conn, "dev-1", "wks-1", "0.1.0", reg, nil) sess.heartbeat = heartbeat sess.Start() defer sess.Close("test done") @@ -405,7 +405,7 @@ func TestSession_HeartbeatDoesNotInferCapabilities(t *testing.T) { reg := NewRegistry() conn := newFakeConn() heartbeat := newFakeHeartbeatStore() - sess := NewSession(conn, "device", "tenant", proto.Version, reg, nil) + sess := newTestSession(conn, "device", "tenant", 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 1fbece704..2cc9177b4 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 := NewSession(newFakeConn(), "device", "tenant", proto.Version, NewRegistry(), nil) + s := newTestSession(newFakeConn(), "device", "tenant", 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 := NewSession(newFakeConn(), "device", "tenant", proto.Version, NewRegistry(), nil) + s := newTestSession(newFakeConn(), "device", "tenant", 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 081d2332d..f66eb052c 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 := NewSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "tenant", "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 := NewSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "tenant", "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 := NewSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "tenant", "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/workspace_directory_test.go b/services/core/internal/runtimegateway/workspace_directory_test.go index 3f6ea3c0d..05704e9cb 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 := NewSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "tenant", "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 := NewSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "tenant", "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 dc1b0bd4d..595b23bf0 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 := NewSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "tenant", "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 := NewSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "tenant", "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 := NewSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "tenant", "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 f9df6a871..ed8089822 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 := NewSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "tenant", "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 := NewSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "tenant", "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 := NewSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "tenant", "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 := NewSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "tenant", "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 := NewSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "tenant", "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 := NewSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "tenant", "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 924566f94..7bd10a4f7 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 := NewSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "tenant", "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 := NewSession(newFakeConn(), "device", "tenant", "test", nil, nil) + s := newTestSession(newFakeConn(), "device", "tenant", "test", nil, nil) defer s.Close("test") ctx, cancel := context.WithCancel(t.Context()) done := make(chan error, 1) diff --git a/services/core/internal/sandbox/microsandbox/resources.go b/services/core/internal/sandbox/microsandbox/resources.go index ce586b389..4303c645b 100644 --- a/services/core/internal/sandbox/microsandbox/resources.go +++ b/services/core/internal/sandbox/microsandbox/resources.go @@ -28,7 +28,7 @@ func (p *Provider) Observe(ctx context.Context, target runtimeobs.Target) (runti Current Compute `json:"current"` } if json.Unmarshal(target.Instance.ProviderState, &state) != nil || state.Current.ID == "" { - // Suspension-disabled allocations predate durable compute receipts. A + // A disabled allocation has no compute receipt yet. A // deterministic name is not an incarnation fence, so do not sample it. if target.Instance.ComputePhase == "disabled" { return runtimeobs.Sample{}, runtimeobs.ErrUnavailable diff --git a/services/core/tests/integration/configuration_validation_public_test.go b/services/core/tests/integration/configuration_validation_public_test.go index 052d5aa40..4847f3af8 100644 --- a/services/core/tests/integration/configuration_validation_public_test.go +++ b/services/core/tests/integration/configuration_validation_public_test.go @@ -7,7 +7,6 @@ import ( "strings" "testing" - "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/agents" "github.com/MiniMax-AI/OpenAgentCore/services/core/internal/runtimedevice" "github.com/google/uuid" ) @@ -32,15 +31,6 @@ func TestAgentConfigurationValidationRejectsWithoutWritesPostgres(t *testing.T) client := pathIDClient{t: t, server: server} const lookup = `{"type":"function","name":"lookup","description":"Look up a value.","parameters":{"type":"object","properties":{"q":{"type":"string"}},"required":["q"]}}` agent := client.created(owner, "/v1/agents", `{"model":"config-model","tools":[`+lookup+`]}`) - // A record saved before the protocol checks existed. - saved := func(tools string) string { - return `{"model":"config-model","name":null,"instructions":null,"multi_agent":{"enabled":false,"max_concurrent_subagents":null},"reasoning":{},"service_tier":"auto","text":{"format":{"type":"text"},"verbosity":"medium"},"tools":` + tools + `}` - } - _, agentService := fixtureAgents(t, s) - legacy, err := agentService.Create(t.Context(), agents.CreateCommand{TenantID: ownerTenant, Metadata: map[string]string{}, Configuration: json.RawMessage(saved(`[{"type":"function","name":"lookup","description":"","parameters":{"type":"string"},"defer_loading":false}]`))}) - if err != nil { - t.Fatal(err) - } before := databaseDigest(t, pool) const tools = `'function', 'tool_search', 'programmatic_tool_calling', 'mcp', and 'web_search'.` @@ -110,14 +100,6 @@ func TestAgentConfigurationValidationRejectsWithoutWritesPostgres(t *testing.T) } } } - // A saved record from before the checks cannot execute; another tenant cannot see it. - schema := `{"error":{"message":"Invalid schema for function 'lookup': schema must be a JSON Schema of 'type: \"object\"', got 'type: \"string\"'.","type":"invalid_request_error","code":"invalid_request_error","param":null}}` + "\n" - if status, body := client.do(owner, http.MethodPost, "/v1/agents/sessions", "application/json", []byte(`{"agent_id":"`+legacy.ID+`","environment":{"type":"none"},"input":"hi"}`)); status != http.StatusBadRequest || body != schema { - t.Errorf("legacy saved Agent: %d %s", status, body) - } - if status, body := client.do(foreign, http.MethodPost, "/v1/agents/sessions", "application/json", []byte(`{"agent_id":"`+legacy.ID+`","environment":{"type":"none"},"input":"hi"}`)); status != http.StatusNotFound { - t.Errorf("foreign legacy saved Agent: %d %s", status, body) - } // K2 keeps the local configuration code; saved enabled web_search (TV-05) is // covered by TestSavedWebSearchPostgres. for _, request := range []struct{ path, body, message string }{ diff --git a/services/core/tests/integration/environments_test.go b/services/core/tests/integration/environments_test.go index 7dce3a10e..3628aea25 100644 --- a/services/core/tests/integration/environments_test.go +++ b/services/core/tests/integration/environments_test.go @@ -262,7 +262,7 @@ func TestEnvironmentDeletionHidesWithoutDestroyingOwnership(t *testing.T) { } } -func TestEnvironmentAbsentForNoneAndLegacySnapshots(t *testing.T) { +func TestEnvironmentAbsentForNoneAndEmptyConfigurations(t *testing.T) { s, _ := testStore(t) ctx := context.Background() tenant := uuid.NewString() diff --git a/services/core/tests/integration/item_reads_test.go b/services/core/tests/integration/item_reads_test.go index 27585bf45..372e7920a 100644 --- a/services/core/tests/integration/item_reads_test.go +++ b/services/core/tests/integration/item_reads_test.go @@ -121,7 +121,7 @@ func TestItemProjectionFailureRollsBackJournalAndAggregateRecovers(t *testing.T) s, _ := testStore(t) journal := executionOwner(t, s).Sessions tenant := uuid.NewString() - session, _ := s.CreateSession(ctx, tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "legacy"}) + session, _ := s.CreateSession(ctx, tenant, sessions.CreateSession{Creator: FixtureCreator(), Engine: "codex", IdempotencyKey: "aggregate"}) input, err := sendMessage(ctx, s, tenant, session.ID, "input", messageText("test")) if err != nil { t.Fatal(err) @@ -142,12 +142,12 @@ func TestItemProjectionFailureRollsBackJournalAndAggregateRecovers(t *testing.T) if err != nil || len(page.Items) != 1 { t.Fatal(page, err) } - _, err = transitionTurn(ctx, s, tenant, session.ID, input.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnInProgress, Status: sessions.TurnCompleted, Outcome: json.RawMessage(`{"done":{"content":"legacy answer","metadata":{"private":"SECRET"}}}`)}) + _, err = transitionTurn(ctx, s, tenant, session.ID, input.TurnID, sessions.TurnTransition{ExpectedStatus: sessions.TurnInProgress, Status: sessions.TurnCompleted, Outcome: json.RawMessage(`{"done":{"content":"aggregate answer","metadata":{"private":"SECRET"}}}`)}) if err != nil { t.Fatal(err) } current, err := sessionAdapter(s).ListItems(ctx, tenant, session.ID, "", 100, true) - if err != nil || len(current.Items) != 2 || *current.Items[1].Content[0].Text != "legacy answer" { + if err != nil || len(current.Items) != 2 || *current.Items[1].Content[0].Text != "aggregate answer" { t.Fatal(current, err) } } @@ -184,7 +184,7 @@ func TestReceiptOnlyTextRecoversWithoutInventingCompletion(t *testing.T) { } } -func TestLegacyFailureRetainsPartialAnswerAcrossRecovery(t *testing.T) { +func TestFailedTurnRetainsPartialAnswerAcrossRecovery(t *testing.T) { ctx := context.Background() s, _ := testStore(t) journal := executionOwner(t, s).Sessions diff --git a/services/core/tests/integration/project_scopes_fixture_test.go b/services/core/tests/integration/project_scopes_fixture_test.go index 574f876f9..461fab0ed 100644 --- a/services/core/tests/integration/project_scopes_fixture_test.go +++ b/services/core/tests/integration/project_scopes_fixture_test.go @@ -13,7 +13,7 @@ import ( var ErrProjectScopeConflict = errors.New("fixture project conflicts with a persisted execution scope") -// EnsureProjectScopes establishes execution scopes for legacy resource fixtures. +// EnsureProjectScopes establishes execution scopes for resource fixtures. func (s *Store) EnsureProjectScopes(ctx context.Context, scopes []identity.ProjectScope) error { validated, err := identity.ProjectScopes(scopes) if err != nil { diff --git a/services/core/tests/integration/runtime_node_lifecycle_fixture_test.go b/services/core/tests/integration/runtime_node_lifecycle_fixture_test.go index c1445835f..90fbc8c5a 100644 --- a/services/core/tests/integration/runtime_node_lifecycle_fixture_test.go +++ b/services/core/tests/integration/runtime_node_lifecycle_fixture_test.go @@ -161,7 +161,7 @@ func (f *nodeIsolationFixture) session(node string, initialize bool) (string, se input.InitialFiles = []environmentconfig.InitialFile{{Type: "inline", Path: "/workspace/seed", Data: []byte("retained")}} } // Placement is automatic and generation readiness is current-connection - // authority. Do not merely change the legacy provider_ready projection. + // authority. Do not merely change the provider_ready projection. type presence struct { id, connection string epoch uint64 diff --git a/services/core/tests/integration/sandbox_snapshot_plan_test.go b/services/core/tests/integration/sandbox_snapshot_plan_test.go index c488917ab..10e5340b9 100644 --- a/services/core/tests/integration/sandbox_snapshot_plan_test.go +++ b/services/core/tests/integration/sandbox_snapshot_plan_test.go @@ -83,23 +83,23 @@ func TestSandboxSnapshotFreshPlan(t *testing.T) { func assertSandboxSnapshotEquivalent(t *testing.T, pool *pgxpool.Pool) { t.Helper() query := sandboxSnapshotSQL(t) - legacy := strings.Replace(query, "WITH deployment AS MATERIALIZED (SELECT * FROM runtime_deployment WHERE singleton = true LIMIT 1),\nobserved AS", "WITH observed AS", 1) - legacy = strings.ReplaceAll(legacy, "CROSS JOIN deployment d", "CROSS JOIN runtime_deployment d") - legacy = strings.TrimSuffix(legacy, "\nWHERE d.singleton = true\nLIMIT 1") + direct := strings.Replace(query, "WITH deployment AS MATERIALIZED (SELECT * FROM runtime_deployment WHERE singleton = true LIMIT 1),\nobserved AS", "WITH observed AS", 1) + direct = strings.ReplaceAll(direct, "CROSS JOIN deployment d", "CROSS JOIN runtime_deployment d") + direct = strings.TrimSuffix(direct, "\nWHERE d.singleton = true\nLIMIT 1") query = strings.Replace(query, "clock_timestamp() AS as_of", "(SELECT as_of FROM snapshot_time) AS as_of", 1) - legacy = strings.Replace(legacy, "clock_timestamp() AS as_of", "(SELECT as_of FROM snapshot_time) AS as_of", 1) + direct = strings.Replace(direct, "clock_timestamp() AS as_of", "(SELECT as_of FROM snapshot_time) AS as_of", 1) tx, err := pool.Begin(t.Context()) if err != nil { t.Fatal(err) } defer tx.Rollback(t.Context()) - // Avoid paying the legacy planner's JIT cost in this result-equivalence check. + // Avoid paying the direct planner's JIT cost in this result-equivalence check. // The separate fresh-plan test keeps JIT enabled. if _, err = tx.Exec(t.Context(), "SET LOCAL jit=off"); err != nil { t.Fatal(err) } var same bool - err = tx.QueryRow(t.Context(), "WITH snapshot_time AS MATERIALIZED (SELECT clock_timestamp() AS as_of) SELECT (SELECT to_jsonb(s) FROM ("+query+") s) IS NOT DISTINCT FROM (SELECT to_jsonb(s) FROM ("+legacy+") s)").Scan(&same) + err = tx.QueryRow(t.Context(), "WITH snapshot_time AS MATERIALIZED (SELECT clock_timestamp() AS as_of) SELECT (SELECT to_jsonb(s) FROM ("+query+") s) IS NOT DISTINCT FROM (SELECT to_jsonb(s) FROM ("+direct+") s)").Scan(&same) if err != nil || !same { t.Fatal("singleton annotation changed snapshot", same, err) } diff --git a/services/core/tests/integration/saved_web_search_public_test.go b/services/core/tests/integration/saved_web_search_public_test.go index a0560a5a9..630513dde 100644 --- a/services/core/tests/integration/saved_web_search_public_test.go +++ b/services/core/tests/integration/saved_web_search_public_test.go @@ -114,17 +114,17 @@ func TestSavedWebSearchPostgres(t *testing.T) { readBack(tc.name, updated, body) } - // A disabled record saved before this batch reads unchanged and is admitted with - // the same frozen Session tool (W7). + // A saved disabled search reads unchanged and is admitted with the same frozen + // Session tool (W7). _, agentService := fixtureAgents(t, s) - legacy, err := agentService.Create(t.Context(), agents.CreateCommand{TenantID: ownerTenant, Metadata: map[string]string{}, Configuration: json.RawMessage( + savedDisabled, err := agentService.Create(t.Context(), agents.CreateCommand{TenantID: ownerTenant, Metadata: map[string]string{}, Configuration: json.RawMessage( `{"model":"search-model","name":null,"instructions":null,"multi_agent":{"enabled":false,"max_concurrent_subagents":null},"reasoning":{},"service_tier":"auto","text":{"format":{"type":"text"},"verbosity":"medium"},"tools":[{"type":"web_search","mode":"disabled","context_size":"medium","allowed_domains":[],"location":null}]}`)}) if err != nil { t.Fatal(err) } - status, body := client.do(owner, http.MethodGet, "/v1/agents/"+legacy.ID, "", nil) - legacyTool := tools("legacy", status, body, http.StatusOK, `{"type":"web_search","mode":"disabled","context_size":"medium","allowed_domains":[],"location":null}`) - readBack("legacy", legacy.ID, body) + status, body := client.do(owner, http.MethodGet, "/v1/agents/"+savedDisabled.ID, "", nil) + disabledTool := tools("disabled", status, body, http.StatusOK, `{"type":"web_search","mode":"disabled","context_size":"medium","allowed_domains":[],"location":null}`) + readBack("disabled", savedDisabled.ID, body) // sessionTool returns the first frozen Session tool in canonical form; Session // snapshots keep their own key order. sessionTool := func(name, session string) string { @@ -141,8 +141,8 @@ func TestSavedWebSearchPostgres(t *testing.T) { } return canonicalTool(t, value.Agent.Tools[0]) } - if got := sessionTool("legacy", client.created(owner, "/v1/agents/sessions", `{"agent_id":"`+legacy.ID+`","environment":{"type":"none"},"input":"hi"}`)); got != canonicalTool(t, json.RawMessage(legacyTool)) { - t.Fatalf("legacy Session tool %s, want %s", got, legacyTool) + if got := sessionTool("disabled", client.created(owner, "/v1/agents/sessions", `{"agent_id":"`+savedDisabled.ID+`","environment":{"type":"none"},"input":"hi"}`)); got != canonicalTool(t, json.RawMessage(disabledTool)) { + t.Fatalf("disabled Session tool %s, want %s", got, disabledTool) } // A same-key retry of a Session that recorded its creation request recovers it @@ -183,7 +183,7 @@ func TestSavedWebSearchPostgres(t *testing.T) { for tool, want := range map[string]string{`{"type":"web_search"}`: live, `{"type":"web_search","mode":null}`: live, `{"type":"web_search","mode":"cached"}`: cached} { for _, request := range []string{ `{"agent":{"model":"m","tools":[` + tool + `]},"environment":{"type":"none"},"input":"hi"}`, - `{"agent_id":"` + legacy.ID + `","agent":{"tools":[` + tool + `]},"environment":{"type":"none"},"input":"hi"}`, + `{"agent_id":"` + savedDisabled.ID + `","agent":{"tools":[` + tool + `]},"environment":{"type":"none"},"input":"hi"}`, } { if got := sessionTool(request, client.created(owner, "/v1/agents/sessions", request)); got != canonicalTool(t, json.RawMessage(want)) { t.Errorf("inline %s: Session tool %s, want %s", request, got, want) diff --git a/services/core/tests/integration/session_diagnostics_test.go b/services/core/tests/integration/session_diagnostics_test.go index 6a21d8671..7f097b231 100644 --- a/services/core/tests/integration/session_diagnostics_test.go +++ b/services/core/tests/integration/session_diagnostics_test.go @@ -52,7 +52,7 @@ func TestDiagnosticItemReceiptSettlementAndReplay(t *testing.T) { if err := journal.AppendTurnEvents(t.Context(), tenant, session.ID, receipt.TurnID, 2, []sessions.ExecutionEvent{after}); err != nil { t.Fatal(err) } - // A terminal legacy Item with unknown settlement must remain unknown even on a repeated upsert. + // A terminal Item with unknown settlement must remain unknown even on a repeated upsert. runtimeSuspensionSQL(t, s.pool, "UPDATE session_items SET settled_at=NULL WHERE id=$1", item.ItemID) if err := journal.AppendTurnEvents(t.Context(), tenant, session.ID, receipt.TurnID, 3, []sessions.ExecutionEvent{after}); err != nil { t.Fatal(err) diff --git a/services/core/tests/integration/session_write_audit_test.go b/services/core/tests/integration/session_write_audit_test.go index eb5b167d6..907dea345 100644 --- a/services/core/tests/integration/session_write_audit_test.go +++ b/services/core/tests/integration/session_write_audit_test.go @@ -209,16 +209,16 @@ func TestSessionWriteAuditInitialInputAndHistoricalReplay(t *testing.T) { t.Fatal(created, err) } sessionAuditCount(t, s, tenant, 1) - input.IdempotencyKey = "legacy" - legacy, err := s.CreateSession(t.Context(), tenant, input) + input.IdempotencyKey = "second" + second, err := s.CreateSession(t.Context(), tenant, input) if err != nil { t.Fatal(err) } - if err := sessionService(t, s).AuditSessionOperation(sessionAuditContext(t, tenant, "b"), sessions.AuditSessionOperationCommand{TenantID: tenant, SessionID: legacy.ID, Action: "create"}); err != nil { + if err := sessionService(t, s).AuditSessionOperation(sessionAuditContext(t, tenant, "b"), sessions.AuditSessionOperationCommand{TenantID: tenant, SessionID: second.ID, Action: "create"}); err != nil { t.Fatal(err) } var owners int - if err := s.pool.QueryRow(t.Context(), "SELECT count(*) FROM write_audit_owners WHERE tenant_id=$1 AND resource_id=$2", tenant, legacy.ID).Scan(&owners); err != nil || owners != 0 { + if err := s.pool.QueryRow(t.Context(), "SELECT count(*) FROM write_audit_owners WHERE tenant_id=$1 AND resource_id=$2", tenant, second.ID).Scan(&owners); err != nil || owners != 0 { t.Fatal("replay attributed historical resource", owners, err) } sessionAuditCount(t, s, tenant, 2) diff --git a/website/.vitepress/config.mts b/website/.vitepress/config.mts index 0efeab0bf..a2c8bb1b6 100644 --- a/website/.vitepress/config.mts +++ b/website/.vitepress/config.mts @@ -6,7 +6,7 @@ import { defineConfig, type SiteConfig } from 'vitepress' import { mermaidFences } from './mermaid.mts' import { repositoryLinks } from './repository-links.mts' import { siteBase } from './site-base.mts' -import { docsNav, docsSidebar, legacyRedirects, pageTitle, readDocsJson, repoRoot, summary } from './docs-nav.mts' +import { docsNav, docsSidebar, pageTitle, readDocsJson, repoRoot, summary } from './docs-nav.mts' // Pages outside this package resolve Vue from here, not from the repository root. const require = createRequire(import.meta.url) @@ -129,7 +129,6 @@ export default defineConfig({ buildEnd: (site) => { writeMarkdownCopies(site) - writeLegacyRedirects(site) copyFileSync(resolve(repoRoot, 'website/install.sh'), resolve(site.outDir, 'install.sh')) }, }) @@ -158,16 +157,3 @@ function writeMarkdownCopies(site: SiteConfig) { } writeFileSync(resolve(site.outDir, 'llms.txt'), lines.join('\n')) } - -/** Writes static redirects for the non-Markdown legacy paths declared in docs.json. */ -function writeLegacyRedirects(site: SiteConfig) { - for (const { from, to } of legacyRedirects()) { - const target = resolve(site.outDir, `${from}.html`) - const url = `${site.site.base}${to.replace(/^\//, '')}` - mkdirSync(dirname(target), { recursive: true }) - writeFileSync( - target, - `Redirecting…${url}\n`, - ) - } -} diff --git a/website/.vitepress/docs-nav.mts b/website/.vitepress/docs-nav.mts index 6f17ebd48..da6fe58fe 100644 --- a/website/.vitepress/docs-nav.mts +++ b/website/.vitepress/docs-nav.mts @@ -16,7 +16,6 @@ interface DocsTab { interface DocsJson { navigation: { tabs: DocsTab[] } - redirects?: { source: string; destination: string }[] } export function readDocsJson(): DocsJson { @@ -106,13 +105,3 @@ export function summary(page: string): string { const sentence = /^(.+?(?:[。!?]|[.!?](?=\s|$)))/.exec(text)?.[1] ?? text return sentence.length > 180 ? `${sentence.slice(0, 177).trimEnd()}…` : sentence } - -/** - * Redirects in docs.json that point a legacy path (such as `README`) at a page. - * `.md` sources are served by the raw Markdown copies, so they are skipped. - */ -export function legacyRedirects(): { from: string; to: string }[] { - return (readDocsJson().redirects ?? []) - .filter((r) => !r.source.endsWith('.md')) - .map((r) => ({ from: r.source.replace(/^\//, ''), to: pageLink(r.destination.replace(/^\//, '')) })) -} diff --git a/website/tests/dist.test.mjs b/website/tests/dist.test.mjs index 44f77f9a7..43ec8dda6 100644 --- a/website/tests/dist.test.mjs +++ b/website/tests/dist.test.mjs @@ -1,12 +1,12 @@ // Checks a finished build: run `pnpm build` first. Every docs.json page has -// an HTML page and a raw Markdown copy, legacy paths redirect, both landing -// pages exist and llms.txt lists every page. +// an HTML page and a raw Markdown copy, both landing pages exist and llms.txt +// lists every page. import assert from 'node:assert/strict' import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs' import { resolve } from 'node:path' import test from 'node:test' import { fileURLToPath } from 'node:url' -import { legacyRedirects, listedPages } from '../.vitepress/docs-nav.mts' +import { listedPages } from '../.vitepress/docs-nav.mts' const dist = fileURLToPath(new URL('../.vitepress/dist/', import.meta.url)) const pages = listedPages() @@ -51,14 +51,6 @@ test('generated navigation and assets stay under the site base and resolve to fi assert.ok(checked > 0, 'generated pages must contain local navigation and assets') }) -test('legacy paths redirect', () => { - for (const { from, to } of legacyRedirects()) { - const source = readFileSync(resolve(dist, `${from}.html`), 'utf8') - const target = `${base}${to.replace(/^\//, '')}` - assert.ok(source.includes(`content="0; url=${target}"`), `${from} → ${target}`) - } -}) - test('llms.txt lists every page', () => { const llms = readFileSync(resolve(dist, 'llms.txt'), 'utf8') for (const page of pages) assert.ok(llms.includes(`](${base}${page}.md)`), `${page} in llms.txt uses ${base}`) diff --git a/website/tests/docs-nav.test.mjs b/website/tests/docs-nav.test.mjs index 234e9b931..3f98db157 100644 --- a/website/tests/docs-nav.test.mjs +++ b/website/tests/docs-nav.test.mjs @@ -4,7 +4,7 @@ import { existsSync } from 'node:fs' import { resolve } from 'node:path' import test from 'node:test' import { translatedSource } from '../.vitepress/locales.mts' -import { docsNav, docsSidebar, legacyRedirects, listedPages, pageLink, pageTitle, readDocsJson, repoRoot, summary, unquote } from '../.vitepress/docs-nav.mts' +import { docsNav, docsSidebar, listedPages, pageLink, pageTitle, readDocsJson, repoRoot, summary, unquote } from '../.vitepress/docs-nav.mts' test('page ids map to clean routes', () => { assert.equal(pageLink('docs/getting-started/index'), '/docs/getting-started/') @@ -67,12 +67,3 @@ test('summaries are one sentence taken from the page opening', () => { assert.ok(summary('docs/architecture').length > 0) for (const page of listedPages()) assert.ok(summary(page).length > 0, `${page} has a summary`) }) - -test('legacy README paths redirect to section indexes', () => { - const redirects = legacyRedirects() - assert.ok(redirects.length > 0) - for (const { from, to } of redirects) { - assert.ok(!from.endsWith('.md'), `${from} is not a Markdown copy`) - assert.ok(to.startsWith('/'), `${to} is a site route`) - } -})