feat: add host commander skeleton
This commit is contained in:
@@ -124,13 +124,13 @@ assertCondition(secretReasonResult.stdout.includes("<redacted>"), "redacted appr
|
||||
|
||||
const doc = readFileSync("docs/reference/host-codex-commander.md", "utf8");
|
||||
for (const snippet of [
|
||||
"不直接重启 Code Queue backend",
|
||||
"不 cancel 或 interrupt 运行中的 Code Queue task",
|
||||
"不读取、打印或持久化 token 明文",
|
||||
"SSH/PTY/stdio",
|
||||
"#20",
|
||||
"#46",
|
||||
"ClaudeQQ",
|
||||
"本地 skeleton 阶段",
|
||||
"/health",
|
||||
"/api/commander/contract",
|
||||
".state/commander/",
|
||||
"trace summary dry-run",
|
||||
"approval draft preview",
|
||||
"sendImplemented=false",
|
||||
]) {
|
||||
assertCondition(doc.includes(snippet), `reference doc missing snippet: ${snippet}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { commanderContract } from "../src/components/microservices/host-codex-commander/src/contract";
|
||||
import { createCommanderRequestHandler, type RuntimeConfig } from "../src/components/microservices/host-codex-commander/src/index";
|
||||
import {
|
||||
buildCommanderApprovalDraft,
|
||||
commanderApprovalPreview,
|
||||
commanderHealth,
|
||||
commanderSessionPreview,
|
||||
commanderStatePaths,
|
||||
listCommanderSessions,
|
||||
readCommanderApproval,
|
||||
readCommanderSession,
|
||||
summarizeCommanderTrace,
|
||||
writeCommanderApproval,
|
||||
writeCommanderSession,
|
||||
} from "../src/components/microservices/host-codex-commander/src/state";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
function assertCondition(condition: unknown, message: string, detail: unknown = {}): void {
|
||||
if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
|
||||
}
|
||||
|
||||
function asRecord(value: unknown, label: string): JsonRecord {
|
||||
assertCondition(typeof value === "object" && value !== null && !Array.isArray(value), `${label} must be an object`, value);
|
||||
return value as JsonRecord;
|
||||
}
|
||||
|
||||
function dataOf(response: JsonRecord): JsonRecord {
|
||||
return asRecord(response.body, "body");
|
||||
}
|
||||
|
||||
async function readJson(response: Response): Promise<JsonRecord> {
|
||||
const body = await response.json();
|
||||
return asRecord(body, "response body");
|
||||
}
|
||||
|
||||
const tmp = mkdtempSync(join(tmpdir(), "host-codex-commander-"));
|
||||
const runtime: RuntimeConfig = {
|
||||
rootDir: tmp,
|
||||
host: "127.0.0.1",
|
||||
port: 4261,
|
||||
logFile: join(tmp, "logs", "commander.jsonl"),
|
||||
serviceId: "host-codex-commander",
|
||||
stateRoot: tmp,
|
||||
sessionId: "primary",
|
||||
};
|
||||
const handler = createCommanderRequestHandler(runtime);
|
||||
|
||||
try {
|
||||
const contract = commanderContract();
|
||||
assertCondition(contract.ok === true, "contract must be ok", contract);
|
||||
assertCondition(contract.serviceId === "host-codex-commander", "contract must expose service id", contract);
|
||||
assertCondition(contract.daemonImplemented === false, "contract must remain skeleton only", contract);
|
||||
assertCondition(contract.currentImplementation === "host-codex-commander-skeleton", "contract must identify skeleton implementation", contract);
|
||||
|
||||
const session = writeCommanderSession(runtime, {
|
||||
sessionId: "primary",
|
||||
state: "running",
|
||||
promptState: "planned",
|
||||
approvalState: "draft",
|
||||
pid: 321,
|
||||
cwd: "/workspace/unidesk",
|
||||
lastSeq: 7,
|
||||
heartbeatAt: "2026-05-21T00:00:00.000Z",
|
||||
updatedAt: "2026-05-21T00:00:00.000Z",
|
||||
notes: ["token=ghp_abcdef1234567890", "trace-summary:running"],
|
||||
});
|
||||
assertCondition(session.notes[0] === "<redacted>", "session notes must be redacted", session);
|
||||
assertCondition(readCommanderSession(runtime, "primary").state === "running", "session read must round-trip", readCommanderSession(runtime, "primary"));
|
||||
assertCondition(listCommanderSessions(runtime).length >= 1, "session listing must include stored session", listCommanderSessions(runtime));
|
||||
assertCondition(commanderSessionPreview(session).notes.includes("<redacted>"), "session preview must redact notes", commanderSessionPreview(session));
|
||||
|
||||
const trace = summarizeCommanderTrace({
|
||||
taskId: "task-123",
|
||||
sessionId: "primary",
|
||||
traceJsonl: [
|
||||
JSON.stringify({ seq: 1, kind: "message", status: "running", summary: "prompt token=ghp_1234567890abcdef" }),
|
||||
JSON.stringify({ seq: 4, kind: "command", status: "attention_required", command: "ask-for-approval", output: "reason=https://user:secret@example.com" }),
|
||||
JSON.stringify({ seq: 8, kind: "event", status: "completed", text: "done" }),
|
||||
].join("\n"),
|
||||
taskSummary: "task summary token=ghp_aaaaaaaaaaaaaaaa",
|
||||
});
|
||||
assertCondition(trace.taskId === "task-123", "trace summary must preserve task id", trace);
|
||||
assertCondition(trace.lastSeq === 8, "trace summary must preserve last seq", trace);
|
||||
assertCondition(trace.status === "terminal", "trace summary should reach terminal status", trace);
|
||||
assertCondition(trace.redactionsApplied >= 2, "trace summary must redact secrets", trace);
|
||||
assertCondition(trace.taskSummaryPreview?.includes("<redacted>") === true, "task summary preview must redact", trace);
|
||||
assertCondition(trace.keyEvents.length > 0, "trace summary must include key events", trace);
|
||||
|
||||
const approval = buildCommanderApprovalDraft({
|
||||
approvalId: "draft-1",
|
||||
action: "code-queue-task-interrupt",
|
||||
taskId: "task-123",
|
||||
reason: "token=ghp_1234567890abcdef https://user:secret@example.com",
|
||||
sessionId: "primary",
|
||||
});
|
||||
assertCondition(approval.reason.includes("<redacted>"), "approval reason must redact", approval);
|
||||
assertCondition(approval.previewMarkdown.includes("<redacted>"), "approval preview must redact", approval);
|
||||
assertCondition(approval.previewJson["sendImplemented"] === false, "approval preview must not imply sending", approval.previewJson);
|
||||
writeCommanderApproval(runtime, approval);
|
||||
assertCondition(readCommanderApproval(runtime, "draft-1").reason.includes("<redacted>"), "approval round-trip must preserve redaction", readCommanderApproval(runtime, "draft-1"));
|
||||
|
||||
const health = commanderHealth(runtime, "2026-05-21T00:00:00.000Z");
|
||||
assertCondition(health.ok === true && health.service === "host-codex-commander", "health must expose service metadata", health);
|
||||
assertCondition(health.stateRoot === tmp, "health must point at temp state root", health);
|
||||
|
||||
const healthBody = await readJson(await handler(new Request("http://localhost/health")));
|
||||
assertCondition(healthBody.ok === true, "health route must succeed", healthBody);
|
||||
|
||||
const contractBody = await readJson(await handler(new Request("http://localhost/api/commander/contract")));
|
||||
assertCondition(contractBody.serviceId === "host-codex-commander", "HTTP contract route must expose service id", contractBody);
|
||||
|
||||
const sessionsBody = await readJson(await handler(new Request("http://localhost/api/commander/sessions")));
|
||||
assertCondition(Array.isArray(sessionsBody.sessions) && sessionsBody.sessions.length >= 1, "sessions route must list sessions", sessionsBody);
|
||||
|
||||
const traceBody = await readJson(await handler(new Request(`http://localhost/api/commander/trace-summary?taskId=task-123&traceJsonl=${encodeURIComponent(JSON.stringify({ seq: 1, status: "running", summary: "hello token=ghp_1234567890abcdef" }))}`)));
|
||||
assertCondition(traceBody.ok === true && asRecord(traceBody.summary, "summary").redactionsApplied >= 1, "trace route must redact and summarize", traceBody);
|
||||
|
||||
const approvalBody = await readJson(await handler(new Request("http://localhost/api/commander/approvals", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ action: "code-queue-task-cancel", reason: "cookie=session=secret", taskId: "task-123" }),
|
||||
})));
|
||||
assertCondition(approvalBody.ok === true, "approval route must succeed", approvalBody);
|
||||
assertCondition(String(JSON.stringify(approvalBody)).includes("<redacted>"), "approval route must redact sensitive text", approvalBody);
|
||||
|
||||
const statePath = commanderStatePaths(runtime);
|
||||
assertCondition(readFileSync(statePath.stateFile, "utf8").length > 0, "state file must be written", statePath);
|
||||
assertCondition(readFileSync(statePath.approvalFile, "utf8").length > 0, "approval file must be written", statePath);
|
||||
|
||||
process.stdout.write(`${JSON.stringify({
|
||||
ok: true,
|
||||
checks: [
|
||||
"commander contract exposes skeleton contract boundaries",
|
||||
"state files round-trip and redact secrets",
|
||||
"trace summary aggregates mock jsonl input",
|
||||
"approval draft preview stays preview-only and redacted",
|
||||
"HTTP handler serves /health, /api/commander/contract, /api/commander/sessions, /api/commander/trace-summary, and /api/commander/approvals",
|
||||
],
|
||||
}, null, 2)}\n`);
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
@@ -22,7 +22,10 @@ const syntaxFiles = [
|
||||
"scripts/src/docker.ts",
|
||||
"scripts/src/e2e.ts",
|
||||
"scripts/src/help.ts",
|
||||
"scripts/src/commander.ts",
|
||||
"scripts/src/remote.ts",
|
||||
"scripts/host-codex-commander-contract-test.ts",
|
||||
"scripts/host-codex-commander-skeleton-contract-test.ts",
|
||||
"src/components/frontend/src/index.ts",
|
||||
"src/components/frontend/src/app.tsx",
|
||||
"src/components/frontend/src/decision-center.tsx",
|
||||
@@ -34,6 +37,10 @@ const syntaxFiles = [
|
||||
"src/components/microservices/decision-center/src/index.ts",
|
||||
"src/components/microservices/code-queue-mgr/src/index.ts",
|
||||
"src/components/microservices/code-agent-sandbox/src/index.ts",
|
||||
"src/components/microservices/host-codex-commander/src/index.ts",
|
||||
"src/components/microservices/host-codex-commander/src/contract.ts",
|
||||
"src/components/microservices/host-codex-commander/src/redaction.ts",
|
||||
"src/components/microservices/host-codex-commander/src/state.ts",
|
||||
];
|
||||
|
||||
export interface CheckOptions {
|
||||
@@ -167,6 +174,7 @@ function unifiedLogRotationItem(): CheckItem {
|
||||
"src/components/microservices/oa-event-flow/src/index.ts",
|
||||
"src/components/microservices/decision-center/src/index.ts",
|
||||
"src/components/microservices/code-agent-sandbox/src/index.ts",
|
||||
"src/components/microservices/host-codex-commander/src/index.ts",
|
||||
];
|
||||
const offenders = serviceFiles.flatMap((path) => {
|
||||
const text = readFileSync(rootPath(path), "utf8");
|
||||
@@ -275,6 +283,13 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
|
||||
fileItem("src/components/microservices/decision-center/src/index.ts"),
|
||||
fileItem("src/components/microservices/code-queue-mgr/src/index.ts"),
|
||||
fileItem("src/components/microservices/code-agent-sandbox/src/index.ts"),
|
||||
fileItem("src/components/microservices/host-codex-commander/package.json"),
|
||||
fileItem("src/components/microservices/host-codex-commander/tsconfig.json"),
|
||||
fileItem("src/components/microservices/host-codex-commander/Dockerfile"),
|
||||
fileItem("src/components/microservices/host-codex-commander/src/index.ts"),
|
||||
fileItem("src/components/microservices/host-codex-commander/src/contract.ts"),
|
||||
fileItem("src/components/microservices/host-codex-commander/src/redaction.ts"),
|
||||
fileItem("src/components/microservices/host-codex-commander/src/state.ts"),
|
||||
fileItem("src/components/microservices/code-queue-mgr/src/prompt-observation.ts"),
|
||||
fileItem("scripts/src/deploy.ts"),
|
||||
fileItem("scripts/code-queue-issue3-regression-test.ts"),
|
||||
@@ -283,6 +298,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
|
||||
fileItem("scripts/code-queue-trace-summary-contract-test.ts"),
|
||||
fileItem("scripts/code-queue-pr-preflight-contract-test.ts"),
|
||||
fileItem("scripts/code-queue-submit-routing-contract-test.ts"),
|
||||
fileItem("scripts/host-codex-commander-skeleton-contract-test.ts"),
|
||||
fileItem("scripts/provider-runner-triage-contract-test.ts"),
|
||||
fileItem("scripts/src/provider-triage.ts"),
|
||||
fileItem("src/components/microservices/code-queue/src/runner-error-classifier.ts"),
|
||||
@@ -309,6 +325,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
|
||||
items.push(commandItem("code-queue:trace-summary-contract", ["bun", "scripts/code-queue-trace-summary-contract-test.ts"], 30_000));
|
||||
items.push(commandItem("code-queue:pr-preflight-contract", ["bun", "scripts/code-queue-pr-preflight-contract-test.ts"], 30_000));
|
||||
items.push(commandItem("code-queue:submit-routing-contract", ["bun", "scripts/code-queue-submit-routing-contract-test.ts"], 30_000));
|
||||
items.push(commandItem("host-codex-commander:skeleton-contract", ["bun", "scripts/host-codex-commander-skeleton-contract-test.ts"], 30_000));
|
||||
items.push(commandItem("provider:runner-triage-contract", ["bun", "scripts/provider-runner-triage-contract-test.ts"], 30_000));
|
||||
items.push(commandItem("deploy:artifact-matrix-contract", ["bun", "scripts/deploy-artifact-matrix-contract-test.ts"], 30_000));
|
||||
items.push(commandItem("decision-center:desired-state-contract", ["bun", "scripts/decision-center-desired-state-contract-test.ts"], 30_000));
|
||||
@@ -329,6 +346,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
|
||||
items.push(skippedItem("code-queue:trace-summary-contract", "Code Queue trace summary contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("code-queue:pr-preflight-contract", "Code Queue PR preflight contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("code-queue:submit-routing-contract", "Code Queue submit routing contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("host-codex-commander:skeleton-contract", "host Codex commander skeleton contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("provider:runner-triage-contract", "Provider runner triage contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("deploy:artifact-matrix-contract", "deploy artifact matrix contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
items.push(skippedItem("decision-center:desired-state-contract", "Decision Center desired-state drift contract is opt-in with script checks", "--scripts-typecheck or --full"));
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
const requiredDryRunMessage = "This first-phase commander contract only supports dry-run planning; live daemon/control operations are not implemented.";
|
||||
import { commanderContract as hostCommanderContract, commanderHighRiskActions as highRiskActions } from "../../src/components/microservices/host-codex-commander/src/contract";
|
||||
|
||||
const highRiskActions = [
|
||||
"code-queue-backend-restart",
|
||||
"code-queue-backend-rebuild",
|
||||
"code-queue-execution-plane-restart",
|
||||
"code-queue-task-interrupt",
|
||||
"code-queue-task-cancel",
|
||||
"prod-runtime-mutation",
|
||||
] as const;
|
||||
const requiredDryRunMessage = "This host Codex commander skeleton only supports dry-run planning; live daemon/control operations are not implemented.";
|
||||
|
||||
type HighRiskAction = typeof highRiskActions[number];
|
||||
|
||||
@@ -37,6 +30,7 @@ function redactText(value: string): { text: string; redactionsApplied: number }
|
||||
/\b(?:sk|ghp|github_pat|xoxb|xoxp|AKIA)[A-Za-z0-9_=-]{8,}\b/g,
|
||||
/\b(?:token|secret|password|passwd|authorization|cookie|api[_-]?key)\s*[:=]\s*[^,\s]+/gi,
|
||||
/\bBearer\s+[A-Za-z0-9._~+/-]+=*\b/gi,
|
||||
/https?:\/\/[^/\s]+:[^@\s]+@[^/\s]+/gi,
|
||||
];
|
||||
let text = value;
|
||||
for (const pattern of patterns) {
|
||||
@@ -52,7 +46,7 @@ function commanderHelp(): Record<string, unknown> {
|
||||
return {
|
||||
command: "commander",
|
||||
output: "json",
|
||||
description: "First-phase source/contract stub for the host Codex commander control microservice; no daemon or live control action is implemented.",
|
||||
description: "Local skeleton contract for the host Codex commander control microservice; no daemon or live control action is implemented.",
|
||||
usage: [
|
||||
"bun scripts/cli.ts commander contract",
|
||||
"bun scripts/cli.ts commander plan --dry-run [--session-id id]",
|
||||
@@ -64,51 +58,7 @@ function commanderHelp(): Record<string, unknown> {
|
||||
}
|
||||
|
||||
export function commanderContract(): Record<string, unknown> {
|
||||
return {
|
||||
ok: true,
|
||||
phase: "source-contract",
|
||||
serviceId: "host-codex-commander",
|
||||
currentImplementation: "cli-contract-stub-only",
|
||||
daemonImplemented: false,
|
||||
liveOperationsImplemented: false,
|
||||
purpose: "Keep a host Codex commander session observable and controllable through a future direct-managed microservice without replacing Code Queue runners.",
|
||||
ownershipBoundary: {
|
||||
hostCodexProcess: "Long-lived Codex process on the master server host.",
|
||||
controlMicroservice: "Future direct-managed bridge that records state, mediates PTY/stdio/SSH streams, injects prompts, and summarizes traces.",
|
||||
codeQueue: "Remains the task execution plane; the commander only supervises through existing safe CLI/API contracts.",
|
||||
claudeqq: "Approval and user-notification path for high-risk actions.",
|
||||
},
|
||||
requiredCapabilities: [
|
||||
"host-codex-process-discovery",
|
||||
"host-codex-start-plan",
|
||||
"ssh-bridge-contract",
|
||||
"pty-bridge-contract",
|
||||
"stdio-bridge-contract",
|
||||
"prompt-guidance-plan",
|
||||
"trace-summary-plan",
|
||||
"issue-20-board-read-write-entry",
|
||||
"issue-46-brief-read-write-entry",
|
||||
"claudeqq-high-risk-approval-entry",
|
||||
],
|
||||
apiContract: {
|
||||
health: "GET /health",
|
||||
contract: "GET /api/commander/contract",
|
||||
sessions: "GET /api/commander/sessions",
|
||||
sessionPlan: "POST /api/commander/sessions/:sessionId/plan-start",
|
||||
promptPlan: "POST /api/commander/sessions/:sessionId/prompt-plan",
|
||||
traceSummary: "GET /api/commander/trace-summary?taskId=<taskId>",
|
||||
issueWritePlan: "POST /api/commander/issues/:issueNumber/write-plan",
|
||||
approvalRequest: "POST /api/commander/approvals",
|
||||
},
|
||||
stateModel: {
|
||||
sessionStates: ["unknown", "discovered", "planned", "starting", "running", "attention_required", "stopping", "stopped", "degraded"],
|
||||
promptStates: ["draft", "planned", "queued_for_injection", "injected", "rejected", "failed"],
|
||||
approvalStates: ["draft", "requested", "approved", "rejected", "expired", "consumed"],
|
||||
storageRoot: ".state/commander/",
|
||||
redactionPolicy: "Never persist or print token, secret, password, key, cookie, or authorization values in cleartext.",
|
||||
},
|
||||
safetyBoundary: safetyBoundary(),
|
||||
};
|
||||
return hostCommanderContract();
|
||||
}
|
||||
|
||||
function safetyBoundary(): Record<string, unknown> {
|
||||
|
||||
+3
-3
@@ -44,7 +44,7 @@ export function rootHelp(): unknown {
|
||||
{ command: "dev-env validate|prewarm-images", description: "Validate D601 unidesk-dev guardrails or prewarm dev foundation images into native k3s containerd through a bounded async job." },
|
||||
{ command: "artifact-registry plan|render|status|health|install|deploy-backend-core|deploy-service", description: "Manage the D601 host-managed CNCF Distribution registry and run pull-only artifact CD for supported services, including D601 direct, k3s-managed, and code-queue dev-only consumers." },
|
||||
{ command: "gh auth|issue|pr", description: "Run safe GitHub issue and PR CRUD/lifecycle operations through REST with body-file update replace/append, comment delete, token diagnostics, hard delete unsupported, and merge blocked." },
|
||||
{ command: "commander contract|plan --dry-run|approval request --dry-run", description: "First-phase host Codex commander source/contract design stub; returns boundaries and approval plans without starting daemons or executing live control actions." },
|
||||
{ command: "commander contract|plan --dry-run|approval request --dry-run", description: "Host Codex commander skeleton contract and dry-run preview; exposes local health, state, trace summary, and approval draft helpers without live bridges or message sends." },
|
||||
{ command: "code-agent-sandbox", description: "Independent Code Agent Sandbox service skeleton for adapter, mode, and credential-boundary diagnostics." },
|
||||
{ command: "schedule list|get|runs|run|retry-run|delete", description: "Manage backend-core scheduled tasks and run history; schedule run <id> supports --wait-ms N and retry-run reuses the failed run's schedule." },
|
||||
{ command: "schedule upsert-pgdata-backup [--time HH:MM] [--remote-base /SERVER_DATA/UNIDESK_PG_DATA]", description: "Create or update the daily PGDATA physical backup task that uploads monthly rotated archives to Baidu Netdisk." },
|
||||
@@ -195,9 +195,9 @@ function commanderHelp(): unknown {
|
||||
"bun scripts/cli.ts commander plan --dry-run [--session-id id]",
|
||||
"bun scripts/cli.ts commander approval request --action <action> --dry-run [--reason text] [--task-id id]",
|
||||
],
|
||||
description: "Inspect the first-phase source/contract design for the future host Codex commander microservice.",
|
||||
description: "Inspect the local host Codex commander skeleton contract, dry-run planner, state helpers, trace summary aggregator, and approval draft preview.",
|
||||
boundary: [
|
||||
"phase one is contract-only and never starts a daemon",
|
||||
"the current skeleton is local-only and never attaches to live bridges",
|
||||
"dry-run commands never open SSH, PTY, or stdio bridges",
|
||||
"high-risk actions only produce a ClaudeQQ approval draft",
|
||||
"token and secret values must never be printed",
|
||||
|
||||
Reference in New Issue
Block a user