diff --git a/AGENTS.md b/AGENTS.md
index 05048c70..8453be7f 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -283,11 +283,17 @@ bun run changeset
**Before committing:**
```bash
-bun run test # Run all tests
+bun run test # Run all tests (root script — NOT `bun test`, which uses Bun's native runner)
bun typecheck # Type check all packages
bun lint # Run linting
```
+Host tests specifically use vitest via the workspace script:
+```bash
+bun run --cwd host test # NODE_ENV=production BOS_CONFIG_PATH=../bos.config.json vitest run
+```
+Always use `bun run test` / `bun run --cwd host test` — never `bun test`, which invokes Bun's built-in runner and produces different (and misleading) results.
+
## Common Patterns
### Authentication Check
diff --git a/host/src/middleware/static-proxy.ts b/host/src/middleware/static-proxy.ts
index 2cbb2b38..27a323ce 100644
--- a/host/src/middleware/static-proxy.ts
+++ b/host/src/middleware/static-proxy.ts
@@ -1,4 +1,12 @@
+import type { Context } from "hono";
import { proxy } from "hono/proxy";
+import type { AuthVariables } from "../lib/auth";
+import { STATIC_ASSET_PATTERN } from "../middleware/security";
+import type { RuntimeConfig } from "../services/config";
+import { getTenantRuntimeErrorResponse, resolveRequestRuntime } from "../services/tenant-runtime";
+import { logger } from "../utils/logger";
+
+type HonoEnv = { Variables: AuthVariables };
export async function proxyRequest(
req: Request,
@@ -83,3 +91,34 @@ export async function proxyStaticAssetRequest(req: Request, targetBase: string):
return response;
}
+
+export function createStaticAssetProxyHandler(config: RuntimeConfig) {
+ return async (c: Context, next: () => Promise) => {
+ const { pathname } = new URL(c.req.url);
+
+ if (
+ pathname === "/" ||
+ pathname === "/api" ||
+ pathname.startsWith("/api/") ||
+ pathname === "/health"
+ ) {
+ return next();
+ }
+
+ const lastSegment = pathname.split("/").pop() ?? "";
+ if (!STATIC_ASSET_PATTERN.test(lastSegment)) {
+ return next();
+ }
+
+ try {
+ const runtime = await resolveRequestRuntime(config, c.req.raw, {
+ verification: "stale-while-revalidate",
+ });
+ return await proxyStaticAssetRequest(c.req.raw, runtime.config.ui.url);
+ } catch (error) {
+ const { message, status } = getTenantRuntimeErrorResponse(error);
+ logger.error(`[Proxy Asset] ${c.req.method} ${c.req.path} — ${message}`);
+ return c.text(message, { status: status as 404 | 500 | 502 });
+ }
+ };
+}
diff --git a/host/src/program.ts b/host/src/program.ts
index da9644d4..8c9b8c3d 100644
--- a/host/src/program.ts
+++ b/host/src/program.ts
@@ -8,128 +8,24 @@ import {
Layer,
ManagedRuntime,
} from "every-plugin/effect";
-import { getBaseStyles, getHydrateScript, getThemeInitScript } from "everything-dev/ui/head";
import { type Context, Hono } from "hono";
import type { AuthVariables } from "./lib/auth";
-import { getCspStrict, SecurityMiddleware, STATIC_ASSET_PATTERN } from "./middleware/security";
-import { proxyStaticAssetRequest } from "./middleware/static-proxy";
+import { getCspStrict, SecurityMiddleware } from "./middleware/security";
+import { createStaticAssetProxyHandler } from "./middleware/static-proxy";
import { setupApiRoutes } from "./routes/api";
import type { HealthLoadingState } from "./routes/health";
-import { buildPluginContext, createSessionMiddleware, registerAuthHandler } from "./services/auth";
-import { type ClientRuntimeConfig, ConfigService, type RuntimeConfig } from "./services/config";
-import { loadRouterModule, resetFederationInstance } from "./services/federation.server";
+import { createSsrFallbackHandler } from "./routes/ssr";
+import { createSessionMiddleware, registerAuthHandler } from "./services/auth";
+import { ConfigService, type RuntimeConfig } from "./services/config";
+import { resetFederationInstance } from "./services/federation.server";
import { startIntegrityMonitor } from "./services/integrity-monitor";
import { closeMcpServer } from "./services/mcp";
-import { createPluginsClient, type PluginResult, PluginsService } from "./services/plugins";
-import { getTenantRuntimeErrorResponse, resolveRequestRuntime } from "./services/tenant-runtime";
-import type { RouterModule, RuntimePlugin } from "./types";
+import { PluginsService } from "./services/plugins";
import { extractErrorDetails } from "./utils/errors";
import { logger } from "./utils/logger";
-import { normalizeUrl } from "./utils/normalize";
type HonoEnv = { Variables: AuthVariables };
-type ActiveRuntimeState = NonNullable;
-
-type RuntimeClientConfig = ClientRuntimeConfig & { runtime?: ActiveRuntimeState };
-
-function getFallbackGatewayId(config: RuntimeConfig) {
- if (config.domain) {
- return config.domain;
- }
-
- return normalizeUrl(config.host?.url)?.replace(/^https?:\/\//, "") ?? "runtime";
-}
-
-function resolveActiveRuntime(config: RuntimeConfig, request: Request) {
- const url = new URL(request.url);
- const fallbackGatewayId = getFallbackGatewayId(config);
- return {
- accountId: config.account,
- gatewayId: fallbackGatewayId,
- runtimeBasePath: "/",
- title: config.title ?? config.account,
- description: config.description ?? null,
- hostUrl: url.origin,
- } satisfies ActiveRuntimeState;
-}
-
-function buildRuntimeClientConfig(
- config: RuntimeConfig,
- request: Request,
- activeRuntime: ActiveRuntimeState,
- plugins: PluginResult,
-): RuntimeClientConfig {
- const requestUrl = new URL(request.url);
- const uiConfig = config.ui;
-
- if (!uiConfig) {
- throw new Error("UI config is required to build the runtime client config");
- }
-
- return {
- env: config.env,
- account: activeRuntime.accountId,
- networkId: config.account.endsWith(".testnet") ? "testnet" : "mainnet",
- hostUrl: requestUrl.origin,
- assetsUrl: uiConfig.url,
- apiBase: "/api",
- rpcBase: "/api/rpc",
- authAvailable: plugins.auth !== null,
- repository: config.repository,
- ui: {
- name: uiConfig.name,
- url: uiConfig.url,
- entry: uiConfig.entry,
- integrity: uiConfig.integrity,
- },
- api: config.api
- ? {
- name: config.api.name,
- url: config.api.url,
- entry: config.api.entry,
- integrity: config.api.integrity,
- ...(config.api.variables ? { variables: config.api.variables } : {}),
- }
- : undefined,
- auth: config.auth
- ? {
- name: config.auth.name,
- url: config.auth.url,
- entry: config.auth.entry,
- integrity: config.auth.integrity,
- ...(config.auth.variables ? { variables: config.auth.variables } : {}),
- }
- : undefined,
- plugins: Object.fromEntries(
- (Object.entries(config.plugins ?? {}) as Array<[string, RuntimePlugin]>).map(
- ([key, plugin]) => [
- key,
- {
- name: plugin.name,
- url: plugin.url,
- entry: plugin.entry,
- integrity: plugin.integrity,
- ...(plugin.variables ? { variables: plugin.variables } : {}),
- ...(plugin.ui
- ? {
- ui: {
- name: plugin.ui.name,
- url: plugin.ui.url,
- entry: plugin.ui.entry,
- source: plugin.ui.source,
- integrity: plugin.ui.integrity,
- },
- }
- : {}),
- },
- ],
- ),
- ),
- runtime: activeRuntime,
- } as RuntimeClientConfig;
-}
-
export const createStartServer = (onReady?: () => void) =>
Effect.gen(function* () {
const port = Number(process.env.PORT) || 3000;
@@ -171,73 +67,7 @@ export const createStartServer = (onReady?: () => void) =>
ssrEnabled: Boolean(uiConfig.ssrUrl),
};
- const renderClientShell = (
- ctx: Context,
- runtimeSourceConfig: RuntimeConfig,
- runtimeConfig: ClientRuntimeConfig,
- error?: Error | null,
- ) => {
- const nonce = CSP_STRICT ? ctx.get("secureHeadersNonce") : undefined;
- const uiIntegrity = runtimeSourceConfig.ui.integrity;
- const assetsUrl = runtimeConfig.assetsUrl.replace(/\/$/, "");
- const nonceAttr = nonce ? ` nonce="${nonce}"` : "";
- const sriAttr = uiIntegrity ? ` integrity="${uiIntegrity}" crossorigin="anonymous"` : "";
- const uiVersion = uiIntegrity ? `?v=${encodeURIComponent(uiIntegrity)}` : "";
-
- const baseStyles = `
- ${getBaseStyles()}
- .shell { min-height: 100vh; min-height: 100dvh; display: flex; align-items: center; justify-content: center; }
- .fade { animation: fadeIn 0.3s ease-in; }
- @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
- .error { color: #fca5a5; }
- `.trim();
-
- const themeScript = ``;
-
- const shellBody = `${
- error
- ? `
SSR unavailable, showing client app.
${error.message}
`
- : `
Loading...
`
- }
`;
-
- const title =
- runtimeConfig.runtime?.title ?? runtimeSourceConfig.title ?? runtimeSourceConfig.account;
- const hydrateScript =
- (
- getHydrateScript(
- runtimeConfig as Partial,
- undefined,
- undefined,
- nonce,
- ) as { children?: string }
- ).children ?? "";
-
- return ctx.html(
- `
-
-
-
-
- ${title}
-
-
-
- ${themeScript}
-
-
-
- ${shellBody}
- `,
- 200,
- );
- };
-
- const proxyUiAssetRequest = async (c: Context) => {
- const runtime = await resolveRequestRuntime(config, c.req.raw, {
- verification: "stale-while-revalidate",
- });
- return await proxyStaticAssetRequest(c.req.raw, runtime.config.ui.url);
- };
+ app.on(["GET", "HEAD"], "*", createStaticAssetProxyHandler(config));
const sessionMiddleware = createSessionMiddleware(plugins);
@@ -256,107 +86,9 @@ export const createStartServer = (onReady?: () => void) =>
setupApiRoutes(app, config, plugins, sessionMiddleware, loadingState),
);
- app.on(["GET", "HEAD"], "*", async (c: Context, next) => {
- const { pathname } = new URL(c.req.url);
-
- if (
- pathname === "/" ||
- pathname === "/api" ||
- pathname.startsWith("/api/") ||
- pathname === "/health"
- ) {
- return next();
- }
-
- const lastSegment = pathname.split("/").pop() ?? "";
- if (!STATIC_ASSET_PATTERN.test(lastSegment)) {
- return next();
- }
-
- try {
- return await proxyUiAssetRequest(c);
- } catch (error) {
- const { message, status } = getTenantRuntimeErrorResponse(error);
- logger.error(`[Proxy Asset] ${c.req.method} ${c.req.path} — ${message}`);
- return c.text(message, { status: status as 404 | 500 | 502 });
- }
- });
-
app.use("/*", sessionMiddleware);
- app.get("*", async (c: Context) => {
- if (c.req.path === "/api" || c.req.path.startsWith("/api/")) {
- return c.notFound();
- }
-
- let resolvedRuntime: Awaited>;
- try {
- resolvedRuntime = await resolveRequestRuntime(config, c.req.raw, {
- verification: "blocking",
- });
- } catch (error) {
- const { message, status } = getTenantRuntimeErrorResponse(error);
- logger.error(`[SSR] ${c.req.method} ${c.req.path} — ${message}`);
- return c.text(message, { status: status as 404 | 500 | 502 });
- }
-
- const effectiveConfig = resolvedRuntime.config;
- const activeRuntime = await resolveActiveRuntime(effectiveConfig, c.req.raw);
- const nonce = CSP_STRICT ? c.get("secureHeadersNonce") : undefined;
- const runtimeConfig = buildRuntimeClientConfig(
- effectiveConfig,
- c.req.raw,
- activeRuntime,
- plugins,
- );
-
- let ssrRouterModule: RouterModule | null = null;
- let moduleLoadError: Error | null = null;
-
- if (effectiveConfig.ui.ssrUrl) {
- const result = await Effect.runPromise(
- loadRouterModule(effectiveConfig).pipe(Effect.either),
- );
- if (result._tag === "Right") {
- ssrRouterModule = result.right;
- } else {
- moduleLoadError = result.left;
- logger.error("[SSR] Failed to load Router module:", moduleLoadError);
- }
- }
-
- if (ssrRouterModule && effectiveConfig.ui.ssrUrl) {
- try {
- const pluginContext = buildPluginContext(c);
- const ssrApiClient = createPluginsClient(plugins, pluginContext);
-
- const render = () =>
- ssrRouterModule.renderToStream(c.req.raw, {
- session: c.get("session") ? { session: c.get("session"), user: c.get("user") } : null,
- basepath: runtimeConfig.runtime?.runtimeBasePath,
- runtimeConfig,
- apiClient: ssrApiClient,
- cspNonce: nonce,
- });
-
- const result = await render();
- const responseHeaders = new Headers(result?.headers);
- const cspHeader = c.res.headers.get("Content-Security-Policy");
- if (cspHeader) {
- responseHeaders.set("Content-Security-Policy", cspHeader);
- }
- return new Response(result?.stream, {
- status: result?.statusCode,
- headers: responseHeaders,
- });
- } catch (error) {
- logger.error("[SSR] Streaming error:", error);
- moduleLoadError = error as Error;
- }
- }
-
- return renderClientShell(c, effectiveConfig, runtimeConfig, moduleLoadError);
- });
+ app.get("*", createSsrFallbackHandler(config, plugins, CSP_STRICT));
const startHttpServer = () => {
const hostname = process.env.HOST || "0.0.0.0";
diff --git a/host/src/routes/html.ts b/host/src/routes/html.ts
new file mode 100644
index 00000000..a1b82347
--- /dev/null
+++ b/host/src/routes/html.ts
@@ -0,0 +1,67 @@
+import { getBaseStyles, getHydrateScript, getThemeInitScript } from "everything-dev/ui/head";
+import type { Context } from "hono";
+import type { AuthVariables } from "../lib/auth";
+import type { ClientRuntimeConfig, RuntimeConfig } from "../services/config";
+
+type HonoEnv = { Variables: AuthVariables };
+
+export function renderClientShell(
+ ctx: Context,
+ nonce: string | undefined,
+ runtimeSourceConfig: RuntimeConfig,
+ runtimeConfig: ClientRuntimeConfig,
+ error?: Error | null,
+) {
+ const uiIntegrity = runtimeSourceConfig.ui.integrity;
+ const assetsUrl = runtimeConfig.assetsUrl.replace(/\/$/, "");
+ const nonceAttr = nonce ? ` nonce="${nonce}"` : "";
+ const sriAttr = uiIntegrity ? ` integrity="${uiIntegrity}" crossorigin="anonymous"` : "";
+ const uiVersion = uiIntegrity ? `?v=${encodeURIComponent(uiIntegrity)}` : "";
+
+ const baseStyles = `
+ ${getBaseStyles()}
+ .shell { min-height: 100vh; min-height: 100dvh; display: flex; align-items: center; justify-content: center; }
+ .fade { animation: fadeIn 0.3s ease-in; }
+ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
+ .error { color: #fca5a5; }
+ `.trim();
+
+ const themeScript = ``;
+
+ const shellBody = `${
+ error
+ ? `
SSR unavailable, showing client app.
${error.message}
`
+ : `
Loading...
`
+ }
`;
+
+ const title =
+ runtimeConfig.runtime?.title ?? runtimeSourceConfig.title ?? runtimeSourceConfig.account;
+ const hydrateScript =
+ (
+ getHydrateScript(
+ runtimeConfig as Partial,
+ undefined,
+ undefined,
+ nonce,
+ ) as { children?: string }
+ ).children ?? "";
+
+ return ctx.html(
+ `
+
+
+
+
+ ${title}
+
+
+
+ ${themeScript}
+
+
+
+ ${shellBody}
+ `,
+ 200,
+ );
+}
diff --git a/host/src/routes/ssr.ts b/host/src/routes/ssr.ts
new file mode 100644
index 00000000..4512d4ca
--- /dev/null
+++ b/host/src/routes/ssr.ts
@@ -0,0 +1,95 @@
+import { Effect } from "every-plugin/effect";
+import type { Context } from "hono";
+import type { AuthVariables } from "../lib/auth";
+import { buildPluginContext } from "../services/auth";
+import {
+ buildRuntimeClientConfig,
+ type RuntimeConfig,
+ resolveActiveRuntime,
+} from "../services/config";
+import { loadRouterModule } from "../services/federation.server";
+import { createPluginsClient, type PluginResult } from "../services/plugins";
+import { getTenantRuntimeErrorResponse, resolveRequestRuntime } from "../services/tenant-runtime";
+import type { RouterModule } from "../types";
+import { logger } from "../utils/logger";
+import { renderClientShell } from "./html";
+
+type HonoEnv = { Variables: AuthVariables };
+
+export function createSsrFallbackHandler(
+ config: RuntimeConfig,
+ plugins: PluginResult,
+ CSP_STRICT: boolean,
+) {
+ return async (c: Context) => {
+ if (c.req.path === "/api" || c.req.path.startsWith("/api/")) {
+ return c.notFound();
+ }
+
+ let resolvedRuntime: Awaited>;
+ try {
+ resolvedRuntime = await resolveRequestRuntime(config, c.req.raw, {
+ verification: "blocking",
+ });
+ } catch (error) {
+ const { message, status } = getTenantRuntimeErrorResponse(error);
+ logger.error(`[SSR] ${c.req.method} ${c.req.path} — ${message}`);
+ return c.text(message, { status: status as 404 | 500 | 502 });
+ }
+
+ const effectiveConfig = resolvedRuntime.config;
+ const activeRuntime = resolveActiveRuntime(effectiveConfig, c.req.raw);
+ const nonce = CSP_STRICT ? c.get("secureHeadersNonce") : undefined;
+ const runtimeConfig = buildRuntimeClientConfig(
+ effectiveConfig,
+ c.req.raw,
+ activeRuntime,
+ plugins.auth !== null,
+ );
+
+ let ssrRouterModule: RouterModule | null = null;
+ let moduleLoadError: Error | null = null;
+
+ if (effectiveConfig.ui.ssrUrl) {
+ const result = await Effect.runPromise(loadRouterModule(effectiveConfig).pipe(Effect.either));
+ if (result._tag === "Right") {
+ ssrRouterModule = result.right;
+ } else {
+ moduleLoadError = result.left;
+ logger.error("[SSR] Failed to load Router module:", moduleLoadError);
+ }
+ }
+
+ if (ssrRouterModule && effectiveConfig.ui.ssrUrl) {
+ try {
+ const pluginContext = buildPluginContext(c);
+ const ssrApiClient = createPluginsClient(plugins, pluginContext);
+
+ const render = () =>
+ ssrRouterModule.renderToStream(c.req.raw, {
+ session: c.get("session") ? { session: c.get("session"), user: c.get("user") } : null,
+ basepath: runtimeConfig.runtime?.runtimeBasePath,
+ runtimeConfig,
+ apiClient: ssrApiClient,
+ cspNonce: nonce,
+ });
+
+ const result = await render();
+ const responseHeaders = new Headers(result?.headers);
+ const cspHeader = c.res.headers.get("Content-Security-Policy");
+ if (cspHeader) {
+ responseHeaders.set("Content-Security-Policy", cspHeader);
+ }
+ return new Response(result?.stream, {
+ status: result?.statusCode,
+ headers: responseHeaders,
+ });
+ } catch (error) {
+ logger.error("[SSR] Streaming error:", error);
+ moduleLoadError = error as Error;
+ }
+ }
+
+ return renderClientShell(c, nonce, effectiveConfig, runtimeConfig, moduleLoadError);
+ };
+}
diff --git a/host/src/services/config.ts b/host/src/services/config.ts
index 34243d53..59826803 100644
--- a/host/src/services/config.ts
+++ b/host/src/services/config.ts
@@ -5,6 +5,8 @@ import type {
SharedConfig,
SourceMode,
} from "everything-dev/types";
+import type { RuntimePlugin } from "../types";
+import { normalizeUrl } from "../utils/normalize";
export type { ClientRuntimeConfig, RuntimeConfig, SharedConfig, SourceMode };
@@ -20,3 +22,103 @@ export function readCorsOrigins(): Effect.Effect {
Effect.withConfigProvider(ConfigProvider.fromEnv()),
);
}
+
+export type ActiveRuntimeState = NonNullable;
+
+export type RuntimeClientConfig = ClientRuntimeConfig & { runtime?: ActiveRuntimeState };
+
+function getFallbackGatewayId(config: RuntimeConfig) {
+ if (config.domain) {
+ return config.domain;
+ }
+ return normalizeUrl(config.host?.url)?.replace(/^https?:\/\//, "") ?? "runtime";
+}
+
+export function resolveActiveRuntime(config: RuntimeConfig, request: Request) {
+ const url = new URL(request.url);
+ const fallbackGatewayId = getFallbackGatewayId(config);
+ return {
+ accountId: config.account,
+ gatewayId: fallbackGatewayId,
+ runtimeBasePath: "/",
+ title: config.title ?? config.account,
+ description: config.description ?? null,
+ hostUrl: url.origin,
+ } satisfies ActiveRuntimeState;
+}
+
+export function buildRuntimeClientConfig(
+ config: RuntimeConfig,
+ request: Request,
+ activeRuntime: ActiveRuntimeState,
+ authAvailable: boolean,
+): RuntimeClientConfig {
+ const requestUrl = new URL(request.url);
+ const uiConfig = config.ui;
+
+ if (!uiConfig) {
+ throw new Error("UI config is required to build the runtime client config");
+ }
+
+ return {
+ env: config.env,
+ account: activeRuntime.accountId,
+ networkId: config.account.endsWith(".testnet") ? "testnet" : "mainnet",
+ hostUrl: requestUrl.origin,
+ assetsUrl: uiConfig.url,
+ apiBase: "/api",
+ rpcBase: "/api/rpc",
+ authAvailable,
+ repository: config.repository,
+ ui: {
+ name: uiConfig.name,
+ url: uiConfig.url,
+ entry: uiConfig.entry,
+ integrity: uiConfig.integrity,
+ },
+ api: config.api
+ ? {
+ name: config.api.name,
+ url: config.api.url,
+ entry: config.api.entry,
+ integrity: config.api.integrity,
+ ...(config.api.variables ? { variables: config.api.variables } : {}),
+ }
+ : undefined,
+ auth: config.auth
+ ? {
+ name: config.auth.name,
+ url: config.auth.url,
+ entry: config.auth.entry,
+ integrity: config.auth.integrity,
+ ...(config.auth.variables ? { variables: config.auth.variables } : {}),
+ }
+ : undefined,
+ plugins: Object.fromEntries(
+ (Object.entries(config.plugins ?? {}) as Array<[string, RuntimePlugin]>).map(
+ ([key, plugin]) => [
+ key,
+ {
+ name: plugin.name,
+ url: plugin.url,
+ entry: plugin.entry,
+ integrity: plugin.integrity,
+ ...(plugin.variables ? { variables: plugin.variables } : {}),
+ ...(plugin.ui
+ ? {
+ ui: {
+ name: plugin.ui.name,
+ url: plugin.ui.url,
+ entry: plugin.ui.entry,
+ source: plugin.ui.source,
+ integrity: plugin.ui.integrity,
+ },
+ }
+ : {}),
+ },
+ ],
+ ),
+ ),
+ runtime: activeRuntime,
+ } as RuntimeClientConfig;
+}
diff --git a/host/tests/e2e/runtime-remote.spec.ts b/host/tests/e2e/runtime-remote.spec.ts
index 70fd99e1..05aac168 100644
--- a/host/tests/e2e/runtime-remote.spec.ts
+++ b/host/tests/e2e/runtime-remote.spec.ts
@@ -148,7 +148,15 @@ for (const scenario of scenarios) {
() => performance.getEntriesByType("navigation").length,
);
- await expect(page.getByRole("link", { name: /^Skill$/ })).toBeVisible();
+ try {
+ await expect(page.getByRole("link", { name: /^Skill$/ })).toBeVisible();
+ } catch (e) {
+ const html = await page.content();
+ console.error("[Diagnostic] Page HTML (first 3000 chars):", html.slice(0, 3000));
+ console.error("[Diagnostic] Page errors:", JSON.stringify(pageErrors));
+ console.error("[Diagnostic] Console errors:", JSON.stringify(consoleErrors));
+ throw e;
+ }
await page.getByRole("link", { name: /^Skill$/ }).dispatchEvent("click");
await page.waitForURL(/\/skill$/);
@@ -176,7 +184,10 @@ for (const scenario of scenarios) {
document.documentElement.classList.contains("dark"),
);
- await page.getByRole("button", { name: "Toggle theme" }).click();
+ await page
+ .getByRole("button", { name: /Switch to .* theme/ })
+ .first()
+ .click();
await expect
.poll(async () =>
diff --git a/host/tests/helpers/json-proxy-target.ts b/host/tests/helpers/json-proxy-target.ts
index 83975486..50137ccf 100644
--- a/host/tests/helpers/json-proxy-target.ts
+++ b/host/tests/helpers/json-proxy-target.ts
@@ -23,6 +23,20 @@ export async function startJsonProxyTarget(): Promise {
return;
}
+ if (req.url?.startsWith("/api/auth/")) {
+ res.statusCode = 200;
+ res.setHeader("content-type", "application/json");
+ res.end(JSON.stringify(null));
+ return;
+ }
+
+ if (req.method === "POST" && req.url?.startsWith("/api/rpc/")) {
+ res.statusCode = 200;
+ res.setHeader("content-type", "application/json");
+ res.end(JSON.stringify({ json: null, meta: [] }));
+ return;
+ }
+
res.statusCode = 404;
res.setHeader("content-type", "application/json");
res.end(JSON.stringify({ error: "Not Found" }));
diff --git a/host/tests/helpers/runtime-config.ts b/host/tests/helpers/runtime-config.ts
index 55b7110c..9abd6774 100644
--- a/host/tests/helpers/runtime-config.ts
+++ b/host/tests/helpers/runtime-config.ts
@@ -25,6 +25,21 @@ export async function loadTestRuntimeConfig(): Promise {
return config;
}
+export function createMockSession() {
+ return {
+ user: {
+ id: "test-user-id",
+ name: "Test User",
+ email: "test@everything.dev",
+ role: "user",
+ },
+ session: {
+ id: "test-session-id",
+ expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
+ },
+ };
+}
+
export function createMockAuthClient(): AuthClient {
const noOp = (): Promise<{ data: null; error: null }> =>
Promise.resolve({ data: null, error: null });
@@ -111,11 +126,12 @@ export function buildTestRenderOptions(
config: RuntimeConfig,
apiClient: ApiClient,
authClient?: AuthClient,
+ session = createMockSession(),
): RenderOptionsWithApi {
return {
runtimeConfig: buildTestClientRuntimeConfig(config),
apiClient,
- session: null,
+ session,
authClient,
} satisfies RenderOptionsWithApi;
}
diff --git a/host/tests/integration/csp-nonce.test.ts b/host/tests/integration/csp-nonce.test.ts
index dcad904b..7dfe0e29 100644
--- a/host/tests/integration/csp-nonce.test.ts
+++ b/host/tests/integration/csp-nonce.test.ts
@@ -6,6 +6,7 @@ import { loadBundledRouterModule } from "../helpers/bundled-ssr-module";
import {
buildTestClientRuntimeConfig,
createMockAuthClient,
+ createMockSession,
loadTestRuntimeConfig,
} from "../helpers/runtime-config";
@@ -24,6 +25,7 @@ async function consumeStream(stream: ReadableStream): Promise {
const mockApiClient = createTestApiClient({});
const mockAuthClient = createMockAuthClient();
+const mockSession = createMockSession();
describe("CSP Nonce Regression Tests", () => {
let routerModule: RouterModule;
@@ -49,7 +51,7 @@ describe("CSP Nonce Regression Tests", () => {
const renderOptions: RenderOptionsWithApi = {
runtimeConfig: buildTestClientRuntimeConfig(config),
apiClient: mockApiClient,
- session: null,
+ session: mockSession,
authClient: mockAuthClient,
cspNonce: testNonce,
};
@@ -77,7 +79,7 @@ describe("CSP Nonce Regression Tests", () => {
const renderOptions: RenderOptionsWithApi = {
runtimeConfig: buildTestClientRuntimeConfig(config),
apiClient: mockApiClient,
- session: null,
+ session: mockSession,
authClient: mockAuthClient,
cspNonce: testNonce,
};
@@ -105,7 +107,7 @@ describe("CSP Nonce Regression Tests", () => {
const renderOptions: RenderOptionsWithApi = {
runtimeConfig: buildTestClientRuntimeConfig(config),
apiClient: mockApiClient,
- session: null,
+ session: mockSession,
authClient: mockAuthClient,
cspNonce: testNonce,
};
@@ -126,7 +128,7 @@ describe("CSP Nonce Regression Tests", () => {
const renderOptions: RenderOptionsWithApi = {
runtimeConfig: buildTestClientRuntimeConfig(config),
apiClient: mockApiClient,
- session: null,
+ session: mockSession,
authClient: mockAuthClient,
};
@@ -161,7 +163,7 @@ describe("CSP Nonce Regression Tests", () => {
rpcBase: "/rpc",
},
apiClient: mockApiClient,
- session: null,
+ session: mockSession,
authClient: mockAuthClient,
cspNonce: "typed-nonce-without-cast",
};
@@ -180,7 +182,7 @@ describe("CSP Nonce Regression Tests", () => {
rpcBase: "/rpc",
},
apiClient: mockApiClient,
- session: null,
+ session: mockSession,
authClient: mockAuthClient,
};
diff --git a/host/tests/integration/ssr-bundled-runtime.test.ts b/host/tests/integration/ssr-bundled-runtime.test.ts
index 1f16465c..d26b3177 100644
--- a/host/tests/integration/ssr-bundled-runtime.test.ts
+++ b/host/tests/integration/ssr-bundled-runtime.test.ts
@@ -57,7 +57,7 @@ describe("bundled host SSR runtime", () => {
const html = await response.text();
expect(response.status).toBe(200);
- expect(html).toContain('
");
diff --git a/host/tests/integration/ssr-metadata.test.ts b/host/tests/integration/ssr-metadata.test.ts
index 2b398837..532a419a 100644
--- a/host/tests/integration/ssr-metadata.test.ts
+++ b/host/tests/integration/ssr-metadata.test.ts
@@ -2,7 +2,11 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest";
import type { RenderOptionsWithApi, RouterModule, RuntimeConfig } from "@/types";
import { createTestApiClient } from "../helpers/api-client";
import { loadBundledRouterModule } from "../helpers/bundled-ssr-module";
-import { buildTestClientRuntimeConfig, loadTestRuntimeConfig } from "../helpers/runtime-config";
+import {
+ buildTestClientRuntimeConfig,
+ createMockSession,
+ loadTestRuntimeConfig,
+} from "../helpers/runtime-config";
async function consumeStream(stream: ReadableStream): Promise {
const reader = stream.getReader();
@@ -63,7 +67,7 @@ describe("SSR Social Metadata", () => {
},
},
apiClient: mockApiClient,
- session: null,
+ session: createMockSession(),
};
const result = await routerModule.renderToStream(
@@ -147,20 +151,20 @@ describe("SSR Social Metadata", () => {
it("og:image URL is absolute", () => {
const content = extractMetaContent(streamHtml, "property", "og:image");
- expect(content).toBeDefined();
expect(
content,
"og:image must be an absolute URL for social scrapers (metatags.io, Facebook, Twitter). Got relative URL.",
- ).toMatch(/^https?:\/\//);
+ ).toBeDefined();
+ expect(content!.toString()).toMatch(/^https?:\/\//);
});
it("twitter:image URL is absolute", () => {
const content = extractMetaContent(streamHtml, "name", "twitter:image");
- expect(content).toBeDefined();
expect(
content,
"twitter:image must be an absolute URL for social scrapers. Got relative URL.",
- ).toMatch(/^https?:\/\//);
+ ).toBeDefined();
+ expect(content!.toString()).toMatch(/^https?:\/\//);
});
});
});
diff --git a/plans/react-native-migration.md b/plans/react-native-migration.md
index 0756b24e..840fdc56 100644
--- a/plans/react-native-migration.md
+++ b/plans/react-native-migration.md
@@ -1,5 +1,9 @@
# React Native Migration Plan (Re.Pack Super-App)
+> This plan describes the implementation strategy. For the architectural design and how
+> native plugins integrate with the `app.ts` surface, `everything-dev` subpath exports, and
+> the generic host composition model, see [beta-v2-native.md](./beta-v2-native.md).
+
## Architecture Overview
```
@@ -322,18 +326,22 @@ The RN app is **another client** of the same API, just like the web app.
## Phase 8: Shared Code Strategy
-Extract platform-agnostic logic into shared packages:
+All shared logic lives in `everything-dev` via subpath exports (see
+[beta-v2-native.md](./beta-v2-native.md) for the full list). No separate packages.
```
-packages/
-├── api-client/ # oRPC client factory (shared)
-├── auth-core/ # Auth actions, session types (shared)
-├── runtime-config/ # Config types, helpers like getAccount() (shared)
-├── types/ # Shared TypeScript types (already exists as everything-dev/types)
-└── contract/ # oRPC contract definitions (shared)
+packages/everything-dev/src/
+├── api-client.ts # "everything-dev/api" — oRPC client factory (shared)
+├── auth-core.ts # "everything-dev/auth" — auth actions, session types (shared)
+├── runtime-config.ts # "everything-dev/config" — config types, getAccount() (shared)
+├── types.ts # "everything-dev/types" — shared TypeScript types
+├── web/
+│ └── compose.ts # "everything-dev/web" — composeApp, defineWebPlugin
+└── native/
+ └── compose.ts # "everything-dev/native" — loadNativePlugins, defineNativePlugin
```
-Platform-specific code stays in `ui/` (web) and `app/` (RN):
+Platform-specific code stays in `web/` and `native/`:
- Component libraries (shadcn vs gluestack)
- Navigation (TanStack Router vs React Navigation)
- Auth client setup (cookies vs tokens)
diff --git a/plans/runtime-config-hot-swap.md b/plans/runtime-config-hot-swap.md
deleted file mode 100644
index cf160191..00000000
--- a/plans/runtime-config-hot-swap.md
+++ /dev/null
@@ -1,493 +0,0 @@
-# Runtime Config Hot-Swap
-
-## Problem
-
-The host freezes `RuntimeConfig` at startup. `ConfigService` is an immutable
-`Layer.succeed` — every downstream service (Auth, Plugins, SSR federation, Hono
-routes) reads config once during construction. After `bos publish --deploy`
-updates CDN URLs in `bos.config.json`, the host must be fully restarted to pick
-up changes.
-
-This blocks the "prompt from browser, see it live" workflow: opencode can edit
-files and deploy, but the running host keeps serving the old UI and API.
-
-## Goal
-
-Allow a running host to pick up new runtime config without a process restart.
-In-flight requests complete on the old config; new requests see the new config.
-
-## Architecture
-
-### Scope-based rebuild with atomic handler swap
-
-The host's Effect services are composed into a `ManagedRuntime` that owns the
-full lifecycle (DB connections, plugin runtimes, auth instances). Rather than
-making each service individually reactive (which would require wrapping Auth,
-Plugins, etc. in `Ref` types), we **swap the entire scope**.
-
-The only mutable state is a single function pointer that delegates HTTP requests
-to the currently active scope's Hono app.
-
-```
- ┌──────────────────────────────────────┐
- │ HTTP Server (long-lived) │
- │ serve({ fetch: (req) => │
- │ activeHandler(req) │
- │ }) │
- └──────────────┬───────────────────────┘
- │
- ┌────────────▼─────────────┐
- │ Scope A (current) │
- │ ConfigService = cfg:v1 │
- │ DB ← acquireRelease │
- │ Auth ← betterAuth(v1) │
- │ Plugins ← runtime(v1) │
- │ Hono app ← all of above│
- └──────────────────────────┘
- ↕ atomic swap
- ┌─────────────────────────┐
- │ Scope B (new) │
- │ ConfigService = cfg:v2│
- │ DB ← acquireRelease │
- │ Auth ← betterAuth(v2) │
- │ Plugins ← runtime(v2) │
- │ Hono app ← all of above│
- └─────────────────────────┘
-```
-
-On reload:
-
-1. Build new `ManagedRuntime` with new config → new scope B
-2. Wait for scope B ready (DB connected, plugins loaded, health checks pass)
-3. Atomically swap `activeHandler` to scope B's `app.fetch`
-4. Dispose old scope A (DB close, plugin shutdown, etc.)
-5. In-flight requests on scope A complete; new requests go to scope B
-
-### Why not reactive Refs on each service?
-
-| Service | Issue with per-service Refs |
-|----------|---------------------------|
-| **AuthService** | `betterAuth()` creates internal state (sessions, CSRF, OAuth). Swapping a `Ref` mid-flight breaks active sessions. |
-| **PluginsService** | `createPluginRuntime()` + `usePlugin()` establish remote connections. Requires graceful teardown. |
-| **Hono routes** | Route handlers are closures over `config`, `auth`, `plugins`. You cannot swap values inside closures. |
-| **SSR federation** | Module-level singleton `federationInstance` is outside the Effect layer system. |
-
-Swapping the entire scope leverages Effect's existing `ManagedRuntime.dispose()` —
-all scoped services (DB, plugins) clean up automatically.
-
-## Two host implementations
-
-There are two host codepaths that need changes:
-
-| File | Purpose | Config source | Approach |
-|------|---------|---------------|----------|
-| `packages/everything-dev/src/host.ts` | CLI-orchestrated dev mode | `RuntimeConfig` passed directly | Lazy reload of mutable state |
-| `host/src/program.ts` + `host/server.ts` | Production Effect-based host | `BOS_RUNTIME_CONFIG` env var | Scope-based rebuild |
-
-### CLI host (`packages/everything-dev/src/host.ts`)
-
-The CLI host already does lazy loading:
-
-- `ensureApiPluginLoaded()` — lazy-loads API plugins on first request
-- `ensureRouterModuleLoaded()` — lazy-loads SSR router on first request
-
-For hot-swap, add a `resetState()` function that clears `apiPlugins`,
-`baseApiPlugin`, `ssrRouterModule`, `rpcHandler`, `openApiHandler` and lets the
-lazy-loading pattern re-fetch from new URLs.
-
-```typescript
-function resetState(newConfig: RuntimeConfig) {
- runtimeConfig = newConfig;
- clientRuntimeConfig = buildClientRuntimeConfig(newConfig);
- apiPlugins = [];
- baseApiPlugin = null;
- apiPluginError = null;
- rpcHandler = null;
- openApiHandler = null;
- ssrRouterModule = null;
- ssrRouterError = null;
- ssrRouterLoading = null;
- // Next request triggers lazy reload with new URLs
-}
-```
-
-### Production host (`host/src/program.ts`)
-
-This is the main refactor. Split `createStartServer` into two parts:
-
-1. `createScopedApp(config: RuntimeConfig)` — builds a complete Effect scope
- (DB, Auth, Plugins, Hono app) and returns `{ app, config, shutdown }`
-2. The HTTP server delegates to `activeHandler`, which atomically switches
-
-The outer Hono app (lives for the server's lifetime) owns the reload endpoint.
-The scoped Hono app handles all application routes.
-
-## Trigger mechanism
-
-### `POST /api/_reload-config`
-
-```typescript
-// On the outer (long-lived) Hono app
-outerApp.post("/api/_reload-config", async (c) => {
- // 1. Authenticate via Better Auth session
- const session = await outerAuth.api.getSession({ headers: c.req.raw.headers });
- if (!session?.user) {
- return c.json({ error: "Unauthorized" }, 401);
- }
-
- // 2. Check admin role
- if (!session.user.role || session.user.role !== "admin") {
- return c.json({ error: "Forbidden: admin required" }, 403);
- }
-
- // 3. Get new config
- let newConfig: RuntimeConfig;
- const body = await c.req.json().catch(() => null);
-
- if (body?.config) {
- // Mode A: config provided in request body
- newConfig = RuntimeConfigSchema.parse(body.config);
- } else {
- // Mode B: re-fetch from FastKV using current account/domain
- const currentConfig = currentScopedApp.config;
- const fetched = await fetchPublishedConfig(currentConfig.account, currentConfig.domain ?? "");
- newConfig = buildRuntimeConfig(fetched);
- }
-
- // 4. Swap scope
- await reloadWithConfig(newConfig);
-
- return c.json({
- status: "reloaded",
- accountId: newConfig.account,
- domain: newConfig.domain,
- uiUrl: newConfig.ui?.url,
- apiUrl: newConfig.api?.url,
- });
-});
-```
-
-### Security model
-
-- **Session-authenticated** — uses the same Better Auth session as every admin endpoint
-- **Admin-only** — checks `session.user.role === "admin"`
-- **Config validated** — `RuntimeConfigSchema.parse()` validates the entire
- structure before accepting
-- **No code execution** — config only changes URLs and string values
-- **Same trust boundary as `bos publish`** — publishing to FastKV requires a
- NEAR account key; reloading the host requires an admin session
-
-### Why an HTTP endpoint instead of file watch
-
-1. **Production hosts don't have local `bos.config.json`** — they receive config
- via `BOS_RUNTIME_CONFIG` env var, resolved from FastKV on NEAR
-2. **Docker/Kubernetes** — file watchers don't work well in containers where
- config changes come via environment or API calls
-3. **Browser-triggerable** — the `/opencode` page can call this endpoint after
- opencode completes a deploy
-4. **CLI-triggerable** — `bos publish --deploy` can call it as a post-deploy hook
-5. **FastKV re-fetch** — optionally re-fetches the canonical config from the
- blockchain, which is the source of truth
-
-### FastKV re-fetch
-
-The CLI already has `fetchBosConfigFromFastKv()` in
-`packages/everything-dev/src/fastkv.ts`. The reload endpoint can reuse this:
-
-```typescript
-import { fetchBosConfigFromFastKv } from "everything-dev/fastkv";
-import { buildRuntimeConfig } from "everything-dev/config";
-
-async function fetchLatestConfig(currentConfig: RuntimeConfig): Promise {
- const bosConfig = await fetchBosConfigFromFastKv(
- `bos://${currentConfig.account}/${currentConfig.domain}`
- );
- return buildRuntimeConfig(bosConfig, { env: currentConfig.env });
-}
-```
-
-This means: anyone publishes to FastKV → admin hits reload → host fetches the
-latest canonical config from the blockchain.
-
-## Implementation phases
-
-### Phase 1: Extract `createScopedApp` from `createStartServer`
-
-**File**: `host/src/program.ts`
-
-Split the monolithic `createStartServer` Effect into:
-
-```typescript
-// New type
-export interface ScopedApp {
- app: Hono;
- config: RuntimeConfig;
- shutdown: () => Promise;
-}
-
-// Extracted from createStartServer — builds all services and Hono app
-// but does NOT start the HTTP server or call Effect.never
-function createScopedApp(config: RuntimeConfig): Effect.Effect {
- return Effect.gen(function* () {
- const ConfigLive = Layer.succeed(ConfigService, config);
- const ServerLive = Layer.provideMerge(
- Layer.mergeAll(BaseLive, PluginsLive),
- ConfigLive,
- );
- const runtime = ManagedRuntime.make(ServerLive);
-
- // ... yield all services, build Hono app ...
- // ... same as current createStartServer, minus the server.listen() and Effect.never ...
-
- return {
- app,
- config,
- shutdown: async () => { await runtime.dispose(); },
- };
- });
-}
-```
-
-**Complexity**: High (refactoring the 300-line `createStartServer` function)
-
-**No behavior change** — the existing `runServerBlocking` path works identically,
-just composing `createScopedApp` + `startServer` instead of one function.
-
-### Phase 2: Mutable handler pattern and reload orchestrator
-
-**File**: `host/src/program.ts`
-
-```typescript
-let activeHandler: (req: Request) => Response | Promise;
-let currentScopedApp: ScopedApp | null = null;
-let currentShutdown: (() => Promise) | null = null;
-
-async function reloadWithConfig(newConfig: RuntimeConfig) {
- logger.info("[Reload] Starting config swap...");
-
- // Build new scope
- const newScopedApp = await Effect.runPromise(
- Effect.scoped(createScopedApp(newConfig))
- );
-
- // Wait for health checks
- // (optional: poll /api/_health on newScopedApp)
-
- // Atomic swap
- const oldShutdown = currentShutdown;
- activeHandler = (req) => newScopedApp.app.fetch(req);
- currentScopedApp = newScopedApp;
- currentShutdown = () => newScopedApp.shutdown();
-
- logger.info("[Reload] Handler swapped. Draining old scope.");
-
- // Dispose old scope (graceful — lets in-flight requests finish)
- if (oldShutdown) {
- await oldShutdown();
- }
-
- logger.info("[Reload] Complete.");
-}
-```
-
-The HTTP server becomes:
-
-```typescript
-const server = serve({
- fetch: (req) => activeHandler(req),
- port,
- hostname,
-});
-```
-
-**Complexity**: Medium
-
-### Phase 3: Two-layer Hono app with reload endpoint
-
-**File**: `host/src/program.ts`
-
-```typescript
-// Outer app: lives for the HTTP server's lifetime
-const outerApp = new Hono();
-
-// Health check on outer app (always available, even during reload)
-outerApp.get("/health", (c) => c.text("OK"));
-
-// Reload endpoint on outer app (admin-only)
-outerApp.post("/api/_reload-config", reloadHandler);
-
-// Everything else delegates to the scoped app
-outerApp.all("/*", (c) => {
- return activeHandler(c.req.raw);
-});
-```
-
-The reload handler authenticates via Better Auth's `auth.api.getSession()` and
-validates the config via `RuntimeConfigSchema`.
-
-**Complexity**: Medium
-
-**Key consideration**: The outer app needs its own auth handler. Since `AuthService`
-is inside the scoped app, we need a lightweight auth check on the outer app. The
-simplest approach: create a standalone `betterAuth` instance for the reload
-endpoint that shares the same DB, or cache the auth instance across reloads.
-
-Alternative: keep a long-lived `auth` reference outside the scope that persists
-across reloads. Since DB connections are cheap to acquire, we can create a
-separate DB+auth pair just for the reload endpoint.
-
-### Phase 4: Federation instance reset
-
-**File**: `host/src/services/federation.server.ts`
-
-The module-level singleton `federationInstance` must be reset on reload:
-
-```typescript
-let federationInstance: ReturnType | null = null;
-
-export function resetFederationInstance() {
- federationInstance = null;
-}
-```
-
-Called during `reloadWithConfig()` before building the new scope. When the new
-scope's SSR loader calls `getOrCreateFederationInstance()`, it creates a fresh
-instance with the new UI URLs.
-
-**Complexity**: Low (one function export)
-
-### Phase 5: CLI host reload
-
-**File**: `packages/everything-dev/src/host.ts`
-
-Add `resetState()` function:
-
-```typescript
-function resetState(newConfig: RuntimeConfig) {
- runtimeConfig = newConfig;
- clientRuntimeConfig = buildClientRuntimeConfig(newConfig);
- apiPlugins = [];
- baseApiPlugin = null;
- apiPluginError = null;
- rpcHandler = null;
- openApiHandler = null;
- ssrRouterModule = null;
- ssrRouterError = null;
- ssrRouterLoading = null;
-}
-```
-
-Add the same `/api/_reload-config` endpoint to the CLI host's Hono app.
-
-**Complexity**: Low
-
-### Phase 6: `bos publish --deploy` integration
-
-**File**: `packages/everything-dev/src/plugin.ts`
-
-After `bos publish --deploy` completes (after Zephyr uploads and
-`bos.config.json` update), call the reload endpoint:
-
-```typescript
-// In the publish handler, after build and deployment succeed:
-const hostUrl = runtimeConfig.hostUrl;
-try {
- await fetch(`${hostUrl}/api/_reload-config`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ config: refreshedRuntimeConfig }),
- });
-} catch {
- // Reload failed — host may not be running or admin auth required
- // This is non-blocking; the host will pick up changes on next restart
-}
-```
-
-**Complexity**: Low (10 lines in the publish handler)
-
-### Phase 7: Browser integration on `/opencode` page
-
-**File**: `ui/src/routes/_layout/opencode.tsx`
-
-Add a "Deploy & Reload" section that describes the workflow:
-
-```tsx
-
- After
- bos publish --deploy
- {" "}
- uploads new builds to Zephyr CDN, the host can reload without restarting.
- Admin users can call{" "}
-
- POST /api/_reload-config
- {" "}
- to swap in the latest config, or it re-fetches from the NEAR blockchain
- automatically.
- >
- }
-/>
-```
-
-Future: a button on the page that triggers deploy via opencode server + reload.
-
-**Complexity**: Low (UI text change)
-
-## Security considerations
-
-| Concern | Mitigation |
-|---------|-----------|
-| Unauthorized reload | Session cookie + admin role check on `POST /api/_reload-config` |
-| Malicious config injection | `RuntimeConfigSchema.parse()` validates the entire structure |
-| Config with wrong URLs | New scope health-check before swap; old scope stays if new fails |
-| Concurrent reloads | Mutex around `reloadWithConfig()` — only one reload at a time |
-| Memory during swap | Old scope disposed immediately after swap; brief 2x memory during transition |
-| Active sessions | Better Auth sessions persist in DB; new scope reads same DB; sessions survive reload |
-| DB connections | Old scope's DB connection closed in dispose; new scope opens its own |
-
-## File change summary
-
-| File | Change | Phase |
-|------|--------|-------|
-| `host/src/program.ts` | Extract `createScopedApp`, add mutable handler, add `reloadWithConfig()` | 1, 2 |
-| `host/src/program.ts` | Two-layer Hono app with `/api/_reload-config` | 3 |
-| `host/src/services/federation.server.ts` | Export `resetFederationInstance()` | 4 |
-| `packages/everything-dev/src/host.ts` | Add `resetState()` + `/api/_reload-config` | 5 |
-| `packages/everything-dev/src/plugin.ts` | Call reload endpoint after publish | 6 |
-| `ui/src/routes/_layout/opencode.tsx` | Add deploy & reload description | 7 |
-| `packages/everything-dev/src/fastkv.ts` | Export `fetchBosConfigFromFastKv` if not already | 3 |
-| `packages/everything-dev/src/config.ts` | Export `buildRuntimeConfig` if not already | 3 |
-
-## What this enables
-
-**Dev workflow (today):**
-
-```
-opencode edits file → HMR at :3003 → see changes instantly
-bos publish --deploy → host restart required → new UI live
-```
-
-**Dev workflow (after this refactor):**
-
-```
-opencode edits file → HMR at :3003 → see changes instantly
-bos publish --deploy → POST /api/_reload-config → new UI live without restart
-```
-
-**Browser-triggered workflow (future):**
-
-```
-/opencode page → prompt opencode server → edit + deploy → reload endpoint → live
-```
-
-**Production workflow (Docker/Railway):**
-
-```
-bos publish --deploy → FastKV updated → POST /api/_reload-config (empty body)
- → host re-fetches from FastKV → rebuilds scope → next request gets new UI
-```
-
-No SSH, no Docker restart, no Railway redeploy. The configuration governance
-stays on the NEAR blockchain where it belongs.
\ No newline at end of file