From 4cdb71eb4892af04ad3f1eb406e2a52a2043cb2a Mon Sep 17 00:00:00 2001 From: bo Date: Tue, 4 Aug 2026 16:54:56 +0800 Subject: [PATCH 1/3] feat(runtime): add resilient recovery control plane --- apps/server/src/app.test.ts | 6 - apps/server/src/app.ts | 3 - apps/server/src/boot.ts | 8 +- apps/server/src/errors.ts | 10 +- apps/server/src/main.ts | 8 + .../server/src/routes/config-recovery.test.ts | 75 + apps/server/src/routes/config-recovery.ts | 296 ++++ .../server/src/routes/runtime-control.test.ts | 129 ++ apps/server/src/routes/runtime-control.ts | 82 ++ apps/server/src/server-host.test.ts | 1274 ++++++++++++++++- apps/server/src/server-host.ts | 631 ++++++-- apps/server/src/setup-grant.test.ts | 12 +- .../src/{setup-grant.ts => terminal-grant.ts} | 15 +- apps/web/package.json | 2 +- apps/web/src/api/config-recovery.test.ts | 55 + apps/web/src/api/config-recovery.ts | 54 + apps/web/src/api/runtime-data.test.ts | 48 + apps/web/src/api/runtime-data.ts | 24 + apps/web/src/api/update.ts | 37 +- .../bootstrap/BootstrapGate.test.tsx | 78 +- .../components/bootstrap/BootstrapGate.tsx | 74 +- .../features/ConfigRecoverySettings.tsx | 28 + ...ettingsConfigRecoveryPanel.interaction.tsx | 185 +++ .../features/SettingsConfigRecoveryPanel.tsx | 326 +++++ .../features/SettingsDialog.interaction.tsx | 266 +++- .../features/SettingsDialog.test.tsx | 38 +- .../components/features/SettingsDialog.tsx | 62 +- .../SettingsRuntimeDataPanel.interaction.tsx | 178 +++ .../features/SettingsRuntimeDataPanel.tsx | 298 ++++ .../features/SettingsUpdatesPanel.tsx | 10 +- .../components/features/settings-helpers.ts | 2 +- .../components/features/settings-panels.tsx | 36 +- apps/web/src/main.tsx | 38 +- design-system/pages/settings.md | 117 ++ ...untime-control-plane-recovery-plan-goal.md | 192 +++ ...runtime-control-plane-recovery-progress.md | 92 ++ .../__arch__/automation-boundaries.test.ts | 9 +- .../__arch__/runtime-data-boundaries.test.ts | 37 + packages/agent-core/src/config/index.ts | 6 + .../src/config/server-config-service.test.ts | 335 ++++- .../src/config/server-config-service.ts | 516 ++++++- packages/agent-core/src/index.ts | 7 + ...ead-architecture-flows.integration.test.ts | 3 +- packages/agent-core/src/main.test.ts | 50 +- .../runtime-automations.integration.test.ts | 3 +- .../src/runtime-automations.test.ts | 3 +- packages/agent-core/src/runtime-data/index.ts | 9 + .../src/runtime-data/service.test.ts | 543 +++++++ .../agent-core/src/runtime-data/service.ts | 588 ++++++++ packages/agent-core/src/runtime-mcp.test.ts | 64 +- packages/agent-core/src/runtime.ts | 96 +- .../tool-output/artifact-lifecycle.test.ts | 14 +- packages/protocol/src/bootstrap.ts | 58 +- packages/protocol/src/index.ts | 1 + packages/protocol/src/runtime-data.ts | 63 + 55 files changed, 6891 insertions(+), 303 deletions(-) create mode 100644 apps/server/src/routes/config-recovery.test.ts create mode 100644 apps/server/src/routes/config-recovery.ts create mode 100644 apps/server/src/routes/runtime-control.test.ts create mode 100644 apps/server/src/routes/runtime-control.ts rename apps/server/src/{setup-grant.ts => terminal-grant.ts} (63%) create mode 100644 apps/web/src/api/config-recovery.test.ts create mode 100644 apps/web/src/api/config-recovery.ts create mode 100644 apps/web/src/api/runtime-data.test.ts create mode 100644 apps/web/src/api/runtime-data.ts create mode 100644 apps/web/src/components/features/ConfigRecoverySettings.tsx create mode 100644 apps/web/src/components/features/SettingsConfigRecoveryPanel.interaction.tsx create mode 100644 apps/web/src/components/features/SettingsConfigRecoveryPanel.tsx create mode 100644 apps/web/src/components/features/SettingsRuntimeDataPanel.interaction.tsx create mode 100644 apps/web/src/components/features/SettingsRuntimeDataPanel.tsx create mode 100644 design-system/pages/settings.md create mode 100644 docs/goals/runtime-control-plane-recovery-plan-goal.md create mode 100644 docs/goals/runtime-control-plane-recovery-progress.md create mode 100644 packages/agent-core/src/__arch__/runtime-data-boundaries.test.ts create mode 100644 packages/agent-core/src/runtime-data/index.ts create mode 100644 packages/agent-core/src/runtime-data/service.test.ts create mode 100644 packages/agent-core/src/runtime-data/service.ts create mode 100644 packages/protocol/src/runtime-data.ts diff --git a/apps/server/src/app.test.ts b/apps/server/src/app.test.ts index 7d6f3a7f..18944de0 100644 --- a/apps/server/src/app.test.ts +++ b/apps/server/src/app.test.ts @@ -5,12 +5,6 @@ import { createRuntimeApp } from "./app"; import { globalEventBus } from "./events/global-event-bus"; const mockRuntime = { - configService: { - getSnapshot: mock(async () => ({ config: { provider: {}, profiles: {} }, revision: "test", modelRuntimeRevision: "test", configPath: "/test", restartRequiredSections: [] })), - getModelRuntimeCatalog: mock(() => ({ revision: "test", providers: [], profileDefaults: {} })), - getProviderAdapterCatalog: mock(() => []), - save: mock(async () => ({ config: { provider: {}, profiles: {} }, revision: "test", modelRuntimeRevision: "test", configPath: "/test", restartRequiredSections: [] })), - }, listAgentDescriptors: mock(() => []), subscribeSessionEvents: mock(() => () => undefined), subscribeHitlEvents: mock(() => () => undefined), diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index cef76075..83d04482 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -7,7 +7,6 @@ import { } from "@archcode/agent-core"; import type { GlobalSSEEvent } from "@archcode/protocol"; import { errorHandler } from "./error-handler"; -import { createConfigRoutes } from "./routes/config"; import { createAgentsRoutes } from "./routes/agents"; import { createCompressionRoutes } from "./routes/compression"; import { createDirectoriesRoutes } from "./routes/directories"; @@ -83,7 +82,6 @@ export function createRuntimeApp( const files = createFilesRoutes(serverRuntime); const directories = createDirectoriesRoutes(); const mcp = createMcpRoutes(serverRuntime); - const config = createConfigRoutes(serverRuntime.configService); app.route("/api", globalWork); app.route("/api/projects", projects); @@ -98,7 +96,6 @@ export function createRuntimeApp( app.route("/api/events", globalEvents); app.route("/api/projects", files); app.route("/api/mcp", mcp); - app.route("/api/config", config); app.route("/api/sessions", new Hono()); app.route("/api/agents", agents); app.route("/api/files", new Hono()); diff --git a/apps/server/src/boot.ts b/apps/server/src/boot.ts index 0f960239..56ecbea8 100644 --- a/apps/server/src/boot.ts +++ b/apps/server/src/boot.ts @@ -45,10 +45,14 @@ export async function bootServer( ...(options.version ? { version: options.version } : {}), }, }); - for (const instruction of host.setupInstructions(url)) { - logger.info("server.setup.required", { message: instruction }); + for (const instruction of host.terminalInstructions(url)) { + logger.info("server.terminal_action.required", { message: instruction }); } + // The listener is deliberately published before Runtime activation begins. + // Runtime startup may be slow or fail; the control plane remains responsive. + host.startRuntimeActivation(); + if (Bun.env[ENV_OPEN_BROWSER]) { // Browser opening will be implemented when the web UI workflow is ready. } diff --git a/apps/server/src/errors.ts b/apps/server/src/errors.ts index 09c946b2..35bf4bcd 100644 --- a/apps/server/src/errors.ts +++ b/apps/server/src/errors.ts @@ -27,6 +27,7 @@ export type ServerErrorCode = | "PROJECT_TODO_DISCUSSION_UNAUTHORIZED" | "CONFIG_REVISION_CONFLICT" | "CONFIG_VALIDATION_ERROR" + | "CONFIG_RECOVERY_CONFLICT" | "TOOL_OUTPUT_FORBIDDEN" | "TOOL_OUTPUT_NOT_FOUND" | "TOOL_OUTPUT_EXPIRED" @@ -55,7 +56,14 @@ export type ServerErrorCode = | "UPDATE_INSTALL_FAILED" | "UPDATE_NOT_AVAILABLE" | "UPDATE_RESTART_UNAVAILABLE" - | "UPDATE_RUNTIME_BUSY"; + | "UPDATE_RUNTIME_BUSY" + | "RUNTIME_BUSY" + | "RUNTIME_CLEANUP_INCOMPLETE" + | "EMPTY_PROJECT_SLUGS" + | "DUPLICATE_PROJECT_SLUG" + | "PROJECT_NOT_REGISTERED" + | "PROJECT_HAS_NO_ISSUES" + | "DELETE_TARGET_UNSAFE"; export class ServerError extends Error { constructor( diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 6d61b96d..d207babf 100755 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -3,6 +3,8 @@ import { createConsoleLogger, createRuntime, type Logger, + ProjectRegistry, + RuntimeDataService, ServerConfigService, } from "@archcode/agent-core"; import { @@ -46,6 +48,10 @@ async function main( const configService = new ServerConfigService(); const compiled = isCompiledRuntime(); const restartController = new ServerRestartController(); + const projectRegistry = new ProjectRegistry({ + logger: logger.child({ module: "projects.registry" }), + }); + const runtimeDataService = new RuntimeDataService({ projectRegistry }); const updateService = new UpdateService({ currentVersion: options.version ?? "development", executablePath: process.execPath, @@ -71,6 +77,8 @@ async function main( version: options.version, updateService, restartController, + projectRegistry, + runtimeDataService, }); await bootServer(host, { port: options.port, diff --git a/apps/server/src/routes/config-recovery.test.ts b/apps/server/src/routes/config-recovery.test.ts new file mode 100644 index 00000000..e58cef45 --- /dev/null +++ b/apps/server/src/routes/config-recovery.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, test } from "bun:test"; +import { safeConfigRecoveryStatus } from "./config-recovery"; + +describe("safe Config Recovery diagnostics", () => { + test("removes dynamic Config keys and raw validation values", () => { + const text = JSON.stringify(safeConfigRecoveryStatus( + "/Users/test/.archcode/config.json", + [{ + path: "provider.raw-secret-provider.options.raw-secret-option", + message: "Unsupported provider package \"raw-secret-package\"", + }, { + path: "provider.raw-secret-provider.models.raw-secret-model.variants.raw-secret-variant.temperature", + message: "raw-secret-value is invalid", + }, { + path: "mcp.servers.raw-secret-server.headers.raw-secret-header", + message: "raw-secret-header-value is invalid", + }, { + path: "memory.raw-secret-field", + message: "raw-secret-memory-value is invalid", + }], + )); + + expect(text).not.toContain("raw-secret"); + expect(JSON.parse(text)).toEqual({ + configPath: "/Users/test/.archcode/config.json", + removableItems: [], + issues: [{ + path: "provider..options.