fix(commander): add ClaudeQQ approval proxy draft path (#134)
Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
@@ -2813,7 +2813,7 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti
|
||||
const drifts = deployJsonService !== null && hasDeployJsonExecutorContract(deployJsonService)
|
||||
? compareDeployJsonExecutorMirrors(deployJsonService, environment, artifactRegistryDeployJsonMirrors(effectiveOptions, spec, target))
|
||||
: [];
|
||||
if (drifts.length > 0) return deployJsonDriftResult(deployJsonService, environment, drifts);
|
||||
if (deployJsonService !== null && drifts.length > 0) return deployJsonDriftResult(deployJsonService, environment, drifts);
|
||||
const verificationBlocked = spec.runtimeVerification === "blocked";
|
||||
const livePolicy = artifactConsumerLivePolicy(spec, environment);
|
||||
const liveReason = artifactConsumerLiveBlockReason(spec, environment);
|
||||
|
||||
+20
-22
@@ -1,4 +1,6 @@
|
||||
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";
|
||||
|
||||
const requiredDryRunMessage = "This host Codex commander skeleton only supports dry-run planning; live daemon/control operations are not implemented.";
|
||||
|
||||
@@ -24,24 +26,6 @@ function isHighRiskAction(value: string): value is HighRiskAction {
|
||||
return highRiskActions.some((action) => action === value);
|
||||
}
|
||||
|
||||
function redactText(value: string): { text: string; redactionsApplied: number } {
|
||||
let redactionsApplied = 0;
|
||||
const patterns = [
|
||||
/\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) {
|
||||
text = text.replace(pattern, () => {
|
||||
redactionsApplied += 1;
|
||||
return "<redacted>";
|
||||
});
|
||||
}
|
||||
return { text, redactionsApplied };
|
||||
}
|
||||
|
||||
function commanderHelp(): Record<string, unknown> {
|
||||
return {
|
||||
command: "commander",
|
||||
@@ -59,7 +43,7 @@ function commanderHelp(): Record<string, unknown> {
|
||||
}
|
||||
|
||||
export function commanderContract(): Record<string, unknown> {
|
||||
return hostCommanderContract();
|
||||
return hostCommanderContract() as unknown as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function safetyBoundary(): Record<string, unknown> {
|
||||
@@ -264,6 +248,9 @@ function commanderPlan(args: string[]): Record<string, unknown> {
|
||||
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(),
|
||||
@@ -342,6 +329,9 @@ function approvalDraftValidation(): Record<string, unknown> {
|
||||
"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: [
|
||||
@@ -423,7 +413,7 @@ function commanderSmoke(args: string[]): Record<string, unknown> {
|
||||
"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, sent by an authorized future path, and matched to an explicit approval id",
|
||||
"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",
|
||||
],
|
||||
};
|
||||
@@ -458,6 +448,8 @@ function commanderApprovalRequest(args: string[]): Record<string, unknown> {
|
||||
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",
|
||||
@@ -468,12 +460,18 @@ function commanderApprovalRequest(args: string[]): Record<string, unknown> {
|
||||
reason: reason.text,
|
||||
redactionsApplied: reason.redactionsApplied,
|
||||
requiresExplicitUserApproval: true,
|
||||
notificationDraft,
|
||||
claudeqq: {
|
||||
mutation: false,
|
||||
endpointShape: "POST /api/microservices/claudeqq/proxy/api/push/text",
|
||||
endpointShape: `POST ${notificationPath.servicePath}`,
|
||||
fallbackEndpointShape: `POST ${notificationPath.fallbackServicePath}`,
|
||||
target: "configured primary user private chat",
|
||||
messageTemplate: `Approval required for ${action}. Reason: ${reason.text}. Reply with explicit approval id before execution.`,
|
||||
messageTemplate: notificationDraft.message,
|
||||
sendImplemented: false,
|
||||
dryRunNoClaudeQqSend: true,
|
||||
notificationDraft,
|
||||
notificationPath,
|
||||
noLocalSkillFallback: true,
|
||||
},
|
||||
approvalRecordShape: {
|
||||
id: "commander-approval-<stable-id>",
|
||||
|
||||
+22
-31
@@ -1314,41 +1314,23 @@ async function sendClaudeQqEndpoint(config: ClaudeQqConfig, endpoint: string, pa
|
||||
method: "POST",
|
||||
body: payload,
|
||||
maxResponseBytes: 240_000,
|
||||
timeoutMs: config.timeoutMs,
|
||||
});
|
||||
if (claudeQqResponseOk(response)) return { ok: true, endpoint, status: isRecord(response) && typeof response.status === "number" ? response.status : 200, response };
|
||||
return summarizeClaudeQqProxyFailure(response, endpoint);
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), config.timeoutMs);
|
||||
try {
|
||||
const response = await fetch(normalizeClaudeQqEndpoint(config.baseUrl, endpoint), {
|
||||
method: "POST",
|
||||
signal: controller.signal,
|
||||
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const parsed = await parseGitHubResponse(response);
|
||||
const bodyFailed = isRecord(parsed) && (parsed.ok === false || parsed.success === false);
|
||||
if (response.ok && !bodyFailed) return { ok: true, endpoint, status: response.status, response: parsed };
|
||||
return {
|
||||
ok: false,
|
||||
endpoint,
|
||||
status: response.status,
|
||||
degradedReason: bodyFailed ? "upstream-rejected" : "http-failed",
|
||||
message: isRecord(parsed) && typeof parsed.error === "string" ? parsed.error : response.statusText,
|
||||
response: sanitizedErrorDetails(parsed),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
endpoint,
|
||||
degradedReason: "network-proxy-failed",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
endpoint,
|
||||
degradedReason: "notification-path-unavailable",
|
||||
message: "ClaudeQQ notifications must use backend-core /api/microservices/claudeqq/proxy; local skill, powershell, direct host, and ad-hoc ClaudeQQ URLs are not supported.",
|
||||
response: {
|
||||
baseUrl: sanitizeUrlForOutput(config.baseUrl),
|
||||
requiredBaseUrl: DEFAULT_COMMANDER_BRIEF_CLAUDEQQ_BASE_URL,
|
||||
recommendedCommand: "bun scripts/cli.ts microservice proxy claudeqq /api/push/text --method POST --body-json '<payload>' --raw",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function sendCommanderBriefClaudeQq(config: ClaudeQqConfig, message: string): Promise<ClaudeQqSendResult> {
|
||||
@@ -1377,6 +1359,7 @@ async function sendCommanderBriefClaudeQq(config: ClaudeQqConfig, message: strin
|
||||
}
|
||||
|
||||
function commanderBriefNotificationPlan(issueNumber: number, body: string, diff: CommanderBriefDiff, config: ClaudeQqConfig): Record<string, unknown> {
|
||||
const proxyBasePath = proxiedClaudeQqBasePath(config.baseUrl);
|
||||
return {
|
||||
enabled: config.enabled,
|
||||
issueNumber,
|
||||
@@ -1395,10 +1378,18 @@ function commanderBriefNotificationPlan(issueNumber: number, body: string, diff:
|
||||
},
|
||||
claudeqq: {
|
||||
dryRun: true,
|
||||
wouldSend: config.enabled && diff.ok,
|
||||
wouldSend: config.enabled && diff.ok && proxyBasePath !== null,
|
||||
baseUrl: sanitizeUrlForOutput(config.baseUrl),
|
||||
proxyBasePath,
|
||||
servicePath: proxyBasePath === null ? null : normalizeClaudeQqEndpoint(proxyBasePath, "/api/push/text"),
|
||||
target: maskedTarget(config),
|
||||
timeoutMs: config.timeoutMs,
|
||||
...(proxyBasePath === null
|
||||
? {
|
||||
blockedReason: "notification-path-unavailable",
|
||||
recommendedCommand: "bun scripts/cli.ts microservice proxy claudeqq /api/push/text --method POST --body-json '<payload>' --raw",
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
+4
-3
@@ -47,7 +47,7 @@ export function rootHelp(): unknown {
|
||||
{ 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: "auth-broker contract|health --dry-run|credential-request --dry-run|pr-preflight --dry-run", description: "Inspect the P0 Rust auth broker and CLI adapter contract without reading token values, writing GitHub, or starting services." },
|
||||
{ command: "gh preflight|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, PR closeout preflight, hard delete unsupported, and merge blocked." },
|
||||
{ command: "commander contract|plan --dry-run|smoke --dry-run|approval request --dry-run", description: "Host Codex commander skeleton contract, no-daemon smoke plan, and dry-run preview; exposes local health, state, trace summary, and approval draft helpers without live bridges or message sends." },
|
||||
{ command: "commander contract|plan --dry-run|smoke --dry-run|approval request --dry-run", description: "Host Codex commander skeleton contract, no-daemon smoke plan, and dry-run approval preview; generates <=200 char Chinese ClaudeQQ drafts plus backend-core proxy command 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." },
|
||||
@@ -219,11 +219,12 @@ function commanderHelp(): unknown {
|
||||
"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]",
|
||||
],
|
||||
description: "Inspect the local host Codex commander skeleton contract, dry-run planner, no-daemon smoke validation plan, state helpers, trace summary aggregator, and approval draft preview.",
|
||||
description: "Inspect the local host Codex commander skeleton contract, dry-run planner, no-daemon smoke validation plan, state helpers, trace summary aggregator, and approval draft preview with a backend-core ClaudeQQ proxy command.",
|
||||
boundary: [
|
||||
"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",
|
||||
"high-risk actions only produce a <=200 char Chinese ClaudeQQ approval draft and notification-path-unavailable blocker",
|
||||
"authorized future sends must use backend-core /api/microservices/claudeqq/proxy, not local skill or powershell paths",
|
||||
"token and secret values must never be printed",
|
||||
],
|
||||
reference: "docs/reference/host-codex-commander.md",
|
||||
|
||||
@@ -142,10 +142,10 @@ function backendCoreContainerMissing(stderr: string): boolean {
|
||||
return stderr.includes("No such container: unidesk-backend-core");
|
||||
}
|
||||
|
||||
export function coreInternalFetch(path: string, init?: { method?: string; body?: unknown; maxResponseBytes?: number }): unknown {
|
||||
export function coreInternalFetch(path: string, init?: { method?: string; body?: unknown; maxResponseBytes?: number; timeoutMs?: number }): unknown {
|
||||
if (!path.startsWith("/")) throw new Error("core internal path must start with /");
|
||||
const command = dockerCoreFetchCommand(path, init);
|
||||
const result = runCommand(command, repoRoot);
|
||||
const result = runCommand(command, repoRoot, { timeoutMs: init?.timeoutMs });
|
||||
if (result.exitCode !== 0) {
|
||||
if (backendCoreContainerMissing(result.stderr)) {
|
||||
const envPath = join(repoRoot, ".state", "docker-compose.env");
|
||||
@@ -170,6 +170,7 @@ export function coreInternalFetch(path: string, init?: { method?: string; body?:
|
||||
return {
|
||||
ok: false,
|
||||
exitCode: result.exitCode,
|
||||
timedOut: result.timedOut,
|
||||
stdoutTail: result.stdout.slice(-1200),
|
||||
stderrTail: result.stderr.slice(-1200),
|
||||
...(codeQueueStableProxy ? {
|
||||
|
||||
Reference in New Issue
Block a user