feat: add code agent sandbox skeleton

This commit is contained in:
Codex
2026-05-20 06:50:25 +00:00
parent e19ea26b0a
commit bbda3e87a9
12 changed files with 869 additions and 5 deletions
@@ -0,0 +1,11 @@
FROM oven/bun:1-alpine
WORKDIR /app/src/components/microservices/code-agent-sandbox
COPY src/components/microservices/code-agent-sandbox/package.json ./package.json
RUN bun install --production
COPY src/components/microservices/code-agent-sandbox/tsconfig.json ./tsconfig.json
COPY src/components/shared /app/src/components/shared
COPY src/components/microservices/code-agent-sandbox/src ./src
EXPOSE 4260
CMD ["bun", "run", "src/index.ts"]
@@ -0,0 +1,15 @@
{
"name": "@unidesk/code-agent-sandbox",
"private": true,
"type": "module",
"scripts": {
"start": "bun run src/index.ts",
"check": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {},
"devDependencies": {
"@types/bun": "latest",
"@types/node": "latest",
"typescript": "latest"
}
}
@@ -0,0 +1,667 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname } from "node:path";
import { createHourlyJsonlWriter, logRetentionBytesForService } from "../../../shared/src/rotating-jsonl";
type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
type JsonRecord = Record<string, JsonValue>;
type SandboxMode = "full-isolation" | "half-isolation" | "bridge";
type AdapterName = "codex" | "opencode" | "future-agent";
type AgentStatus = "idle" | "starting" | "attached" | "prompting" | "steering" | "interrupted" | "resumed" | "forked" | "finished" | "failed";
type TerminalStatus = "completed" | "interrupted" | "failed" | "unknown";
interface RuntimeConfig {
host: string;
port: number;
dataDir: string;
logFile: string;
stateFile: string;
adapterSummaryFile: string;
envFilePath: string;
configTomlPath: string;
authJsonPath: string;
sandboxMode: SandboxMode;
tokenProvider: string;
tokenProviderSwitchAllowed: boolean;
configWritable: boolean;
authWritable: boolean;
envWritable: boolean;
adapterNames: AdapterName[];
defaultAdapter: AdapterName;
maxPromptChars: number;
maxTraceItems: number;
deployServiceId: string;
}
interface AdapterState {
name: AdapterName;
status: AgentStatus;
terminalStatus: TerminalStatus;
startedAt: string | null;
updatedAt: string | null;
threadId: string | null;
turnId: string | null;
promptCount: number;
steerCount: number;
interruptCount: number;
resumeCount: number;
forkCount: number;
lastPrompt: string | null;
lastSteer: string | null;
lastArtifactSummary: JsonRecord | null;
notes: string[];
}
interface SandboxSnapshot {
serviceId: string;
serviceName: string;
mode: SandboxMode;
tokenProvider: string;
tokenProviderSwitchAllowed: boolean;
configWritable: boolean;
authWritable: boolean;
envWritable: boolean;
configTomlText: string;
authJsonText: string;
envVars: Record<string, string>;
adapters: Record<string, AdapterState>;
createdAt: string;
updatedAt: string;
}
const startedAt = new Date().toISOString();
const recentLogs: JsonRecord[] = [];
function envString(name: string, fallback: string): string {
const value = process.env[name];
return value === undefined || value.length === 0 ? fallback : value;
}
function envNumber(name: string, fallback: number): number {
const raw = process.env[name];
if (raw === undefined || raw.trim().length === 0) return fallback;
const parsed = Number(raw);
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
return Math.floor(parsed);
}
function envBoolean(name: string, fallback: boolean): boolean {
const raw = process.env[name];
if (raw === undefined || raw.trim().length === 0) return fallback;
return ["1", "true", "yes", "on"].includes(raw.trim().toLowerCase());
}
function parseSandboxMode(value: string): SandboxMode {
const normalized = value.trim().toLowerCase();
if (normalized === "full-isolation" || normalized === "full" || normalized === "isolated") return "full-isolation";
if (normalized === "half-isolation" || normalized === "half" || normalized === "semi-isolated") return "half-isolation";
if (normalized === "bridge" || normalized === "bridged") return "bridge";
throw new Error(`unsupported CODE_AGENT_SANDBOX_MODE: ${value}`);
}
function parseAdapterNames(value: string): AdapterName[] {
const names = value.split(",").map((item) => item.trim().toLowerCase()).filter(Boolean);
const allowed: AdapterName[] = [];
for (const name of names) {
if (name === "codex" || name === "opencode" || name === "future-agent") {
allowed.push(name);
continue;
}
throw new Error(`unsupported adapter name: ${name}`);
}
return Array.from(new Set(allowed));
}
function buildConfig(): RuntimeConfig {
const port = envNumber("PORT", 4260);
const adapterNames = parseAdapterNames(envString("CODE_AGENT_SANDBOX_ADAPTERS", "codex,opencode,future-agent"));
const defaultAdapter = envString("CODE_AGENT_SANDBOX_DEFAULT_ADAPTER", "codex").trim().toLowerCase() as AdapterName;
if (!adapterNames.includes(defaultAdapter)) {
throw new Error(`CODE_AGENT_SANDBOX_DEFAULT_ADAPTER must be one of: ${adapterNames.join(", ")}`);
}
return {
host: envString("HOST", "0.0.0.0"),
port,
dataDir: envString("CODE_AGENT_SANDBOX_DATA_DIR", "/var/lib/unidesk/code-agent-sandbox"),
logFile: envString("LOG_FILE", "/var/log/unidesk/code-agent-sandbox.jsonl"),
stateFile: envString("CODE_AGENT_SANDBOX_STATE_FILE", "/var/lib/unidesk/code-agent-sandbox/state.json"),
adapterSummaryFile: envString("CODE_AGENT_SANDBOX_ADAPTER_SUMMARY_FILE", "/var/lib/unidesk/code-agent-sandbox/adapters.json"),
envFilePath: envString("CODE_AGENT_SANDBOX_ENV_FILE_PATH", "/var/lib/unidesk/code-agent-sandbox/env.json"),
configTomlPath: envString("CODE_AGENT_SANDBOX_CONFIG_TOML_PATH", "/var/lib/unidesk/code-agent-sandbox/config.toml"),
authJsonPath: envString("CODE_AGENT_SANDBOX_AUTH_JSON_PATH", "/var/lib/unidesk/code-agent-sandbox/auth.json"),
sandboxMode: parseSandboxMode(envString("CODE_AGENT_SANDBOX_MODE", "half-isolation")),
tokenProvider: envString("CODE_AGENT_SANDBOX_TOKEN_PROVIDER", "codex"),
tokenProviderSwitchAllowed: envBoolean("CODE_AGENT_SANDBOX_TOKEN_PROVIDER_SWITCH_ALLOWED", true),
configWritable: envBoolean("CODE_AGENT_SANDBOX_CONFIG_WRITABLE", true),
authWritable: envBoolean("CODE_AGENT_SANDBOX_AUTH_WRITABLE", true),
envWritable: envBoolean("CODE_AGENT_SANDBOX_ENV_WRITABLE", true),
adapterNames,
defaultAdapter,
maxPromptChars: envNumber("CODE_AGENT_SANDBOX_MAX_PROMPT_CHARS", 12_000),
maxTraceItems: envNumber("CODE_AGENT_SANDBOX_MAX_TRACE_ITEMS", 200),
deployServiceId: envString("UNIDESK_DEPLOY_SERVICE_ID", "code-agent-sandbox"),
};
}
const config = buildConfig();
let logWriter: ReturnType<typeof createHourlyJsonlWriter> | null = null;
function getLogWriter(): ReturnType<typeof createHourlyJsonlWriter> {
if (logWriter === null) {
mkdirSync(config.dataDir, { recursive: true });
logWriter = createHourlyJsonlWriter({
baseLogFile: config.logFile,
service: "code-agent-sandbox",
maxBytes: logRetentionBytesForService("code-agent-sandbox"),
});
logWriter.prune();
}
return logWriter;
}
function log(event: string, detail: JsonRecord = {}): void {
const record: JsonRecord = { at: new Date().toISOString(), service: "code-agent-sandbox", event, ...detail };
recentLogs.push(record);
while (recentLogs.length > 500) recentLogs.shift();
try {
getLogWriter().appendJson(record, new Date(String(record.at)));
} catch {
// Logging must not break the sandbox control plane.
}
console.log(JSON.stringify(record));
}
function jsonResponse(body: unknown, status = 200, headers: Record<string, string> = {}): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "content-type": "application/json; charset=utf-8", ...headers },
});
}
function errorToJson(error: unknown): JsonRecord {
if (error instanceof Error) return { name: error.name, message: error.message, stack: error.stack ?? "" };
return { message: String(error) };
}
function errorResponse(error: unknown, status = 500): Response {
log("request_failed", { status, error: errorToJson(error) });
return jsonResponse({ ok: false, error: error instanceof Error ? error.message : String(error) }, status);
}
function clampText(value: string, maxChars: number): string {
const compact = value.replace(/\s+/gu, " ").trim();
return compact.length <= maxChars ? compact : `${compact.slice(0, Math.max(0, maxChars - 12))}…<truncated>`;
}
function nowIso(): string {
return new Date().toISOString();
}
function readTextOrEmpty(path: string): string {
if (!existsSync(path)) return "";
try {
return readFileSync(path, "utf8");
} catch {
return "";
}
}
function readJsonObjectOrEmpty(path: string): Record<string, string> {
const raw = readTextOrEmpty(path).trim();
if (raw.length === 0) return {};
try {
const parsed = JSON.parse(raw) as unknown;
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
return Object.fromEntries(Object.entries(parsed).flatMap(([key, value]) => typeof value === "string" ? [[key, value]] : []));
} catch {
return {};
}
}
function loadState(): SandboxSnapshot {
try {
if (!existsSync(config.stateFile)) throw new Error("state file missing");
const parsed = JSON.parse(readFileSync(config.stateFile, "utf8")) as unknown;
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("state file is not an object");
const record = parsed as Record<string, unknown>;
const adapters = typeof record.adapters === "object" && record.adapters !== null && !Array.isArray(record.adapters)
? record.adapters as Record<string, unknown>
: {};
const fresh = freshSnapshot();
return {
serviceId: String(record.serviceId ?? fresh.serviceId),
serviceName: String(record.serviceName ?? fresh.serviceName),
mode: parseSandboxMode(String(record.mode ?? fresh.mode)),
tokenProvider: String(record.tokenProvider ?? fresh.tokenProvider),
tokenProviderSwitchAllowed: typeof record.tokenProviderSwitchAllowed === "boolean"
? record.tokenProviderSwitchAllowed
: fresh.tokenProviderSwitchAllowed,
configWritable: typeof record.configWritable === "boolean" ? record.configWritable : fresh.configWritable,
authWritable: typeof record.authWritable === "boolean" ? record.authWritable : fresh.authWritable,
envWritable: typeof record.envWritable === "boolean" ? record.envWritable : fresh.envWritable,
configTomlText: typeof record.configTomlText === "string" ? record.configTomlText : readTextOrEmpty(config.configTomlPath),
authJsonText: typeof record.authJsonText === "string" ? record.authJsonText : readTextOrEmpty(config.authJsonPath),
envVars: normalizeEnvVars(record.envVars, readJsonObjectOrEmpty(config.envFilePath)),
adapters: Object.fromEntries(config.adapterNames.map((name) => [name, normalizeAdapterState(name, adapters[name] ?? fresh.adapters[name])])),
createdAt: String(record.createdAt ?? fresh.createdAt),
updatedAt: String(record.updatedAt ?? fresh.updatedAt),
};
} catch {
return freshSnapshot();
}
}
function normalizeAdapterState(name: string, value: unknown): AdapterState {
const record = typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
return {
name: name as AdapterName,
status: (record.status as AgentStatus) ?? "idle",
terminalStatus: (record.terminalStatus as TerminalStatus) ?? "unknown",
startedAt: typeof record.startedAt === "string" ? record.startedAt : null,
updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : null,
threadId: typeof record.threadId === "string" ? record.threadId : null,
turnId: typeof record.turnId === "string" ? record.turnId : null,
promptCount: Number(record.promptCount ?? 0) || 0,
steerCount: Number(record.steerCount ?? 0) || 0,
interruptCount: Number(record.interruptCount ?? 0) || 0,
resumeCount: Number(record.resumeCount ?? 0) || 0,
forkCount: Number(record.forkCount ?? 0) || 0,
lastPrompt: typeof record.lastPrompt === "string" ? record.lastPrompt : null,
lastSteer: typeof record.lastSteer === "string" ? record.lastSteer : null,
lastArtifactSummary: typeof record.lastArtifactSummary === "object" && record.lastArtifactSummary !== null && !Array.isArray(record.lastArtifactSummary)
? record.lastArtifactSummary as JsonRecord
: null,
notes: Array.isArray(record.notes) ? record.notes.filter((item): item is string => typeof item === "string") : [],
};
}
function freshSnapshot(): SandboxSnapshot {
const adapters = Object.fromEntries(config.adapterNames.map((name) => [name, freshAdapterState(name)]));
return {
serviceId: config.deployServiceId || "code-agent-sandbox",
serviceName: "Code Agent Sandbox",
mode: config.sandboxMode,
tokenProvider: config.tokenProvider,
tokenProviderSwitchAllowed: config.tokenProviderSwitchAllowed,
configWritable: config.configWritable,
authWritable: config.authWritable,
envWritable: config.envWritable,
configTomlText: "",
authJsonText: "{}",
envVars: {},
adapters,
createdAt: startedAt,
updatedAt: startedAt,
};
}
function freshAdapterState(name: AdapterName): AdapterState {
return {
name,
status: "idle",
terminalStatus: "unknown",
startedAt: null,
updatedAt: null,
threadId: null,
turnId: null,
promptCount: 0,
steerCount: 0,
interruptCount: 0,
resumeCount: 0,
forkCount: 0,
lastPrompt: null,
lastSteer: null,
lastArtifactSummary: null,
notes: [],
};
}
let state = freshSnapshot();
function persistState(): void {
state.updatedAt = nowIso();
mkdirSync(dirname(config.stateFile), { recursive: true });
mkdirSync(dirname(config.adapterSummaryFile), { recursive: true });
writeFileSync(config.stateFile, `${JSON.stringify(state, null, 2)}\n`, "utf8");
writeFileSync(config.adapterSummaryFile, `${JSON.stringify({ updatedAt: state.updatedAt, adapters: state.adapters }, null, 2)}\n`, "utf8");
if (state.mode !== "bridge" && state.configWritable) {
mkdirSync(dirname(config.configTomlPath), { recursive: true });
writeFileSync(config.configTomlPath, state.configTomlText, "utf8");
}
if (state.mode !== "bridge" && state.authWritable) {
mkdirSync(dirname(config.authJsonPath), { recursive: true });
writeFileSync(config.authJsonPath, `${state.authJsonText}\n`, "utf8");
}
if (state.mode !== "bridge" && state.envWritable) {
mkdirSync(dirname(config.envFilePath), { recursive: true });
writeFileSync(config.envFilePath, `${JSON.stringify(state.envVars, null, 2)}\n`, "utf8");
}
}
function selectAdapter(name: string | null | undefined): AdapterState {
const chosen = (name ?? config.defaultAdapter) as AdapterName;
if (!config.adapterNames.includes(chosen)) throw new Error(`adapter not enabled: ${chosen}`);
const adapter = state.adapters[chosen];
if (adapter === undefined) throw new Error(`adapter state missing: ${chosen}`);
return adapter;
}
function adapterSummary(): JsonRecord {
return {
ok: true,
serviceId: state.serviceId,
mode: state.mode,
tokenProvider: state.tokenProvider,
tokenProviderSwitchAllowed: state.tokenProviderSwitchAllowed,
configWritable: state.configWritable,
authWritable: state.authWritable,
envWritable: state.envWritable,
adapterNames: config.adapterNames,
defaultAdapter: config.defaultAdapter,
adapters: Object.values(state.adapters).map((adapter) => ({
name: adapter.name,
status: adapter.status,
terminalStatus: adapter.terminalStatus,
threadId: adapter.threadId,
turnId: adapter.turnId,
promptCount: adapter.promptCount,
steerCount: adapter.steerCount,
interruptCount: adapter.interruptCount,
resumeCount: adapter.resumeCount,
forkCount: adapter.forkCount,
lastPromptPreview: adapter.lastPrompt ? clampText(adapter.lastPrompt, 240) : null,
lastSteerPreview: adapter.lastSteer ? clampText(adapter.lastSteer, 240) : null,
notes: adapter.notes.slice(-5),
lastArtifactSummary: adapter.lastArtifactSummary,
})),
};
}
function serviceHealth(): JsonRecord {
return {
ok: true,
status: "healthy",
serviceId: state.serviceId,
serviceName: state.serviceName,
mode: state.mode,
startedAt,
updatedAt: state.updatedAt,
tokenProvider: state.tokenProvider,
tokenProviderSwitchAllowed: state.tokenProviderSwitchAllowed,
credentialBoundary: {
fullIsolation: {
configTomlWritable: true,
authJsonWritable: true,
envWritable: true,
tokenProviderSwitchAllowed: true,
},
halfIsolation: {
configTomlWritable: state.configWritable,
authJsonWritable: state.authWritable,
envWritable: state.envWritable,
tokenProviderSwitchAllowed: state.tokenProviderSwitchAllowed,
},
bridge: {
configTomlWritable: false,
authJsonWritable: false,
envWritable: false,
tokenProviderSwitchAllowed: false,
},
},
};
}
function diagnostics(): JsonRecord {
return {
ok: true,
serviceId: state.serviceId,
mode: state.mode,
adapterNames: config.adapterNames,
defaultAdapter: config.defaultAdapter,
credentialBoundary: {
configTomlPath: config.configTomlPath,
authJsonPath: config.authJsonPath,
configTomlWritable: state.mode === "bridge" ? false : state.configWritable,
authJsonWritable: state.mode === "bridge" ? false : state.authWritable,
envWritable: state.mode === "bridge" ? false : state.envWritable,
tokenProvider: state.tokenProvider,
tokenProviderSwitchAllowed: state.mode === "bridge" ? false : state.tokenProviderSwitchAllowed,
},
recoveryBoundary: {
statusRestoreSupported: true,
queuedTaskRerouteSupported: true,
interruptedAttemptRetrySupported: true,
liveMigrationSupported: false,
resultLossTolerance: "none",
},
adapterSummary: adapterSummary().adapters,
notes: [
"Code Queue is not a dependency of this sandbox service.",
"Adapters are contract-first stubs; integration is delegated to follow-up tasks.",
],
};
}
function trace(): JsonRecord {
return {
ok: true,
serviceId: state.serviceId,
generatedAt: nowIso(),
limit: config.maxTraceItems,
terminalStatus: Object.values(state.adapters).reduce<Record<string, TerminalStatus>>((acc, adapter) => {
acc[adapter.name] = adapter.terminalStatus;
return acc;
}, {}),
adapters: Object.values(state.adapters).slice(0, config.maxTraceItems).map((adapter) => ({
name: adapter.name,
status: adapter.status,
terminalStatus: adapter.terminalStatus,
startedAt: adapter.startedAt,
updatedAt: adapter.updatedAt,
threadId: adapter.threadId,
turnId: adapter.turnId,
promptCount: adapter.promptCount,
steerCount: adapter.steerCount,
interruptCount: adapter.interruptCount,
resumeCount: adapter.resumeCount,
forkCount: adapter.forkCount,
lastPromptPreview: adapter.lastPrompt ? clampText(adapter.lastPrompt, 120) : null,
lastSteerPreview: adapter.lastSteer ? clampText(adapter.lastSteer, 120) : null,
lastArtifactSummary: adapter.lastArtifactSummary,
notes: adapter.notes.slice(-10),
})),
};
}
function recordAdapterMutation(adapter: AdapterState, status: AgentStatus, note: string): void {
adapter.status = status;
adapter.updatedAt = nowIso();
adapter.notes.push(note);
if (adapter.notes.length > 50) adapter.notes.splice(0, adapter.notes.length - 50);
persistState();
}
function parseBody(body: unknown): Record<string, unknown> {
if (typeof body === "object" && body !== null && !Array.isArray(body)) return body as Record<string, unknown>;
return {};
}
function normalizeEnvVars(value: unknown, fallback: Record<string, string>): Record<string, string> {
if (typeof value !== "object" || value === null || Array.isArray(value)) return { ...fallback };
const result: Record<string, string> = {};
for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
if (typeof raw === "string") result[key] = raw;
}
return result;
}
function adapterAction(action: string, body: Record<string, unknown>): JsonRecord {
const adapter = selectAdapter(typeof body.adapter === "string" ? body.adapter : null);
const mode: SandboxMode = state.mode;
const prompt = typeof body.prompt === "string" ? body.prompt : null;
const steerPrompt = typeof body.steerPrompt === "string" ? body.steerPrompt : null;
const artifact = typeof body.artifactSummary === "object" && body.artifactSummary !== null && !Array.isArray(body.artifactSummary)
? body.artifactSummary as JsonRecord
: null;
switch (action) {
case "start":
adapter.startedAt = nowIso();
adapter.threadId = adapter.threadId ?? `sandbox-${adapter.name}-${Date.now()}`;
adapter.turnId = null;
adapter.promptCount += 1;
adapter.lastPrompt = prompt;
recordAdapterMutation(adapter, "starting", `start:${adapter.threadId}`);
return { ok: true, action, adapter: adapter.name, threadId: adapter.threadId, terminalStatus: adapter.terminalStatus };
case "attach":
adapter.threadId = adapter.threadId ?? `attached-${adapter.name}-${Date.now()}`;
adapter.turnId = adapter.turnId ?? `turn-${Date.now()}`;
recordAdapterMutation(adapter, "attached", `attach:${adapter.threadId}`);
return { ok: true, action, adapter: adapter.name, threadId: adapter.threadId, turnId: adapter.turnId };
case "prompt":
adapter.promptCount += 1;
adapter.lastPrompt = prompt;
adapter.turnId = adapter.turnId ?? `turn-${Date.now()}`;
recordAdapterMutation(adapter, "prompting", `prompt:${clampText(prompt ?? "", 180)}`);
return { ok: true, action, adapter: adapter.name, turnId: adapter.turnId, promptPreview: clampText(prompt ?? "", config.maxPromptChars) };
case "steer":
adapter.steerCount += 1;
adapter.lastSteer = steerPrompt;
recordAdapterMutation(adapter, "steering", `steer:${clampText(steerPrompt ?? "", 180)}`);
return { ok: true, action, adapter: adapter.name, turnId: adapter.turnId, steerPreview: clampText(steerPrompt ?? "", config.maxPromptChars) };
case "interrupt":
adapter.interruptCount += 1;
recordAdapterMutation(adapter, "interrupted", "interrupt");
adapter.terminalStatus = "interrupted";
persistState();
return { ok: true, action, adapter: adapter.name, terminalStatus: adapter.terminalStatus };
case "resume":
adapter.resumeCount += 1;
recordAdapterMutation(adapter, "resumed", "resume");
adapter.terminalStatus = "unknown";
persistState();
return { ok: true, action, adapter: adapter.name, terminalStatus: adapter.terminalStatus };
case "fork":
adapter.forkCount += 1;
recordAdapterMutation(adapter, "forked", "fork");
return { ok: true, action, adapter: adapter.name, forkCount: adapter.forkCount };
case "terminal-status":
return {
ok: true,
action,
adapter: adapter.name,
terminalStatus: adapter.terminalStatus,
status: adapter.status,
};
case "artifact-summary":
adapter.lastArtifactSummary = artifact ?? {
summary: "placeholder",
source: "code-agent-sandbox",
generatedAt: nowIso(),
};
recordAdapterMutation(adapter, "finished", "artifact-summary");
return { ok: true, action, adapter: adapter.name, artifactSummary: adapter.lastArtifactSummary };
case "config-toml":
if (mode === "bridge") throw new Error("bridge mode is read-only for config.toml");
if (!state.configWritable) throw new Error("config.toml writes are disabled");
if (typeof body.text === "string") state.configTomlText = body.text;
persistState();
return { ok: true, action, path: config.configTomlPath, writable: state.configWritable, chars: state.configTomlText.length };
case "auth-json":
if (mode === "bridge") throw new Error("bridge mode is read-only for auth.json");
if (!state.authWritable) throw new Error("auth.json writes are disabled");
if (typeof body.text === "string") state.authJsonText = body.text;
persistState();
return { ok: true, action, path: config.authJsonPath, writable: state.authWritable, chars: state.authJsonText.length };
case "env":
if (mode === "bridge") throw new Error("bridge mode is read-only for env files");
if (!state.envWritable) throw new Error("env writes are disabled");
state.envVars = normalizeEnvVars(body.env, state.envVars);
persistState();
return { ok: true, action, path: config.envFilePath, writable: state.envWritable, keys: Object.keys(state.envVars).sort() };
case "token-provider":
if (mode === "bridge") throw new Error("bridge mode is read-only for token provider switching");
if (!state.tokenProviderSwitchAllowed) throw new Error("token provider switching is disabled");
state.tokenProvider = typeof body.provider === "string" && body.provider.length > 0 ? body.provider : state.tokenProvider;
persistState();
return { ok: true, action, tokenProvider: state.tokenProvider };
case "trace":
return trace();
default:
throw new Error(`unsupported adapter action: ${action}`);
}
}
async function readJson(req: Request): Promise<Record<string, unknown>> {
const text = await req.text();
if (text.trim().length === 0) return {};
const parsed = JSON.parse(text) as unknown;
return parseBody(parsed);
}
async function route(req: Request): Promise<Response> {
const url = new URL(req.url);
if (req.method === "GET" && (url.pathname === "/" || url.pathname === "/health")) return jsonResponse(serviceHealth());
if (req.method === "GET" && url.pathname === "/diagnostics") return jsonResponse(diagnostics());
if (req.method === "GET" && url.pathname === "/trace") return jsonResponse(trace());
if (req.method === "GET" && url.pathname === "/artifacts") return jsonResponse({ ok: true, serviceId: state.serviceId, summary: adapterSummary() });
if (req.method === "GET" && url.pathname === "/api/config/toml") return jsonResponse({ ok: true, path: config.configTomlPath, writable: state.mode !== "bridge" && state.configWritable, text: state.configTomlText, chars: state.configTomlText.length });
if (req.method === "GET" && url.pathname === "/api/auth/json") return jsonResponse({ ok: true, path: config.authJsonPath, writable: state.mode !== "bridge" && state.authWritable, text: state.authJsonText, chars: state.authJsonText.length });
if (req.method === "GET" && url.pathname === "/api/env") return jsonResponse({ ok: true, path: config.envFilePath, writable: state.mode !== "bridge" && state.envWritable, env: state.envVars, keys: Object.keys(state.envVars).sort() });
if (req.method === "GET" && url.pathname === "/api/token-provider") return jsonResponse({ ok: true, tokenProvider: state.tokenProvider, switchAllowed: state.mode !== "bridge" && state.tokenProviderSwitchAllowed });
if (req.method === "GET" && url.pathname === "/logs") {
return jsonResponse({ ok: true, logs: recentLogs.slice(-200), logFile: config.logFile, currentLogFile: getLogWriter().currentPath(), startedAt });
}
if (req.method === "GET" && url.pathname === "/status") return jsonResponse({ ok: true, serviceId: state.serviceId, state, config: { mode: state.mode, adapterNames: config.adapterNames, defaultAdapter: config.defaultAdapter } });
if (req.method === "POST" && url.pathname.startsWith("/api/adapters/")) {
const rest = url.pathname.slice("/api/adapters/".length);
const [adapterName, action] = rest.split("/", 2);
if (!adapterName || !action) return jsonResponse({ ok: false, error: "adapter name and action are required" }, 400);
const body = await readJson(req);
if (typeof body.adapter !== "string") body.adapter = adapterName;
const result = adapterAction(action, body);
log("adapter_action", { adapterName, action, bodyPreview: clampText(JSON.stringify(body), 600) });
return jsonResponse(result);
}
if (req.method === "POST" && url.pathname === "/api/state/reload") {
state = loadState();
persistState();
return jsonResponse({ ok: true, state });
}
if (req.method === "POST" && url.pathname === "/api/state/seed") {
const body = await readJson(req);
const nextMode = typeof body.mode === "string" ? parseSandboxMode(body.mode) : state.mode;
state.mode = nextMode;
state.tokenProvider = typeof body.tokenProvider === "string" ? body.tokenProvider : state.tokenProvider;
state.tokenProviderSwitchAllowed = typeof body.tokenProviderSwitchAllowed === "boolean" ? body.tokenProviderSwitchAllowed : state.tokenProviderSwitchAllowed;
state.configWritable = typeof body.configWritable === "boolean" ? body.configWritable : state.configWritable;
state.authWritable = typeof body.authWritable === "boolean" ? body.authWritable : state.authWritable;
state.envWritable = typeof body.envWritable === "boolean" ? body.envWritable : state.envWritable;
if (typeof body.configTomlText === "string") state.configTomlText = body.configTomlText;
if (typeof body.authJsonText === "string") state.authJsonText = body.authJsonText;
if (typeof body.env === "object" && body.env !== null && !Array.isArray(body.env)) state.envVars = normalizeEnvVars(body.env, state.envVars);
persistState();
return jsonResponse({ ok: true, state });
}
if (req.method === "GET" && url.pathname === "/api/adapter-summary") return jsonResponse({ ok: true, summary: adapterSummary() });
return jsonResponse({
ok: false,
error: "not found",
serviceId: state.serviceId,
available: ["/health", "/diagnostics", "/trace", "/logs", "/status", "/api/config/toml", "/api/auth/json", "/api/env", "/api/token-provider", "/api/adapters/:adapter/:action", "/api/state/reload", "/api/state/seed"],
}, 404);
}
if (import.meta.main) {
state = loadState();
persistState();
log("service_started", { startedAt, serviceId: state.serviceId, mode: state.mode, adapterNames: config.adapterNames });
Bun.serve({
hostname: config.host,
port: config.port,
idleTimeout: 120,
fetch(req) {
return route(req).catch((error) => errorResponse(error));
},
});
}
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"composite": true,
"rootDir": "../../",
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["bun", "node"],
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "dist",
"skipLibCheck": true
},
"include": ["src/**/*.ts", "../../shared/src/**/*.ts"]
}