508 lines
19 KiB
TypeScript
508 lines
19 KiB
TypeScript
import { buildCommanderApprovalNotificationDraft, commanderApprovalNotificationPathUnavailable } from "../../src/components/microservices/host-codex-commander/src/approval-notification";
|
|
import { commanderContract as hostCommanderContract, commanderHighRiskActions as highRiskActions } from "../../src/components/microservices/host-codex-commander/src/contract";
|
|
import { redactText } from "../../src/components/microservices/host-codex-commander/src/redaction";
|
|
import { runCommanderPromptLintCommand } from "./commander-prompt-lint";
|
|
|
|
const requiredDryRunMessage = "This host Codex commander skeleton only supports dry-run planning; live daemon/control operations are not implemented.";
|
|
|
|
type HighRiskAction = typeof highRiskActions[number];
|
|
|
|
function hasFlag(args: string[], flag: string): boolean {
|
|
return args.includes(flag);
|
|
}
|
|
|
|
function optionValue(args: string[], name: string): string | undefined {
|
|
const index = args.indexOf(name);
|
|
if (index === -1) return undefined;
|
|
const value = args[index + 1];
|
|
if (value === undefined || value.startsWith("--")) throw new Error(`${name} requires a value`);
|
|
return value;
|
|
}
|
|
|
|
function isHelpToken(value: string | undefined): boolean {
|
|
return value === "help" || value === "--help" || value === "-h";
|
|
}
|
|
|
|
function isHighRiskAction(value: string): value is HighRiskAction {
|
|
return highRiskActions.some((action) => action === value);
|
|
}
|
|
|
|
function commanderHelp(): Record<string, unknown> {
|
|
return {
|
|
command: "commander",
|
|
output: "json",
|
|
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]",
|
|
"bun scripts/cli.ts commander smoke --dry-run [--session-id id]",
|
|
"bun scripts/cli.ts commander approval request --action <action> --dry-run [--reason text] [--task-id id]",
|
|
"bun scripts/cli.ts commander prompt-lint --kind gpt55-pr (--prompt-file <file>|--stdin)",
|
|
],
|
|
highRiskActions,
|
|
reference: "docs/reference/host-codex-commander.md",
|
|
};
|
|
}
|
|
|
|
export function commanderContract(): Record<string, unknown> {
|
|
return hostCommanderContract() as unknown as Record<string, unknown>;
|
|
}
|
|
|
|
function safetyBoundary(): Record<string, unknown> {
|
|
return {
|
|
phaseOneMutationAllowed: false,
|
|
forbiddenWithoutExplicitUserApproval: highRiskActions,
|
|
alwaysForbidden: [
|
|
"print-token-values",
|
|
"read-token-files-for-display",
|
|
"direct-database-state-patch",
|
|
"bypass-code-queue-backend-confirmation-policy",
|
|
"replace-code-queue-runner",
|
|
"deploy-or-restart-production-runtime-from-this-contract-stub",
|
|
],
|
|
confirmationPolicy: "High-risk actions must draft a ClaudeQQ request, wait for explicit user approval, bind approval to one exact action, and record the decision before any future live executor may proceed.",
|
|
};
|
|
}
|
|
|
|
function processDiscoveryPlan(sessionId: string): Record<string, unknown> {
|
|
return {
|
|
sessionId,
|
|
mutation: false,
|
|
signals: [
|
|
".state/commander/sessions/<sessionId>.json",
|
|
"host process table filtered by executable and cwd markers",
|
|
"PTY/stdio bridge heartbeat file",
|
|
"last prompt/trace event sequence",
|
|
],
|
|
startPlan: {
|
|
enabled: false,
|
|
commandShape: "codex <reviewed host profile> --cwd /workspace/unidesk",
|
|
supervisor: "future direct-managed microservice on the master server host",
|
|
reason: requiredDryRunMessage,
|
|
},
|
|
};
|
|
}
|
|
|
|
function bridgePlan(): Record<string, unknown> {
|
|
return {
|
|
mutation: false,
|
|
adapters: {
|
|
ssh: {
|
|
purpose: "Reach provider hosts through existing UniDesk Host SSH / WSL SSH maintenance bridge.",
|
|
allowedUse: ["readonly diagnostics", "bounded reviewed maintenance commands", "future approved recovery commands"],
|
|
},
|
|
pty: {
|
|
purpose: "Keep the host Codex interactive session alive and observable.",
|
|
allowedUse: ["windowed stdout/stderr capture", "prompt injection after policy checks", "heartbeat"],
|
|
},
|
|
stdio: {
|
|
purpose: "Bridge non-interactive Codex or helper subprocesses with bounded transcript capture.",
|
|
allowedUse: ["contracted command execution", "trace collection", "structured result capture"],
|
|
},
|
|
},
|
|
guardrails: [
|
|
"bounded output by default",
|
|
"redact secret-like values before state persistence",
|
|
"persist event sequence before acknowledging prompt injection",
|
|
"never open an interactive shell from dry-run commands",
|
|
],
|
|
};
|
|
}
|
|
|
|
function issueEntryPlan(): Record<string, unknown> {
|
|
return {
|
|
mutation: false,
|
|
issues: [
|
|
{
|
|
number: 20,
|
|
role: "Code Queue total board",
|
|
readEntrypoints: ["bun scripts/cli.ts gh issue board-audit --board-issue 20 --dry-run", "bun scripts/cli.ts gh issue board-row list --board-issue 20"],
|
|
writeEntrypoints: ["bun scripts/cli.ts gh issue board-row update <issueNumber> --board-issue 20 --field <field> --value <text> --expect-body-sha <sha>"],
|
|
},
|
|
{
|
|
number: 46,
|
|
role: "daily commander brief",
|
|
readEntrypoints: ["bun scripts/cli.ts gh issue read 46 --json body,title,state,updatedAt"],
|
|
writeEntrypoints: ["bun scripts/cli.ts gh issue update 46 --body-profile commander-brief --body-file <file> --expect-updated-at <ts>"],
|
|
},
|
|
],
|
|
writeGuards: ["body-file-only", "dry-run-first", "body-sha-or-updated-at-required", "no-token-output"],
|
|
};
|
|
}
|
|
|
|
function prCloseoutPlan(): Record<string, unknown> {
|
|
return {
|
|
mutation: false,
|
|
purpose: "PR closeout checklist for commander sessions and PR-bound GPT-5.5 runners; this plan is read-only and does not merge or close anything.",
|
|
runnerBoundary: {
|
|
mayCreateUpdateComment: true,
|
|
maySelfCloseOrMergeOrdinaryPrWithinTaskBoundary: true,
|
|
conditions: [
|
|
"task explicitly authorizes PR closeout or merge/close",
|
|
"PR is ordinary UniDesk source work, not production, release/v1, destructive rollback, secret, database, or runtime hot patch",
|
|
"checks required by the task have passed or unrelated broad failures are documented",
|
|
"base/head are reviewed and merge/close target matches the task boundary",
|
|
],
|
|
commanderRequiredWhen: [
|
|
"conflicts or ambiguous ownership",
|
|
"failed required checks without accepted explanation",
|
|
"production/runtime/release/security/database scope",
|
|
"user or commander explicitly reserves final action",
|
|
],
|
|
},
|
|
readOnlyChecklist: [
|
|
{
|
|
actor: "runner-or-host",
|
|
command: "bun scripts/cli.ts gh pr view <number> --repo pikasTech/unidesk --json body,title,state,stateDetail,head,base,headRefName,baseRefName,mergeable,mergeStateStatus,statusCheckRollup",
|
|
mutation: false,
|
|
},
|
|
{
|
|
actor: "runner-or-host",
|
|
command: "bun scripts/cli.ts gh pr files <number> --repo pikasTech/unidesk --limit 30",
|
|
mutation: false,
|
|
},
|
|
{
|
|
actor: "runner-or-host",
|
|
command: "comment or hand off changed files, validation, residual risks, and read-only mergeability/status evidence",
|
|
mutation: false,
|
|
},
|
|
],
|
|
finalAction: {
|
|
ordinaryRunnerAllowed: true,
|
|
hostCommanderAllowedAfterReview: true,
|
|
tools: [
|
|
"bun scripts/cli.ts gh pr merge <number> --repo pikasTech/unidesk",
|
|
"GitHub UI merge/close controls",
|
|
"bun scripts/cli.ts gh pr close <number> --repo pikasTech/unidesk",
|
|
],
|
|
sourceMergeClosePolicy: "Use repo-owned, auditable GitHub paths; do not directly push target branches as a merge substitute.",
|
|
},
|
|
unideskCliBoundary: {
|
|
mergeSupported: true,
|
|
closeSupported: true,
|
|
command: "bun scripts/cli.ts gh pr merge <number> --repo pikasTech/unidesk",
|
|
dryRunCommand: "bun scripts/cli.ts gh pr merge <number> --repo pikasTech/unidesk --dry-run",
|
|
preflightRequired: true,
|
|
automatedMergeImplemented: true,
|
|
},
|
|
};
|
|
}
|
|
|
|
function traceSummaryPlan(): Record<string, unknown> {
|
|
return {
|
|
mutation: false,
|
|
sources: [
|
|
"bounded host Codex PTY/stdio event window",
|
|
"Code Queue task trace via codex task --trace",
|
|
"Code Queue output pages via codex output",
|
|
"commander approval and prompt event JSONL",
|
|
],
|
|
summaryShape: {
|
|
taskId: "string|null",
|
|
sessionId: "string",
|
|
lastSeq: "number",
|
|
status: "running|attention_required|blocked|terminal|unknown",
|
|
keyEvents: "bounded array",
|
|
openQuestions: "bounded array",
|
|
recommendedNextActions: "bounded array",
|
|
redactionsApplied: "number",
|
|
},
|
|
outputPolicy: "Default summaries must omit raw transcript text unless explicitly requested by a future reviewed endpoint.",
|
|
};
|
|
}
|
|
|
|
function promptGuidancePlan(): Record<string, unknown> {
|
|
return {
|
|
mutation: false,
|
|
stages: [
|
|
"classify intent and risk",
|
|
"attach current #20/#46/task context summary",
|
|
"apply forbidden-action guard",
|
|
"redact secret-like strings",
|
|
"persist prompt plan",
|
|
"inject only through future live executor after policy checks",
|
|
],
|
|
highRiskEscalation: "code-queue restart/rebuild, task interrupt/cancel, production mutation, token access, and destructive Git operations require ClaudeQQ approval first.",
|
|
};
|
|
}
|
|
|
|
function commanderPlan(args: string[]): Record<string, unknown> {
|
|
if (!hasFlag(args, "--dry-run")) {
|
|
return {
|
|
ok: false,
|
|
error: "dry-run-required",
|
|
message: requiredDryRunMessage,
|
|
command: "bun scripts/cli.ts commander plan --dry-run",
|
|
};
|
|
}
|
|
const sessionId = optionValue(args, "--session-id") ?? "primary";
|
|
return {
|
|
ok: true,
|
|
phase: "source-contract",
|
|
mode: "dry-run",
|
|
mutation: false,
|
|
serviceId: "host-codex-commander",
|
|
processDiscovery: processDiscoveryPlan(sessionId),
|
|
bridge: bridgePlan(),
|
|
promptGuidance: promptGuidancePlan(),
|
|
traceSummary: traceSummaryPlan(),
|
|
issueEntries: issueEntryPlan(),
|
|
prCloseout: prCloseoutPlan(),
|
|
claudeqqApproval: {
|
|
mutation: false,
|
|
commandShape: "bun scripts/cli.ts commander approval request --action <action> --dry-run",
|
|
dryRunDraft: "Chinese plain-text ClaudeQQ approval draft, <=200 chars, no Markdown",
|
|
liveSendBlockedBy: "notification-path-unavailable",
|
|
backendCoreProxyOnly: "bun scripts/cli.ts microservice proxy claudeqq /api/push/text --method POST --body-json '<payload>' --raw",
|
|
highRiskActions,
|
|
},
|
|
safetyBoundary: safetyBoundary(),
|
|
};
|
|
}
|
|
|
|
function healthEndpointValidation(): Record<string, unknown> {
|
|
return {
|
|
surface: "health endpoint",
|
|
endpoint: "GET /health",
|
|
validationMethod: "invoke createCommanderRequestHandler with a temporary RuntimeConfig inside a short-lived contract test",
|
|
expectedEvidence: [
|
|
"body.ok=true",
|
|
"body.service=host-codex-commander",
|
|
"body.stateRoot points at the temp directory",
|
|
"body.currentLogFile is reported",
|
|
],
|
|
noRuntimeSideEffects: [
|
|
"do not run Bun.serve",
|
|
"do not publish or expose a port",
|
|
"do not restart any service",
|
|
],
|
|
};
|
|
}
|
|
|
|
function stateFileValidation(sessionId: string): Record<string, unknown> {
|
|
return {
|
|
surface: "state file",
|
|
storageRoot: ".state/commander/",
|
|
validationMethod: "write and read a session record only under a temporary directory owned by the contract test",
|
|
files: [
|
|
`sessions/${sessionId}.json`,
|
|
`events/${sessionId}.jsonl`,
|
|
"approvals/draft.json",
|
|
"logs/commander.jsonl",
|
|
],
|
|
expectedEvidence: [
|
|
"session state round-trips through writeCommanderSession/readCommanderSession",
|
|
"secret-like notes are redacted before persistence",
|
|
"temporary state root is deleted after the smoke contract",
|
|
],
|
|
noRuntimeSideEffects: [
|
|
"do not touch the live .state/commander directory",
|
|
"do not patch database state",
|
|
"do not discover or signal live host Codex processes",
|
|
],
|
|
};
|
|
}
|
|
|
|
function traceSummaryValidation(sessionId: string): Record<string, unknown> {
|
|
return {
|
|
surface: "trace summary dry-run",
|
|
validationMethod: "summarize mock JSONL trace input through summarizeCommanderTrace",
|
|
inputPolicy: "bounded mock Code Queue trace JSONL only; no live codex task trace fetch",
|
|
expectedEvidence: [
|
|
"taskId and sessionId are preserved",
|
|
"lastSeq is computed from mock seq fields",
|
|
"status is derived without returning raw transcript",
|
|
"secret-like text is redacted",
|
|
],
|
|
sampleSessionId: sessionId,
|
|
noRuntimeSideEffects: [
|
|
"do not call Code Queue manager",
|
|
"do not mark tasks read",
|
|
"do not interrupt or cancel tasks",
|
|
],
|
|
};
|
|
}
|
|
|
|
function approvalDraftValidation(): Record<string, unknown> {
|
|
return {
|
|
surface: "approval draft preview",
|
|
validationMethod: "build a draft preview for one high-risk action with --dry-run",
|
|
commandShape: "bun scripts/cli.ts commander approval request --action code-queue-task-interrupt --reason <text> --dry-run",
|
|
expectedEvidence: [
|
|
"requiresExplicitUserApproval=true",
|
|
"claudeqq.mutation=false",
|
|
"claudeqq.sendImplemented=false",
|
|
"claudeqq.notificationDraft.message is <=200 Chinese chars and plain text",
|
|
"claudeqq.notificationPath.error=notification-path-unavailable",
|
|
"backend-core microservice proxy command is returned instead of a local skill/powershell command",
|
|
"reason and messageTemplate are redacted",
|
|
],
|
|
noRuntimeSideEffects: [
|
|
"do not POST to ClaudeQQ",
|
|
"do not notify any QQ user or group",
|
|
"do not record an approval as consumed",
|
|
],
|
|
};
|
|
}
|
|
|
|
function sshBridgeBoundaryValidation(): Record<string, unknown> {
|
|
return {
|
|
surface: "SSH bridge boundary",
|
|
validationMethod: "assert contract-only bridge metadata from commander plan --dry-run",
|
|
allowedAtThisStage: [
|
|
"readonly contract description",
|
|
"future reviewed maintenance command shape",
|
|
"explicit approval precondition text",
|
|
],
|
|
noRuntimeSideEffects: [
|
|
"do not open provider SSH session",
|
|
"do not open PTY bridge",
|
|
"do not open stdio bridge",
|
|
"do not inject prompt",
|
|
"do not restart services",
|
|
"do not interrupt or cancel tasks",
|
|
],
|
|
expectedEvidence: [
|
|
"bridge.mutation=false",
|
|
"startPlan.enabled=false",
|
|
"safetyBoundary.phaseOneMutationAllowed=false",
|
|
],
|
|
};
|
|
}
|
|
|
|
function commanderSmoke(args: string[]): Record<string, unknown> {
|
|
if (!hasFlag(args, "--dry-run")) {
|
|
return {
|
|
ok: false,
|
|
error: "dry-run-required",
|
|
message: requiredDryRunMessage,
|
|
command: "bun scripts/cli.ts commander smoke --dry-run",
|
|
};
|
|
}
|
|
const sessionId = optionValue(args, "--session-id") ?? "primary";
|
|
return {
|
|
ok: true,
|
|
phase: "source-contract",
|
|
mode: "dry-run",
|
|
mutation: false,
|
|
serviceId: "host-codex-commander",
|
|
noDaemonSmokeContract: {
|
|
startsDaemon: false,
|
|
startsPtyBridge: false,
|
|
startsStdioBridge: false,
|
|
opensSshBridge: false,
|
|
sendsClaudeqq: false,
|
|
restartsServices: false,
|
|
interruptsTasks: false,
|
|
cancelsTasks: false,
|
|
deploys: false,
|
|
runsFullCheckOrE2e: false,
|
|
allowedCommands: [
|
|
"bun scripts/cli.ts commander contract",
|
|
"bun scripts/cli.ts commander plan --dry-run",
|
|
"bun scripts/cli.ts commander smoke --dry-run",
|
|
"bun scripts/cli.ts commander approval request --action <action> --dry-run",
|
|
"bun scripts/host-codex-commander-no-daemon-smoke-contract-test.ts",
|
|
],
|
|
},
|
|
validationPlan: [
|
|
healthEndpointValidation(),
|
|
stateFileValidation(sessionId),
|
|
traceSummaryValidation(sessionId),
|
|
approvalDraftValidation(),
|
|
sshBridgeBoundaryValidation(),
|
|
],
|
|
manualAuthorizationBeforeLiveRuntime: [
|
|
"operator explicitly names the exact live action and target session/task/service",
|
|
"current source-contract smoke and skeleton contract tests are green",
|
|
"risk review confirms no token output, no direct database patch, and no backend restart bypass",
|
|
"ClaudeQQ approval draft is reviewed, any authorized send uses backend-core /api/microservices/claudeqq/proxy, and the reply is matched to an explicit approval id",
|
|
"rollback and observation steps are written before enabling any daemon or bridge",
|
|
],
|
|
};
|
|
}
|
|
|
|
function commanderApprovalRequest(args: string[]): Record<string, unknown> {
|
|
if (!hasFlag(args, "--dry-run")) {
|
|
return {
|
|
ok: false,
|
|
error: "dry-run-required",
|
|
message: requiredDryRunMessage,
|
|
command: "bun scripts/cli.ts commander approval request --action <action> --dry-run",
|
|
};
|
|
}
|
|
const action = optionValue(args, "--action");
|
|
if (action === undefined) {
|
|
return {
|
|
ok: false,
|
|
error: "validation-failed",
|
|
message: "--action is required",
|
|
highRiskActions,
|
|
};
|
|
}
|
|
if (!isHighRiskAction(action)) {
|
|
return {
|
|
ok: false,
|
|
error: "validation-failed",
|
|
message: `unsupported high-risk action: ${action}`,
|
|
highRiskActions,
|
|
};
|
|
}
|
|
const rawReason = optionValue(args, "--reason") ?? "operator-supplied reason required before live execution";
|
|
const reason = redactText(rawReason);
|
|
const taskId = optionValue(args, "--task-id") ?? null;
|
|
const notificationDraft = buildCommanderApprovalNotificationDraft({ action, taskId, reason: rawReason });
|
|
const notificationPath = commanderApprovalNotificationPathUnavailable(notificationDraft.message);
|
|
return {
|
|
ok: true,
|
|
phase: "source-contract",
|
|
mode: "dry-run",
|
|
mutation: false,
|
|
action,
|
|
taskId,
|
|
reason: reason.text,
|
|
redactionsApplied: reason.redactionsApplied,
|
|
requiresExplicitUserApproval: true,
|
|
notificationDraft,
|
|
claudeqq: {
|
|
mutation: false,
|
|
endpointShape: `POST ${notificationPath.servicePath}`,
|
|
fallbackEndpointShape: `POST ${notificationPath.fallbackServicePath}`,
|
|
target: "configured primary user private chat",
|
|
messageTemplate: notificationDraft.message,
|
|
sendImplemented: false,
|
|
dryRunNoClaudeQqSend: true,
|
|
notificationDraft,
|
|
notificationPath,
|
|
noLocalSkillFallback: true,
|
|
},
|
|
approvalRecordShape: {
|
|
id: "commander-approval-<stable-id>",
|
|
action,
|
|
taskId,
|
|
reason: reason.text,
|
|
status: "draft",
|
|
approvedBy: null,
|
|
approvedAt: null,
|
|
expiresAt: "future reviewed timeout",
|
|
},
|
|
blockedUntilApproved: [action],
|
|
};
|
|
}
|
|
|
|
export function runCommanderCommand(args: string[]): Record<string, unknown> {
|
|
const [sub, second] = args;
|
|
if (sub === undefined || isHelpToken(sub)) return commanderHelp();
|
|
if (sub === "contract") return commanderContract();
|
|
if (sub === "plan") return commanderPlan(args.slice(1));
|
|
if (sub === "smoke") return commanderSmoke(args.slice(1));
|
|
if (sub === "approval" && second === "request") return commanderApprovalRequest(args.slice(2));
|
|
if (sub === "prompt-lint") return runCommanderPromptLintCommand(args.slice(1)) as unknown as Record<string, unknown>;
|
|
return {
|
|
ok: false,
|
|
error: "unsupported-command",
|
|
message: `Unsupported commander command: ${args.join(" ")}`,
|
|
help: commanderHelp(),
|
|
};
|
|
}
|