From 7ac3381d8fcb3d826f76e44f004a10a7cad66299 Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 22 Sep 2026 23:30:51 +0800 Subject: [PATCH 1/9] feat: expose Core startup configuration --- apps/web/e2e/agents-lifecycle.spec.ts | 16 +- apps/web/e2e/core-connection.spec.ts | 103 ++++++++++ apps/web/e2e/fixture-core.mjs | 24 +++ apps/web/src/App.tsx | 83 ++++++-- apps/web/src/features/system/SystemView.css | 166 ++++++++++++++- .../src/features/system/SystemView.test.tsx | 171 ++++++++-------- apps/web/src/features/system/SystemView.tsx | 193 ++++++++++++++---- contracts/agents-api/README.md | 4 + contracts/agents-api/openapi.yaml | 143 +++++++++++++ contracts/agents-api/startup-configuration.md | 31 +++ .../agents-api/v1/startup_configuration.go | 35 ++++ docs/web/README.md | 14 +- docs/web/README.zh-CN.md | 7 +- docs/web/architecture.md | 4 +- docs/web/protocol-coverage.md | 9 +- packages/agents-client/src/client.ts | 83 ++++++++ .../src/startup-configuration.test.ts | 91 +++++++++ packages/agents-client/src/types.ts | 28 +++ services/agents-api/README.md | 2 + .../cmd/server/execution_options.go | 49 ++++- .../cmd/server/execution_options_test.go | 48 +++++ services/agents-api/cmd/server/main.go | 13 +- .../agents-api/cmd/server/managed_runtimes.go | 37 ++-- .../cmd/server/startup_configuration.go | 29 +++ .../cmd/server/startup_configuration_test.go | 43 ++++ services/agents-api/internal/api/handler.go | 30 +-- .../internal/api/startup_configuration.go | 47 +++++ .../api/startup_configuration_test.go | 75 +++++++ .../agents-api/internal/engine/profile.go | 11 + .../internal/engine/profile_test.go | 17 +- 30 files changed, 1401 insertions(+), 205 deletions(-) create mode 100644 contracts/agents-api/startup-configuration.md create mode 100644 contracts/agents-api/v1/startup_configuration.go create mode 100644 packages/agents-client/src/startup-configuration.test.ts create mode 100644 services/agents-api/cmd/server/startup_configuration.go create mode 100644 services/agents-api/cmd/server/startup_configuration_test.go create mode 100644 services/agents-api/internal/api/startup_configuration.go create mode 100644 services/agents-api/internal/api/startup_configuration_test.go diff --git a/apps/web/e2e/agents-lifecycle.spec.ts b/apps/web/e2e/agents-lifecycle.spec.ts index d179f0e0f..08e3f41c7 100644 --- a/apps/web/e2e/agents-lifecycle.spec.ts +++ b/apps/web/e2e/agents-lifecycle.spec.ts @@ -2373,11 +2373,18 @@ test("presents Dashboard page-chain results and System boundaries without extra const system = page.locator(".system-page"); await expect(system.getByRole("listitem").filter({ hasText: "Core API" })).toContainText("Available"); await expect(system.getByRole("listitem").filter({ hasText: "Vaults" })).toContainText("Available"); - await expect(system.getByRole("listitem").filter({ hasText: "Self-hosted" })).toContainText("Enabled"); - await expect(system.getByRole("listitem").filter({ hasText: "Runtime status" })).toContainText("Cannot be pre-checked"); - await expect(system.getByRole("listitem")).toHaveCount(4); + await expect(system.getByRole("listitem").filter({ hasText: "Default harness" })).toContainText("Codex"); + await expect(system.getByRole("listitem").filter({ hasText: "Managed sandbox" })).toContainText("Docker"); + await expect(system.getByRole("listitem").filter({ hasText: "LLM endpoints" })).toContainText("1/2 configured"); + await expect(system.getByRole("listitem")).toHaveCount(5); + await expect(system).toContainText("Configured for this process"); + await expect(system).toContainText("Daemon gateway"); + await expect(system).toContainText("Operator LLM endpoint: configured"); + await expect(system).toContainText("Known by this build but not enabled for this process"); + await expect(system).toContainText("Runtime connection, native binary availability, sandbox health, and model execution belong to the relevant Session or Environment"); await expect(system).not.toContainText("Source Files"); await expect(system).not.toContainText("Public capability surface"); + await expect(system).not.toContainText("Cannot be pre-checked"); const beforeSystemRefresh = await fixtureRequests(request); const systemRefresh = system.getByRole("button", { name: "Refresh System status" }); @@ -2392,6 +2399,9 @@ test("presents Dashboard page-chain results and System boundaries without extra const afterSystemRefresh = await fixtureRequests(request); expect(count(afterSystemRefresh, "/v1/agents")).toBe(count(beforeSystemRefresh, "/v1/agents") + 1); expect(count(afterSystemRefresh, "/v1/agents/sessions")).toBe(count(beforeSystemRefresh, "/v1/agents/sessions") + 1); + expect(count(afterSystemRefresh, "/v1/agents/core/startup-configuration")).toBe( + count(beforeSystemRefresh, "/v1/agents/core/startup-configuration") + 1, + ); for (const path of detailPaths) expect(count(afterSystemRefresh, path)).toBe(count(beforeSystemRefresh, path)); await attachScreenshot(page, testInfo, "desktop-system-contract-boundary"); diff --git a/apps/web/e2e/core-connection.spec.ts b/apps/web/e2e/core-connection.spec.ts index e357bcd74..401a311d3 100644 --- a/apps/web/e2e/core-connection.spec.ts +++ b/apps/web/e2e/core-connection.spec.ts @@ -8,6 +8,10 @@ interface ProbeInstrumentationWindow extends Window { __probeCallCount?: number; __resolveFirstProbe?: (() => void) | null; __stalledProbeCallCount?: number; + __holdNextLocalStartup?: boolean; + __oldStartupAbortCount?: number; + __resolveOldStartup?: (() => void) | null; + __resolveNewStartup?: (() => void) | null; } async function resetFixture(request: APIRequestContext) { @@ -260,6 +264,105 @@ test("switches real connection modes and fences stale probes when the draft chan await expect(reopened.getByRole("alert")).toHaveCount(0); }); +test("clears startup configuration synchronously and fences a stale read when the Core changes", async ({ + page, + request, +}) => { + await page.addInitScript(() => { + const target = window as ProbeInstrumentationWindow; + const originalFetch = window.fetch.bind(window); + const startupResponse = (defaultHarness: "codex" | "claude_sdk", provider: "docker" | "microsandbox") => new Response(JSON.stringify({ + object: "agents.core.startup_configuration", + schema_version: 1, + supported: { + harnesses: ["claude_sdk", "codex", "mcode"], + managed_sandbox_providers: ["docker", "microsandbox"], + }, + configured: { + default_harness: defaultHarness, + enabled_harnesses: [defaultHarness], + daemon_gateway: true, + self_hosted: true, + managed_sandbox: { enabled: true, provider, maintenance: false }, + model_providers: [{ harness: defaultHarness, endpoint_configured: true }], + }, + }), { status: 200, headers: { "Content-Type": "application/json" } }); + + target.__holdNextLocalStartup = false; + target.__oldStartupAbortCount = 0; + target.__resolveOldStartup = null; + target.__resolveNewStartup = null; + window.fetch = (input, init) => { + const value = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; + const candidate = new URL(value, window.location.href); + if (!candidate.pathname.endsWith("/v1/agents/core/startup-configuration")) return originalFetch(input, init); + const authorization = new Headers(init?.headers).get("Authorization"); + if (target.__holdNextLocalStartup && authorization == null) { + target.__holdNextLocalStartup = false; + init?.signal?.addEventListener("abort", () => { + target.__oldStartupAbortCount = (target.__oldStartupAbortCount ?? 0) + 1; + }, { once: true }); + return new Promise((resolve) => { + target.__resolveOldStartup = () => resolve(startupResponse("codex", "docker")); + }); + } + if (authorization === "Bearer replacement-token") { + return new Promise((resolve) => { + target.__resolveNewStartup = () => resolve(startupResponse("claude_sdk", "microsandbox")); + }); + } + return originalFetch(input, init); + }; + }); + + await boot(page, request); + await page.getByRole("button", { name: "System", exact: true }).click(); + const system = page.locator(".system-page"); + await expect(system.getByRole("listitem").filter({ hasText: "Default harness" })).toContainText("Codex"); + await page.evaluate(() => { + (window as ProbeInstrumentationWindow).__holdNextLocalStartup = true; + }); + await system.getByRole("button", { name: "Refresh System status" }).click(); + await expect.poll(() => page.evaluate(() => typeof (window as ProbeInstrumentationWindow).__resolveOldStartup)).toBe("function"); + + const { dialog } = await openConnection(page); + await dialog.getByRole("radio", { name: /Other compatible Core/ }).click(); + await dialog.getByLabel("Compatible Core base URL").fill(`${new URL(page.url()).origin}/v1`); + await dialog.getByLabel("Bearer token").fill("replacement-token"); + await dialog.getByRole("button", { name: "Apply connection" }).click(); + + const defaultHarness = system.getByRole("listitem").filter({ hasText: "Default harness" }); + await expect(defaultHarness).toContainText("Checking…"); + await expect(system).not.toContainText("Configured for this process"); + await expect.poll(() => page.evaluate(() => (window as ProbeInstrumentationWindow).__oldStartupAbortCount ?? 0)).toBe(1); + await expect.poll(() => page.evaluate(() => typeof (window as ProbeInstrumentationWindow).__resolveNewStartup)).toBe("function"); + await page.evaluate(() => (window as ProbeInstrumentationWindow).__resolveNewStartup?.()); + await expect(defaultHarness).toContainText("Claude SDK"); + await expect(system.getByRole("listitem").filter({ hasText: "Managed sandbox" })).toContainText("Microsandbox"); + + await page.evaluate(() => (window as ProbeInstrumentationWindow).__resolveOldStartup?.()); + await expect(defaultHarness).toContainText("Claude SDK"); + await expect(system).not.toContainText("Docker · Maintenance"); +}); + +for (const status of [404, 405]) { + test(`shows startup configuration as unsupported when an older Core returns ${status}`, async ({ page, request }) => { + await page.route("**/v1/agents/core/startup-configuration", (route) => route.fulfill({ + status, + contentType: "application/json", + body: JSON.stringify({ error: { code: "unsupported", message: "Unavailable." } }), + })); + await boot(page, request); + await page.getByRole("button", { name: "System", exact: true }).click(); + + const system = page.locator(".system-page"); + await expect(system.getByRole("listitem").filter({ hasText: "Default harness" })).toContainText("Not exposed"); + await expect(system).toContainText("This Core version does not expose the startup configuration extension"); + await expect(system).not.toContainText("Configured for this process"); + await expect(system).not.toContainText("Checking…"); + }); +} + test("announces loading, authenticated access, and each safe failure state from one GET", async ({ page, request, diff --git a/apps/web/e2e/fixture-core.mjs b/apps/web/e2e/fixture-core.mjs index b950ef6b6..bedeb9584 100644 --- a/apps/web/e2e/fixture-core.mjs +++ b/apps/web/e2e/fixture-core.mjs @@ -879,6 +879,30 @@ const server = http.createServer(async (request, response) => { const body = request.method === "GET" || request.method === "DELETE" ? undefined : await readJson(request); recordRequest(request, url, body); + if (request.method === "GET" && url.pathname === "/v1/agents/core/startup-configuration") { + if (url.search) return sendError(response, 400, "Fixture startup configuration does not accept query parameters."); + response.setHeader("cache-control", "no-store"); + return sendJson(response, { + object: "agents.core.startup_configuration", + schema_version: 1, + supported: { + harnesses: ["claude_sdk", "codex", "mcode"], + managed_sandbox_providers: ["docker", "microsandbox"], + }, + configured: { + default_harness: "codex", + enabled_harnesses: ["claude_sdk", "codex"], + daemon_gateway: true, + self_hosted: true, + managed_sandbox: { enabled: true, provider: "docker", maintenance: false }, + model_providers: [ + { harness: "claude_sdk", endpoint_configured: false }, + { harness: "codex", endpoint_configured: true }, + ], + }, + }); + } + if (url.pathname === "/v1/vaults") { if (request.method === "GET") return sendJson(response, page(state.vaults)); if (request.method === "POST") { diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index e4a7d6f85..a5d093046 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -6,6 +6,7 @@ import type { AgentCore, AgentSession, AgentTurn, + CoreStartupConfiguration, CreateAgentInput, CreateEnvironmentTemplateInput, FunctionResultInput, @@ -79,7 +80,6 @@ import { upsertTurn, } from "./features/sessions/turns/turn-state"; import { SystemView } from "./features/system/SystemView"; -import type { SourceFilesOperations } from "./features/system/SourceFilesPanel"; import { VaultsView, type VaultOperations } from "./features/vaults/VaultsView"; import { deriveSessionVaultPlan, loadVaultCatalog, type VaultCatalog } from "./features/vaults/vault-catalog"; import { requestVaultCreate } from "./features/vaults/vault-operations"; @@ -249,6 +249,9 @@ export function App() { const [vaultCollectionState, setVaultCollectionState] = useState("connecting"); const [vaultCollectionError, setVaultCollectionError] = useState(null); const [vaultSupported, setVaultSupported] = useState(null); + const [startupConfiguration, setStartupConfiguration] = useState(null); + const [startupConfigurationState, setStartupConfigurationState] = useState("connecting"); + const [startupConfigurationSupported, setStartupConfigurationSupported] = useState(null); const [environmentTemplates, setEnvironmentTemplates] = useState(null); const [sessions, setSessions] = useState([]); const [selectedId, setSelectedId] = useState(null); @@ -309,8 +312,10 @@ export function App() { const filteredSessionCollectionRequestRef = useRef(0); const sessionAgentFilterRef = useRef(sessionAgentFilter); const vaultCollectionAbortRef = useRef(null); + const startupConfigurationAbortRef = useRef(null); const environmentTemplateAbortRef = useRef(null); const vaultCollectionRequestRef = useRef(0); + const startupConfigurationRequestRef = useRef(0); const agentCollectionRevisionRef = useRef(0); const sessionCollectionRevisionRef = useRef(0); const sessionRequestRef = useRef(new Map()); @@ -331,15 +336,6 @@ export function App() { sessionAgentFilterRef.current = sessionAgentFilter; const core = useMemo(() => createCore(connection), [connection]); - const sourceFilesOperations = useMemo(() => ({ - uploadSourceFile: (input, options) => core.uploadSourceFile(input, options), - retrieveSourceFile: (fileId, options) => core.retrieveSourceFile(fileId, options), - downloadSourceFile: (fileId, options) => core.downloadSourceFile(fileId, options), - deleteSourceFile: (fileId, options) => core.deleteSourceFile(fileId, options), - retrieveEnvironment: (environmentId, options) => core.retrieveEnvironment(environmentId, options), - createEnvironmentFile: (environmentId, input, options) => core.createEnvironmentFile(environmentId, input, options), - listEnvironmentFiles: (environmentId, options) => core.listEnvironmentFiles(environmentId, options), - }), [core]); const coreGeneration = connectionGenerationRef.current; const coreState: CoreConnectionState = agentCollectionState === "ready" || sessionCollectionState === "ready" ? "ready" @@ -621,6 +617,44 @@ export function App() { } }, [core, coreGeneration]); + const refreshStartupConfiguration = useCallback(async () => { + if (coreGeneration !== connectionGenerationRef.current) return false; + startupConfigurationAbortRef.current?.abort(); + const controller = new AbortController(); + startupConfigurationAbortRef.current = controller; + const request = startupConfigurationRequestRef.current + 1; + startupConfigurationRequestRef.current = request; + setStartupConfigurationState("connecting"); + try { + const configuration = await core.retrieveStartupConfiguration({ signal: controller.signal }); + if ( + coreGeneration !== connectionGenerationRef.current || + request !== startupConfigurationRequestRef.current + ) return false; + setStartupConfiguration(configuration); + setStartupConfigurationSupported(true); + setStartupConfigurationState("ready"); + return true; + } catch (error) { + if ( + coreGeneration !== connectionGenerationRef.current || + request !== startupConfigurationRequestRef.current || + isAbort(error) + ) return false; + setStartupConfiguration(null); + if (error instanceof AgentCoreError && (error.status === 404 || error.status === 405)) { + setStartupConfigurationSupported(false); + setStartupConfigurationState("ready"); + return false; + } + setStartupConfigurationSupported(null); + setStartupConfigurationState("failed"); + return false; + } finally { + if (startupConfigurationAbortRef.current === controller) startupConfigurationAbortRef.current = null; + } + }, [core, coreGeneration]); + const refreshEnvironmentTemplates = useCallback(async () => { // Managed Environment configuration is only ever read for the Web build that // can request it. An unread catalog stays null, never an implied capability. @@ -859,6 +893,11 @@ export function App() { if (filter) void refreshFilteredSessions(filter); }, [refreshAgents, refreshEnvironmentTemplates, refreshFilteredSessions, refreshSessions, refreshVaults]); + const refreshSystem = useCallback(() => { + refreshDashboard(); + void refreshStartupConfiguration(); + }, [refreshDashboard, refreshStartupConfiguration]); + const changeSessionAgentFilter = useCallback((agentId: string | null) => { if (sessionAgentFilterRef.current === agentId) return; filteredSessionCollectionAbortRef.current?.abort(); @@ -891,6 +930,9 @@ export function App() { setVaultCollectionState("connecting"); setVaultCollectionError(null); setVaultSupported(null); + setStartupConfiguration(null); + setStartupConfigurationState("connecting"); + setStartupConfigurationSupported(null); setSessions([]); setAgentCollectionHasSnapshot(false); setSessionCollectionHasSnapshot(false); @@ -908,7 +950,8 @@ export function App() { void refreshSessions(); void refreshVaults(); void refreshEnvironmentTemplates(); - }, [refreshAgents, refreshEnvironmentTemplates, refreshSessions, refreshVaults]); + void refreshStartupConfiguration(); + }, [refreshAgents, refreshEnvironmentTemplates, refreshSessions, refreshStartupConfiguration, refreshVaults]); useEffect(() => { filteredSessionCollectionAbortRef.current?.abort(); @@ -1945,6 +1988,7 @@ export function App() { sessionCollectionRequestRef.current += 1; filteredSessionCollectionRequestRef.current += 1; vaultCollectionRequestRef.current += 1; + startupConfigurationRequestRef.current += 1; agentCollectionRevisionRef.current = 0; sessionCollectionRevisionRef.current = 0; sessionRequestRef.current.clear(); @@ -1970,6 +2014,8 @@ export function App() { filteredSessionCollectionAbortRef.current = null; vaultCollectionAbortRef.current?.abort(); vaultCollectionAbortRef.current = null; + startupConfigurationAbortRef.current?.abort(); + startupConfigurationAbortRef.current = null; environmentTemplateAbortRef.current?.abort(); environmentTemplateAbortRef.current = null; setEnvironmentTemplates(null); @@ -1991,6 +2037,9 @@ export function App() { setVaultCollectionError(null); setVaultSupported(null); setVaultCatalog(null); + setStartupConfiguration(null); + setStartupConfigurationState("connecting"); + setStartupConfigurationSupported(null); setAgents([]); setSessions([]); setItems([]); @@ -2201,16 +2250,20 @@ export function App() { key={`system:${coreGeneration}`} coreState={coreState} coreBaseUrl={connection.baseUrl} - selfHostedEnabled={__AGENTS_CORE_WEB_SELF_HOSTED_SESSIONS__} + startupConfiguration={startupConfiguration} + startupConfigurationState={startupConfigurationState} + startupConfigurationSupported={startupConfigurationSupported} vaultCollectionState={vaultCollectionState} vaultSupported={vaultSupported} - sourceFilesOperations={sourceFilesOperations} + selfHostedWebEnabled={__AGENTS_CORE_WEB_SELF_HOSTED_SESSIONS__} + managedWebEnabled={__AGENTS_CORE_WEB_OPENAI_HOSTED_SESSIONS__} refreshing={ agentCollectionState === "connecting" || sessionCollectionState === "connecting" || - vaultCollectionState === "connecting" + vaultCollectionState === "connecting" || + startupConfigurationState === "connecting" } - onRefresh={refreshDashboard} + onRefresh={refreshSystem} /> ) : null} diff --git a/apps/web/src/features/system/SystemView.css b/apps/web/src/features/system/SystemView.css index 674df954d..f00298c55 100644 --- a/apps/web/src/features/system/SystemView.css +++ b/apps/web/src/features/system/SystemView.css @@ -1,6 +1,6 @@ .system-summary { display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); + grid-template-columns: repeat(5, minmax(0, 1fr)); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); } @@ -42,18 +42,163 @@ overflow-wrap: anywhere; } +.system-config-section { + margin-top: 30px; +} + +.system-config-section > header { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 16px; + margin-bottom: 12px; +} + +.system-config-section > header h2 { + margin: 0; + color: var(--fg); + font-size: 14px; + font-weight: 500; +} + +.system-config-section > header span { + color: var(--fg-muted); + font-size: 11px; +} + +.system-config-list { + overflow: hidden; + border: 1px solid var(--line); + border-radius: 10px; + background: var(--surface); +} + +.system-config-row { + display: grid; + grid-template-columns: minmax(150px, .8fr) minmax(150px, .9fr) minmax(240px, 1.7fr); + align-items: center; + gap: 18px; + min-height: 64px; + padding: 13px 18px; +} + +.system-config-row + .system-config-row { + border-top: 1px solid var(--line); +} + +.system-config-row > strong, +.system-harness-card strong { + color: var(--fg); + font-size: 13px; + font-weight: 500; +} + +.system-config-row > small, +.system-harness-card > small { + color: var(--fg-muted); + font-size: 11px; + line-height: 16px; +} + +.system-config-status { + display: inline-flex; + align-items: center; + gap: 7px; + width: fit-content; + min-height: 26px; + padding: 0 9px; + border-radius: 999px; + background: var(--surface-subtle); + color: var(--fg-muted); + font-size: 11px; + font-weight: 500; +} + +.system-config-status > span { + width: 6px; + height: 6px; + border-radius: 50%; + background: currentColor; +} + +.system-config-status.enabled { + background: color-mix(in srgb, var(--success) 12%, transparent); + color: var(--success); +} + +.system-harness-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; +} + +.system-harness-card { + min-width: 0; + padding: 15px; + border: 1px solid var(--line); + border-radius: 10px; + background: var(--surface); +} + +.system-harness-card > div { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.system-harness-card > small { + display: block; + margin-top: 11px; +} + +.system-boundary-note { + display: flex; + align-items: flex-start; + gap: 9px; + margin: 24px 0 0; + padding: 13px 15px; + border-radius: 9px; + background: var(--surface-subtle); + color: var(--fg-muted); + font-size: 11px; + line-height: 16px; +} + +.system-boundary-note svg { + flex: 0 0 auto; + margin-top: 1px; +} + @media (max-width: 920px) { .system-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); } - .system-summary-cell:nth-child(2) { + .system-summary-cell:nth-child(2n) { border-right: 0; } - .system-summary-cell:nth-child(-n + 2) { + .system-summary-cell { border-bottom: 1px solid var(--line); } + + .system-summary-cell:last-child { + border-right: 0; + border-bottom: 0; + } + + .system-config-row { + grid-template-columns: minmax(140px, .8fr) minmax(140px, 1fr); + } + + .system-config-row > small { + grid-column: 1 / -1; + } + + .system-harness-grid { + grid-template-columns: 1fr; + } } @media (max-width: 560px) { @@ -71,4 +216,19 @@ .system-summary-cell:last-child { border-bottom: 0; } + + .system-config-section > header { + align-items: flex-start; + flex-direction: column; + gap: 4px; + } + + .system-config-row { + grid-template-columns: 1fr; + gap: 8px; + } + + .system-config-row > small { + grid-column: auto; + } } diff --git a/apps/web/src/features/system/SystemView.test.tsx b/apps/web/src/features/system/SystemView.test.tsx index e4a0d2f67..877d9543f 100644 --- a/apps/web/src/features/system/SystemView.test.tsx +++ b/apps/web/src/features/system/SystemView.test.tsx @@ -1,106 +1,113 @@ import { renderToStaticMarkup } from "react-dom/server"; import { describe, expect, it } from "vitest"; +import type { CoreStartupConfiguration } from "@agents-core-web/agents-client"; + import { safeCoreBaseUrlLabel, SystemView } from "./SystemView"; +const startup: CoreStartupConfiguration = { + object: "agents.core.startup_configuration", + schema_version: 1, + supported: { + harnesses: ["claude_sdk", "codex", "mcode"], + managed_sandbox_providers: ["docker", "microsandbox"], + }, + configured: { + default_harness: "codex", + enabled_harnesses: ["claude_sdk", "codex"], + daemon_gateway: true, + self_hosted: true, + managed_sandbox: { enabled: true, provider: "docker", maintenance: false }, + model_providers: [ + { harness: "claude_sdk", endpoint_configured: false }, + { harness: "codex", endpoint_configured: true }, + ], + }, +}; + +function render(overrides: Partial[0]> = {}): string { + return renderToStaticMarkup( + undefined} + {...overrides} + />, + ); +} + describe("SystemView", () => { - it("shows only the four operator-facing status cards", () => { - const html = renderToStaticMarkup( - undefined} - />, - ); + it("renders startup support and configuration without claiming Runtime readiness", () => { + const html = render(); - expect(html).toContain("Connection status"); - expect(html.match(/role="listitem"/g)).toHaveLength(4); - expect(html).toContain('aria-live="polite"'); - expect(html).toContain('aria-busy="false"'); - expect(html).toContain("Core API"); - expect(html).toContain("https://core.example/v1"); - expect(html).toContain("confirmed by an Agent or Session API request"); - expect(html).toContain("Vaults"); + expect(html).toContain("Core startup configuration"); + expect(html.match(/role="listitem"/g)).toHaveLength(5); expect(html).toContain("Vault catalog loaded"); - expect(html).toContain("Self-hosted"); - expect(html).toContain("Enabled"); - expect(html).toContain("Runtime status"); - expect(html).toContain("Cannot be pre-checked"); - expect(html).toContain("Runtime availability is verified when a Session executes"); - expect(html).toContain("Refresh System status"); - expect(html).not.toContain("Source Files"); - expect(html).not.toContain("Public capability surface"); - expect(html).not.toContain("Ownership layers"); - expect(html).not.toContain("Environment profiles"); - expect(html).not.toContain("Core contract"); - expect(html).not.toContain("What this page proves"); + expect(html).toContain("Default harness"); + expect(html).toContain("Managed sandbox"); + expect(html).toContain("LLM endpoints"); + expect(html).toContain("1/2 configured"); + expect(html).toContain("Configured for this process"); + expect(html).toContain("Daemon gateway"); + expect(html).toContain("Supported by this build: Docker, Microsandbox"); + expect(html).toContain("Claude SDK"); + expect(html).toContain("MiniMax Code"); + expect(html).toContain("Operator LLM endpoint: configured"); + expect(html).toContain("Runtime connection, native binary availability, sandbox health, and model execution belong to the relevant Session or Environment"); + expect(html).not.toContain("Runtime status"); + expect(html).not.toContain("ready"); expect(html).not.toContain("user:pass"); expect(html).not.toContain("token=secret"); expect(html).not.toContain("fragment"); - expect(html).not.toContain('role="img"'); }); - it("keeps unsupported and disabled states explicit", () => { - const html = renderToStaticMarkup( - undefined} - />, - ); + it("shows maintenance and Web-build boundaries separately from Core configuration", () => { + const maintenance: CoreStartupConfiguration = { + ...startup, + configured: { + ...startup.configured, + managed_sandbox: { enabled: true, provider: "microsandbox", maintenance: true }, + }, + }; + const html = render({ startupConfiguration: maintenance, selfHostedWebEnabled: false, managedWebEnabled: false }); - expect(html).toContain("Checking…"); - expect(html).toContain("This Core does not expose the Vaults API"); - expect(html).toContain("Disabled"); - expect(html).toContain("Self-hosted Session creation is disabled in this Web build"); - expect(html).toContain("Refreshing…"); - expect(html).toContain('aria-busy="true"'); - expect(html).toContain("disabled"); - expect(html).toContain("/v1"); + expect(html).toContain("Microsandbox · Maintenance"); + expect(html).toContain("Selected provider is in maintenance mode"); + expect(html).toContain("This Web build cannot request managed Sessions"); + expect(html).toContain("This Web build cannot request self-hosted Sessions"); }); - it("shows a failed Vault capability check instead of a permanent pending state", () => { - const html = renderToStaticMarkup( - undefined} - />, - ); + it("handles an older Core without leaving a pending state", () => { + const html = render({ + startupConfiguration: null, + startupConfigurationState: "ready", + startupConfigurationSupported: false, + }); - expect(html).toContain("Check failed"); - expect(html).toContain("The Vaults API check failed"); + expect(html).toContain("Not exposed"); + expect(html).toContain("This Core version does not expose the startup configuration extension"); + expect(html).not.toContain("Configured for this process"); expect(html).not.toContain("Checking…"); }); - it("distinguishes a failed Vault refresh from an unsupported Core", () => { - const html = renderToStaticMarkup( - undefined} - />, - ); + it("fails closed when the startup configuration read fails", () => { + const html = render({ + startupConfiguration: null, + startupConfigurationState: "failed", + startupConfigurationSupported: null, + }); - expect(html).toContain("Refresh failed"); - expect(html).toContain("The latest Vault catalog request failed"); - expect(html).not.toContain("This Core does not expose the Vaults API"); + expect(html).toContain("Unavailable"); + expect(html).toContain("No configuration is inferred"); + expect(html).not.toContain("Configured for this process"); }); it("sanitizes Core labels independently from connection storage", () => { diff --git a/apps/web/src/features/system/SystemView.tsx b/apps/web/src/features/system/SystemView.tsx index c701eb89d..c64cb2e7a 100644 --- a/apps/web/src/features/system/SystemView.tsx +++ b/apps/web/src/features/system/SystemView.tsx @@ -1,8 +1,9 @@ -import { RefreshCw } from "lucide-react"; +import { Info, RefreshCw } from "lucide-react"; + +import type { CoreStartupConfiguration } from "@agents-core-web/agents-client"; import { StatusIcon, type StatusKind } from "../../components/StatusIcon"; import type { CoreConnectionState } from "../../lib/connection"; -import type { SourceFilesOperations } from "./SourceFilesPanel"; import "./SystemView.css"; @@ -18,6 +19,19 @@ function stateLabel(state: CoreConnectionState): string { return "Checking…"; } +function harnessLabel(harness: string): string { + if (harness === "claude_sdk") return "Claude SDK"; + if (harness === "mcode") return "MiniMax Code"; + if (harness === "codex") return "Codex"; + return harness; +} + +function providerLabel(provider: string | null): string { + if (provider === "microsandbox") return "Microsandbox"; + if (provider === "docker") return "Docker"; + return "None"; +} + export function safeCoreBaseUrlLabel(value: string): string { const candidate = value.trim() || "/v1"; if (candidate.startsWith("/")) return candidate; @@ -40,55 +54,70 @@ interface SystemStatusCard { value: string; } +function StartupStatus({ enabled, label }: { enabled: boolean; label?: string }) { + return ( + + + ); +} + export function SystemView({ coreState, coreBaseUrl, - selfHostedEnabled, + startupConfiguration, + startupConfigurationState, + startupConfigurationSupported, vaultCollectionState, vaultSupported, + selfHostedWebEnabled, + managedWebEnabled, refreshing, onRefresh, }: { coreState: CoreConnectionState; coreBaseUrl: string; - selfHostedEnabled: boolean; - sourceFilesOperations?: SourceFilesOperations; + startupConfiguration: CoreStartupConfiguration | null; + startupConfigurationState: CoreConnectionState; + startupConfigurationSupported: boolean | null; vaultCollectionState: CoreConnectionState; vaultSupported: boolean | null; + selfHostedWebEnabled: boolean; + managedWebEnabled: boolean; refreshing: boolean; onRefresh: () => void; }) { + const configuration = startupConfigurationState === "ready" && startupConfigurationSupported === true + ? startupConfiguration + : null; + const configuredEndpoints = configuration?.configured.model_providers.filter((provider) => provider.endpoint_configured).length ?? 0; + const endpointTotal = configuration?.configured.model_providers.length ?? 0; + const startupUnavailable = startupConfigurationSupported === false; + + const startupValue = (value: string): string => { + if (configuration) return value; + if (startupUnavailable) return "Not exposed"; + if (startupConfigurationState === "failed") return "Unavailable"; + return "Checking…"; + }; + const startupStatus: StatusKind = configuration + ? "completed" + : startupUnavailable + ? "interrupted" + : stateKind(startupConfigurationState); + const startupDetail = startupUnavailable + ? "This Core version does not expose the startup configuration extension." + : startupConfigurationState === "failed" + ? "The startup configuration request failed. No configuration is inferred." + : "Reported by the safe Core startup configuration extension."; const vaultStatus: SystemStatusCard = vaultSupported === false - ? { - label: "Vaults", - status: "interrupted", - value: "Unavailable", - detail: "This Core does not expose the Vaults API.", - } + ? { label: "Vaults", status: "interrupted", value: "Unavailable", detail: "This Core does not expose the Vaults API." } : vaultCollectionState === "failed" - ? { - label: "Vaults", - status: "failed", - value: vaultSupported === true ? "Refresh failed" : "Check failed", - detail: vaultSupported === true - ? "The latest Vault catalog request failed." - : "The Vaults API check failed.", - } + ? { label: "Vaults", status: "failed", value: vaultSupported === true ? "Refresh failed" : "Check failed", detail: "The Vault catalog request failed." } : vaultSupported === true && vaultCollectionState === "ready" - ? { - label: "Vaults", - status: "completed", - value: "Available", - detail: "Vault catalog loaded.", - } - : { - label: "Vaults", - status: "running", - value: "Checking…", - detail: vaultSupported === true - ? "Refreshing the Vault catalog." - : "Checking whether this Core exposes the Vaults API.", - }; + ? { label: "Vaults", status: "completed", value: "Available", detail: "Vault catalog loaded." } + : { label: "Vaults", status: "running", value: "Checking…", detail: "Checking whether this Core exposes the Vaults API." }; const cards: SystemStatusCard[] = [ { @@ -105,25 +134,35 @@ export function SystemView({ }, vaultStatus, { - label: "Self-hosted", - status: selfHostedEnabled ? "completed" : "interrupted", - value: selfHostedEnabled ? "Enabled" : "Disabled", - detail: selfHostedEnabled - ? "This Web build allows self-hosted Session creation. Runtime is not verified here." - : "Self-hosted Session creation is disabled in this Web build.", + label: "Default harness", + status: startupStatus, + value: startupValue(configuration ? harnessLabel(configuration.configured.default_harness) : ""), + detail: startupDetail, + }, + { + label: "Managed sandbox", + status: startupStatus, + value: startupValue(configuration ? providerLabel(configuration.configured.managed_sandbox.provider) : ""), + detail: configuration?.configured.managed_sandbox.maintenance + ? "Selected provider is in maintenance mode." + : startupDetail, }, { - label: "Runtime status", - status: "interrupted", - value: "Cannot be pre-checked", - detail: "Runtime availability is verified when a Session executes.", + label: "LLM endpoints", + status: startupStatus, + value: startupValue(endpointTotal === 0 || configuredEndpoints === 0 + ? "Not configured" + : configuredEndpoints === endpointTotal ? "Configured" : `${configuredEndpoints}/${endpointTotal} configured`), + detail: configuration + ? "Reports operator endpoint configuration presence only; addresses and credentials stay hidden." + : startupDetail, }, ]; return (
-

System Connection status

+

System Core startup configuration

-
+
{cards.map((card) => (
{card.label} @@ -147,6 +186,70 @@ export function SystemView({
))}
+ + {configuration ? ( + <> +
+
+

Configured for this process

+ Validated or detected when Core started +
+
+
+ Daemon gateway + + Accepts authenticated execution Runtime connections when enabled. +
+
+ Managed execution + + + Supported by this build: {configuration.supported.managed_sandbox_providers.map(providerLabel).join(", ")}. + {managedWebEnabled ? " This Web build can request managed Sessions." : " This Web build cannot request managed Sessions."} + +
+
+ Self-hosted execution + + {selfHostedWebEnabled ? "This Web build can request self-hosted Sessions." : "This Web build cannot request self-hosted Sessions."} +
+
+
+ +
+
+

Harnesses

+ Configuration, not execution readiness +
+
+ {configuration.supported.harnesses.map((harness) => { + const enabled = configuration.configured.enabled_harnesses.includes(harness); + const endpoint = configuration.configured.model_providers.find((provider) => provider.harness === harness); + return ( +
+
+ {harnessLabel(harness)} + +
+ + {enabled + ? `Operator LLM endpoint: ${endpoint?.endpoint_configured ? "configured" : "not configured"}.` + : "Known by this build but not enabled for this process."} + +
+ ); + })} +
+
+ +

+ + ) : null}
); } diff --git a/contracts/agents-api/README.md b/contracts/agents-api/README.md index 9cfad97fe..05ab3ecc8 100644 --- a/contracts/agents-api/README.md +++ b/contracts/agents-api/README.md @@ -818,6 +818,10 @@ establish complete ownership, hosted key lifecycle or error compatibility. See t Core documents its optional [harness selection extension](harness-selection.md) separately from the pinned upstream contract. +The [Core startup configuration extension](startup-configuration.md) exposes only +safe build support and process configuration facts. It does not report Runtime, +Session or Environment observations and is not a readiness endpoint. + Model endpoints and credentials may be supplied at Session creation through the [write-only execution extension](model-execution.md). Provider catalogs and their business permissions remain client/product responsibilities. diff --git a/contracts/agents-api/openapi.yaml b/contracts/agents-api/openapi.yaml index c3a8fcfef..9607a0ea5 100644 --- a/contracts/agents-api/openapi.yaml +++ b/contracts/agents-api/openapi.yaml @@ -97,6 +97,112 @@ definitions: required: - harness type: object + v1.CoreConfiguredStartupConfiguration: + properties: + daemon_gateway: + type: boolean + default_harness: + enum: + - codex + - claude_sdk + - mcode + type: string + enabled_harnesses: + items: + enum: + - claude_sdk + - codex + - mcode + type: string + type: array + managed_sandbox: + $ref: '#/definitions/v1.CoreManagedSandboxConfiguration' + model_providers: + items: + $ref: '#/definitions/v1.CoreHarnessModelProviderConfiguration' + type: array + self_hosted: + type: boolean + required: + - daemon_gateway + - default_harness + - enabled_harnesses + - managed_sandbox + - model_providers + - self_hosted + type: object + v1.CoreHarnessModelProviderConfiguration: + properties: + endpoint_configured: + type: boolean + harness: + enum: + - codex + - claude_sdk + - mcode + type: string + required: + - endpoint_configured + - harness + type: object + v1.CoreManagedSandboxConfiguration: + properties: + enabled: + type: boolean + maintenance: + type: boolean + provider: + enum: + - docker + - microsandbox + type: string + x-nullable: true + required: + - enabled + - maintenance + - provider + type: object + v1.CoreStartupConfiguration: + properties: + configured: + $ref: '#/definitions/v1.CoreConfiguredStartupConfiguration' + object: + enum: + - agents.core.startup_configuration + type: string + schema_version: + enum: + - 1 + type: integer + supported: + $ref: '#/definitions/v1.CoreSupportedConfiguration' + required: + - configured + - object + - schema_version + - supported + type: object + v1.CoreSupportedConfiguration: + properties: + harnesses: + items: + enum: + - claude_sdk + - codex + - mcode + type: string + type: array + managed_sandbox_providers: + items: + enum: + - docker + - microsandbox + type: string + type: array + required: + - harnesses + - managed_sandbox_providers + type: object v1.CreateAgentRequest: properties: instructions: @@ -2248,6 +2354,43 @@ paths: summary: Update a reusable Agent tags: - Agents + /agents/core/startup-configuration: + get: + description: Returns a secret-free snapshot of supported build capabilities + and validated process startup selections. It does not inspect or aggregate + daemon heartbeats, Sessions, Environments or Runtime state, and does not prove + model-provider reachability, credentials, native readiness, sandbox isolation + or successful execution. + parameters: + - description: agents=v1 + in: header + name: OpenAI-Beta + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/v1.CoreStartupConfiguration' + "400": + description: Bad Request + schema: + $ref: '#/definitions/v1.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/v1.ErrorResponse' + "503": + description: Service Unavailable + schema: + $ref: '#/definitions/v1.ErrorResponse' + security: + - BearerAuth: [] + summary: Retrieve safe Core startup configuration + tags: + - Core extensions /agents/environments/{environment_id}: get: description: Returns durable connection status and safe installed metadata for diff --git a/contracts/agents-api/startup-configuration.md b/contracts/agents-api/startup-configuration.md new file mode 100644 index 000000000..7b6fc4caf --- /dev/null +++ b/contracts/agents-api/startup-configuration.md @@ -0,0 +1,31 @@ +# Core startup configuration extension + +`GET /v1/agents/core/startup-configuration` returns a read-only, project-authenticated +snapshot of safe configuration facts established when the Core process starts. It +is a Core extension outside the pinned upstream Agents API. + +The response separates: + +- `supported`: harness and managed sandbox provider kinds compiled into this Core + build; and +- `configured`: the default and enabled harnesses, daemon gateway/self-hosted + composition, selected managed provider and maintenance state, plus whether an + operator-supplied model endpoint is present for each enabled harness. + +Model endpoint reporting is boolean. Core never returns the URL, credentials, +headers, query parameters, raw execution options or their file path. Managed +sandbox output is limited to `docker`, `microsandbox` or no selected provider; it +does not expose installation IDs, socket/runtime paths, image references or +provider-native identifiers. + +This snapshot never reads or aggregates daemon heartbeats, Runtime registrations, +Sessions, Environments or allocations. `configured` does not mean reachable, +authenticated, ready, isolated or successfully executed. Those observations belong +to the scoped Session or Environment. A Session may also supply the separate +[write-only model execution extension](model-execution.md); that private input is +not reflected in this process-level endpoint. + +The response has a fixed `schema_version`. Clients should reject unknown or +incomplete shapes rather than infer configuration. The endpoint rejects query +parameters, returns `Cache-Control: no-store`, and requires the standard project +bearer key plus `OpenAI-Beta: agents=v1`. diff --git a/contracts/agents-api/v1/startup_configuration.go b/contracts/agents-api/v1/startup_configuration.go new file mode 100644 index 000000000..6036bbbc4 --- /dev/null +++ b/contracts/agents-api/v1/startup_configuration.go @@ -0,0 +1,35 @@ +package v1 + +// CoreStartupConfiguration is a secret-free snapshot of the validated process +// configuration. It never describes a Session, Environment or live Runtime. +type CoreStartupConfiguration struct { + Object string `json:"object" binding:"required" enums:"agents.core.startup_configuration"` + SchemaVersion int `json:"schema_version" binding:"required" enums:"1"` + Supported CoreSupportedConfiguration `json:"supported" binding:"required"` + Configured CoreConfiguredStartupConfiguration `json:"configured" binding:"required"` +} + +type CoreSupportedConfiguration struct { + Harnesses []string `json:"harnesses" binding:"required" enums:"claude_sdk,codex,mcode"` + ManagedSandboxProviders []string `json:"managed_sandbox_providers" binding:"required" enums:"docker,microsandbox"` +} + +type CoreConfiguredStartupConfiguration struct { + DefaultHarness string `json:"default_harness" binding:"required" enums:"codex,claude_sdk,mcode"` + EnabledHarnesses []string `json:"enabled_harnesses" binding:"required" enums:"claude_sdk,codex,mcode"` + DaemonGateway bool `json:"daemon_gateway" binding:"required"` + SelfHosted bool `json:"self_hosted" binding:"required"` + ManagedSandbox CoreManagedSandboxConfiguration `json:"managed_sandbox" binding:"required"` + ModelProviders []CoreHarnessModelProviderConfiguration `json:"model_providers" binding:"required"` +} + +type CoreManagedSandboxConfiguration struct { + Enabled bool `json:"enabled" binding:"required"` + Provider *string `json:"provider" binding:"required" enums:"docker,microsandbox" extensions:"x-nullable"` + Maintenance bool `json:"maintenance" binding:"required"` +} + +type CoreHarnessModelProviderConfiguration struct { + Harness string `json:"harness" binding:"required" enums:"codex,claude_sdk,mcode"` + EndpointConfigured bool `json:"endpoint_configured" binding:"required"` +} diff --git a/docs/web/README.md b/docs/web/README.md index 3703cddeb..203532d09 100644 --- a/docs/web/README.md +++ b/docs/web/README.md @@ -21,8 +21,9 @@ credentials or execution into the browser. inspect command and patch activity, and attach write-only MCP credentials through Vaults. - **Choose an Environment** — use Core's default execution path, connect a caller-managed self-hosted executor, or use an operator-enabled managed Runtime. -- **Understand the connection** — view Core reachability and exposed product capabilities, - and get actionable local Docker recovery guidance when the backend is not ready. +- **Understand the connection** — view Core reachability, build-supported harnesses, + safe process startup selections, and whether operator model endpoints are configured, + without exposing their addresses or credentials. ## Product tour @@ -51,9 +52,10 @@ and follow-up input without losing the durable record. ### System and connection status -System shows what the connected Core exposes to this Web build. It separates API access, -Vault availability, self-hosted presentation, and runtime readiness so a healthy HTTP -service is not mistaken for a ready model execution path. +System shows what the connected Core build supports and what this process configured at +startup: harnesses, daemon gateway, self-hosted execution, managed sandbox provider and +operator model endpoint presence. It does not aggregate Runtime/daemon observations, so +configuration is never presented as model execution readiness. ![Core connection and capability status](images/system.png) @@ -69,7 +71,7 @@ service is not mistaken for a ready model execution path. | Vaults | Create project Vaults and manage write-only MCP bearer credentials without reading tokens back | | Environments | Default execution, optional self-hosted executor connection, and optional managed Runtime views | | Workspace and Files | Inspect supported Environment files and manage project Source Files when enabled by Core | -| System | Connection status, surfaced capabilities, Core ownership boundaries, and local recovery guidance | +| System | Connection status plus safe build support and process startup configuration, clearly separated from Session/Environment runtime state | Capabilities appear only when the connected Core and the Web operator configuration expose them. Saving an Agent proves that its definition was stored; actual execution still depends diff --git a/docs/web/README.zh-CN.md b/docs/web/README.zh-CN.md index f444beddb..76f435e59 100644 --- a/docs/web/README.zh-CN.md +++ b/docs/web/README.zh-CN.md @@ -47,8 +47,9 @@ Session 的对话和工作历史保存在 Core 中。一个页面同时提供 Se ### System 与连接状态 -System 展示当前 Core 对 Web 暴露的能力,并区分 API 访问、Vault、self-hosted 展示 -开关和运行时就绪状态,避免把 HTTP 服务正常误认为模型执行链路已经可用。 +System 展示当前 Core 构建支持的 harness,以及本次进程启动时配置的 daemon gateway、 +self-hosted、managed sandbox 和 LLM endpoint 是否存在。页面不聚合 Runtime/daemon +运行态,避免把已配置误认为模型执行链路已经就绪。 ![Core 连接和能力状态](images/system.png) @@ -64,7 +65,7 @@ System 展示当前 Core 对 Web 暴露的能力,并区分 API 访问、Vault | Vaults | 创建项目 Vault,管理只写 MCP Bearer 凭据,Web 不会读回 Token | | Environments | 默认执行、可选 self-hosted executor 连接和可选 managed Runtime | | Workspace 与 Files | 在 Core 支持时查看 Environment 文件并管理项目 Source Files | -| System | 查看连接、已暴露能力、Core 所有权边界和本地恢复引导 | +| System | 查看连接、构建支持项和安全的进程启动配置,并与 Session/Environment 运行态明确分离 | 只有连接的 Core 和 Web 运维配置明确暴露的能力才会显示。Agent 保存成功只代表定义 已经持久化;实际执行仍依赖 Core 的运行时、模型提供商、凭据和工具连接。 diff --git a/docs/web/architecture.md b/docs/web/architecture.md index e7852f84a..2ad6bd0f3 100644 --- a/docs/web/architecture.md +++ b/docs/web/architecture.md @@ -98,7 +98,7 @@ capability and its lifecycle behavior is verified. | Boundary | Wire protocol | Authentication | Contract owner | | --- | --- | --- | --- | | Browser → proxy/BFF | Same-origin HTTP under `/v1` | Web deployment policy; local proxy holds a Core bearer | Agents Core Web deployment | -| Proxy/BFF → Core | HTTP JSON/SSE under `/v1/agents/**`; HTTP JSON under `/v1/vaults/**`; multipart/JSON/binary under `/v1/files**` | `Authorization: Bearer …`; route-specific `OpenAI-Beta: agents=v1` | Pinned Agents, Vault/Credential, and Source Files subset | +| Proxy/BFF → Core | HTTP JSON/SSE under `/v1/agents/**`, including the read-only startup configuration extension; HTTP JSON under `/v1/vaults/**`; multipart/JSON/binary under `/v1/files**` | `Authorization: Bearer …`; route-specific `OpenAI-Beta: agents=v1` | Pinned Agents, Core extensions, Vault/Credential, and Source Files subset | | Core ↔ daemon | Private reverse WebSocket, Parsar JSON envelope protocol | Separate device credential | Parsar internal protocol | | Core ↔ self-hosted executor | Native registration plus opaque relay outside `/v1/agents/**` | Operator-issued executor principal credential | Parsar native executor contract | | Daemon ↔ Codex | `codex app-server --stdio`; JSON-RPC 2.0 over newline-delimited JSON | Native host configuration | Codex adapter | @@ -111,6 +111,8 @@ These interfaces are not interchangeable. In particular: - the daemon WebSocket URL is not an Agents API base URL; - OpenAI Agents API is not the same thing as OpenAI Agents SDK or Responses API; - saving a model ID does not select an executor or prove provider availability; +- startup support/configuration does not report daemon or Runtime observations and + does not prove model endpoint reachability, credentials or sandbox readiness; - a Session Environment ID, connection state, or copied launcher command does not prove native readiness, isolation, or completed execution; - multiple saved Agents do not imply protocol multi-agent/Subagent support. diff --git a/docs/web/protocol-coverage.md b/docs/web/protocol-coverage.md index 8379a5c81..fe7badf7a 100644 --- a/docs/web/protocol-coverage.md +++ b/docs/web/protocol-coverage.md @@ -14,7 +14,8 @@ OpenAI-hosted service compatibility. both supported Environment resource projections, and Environment Files.create for that qualified managed placement. `OpenAI-Beta: agents=v1` plus the `/v1/agents/**` resources and Session events endpoint remain the versioned Web/Core - contract. Core exposes no public execution-readiness, capability-discovery, or + contract. The Core startup extension exposes a safe build-support and process- + configuration snapshot, but no execution-readiness, live Runtime capability, or build-version resource. It exposes Codex-only, Session-scoped `self_hosted` creation; basic Codex/Docker `openai_hosted` creation when Core is explicitly configured with a qualified managed provider; Environment retrieval; bounded @@ -30,8 +31,9 @@ OpenAI-hosted service compatibility. three harnesses, alongside the earlier Docker qualification. E2B is a Core-side provider behind the unchanged `openai_hosted` discriminator: the public contract exposes no provider field, no image or sandbox template selector, and no - provider-discovery route, so Web cannot select, name, or verify which provider - backs a managed Runtime. A Core build without the Template resource rejects that + provider configuration route. The startup extension can name the selected + provider kind without exposing provider identity or proving Runtime readiness; + Web still cannot select or configure it. A Core build without the Template resource rejects that collection path, and Web reports the absent capability instead of an empty configuration list. - Upstream resource source: `openai-python` 3.13.0 beta Agents resources at @@ -54,6 +56,7 @@ the Core key binding. Agents Core Web's local proxy owns the bearer server-side. | Resource / behavior | TypeScript client | Initial UI | Notes | | --- | --- | --- | --- | +| Core startup configuration extension | Yes | Yes, System | Strictly projects build-supported harness/provider kinds and validated process selections. Operator endpoint reporting is boolean; URLs, credentials, paths, daemon/Runtime identity and Session/Environment observations are excluded. Unsupported older Core versions remain explicit | | Saved Agents create/list | Yes | Yes | Dedicated setup covers model, name, instructions, bounded metadata, the Session-safe text/medium/implicit-reasoning/auto-tier profile, and the strictly bounded Function/service-origin HTTP MCP form profiles; the broader Saved Agent contract is not execution proof | | Saved Agents retrieve/update/delete | Yes | Yes | Agent details support viewing, editing, and deleting saved Agents; unsupported or saved-only Tool values remain read-only and are omitted from unrelated updates | | Function form profile | Yes | Yes | Web validates at most 64 non-deferred definitions, unique non-whitespace names of at most 512 UTF-8 bytes, descriptions, and object JSON-Schema parameters before writing | diff --git a/packages/agents-client/src/client.ts b/packages/agents-client/src/client.ts index c491cf554..b856ab77d 100644 --- a/packages/agents-client/src/client.ts +++ b/packages/agents-client/src/client.ts @@ -25,6 +25,9 @@ import type { CreateAgentInput, CreateSessionInput, CreateSessionStreamOptions, + CoreStartupConfiguration, + CoreHarnessKind, + CoreManagedSandboxProvider, FunctionResultContent, FunctionResultInput, InputMessage, @@ -97,6 +100,81 @@ function trimTrailingSlash(value: string): string { return value.replace(/\/+$/, ""); } +const startupConfigurationFields = new Set(["object", "schema_version", "supported", "configured"]); +const startupSupportedFields = new Set(["harnesses", "managed_sandbox_providers"]); +const startupConfiguredFields = new Set(["default_harness", "enabled_harnesses", "daemon_gateway", "self_hosted", "managed_sandbox", "model_providers"]); +const startupManagedSandboxFields = new Set(["enabled", "provider", "maintenance"]); +const startupModelProviderFields = new Set(["harness", "endpoint_configured"]); +const harnessKinds = new Set(["claude_sdk", "codex", "mcode"]); +const sandboxProviders = new Set(["docker", "microsandbox"]); + +function isHarnessKind(value: unknown): value is CoreHarnessKind { + return typeof value === "string" && harnessKinds.has(value as CoreHarnessKind); +} + +function isSandboxProvider(value: unknown): value is CoreManagedSandboxProvider { + return typeof value === "string" && sandboxProviders.has(value as CoreManagedSandboxProvider); +} + +function sortedUnique(value: unknown, accept: (entry: unknown) => entry is T): value is T[] { + return Array.isArray(value) && value.every(accept) && new Set(value).size === value.length && + value.every((entry, index) => index === 0 || value[index - 1]! < entry); +} + +function invalidStartupConfiguration(): never { + throw new AgentCoreError("Agent Core returned an invalid startup configuration.", 502, "invalid_startup_configuration"); +} + +function projectStartupConfiguration(value: unknown): CoreStartupConfiguration { + if (!isRecord(value) || !exactFields(value, startupConfigurationFields) || value.object !== "agents.core.startup_configuration" || value.schema_version !== 1 || + !isRecord(value.supported) || !exactFields(value.supported, startupSupportedFields) || + !sortedUnique(value.supported.harnesses, isHarnessKind) || !sortedUnique(value.supported.managed_sandbox_providers, isSandboxProvider) || + !isRecord(value.configured) || !exactFields(value.configured, startupConfiguredFields) || + !isHarnessKind(value.configured.default_harness) || !sortedUnique(value.configured.enabled_harnesses, isHarnessKind) || value.configured.enabled_harnesses.length === 0 || + !value.configured.enabled_harnesses.includes(value.configured.default_harness) || typeof value.configured.daemon_gateway !== "boolean" || + typeof value.configured.self_hosted !== "boolean" || value.configured.self_hosted !== value.configured.daemon_gateway || + !isRecord(value.configured.managed_sandbox) || !exactFields(value.configured.managed_sandbox, startupManagedSandboxFields) || + typeof value.configured.managed_sandbox.enabled !== "boolean" || typeof value.configured.managed_sandbox.maintenance !== "boolean" || + !Array.isArray(value.configured.model_providers)) { + return invalidStartupConfiguration(); + } + const configured = value.configured; + const managed = configured.managed_sandbox as Record; + const supportedHarnesses = value.supported.harnesses as CoreHarnessKind[]; + const supportedSandboxProviders = value.supported.managed_sandbox_providers as CoreManagedSandboxProvider[]; + const enabledHarnesses = configured.enabled_harnesses as CoreHarnessKind[]; + if (managed.enabled + ? !isSandboxProvider(managed.provider) || !supportedSandboxProviders.includes(managed.provider) || !configured.daemon_gateway + : managed.provider !== null || managed.maintenance) { + return invalidStartupConfiguration(); + } + if (enabledHarnesses.some((harness) => !supportedHarnesses.includes(harness))) { + return invalidStartupConfiguration(); + } + const modelProviders = configured.model_providers as unknown[]; + if (modelProviders.length !== enabledHarnesses.length || modelProviders.some((entry, index) => + !isRecord(entry) || !exactFields(entry, startupModelProviderFields) || entry.harness !== enabledHarnesses[index] || typeof entry.endpoint_configured !== "boolean")) { + return invalidStartupConfiguration(); + } + const projectedProviders = modelProviders as Array>; + return { + object: "agents.core.startup_configuration", + schema_version: 1, + supported: { + harnesses: [...supportedHarnesses], + managed_sandbox_providers: [...supportedSandboxProviders], + }, + configured: { + default_harness: configured.default_harness as CoreHarnessKind, + enabled_harnesses: [...enabledHarnesses], + daemon_gateway: configured.daemon_gateway as boolean, + self_hosted: configured.self_hosted as boolean, + managed_sandbox: { enabled: managed.enabled as boolean, provider: managed.provider as CoreManagedSandboxProvider | null, maintenance: managed.maintenance as boolean }, + model_providers: projectedProviders.map((entry) => ({ harness: entry.harness as CoreHarnessKind, endpoint_configured: entry.endpoint_configured as boolean })), + }, + }; +} + export function createIdempotencyKey(): string { if (typeof crypto !== "undefined" && "randomUUID" in crypto) { return crypto.randomUUID(); @@ -1535,6 +1613,11 @@ function projectEnvironmentFileList( } export class OpenAIAgentsClient implements AgentCore { + async retrieveStartupConfiguration(options?: ReadOptions): Promise { + const value = await this.request("/agents/core/startup-configuration", { signal: options?.signal }, 200); + return projectStartupConfiguration(value); + } + private readonly baseUrl: string; private readonly token: OpenAIAgentsClientOptions["token"]; private readonly fetchImpl: typeof fetch; diff --git a/packages/agents-client/src/startup-configuration.test.ts b/packages/agents-client/src/startup-configuration.test.ts new file mode 100644 index 000000000..5ac49bc6f --- /dev/null +++ b/packages/agents-client/src/startup-configuration.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, it } from "vitest"; + +import { OpenAIAgentsClient } from "./client"; + +function response(body: unknown): Response { + return new Response(JSON.stringify(body), { status: 200, headers: { "Content-Type": "application/json" } }); +} + +function fixture(): Record { + return { + object: "agents.core.startup_configuration", + schema_version: 1, + supported: { + harnesses: ["claude_sdk", "codex", "mcode"], + managed_sandbox_providers: ["docker", "microsandbox"], + }, + configured: { + default_harness: "codex", + enabled_harnesses: ["claude_sdk", "codex"], + daemon_gateway: true, + self_hosted: true, + managed_sandbox: { enabled: true, provider: "docker", maintenance: false }, + model_providers: [ + { harness: "claude_sdk", endpoint_configured: false }, + { harness: "codex", endpoint_configured: true }, + ], + }, + }; +} + +describe("Core startup configuration", () => { + it("reads the exact authenticated extension path and returns a defensive projection", async () => { + const calls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = []; + const body = fixture(); + const client = new OpenAIAgentsClient({ + baseUrl: "https://core.example/v1/", + token: "project-key", + fetch: (async (input: RequestInfo | URL, init?: RequestInit) => { + calls.push({ input, init }); + return response(body); + }) as typeof fetch, + }); + + const result = await client.retrieveStartupConfiguration(); + (body.supported as { harnesses: string[] }).harnesses[0] = "mutated"; + + expect(String(calls[0]?.input)).toBe("https://core.example/v1/agents/core/startup-configuration"); + expect(new Headers(calls[0]?.init?.headers).get("Authorization")).toBe("Bearer project-key"); + expect(result.supported.harnesses).toEqual(["claude_sdk", "codex", "mcode"]); + expect(result.configured.managed_sandbox).toEqual({ enabled: true, provider: "docker", maintenance: false }); + }); + + it.each([ + ["unknown field", (value: any) => { value.secret = "private"; }], + ["unknown harness", (value: any) => { value.supported.harnesses[0] = "future"; }], + ["unsorted harnesses", (value: any) => { value.supported.harnesses.reverse(); }], + ["enabled harness missing from supported", (value: any) => { value.supported.harnesses = ["codex", "mcode"]; }], + ["default not enabled", (value: any) => { value.configured.enabled_harnesses = ["claude_sdk"]; value.configured.model_providers = [{ harness: "claude_sdk", endpoint_configured: false }]; }], + ["managed provider without gateway", (value: any) => { value.configured.daemon_gateway = false; value.configured.self_hosted = false; }], + ["managed provider missing from supported", (value: any) => { value.supported.managed_sandbox_providers = ["microsandbox"]; }], + ["mismatched provider projection", (value: any) => { value.configured.model_providers[0].harness = "codex"; }], + ])("rejects %s", async (_name, mutate) => { + const body = fixture(); + mutate(body); + const client = new OpenAIAgentsClient({ fetch: (async () => response(body)) as typeof fetch }); + await expect(client.retrieveStartupConfiguration()).rejects.toMatchObject({ + code: "invalid_startup_configuration", + status: 502, + }); + }); + + it("does not reflect rejected private fields in its error", async () => { + const body = fixture() as any; + body.configured.private_endpoint = "https://user:secret@example.test/v1?token=private"; + const client = new OpenAIAgentsClient({ fetch: (async () => response(body)) as typeof fetch }); + + await expect(client.retrieveStartupConfiguration()).rejects.not.toThrow(/user:secret|example\.test|token=private/u); + }); + + it("accepts a fully unconfigured execution surface", async () => { + const body = fixture() as any; + body.configured.daemon_gateway = false; + body.configured.self_hosted = false; + body.configured.managed_sandbox = { enabled: false, provider: null, maintenance: false }; + body.configured.enabled_harnesses = ["codex"]; + body.configured.model_providers = [{ harness: "codex", endpoint_configured: false }]; + const client = new OpenAIAgentsClient({ fetch: (async () => response(body)) as typeof fetch }); + + await expect(client.retrieveStartupConfiguration()).resolves.toMatchObject({ configured: { daemon_gateway: false } }); + }); +}); diff --git a/packages/agents-client/src/types.ts b/packages/agents-client/src/types.ts index ca2ab73f7..40c550700 100644 --- a/packages/agents-client/src/types.ts +++ b/packages/agents-client/src/types.ts @@ -715,7 +715,35 @@ export interface CreateSessionStreamOptions extends StreamOptions { onSession: (session: AgentSession) => void; } +export type CoreHarnessKind = "claude_sdk" | "codex" | "mcode"; +export type CoreManagedSandboxProvider = "docker" | "microsandbox"; + +export interface CoreStartupConfiguration { + object: "agents.core.startup_configuration"; + schema_version: 1; + supported: { + harnesses: CoreHarnessKind[]; + managed_sandbox_providers: CoreManagedSandboxProvider[]; + }; + configured: { + default_harness: CoreHarnessKind; + enabled_harnesses: CoreHarnessKind[]; + daemon_gateway: boolean; + self_hosted: boolean; + managed_sandbox: { + enabled: boolean; + provider: CoreManagedSandboxProvider | null; + maintenance: boolean; + }; + model_providers: Array<{ + harness: CoreHarnessKind; + endpoint_configured: boolean; + }>; + }; +} + export interface AgentCore { + retrieveStartupConfiguration(options?: ReadOptions): Promise; listAgents(options?: PageOptions): Promise>; createAgent(input: CreateAgentInput): Promise; retrieveAgent(agentId: string): Promise; diff --git a/services/agents-api/README.md b/services/agents-api/README.md index 08842a859..b7643b7f3 100644 --- a/services/agents-api/README.md +++ b/services/agents-api/README.md @@ -202,6 +202,8 @@ The SDK base URL is `http://127.0.0.1:8091/v1`. Requests require a bearer key. Agents and Vault routes also require `OpenAI-Beta: agents=v1` (set by their SDK resources); general Files routes do not. Supported operations include: +- Safe read-only [Core startup configuration](../../contracts/agents-api/startup-configuration.md) + for build support and process selections, without Runtime or Session observations. - Saved Agent create/retrieve/update/list/delete. - Session create/retrieve/list/delete and metadata-only update. Creation supports inline configuration or a saved `agent_id`, field replacements, optional initial text diff --git a/services/agents-api/cmd/server/execution_options.go b/services/agents-api/cmd/server/execution_options.go index 2618d9ce4..42725dd4c 100644 --- a/services/agents-api/cmd/server/execution_options.go +++ b/services/agents-api/cmd/server/execution_options.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "os" + "strings" "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/store" ) @@ -12,17 +13,22 @@ import ( // Operator options use the existing transient adapter configuration path. They // are not public Session configuration and are never persisted with its snapshot. func executionOptions() (func(context.Context, store.Session) (map[string]any, error), error) { + resolve, _, err := executionOptionsConfiguration() + return resolve, err +} + +func executionOptionsConfiguration() (func(context.Context, store.Session) (map[string]any, error), map[string]bool, error) { file := os.Getenv("AGENTS_API_EXECUTION_OPTIONS_FILE") if file == "" { - return nil, nil + return nil, map[string]bool{}, nil } raw, err := os.ReadFile(file) if err != nil { - return nil, errors.New("cannot read AGENTS_API_EXECUTION_OPTIONS_FILE") + return nil, nil, errors.New("cannot read AGENTS_API_EXECUTION_OPTIONS_FILE") } var check map[string]any if json.Unmarshal(raw, &check) != nil || check == nil { - return nil, errors.New("execution options must contain a JSON object") + return nil, nil, errors.New("execution options must contain a JSON object") } defaultEngine := os.Getenv("AGENTS_API_ENGINE") if defaultEngine == "" { @@ -32,16 +38,26 @@ func executionOptions() (func(context.Context, store.Session) (map[string]any, e if value, exists := check["by_harness"]; exists { encoded, _ := json.Marshal(value) if len(check) != 1 || json.Unmarshal(encoded, &byHarness) != nil || byHarness == nil { - return nil, errors.New("execution by_harness options must be an exclusive object") + return nil, nil, errors.New("execution by_harness options must be an exclusive object") } for _, entry := range byHarness { var object map[string]any if json.Unmarshal(entry, &object) != nil || object == nil { - return nil, errors.New("execution harness options must be objects") + return nil, nil, errors.New("execution harness options must be objects") } } } - return func(_ context.Context, session store.Session) (map[string]any, error) { + configured := make(map[string]bool) + if byHarness != nil { + for harness, entry := range byHarness { + var object map[string]any + _ = json.Unmarshal(entry, &object) + configured[harness] = modelProviderEndpointConfigured(harness, object) + } + } else { + configured[defaultEngine] = modelProviderEndpointConfigured(defaultEngine, check) + } + resolve := func(_ context.Context, session store.Session) (map[string]any, error) { selected := raw if byHarness != nil { var ok bool @@ -56,5 +72,24 @@ func executionOptions() (func(context.Context, store.Session) (map[string]any, e var options map[string]any err := json.Unmarshal(selected, &options) return options, err - }, nil + } + return resolve, configured, nil +} + +func modelProviderEndpointConfigured(harness string, options map[string]any) bool { + key := map[string]string{"codex": "codex_provider", "claude_sdk": "claude_provider", "mcode": "mcode_provider"}[harness] + provider, ok := options[key].(map[string]any) + if !ok { + return false + } + if harness == "mcode" { + nativeOptions, ok := provider["options"].(map[string]any) + if !ok { + return false + } + baseURL, ok := nativeOptions["baseURL"].(string) + return ok && strings.TrimSpace(baseURL) != "" + } + baseURL, ok := provider["base_url"].(string) + return ok && strings.TrimSpace(baseURL) != "" } diff --git a/services/agents-api/cmd/server/execution_options_test.go b/services/agents-api/cmd/server/execution_options_test.go index 9d7a769b2..518bca84f 100644 --- a/services/agents-api/cmd/server/execution_options_test.go +++ b/services/agents-api/cmd/server/execution_options_test.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "os" "path/filepath" "strings" @@ -71,3 +72,50 @@ func TestExecutionOptionsSeparateHarnessCredentials(t *testing.T) { t.Fatal("legacy credentials crossed harness boundary") } } + +func TestExecutionOptionsConfigurationReportsOnlyEndpointPresence(t *testing.T) { + file := filepath.Join(t.TempDir(), "options.json") + t.Setenv("AGENTS_API_EXECUTION_OPTIONS_FILE", file) + t.Setenv("AGENTS_API_ENGINE", "codex") + raw := `{"by_harness":{"codex":{"codex_provider":{"base_url":"https://user:secret@example.test/v1?token=private","bearer_token":"codex-secret"}},"claude_sdk":{"claude_provider":{"base_url":" ","bearer_token":"claude-secret"}},"mcode":{"mcode_provider":{"options":{"baseURL":"https://mcode.example.test","apiKey":"mcode-secret"}}}}}` + if err := os.WriteFile(file, []byte(raw), 0600); err != nil { + t.Fatal(err) + } + _, configured, err := executionOptionsConfiguration() + if err != nil { + t.Fatal(err) + } + if !configured["codex"] || configured["claude_sdk"] || !configured["mcode"] { + t.Fatalf("endpoint presence = %#v", configured) + } + for _, value := range configured { + if value != true && value != false { + t.Fatal("non-boolean projection") + } + } + encoded, err := json.Marshal(coreStartupConfiguration("codex", []string{"claude_sdk", "codex", "mcode"}, true, configured, "", nil)) + if err != nil { + t.Fatal(err) + } + for _, private := range []string{"example.test", "user:secret", "token=private", "codex-secret", "claude-secret", "mcode-secret", "base_url", "baseURL", "apiKey", "bearer_token"} { + if strings.Contains(string(encoded), private) { + t.Fatalf("private execution option %q leaked into startup projection: %s", private, encoded) + } + } +} + +func TestExecutionOptionsConfigurationRejectsLegacyMCodeEndpointShape(t *testing.T) { + file := filepath.Join(t.TempDir(), "options.json") + t.Setenv("AGENTS_API_EXECUTION_OPTIONS_FILE", file) + t.Setenv("AGENTS_API_ENGINE", "mcode") + if err := os.WriteFile(file, []byte(`{"mcode_provider":{"base_url":"https://legacy.example.test","api_key":"private"}}`), 0600); err != nil { + t.Fatal(err) + } + _, configured, err := executionOptionsConfiguration() + if err != nil { + t.Fatal(err) + } + if configured["mcode"] { + t.Fatalf("legacy mcode endpoint shape reported as configured: %#v", configured) + } +} diff --git a/services/agents-api/cmd/server/main.go b/services/agents-api/cmd/server/main.go index e1fc46947..ae41f2323 100644 --- a/services/agents-api/cmd/server/main.go +++ b/services/agents-api/cmd/server/main.go @@ -76,12 +76,16 @@ func run() error { if engine == "" { engine = "codex" } - managed, closeManaged, err := managedRuntimes() + kinds, err := enabledHarnesses(engine) + if err != nil { + return err + } + managed, managedProviderKind, closeManaged, err := managedRuntimesWithKind() if err != nil { return err } defer closeManaged() - transientOptions, err := executionOptions() + transientOptions, modelProviderEndpoints, err := executionOptionsConfiguration() if err != nil { return err } @@ -123,15 +127,12 @@ func run() error { } }() options = append(options, api.WithExecution(worker), api.WithEnvironmentDirectoryReader(worker), api.WithEnvironmentFileWriter(worker)) - kinds, err := enabledHarnesses(engine) - if err != nil { - return err - } options = append(options, api.WithHarnesses(kinds)) if managed != nil { options = append(options, api.WithHostedEnvironments()) } } + options = append(options, api.WithStartupConfiguration(coreStartupConfiguration(engine, kinds, registry != nil, modelProviderEndpoints, managedProviderKind, managed))) handler, err := api.NewHandler(executionStore, auth, engine, options...) if err != nil { return err diff --git a/services/agents-api/cmd/server/managed_runtimes.go b/services/agents-api/cmd/server/managed_runtimes.go index db2f8b054..04922343d 100644 --- a/services/agents-api/cmd/server/managed_runtimes.go +++ b/services/agents-api/cmd/server/managed_runtimes.go @@ -38,75 +38,80 @@ type managedDockerConfig struct { // One deployment owns one explicit backend. Maintenance keeps that adapter // available for existing resources while Core blocks new compute admission. func managedRuntimes() (*execution.RuntimeProvider, func(), error) { + result, _, closeProvider, err := managedRuntimesWithKind() + return result, closeProvider, err +} + +func managedRuntimesWithKind() (*execution.RuntimeProvider, string, func(), error) { closeProvider := func() {} file := os.Getenv("AGENTS_API_MANAGED_RUNTIMES_FILE") if file == "" { - return nil, closeProvider, nil + return nil, "", closeProvider, nil } if os.Getenv("AGENTS_API_DAEMON_WS_URL") == "" { - return nil, closeProvider, errors.New("managed Runtime configuration requires AGENTS_API_DAEMON_WS_URL") + return nil, "", closeProvider, errors.New("managed Runtime configuration requires AGENTS_API_DAEMON_WS_URL") } raw, err := os.ReadFile(file) if err != nil { - return nil, closeProvider, errors.New("cannot read AGENTS_API_MANAGED_RUNTIMES_FILE") + return nil, "", closeProvider, errors.New("cannot read AGENTS_API_MANAGED_RUNTIMES_FILE") } var config managedRuntimeConfig decoder := json.NewDecoder(bytes.NewReader(raw)) decoder.DisallowUnknownFields() if decoder.Decode(&config) != nil || decoder.Decode(new(any)) != io.EOF { - return nil, closeProvider, errors.New("invalid managed Runtime configuration") + return nil, "", closeProvider, errors.New("invalid managed Runtime configuration") } var fields map[string]json.RawMessage if json.Unmarshal(raw, &fields) != nil { - return nil, closeProvider, errors.New("invalid managed Runtime configuration") + return nil, "", closeProvider, errors.New("invalid managed Runtime configuration") } _, hasDocker := fields["docker"] _, hasMicrosandbox := fields["microsandbox"] if maintenance, exists := fields["maintenance"]; exists && bytes.Equal(bytes.TrimSpace(maintenance), []byte("null")) { - return nil, closeProvider, errors.New("managed maintenance must be a boolean") + return nil, "", closeProvider, errors.New("managed maintenance must be a boolean") } id, err := uuid.Parse(config.InstallationID) if err != nil || id == uuid.Nil || id.String() != config.InstallationID { - return nil, closeProvider, errors.New("managed Runtime requires a canonical installation_id UUID") + return nil, "", closeProvider, errors.New("managed Runtime requires a canonical installation_id UUID") } result := &execution.RuntimeProvider{CoreURL: config.CoreURL, InstallationID: config.InstallationID, Maintenance: config.Maintenance} switch config.Provider { case "docker": if config.Docker == nil || !hasDocker || hasMicrosandbox { - return nil, closeProvider, errors.New("managed Docker requires only the docker configuration object") + return nil, "", closeProvider, errors.New("managed Docker requires only the docker configuration object") } entry := config.Docker host, err := url.Parse(entry.Host) if err != nil || host.Scheme != "unix" || host.Host != "" || host.User != nil || host.RawQuery != "" || host.Fragment != "" || host.RawPath != "" || host.Path == "/" || !filepath.IsAbs(host.Path) || filepath.Clean(host.Path) != host.Path || entry.Host != "unix://"+host.Path { - return nil, closeProvider, errors.New("managed Docker host must be an explicit canonical unix socket") + return nil, "", closeProvider, errors.New("managed Docker host must be an explicit canonical unix socket") } seccomp, err := os.ReadFile(entry.SeccompFile) if err != nil || !json.Valid(seccomp) { - return nil, closeProvider, errors.New("cannot read managed Docker seccomp JSON") + return nil, "", closeProvider, errors.New("cannot read managed Docker seccomp JSON") } c, err := client.New(client.WithHost(entry.Host)) if err != nil { - return nil, closeProvider, errors.New("invalid managed Docker endpoint") + return nil, "", closeProvider, errors.New("invalid managed Docker endpoint") } closeProvider = func() { _ = c.Close() } provider, err := sandboxdocker.New(c, sandboxdocker.Config{InstallationID: config.InstallationID, Image: entry.Image, Network: entry.Network, Seccomp: string(seccomp), ExtraHosts: entry.ExtraHosts, NestedSandbox: entry.NestedSandbox}) if err != nil { closeProvider() - return nil, func() {}, errors.New("invalid managed Docker provider configuration") + return nil, "", func() {}, errors.New("invalid managed Docker provider configuration") } result.Provider = provider result.BackendFingerprint = managedBackendFingerprint(config.Provider, entry.Host) case "microsandbox": if config.Microsandbox == nil || !hasMicrosandbox || hasDocker { - return nil, closeProvider, errors.New("managed microsandbox requires only the microsandbox configuration object") + return nil, "", closeProvider, errors.New("managed microsandbox requires only the microsandbox configuration object") } if err := configureManagedMicrosandbox(*config.Microsandbox, result); err != nil { - return nil, closeProvider, err + return nil, "", closeProvider, err } default: - return nil, closeProvider, errors.New("managed provider must be docker or microsandbox") + return nil, "", closeProvider, errors.New("managed provider must be docker or microsandbox") } - return result, closeProvider, nil + return result, config.Provider, closeProvider, nil } func managedBackendFingerprint(kind, namespace string) string { diff --git a/services/agents-api/cmd/server/startup_configuration.go b/services/agents-api/cmd/server/startup_configuration.go new file mode 100644 index 000000000..342558c35 --- /dev/null +++ b/services/agents-api/cmd/server/startup_configuration.go @@ -0,0 +1,29 @@ +package main + +import ( + v1 "github.com/MiniMax-AI-Dev/parsar/contracts/agents-api/v1" + "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/engine" + "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/execution" +) + +func coreStartupConfiguration(defaultHarness string, enabledHarnesses []string, daemonGateway bool, endpoints map[string]bool, managedProviderKind string, managed *execution.RuntimeProvider) v1.CoreStartupConfiguration { + modelProviders := make([]v1.CoreHarnessModelProviderConfiguration, 0, len(enabledHarnesses)) + for _, kind := range enabledHarnesses { + modelProviders = append(modelProviders, v1.CoreHarnessModelProviderConfiguration{Harness: kind, EndpointConfigured: endpoints[kind]}) + } + managedConfiguration := v1.CoreManagedSandboxConfiguration{} + if managed != nil { + provider := managedProviderKind + managedConfiguration = v1.CoreManagedSandboxConfiguration{Enabled: true, Provider: &provider, Maintenance: managed.Maintenance} + } + return v1.CoreStartupConfiguration{ + Object: "agents.core.startup_configuration", SchemaVersion: 1, + Supported: v1.CoreSupportedConfiguration{ + Harnesses: (engine.Catalog{}).Kinds(), ManagedSandboxProviders: []string{"docker", "microsandbox"}, + }, + Configured: v1.CoreConfiguredStartupConfiguration{ + DefaultHarness: defaultHarness, EnabledHarnesses: enabledHarnesses, DaemonGateway: daemonGateway, SelfHosted: daemonGateway, + ManagedSandbox: managedConfiguration, ModelProviders: modelProviders, + }, + } +} diff --git a/services/agents-api/cmd/server/startup_configuration_test.go b/services/agents-api/cmd/server/startup_configuration_test.go new file mode 100644 index 000000000..75619e52b --- /dev/null +++ b/services/agents-api/cmd/server/startup_configuration_test.go @@ -0,0 +1,43 @@ +package main + +import ( + "reflect" + "testing" + + "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/execution" +) + +func TestCoreStartupConfigurationSeparatesSupportAndConfiguration(t *testing.T) { + managed := &execution.RuntimeProvider{Maintenance: true} + got := coreStartupConfiguration("codex", []string{"claude_sdk", "codex"}, true, map[string]bool{"codex": true}, "docker", managed) + if got.Object != "agents.core.startup_configuration" || got.SchemaVersion != 1 || got.Configured.DefaultHarness != "codex" || !got.Configured.DaemonGateway || !got.Configured.SelfHosted { + t.Fatalf("unexpected configuration: %#v", got) + } + if !reflect.DeepEqual(got.Supported.Harnesses, []string{"claude_sdk", "codex", "mcode"}) || !reflect.DeepEqual(got.Supported.ManagedSandboxProviders, []string{"docker", "microsandbox"}) { + t.Fatalf("unexpected supported surface: %#v", got.Supported) + } + if got.Configured.ManagedSandbox.Provider == nil || *got.Configured.ManagedSandbox.Provider != "docker" || !got.Configured.ManagedSandbox.Enabled || !got.Configured.ManagedSandbox.Maintenance { + t.Fatalf("unexpected managed sandbox: %#v", got.Configured.ManagedSandbox) + } + if len(got.Configured.ModelProviders) != 2 || got.Configured.ModelProviders[0].EndpointConfigured || !got.Configured.ModelProviders[1].EndpointConfigured { + t.Fatalf("unexpected provider projection: %#v", got.Configured.ModelProviders) + } +} + +func TestCoreStartupConfigurationWithoutExecution(t *testing.T) { + got := coreStartupConfiguration("codex", []string{"codex"}, false, nil, "", nil) + if got.Configured.DaemonGateway || got.Configured.SelfHosted || got.Configured.ManagedSandbox.Enabled || got.Configured.ManagedSandbox.Provider != nil || got.Configured.ManagedSandbox.Maintenance || got.Configured.ModelProviders[0].EndpointConfigured { + t.Fatalf("unconfigured execution was reported: %#v", got.Configured) + } +} + +func TestCoreStartupConfigurationReportsMicrosandboxSelection(t *testing.T) { + managed := &execution.RuntimeProvider{} + got := coreStartupConfiguration("mcode", []string{"mcode"}, true, map[string]bool{"mcode": true}, "microsandbox", managed) + if got.Configured.ManagedSandbox.Provider == nil || *got.Configured.ManagedSandbox.Provider != "microsandbox" || !got.Configured.ManagedSandbox.Enabled { + t.Fatalf("unexpected managed sandbox: %#v", got.Configured.ManagedSandbox) + } + if len(got.Configured.ModelProviders) != 1 || !got.Configured.ModelProviders[0].EndpointConfigured { + t.Fatalf("unexpected model endpoint projection: %#v", got.Configured.ModelProviders) + } +} diff --git a/services/agents-api/internal/api/handler.go b/services/agents-api/internal/api/handler.go index bcbb51fc9..3a5824115 100644 --- a/services/agents-api/internal/api/handler.go +++ b/services/agents-api/internal/api/handler.go @@ -35,20 +35,21 @@ type ResourceStore interface { } type Handler struct { - policy execution.Policy - store ResourceStore - auth *Authenticator - harnesses map[string]bool - engine string - inputs InputSubmitter - executorURL string - hostedEnvironments bool - directoryReader EnvironmentDirectoryReader - fileWriter EnvironmentFileWriter - skills SkillStore - sourceFiles SourceFileStore - artifacts SessionArtifactStore - subagents SubagentStore + policy execution.Policy + store ResourceStore + auth *Authenticator + harnesses map[string]bool + engine string + inputs InputSubmitter + executorURL string + hostedEnvironments bool + directoryReader EnvironmentDirectoryReader + fileWriter EnvironmentFileWriter + skills SkillStore + sourceFiles SourceFileStore + artifacts SessionArtifactStore + subagents SubagentStore + startupConfiguration *v1.CoreStartupConfiguration } func NewHandler(s ResourceStore, auth *Authenticator, engine string, options ...Option) (http.Handler, error) { @@ -89,6 +90,7 @@ func NewHandler(s ResourceStore, auth *Authenticator, engine string, options ... r.Get("/agents/{agent_id}", h.getAgent) r.Post("/agents/{agent_id}", h.updateAgent) r.Delete("/agents/{agent_id}", h.deleteAgent) + r.Get("/agents/core/startup-configuration", h.getStartupConfiguration) r.Post("/agents/environments/templates", h.createEnvironmentTemplate) r.Get("/agents/environments/templates", h.listEnvironmentTemplates) r.Get("/agents/environments/templates/{environment_template_id}", h.getEnvironmentTemplate) diff --git a/services/agents-api/internal/api/startup_configuration.go b/services/agents-api/internal/api/startup_configuration.go new file mode 100644 index 000000000..2506644b3 --- /dev/null +++ b/services/agents-api/internal/api/startup_configuration.go @@ -0,0 +1,47 @@ +package api + +import ( + "net/http" + "slices" + + v1 "github.com/MiniMax-AI-Dev/parsar/contracts/agents-api/v1" +) + +// WithStartupConfiguration exposes a defensive copy of startup facts already +// validated by composition. It must not contain raw operator configuration. +func WithStartupConfiguration(configuration v1.CoreStartupConfiguration) Option { + return func(h *Handler) { + copy := configuration + copy.Supported.Harnesses = slices.Clone(configuration.Supported.Harnesses) + copy.Supported.ManagedSandboxProviders = slices.Clone(configuration.Supported.ManagedSandboxProviders) + copy.Configured.EnabledHarnesses = slices.Clone(configuration.Configured.EnabledHarnesses) + copy.Configured.ModelProviders = slices.Clone(configuration.Configured.ModelProviders) + if configuration.Configured.ManagedSandbox.Provider != nil { + provider := *configuration.Configured.ManagedSandbox.Provider + copy.Configured.ManagedSandbox.Provider = &provider + } + h.startupConfiguration = © + } +} + +// @Summary Retrieve safe Core startup configuration +// @Description Returns a secret-free snapshot of supported build capabilities and validated process startup selections. It does not inspect or aggregate daemon heartbeats, Sessions, Environments or Runtime state, and does not prove model-provider reachability, credentials, native readiness, sandbox isolation or successful execution. +// @Tags Core extensions +// @Produce json +// @Security BearerAuth +// @Param OpenAI-Beta header string true "agents=v1" +// @Success 200 {object} v1.CoreStartupConfiguration +// @Failure 400,401,503 {object} v1.ErrorResponse +// @Router /agents/core/startup-configuration [get] +func (h *Handler) getStartupConfiguration(w http.ResponseWriter, r *http.Request) { + if r.URL.RawQuery != "" { + writeError(w, http.StatusBadRequest, "unsupported_parameter", "Core startup configuration does not accept query parameters.") + return + } + if h.startupConfiguration == nil { + writeError(w, http.StatusServiceUnavailable, "startup_configuration_unavailable", "Core startup configuration is unavailable.") + return + } + w.Header().Set("Cache-Control", "no-store") + writeJSON(w, http.StatusOK, h.startupConfiguration) +} diff --git a/services/agents-api/internal/api/startup_configuration_test.go b/services/agents-api/internal/api/startup_configuration_test.go new file mode 100644 index 000000000..62da24d89 --- /dev/null +++ b/services/agents-api/internal/api/startup_configuration_test.go @@ -0,0 +1,75 @@ +package api + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + v1 "github.com/MiniMax-AI-Dev/parsar/contracts/agents-api/v1" +) + +func TestStartupConfigurationHTTP(t *testing.T) { + provider := "docker" + configuration := v1.CoreStartupConfiguration{ + Object: "agents.core.startup_configuration", SchemaVersion: 1, + Supported: v1.CoreSupportedConfiguration{Harnesses: []string{"claude_sdk", "codex", "mcode"}, ManagedSandboxProviders: []string{"docker", "microsandbox"}}, + Configured: v1.CoreConfiguredStartupConfiguration{ + DefaultHarness: "codex", EnabledHarnesses: []string{"claude_sdk", "codex"}, DaemonGateway: true, SelfHosted: true, + ManagedSandbox: v1.CoreManagedSandboxConfiguration{Enabled: true, Provider: &provider}, + ModelProviders: []v1.CoreHarnessModelProviderConfiguration{{Harness: "claude_sdk"}, {Harness: "codex", EndpointConfigured: true}}, + }, + } + h, _, _ := testHandler(t, WithStartupConfiguration(configuration)) + configuration.Supported.Harnesses[0] = "mutated" + configuration.Configured.EnabledHarnesses[0] = "mutated" + *configuration.Configured.ManagedSandbox.Provider = "mutated" + + request := httptest.NewRequest(http.MethodGet, "/v1/agents/core/startup-configuration", nil) + request.Header.Set("Authorization", "Bearer test-api-key") + request.Header.Set("OpenAI-Beta", "agents=v1") + w := httptest.NewRecorder() + h.ServeHTTP(w, request) + var response v1.CoreStartupConfiguration + if w.Code != http.StatusOK || json.Unmarshal(w.Body.Bytes(), &response) != nil { + t.Fatalf("response = %d %s", w.Code, w.Body) + } + if response.Object != "agents.core.startup_configuration" || response.SchemaVersion != 1 || response.Configured.DefaultHarness != "codex" || response.Configured.ManagedSandbox.Provider == nil || *response.Configured.ManagedSandbox.Provider != "docker" { + t.Fatalf("unexpected response: %#v", response) + } + if strings.Contains(w.Body.String(), "mutated") || strings.Contains(w.Body.String(), "base_url") || strings.Contains(w.Body.String(), "token") { + t.Fatalf("mutable or private configuration leaked: %s", w.Body) + } + if w.Header().Get("Cache-Control") != "no-store" { + t.Fatalf("Cache-Control = %q", w.Header().Get("Cache-Control")) + } +} + +func TestStartupConfigurationRejectsUnsupportedReads(t *testing.T) { + configured := v1.CoreStartupConfiguration{Object: "agents.core.startup_configuration", SchemaVersion: 1} + for _, test := range []struct { + name string + option []Option + path string + auth string + status int + }{ + {name: "missing auth", option: []Option{WithStartupConfiguration(configured)}, path: "/v1/agents/core/startup-configuration", status: http.StatusUnauthorized}, + {name: "query", option: []Option{WithStartupConfiguration(configured)}, path: "/v1/agents/core/startup-configuration?raw=true", auth: "Bearer test-api-key", status: http.StatusBadRequest}, + {name: "malformed query", option: []Option{WithStartupConfiguration(configured)}, path: "/v1/agents/core/startup-configuration?raw;private", auth: "Bearer test-api-key", status: http.StatusBadRequest}, + {name: "composition missing", path: "/v1/agents/core/startup-configuration", auth: "Bearer test-api-key", status: http.StatusServiceUnavailable}, + } { + t.Run(test.name, func(t *testing.T) { + h, _, _ := testHandler(t, test.option...) + request := httptest.NewRequest(http.MethodGet, test.path, nil) + request.Header.Set("Authorization", test.auth) + request.Header.Set("OpenAI-Beta", "agents=v1") + w := httptest.NewRecorder() + h.ServeHTTP(w, request) + if w.Code != test.status { + t.Fatalf("response = %d %s", w.Code, w.Body) + } + }) + } +} diff --git a/services/agents-api/internal/engine/profile.go b/services/agents-api/internal/engine/profile.go index ad9d5f7a7..79b655f67 100644 --- a/services/agents-api/internal/engine/profile.go +++ b/services/agents-api/internal/engine/profile.go @@ -2,6 +2,7 @@ package engine import ( "errors" + "maps" "slices" v1 "github.com/MiniMax-AI-Dev/parsar/contracts/agents-api/v1" @@ -53,6 +54,16 @@ func (c Catalog) Lookup(kind string) (Profile, bool) { return profile, ok } +// Kinds returns a stable snapshot of harnesses qualified by this build. +func (c Catalog) Kinds() []string { + if c.profiles == nil { + c = qualified + } + kinds := slices.Collect(maps.Keys(c.profiles)) + slices.Sort(kinds) + return kinds +} + var qualified = NewCatalog(map[string]Profile{ "codex": codexProfile(), "claude_sdk": claudeProfile(), diff --git a/services/agents-api/internal/engine/profile_test.go b/services/agents-api/internal/engine/profile_test.go index c793314fd..d95ea84d6 100644 --- a/services/agents-api/internal/engine/profile_test.go +++ b/services/agents-api/internal/engine/profile_test.go @@ -1,6 +1,9 @@ package engine -import "testing" +import ( + "reflect" + "testing" +) func TestCatalogOwnsQualification(t *testing.T) { placements := []string{"none"} @@ -27,6 +30,18 @@ func TestCatalogOwnsQualification(t *testing.T) { } } +func TestCatalogKindsAreStableAndDefensive(t *testing.T) { + catalog := NewCatalog(map[string]Profile{"zeta": {}, "alpha": {}}) + got := catalog.Kinds() + if !reflect.DeepEqual(got, []string{"alpha", "zeta"}) { + t.Fatalf("Kinds = %#v", got) + } + got[0] = "changed" + if again := catalog.Kinds(); !reflect.DeepEqual(again, []string{"alpha", "zeta"}) { + t.Fatalf("caller mutated catalog kinds: %#v", again) + } +} + func TestExplicitCatalogDoesNotInheritBuiltins(t *testing.T) { for _, catalog := range []Catalog{NewCatalog(nil), NewCatalog(map[string]Profile{"fixture": {Placements: []string{"none"}}})} { if _, ok := catalog.Lookup("codex"); ok { From c9994323a726adc25bd73fb5fa1d184795f85627 Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 22 Sep 2026 23:51:47 +0800 Subject: [PATCH 2/9] refactor: simplify startup configuration composition --- services/agents-api/cmd/server/main.go | 4 +- .../cmd/server/managed_microsandbox_test.go | 2 +- .../agents-api/cmd/server/managed_runtimes.go | 52 +++++++++++-------- .../cmd/server/managed_runtimes_test.go | 2 +- services/agents-api/internal/api/handler.go | 30 +++++------ .../internal/api/startup_configuration.go | 6 +-- 6 files changed, 53 insertions(+), 43 deletions(-) diff --git a/services/agents-api/cmd/server/main.go b/services/agents-api/cmd/server/main.go index ae41f2323..02761609a 100644 --- a/services/agents-api/cmd/server/main.go +++ b/services/agents-api/cmd/server/main.go @@ -80,7 +80,7 @@ func run() error { if err != nil { return err } - managed, managedProviderKind, closeManaged, err := managedRuntimesWithKind() + managed, closeManaged, err := managedRuntimes() if err != nil { return err } @@ -132,7 +132,7 @@ func run() error { options = append(options, api.WithHostedEnvironments()) } } - options = append(options, api.WithStartupConfiguration(coreStartupConfiguration(engine, kinds, registry != nil, modelProviderEndpoints, managedProviderKind, managed))) + options = append(options, api.WithStartupConfiguration(coreStartupConfiguration(engine, kinds, registry != nil, modelProviderEndpoints, managedRuntimeProviderKind(managed), managed))) handler, err := api.NewHandler(executionStore, auth, engine, options...) if err != nil { return err diff --git a/services/agents-api/cmd/server/managed_microsandbox_test.go b/services/agents-api/cmd/server/managed_microsandbox_test.go index ca0ba2f2e..a3d29839c 100644 --- a/services/agents-api/cmd/server/managed_microsandbox_test.go +++ b/services/agents-api/cmd/server/managed_microsandbox_test.go @@ -49,7 +49,7 @@ func TestManagedMicrosandboxConfigurationAndPolicy(t *testing.T) { t.Fatal(err) } defer close() - if result.InstallationID != key || result.Provider == nil || result.Maintenance { + if result.InstallationID != key || result.Provider == nil || result.Maintenance || managedRuntimeProviderKind(result) != "microsandbox" { t.Fatal("microsandbox identity lost") } if _, ok := result.Provider.(sandbox.CheckpointProvider); !ok { diff --git a/services/agents-api/cmd/server/managed_runtimes.go b/services/agents-api/cmd/server/managed_runtimes.go index 04922343d..642f7fbee 100644 --- a/services/agents-api/cmd/server/managed_runtimes.go +++ b/services/agents-api/cmd/server/managed_runtimes.go @@ -13,6 +13,7 @@ import ( "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/execution" sandboxdocker "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/sandbox/docker" + sandboxmicrosandbox "github.com/MiniMax-AI-Dev/parsar/services/agents-api/internal/sandbox/microsandbox" "github.com/google/uuid" "github.com/moby/moby/client" ) @@ -38,80 +39,89 @@ type managedDockerConfig struct { // One deployment owns one explicit backend. Maintenance keeps that adapter // available for existing resources while Core blocks new compute admission. func managedRuntimes() (*execution.RuntimeProvider, func(), error) { - result, _, closeProvider, err := managedRuntimesWithKind() - return result, closeProvider, err -} - -func managedRuntimesWithKind() (*execution.RuntimeProvider, string, func(), error) { closeProvider := func() {} file := os.Getenv("AGENTS_API_MANAGED_RUNTIMES_FILE") if file == "" { - return nil, "", closeProvider, nil + return nil, closeProvider, nil } if os.Getenv("AGENTS_API_DAEMON_WS_URL") == "" { - return nil, "", closeProvider, errors.New("managed Runtime configuration requires AGENTS_API_DAEMON_WS_URL") + return nil, closeProvider, errors.New("managed Runtime configuration requires AGENTS_API_DAEMON_WS_URL") } raw, err := os.ReadFile(file) if err != nil { - return nil, "", closeProvider, errors.New("cannot read AGENTS_API_MANAGED_RUNTIMES_FILE") + return nil, closeProvider, errors.New("cannot read AGENTS_API_MANAGED_RUNTIMES_FILE") } var config managedRuntimeConfig decoder := json.NewDecoder(bytes.NewReader(raw)) decoder.DisallowUnknownFields() if decoder.Decode(&config) != nil || decoder.Decode(new(any)) != io.EOF { - return nil, "", closeProvider, errors.New("invalid managed Runtime configuration") + return nil, closeProvider, errors.New("invalid managed Runtime configuration") } var fields map[string]json.RawMessage if json.Unmarshal(raw, &fields) != nil { - return nil, "", closeProvider, errors.New("invalid managed Runtime configuration") + return nil, closeProvider, errors.New("invalid managed Runtime configuration") } _, hasDocker := fields["docker"] _, hasMicrosandbox := fields["microsandbox"] if maintenance, exists := fields["maintenance"]; exists && bytes.Equal(bytes.TrimSpace(maintenance), []byte("null")) { - return nil, "", closeProvider, errors.New("managed maintenance must be a boolean") + return nil, closeProvider, errors.New("managed maintenance must be a boolean") } id, err := uuid.Parse(config.InstallationID) if err != nil || id == uuid.Nil || id.String() != config.InstallationID { - return nil, "", closeProvider, errors.New("managed Runtime requires a canonical installation_id UUID") + return nil, closeProvider, errors.New("managed Runtime requires a canonical installation_id UUID") } result := &execution.RuntimeProvider{CoreURL: config.CoreURL, InstallationID: config.InstallationID, Maintenance: config.Maintenance} switch config.Provider { case "docker": if config.Docker == nil || !hasDocker || hasMicrosandbox { - return nil, "", closeProvider, errors.New("managed Docker requires only the docker configuration object") + return nil, closeProvider, errors.New("managed Docker requires only the docker configuration object") } entry := config.Docker host, err := url.Parse(entry.Host) if err != nil || host.Scheme != "unix" || host.Host != "" || host.User != nil || host.RawQuery != "" || host.Fragment != "" || host.RawPath != "" || host.Path == "/" || !filepath.IsAbs(host.Path) || filepath.Clean(host.Path) != host.Path || entry.Host != "unix://"+host.Path { - return nil, "", closeProvider, errors.New("managed Docker host must be an explicit canonical unix socket") + return nil, closeProvider, errors.New("managed Docker host must be an explicit canonical unix socket") } seccomp, err := os.ReadFile(entry.SeccompFile) if err != nil || !json.Valid(seccomp) { - return nil, "", closeProvider, errors.New("cannot read managed Docker seccomp JSON") + return nil, closeProvider, errors.New("cannot read managed Docker seccomp JSON") } c, err := client.New(client.WithHost(entry.Host)) if err != nil { - return nil, "", closeProvider, errors.New("invalid managed Docker endpoint") + return nil, closeProvider, errors.New("invalid managed Docker endpoint") } closeProvider = func() { _ = c.Close() } provider, err := sandboxdocker.New(c, sandboxdocker.Config{InstallationID: config.InstallationID, Image: entry.Image, Network: entry.Network, Seccomp: string(seccomp), ExtraHosts: entry.ExtraHosts, NestedSandbox: entry.NestedSandbox}) if err != nil { closeProvider() - return nil, "", func() {}, errors.New("invalid managed Docker provider configuration") + return nil, func() {}, errors.New("invalid managed Docker provider configuration") } result.Provider = provider result.BackendFingerprint = managedBackendFingerprint(config.Provider, entry.Host) case "microsandbox": if config.Microsandbox == nil || !hasMicrosandbox || hasDocker { - return nil, "", closeProvider, errors.New("managed microsandbox requires only the microsandbox configuration object") + return nil, closeProvider, errors.New("managed microsandbox requires only the microsandbox configuration object") } if err := configureManagedMicrosandbox(*config.Microsandbox, result); err != nil { - return nil, "", closeProvider, err + return nil, closeProvider, err } default: - return nil, "", closeProvider, errors.New("managed provider must be docker or microsandbox") + return nil, closeProvider, errors.New("managed provider must be docker or microsandbox") + } + return result, closeProvider, nil +} + +func managedRuntimeProviderKind(runtime *execution.RuntimeProvider) string { + if runtime == nil { + return "" + } + switch runtime.Provider.(type) { + case *sandboxdocker.Provider: + return "docker" + case *sandboxmicrosandbox.Provider: + return "microsandbox" + default: + return "" } - return result, config.Provider, closeProvider, nil } func managedBackendFingerprint(kind, namespace string) string { diff --git a/services/agents-api/cmd/server/managed_runtimes_test.go b/services/agents-api/cmd/server/managed_runtimes_test.go index b322bffbf..2082929f9 100644 --- a/services/agents-api/cmd/server/managed_runtimes_test.go +++ b/services/agents-api/cmd/server/managed_runtimes_test.go @@ -38,7 +38,7 @@ func TestManagedRuntimeOperatorConfigurationIsExplicit(t *testing.T) { t.Fatal(err) } close() - if result.InstallationID != config.InstallationID || result.Provider == nil || result.Suspension != nil || result.Maintenance { + if result.InstallationID != config.InstallationID || result.Provider == nil || result.Suspension != nil || result.Maintenance || managedRuntimeProviderKind(result) != "docker" { t.Fatal("provider identity or admission policy lost") } for _, mutate := range []func(*managedRuntimeConfig){ diff --git a/services/agents-api/internal/api/handler.go b/services/agents-api/internal/api/handler.go index 3a5824115..a564fd212 100644 --- a/services/agents-api/internal/api/handler.go +++ b/services/agents-api/internal/api/handler.go @@ -35,21 +35,21 @@ type ResourceStore interface { } type Handler struct { - policy execution.Policy - store ResourceStore - auth *Authenticator - harnesses map[string]bool - engine string - inputs InputSubmitter - executorURL string - hostedEnvironments bool - directoryReader EnvironmentDirectoryReader - fileWriter EnvironmentFileWriter - skills SkillStore - sourceFiles SourceFileStore - artifacts SessionArtifactStore - subagents SubagentStore - startupConfiguration *v1.CoreStartupConfiguration + policy execution.Policy + store ResourceStore + auth *Authenticator + harnesses map[string]bool + engine string + inputs InputSubmitter + executorURL string + hostedEnvironments bool + directoryReader EnvironmentDirectoryReader + fileWriter EnvironmentFileWriter + skills SkillStore + sourceFiles SourceFileStore + artifacts SessionArtifactStore + subagents SubagentStore + startup *v1.CoreStartupConfiguration } func NewHandler(s ResourceStore, auth *Authenticator, engine string, options ...Option) (http.Handler, error) { diff --git a/services/agents-api/internal/api/startup_configuration.go b/services/agents-api/internal/api/startup_configuration.go index 2506644b3..15445a8bd 100644 --- a/services/agents-api/internal/api/startup_configuration.go +++ b/services/agents-api/internal/api/startup_configuration.go @@ -20,7 +20,7 @@ func WithStartupConfiguration(configuration v1.CoreStartupConfiguration) Option provider := *configuration.Configured.ManagedSandbox.Provider copy.Configured.ManagedSandbox.Provider = &provider } - h.startupConfiguration = © + h.startup = © } } @@ -38,10 +38,10 @@ func (h *Handler) getStartupConfiguration(w http.ResponseWriter, r *http.Request writeError(w, http.StatusBadRequest, "unsupported_parameter", "Core startup configuration does not accept query parameters.") return } - if h.startupConfiguration == nil { + if h.startup == nil { writeError(w, http.StatusServiceUnavailable, "startup_configuration_unavailable", "Core startup configuration is unavailable.") return } w.Header().Set("Cache-Control", "no-store") - writeJSON(w, http.StatusOK, h.startupConfiguration) + writeJSON(w, http.StatusOK, h.startup) } From 09f4419d03319a8cbb22722e5fe42040ebc85f40 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 23 Sep 2026 00:26:05 +0800 Subject: [PATCH 3/9] fix: clarify startup capability status --- apps/web/e2e/agents-lifecycle.spec.ts | 20 ++++++++-- apps/web/e2e/core-connection.spec.ts | 6 +-- apps/web/src/features/system/SystemView.css | 12 ++++++ .../src/features/system/SystemView.test.tsx | 34 ++++++++++++++-- apps/web/src/features/system/SystemView.tsx | 39 +++++++++++++------ apps/web/src/style.css | 5 ++- packages/agents-client/src/client.ts | 8 +++- .../src/startup-configuration.test.ts | 13 +++++-- .../cmd/server/startup_configuration.go | 4 ++ .../cmd/server/startup_configuration_test.go | 5 ++- 10 files changed, 115 insertions(+), 31 deletions(-) diff --git a/apps/web/e2e/agents-lifecycle.spec.ts b/apps/web/e2e/agents-lifecycle.spec.ts index 08e3f41c7..6f3590aa0 100644 --- a/apps/web/e2e/agents-lifecycle.spec.ts +++ b/apps/web/e2e/agents-lifecycle.spec.ts @@ -2373,14 +2373,15 @@ test("presents Dashboard page-chain results and System boundaries without extra const system = page.locator(".system-page"); await expect(system.getByRole("listitem").filter({ hasText: "Core API" })).toContainText("Available"); await expect(system.getByRole("listitem").filter({ hasText: "Vaults" })).toContainText("Available"); - await expect(system.getByRole("listitem").filter({ hasText: "Default harness" })).toContainText("Codex"); + await expect(system.getByRole("listitem").filter({ hasText: "Default adapter" })).toContainText("Codex"); await expect(system.getByRole("listitem").filter({ hasText: "Managed sandbox" })).toContainText("Docker"); - await expect(system.getByRole("listitem").filter({ hasText: "LLM endpoints" })).toContainText("1/2 configured"); + await expect(system.getByRole("listitem").filter({ hasText: "Endpoint overrides" })).toContainText("1 explicit"); await expect(system.getByRole("listitem")).toHaveCount(5); await expect(system).toContainText("Configured for this process"); await expect(system).toContainText("Daemon gateway"); - await expect(system).toContainText("Operator LLM endpoint: configured"); - await expect(system).toContainText("Known by this build but not enabled for this process"); + await expect(system).toContainText("Agents created in this Web UI use Codex"); + await expect(system).toContainText("Operator endpoint override: configured"); + await expect(system).toContainText("Compiled into this build, but not enabled when this Core process started"); await expect(system).toContainText("Runtime connection, native binary availability, sandbox health, and model execution belong to the relevant Session or Environment"); await expect(system).not.toContainText("Source Files"); await expect(system).not.toContainText("Public capability surface"); @@ -2405,6 +2406,17 @@ test("presents Dashboard page-chain results and System boundaries without extra for (const path of detailPaths) expect(count(afterSystemRefresh, path)).toBe(count(beforeSystemRefresh, path)); await attachScreenshot(page, testInfo, "desktop-system-contract-boundary"); + await page.setViewportSize({ width: 778, height: 844 }); + const compactSystemBounds = await system.evaluate((element) => ({ + viewportWidth: innerWidth, + documentWidth: document.documentElement.scrollWidth, + bodyWidth: document.body.scrollWidth, + right: element.getBoundingClientRect().right, + })); + expect(compactSystemBounds.documentWidth).toBeLessThanOrEqual(compactSystemBounds.viewportWidth); + expect(compactSystemBounds.bodyWidth).toBeLessThanOrEqual(compactSystemBounds.viewportWidth); + expect(compactSystemBounds.right).toBeLessThanOrEqual(compactSystemBounds.viewportWidth); + await page.setViewportSize({ width: 390, height: 844 }); const systemBounds = await system.evaluate((element) => { const rows = [...element.querySelectorAll(".system-summary-cell")].map((row) => row.getBoundingClientRect()); diff --git a/apps/web/e2e/core-connection.spec.ts b/apps/web/e2e/core-connection.spec.ts index 401a311d3..8a8b045a6 100644 --- a/apps/web/e2e/core-connection.spec.ts +++ b/apps/web/e2e/core-connection.spec.ts @@ -318,7 +318,7 @@ test("clears startup configuration synchronously and fences a stale read when th await boot(page, request); await page.getByRole("button", { name: "System", exact: true }).click(); const system = page.locator(".system-page"); - await expect(system.getByRole("listitem").filter({ hasText: "Default harness" })).toContainText("Codex"); + await expect(system.getByRole("listitem").filter({ hasText: "Default adapter" })).toContainText("Codex"); await page.evaluate(() => { (window as ProbeInstrumentationWindow).__holdNextLocalStartup = true; }); @@ -331,7 +331,7 @@ test("clears startup configuration synchronously and fences a stale read when th await dialog.getByLabel("Bearer token").fill("replacement-token"); await dialog.getByRole("button", { name: "Apply connection" }).click(); - const defaultHarness = system.getByRole("listitem").filter({ hasText: "Default harness" }); + const defaultHarness = system.getByRole("listitem").filter({ hasText: "Default adapter" }); await expect(defaultHarness).toContainText("Checking…"); await expect(system).not.toContainText("Configured for this process"); await expect.poll(() => page.evaluate(() => (window as ProbeInstrumentationWindow).__oldStartupAbortCount ?? 0)).toBe(1); @@ -356,7 +356,7 @@ for (const status of [404, 405]) { await page.getByRole("button", { name: "System", exact: true }).click(); const system = page.locator(".system-page"); - await expect(system.getByRole("listitem").filter({ hasText: "Default harness" })).toContainText("Not exposed"); + await expect(system.getByRole("listitem").filter({ hasText: "Default adapter" })).toContainText("Not exposed"); await expect(system).toContainText("This Core version does not expose the startup configuration extension"); await expect(system).not.toContainText("Configured for this process"); await expect(system).not.toContainText("Checking…"); diff --git a/apps/web/src/features/system/SystemView.css b/apps/web/src/features/system/SystemView.css index f00298c55..6d79282f0 100644 --- a/apps/web/src/features/system/SystemView.css +++ b/apps/web/src/features/system/SystemView.css @@ -61,6 +61,11 @@ font-weight: 500; } +.system-config-section > header h2 span { + color: var(--fg-muted); + font-weight: 400; +} + .system-config-section > header span { color: var(--fg-muted); font-size: 11px; @@ -132,6 +137,13 @@ gap: 12px; } +.system-config-explanation { + margin: -2px 0 12px; + color: var(--fg-muted); + font-size: 11px; + line-height: 16px; +} + .system-harness-card { min-width: 0; padding: 15px; diff --git a/apps/web/src/features/system/SystemView.test.tsx b/apps/web/src/features/system/SystemView.test.tsx index 877d9543f..dcefa0206 100644 --- a/apps/web/src/features/system/SystemView.test.tsx +++ b/apps/web/src/features/system/SystemView.test.tsx @@ -51,16 +51,21 @@ describe("SystemView", () => { expect(html).toContain("Core startup configuration"); expect(html.match(/role="listitem"/g)).toHaveLength(5); expect(html).toContain("Vault catalog loaded"); - expect(html).toContain("Default harness"); + expect(html).toContain("Default adapter"); expect(html).toContain("Managed sandbox"); - expect(html).toContain("LLM endpoints"); - expect(html).toContain("1/2 configured"); + expect(html).toContain("Endpoint overrides"); + expect(html).toContain("1 explicit"); expect(html).toContain("Configured for this process"); expect(html).toContain("Daemon gateway"); expect(html).toContain("Supported by this build: Docker, Microsandbox"); expect(html).toContain("Claude SDK"); expect(html).toContain("MiniMax Code"); - expect(html).toContain("Operator LLM endpoint: configured"); + expect(html).toContain("Execution adapters"); + expect(html).toContain("Agents created in this Web UI use Codex"); + expect(html).toContain("Enabled · default"); + expect(html).toContain("Operator endpoint override: configured"); + expect(html).toContain("Operator endpoint override: not set; the harness may use its native default"); + expect(html).toContain("Build only"); expect(html).toContain("Runtime connection, native binary availability, sandbox health, and model execution belong to the relevant Session or Environment"); expect(html).not.toContain("Runtime status"); expect(html).not.toContain("ready"); @@ -85,6 +90,27 @@ describe("SystemView", () => { expect(html).toContain("This Web build cannot request self-hosted Sessions"); }); + it("distinguishes build support from an inactive execution surface", () => { + const inactive: CoreStartupConfiguration = { + ...startup, + configured: { + ...startup.configured, + enabled_harnesses: [], + daemon_gateway: false, + self_hosted: false, + managed_sandbox: { enabled: false, provider: null, maintenance: false }, + model_providers: [], + }, + }; + const html = render({ startupConfiguration: inactive }); + + expect(html).toContain("0 explicit"); + expect(html).toContain("Across 0 startup-enabled adapters"); + expect(html).toContain("Codex is the configured default, but execution adapters are inactive because this Core process has no daemon gateway"); + expect(html.match(/Build only/g)).toHaveLength(3); + expect(html).not.toContain("Enabled · default"); + }); + it("handles an older Core without leaving a pending state", () => { const html = render({ startupConfiguration: null, diff --git a/apps/web/src/features/system/SystemView.tsx b/apps/web/src/features/system/SystemView.tsx index c64cb2e7a..99a87f45d 100644 --- a/apps/web/src/features/system/SystemView.tsx +++ b/apps/web/src/features/system/SystemView.tsx @@ -93,6 +93,7 @@ export function SystemView({ : null; const configuredEndpoints = configuration?.configured.model_providers.filter((provider) => provider.endpoint_configured).length ?? 0; const endpointTotal = configuration?.configured.model_providers.length ?? 0; + const executionAdaptersEnabled = (configuration?.configured.enabled_harnesses.length ?? 0) > 0; const startupUnavailable = startupConfigurationSupported === false; const startupValue = (value: string): string => { @@ -134,10 +135,14 @@ export function SystemView({ }, vaultStatus, { - label: "Default harness", + label: "Default adapter", status: startupStatus, value: startupValue(configuration ? harnessLabel(configuration.configured.default_harness) : ""), - detail: startupDetail, + detail: configuration + ? executionAdaptersEnabled + ? "Used by Agents created in this Web UI unless an API request selects another enabled harness." + : "Configured default; execution adapters are inactive because this Core process has no daemon gateway." + : startupDetail, }, { label: "Managed sandbox", @@ -148,13 +153,11 @@ export function SystemView({ : startupDetail, }, { - label: "LLM endpoints", + label: "Endpoint overrides", status: startupStatus, - value: startupValue(endpointTotal === 0 || configuredEndpoints === 0 - ? "Not configured" - : configuredEndpoints === endpointTotal ? "Configured" : `${configuredEndpoints}/${endpointTotal} configured`), + value: startupValue(`${configuredEndpoints} explicit`), detail: configuration - ? "Reports operator endpoint configuration presence only; addresses and credentials stay hidden." + ? `Across ${endpointTotal} startup-enabled adapter${endpointTotal === 1 ? "" : "s"}. No override may mean the harness uses its native default.` : startupDetail, }, ]; @@ -223,9 +226,14 @@ export function SystemView({
-

Harnesses

- Configuration, not execution readiness +

Execution adapters (harnesses)

+ Build support vs startup enablement
+

+ {executionAdaptersEnabled + ? <>Agents created in this Web UI use {harnessLabel(configuration.configured.default_harness)}. Another enabled adapter can be selected through the API extension; this UI does not expose that control. + : <>{harnessLabel(configuration.configured.default_harness)} is the configured default, but execution adapters are inactive because this Core process has no daemon gateway.} +

{configuration.supported.harnesses.map((harness) => { const enabled = configuration.configured.enabled_harnesses.includes(harness); @@ -234,12 +242,19 @@ export function SystemView({
{harnessLabel(harness)} - +
{enabled - ? `Operator LLM endpoint: ${endpoint?.endpoint_configured ? "configured" : "not configured"}.` - : "Known by this build but not enabled for this process."} + ? endpoint?.endpoint_configured + ? "Operator endpoint override: configured." + : "Operator endpoint override: not set; the harness may use its native default." + : "Compiled into this build, but not enabled when this Core process started."}
); diff --git a/apps/web/src/style.css b/apps/web/src/style.css index e495c7940..98ce98804 100644 --- a/apps/web/src/style.css +++ b/apps/web/src/style.css @@ -4332,10 +4332,13 @@ button.trace-step-row:hover { overflow-wrap: anywhere; } -@media (max-width: 640px) { +@media (max-width: 960px) { body { min-width: 0; } +} + +@media (max-width: 640px) { .app-sidebar { width: 52px; diff --git a/packages/agents-client/src/client.ts b/packages/agents-client/src/client.ts index b856ab77d..504e0d9f5 100644 --- a/packages/agents-client/src/client.ts +++ b/packages/agents-client/src/client.ts @@ -130,8 +130,7 @@ function projectStartupConfiguration(value: unknown): CoreStartupConfiguration { !isRecord(value.supported) || !exactFields(value.supported, startupSupportedFields) || !sortedUnique(value.supported.harnesses, isHarnessKind) || !sortedUnique(value.supported.managed_sandbox_providers, isSandboxProvider) || !isRecord(value.configured) || !exactFields(value.configured, startupConfiguredFields) || - !isHarnessKind(value.configured.default_harness) || !sortedUnique(value.configured.enabled_harnesses, isHarnessKind) || value.configured.enabled_harnesses.length === 0 || - !value.configured.enabled_harnesses.includes(value.configured.default_harness) || typeof value.configured.daemon_gateway !== "boolean" || + !isHarnessKind(value.configured.default_harness) || !sortedUnique(value.configured.enabled_harnesses, isHarnessKind) || typeof value.configured.daemon_gateway !== "boolean" || typeof value.configured.self_hosted !== "boolean" || value.configured.self_hosted !== value.configured.daemon_gateway || !isRecord(value.configured.managed_sandbox) || !exactFields(value.configured.managed_sandbox, startupManagedSandboxFields) || typeof value.configured.managed_sandbox.enabled !== "boolean" || typeof value.configured.managed_sandbox.maintenance !== "boolean" || @@ -143,6 +142,11 @@ function projectStartupConfiguration(value: unknown): CoreStartupConfiguration { const supportedHarnesses = value.supported.harnesses as CoreHarnessKind[]; const supportedSandboxProviders = value.supported.managed_sandbox_providers as CoreManagedSandboxProvider[]; const enabledHarnesses = configured.enabled_harnesses as CoreHarnessKind[]; + if (configured.daemon_gateway + ? !enabledHarnesses.includes(configured.default_harness as CoreHarnessKind) + : enabledHarnesses.length !== 0) { + return invalidStartupConfiguration(); + } if (managed.enabled ? !isSandboxProvider(managed.provider) || !supportedSandboxProviders.includes(managed.provider) || !configured.daemon_gateway : managed.provider !== null || managed.maintenance) { diff --git a/packages/agents-client/src/startup-configuration.test.ts b/packages/agents-client/src/startup-configuration.test.ts index 5ac49bc6f..a0148b9d1 100644 --- a/packages/agents-client/src/startup-configuration.test.ts +++ b/packages/agents-client/src/startup-configuration.test.ts @@ -56,6 +56,11 @@ describe("Core startup configuration", () => { ["unsorted harnesses", (value: any) => { value.supported.harnesses.reverse(); }], ["enabled harness missing from supported", (value: any) => { value.supported.harnesses = ["codex", "mcode"]; }], ["default not enabled", (value: any) => { value.configured.enabled_harnesses = ["claude_sdk"]; value.configured.model_providers = [{ harness: "claude_sdk", endpoint_configured: false }]; }], + ["enabled harness without gateway", (value: any) => { + value.configured.daemon_gateway = false; + value.configured.self_hosted = false; + value.configured.managed_sandbox = { enabled: false, provider: null, maintenance: false }; + }], ["managed provider without gateway", (value: any) => { value.configured.daemon_gateway = false; value.configured.self_hosted = false; }], ["managed provider missing from supported", (value: any) => { value.supported.managed_sandbox_providers = ["microsandbox"]; }], ["mismatched provider projection", (value: any) => { value.configured.model_providers[0].harness = "codex"; }], @@ -82,10 +87,12 @@ describe("Core startup configuration", () => { body.configured.daemon_gateway = false; body.configured.self_hosted = false; body.configured.managed_sandbox = { enabled: false, provider: null, maintenance: false }; - body.configured.enabled_harnesses = ["codex"]; - body.configured.model_providers = [{ harness: "codex", endpoint_configured: false }]; + body.configured.enabled_harnesses = []; + body.configured.model_providers = []; const client = new OpenAIAgentsClient({ fetch: (async () => response(body)) as typeof fetch }); - await expect(client.retrieveStartupConfiguration()).resolves.toMatchObject({ configured: { daemon_gateway: false } }); + await expect(client.retrieveStartupConfiguration()).resolves.toMatchObject({ + configured: { daemon_gateway: false, enabled_harnesses: [], model_providers: [] }, + }); }); }); diff --git a/services/agents-api/cmd/server/startup_configuration.go b/services/agents-api/cmd/server/startup_configuration.go index 342558c35..a30d3c79d 100644 --- a/services/agents-api/cmd/server/startup_configuration.go +++ b/services/agents-api/cmd/server/startup_configuration.go @@ -7,6 +7,10 @@ import ( ) func coreStartupConfiguration(defaultHarness string, enabledHarnesses []string, daemonGateway bool, endpoints map[string]bool, managedProviderKind string, managed *execution.RuntimeProvider) v1.CoreStartupConfiguration { + if !daemonGateway { + enabledHarnesses = []string{} + managed = nil + } modelProviders := make([]v1.CoreHarnessModelProviderConfiguration, 0, len(enabledHarnesses)) for _, kind := range enabledHarnesses { modelProviders = append(modelProviders, v1.CoreHarnessModelProviderConfiguration{Harness: kind, EndpointConfigured: endpoints[kind]}) diff --git a/services/agents-api/cmd/server/startup_configuration_test.go b/services/agents-api/cmd/server/startup_configuration_test.go index 75619e52b..897694f0d 100644 --- a/services/agents-api/cmd/server/startup_configuration_test.go +++ b/services/agents-api/cmd/server/startup_configuration_test.go @@ -25,8 +25,9 @@ func TestCoreStartupConfigurationSeparatesSupportAndConfiguration(t *testing.T) } func TestCoreStartupConfigurationWithoutExecution(t *testing.T) { - got := coreStartupConfiguration("codex", []string{"codex"}, false, nil, "", nil) - if got.Configured.DaemonGateway || got.Configured.SelfHosted || got.Configured.ManagedSandbox.Enabled || got.Configured.ManagedSandbox.Provider != nil || got.Configured.ManagedSandbox.Maintenance || got.Configured.ModelProviders[0].EndpointConfigured { + managed := &execution.RuntimeProvider{Maintenance: true} + got := coreStartupConfiguration("codex", []string{"claude_sdk", "codex"}, false, map[string]bool{"codex": true}, "docker", managed) + if got.Configured.DaemonGateway || got.Configured.SelfHosted || got.Configured.EnabledHarnesses == nil || len(got.Configured.EnabledHarnesses) != 0 || got.Configured.ManagedSandbox.Enabled || got.Configured.ManagedSandbox.Provider != nil || got.Configured.ManagedSandbox.Maintenance || len(got.Configured.ModelProviders) != 0 { t.Fatalf("unconfigured execution was reported: %#v", got.Configured) } } From c06f1fc0c8168f933b76f1be2103ee88859cb30e Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 23 Sep 2026 07:22:10 +0800 Subject: [PATCH 4/9] fix: align System configuration sections --- apps/web/e2e/agents-lifecycle.spec.ts | 22 +++++++++++++++++++++ apps/web/src/features/system/SystemView.css | 15 ++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/apps/web/e2e/agents-lifecycle.spec.ts b/apps/web/e2e/agents-lifecycle.spec.ts index 6f3590aa0..07323e93f 100644 --- a/apps/web/e2e/agents-lifecycle.spec.ts +++ b/apps/web/e2e/agents-lifecycle.spec.ts @@ -2412,10 +2412,26 @@ test("presents Dashboard page-chain results and System boundaries without extra documentWidth: document.documentElement.scrollWidth, bodyWidth: document.body.scrollWidth, right: element.getBoundingClientRect().right, + sectionInset: element.querySelector(".system-config-section")!.getBoundingClientRect().left - element.getBoundingClientRect().left, + sectionEdges: [...element.querySelectorAll(".system-config-section")].map((section) => ({ + left: section.getBoundingClientRect().left - element.getBoundingClientRect().left, + right: element.getBoundingClientRect().right - section.getBoundingClientRect().right, + header: section.querySelector("header")!.getBoundingClientRect().left, + content: section.querySelector(".system-config-list, .system-harness-grid")!.getBoundingClientRect().left, + explanation: section.querySelector(".system-config-explanation")?.getBoundingClientRect().left ?? null, + })), + boundaryInset: element.querySelector(".system-boundary-note")!.getBoundingClientRect().left - element.getBoundingClientRect().left, })); expect(compactSystemBounds.documentWidth).toBeLessThanOrEqual(compactSystemBounds.viewportWidth); expect(compactSystemBounds.bodyWidth).toBeLessThanOrEqual(compactSystemBounds.viewportWidth); expect(compactSystemBounds.right).toBeLessThanOrEqual(compactSystemBounds.viewportWidth); + expect(compactSystemBounds.sectionInset).toBe(24); + expect(compactSystemBounds.boundaryInset).toBe(24); + for (const edge of compactSystemBounds.sectionEdges) { + expect({ left: edge.left, right: edge.right }).toEqual({ left: 24, right: 24 }); + expect(edge.header).toBe(edge.content); + if (edge.explanation !== null) expect(edge.explanation).toBe(edge.header); + } await page.setViewportSize({ width: 390, height: 844 }); const systemBounds = await system.evaluate((element) => { @@ -2423,10 +2439,16 @@ test("presents Dashboard page-chain results and System boundaries without extra return { viewportWidth: innerWidth, documentWidth: document.documentElement.scrollWidth, + sectionInset: element.querySelector(".system-config-section")!.getBoundingClientRect().left - element.getBoundingClientRect().left, + sectionRightInset: element.getBoundingClientRect().right - element.querySelector(".system-config-section")!.getBoundingClientRect().right, + boundaryInset: element.querySelector(".system-boundary-note")!.getBoundingClientRect().left - element.getBoundingClientRect().left, rowBounds: rows.map((row) => ({ top: row.top, bottom: row.bottom, height: row.height })), }; }); expect(systemBounds.documentWidth).toBeLessThanOrEqual(systemBounds.viewportWidth); + expect(systemBounds.sectionInset).toBe(12); + expect(systemBounds.sectionRightInset).toBe(12); + expect(systemBounds.boundaryInset).toBe(12); for (let index = 1; index < systemBounds.rowBounds.length; index += 1) { expect(systemBounds.rowBounds[index]!.top).toBeGreaterThanOrEqual(systemBounds.rowBounds[index - 1]!.bottom); } diff --git a/apps/web/src/features/system/SystemView.css b/apps/web/src/features/system/SystemView.css index 6d79282f0..873fc2fa7 100644 --- a/apps/web/src/features/system/SystemView.css +++ b/apps/web/src/features/system/SystemView.css @@ -43,7 +43,7 @@ } .system-config-section { - margin-top: 30px; + margin: 30px 24px 0; } .system-config-section > header { @@ -168,7 +168,7 @@ display: flex; align-items: flex-start; gap: 9px; - margin: 24px 0 0; + margin: 24px 24px 30px; padding: 13px 15px; border-radius: 9px; background: var(--surface-subtle); @@ -214,6 +214,17 @@ } @media (max-width: 560px) { + .system-config-section { + margin-right: 12px; + margin-left: 12px; + } + + .system-boundary-note { + margin-right: 12px; + margin-bottom: 20px; + margin-left: 12px; + } + .system-summary { grid-template-columns: 1fr; } From 947387aeb77a0f93520fff9796381e46f85541d9 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 23 Sep 2026 07:52:31 +0800 Subject: [PATCH 5/9] fix: simplify endpoint override summary --- apps/web/e2e/agents-lifecycle.spec.ts | 2 +- .../src/features/system/SystemView.test.tsx | 27 ++++++++++++++++--- apps/web/src/features/system/SystemView.tsx | 11 +++++--- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/apps/web/e2e/agents-lifecycle.spec.ts b/apps/web/e2e/agents-lifecycle.spec.ts index 07323e93f..a78d98139 100644 --- a/apps/web/e2e/agents-lifecycle.spec.ts +++ b/apps/web/e2e/agents-lifecycle.spec.ts @@ -2375,7 +2375,7 @@ test("presents Dashboard page-chain results and System boundaries without extra await expect(system.getByRole("listitem").filter({ hasText: "Vaults" })).toContainText("Available"); await expect(system.getByRole("listitem").filter({ hasText: "Default adapter" })).toContainText("Codex"); await expect(system.getByRole("listitem").filter({ hasText: "Managed sandbox" })).toContainText("Docker"); - await expect(system.getByRole("listitem").filter({ hasText: "Endpoint overrides" })).toContainText("1 explicit"); + await expect(system.getByRole("listitem").filter({ hasText: "Endpoint overrides" })).toContainText("Configured"); await expect(system.getByRole("listitem")).toHaveCount(5); await expect(system).toContainText("Configured for this process"); await expect(system).toContainText("Daemon gateway"); diff --git a/apps/web/src/features/system/SystemView.test.tsx b/apps/web/src/features/system/SystemView.test.tsx index dcefa0206..3b076c20f 100644 --- a/apps/web/src/features/system/SystemView.test.tsx +++ b/apps/web/src/features/system/SystemView.test.tsx @@ -54,7 +54,9 @@ describe("SystemView", () => { expect(html).toContain("Default adapter"); expect(html).toContain("Managed sandbox"); expect(html).toContain("Endpoint overrides"); - expect(html).toContain("1 explicit"); + expect(html).toContain("Configured"); + expect(html).not.toContain("1 explicit"); + expect(html).toContain("Explicit operator overrides are shown per adapter below; others may use native defaults"); expect(html).toContain("Configured for this process"); expect(html).toContain("Daemon gateway"); expect(html).toContain("Supported by this build: Docker, Microsandbox"); @@ -104,13 +106,32 @@ describe("SystemView", () => { }; const html = render({ startupConfiguration: inactive }); - expect(html).toContain("0 explicit"); - expect(html).toContain("Across 0 startup-enabled adapters"); + expect(html).toContain("Not configured"); + expect(html).not.toContain("0 explicit"); + expect(html).toContain("No execution adapters are enabled for this Core process"); expect(html).toContain("Codex is the configured default, but execution adapters are inactive because this Core process has no daemon gateway"); expect(html.match(/Build only/g)).toHaveLength(3); expect(html).not.toContain("Enabled · default"); }); + it("does not treat native endpoint defaults as unavailable", () => { + const nativeDefaults: CoreStartupConfiguration = { + ...startup, + configured: { + ...startup.configured, + model_providers: startup.configured.model_providers.map((provider) => ({ + ...provider, + endpoint_configured: false, + })), + }, + }; + const html = render({ startupConfiguration: nativeDefaults }); + + expect(html).toContain("Not configured"); + expect(html).toContain("No explicit operator overrides; enabled adapters may use native defaults"); + expect(html).toContain("Enabled · default"); + }); + it("handles an older Core without leaving a pending state", () => { const html = render({ startupConfiguration: null, diff --git a/apps/web/src/features/system/SystemView.tsx b/apps/web/src/features/system/SystemView.tsx index 99a87f45d..7bd79657d 100644 --- a/apps/web/src/features/system/SystemView.tsx +++ b/apps/web/src/features/system/SystemView.tsx @@ -91,8 +91,7 @@ export function SystemView({ const configuration = startupConfigurationState === "ready" && startupConfigurationSupported === true ? startupConfiguration : null; - const configuredEndpoints = configuration?.configured.model_providers.filter((provider) => provider.endpoint_configured).length ?? 0; - const endpointTotal = configuration?.configured.model_providers.length ?? 0; + const endpointOverrideConfigured = configuration?.configured.model_providers.some((provider) => provider.endpoint_configured) ?? false; const executionAdaptersEnabled = (configuration?.configured.enabled_harnesses.length ?? 0) > 0; const startupUnavailable = startupConfigurationSupported === false; @@ -155,9 +154,13 @@ export function SystemView({ { label: "Endpoint overrides", status: startupStatus, - value: startupValue(`${configuredEndpoints} explicit`), + value: startupValue(endpointOverrideConfigured ? "Configured" : "Not configured"), detail: configuration - ? `Across ${endpointTotal} startup-enabled adapter${endpointTotal === 1 ? "" : "s"}. No override may mean the harness uses its native default.` + ? endpointOverrideConfigured + ? "Explicit operator overrides are shown per adapter below; others may use native defaults." + : executionAdaptersEnabled + ? "No explicit operator overrides; enabled adapters may use native defaults." + : "No execution adapters are enabled for this Core process." : startupDetail, }, ]; From 4fcbbc81d2f8eeb19afdccc749166c9e2dcc11f3 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 23 Sep 2026 07:58:05 +0800 Subject: [PATCH 6/9] fix: deduplicate default adapter status --- apps/web/e2e/agents-lifecycle.spec.ts | 4 +++- apps/web/src/features/system/SystemView.test.tsx | 10 ++++++---- apps/web/src/features/system/SystemView.tsx | 12 +++++------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/apps/web/e2e/agents-lifecycle.spec.ts b/apps/web/e2e/agents-lifecycle.spec.ts index a78d98139..01b1a690c 100644 --- a/apps/web/e2e/agents-lifecycle.spec.ts +++ b/apps/web/e2e/agents-lifecycle.spec.ts @@ -2379,7 +2379,9 @@ test("presents Dashboard page-chain results and System boundaries without extra await expect(system.getByRole("listitem")).toHaveCount(5); await expect(system).toContainText("Configured for this process"); await expect(system).toContainText("Daemon gateway"); - await expect(system).toContainText("Agents created in this Web UI use Codex"); + await expect(system).toContainText("Used by Agents created in this Web UI"); + await expect(system).toContainText("This UI does not expose adapter selection"); + await expect(system).not.toContainText("Enabled · default"); await expect(system).toContainText("Operator endpoint override: configured"); await expect(system).toContainText("Compiled into this build, but not enabled when this Core process started"); await expect(system).toContainText("Runtime connection, native binary availability, sandbox health, and model execution belong to the relevant Session or Environment"); diff --git a/apps/web/src/features/system/SystemView.test.tsx b/apps/web/src/features/system/SystemView.test.tsx index 3b076c20f..d19870963 100644 --- a/apps/web/src/features/system/SystemView.test.tsx +++ b/apps/web/src/features/system/SystemView.test.tsx @@ -63,8 +63,9 @@ describe("SystemView", () => { expect(html).toContain("Claude SDK"); expect(html).toContain("MiniMax Code"); expect(html).toContain("Execution adapters"); - expect(html).toContain("Agents created in this Web UI use Codex"); - expect(html).toContain("Enabled · default"); + expect(html).toContain("Used by Agents created in this Web UI"); + expect(html).toContain("This UI does not expose adapter selection. API requests can select any enabled adapter"); + expect(html).not.toContain("Enabled · default"); expect(html).toContain("Operator endpoint override: configured"); expect(html).toContain("Operator endpoint override: not set; the harness may use its native default"); expect(html).toContain("Build only"); @@ -109,7 +110,8 @@ describe("SystemView", () => { expect(html).toContain("Not configured"); expect(html).not.toContain("0 explicit"); expect(html).toContain("No execution adapters are enabled for this Core process"); - expect(html).toContain("Codex is the configured default, but execution adapters are inactive because this Core process has no daemon gateway"); + expect(html).toContain("Configured default; not active for execution in this Core process"); + expect(html).toContain("This Core process has no daemon gateway, so no execution adapters are active"); expect(html.match(/Build only/g)).toHaveLength(3); expect(html).not.toContain("Enabled · default"); }); @@ -129,7 +131,7 @@ describe("SystemView", () => { expect(html).toContain("Not configured"); expect(html).toContain("No explicit operator overrides; enabled adapters may use native defaults"); - expect(html).toContain("Enabled · default"); + expect(html).not.toContain("Enabled · default"); }); it("handles an older Core without leaving a pending state", () => { diff --git a/apps/web/src/features/system/SystemView.tsx b/apps/web/src/features/system/SystemView.tsx index 7bd79657d..38ddc3191 100644 --- a/apps/web/src/features/system/SystemView.tsx +++ b/apps/web/src/features/system/SystemView.tsx @@ -139,8 +139,8 @@ export function SystemView({ value: startupValue(configuration ? harnessLabel(configuration.configured.default_harness) : ""), detail: configuration ? executionAdaptersEnabled - ? "Used by Agents created in this Web UI unless an API request selects another enabled harness." - : "Configured default; execution adapters are inactive because this Core process has no daemon gateway." + ? "Used by Agents created in this Web UI." + : "Configured default; not active for execution in this Core process." : startupDetail, }, { @@ -234,8 +234,8 @@ export function SystemView({

{executionAdaptersEnabled - ? <>Agents created in this Web UI use {harnessLabel(configuration.configured.default_harness)}. Another enabled adapter can be selected through the API extension; this UI does not expose that control. - : <>{harnessLabel(configuration.configured.default_harness)} is the configured default, but execution adapters are inactive because this Core process has no daemon gateway.} + ? <>This UI does not expose adapter selection. API requests can select any enabled adapter. + : <>This Core process has no daemon gateway, so no execution adapters are active.}

{configuration.supported.harnesses.map((harness) => { @@ -247,9 +247,7 @@ export function SystemView({ {harnessLabel(harness)}
From edd47911d0a8d4dc25abfc684ff6fb16b18cd917 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 23 Sep 2026 08:12:32 +0800 Subject: [PATCH 7/9] refactor: focus System on startup configuration --- apps/web/e2e/agents-lifecycle.spec.ts | 23 +++----- apps/web/e2e/core-connection.spec.ts | 12 ++-- apps/web/src/App.tsx | 14 +---- apps/web/src/features/system/SystemView.css | 19 +----- .../src/features/system/SystemView.test.tsx | 23 +++----- apps/web/src/features/system/SystemView.tsx | 59 ++----------------- 6 files changed, 31 insertions(+), 119 deletions(-) diff --git a/apps/web/e2e/agents-lifecycle.spec.ts b/apps/web/e2e/agents-lifecycle.spec.ts index 01b1a690c..596733352 100644 --- a/apps/web/e2e/agents-lifecycle.spec.ts +++ b/apps/web/e2e/agents-lifecycle.spec.ts @@ -2371,15 +2371,13 @@ test("presents Dashboard page-chain results and System boundaries without extra await page.getByRole("button", { name: "System", exact: true }).click(); const system = page.locator(".system-page"); - await expect(system.getByRole("listitem").filter({ hasText: "Core API" })).toContainText("Available"); - await expect(system.getByRole("listitem").filter({ hasText: "Vaults" })).toContainText("Available"); - await expect(system.getByRole("listitem").filter({ hasText: "Default adapter" })).toContainText("Codex"); + await expect(system.getByRole("listitem").filter({ hasText: "Harnesses" })).toContainText("Claude SDK, Codex"); await expect(system.getByRole("listitem").filter({ hasText: "Managed sandbox" })).toContainText("Docker"); await expect(system.getByRole("listitem").filter({ hasText: "Endpoint overrides" })).toContainText("Configured"); - await expect(system.getByRole("listitem")).toHaveCount(5); + await expect(system.getByRole("listitem")).toHaveCount(3); await expect(system).toContainText("Configured for this process"); await expect(system).toContainText("Daemon gateway"); - await expect(system).toContainText("Used by Agents created in this Web UI"); + await expect(system).toContainText("Codex is used by Agents created in this Web UI"); await expect(system).toContainText("This UI does not expose adapter selection"); await expect(system).not.toContainText("Enabled · default"); await expect(system).toContainText("Operator endpoint override: configured"); @@ -2392,16 +2390,13 @@ test("presents Dashboard page-chain results and System boundaries without extra const beforeSystemRefresh = await fixtureRequests(request); const systemRefresh = system.getByRole("button", { name: "Refresh System status" }); await systemRefresh.click(); - await expect.poll(async () => { - const entries = await fixtureRequests(request); - return [count(entries, "/v1/agents"), count(entries, "/v1/agents/sessions")]; - }).toEqual([ - count(beforeSystemRefresh, "/v1/agents") + 1, - count(beforeSystemRefresh, "/v1/agents/sessions") + 1, - ]); + await expect.poll(async () => count(await fixtureRequests(request), "/v1/agents/core/startup-configuration")).toBe( + count(beforeSystemRefresh, "/v1/agents/core/startup-configuration") + 1, + ); const afterSystemRefresh = await fixtureRequests(request); - expect(count(afterSystemRefresh, "/v1/agents")).toBe(count(beforeSystemRefresh, "/v1/agents") + 1); - expect(count(afterSystemRefresh, "/v1/agents/sessions")).toBe(count(beforeSystemRefresh, "/v1/agents/sessions") + 1); + expect(count(afterSystemRefresh, "/v1/agents")).toBe(count(beforeSystemRefresh, "/v1/agents")); + expect(count(afterSystemRefresh, "/v1/agents/sessions")).toBe(count(beforeSystemRefresh, "/v1/agents/sessions")); + expect(count(afterSystemRefresh, "/v1/vaults")).toBe(count(beforeSystemRefresh, "/v1/vaults")); expect(count(afterSystemRefresh, "/v1/agents/core/startup-configuration")).toBe( count(beforeSystemRefresh, "/v1/agents/core/startup-configuration") + 1, ); diff --git a/apps/web/e2e/core-connection.spec.ts b/apps/web/e2e/core-connection.spec.ts index 8a8b045a6..f4a9edb9e 100644 --- a/apps/web/e2e/core-connection.spec.ts +++ b/apps/web/e2e/core-connection.spec.ts @@ -318,7 +318,7 @@ test("clears startup configuration synchronously and fences a stale read when th await boot(page, request); await page.getByRole("button", { name: "System", exact: true }).click(); const system = page.locator(".system-page"); - await expect(system.getByRole("listitem").filter({ hasText: "Default adapter" })).toContainText("Codex"); + await expect(system.getByRole("listitem").filter({ hasText: "Harnesses" })).toContainText("Codex"); await page.evaluate(() => { (window as ProbeInstrumentationWindow).__holdNextLocalStartup = true; }); @@ -331,17 +331,17 @@ test("clears startup configuration synchronously and fences a stale read when th await dialog.getByLabel("Bearer token").fill("replacement-token"); await dialog.getByRole("button", { name: "Apply connection" }).click(); - const defaultHarness = system.getByRole("listitem").filter({ hasText: "Default adapter" }); - await expect(defaultHarness).toContainText("Checking…"); + const harnesses = system.getByRole("listitem").filter({ hasText: "Harnesses" }); + await expect(harnesses).toContainText("Checking…"); await expect(system).not.toContainText("Configured for this process"); await expect.poll(() => page.evaluate(() => (window as ProbeInstrumentationWindow).__oldStartupAbortCount ?? 0)).toBe(1); await expect.poll(() => page.evaluate(() => typeof (window as ProbeInstrumentationWindow).__resolveNewStartup)).toBe("function"); await page.evaluate(() => (window as ProbeInstrumentationWindow).__resolveNewStartup?.()); - await expect(defaultHarness).toContainText("Claude SDK"); + await expect(harnesses).toContainText("Claude SDK"); await expect(system.getByRole("listitem").filter({ hasText: "Managed sandbox" })).toContainText("Microsandbox"); await page.evaluate(() => (window as ProbeInstrumentationWindow).__resolveOldStartup?.()); - await expect(defaultHarness).toContainText("Claude SDK"); + await expect(harnesses).toContainText("Claude SDK"); await expect(system).not.toContainText("Docker · Maintenance"); }); @@ -356,7 +356,7 @@ for (const status of [404, 405]) { await page.getByRole("button", { name: "System", exact: true }).click(); const system = page.locator(".system-page"); - await expect(system.getByRole("listitem").filter({ hasText: "Default adapter" })).toContainText("Not exposed"); + await expect(system.getByRole("listitem").filter({ hasText: "Harnesses" })).toContainText("Not exposed"); await expect(system).toContainText("This Core version does not expose the startup configuration extension"); await expect(system).not.toContainText("Configured for this process"); await expect(system).not.toContainText("Checking…"); diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index a5d093046..5e874e518 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -894,9 +894,8 @@ export function App() { }, [refreshAgents, refreshEnvironmentTemplates, refreshFilteredSessions, refreshSessions, refreshVaults]); const refreshSystem = useCallback(() => { - refreshDashboard(); void refreshStartupConfiguration(); - }, [refreshDashboard, refreshStartupConfiguration]); + }, [refreshStartupConfiguration]); const changeSessionAgentFilter = useCallback((agentId: string | null) => { if (sessionAgentFilterRef.current === agentId) return; @@ -2248,21 +2247,12 @@ export function App() { {view === "system" ? ( ) : null} diff --git a/apps/web/src/features/system/SystemView.css b/apps/web/src/features/system/SystemView.css index 873fc2fa7..88114ff25 100644 --- a/apps/web/src/features/system/SystemView.css +++ b/apps/web/src/features/system/SystemView.css @@ -1,6 +1,6 @@ .system-summary { display: grid; - grid-template-columns: repeat(5, minmax(0, 1fr)); + grid-template-columns: repeat(3, minmax(0, 1fr)); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); } @@ -183,23 +183,6 @@ } @media (max-width: 920px) { - .system-summary { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .system-summary-cell:nth-child(2n) { - border-right: 0; - } - - .system-summary-cell { - border-bottom: 1px solid var(--line); - } - - .system-summary-cell:last-child { - border-right: 0; - border-bottom: 0; - } - .system-config-row { grid-template-columns: minmax(140px, .8fr) minmax(140px, 1fr); } diff --git a/apps/web/src/features/system/SystemView.test.tsx b/apps/web/src/features/system/SystemView.test.tsx index d19870963..a9ed1f2f5 100644 --- a/apps/web/src/features/system/SystemView.test.tsx +++ b/apps/web/src/features/system/SystemView.test.tsx @@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest"; import type { CoreStartupConfiguration } from "@agents-core-web/agents-client"; -import { safeCoreBaseUrlLabel, SystemView } from "./SystemView"; +import { SystemView } from "./SystemView"; const startup: CoreStartupConfiguration = { object: "agents.core.startup_configuration", @@ -28,13 +28,9 @@ const startup: CoreStartupConfiguration = { function render(overrides: Partial[0]> = {}): string { return renderToStaticMarkup( { const html = render(); expect(html).toContain("Core startup configuration"); - expect(html.match(/role="listitem"/g)).toHaveLength(5); - expect(html).toContain("Vault catalog loaded"); - expect(html).toContain("Default adapter"); + expect(html.match(/role="listitem"/g)).toHaveLength(3); + expect(html).not.toContain("Core API"); + expect(html).not.toContain("Vault catalog loaded"); + expect(html).toContain("Harnesses"); + expect(html).toContain("Claude SDK, Codex"); expect(html).toContain("Managed sandbox"); expect(html).toContain("Endpoint overrides"); expect(html).toContain("Configured"); @@ -63,7 +61,7 @@ describe("SystemView", () => { expect(html).toContain("Claude SDK"); expect(html).toContain("MiniMax Code"); expect(html).toContain("Execution adapters"); - expect(html).toContain("Used by Agents created in this Web UI"); + expect(html).toContain("Codex is used by Agents created in this Web UI"); expect(html).toContain("This UI does not expose adapter selection. API requests can select any enabled adapter"); expect(html).not.toContain("Enabled · default"); expect(html).toContain("Operator endpoint override: configured"); @@ -110,7 +108,7 @@ describe("SystemView", () => { expect(html).toContain("Not configured"); expect(html).not.toContain("0 explicit"); expect(html).toContain("No execution adapters are enabled for this Core process"); - expect(html).toContain("Configured default; not active for execution in this Core process"); + expect(html).toContain("Codex is configured as the default, but no harness is active"); expect(html).toContain("This Core process has no daemon gateway, so no execution adapters are active"); expect(html.match(/Build only/g)).toHaveLength(3); expect(html).not.toContain("Enabled · default"); @@ -159,9 +157,4 @@ describe("SystemView", () => { expect(html).not.toContain("Configured for this process"); }); - it("sanitizes Core labels independently from connection storage", () => { - expect(safeCoreBaseUrlLabel("/v1")).toBe("/v1"); - expect(safeCoreBaseUrlLabel("https://user:pass@core.example/v1?secret=1#token")).toBe("https://core.example/v1"); - expect(safeCoreBaseUrlLabel("not a URL")).toBe("Configured Core"); - }); }); diff --git a/apps/web/src/features/system/SystemView.tsx b/apps/web/src/features/system/SystemView.tsx index 38ddc3191..1baa141ac 100644 --- a/apps/web/src/features/system/SystemView.tsx +++ b/apps/web/src/features/system/SystemView.tsx @@ -13,12 +13,6 @@ function stateKind(state: CoreConnectionState): StatusKind { return "running"; } -function stateLabel(state: CoreConnectionState): string { - if (state === "ready") return "Available"; - if (state === "failed") return "Unavailable"; - return "Checking…"; -} - function harnessLabel(harness: string): string { if (harness === "claude_sdk") return "Claude SDK"; if (harness === "mcode") return "MiniMax Code"; @@ -32,21 +26,6 @@ function providerLabel(provider: string | null): string { return "None"; } -export function safeCoreBaseUrlLabel(value: string): string { - const candidate = value.trim() || "/v1"; - if (candidate.startsWith("/")) return candidate; - try { - const url = new URL(candidate); - url.username = ""; - url.password = ""; - url.search = ""; - url.hash = ""; - return url.toString().replace(/\/$/, ""); - } catch { - return "Configured Core"; - } -} - interface SystemStatusCard { detail: string; label: string; @@ -64,25 +43,17 @@ function StartupStatus({ enabled, label }: { enabled: boolean; label?: string }) } export function SystemView({ - coreState, - coreBaseUrl, startupConfiguration, startupConfigurationState, startupConfigurationSupported, - vaultCollectionState, - vaultSupported, selfHostedWebEnabled, managedWebEnabled, refreshing, onRefresh, }: { - coreState: CoreConnectionState; - coreBaseUrl: string; startupConfiguration: CoreStartupConfiguration | null; startupConfigurationState: CoreConnectionState; startupConfigurationSupported: boolean | null; - vaultCollectionState: CoreConnectionState; - vaultSupported: boolean | null; selfHostedWebEnabled: boolean; managedWebEnabled: boolean; refreshing: boolean; @@ -93,6 +64,7 @@ export function SystemView({ : null; const endpointOverrideConfigured = configuration?.configured.model_providers.some((provider) => provider.endpoint_configured) ?? false; const executionAdaptersEnabled = (configuration?.configured.enabled_harnesses.length ?? 0) > 0; + const enabledHarnessLabels = configuration?.configured.enabled_harnesses.map(harnessLabel).join(", ") ?? ""; const startupUnavailable = startupConfigurationSupported === false; const startupValue = (value: string): string => { @@ -111,36 +83,15 @@ export function SystemView({ : startupConfigurationState === "failed" ? "The startup configuration request failed. No configuration is inferred." : "Reported by the safe Core startup configuration extension."; - const vaultStatus: SystemStatusCard = vaultSupported === false - ? { label: "Vaults", status: "interrupted", value: "Unavailable", detail: "This Core does not expose the Vaults API." } - : vaultCollectionState === "failed" - ? { label: "Vaults", status: "failed", value: vaultSupported === true ? "Refresh failed" : "Check failed", detail: "The Vault catalog request failed." } - : vaultSupported === true && vaultCollectionState === "ready" - ? { label: "Vaults", status: "completed", value: "Available", detail: "Vault catalog loaded." } - : { label: "Vaults", status: "running", value: "Checking…", detail: "Checking whether this Core exposes the Vaults API." }; - const cards: SystemStatusCard[] = [ { - label: "Core API", - status: stateKind(coreState), - value: stateLabel(coreState), - detail: `${safeCoreBaseUrlLabel(coreBaseUrl)} · ${ - coreState === "ready" - ? "confirmed by an Agent or Session API request" - : coreState === "failed" - ? "Agent and Session API requests failed" - : "checking Agent and Session APIs" - }.`, - }, - vaultStatus, - { - label: "Default adapter", + label: "Harnesses", status: startupStatus, - value: startupValue(configuration ? harnessLabel(configuration.configured.default_harness) : ""), + value: startupValue(configuration ? executionAdaptersEnabled ? enabledHarnessLabels : "None enabled" : ""), detail: configuration ? executionAdaptersEnabled - ? "Used by Agents created in this Web UI." - : "Configured default; not active for execution in this Core process." + ? `${harnessLabel(configuration.configured.default_harness)} is used by Agents created in this Web UI.` + : `${harnessLabel(configuration.configured.default_harness)} is configured as the default, but no harness is active.` : startupDetail, }, { From f7146e5d1e7f647d72509b952fd57e8758f06562 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 23 Sep 2026 08:30:24 +0800 Subject: [PATCH 8/9] refactor: remove duplicate System configuration --- apps/web/e2e/agents-lifecycle.spec.ts | 25 +++++--- apps/web/e2e/core-connection.spec.ts | 12 ++-- apps/web/src/features/system/SystemView.css | 52 ++++++---------- .../src/features/system/SystemView.test.tsx | 13 ++-- apps/web/src/features/system/SystemView.tsx | 59 ++++++------------- 5 files changed, 66 insertions(+), 95 deletions(-) diff --git a/apps/web/e2e/agents-lifecycle.spec.ts b/apps/web/e2e/agents-lifecycle.spec.ts index 596733352..4edde3568 100644 --- a/apps/web/e2e/agents-lifecycle.spec.ts +++ b/apps/web/e2e/agents-lifecycle.spec.ts @@ -2371,13 +2371,15 @@ test("presents Dashboard page-chain results and System boundaries without extra await page.getByRole("button", { name: "System", exact: true }).click(); const system = page.locator(".system-page"); - await expect(system.getByRole("listitem").filter({ hasText: "Harnesses" })).toContainText("Claude SDK, Codex"); + await expect(system.getByRole("listitem").filter({ hasText: "Default harness" })).toContainText("Codex"); + await expect(system.getByRole("listitem").filter({ hasText: "Daemon gateway" })).toContainText("Enabled"); await expect(system.getByRole("listitem").filter({ hasText: "Managed sandbox" })).toContainText("Docker"); await expect(system.getByRole("listitem").filter({ hasText: "Endpoint overrides" })).toContainText("Configured"); - await expect(system.getByRole("listitem")).toHaveCount(3); - await expect(system).toContainText("Configured for this process"); - await expect(system).toContainText("Daemon gateway"); - await expect(system).toContainText("Codex is used by Agents created in this Web UI"); + await expect(system.getByRole("listitem")).toHaveCount(4); + await expect(system).not.toContainText("Configured for this process"); + await expect(system).not.toContainText("Managed execution"); + await expect(system).not.toContainText("Self-hosted execution"); + await expect(system).toContainText("Used by Agents created in this Web UI"); await expect(system).toContainText("This UI does not expose adapter selection"); await expect(system).not.toContainText("Enabled · default"); await expect(system).toContainText("Operator endpoint override: configured"); @@ -2414,7 +2416,7 @@ test("presents Dashboard page-chain results and System boundaries without extra left: section.getBoundingClientRect().left - element.getBoundingClientRect().left, right: element.getBoundingClientRect().right - section.getBoundingClientRect().right, header: section.querySelector("header")!.getBoundingClientRect().left, - content: section.querySelector(".system-config-list, .system-harness-grid")!.getBoundingClientRect().left, + content: section.querySelector(".system-harness-grid")!.getBoundingClientRect().left, explanation: section.querySelector(".system-config-explanation")?.getBoundingClientRect().left ?? null, })), boundaryInset: element.querySelector(".system-boundary-note")!.getBoundingClientRect().left - element.getBoundingClientRect().left, @@ -2432,14 +2434,19 @@ test("presents Dashboard page-chain results and System boundaries without extra await page.setViewportSize({ width: 390, height: 844 }); const systemBounds = await system.evaluate((element) => { - const rows = [...element.querySelectorAll(".system-summary-cell")].map((row) => row.getBoundingClientRect()); + const rows = [...element.querySelectorAll(".system-summary-cell")]; return { viewportWidth: innerWidth, documentWidth: document.documentElement.scrollWidth, sectionInset: element.querySelector(".system-config-section")!.getBoundingClientRect().left - element.getBoundingClientRect().left, sectionRightInset: element.getBoundingClientRect().right - element.querySelector(".system-config-section")!.getBoundingClientRect().right, boundaryInset: element.querySelector(".system-boundary-note")!.getBoundingClientRect().left - element.getBoundingClientRect().left, - rowBounds: rows.map((row) => ({ top: row.top, bottom: row.bottom, height: row.height })), + rowBounds: rows.map((row) => ({ + top: row.getBoundingClientRect().top, + bottom: row.getBoundingClientRect().bottom, + height: row.getBoundingClientRect().height, + borderBottomWidth: getComputedStyle(row).borderBottomWidth, + })), }; }); expect(systemBounds.documentWidth).toBeLessThanOrEqual(systemBounds.viewportWidth); @@ -2450,6 +2457,8 @@ test("presents Dashboard page-chain results and System boundaries without extra expect(systemBounds.rowBounds[index]!.top).toBeGreaterThanOrEqual(systemBounds.rowBounds[index - 1]!.bottom); } expect(systemBounds.rowBounds.every((row) => row.height >= 36)).toBe(true); + expect(systemBounds.rowBounds.slice(0, -1).every((row) => row.borderBottomWidth !== "0px")).toBe(true); + expect(systemBounds.rowBounds.at(-1)?.borderBottomWidth).toBe("0px"); await attachScreenshot(page, testInfo, "narrow-system-contract-boundary"); }); diff --git a/apps/web/e2e/core-connection.spec.ts b/apps/web/e2e/core-connection.spec.ts index f4a9edb9e..401a311d3 100644 --- a/apps/web/e2e/core-connection.spec.ts +++ b/apps/web/e2e/core-connection.spec.ts @@ -318,7 +318,7 @@ test("clears startup configuration synchronously and fences a stale read when th await boot(page, request); await page.getByRole("button", { name: "System", exact: true }).click(); const system = page.locator(".system-page"); - await expect(system.getByRole("listitem").filter({ hasText: "Harnesses" })).toContainText("Codex"); + await expect(system.getByRole("listitem").filter({ hasText: "Default harness" })).toContainText("Codex"); await page.evaluate(() => { (window as ProbeInstrumentationWindow).__holdNextLocalStartup = true; }); @@ -331,17 +331,17 @@ test("clears startup configuration synchronously and fences a stale read when th await dialog.getByLabel("Bearer token").fill("replacement-token"); await dialog.getByRole("button", { name: "Apply connection" }).click(); - const harnesses = system.getByRole("listitem").filter({ hasText: "Harnesses" }); - await expect(harnesses).toContainText("Checking…"); + const defaultHarness = system.getByRole("listitem").filter({ hasText: "Default harness" }); + await expect(defaultHarness).toContainText("Checking…"); await expect(system).not.toContainText("Configured for this process"); await expect.poll(() => page.evaluate(() => (window as ProbeInstrumentationWindow).__oldStartupAbortCount ?? 0)).toBe(1); await expect.poll(() => page.evaluate(() => typeof (window as ProbeInstrumentationWindow).__resolveNewStartup)).toBe("function"); await page.evaluate(() => (window as ProbeInstrumentationWindow).__resolveNewStartup?.()); - await expect(harnesses).toContainText("Claude SDK"); + await expect(defaultHarness).toContainText("Claude SDK"); await expect(system.getByRole("listitem").filter({ hasText: "Managed sandbox" })).toContainText("Microsandbox"); await page.evaluate(() => (window as ProbeInstrumentationWindow).__resolveOldStartup?.()); - await expect(harnesses).toContainText("Claude SDK"); + await expect(defaultHarness).toContainText("Claude SDK"); await expect(system).not.toContainText("Docker · Maintenance"); }); @@ -356,7 +356,7 @@ for (const status of [404, 405]) { await page.getByRole("button", { name: "System", exact: true }).click(); const system = page.locator(".system-page"); - await expect(system.getByRole("listitem").filter({ hasText: "Harnesses" })).toContainText("Not exposed"); + await expect(system.getByRole("listitem").filter({ hasText: "Default harness" })).toContainText("Not exposed"); await expect(system).toContainText("This Core version does not expose the startup configuration extension"); await expect(system).not.toContainText("Configured for this process"); await expect(system).not.toContainText("Checking…"); diff --git a/apps/web/src/features/system/SystemView.css b/apps/web/src/features/system/SystemView.css index 88114ff25..f71ef8044 100644 --- a/apps/web/src/features/system/SystemView.css +++ b/apps/web/src/features/system/SystemView.css @@ -1,6 +1,6 @@ .system-summary { display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); + grid-template-columns: repeat(4, minmax(0, 1fr)); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); } @@ -71,34 +71,12 @@ font-size: 11px; } -.system-config-list { - overflow: hidden; - border: 1px solid var(--line); - border-radius: 10px; - background: var(--surface); -} - -.system-config-row { - display: grid; - grid-template-columns: minmax(150px, .8fr) minmax(150px, .9fr) minmax(240px, 1.7fr); - align-items: center; - gap: 18px; - min-height: 64px; - padding: 13px 18px; -} - -.system-config-row + .system-config-row { - border-top: 1px solid var(--line); -} - -.system-config-row > strong, .system-harness-card strong { color: var(--fg); font-size: 13px; font-weight: 500; } -.system-config-row > small, .system-harness-card > small { color: var(--fg-muted); font-size: 11px; @@ -183,12 +161,20 @@ } @media (max-width: 920px) { - .system-config-row { - grid-template-columns: minmax(140px, .8fr) minmax(140px, 1fr); + .system-summary { + grid-template-columns: repeat(2, minmax(0, 1fr)); } - .system-config-row > small { - grid-column: 1 / -1; + .system-summary-cell { + border-bottom: 1px solid var(--line); + } + + .system-summary-cell:nth-child(2n) { + border-right: 0; + } + + .system-summary-cell:nth-last-child(-n + 2) { + border-bottom: 0; } .system-harness-grid { @@ -219,6 +205,10 @@ border-bottom: 1px solid var(--line); } + .system-summary-cell:nth-last-child(-n + 2) { + border-bottom: 1px solid var(--line); + } + .system-summary-cell:last-child { border-bottom: 0; } @@ -229,12 +219,4 @@ gap: 4px; } - .system-config-row { - grid-template-columns: 1fr; - gap: 8px; - } - - .system-config-row > small { - grid-column: auto; - } } diff --git a/apps/web/src/features/system/SystemView.test.tsx b/apps/web/src/features/system/SystemView.test.tsx index a9ed1f2f5..04feb7780 100644 --- a/apps/web/src/features/system/SystemView.test.tsx +++ b/apps/web/src/features/system/SystemView.test.tsx @@ -45,23 +45,24 @@ describe("SystemView", () => { const html = render(); expect(html).toContain("Core startup configuration"); - expect(html.match(/role="listitem"/g)).toHaveLength(3); + expect(html.match(/role="listitem"/g)).toHaveLength(4); expect(html).not.toContain("Core API"); expect(html).not.toContain("Vault catalog loaded"); - expect(html).toContain("Harnesses"); - expect(html).toContain("Claude SDK, Codex"); + expect(html).toContain("Default harness"); + expect(html).toContain("Used by Agents created in this Web UI"); expect(html).toContain("Managed sandbox"); expect(html).toContain("Endpoint overrides"); expect(html).toContain("Configured"); expect(html).not.toContain("1 explicit"); expect(html).toContain("Explicit operator overrides are shown per adapter below; others may use native defaults"); - expect(html).toContain("Configured for this process"); expect(html).toContain("Daemon gateway"); expect(html).toContain("Supported by this build: Docker, Microsandbox"); expect(html).toContain("Claude SDK"); expect(html).toContain("MiniMax Code"); expect(html).toContain("Execution adapters"); - expect(html).toContain("Codex is used by Agents created in this Web UI"); + expect(html).not.toContain("Configured for this process"); + expect(html).not.toContain("Managed execution"); + expect(html).not.toContain("Self-hosted execution"); expect(html).toContain("This UI does not expose adapter selection. API requests can select any enabled adapter"); expect(html).not.toContain("Enabled · default"); expect(html).toContain("Operator endpoint override: configured"); @@ -108,7 +109,7 @@ describe("SystemView", () => { expect(html).toContain("Not configured"); expect(html).not.toContain("0 explicit"); expect(html).toContain("No execution adapters are enabled for this Core process"); - expect(html).toContain("Codex is configured as the default, but no harness is active"); + expect(html).toContain("Configured as the default, but no harness is active"); expect(html).toContain("This Core process has no daemon gateway, so no execution adapters are active"); expect(html.match(/Build only/g)).toHaveLength(3); expect(html).not.toContain("Enabled · default"); diff --git a/apps/web/src/features/system/SystemView.tsx b/apps/web/src/features/system/SystemView.tsx index 1baa141ac..cb2cd8c32 100644 --- a/apps/web/src/features/system/SystemView.tsx +++ b/apps/web/src/features/system/SystemView.tsx @@ -64,7 +64,6 @@ export function SystemView({ : null; const endpointOverrideConfigured = configuration?.configured.model_providers.some((provider) => provider.endpoint_configured) ?? false; const executionAdaptersEnabled = (configuration?.configured.enabled_harnesses.length ?? 0) > 0; - const enabledHarnessLabels = configuration?.configured.enabled_harnesses.map(harnessLabel).join(", ") ?? ""; const startupUnavailable = startupConfigurationSupported === false; const startupValue = (value: string): string => { @@ -85,21 +84,33 @@ export function SystemView({ : "Reported by the safe Core startup configuration extension."; const cards: SystemStatusCard[] = [ { - label: "Harnesses", + label: "Default harness", status: startupStatus, - value: startupValue(configuration ? executionAdaptersEnabled ? enabledHarnessLabels : "None enabled" : ""), + value: startupValue(configuration ? harnessLabel(configuration.configured.default_harness) : ""), detail: configuration ? executionAdaptersEnabled - ? `${harnessLabel(configuration.configured.default_harness)} is used by Agents created in this Web UI.` - : `${harnessLabel(configuration.configured.default_harness)} is configured as the default, but no harness is active.` + ? "Used by Agents created in this Web UI." + : "Configured as the default, but no harness is active." + : startupDetail, + }, + { + label: "Daemon gateway", + status: startupStatus, + value: startupValue(configuration?.configured.daemon_gateway ? "Enabled" : "Not configured"), + detail: configuration + ? configuration.configured.daemon_gateway + ? `Accepts authenticated execution Runtime connections. ${selfHostedWebEnabled ? "This Web build can request self-hosted Sessions." : "This Web build cannot request self-hosted Sessions."}` + : "No execution Runtime gateway is enabled for this Core process." : startupDetail, }, { label: "Managed sandbox", status: startupStatus, - value: startupValue(configuration ? providerLabel(configuration.configured.managed_sandbox.provider) : ""), - detail: configuration?.configured.managed_sandbox.maintenance - ? "Selected provider is in maintenance mode." + value: startupValue(configuration + ? `${providerLabel(configuration.configured.managed_sandbox.provider)}${configuration.configured.managed_sandbox.maintenance ? " · Maintenance" : ""}` + : ""), + detail: configuration + ? `${configuration.configured.managed_sandbox.maintenance ? "Selected provider is in maintenance mode. " : ""}Supported by this build: ${configuration.supported.managed_sandbox_providers.map(providerLabel).join(", ")}. ${managedWebEnabled ? "This Web build can request managed Sessions." : "This Web build cannot request managed Sessions."}` : startupDetail, }, { @@ -146,38 +157,6 @@ export function SystemView({ {configuration ? ( <> -
-
-

Configured for this process

- Validated or detected when Core started -
-
-
- Daemon gateway - - Accepts authenticated execution Runtime connections when enabled. -
-
- Managed execution - - - Supported by this build: {configuration.supported.managed_sandbox_providers.map(providerLabel).join(", ")}. - {managedWebEnabled ? " This Web build can request managed Sessions." : " This Web build cannot request managed Sessions."} - -
-
- Self-hosted execution - - {selfHostedWebEnabled ? "This Web build can request self-hosted Sessions." : "This Web build cannot request self-hosted Sessions."} -
-
-
-

Execution adapters (harnesses)

From 215caebb9eb4700aad5763e8e26c4437a6d8b577 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 23 Sep 2026 09:15:43 +0800 Subject: [PATCH 9/9] feat: select enabled harnesses for agents --- apps/web/e2e/agents-lifecycle.spec.ts | 14 +++- apps/web/e2e/core-connection.spec.ts | 12 +-- apps/web/e2e/fixture-core.mjs | 1 + apps/web/src/App.tsx | 1 + apps/web/src/features/agents/AgentForm.tsx | 74 ++++++++++++++++++- .../features/agents/AgentSetupView.test.tsx | 23 ++++++ .../src/features/agents/AgentSetupView.tsx | 17 ++++- apps/web/src/features/agents/AgentsView.tsx | 6 +- .../src/features/agents/agent-form.test.ts | 34 +++++++++ apps/web/src/features/agents/agent-form.ts | 11 +++ .../src/features/agents/agent-preview.test.ts | 12 +++ apps/web/src/features/agents/agent-preview.ts | 5 ++ apps/web/src/features/system/SystemView.css | 4 +- .../src/features/system/SystemView.test.tsx | 8 +- apps/web/src/features/system/SystemView.tsx | 12 +-- packages/agents-client/src/client.test.ts | 11 +++ packages/agents-client/src/client.ts | 13 +++- .../agents-client/src/protocol-types.test.ts | 9 +++ packages/agents-client/src/types.ts | 7 ++ 19 files changed, 237 insertions(+), 37 deletions(-) diff --git a/apps/web/e2e/agents-lifecycle.spec.ts b/apps/web/e2e/agents-lifecycle.spec.ts index 4edde3568..ebe285476 100644 --- a/apps/web/e2e/agents-lifecycle.spec.ts +++ b/apps/web/e2e/agents-lifecycle.spec.ts @@ -335,6 +335,7 @@ test("creates, previews, edits, and removes bounded Function and anonymous HTTP await page.getByRole("button", { name: /^Create agent/ }).click(); await page.getByLabel("Name").fill("Tool Agent"); await page.getByLabel("Instructions").fill("Use only the configured tools."); + await page.getByLabel("Harness").selectOption("claude_sdk"); await page.getByLabel("Model").selectOption({ label: "Custom model ID…" }); await page.getByLabel("Custom model ID").fill("fixture/tool-model"); @@ -360,6 +361,7 @@ test("creates, previews, edits, and removes bounded Function and anonymous HTTP await expect(previewBody).toContainText('"name": "lookup_customer"'); await expect(previewBody).toContainText('"type": "mcp"'); await expect(previewBody).toContainText('"required": true'); + await expect(previewBody).toContainText('"harness": "claude_sdk"'); await page.getByRole("button", { name: "Save Agent definition" }).click(); await expect(page.getByRole("status")).toContainText("Agent definition saved as"); @@ -367,6 +369,7 @@ test("creates, previews, edits, and removes bounded Function and anonymous HTTP const creates = requests.filter((entry) => entry.method === "POST" && entry.path === "/v1/agents"); expect(creates).toHaveLength(1); expect(creates[0]?.body).toEqual({ + x_agents_core: { harness: "claude_sdk" }, model: "fixture/tool-model", name: "Tool Agent", instructions: "Use only the configured tools.", @@ -397,6 +400,7 @@ test("creates, previews, edits, and removes bounded Function and anonymous HTTP const setup = page.locator(".agent-setup-page"); await expect(setup.getByRole("heading", { name: "Saved definition" })).toBeVisible(); await expect(page.getByRole("dialog")).toHaveCount(0); + await expect(setup.getByLabel("Harness")).toHaveValue("claude_sdk"); const editFunction = setup.locator(".agent-tool-card").filter({ hasText: "Function" }).first(); await editFunction.getByLabel("Name", { exact: true }).fill("lookup_customer_v2"); const editMcp = setup.locator(".agent-tool-card").filter({ hasText: "Anonymous HTTP MCP" }).first(); @@ -416,7 +420,9 @@ test("creates, previews, edits, and removes bounded Function and anonymous HTTP defer_loading: false, }, ]); + expect(updates[0]?.body).not.toHaveProperty("x_agents_core"); + await setup.getByLabel("Harness").selectOption("codex"); await setup.locator(".agent-tool-card").filter({ hasText: "Function" }).first().getByRole("button", { name: "Remove" }).click(); await setup.getByRole("button", { name: "Save changes" }).click(); await expect(setup.getByRole("status")).toContainText("Agent definition updated"); @@ -425,6 +431,7 @@ test("creates, previews, edits, and removes bounded Function and anonymous HTTP updates = requests.filter((entry) => entry.method === "POST" && entry.path.startsWith("/v1/agents/agent_created_")); expect(updates).toHaveLength(2); expect(updates[1]?.body?.tools).toEqual([]); + expect(updates[1]?.body?.x_agents_core).toEqual({ harness: "codex" }); }); test("keeps Source Files controls out of the System status page", async ({ page, request }) => { @@ -2371,16 +2378,15 @@ test("presents Dashboard page-chain results and System boundaries without extra await page.getByRole("button", { name: "System", exact: true }).click(); const system = page.locator(".system-page"); - await expect(system.getByRole("listitem").filter({ hasText: "Default harness" })).toContainText("Codex"); await expect(system.getByRole("listitem").filter({ hasText: "Daemon gateway" })).toContainText("Enabled"); await expect(system.getByRole("listitem").filter({ hasText: "Managed sandbox" })).toContainText("Docker"); await expect(system.getByRole("listitem").filter({ hasText: "Endpoint overrides" })).toContainText("Configured"); - await expect(system.getByRole("listitem")).toHaveCount(4); + await expect(system.getByRole("listitem")).toHaveCount(3); await expect(system).not.toContainText("Configured for this process"); await expect(system).not.toContainText("Managed execution"); await expect(system).not.toContainText("Self-hosted execution"); - await expect(system).toContainText("Used by Agents created in this Web UI"); - await expect(system).toContainText("This UI does not expose adapter selection"); + await expect(system).not.toContainText("Default harness"); + await expect(system).toContainText("Agent create and edit forms can select any adapter enabled for this Core process"); await expect(system).not.toContainText("Enabled · default"); await expect(system).toContainText("Operator endpoint override: configured"); await expect(system).toContainText("Compiled into this build, but not enabled when this Core process started"); diff --git a/apps/web/e2e/core-connection.spec.ts b/apps/web/e2e/core-connection.spec.ts index 401a311d3..38b5c79a5 100644 --- a/apps/web/e2e/core-connection.spec.ts +++ b/apps/web/e2e/core-connection.spec.ts @@ -318,7 +318,7 @@ test("clears startup configuration synchronously and fences a stale read when th await boot(page, request); await page.getByRole("button", { name: "System", exact: true }).click(); const system = page.locator(".system-page"); - await expect(system.getByRole("listitem").filter({ hasText: "Default harness" })).toContainText("Codex"); + await expect(system.locator(".system-harness-card").filter({ hasText: "Codex" })).toContainText("Enabled"); await page.evaluate(() => { (window as ProbeInstrumentationWindow).__holdNextLocalStartup = true; }); @@ -331,17 +331,17 @@ test("clears startup configuration synchronously and fences a stale read when th await dialog.getByLabel("Bearer token").fill("replacement-token"); await dialog.getByRole("button", { name: "Apply connection" }).click(); - const defaultHarness = system.getByRole("listitem").filter({ hasText: "Default harness" }); - await expect(defaultHarness).toContainText("Checking…"); + const daemonGateway = system.getByRole("listitem").filter({ hasText: "Daemon gateway" }); + await expect(daemonGateway).toContainText("Checking…"); await expect(system).not.toContainText("Configured for this process"); await expect.poll(() => page.evaluate(() => (window as ProbeInstrumentationWindow).__oldStartupAbortCount ?? 0)).toBe(1); await expect.poll(() => page.evaluate(() => typeof (window as ProbeInstrumentationWindow).__resolveNewStartup)).toBe("function"); await page.evaluate(() => (window as ProbeInstrumentationWindow).__resolveNewStartup?.()); - await expect(defaultHarness).toContainText("Claude SDK"); + await expect(system.locator(".system-harness-card").filter({ hasText: "Claude SDK" })).toContainText("Enabled"); await expect(system.getByRole("listitem").filter({ hasText: "Managed sandbox" })).toContainText("Microsandbox"); await page.evaluate(() => (window as ProbeInstrumentationWindow).__resolveOldStartup?.()); - await expect(defaultHarness).toContainText("Claude SDK"); + await expect(system.locator(".system-harness-card").filter({ hasText: "Claude SDK" })).toContainText("Enabled"); await expect(system).not.toContainText("Docker · Maintenance"); }); @@ -356,7 +356,7 @@ for (const status of [404, 405]) { await page.getByRole("button", { name: "System", exact: true }).click(); const system = page.locator(".system-page"); - await expect(system.getByRole("listitem").filter({ hasText: "Default harness" })).toContainText("Not exposed"); + await expect(system.getByRole("listitem").filter({ hasText: "Daemon gateway" })).toContainText("Not exposed"); await expect(system).toContainText("This Core version does not expose the startup configuration extension"); await expect(system).not.toContainText("Configured for this process"); await expect(system).not.toContainText("Checking…"); diff --git a/apps/web/e2e/fixture-core.mjs b/apps/web/e2e/fixture-core.mjs index bedeb9584..d997c07df 100644 --- a/apps/web/e2e/fixture-core.mjs +++ b/apps/web/e2e/fixture-core.mjs @@ -1001,6 +1001,7 @@ const server = http.createServer(async (request, response) => { const defaults = savedAgent(`agent_created_${state.sequence}`, body.name ?? null, body.model, baseline + state.sequence); const created = { ...defaults, + ...(body.x_agents_core === undefined ? {} : { x_agents_core: body.x_agents_core }), instructions: body.instructions ?? null, metadata: body.metadata ?? {}, multi_agent: body.multi_agent ?? { enabled: false, max_concurrent_subagents: null }, diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 5e874e518..57cbbc10e 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -2223,6 +2223,7 @@ export function App() { coreBaseUrl={connection.baseUrl} coreError={agentCollectionError} coreState={agentCollectionState} + startupConfiguration={startupConfigurationState === "ready" && startupConfigurationSupported === true ? startupConfiguration : null} vaultCatalog={sessionVaultCatalog} createRequest={agentCreateRequest ?? 0} onCreateRequestConsumed={consumeAgentCreateRequest} diff --git a/apps/web/src/features/agents/AgentForm.tsx b/apps/web/src/features/agents/AgentForm.tsx index 2d5005796..a7e50c586 100644 --- a/apps/web/src/features/agents/AgentForm.tsx +++ b/apps/web/src/features/agents/AgentForm.tsx @@ -1,7 +1,7 @@ import { Info } from "lucide-react"; import { useEffect, useRef, useState, type FormEvent } from "react"; -import type { SavedAgent } from "@agents-core-web/agents-client"; +import type { CoreHarnessKind, SavedAgent } from "@agents-core-web/agents-client"; import { buildModelOptionGroups, @@ -15,7 +15,9 @@ import { type AgentFormSubmitInput, type AgentFormValues, type AgentToolDraft, v interface AgentFormProps { agent?: SavedAgent; + defaultHarness?: CoreHarnessKind; disabled?: boolean; + enabledHarnesses?: readonly CoreHarnessKind[] | null; formId: string; initialValues?: AgentFormValues; knownModels: string[]; @@ -24,9 +26,29 @@ interface AgentFormProps { onSubmit: (input: AgentFormSubmitInput) => Promise; } -export function AgentForm({ agent, disabled = false, formId, initialValues, knownModels, vaultCatalog = null, onDraftChange, onSubmit }: AgentFormProps) { +function harnessLabel(harness: CoreHarnessKind): string { + if (harness === "claude_sdk") return "Claude SDK"; + if (harness === "mcode") return "MiniMax Code"; + return "Codex"; +} + +export function AgentForm({ + agent, + defaultHarness, + disabled = false, + enabledHarnesses = null, + formId, + initialValues, + knownModels, + vaultCatalog = null, + onDraftChange, + onSubmit, +}: AgentFormProps) { const nameRef = useRef(null); const initial = agent ? valuesFromAgent(agent, vaultCatalog) : initialValues ?? valuesFromAgent(undefined, vaultCatalog); + const initialHarness = agent + ? initial.harness + : initial.harness || (defaultHarness && enabledHarnesses?.includes(defaultHarness) ? defaultHarness : ""); const options = buildModelOptionGroups( knownModels, import.meta.env.VITE_AGENT_MODEL_PRESETS, @@ -34,6 +56,8 @@ export function AgentForm({ agent, disabled = false, formId, initialValues, know ); const initialIsSuggested = [...options.configured, ...options.previouslyUsed].includes(initial.model); const [name, setName] = useState(initial.name); + const [harness, setHarness] = useState(initialHarness); + const [harnessModified, setHarnessModified] = useState(initial.harnessModified); const [modelChoice, setModelChoice] = useState( initial.model && !initialIsSuggested ? CUSTOM_MODEL_OPTION : modelOptionValue(initial.model || options.defaultModel), ); @@ -65,9 +89,17 @@ export function AgentForm({ agent, disabled = false, formId, initialValues, know return () => window.cancelAnimationFrame(frame); }, []); + useEffect(() => { + if (!agent && !harness && defaultHarness && enabledHarnesses?.includes(defaultHarness)) { + setHarness(defaultHarness); + } + }, [agent, defaultHarness, enabledHarnesses, harness]); + useEffect(() => { onDraftChange?.({ name, + harness, + harnessModified, model, instructions, metadata, @@ -79,12 +111,14 @@ export function AgentForm({ agent, disabled = false, formId, initialValues, know tools, toolsModified, }); - }, [instructions, metadata, model, name, onDraftChange, reasoningEffort, reasoningSummary, serviceTier, textFormat, textVerbosity, tools, toolsModified]); + }, [harness, harnessModified, instructions, metadata, model, name, onDraftChange, reasoningEffort, reasoningSummary, serviceTier, textFormat, textVerbosity, tools, toolsModified]); const submit = async (event: FormEvent) => { event.preventDefault(); const result = validateAgentForm({ name, + harness, + harnessModified, model, instructions, metadata, @@ -134,6 +168,40 @@ export function AgentForm({ agent, disabled = false, formId, initialValues, know rows={5} /> + {enabledHarnesses !== null ? ( +
+ + + + Only harnesses enabled when this Core process started are selectable. Selection does not prove Runtime, provider, or model readiness. + +
+ ) : null}

{executionAdaptersEnabled - ? <>This UI does not expose adapter selection. API requests can select any enabled adapter. + ? <>Agent create and edit forms can select any adapter enabled for this Core process. : <>This Core process has no daemon gateway, so no execution adapters are active.}

diff --git a/packages/agents-client/src/client.test.ts b/packages/agents-client/src/client.test.ts index 13c0a237d..088206626 100644 --- a/packages/agents-client/src/client.test.ts +++ b/packages/agents-client/src/client.test.ts @@ -529,6 +529,17 @@ describe("OpenAIAgentsClient", () => { } }); + it("projects an explicit Core harness from an effective Session Agent", async () => { + const client = new OpenAIAgentsClient({ fetch: recordingFetch(jsonResponse({ + ...sessionResource(), + agent: { ...agentSnapshot(), x_agents_core: { harness: "claude_sdk" } }, + }), []) }); + + await expect(client.retrieveSession("session")).resolves.toMatchObject({ + agent: { x_agents_core: { harness: "claude_sdk" } }, + }); + }); + it("preserves a pre-stream Session creation API error without opening or retrying", async () => { const calls: FetchCall[] = []; const onOpen = vi.fn(); diff --git a/packages/agents-client/src/client.ts b/packages/agents-client/src/client.ts index 504e0d9f5..42a80e7dd 100644 --- a/packages/agents-client/src/client.ts +++ b/packages/agents-client/src/client.ts @@ -261,6 +261,7 @@ const agentSnapshotFields = new Set([ "id", "model", "name", "instructions", "multi_agent", "reasoning", "service_tier", "text", "tools", ]); +const agentSnapshotAcceptedFields = new Set([...agentSnapshotFields, "x_agents_core"]); const multiAgentFields = new Set(["enabled", "max_concurrent_subagents"]); const reasoningFields = new Set(["effort", "summary"]); const textFields = new Set(["format", "verbosity"]); @@ -645,7 +646,11 @@ function invalidSessionResource(message = "Agent Core returned an invalid Sessio } function projectAgentSnapshot(value: unknown): AgentSession["agent"] { - if (!isRecord(value) || !exactFields(value, agentSnapshotFields)) return invalidSessionResource(); + if ( + !isRecord(value) || !onlyFields(value, agentSnapshotAcceptedFields) || + [...agentSnapshotFields].some((field) => !hasOwn(value, field)) + ) return invalidSessionResource(); + const agentsCore = value.x_agents_core; const multiAgent = value.multi_agent; const reasoning = value.reasoning; const text = value.text; @@ -654,6 +659,9 @@ function projectAgentSnapshot(value: unknown): AgentSession["agent"] { typeof value.model !== "string" || value.model.trim() === "" || !(value.name === null || typeof value.name === "string") || !(value.instructions === null || typeof value.instructions === "string") || + !(agentsCore === undefined || agentsCore === null || ( + isRecord(agentsCore) && exactFields(agentsCore, new Set(["harness"])) && isHarnessKind(agentsCore.harness) + )) || !isRecord(multiAgent) || !exactFields(multiAgent, multiAgentFields) || typeof multiAgent.enabled !== "boolean" || !(multiAgent.max_concurrent_subagents === null || @@ -677,6 +685,9 @@ function projectAgentSnapshot(value: unknown): AgentSession["agent"] { return { id: value.id, + ...(agentsCore === undefined + ? {} + : { x_agents_core: agentsCore === null ? null : { harness: agentsCore.harness as CoreHarnessKind } }), model: value.model, name: value.name, instructions: value.instructions, diff --git a/packages/agents-client/src/protocol-types.test.ts b/packages/agents-client/src/protocol-types.test.ts index 3c1384129..d82bee6b2 100644 --- a/packages/agents-client/src/protocol-types.test.ts +++ b/packages/agents-client/src/protocol-types.test.ts @@ -12,6 +12,7 @@ import turnResources from "./fixtures/parsar-0438880a/turn-resources.json"; import toolProfiles from "./fixtures/parsar-2b34ea46/tool-profiles.json"; import type { AnonymousHttpMcpToolInput, + AgentsCoreSelection, AgentCore, AgentSession, AgentEnvironmentResource, @@ -156,6 +157,14 @@ describe("Parsar 2b34ea46 bounded tool profiles", () => { }); }); +describe("Core harness selection extension", () => { + it("uses the same nullable selection shape for saved, create, update, and inline Agents", () => { + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + }); +}); + describe("Parsar c31f8167 Environment files list", () => { it("pins the direct-file metadata page and opaque cursor shape", () => { const first = environmentFiles.pages[0] as EnvironmentFileList; diff --git a/packages/agents-client/src/types.ts b/packages/agents-client/src/types.ts index 40c550700..b533aaedd 100644 --- a/packages/agents-client/src/types.ts +++ b/packages/agents-client/src/types.ts @@ -173,6 +173,7 @@ export interface VaultCredentialDeleted { export interface SavedAgent { id: string; object: "agent"; + x_agents_core?: AgentsCoreSelection | null; model: string; name: string | null; instructions: string | null; @@ -190,6 +191,7 @@ export interface SavedAgent { } export interface CreateAgentInput { + x_agents_core?: AgentsCoreSelection | null; model: string; name?: string | null; instructions?: string | null; @@ -204,6 +206,7 @@ export interface CreateAgentInput { export type UpdateAgentInput = Partial; export interface InlineAgentInput { + x_agents_core?: AgentsCoreSelection | null; model?: string; instructions?: string | null; tools?: ConfigurableAgentToolInput[] | null; @@ -718,6 +721,10 @@ export interface CreateSessionStreamOptions extends StreamOptions { export type CoreHarnessKind = "claude_sdk" | "codex" | "mcode"; export type CoreManagedSandboxProvider = "docker" | "microsandbox"; +export interface AgentsCoreSelection { + harness: CoreHarnessKind; +} + export interface CoreStartupConfiguration { object: "agents.core.startup_configuration"; schema_version: 1;