Files
pikasTech-unidesk/scripts/src/commander.ts
T

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(),
};
}