mirror of
https://github.com/different-ai/openwork
synced 2026-05-12 18:16:24 +02:00
Merge pull request #440 from different-ai/task/headless-otel-logs
Improve openwrk observability and request logging
This commit is contained in:
@@ -40,6 +40,20 @@ pnpm --filter openwrk dev -- \
|
||||
|
||||
The command prints pairing details (OpenWork server URL + token, OpenCode URL + auth) so remote OpenWork clients can connect.
|
||||
|
||||
## Logging
|
||||
|
||||
`openwrk` emits a unified log stream from OpenCode, OpenWork server, and Owpenbot. Use JSON format for
|
||||
structured, OpenTelemetry-friendly logs and a stable run id for correlation.
|
||||
|
||||
```bash
|
||||
OPENWRK_LOG_FORMAT=json openwrk start --workspace /path/to/workspace
|
||||
```
|
||||
|
||||
Use `--run-id` or `OPENWRK_RUN_ID` to supply your own correlation id.
|
||||
|
||||
OpenWork server logs every request with method, path, status, and duration. Disable this when running
|
||||
`openwork-server` directly by setting `OPENWORK_LOG_REQUESTS=0` or passing `--no-log-requests`.
|
||||
|
||||
## Router daemon (multi-workspace)
|
||||
|
||||
The router keeps a single OpenCode process alive and switches workspaces JIT using the `directory` parameter.
|
||||
|
||||
@@ -15,6 +15,30 @@ import { createOpencodeClient } from "@opencode-ai/sdk/v2/client";
|
||||
|
||||
type ApprovalMode = "manual" | "auto";
|
||||
|
||||
type LogFormat = "pretty" | "json";
|
||||
|
||||
type LogLevel = "debug" | "info" | "warn" | "error";
|
||||
|
||||
type LogAttributes = Record<string, unknown>;
|
||||
|
||||
type LoggerChild = {
|
||||
log: (level: LogLevel, message: string, attributes?: LogAttributes) => void;
|
||||
debug: (message: string, attributes?: LogAttributes) => void;
|
||||
info: (message: string, attributes?: LogAttributes) => void;
|
||||
warn: (message: string, attributes?: LogAttributes) => void;
|
||||
error: (message: string, attributes?: LogAttributes) => void;
|
||||
};
|
||||
|
||||
type Logger = {
|
||||
format: LogFormat;
|
||||
log: (level: LogLevel, message: string, attributes?: LogAttributes, component?: string) => void;
|
||||
debug: (message: string, attributes?: LogAttributes, component?: string) => void;
|
||||
info: (message: string, attributes?: LogAttributes, component?: string) => void;
|
||||
warn: (message: string, attributes?: LogAttributes, component?: string) => void;
|
||||
error: (message: string, attributes?: LogAttributes, component?: string) => void;
|
||||
child: (component: string, attributes?: LogAttributes) => LoggerChild;
|
||||
};
|
||||
|
||||
const FALLBACK_VERSION = "0.1.0";
|
||||
const DEFAULT_OPENWORK_PORT = 8787;
|
||||
const DEFAULT_OWPENBOT_HEALTH_PORT = 3005;
|
||||
@@ -300,6 +324,20 @@ function readBinarySource(
|
||||
throw new Error(`Invalid ${key} value: ${raw}. Use auto|bundled|downloaded|external.`);
|
||||
}
|
||||
|
||||
function readLogFormat(
|
||||
flags: Map<string, string | boolean>,
|
||||
key: string,
|
||||
fallback: LogFormat,
|
||||
envKey?: string,
|
||||
): LogFormat {
|
||||
const raw = readFlag(flags, key) ?? (envKey ? process.env[envKey] : undefined);
|
||||
if (!raw) return fallback;
|
||||
const normalized = String(raw).trim().toLowerCase();
|
||||
if (normalized === "json") return "json";
|
||||
if (normalized === "pretty" || normalized === "text" || normalized === "human") return "pretty";
|
||||
throw new Error(`Invalid ${key} value: ${raw}. Use pretty|json.`);
|
||||
}
|
||||
|
||||
async function fileExists(path: string): Promise<boolean> {
|
||||
try {
|
||||
await stat(path);
|
||||
@@ -466,6 +504,8 @@ function prefixStream(
|
||||
stream: NodeJS.ReadableStream | null,
|
||||
label: string,
|
||||
level: "stdout" | "stderr",
|
||||
logger: Logger,
|
||||
pid?: number,
|
||||
): void {
|
||||
if (!stream) return;
|
||||
stream.setEncoding("utf8");
|
||||
@@ -476,22 +516,22 @@ function prefixStream(
|
||||
buffer = lines.pop() ?? "";
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
const message = `[${label}] ${line}`;
|
||||
if (level === "stderr") {
|
||||
console.error(message);
|
||||
} else {
|
||||
console.log(message);
|
||||
if (logger.format === "json" && looksLikeOtelLogLine(line)) {
|
||||
process.stdout.write(`${line}\n`);
|
||||
continue;
|
||||
}
|
||||
const severity: LogLevel = level === "stderr" ? "error" : "info";
|
||||
logger.log(severity, line, { stream: level, pid }, label);
|
||||
}
|
||||
});
|
||||
stream.on("end", () => {
|
||||
if (!buffer.trim()) return;
|
||||
const message = `[${label}] ${buffer}`;
|
||||
if (level === "stderr") {
|
||||
console.error(message);
|
||||
} else {
|
||||
console.log(message);
|
||||
if (logger.format === "json" && looksLikeOtelLogLine(buffer)) {
|
||||
process.stdout.write(`${buffer}\n`);
|
||||
return;
|
||||
}
|
||||
const severity: LogLevel = level === "stderr" ? "error" : "info";
|
||||
logger.log(severity, buffer, { stream: level, pid }, label);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1389,6 +1429,8 @@ function printHelp(): void {
|
||||
" --check-events Verify SSE events during check",
|
||||
" --json Output JSON when applicable",
|
||||
" --verbose Print additional diagnostics",
|
||||
" --log-format <format> Log output format: pretty | json",
|
||||
" --run-id <id> Correlation id for logs (default: random UUID)",
|
||||
" --help Show help",
|
||||
" --version Show version",
|
||||
].join("\n");
|
||||
@@ -1426,6 +1468,9 @@ async function startOpencode(options: {
|
||||
username?: string;
|
||||
password?: string;
|
||||
corsOrigins: string[];
|
||||
logger: Logger;
|
||||
runId: string;
|
||||
logFormat: LogFormat;
|
||||
}) {
|
||||
const args = ["serve", "--hostname", options.bindHost, "--port", String(options.port)];
|
||||
for (const origin of options.corsOrigins) {
|
||||
@@ -1439,13 +1484,22 @@ async function startOpencode(options: {
|
||||
...process.env,
|
||||
OPENCODE_CLIENT: "openwrk",
|
||||
OPENWORK: "1",
|
||||
OPENWRK_RUN_ID: options.runId,
|
||||
OPENWRK_LOG_FORMAT: options.logFormat,
|
||||
OTEL_RESOURCE_ATTRIBUTES: mergeResourceAttributes(
|
||||
{
|
||||
"service.name": "opencode",
|
||||
"service.instance.id": options.runId,
|
||||
},
|
||||
process.env.OTEL_RESOURCE_ATTRIBUTES,
|
||||
),
|
||||
...(options.username ? { OPENCODE_SERVER_USERNAME: options.username } : {}),
|
||||
...(options.password ? { OPENCODE_SERVER_PASSWORD: options.password } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
prefixStream(child.stdout, "opencode", "stdout");
|
||||
prefixStream(child.stderr, "opencode", "stderr");
|
||||
prefixStream(child.stdout, "opencode", "stdout", options.logger, child.pid ?? undefined);
|
||||
prefixStream(child.stderr, "opencode", "stderr", options.logger, child.pid ?? undefined);
|
||||
|
||||
return child;
|
||||
}
|
||||
@@ -1466,6 +1520,9 @@ async function startOpenworkServer(options: {
|
||||
opencodeUsername?: string;
|
||||
opencodePassword?: string;
|
||||
owpenbotHealthPort?: number;
|
||||
logger: Logger;
|
||||
runId: string;
|
||||
logFormat: LogFormat;
|
||||
}) {
|
||||
const args = [
|
||||
"--host",
|
||||
@@ -1504,6 +1561,9 @@ async function startOpenworkServer(options: {
|
||||
if (options.opencodePassword) {
|
||||
args.push("--opencode-password", options.opencodePassword);
|
||||
}
|
||||
if (options.logFormat) {
|
||||
args.push("--log-format", options.logFormat);
|
||||
}
|
||||
|
||||
const resolved = resolveBinCommand(options.bin);
|
||||
const child = spawn(resolved.command, [...resolved.prefixArgs, ...args], {
|
||||
@@ -1513,6 +1573,16 @@ async function startOpenworkServer(options: {
|
||||
...process.env,
|
||||
OPENWORK_TOKEN: options.token,
|
||||
OPENWORK_HOST_TOKEN: options.hostToken,
|
||||
OPENWRK_RUN_ID: options.runId,
|
||||
OPENWORK_RUN_ID: options.runId,
|
||||
OPENWORK_LOG_FORMAT: options.logFormat,
|
||||
OTEL_RESOURCE_ATTRIBUTES: mergeResourceAttributes(
|
||||
{
|
||||
"service.name": "openwork-server",
|
||||
"service.instance.id": options.runId,
|
||||
},
|
||||
process.env.OTEL_RESOURCE_ATTRIBUTES,
|
||||
),
|
||||
...(options.owpenbotHealthPort ? { OWPENBOT_HEALTH_PORT: String(options.owpenbotHealthPort) } : {}),
|
||||
...(options.opencodeBaseUrl ? { OPENWORK_OPENCODE_BASE_URL: options.opencodeBaseUrl } : {}),
|
||||
...(options.opencodeDirectory ? { OPENWORK_OPENCODE_DIRECTORY: options.opencodeDirectory } : {}),
|
||||
@@ -1521,8 +1591,8 @@ async function startOpenworkServer(options: {
|
||||
},
|
||||
});
|
||||
|
||||
prefixStream(child.stdout, "openwork-server", "stdout");
|
||||
prefixStream(child.stderr, "openwork-server", "stderr");
|
||||
prefixStream(child.stdout, "openwork-server", "stdout", options.logger, child.pid ?? undefined);
|
||||
prefixStream(child.stderr, "openwork-server", "stderr", options.logger, child.pid ?? undefined);
|
||||
|
||||
return child;
|
||||
}
|
||||
@@ -1534,6 +1604,9 @@ async function startOwpenbot(options: {
|
||||
opencodeUsername?: string;
|
||||
opencodePassword?: string;
|
||||
owpenbotHealthPort?: number;
|
||||
logger: Logger;
|
||||
runId: string;
|
||||
logFormat: LogFormat;
|
||||
}) {
|
||||
const args = ["start", options.workspace];
|
||||
if (options.opencodeUrl) {
|
||||
@@ -1549,6 +1622,15 @@ async function startOwpenbot(options: {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
env: {
|
||||
...process.env,
|
||||
OPENWRK_RUN_ID: options.runId,
|
||||
OPENWRK_LOG_FORMAT: options.logFormat,
|
||||
OTEL_RESOURCE_ATTRIBUTES: mergeResourceAttributes(
|
||||
{
|
||||
"service.name": "owpenbot",
|
||||
"service.instance.id": options.runId,
|
||||
},
|
||||
process.env.OTEL_RESOURCE_ATTRIBUTES,
|
||||
),
|
||||
...(options.opencodeUrl ? { OPENCODE_URL: options.opencodeUrl } : {}),
|
||||
OPENCODE_DIRECTORY: options.workspace,
|
||||
...(options.owpenbotHealthPort ? { OWPENBOT_HEALTH_PORT: String(options.owpenbotHealthPort) } : {}),
|
||||
@@ -1557,8 +1639,8 @@ async function startOwpenbot(options: {
|
||||
},
|
||||
});
|
||||
|
||||
prefixStream(child.stdout, "owpenbot", "stdout");
|
||||
prefixStream(child.stderr, "owpenbot", "stderr");
|
||||
prefixStream(child.stdout, "owpenbot", "stdout", options.logger, child.pid ?? undefined);
|
||||
prefixStream(child.stderr, "owpenbot", "stderr", options.logger, child.pid ?? undefined);
|
||||
|
||||
return child;
|
||||
}
|
||||
@@ -1781,13 +1863,127 @@ function outputError(error: unknown, json: boolean): void {
|
||||
console.error(message);
|
||||
}
|
||||
|
||||
function createVerboseLogger(enabled: boolean) {
|
||||
function createVerboseLogger(enabled: boolean, logger?: Logger, component = "openwrk") {
|
||||
return (message: string) => {
|
||||
if (!enabled) return;
|
||||
console.log(`[openwrk] ${message}`);
|
||||
if (logger) {
|
||||
logger.debug(message, undefined, component);
|
||||
return;
|
||||
}
|
||||
console.log(`[${component}] ${message}`);
|
||||
};
|
||||
}
|
||||
|
||||
const LOG_LEVEL_NUMBERS: Record<LogLevel, number> = {
|
||||
debug: 5,
|
||||
info: 9,
|
||||
warn: 13,
|
||||
error: 17,
|
||||
};
|
||||
|
||||
function toUnixNano(): string {
|
||||
return (BigInt(Date.now()) * 1_000_000n).toString();
|
||||
}
|
||||
|
||||
function mergeResourceAttributes(additional: Record<string, string>, existing?: string): string {
|
||||
const entries = new Map<string, string>();
|
||||
if (existing) {
|
||||
for (const part of existing.split(",")) {
|
||||
const trimmed = part.trim();
|
||||
if (!trimmed) continue;
|
||||
const [key, ...rest] = trimmed.split("=");
|
||||
if (!key || rest.length === 0) continue;
|
||||
entries.set(key, rest.join("=").replace(/,/g, ";"));
|
||||
}
|
||||
}
|
||||
for (const [key, value] of Object.entries(additional)) {
|
||||
if (!key) continue;
|
||||
if (value === undefined || value === null) continue;
|
||||
entries.set(key, String(value).replace(/,/g, ";"));
|
||||
}
|
||||
return Array.from(entries.entries())
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join(",");
|
||||
}
|
||||
|
||||
function createLogger(options: {
|
||||
format: LogFormat;
|
||||
runId: string;
|
||||
serviceName: string;
|
||||
serviceVersion?: string;
|
||||
}): Logger {
|
||||
const host = hostname().trim();
|
||||
const resource: Record<string, string> = {
|
||||
"service.name": options.serviceName,
|
||||
"service.instance.id": options.runId,
|
||||
};
|
||||
if (options.serviceVersion) {
|
||||
resource["service.version"] = options.serviceVersion;
|
||||
}
|
||||
if (host) {
|
||||
resource["host.name"] = host;
|
||||
}
|
||||
const baseAttributes: LogAttributes = {
|
||||
"run.id": options.runId,
|
||||
"process.pid": process.pid,
|
||||
};
|
||||
|
||||
const emit = (level: LogLevel, message: string, attributes?: LogAttributes, component?: string) => {
|
||||
const mergedAttributes: LogAttributes = {
|
||||
...baseAttributes,
|
||||
...(component ? { "service.component": component } : {}),
|
||||
...(attributes ?? {}),
|
||||
};
|
||||
if (options.format === "json") {
|
||||
const record = {
|
||||
timeUnixNano: toUnixNano(),
|
||||
severityText: level.toUpperCase(),
|
||||
severityNumber: LOG_LEVEL_NUMBERS[level],
|
||||
body: message,
|
||||
attributes: mergedAttributes,
|
||||
resource,
|
||||
};
|
||||
process.stdout.write(`${JSON.stringify(record)}\n`);
|
||||
return;
|
||||
}
|
||||
const label = component ?? options.serviceName;
|
||||
const levelTag = level === "info" ? "" : ` ${level.toUpperCase()}`;
|
||||
const tag = label ? `${`[${label}]`}${levelTag}` : levelTag.trim();
|
||||
const line = tag ? `${tag} ${message}` : message;
|
||||
process.stdout.write(`${line}\n`);
|
||||
};
|
||||
|
||||
const child = (component: string, attributes?: LogAttributes): LoggerChild => ({
|
||||
log: (level, message, attrs) => emit(level, message, { ...(attributes ?? {}), ...(attrs ?? {}) }, component),
|
||||
debug: (message, attrs) => emit("debug", message, { ...(attributes ?? {}), ...(attrs ?? {}) }, component),
|
||||
info: (message, attrs) => emit("info", message, { ...(attributes ?? {}), ...(attrs ?? {}) }, component),
|
||||
warn: (message, attrs) => emit("warn", message, { ...(attributes ?? {}), ...(attrs ?? {}) }, component),
|
||||
error: (message, attrs) => emit("error", message, { ...(attributes ?? {}), ...(attrs ?? {}) }, component),
|
||||
});
|
||||
|
||||
return {
|
||||
format: options.format,
|
||||
log: emit,
|
||||
debug: (message, attrs, component) => emit("debug", message, attrs, component),
|
||||
info: (message, attrs, component) => emit("info", message, attrs, component),
|
||||
warn: (message, attrs, component) => emit("warn", message, attrs, component),
|
||||
error: (message, attrs, component) => emit("error", message, attrs, component),
|
||||
child,
|
||||
};
|
||||
}
|
||||
|
||||
function looksLikeOtelLogLine(line: string): boolean {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return false;
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed) as Record<string, unknown>;
|
||||
if (!parsed || typeof parsed !== "object") return false;
|
||||
return typeof parsed.timeUnixNano === "string" && typeof parsed.severityText === "string";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function spawnRouterDaemon(args: ParsedArgs, dataDir: string, host: string, port: number) {
|
||||
const self = resolveSelfCommand();
|
||||
const commandArgs = [
|
||||
@@ -1813,6 +2009,8 @@ async function spawnRouterDaemon(args: ParsedArgs, dataDir: string, host: string
|
||||
const sidecarSource = readFlag(args.flags, "sidecar-source") ?? process.env.OPENWRK_SIDECAR_SOURCE;
|
||||
const opencodeSource = readFlag(args.flags, "opencode-source") ?? process.env.OPENWRK_OPENCODE_SOURCE;
|
||||
const verbose = readBool(args.flags, "verbose", false, "OPENWRK_VERBOSE");
|
||||
const logFormat = readFlag(args.flags, "log-format") ?? process.env.OPENWRK_LOG_FORMAT;
|
||||
const runId = readFlag(args.flags, "run-id") ?? process.env.OPENWRK_RUN_ID;
|
||||
|
||||
if (opencodeBin) commandArgs.push("--opencode-bin", opencodeBin);
|
||||
if (opencodeHost) commandArgs.push("--opencode-host", opencodeHost);
|
||||
@@ -1825,6 +2023,8 @@ async function spawnRouterDaemon(args: ParsedArgs, dataDir: string, host: string
|
||||
if (sidecarSource) commandArgs.push("--sidecar-source", sidecarSource);
|
||||
if (opencodeSource) commandArgs.push("--opencode-source", opencodeSource);
|
||||
if (verbose) commandArgs.push("--verbose");
|
||||
if (logFormat) commandArgs.push("--log-format", String(logFormat));
|
||||
if (runId) commandArgs.push("--run-id", String(runId));
|
||||
|
||||
const child = spawn(self.command, commandArgs, {
|
||||
detached: true,
|
||||
@@ -1995,7 +2195,16 @@ async function runInstanceCommand(args: ParsedArgs) {
|
||||
async function runRouterDaemon(args: ParsedArgs) {
|
||||
const outputJson = readBool(args.flags, "json", false);
|
||||
const verbose = readBool(args.flags, "verbose", false, "OPENWRK_VERBOSE");
|
||||
const logVerbose = createVerboseLogger(verbose && !outputJson);
|
||||
const logFormat = readLogFormat(args.flags, "log-format", "pretty", "OPENWRK_LOG_FORMAT");
|
||||
const runId = readFlag(args.flags, "run-id") ?? process.env.OPENWRK_RUN_ID ?? randomUUID();
|
||||
const cliVersion = await resolveCliVersion();
|
||||
const logger = createLogger({
|
||||
format: logFormat,
|
||||
runId,
|
||||
serviceName: "openwrk",
|
||||
serviceVersion: cliVersion,
|
||||
});
|
||||
const logVerbose = createVerboseLogger(verbose && !outputJson, logger, "openwrk");
|
||||
const sidecarSource = readBinarySource(args.flags, "sidecar-source", "auto", "OPENWRK_SIDECAR_SOURCE");
|
||||
const opencodeSource = readBinarySource(args.flags, "opencode-source", "auto", "OPENWRK_OPENCODE_SOURCE");
|
||||
const dataDir = resolveRouterDataDir(args.flags);
|
||||
@@ -2033,8 +2242,12 @@ async function runRouterDaemon(args: ParsedArgs) {
|
||||
const activeWorkspace = state.workspaces.find((entry) => entry.id === state.activeId && entry.workspaceType === "local");
|
||||
const opencodeWorkdir = opencodeWorkdirFlag ?? activeWorkspace?.path ?? process.cwd();
|
||||
const resolvedWorkdir = await ensureWorkspace(opencodeWorkdir);
|
||||
logger.info(
|
||||
"Daemon starting",
|
||||
{ runId, logFormat, workdir: resolvedWorkdir, host, port },
|
||||
"openwrk",
|
||||
);
|
||||
|
||||
const cliVersion = await resolveCliVersion();
|
||||
const sidecar = resolveSidecarConfig(args.flags, cliVersion);
|
||||
const allowExternal = readBool(args.flags, "allow-external", false, "OPENWRK_ALLOW_EXTERNAL");
|
||||
const manifest = await readVersionManifest();
|
||||
@@ -2112,15 +2325,21 @@ async function runRouterDaemon(args: ParsedArgs) {
|
||||
username: opencodePassword ? opencodeUsername : undefined,
|
||||
password: opencodePassword,
|
||||
corsOrigins: corsOrigins.length ? corsOrigins : ["*"],
|
||||
logger,
|
||||
runId,
|
||||
logFormat,
|
||||
});
|
||||
opencodeChild = child;
|
||||
logger.info("Process spawned", { pid: child.pid ?? 0 }, "opencode");
|
||||
const baseUrl = `http://${opencodeHost}:${opencodePort}`;
|
||||
const client = createOpencodeClient({
|
||||
baseUrl,
|
||||
directory: resolvedWorkdir,
|
||||
headers: authHeaders,
|
||||
});
|
||||
logger.info("Waiting for health", { url: baseUrl }, "opencode");
|
||||
await waitForOpencodeHealthy(client);
|
||||
logger.info("Healthy", { url: baseUrl }, "opencode");
|
||||
state.opencode = {
|
||||
pid: child.pid ?? 0,
|
||||
port: opencodePort,
|
||||
@@ -2135,6 +2354,22 @@ async function runRouterDaemon(args: ParsedArgs) {
|
||||
await ensureOpencode();
|
||||
|
||||
const server = createHttpServer(async (req, res) => {
|
||||
const startedAt = Date.now();
|
||||
const method = req.method ?? "GET";
|
||||
const url = new URL(req.url ?? "/", `http://${host}:${port}`);
|
||||
res.on("finish", () => {
|
||||
logger.info(
|
||||
"Router request",
|
||||
{
|
||||
method,
|
||||
path: url.pathname,
|
||||
status: res.statusCode,
|
||||
durationMs: Date.now() - startedAt,
|
||||
activeId: state.activeId,
|
||||
},
|
||||
"openwrk-router",
|
||||
);
|
||||
});
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
|
||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
||||
@@ -2144,8 +2379,6 @@ async function runRouterDaemon(args: ParsedArgs) {
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const url = new URL(req.url ?? "/", `http://${host}:${port}`);
|
||||
const parts = url.pathname.split("/").filter(Boolean);
|
||||
|
||||
const send = (status: number, payload: unknown) => {
|
||||
@@ -2328,6 +2561,7 @@ async function runRouterDaemon(args: ParsedArgs) {
|
||||
});
|
||||
|
||||
const shutdown = async () => {
|
||||
logger.info("Daemon shutting down", { host, port }, "openwrk-router");
|
||||
try {
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
} catch {
|
||||
@@ -2358,7 +2592,11 @@ async function runRouterDaemon(args: ParsedArgs) {
|
||||
if (outputJson) {
|
||||
outputResult({ ok: true, daemon: state.daemon }, true);
|
||||
} else {
|
||||
console.log(`openwrk daemon running on ${host}:${port}`);
|
||||
if (logFormat === "json") {
|
||||
logger.info("Daemon running", { host, port }, "openwrk-router");
|
||||
} else {
|
||||
console.log(`openwrk daemon running on ${host}:${port}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2479,12 +2717,22 @@ async function runStart(args: ParsedArgs) {
|
||||
const checkOnly = readBool(args.flags, "check", false);
|
||||
const checkEvents = readBool(args.flags, "check-events", false);
|
||||
const verbose = readBool(args.flags, "verbose", false, "OPENWRK_VERBOSE");
|
||||
const logVerbose = createVerboseLogger(verbose && !outputJson);
|
||||
const logFormat = readLogFormat(args.flags, "log-format", "pretty", "OPENWRK_LOG_FORMAT");
|
||||
const runId = readFlag(args.flags, "run-id") ?? process.env.OPENWRK_RUN_ID ?? randomUUID();
|
||||
const cliVersion = await resolveCliVersion();
|
||||
const logger = createLogger({
|
||||
format: logFormat,
|
||||
runId,
|
||||
serviceName: "openwrk",
|
||||
serviceVersion: cliVersion,
|
||||
});
|
||||
const logVerbose = createVerboseLogger(verbose && !outputJson, logger, "openwrk");
|
||||
const sidecarSource = readBinarySource(args.flags, "sidecar-source", "auto", "OPENWRK_SIDECAR_SOURCE");
|
||||
const opencodeSource = readBinarySource(args.flags, "opencode-source", "auto", "OPENWRK_OPENCODE_SOURCE");
|
||||
|
||||
const workspace = readFlag(args.flags, "workspace") ?? process.env.OPENWORK_WORKSPACE ?? process.cwd();
|
||||
const resolvedWorkspace = await ensureWorkspace(workspace);
|
||||
logger.info("Run starting", { workspace: resolvedWorkspace, logFormat, runId }, "openwrk");
|
||||
|
||||
const explicitOpencodeBin = readFlag(args.flags, "opencode-bin") ?? process.env.OPENWORK_OPENCODE_BIN;
|
||||
const opencodeBindHost = readFlag(args.flags, "opencode-host") ?? process.env.OPENWORK_OPENCODE_BIND_HOST ?? "0.0.0.0";
|
||||
@@ -2528,7 +2776,6 @@ async function runStart(args: ParsedArgs) {
|
||||
const corsOrigins = parseList(corsValue);
|
||||
const connectHost = readFlag(args.flags, "connect-host");
|
||||
|
||||
const cliVersion = await resolveCliVersion();
|
||||
const sidecar = resolveSidecarConfig(args.flags, cliVersion);
|
||||
const manifest = await readVersionManifest();
|
||||
const allowExternal = readBool(args.flags, "allow-external", false, "OPENWRK_ALLOW_EXTERNAL");
|
||||
@@ -2587,19 +2834,20 @@ async function runStart(args: ParsedArgs) {
|
||||
const shutdown = async () => {
|
||||
if (shuttingDown) return;
|
||||
shuttingDown = true;
|
||||
logger.info("Shutting down", { children: children.map((handle) => handle.name) }, "openwrk");
|
||||
await Promise.all(children.map((handle) => stopChild(handle.child)));
|
||||
};
|
||||
|
||||
const handleExit = (name: string, code: number | null, signal: NodeJS.Signals | null) => {
|
||||
if (shuttingDown) return;
|
||||
const reason = code !== null ? `code ${code}` : signal ? `signal ${signal}` : "unknown";
|
||||
console.error(`[${name}] exited (${reason})`);
|
||||
logger.error("Process exited", { reason, code, signal }, name);
|
||||
void shutdown().then(() => process.exit(code ?? 1));
|
||||
};
|
||||
|
||||
const handleSpawnError = (name: string, error: unknown) => {
|
||||
if (shuttingDown) return;
|
||||
console.error(`[${name}] failed to start: ${String(error)}`);
|
||||
logger.error("Process failed to start", { error: String(error) }, name);
|
||||
void shutdown().then(() => process.exit(1));
|
||||
};
|
||||
|
||||
@@ -2613,8 +2861,12 @@ async function runStart(args: ParsedArgs) {
|
||||
username: opencodeUsername,
|
||||
password: opencodePassword,
|
||||
corsOrigins: corsOrigins.length ? corsOrigins : ["*"],
|
||||
logger,
|
||||
runId,
|
||||
logFormat,
|
||||
});
|
||||
children.push({ name: "opencode", child: opencodeChild });
|
||||
logger.info("Process spawned", { pid: opencodeChild.pid ?? 0 }, "opencode");
|
||||
opencodeChild.on("exit", (code, signal) => handleExit("opencode", code, signal));
|
||||
opencodeChild.on("error", (error) => handleSpawnError("opencode", error));
|
||||
|
||||
@@ -2628,7 +2880,9 @@ async function runStart(args: ParsedArgs) {
|
||||
headers: Object.keys(authHeaders).length ? authHeaders : undefined,
|
||||
});
|
||||
|
||||
logger.info("Waiting for health", { url: opencodeBaseUrl }, "opencode");
|
||||
await waitForOpencodeHealthy(opencodeClient);
|
||||
logger.info("Healthy", { url: opencodeBaseUrl }, "opencode");
|
||||
|
||||
const openworkChild = await startOpenworkServer({
|
||||
bin: openworkServerBinary.bin,
|
||||
@@ -2646,12 +2900,18 @@ async function runStart(args: ParsedArgs) {
|
||||
opencodeUsername,
|
||||
opencodePassword,
|
||||
owpenbotHealthPort,
|
||||
logger,
|
||||
runId,
|
||||
logFormat,
|
||||
});
|
||||
children.push({ name: "openwork-server", child: openworkChild });
|
||||
logger.info("Process spawned", { pid: openworkChild.pid ?? 0 }, "openwork-server");
|
||||
openworkChild.on("exit", (code, signal) => handleExit("openwork-server", code, signal));
|
||||
openworkChild.on("error", (error) => handleSpawnError("openwork-server", error));
|
||||
|
||||
logger.info("Waiting for health", { url: openworkBaseUrl }, "openwork-server");
|
||||
await waitForHealthy(openworkBaseUrl);
|
||||
logger.info("Healthy", { url: openworkBaseUrl }, "openwork-server");
|
||||
|
||||
const openworkActualVersion = await verifyOpenworkServer({
|
||||
baseUrl: openworkBaseUrl,
|
||||
@@ -2679,20 +2939,25 @@ async function runStart(args: ParsedArgs) {
|
||||
opencodeUsername,
|
||||
opencodePassword,
|
||||
owpenbotHealthPort,
|
||||
logger,
|
||||
runId,
|
||||
logFormat,
|
||||
});
|
||||
children.push({ name: "owpenbot", child: owpenbotChild });
|
||||
logger.info("Process spawned", { pid: owpenbotChild.pid ?? 0 }, "owpenbot");
|
||||
owpenbotChild.on("exit", (code, signal) => {
|
||||
if (owpenbotRequired) {
|
||||
handleExit("owpenbot", code, signal);
|
||||
return;
|
||||
}
|
||||
const reason = code !== null ? `code ${code}` : signal ? `signal ${signal}` : "unknown";
|
||||
console.warn(`[owpenbot] exited (${reason}). Continuing without owpenbot.`);
|
||||
logger.warn("Process exited, continuing without owpenbot", { reason, code, signal }, "owpenbot");
|
||||
});
|
||||
owpenbotChild.on("error", (error) => handleSpawnError("owpenbot", error));
|
||||
}
|
||||
|
||||
const payload = {
|
||||
runId,
|
||||
workspace: resolvedWorkspace,
|
||||
approval: {
|
||||
mode: approvalMode,
|
||||
@@ -2760,8 +3025,20 @@ async function runStart(args: ParsedArgs) {
|
||||
|
||||
if (outputJson) {
|
||||
console.log(JSON.stringify(payload, null, 2));
|
||||
} else if (logFormat === "json") {
|
||||
logger.info(
|
||||
"Ready",
|
||||
{
|
||||
workspace: payload.workspace,
|
||||
opencode: payload.opencode,
|
||||
openwork: payload.openwork,
|
||||
owpenbot: payload.owpenbot,
|
||||
},
|
||||
"openwrk",
|
||||
);
|
||||
} else {
|
||||
console.log("Openwrk running");
|
||||
console.log(`Run ID: ${runId}`);
|
||||
console.log(`Workspace: ${payload.workspace}`);
|
||||
console.log(`OpenCode: ${payload.opencode.baseUrl}`);
|
||||
console.log(`OpenCode connect URL: ${payload.opencode.connectUrl}`);
|
||||
@@ -2782,11 +3059,12 @@ async function runStart(args: ParsedArgs) {
|
||||
openworkToken,
|
||||
checkEvents,
|
||||
});
|
||||
if (!outputJson) {
|
||||
logger.info("Checks ok", { checkEvents }, "openwrk");
|
||||
if (!outputJson && logFormat === "pretty") {
|
||||
console.log("Checks: ok");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Checks failed: ${String(error)}`);
|
||||
logger.error("Checks failed", { error: String(error) }, "openwrk");
|
||||
await shutdown();
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -2799,7 +3077,7 @@ async function runStart(args: ParsedArgs) {
|
||||
await new Promise(() => undefined);
|
||||
} catch (error) {
|
||||
await shutdown();
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
logger.error("Run failed", { error: error instanceof Error ? error.message : String(error) }, "openwrk");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { parseCliArgs, printHelp, resolveServerConfig } from "./config.js";
|
||||
import { startServer } from "./server.js";
|
||||
import { createServerLogger, startServer } from "./server.js";
|
||||
import pkg from "../package.json" with { type: "json" };
|
||||
|
||||
const args = parseCliArgs(process.argv.slice(2));
|
||||
@@ -17,31 +17,32 @@ if (args.version) {
|
||||
}
|
||||
|
||||
const config = await resolveServerConfig(args);
|
||||
const logger = createServerLogger(config);
|
||||
const server = startServer(config);
|
||||
|
||||
const url = `http://${config.host}:${server.port}`;
|
||||
console.log(`OpenWork server listening on ${url}`);
|
||||
logger.log("info", `OpenWork server listening on ${url}`);
|
||||
|
||||
if (config.tokenSource === "generated") {
|
||||
console.log(`Client token: ${config.token}`);
|
||||
logger.log("info", `Client token: ${config.token}`);
|
||||
}
|
||||
|
||||
if (config.hostTokenSource === "generated") {
|
||||
console.log(`Host token: ${config.hostToken}`);
|
||||
logger.log("info", `Host token: ${config.hostToken}`);
|
||||
}
|
||||
|
||||
if (config.workspaces.length === 0) {
|
||||
console.log("No workspaces configured. Add --workspace or update server.json.");
|
||||
logger.log("info", "No workspaces configured. Add --workspace or update server.json.");
|
||||
} else {
|
||||
console.log(`Workspaces: ${config.workspaces.length}`);
|
||||
logger.log("info", `Workspaces: ${config.workspaces.length}`);
|
||||
}
|
||||
|
||||
if (args.verbose) {
|
||||
console.log(`Config path: ${config.configPath ?? "unknown"}`);
|
||||
console.log(`Read-only: ${config.readOnly ? "true" : "false"}`);
|
||||
console.log(`Approval: ${config.approval.mode} (${config.approval.timeoutMs}ms)`);
|
||||
console.log(`CORS origins: ${config.corsOrigins.join(", ")}`);
|
||||
console.log(`Authorized roots: ${config.authorizedRoots.join(", ")}`);
|
||||
console.log(`Token source: ${config.tokenSource}`);
|
||||
console.log(`Host token source: ${config.hostTokenSource}`);
|
||||
logger.log("info", `Config path: ${config.configPath ?? "unknown"}`);
|
||||
logger.log("info", `Read-only: ${config.readOnly ? "true" : "false"}`);
|
||||
logger.log("info", `Approval: ${config.approval.mode} (${config.approval.timeoutMs}ms)`);
|
||||
logger.log("info", `CORS origins: ${config.corsOrigins.join(", ")}`);
|
||||
logger.log("info", `Authorized roots: ${config.authorizedRoots.join(", ")}`);
|
||||
logger.log("info", `Token source: ${config.tokenSource}`);
|
||||
logger.log("info", `Host token source: ${config.hostTokenSource}`);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { homedir } from "node:os";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import type { ApprovalMode, ApprovalConfig, ServerConfig, WorkspaceConfig } from "./types.js";
|
||||
import type { ApprovalMode, ApprovalConfig, ServerConfig, WorkspaceConfig, LogFormat } from "./types.js";
|
||||
import { buildWorkspaceInfos } from "./workspaces.js";
|
||||
import { parseList, readJsonFile, shortId } from "./utils.js";
|
||||
|
||||
@@ -20,6 +20,8 @@ interface CliArgs {
|
||||
corsOrigins?: string[];
|
||||
readOnly?: boolean;
|
||||
verbose?: boolean;
|
||||
logFormat?: LogFormat;
|
||||
logRequests?: boolean;
|
||||
version?: boolean;
|
||||
help?: boolean;
|
||||
}
|
||||
@@ -36,11 +38,31 @@ interface FileConfig {
|
||||
readOnly?: boolean;
|
||||
opencodeUsername?: string;
|
||||
opencodePassword?: string;
|
||||
logFormat?: LogFormat;
|
||||
logRequests?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_PORT = 8787;
|
||||
const DEFAULT_HOST = "127.0.0.1";
|
||||
const DEFAULT_TIMEOUT_MS = 30000;
|
||||
const DEFAULT_LOG_FORMAT: LogFormat = "pretty";
|
||||
const DEFAULT_LOG_REQUESTS = true;
|
||||
|
||||
function normalizeLogFormat(value: string | undefined): LogFormat | undefined {
|
||||
if (!value) return undefined;
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (normalized === "json") return "json";
|
||||
if (normalized === "pretty" || normalized === "text" || normalized === "human") return "pretty";
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function parseBoolean(value: string | undefined): boolean | undefined {
|
||||
if (!value) return undefined;
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (["true", "1", "yes", "on"].includes(normalized)) return true;
|
||||
if (["false", "0", "no", "off"].includes(normalized)) return false;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function parseCliArgs(argv: string[]): CliArgs {
|
||||
const args: CliArgs = { workspaces: [] };
|
||||
@@ -59,6 +81,19 @@ export function parseCliArgs(argv: string[]): CliArgs {
|
||||
args.verbose = true;
|
||||
continue;
|
||||
}
|
||||
if (value === "--log-format") {
|
||||
args.logFormat = argv[index + 1] as LogFormat | undefined;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (value === "--log-requests") {
|
||||
args.logRequests = true;
|
||||
continue;
|
||||
}
|
||||
if (value === "--no-log-requests") {
|
||||
args.logRequests = false;
|
||||
continue;
|
||||
}
|
||||
if (value === "--config") {
|
||||
args.configPath = argv[index + 1];
|
||||
index += 1;
|
||||
@@ -155,6 +190,9 @@ export function printHelp(): void {
|
||||
" --workspace <path> Workspace root (repeatable)",
|
||||
" --cors <origins> Comma-separated origins or *",
|
||||
" --read-only Disable writes",
|
||||
" --log-format <format> Log output format: pretty | json",
|
||||
" --log-requests Log incoming requests (default: true)",
|
||||
" --no-log-requests Disable request logging",
|
||||
" --verbose Print resolved config",
|
||||
" --version Show version",
|
||||
].join("\n");
|
||||
@@ -255,6 +293,16 @@ export async function resolveServerConfig(cli: CliArgs): Promise<ServerConfig> {
|
||||
: undefined;
|
||||
const readOnly = cli.readOnly ?? parsedReadOnly ?? fileConfig.readOnly ?? false;
|
||||
|
||||
const envLogFormat = process.env.OPENWORK_LOG_FORMAT;
|
||||
const logFormat =
|
||||
cli.logFormat ??
|
||||
normalizeLogFormat(envLogFormat) ??
|
||||
normalizeLogFormat(fileConfig.logFormat) ??
|
||||
DEFAULT_LOG_FORMAT;
|
||||
|
||||
const envLogRequests = parseBoolean(process.env.OPENWORK_LOG_REQUESTS);
|
||||
const logRequests = cli.logRequests ?? envLogRequests ?? fileConfig.logRequests ?? DEFAULT_LOG_REQUESTS;
|
||||
|
||||
const authorizedRoots =
|
||||
fileConfig.authorizedRoots?.length
|
||||
? fileConfig.authorizedRoots.map((root) => resolve(configDir, root))
|
||||
@@ -277,5 +325,7 @@ export async function resolveServerConfig(cli: CliArgs): Promise<ServerConfig> {
|
||||
startedAt: Date.now(),
|
||||
tokenSource,
|
||||
hostTokenSource,
|
||||
logFormat,
|
||||
logRequests,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { readFile, writeFile, rm } from "node:fs/promises";
|
||||
import { homedir } from "node:os";
|
||||
import { homedir, hostname } from "node:os";
|
||||
import { join, resolve, sep } from "node:path";
|
||||
import type { ApprovalRequest, Capabilities, ServerConfig, WorkspaceInfo, Actor, ReloadReason, ReloadTrigger } from "./types.js";
|
||||
import { ApprovalService } from "./approvals.js";
|
||||
@@ -20,6 +20,91 @@ import pkg from "../package.json" with { type: "json" };
|
||||
|
||||
const SERVER_VERSION = pkg.version;
|
||||
|
||||
type LogLevel = "info" | "warn" | "error";
|
||||
|
||||
type LogAttributes = Record<string, unknown>;
|
||||
|
||||
type ServerLogger = {
|
||||
log: (level: LogLevel, message: string, attributes?: LogAttributes) => void;
|
||||
};
|
||||
|
||||
const LOG_LEVEL_NUMBERS: Record<LogLevel, number> = {
|
||||
info: 9,
|
||||
warn: 13,
|
||||
error: 17,
|
||||
};
|
||||
|
||||
function toUnixNano(): string {
|
||||
return (BigInt(Date.now()) * 1_000_000n).toString();
|
||||
}
|
||||
|
||||
export function createServerLogger(config: ServerConfig): ServerLogger {
|
||||
const runId = process.env.OPENWRK_RUN_ID ?? process.env.OPENWORK_RUN_ID ?? shortId();
|
||||
const host = hostname().trim();
|
||||
const resource: Record<string, string> = {
|
||||
"service.name": "openwork-server",
|
||||
"service.version": SERVER_VERSION,
|
||||
"service.instance.id": runId,
|
||||
};
|
||||
if (host) {
|
||||
resource["host.name"] = host;
|
||||
}
|
||||
const baseAttributes: LogAttributes = {
|
||||
"run.id": runId,
|
||||
"process.pid": process.pid,
|
||||
};
|
||||
|
||||
const emit = (level: LogLevel, message: string, attributes?: LogAttributes) => {
|
||||
const merged = { ...baseAttributes, ...(attributes ?? {}) };
|
||||
if (config.logFormat === "json") {
|
||||
const record = {
|
||||
timeUnixNano: toUnixNano(),
|
||||
severityText: level.toUpperCase(),
|
||||
severityNumber: LOG_LEVEL_NUMBERS[level],
|
||||
body: message,
|
||||
attributes: merged,
|
||||
resource,
|
||||
};
|
||||
process.stdout.write(`${JSON.stringify(record)}\n`);
|
||||
return;
|
||||
}
|
||||
process.stdout.write(`${message}\n`);
|
||||
};
|
||||
|
||||
return { log: emit };
|
||||
}
|
||||
|
||||
function logRequest(input: {
|
||||
logger: ServerLogger;
|
||||
request: Request;
|
||||
response: Response;
|
||||
durationMs: number;
|
||||
authMode: AuthMode;
|
||||
proxyBaseUrl?: string;
|
||||
error?: string;
|
||||
}) {
|
||||
const { logger, request, response, durationMs, authMode, proxyBaseUrl, error } = input;
|
||||
const status = response.status;
|
||||
const level: LogLevel = status >= 500 ? "error" : status >= 400 ? "warn" : "info";
|
||||
const url = new URL(request.url);
|
||||
const method = request.method.toUpperCase();
|
||||
const message = `${method} ${url.pathname} ${status} ${durationMs}ms${proxyBaseUrl ? " (opencode)" : ""}`;
|
||||
const attributes: LogAttributes = {
|
||||
method,
|
||||
path: url.pathname,
|
||||
status,
|
||||
durationMs,
|
||||
auth: authMode,
|
||||
};
|
||||
if (proxyBaseUrl) {
|
||||
attributes["opencode.base_url"] = proxyBaseUrl;
|
||||
}
|
||||
if (error) {
|
||||
attributes.error = error;
|
||||
}
|
||||
logger.log(level, message, attributes);
|
||||
}
|
||||
|
||||
type AuthMode = "none" | "client" | "host";
|
||||
|
||||
interface Route {
|
||||
@@ -44,6 +129,7 @@ export function startServer(config: ServerConfig) {
|
||||
const approvals = new ApprovalService(config.approval);
|
||||
const reloadEvents = new ReloadEventStore();
|
||||
const routes = createRoutes(config, approvals);
|
||||
const logger = createServerLogger(config);
|
||||
|
||||
const serverOptions: {
|
||||
hostname: string;
|
||||
@@ -54,28 +140,54 @@ export function startServer(config: ServerConfig) {
|
||||
port: config.port,
|
||||
fetch: async (request: Request) => {
|
||||
const url = new URL(request.url);
|
||||
const startedAt = Date.now();
|
||||
let authMode: AuthMode = "none";
|
||||
let proxyBaseUrl: string | undefined;
|
||||
let errorMessage: string | undefined;
|
||||
|
||||
const finalize = (response: Response) => {
|
||||
const wrapped = withCors(response, request, config);
|
||||
if (config.logRequests) {
|
||||
logRequest({
|
||||
logger,
|
||||
request,
|
||||
response: wrapped,
|
||||
durationMs: Date.now() - startedAt,
|
||||
authMode,
|
||||
proxyBaseUrl,
|
||||
error: errorMessage,
|
||||
});
|
||||
}
|
||||
return wrapped;
|
||||
};
|
||||
|
||||
if (request.method === "OPTIONS") {
|
||||
return withCors(new Response(null, { status: 204 }), request, config);
|
||||
return finalize(new Response(null, { status: 204 }));
|
||||
}
|
||||
|
||||
if (url.pathname === "/opencode" || url.pathname.startsWith("/opencode/")) {
|
||||
authMode = "client";
|
||||
proxyBaseUrl = config.workspaces[0]?.baseUrl?.trim() || undefined;
|
||||
try {
|
||||
requireClient(request, config);
|
||||
const response = await proxyOpencodeRequest({ request, url, config });
|
||||
return withCors(response, request, config);
|
||||
return finalize(response);
|
||||
} catch (error) {
|
||||
const apiError = error instanceof ApiError
|
||||
? error
|
||||
: new ApiError(500, "internal_error", "Unexpected server error");
|
||||
return withCors(jsonResponse(formatError(apiError), apiError.status), request, config);
|
||||
errorMessage = apiError.message;
|
||||
return finalize(jsonResponse(formatError(apiError), apiError.status));
|
||||
}
|
||||
}
|
||||
|
||||
const route = matchRoute(routes, request.method, url.pathname);
|
||||
if (!route) {
|
||||
return withCors(jsonResponse({ code: "not_found", message: "Not found" }, 404), request, config);
|
||||
errorMessage = "not_found";
|
||||
return finalize(jsonResponse({ code: "not_found", message: "Not found" }, 404));
|
||||
}
|
||||
|
||||
authMode = route.auth;
|
||||
try {
|
||||
const actor = route.auth === "host" ? requireHost(request, config) : route.auth === "client" ? requireClient(request, config) : undefined;
|
||||
const response = await route.handler({
|
||||
@@ -87,12 +199,13 @@ export function startServer(config: ServerConfig) {
|
||||
reloadEvents,
|
||||
actor,
|
||||
});
|
||||
return withCors(response, request, config);
|
||||
return finalize(response);
|
||||
} catch (error) {
|
||||
const apiError = error instanceof ApiError
|
||||
? error
|
||||
: new ApiError(500, "internal_error", "Unexpected server error");
|
||||
return withCors(jsonResponse(formatError(apiError), apiError.status), request, config);
|
||||
errorMessage = apiError.message;
|
||||
return finalize(jsonResponse(formatError(apiError), apiError.status));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -2,6 +2,8 @@ export type WorkspaceType = "local" | "remote";
|
||||
|
||||
export type ApprovalMode = "manual" | "auto";
|
||||
|
||||
export type LogFormat = "pretty" | "json";
|
||||
|
||||
export interface WorkspaceConfig {
|
||||
path: string;
|
||||
name?: string;
|
||||
@@ -48,6 +50,8 @@ export interface ServerConfig {
|
||||
startedAt: number;
|
||||
tokenSource: "cli" | "env" | "file" | "generated";
|
||||
hostTokenSource: "cli" | "env" | "file" | "generated";
|
||||
logFormat: LogFormat;
|
||||
logRequests: boolean;
|
||||
}
|
||||
|
||||
export interface Capabilities {
|
||||
|
||||
Reference in New Issue
Block a user