fix(commander): add ClaudeQQ approval proxy draft path (#134)
Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
@@ -21,12 +21,12 @@ function asRecord(value: unknown, path: string): JsonRecord {
|
||||
|
||||
function asArray(value: unknown, path: string): unknown[] {
|
||||
assertCondition(Array.isArray(value), `${path} must be an array`);
|
||||
return value;
|
||||
return value as unknown[];
|
||||
}
|
||||
|
||||
function stringField(value: unknown, path: string): string {
|
||||
assertCondition(typeof value === "string" && value.length > 0, `${path} must be a non-empty string`);
|
||||
return value;
|
||||
return value as string;
|
||||
}
|
||||
|
||||
function serviceById(environment: JsonRecord, id: string, path: string): JsonRecord {
|
||||
|
||||
@@ -656,7 +656,7 @@ async function main(): Promise<void> {
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}), "github transient full record");
|
||||
}));
|
||||
const githubTransientPreflight = asRecord(githubTransientFullRecord.preflight);
|
||||
assertCondition(githubTransientPreflight.retryable === true, "GitHub transient full preflight should be retryable", githubTransientPreflight);
|
||||
assertCondition(githubTransientPreflight.commanderAction === "retry-backoff-or-keep-running-if-heartbeat-fresh", "GitHub transient full preflight should expose bounded commander action", githubTransientPreflight);
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { commanderApprovalProxyFailureSummary } from "../src/components/microservices/host-codex-commander/src/approval-notification";
|
||||
|
||||
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 runCli(args: string[], expectStatus: number, extraEnv: Record<string, string> = {}): JsonRecord {
|
||||
const result = spawnSync(process.execPath, ["scripts/cli.ts", ...args], {
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...process.env,
|
||||
...extraEnv,
|
||||
},
|
||||
maxBuffer: 4 * 1024 * 1024,
|
||||
});
|
||||
assertCondition(result.status === expectStatus, `status mismatch for ${args.join(" ")}`, {
|
||||
status: result.status,
|
||||
stdout: result.stdout.slice(-2000),
|
||||
stderr: result.stderr.slice(-2000),
|
||||
});
|
||||
assertCondition(result.stdout.trim().length > 0, `command produced no stdout: ${args.join(" ")}`);
|
||||
return asRecord(JSON.parse(result.stdout) as unknown, "cli envelope");
|
||||
}
|
||||
|
||||
function dataOf(envelope: JsonRecord): JsonRecord {
|
||||
return asRecord(envelope.data, "data");
|
||||
}
|
||||
|
||||
function charLength(value: string): number {
|
||||
return Array.from(value).length;
|
||||
}
|
||||
|
||||
function assertPlainApprovalMessage(message: string): void {
|
||||
assertCondition(charLength(message) <= 200, "approval notification draft must be <=200 chars", { message, chars: charLength(message) });
|
||||
assertCondition(!/[`*_#[\]|]/u.test(message), "approval notification draft must not contain Markdown syntax", { message });
|
||||
assertCondition(!message.includes("\n"), "approval notification draft must be one paragraph", { message });
|
||||
}
|
||||
|
||||
const approval = dataOf(runCli([
|
||||
"commander",
|
||||
"approval",
|
||||
"request",
|
||||
"--action",
|
||||
"code-queue-task-interrupt",
|
||||
"--task-id",
|
||||
"stale-active-118",
|
||||
"--reason",
|
||||
"stale-active 恢复需要 interrupt;token=ghp_1234567890abcdef;不要使用 `local` 路径",
|
||||
"--dry-run",
|
||||
], 0, {
|
||||
PATH: "/usr/bin:/bin",
|
||||
}));
|
||||
|
||||
assertCondition(approval.ok === true, "approval dry-run must succeed without local powershell", approval);
|
||||
assertCondition(approval.mutation === false, "approval dry-run must be non-mutating", approval);
|
||||
assertCondition(!JSON.stringify(approval).includes("ghp_1234567890abcdef"), "approval dry-run must redact secret-like reason", approval);
|
||||
|
||||
const claudeqq = asRecord(approval.claudeqq, "claudeqq");
|
||||
assertCondition(claudeqq.mutation === false, "ClaudeQQ dry-run must not mutate", claudeqq);
|
||||
assertCondition(claudeqq.sendImplemented === false, "commander skeleton must not claim send implementation", claudeqq);
|
||||
assertCondition(claudeqq.dryRunNoClaudeQqSend === true, "dry-run must explicitly report no ClaudeQQ send", claudeqq);
|
||||
|
||||
const draft = asRecord(claudeqq.notificationDraft, "claudeqq.notificationDraft");
|
||||
assertCondition(draft.format === "plain-text", "approval draft must be plain text", draft);
|
||||
assertCondition(draft.markdownAllowed === false, "approval draft must forbid Markdown", draft);
|
||||
assertCondition(draft.containsMarkdownSyntax === false, "approval draft must report no Markdown syntax", draft);
|
||||
assertPlainApprovalMessage(String(draft.message));
|
||||
|
||||
const path = asRecord(claudeqq.notificationPath, "claudeqq.notificationPath");
|
||||
assertCondition(path.error === "notification-path-unavailable", "dry-run must expose notification-path-unavailable blocker", path);
|
||||
assertCondition(path.servicePath === "/api/microservices/claudeqq/proxy/api/push/text", "service path must be backend-core ClaudeQQ proxy", path);
|
||||
assertCondition(String(path.backendCoreProxyCommand).includes("bun scripts/cli.ts microservice proxy claudeqq /api/push/text --method POST"), "backend-core proxy command must be returned", path);
|
||||
assertCondition(!String(path.backendCoreProxyCommand).includes("powershell"), "backend-core proxy command must not use local powershell", path);
|
||||
assertCondition(!String(path.backendCoreProxyCommand).includes(".agents/skills/claudeqq"), "backend-core proxy command must not use local skill path", path);
|
||||
assertCondition(path.timeoutMs === 15000, "proxy path must disclose timeout", path);
|
||||
|
||||
const failure = commanderApprovalProxyFailureSummary({
|
||||
ok: false,
|
||||
status: 503,
|
||||
error: "microservice proxy task failed",
|
||||
stderrTail: "token=ghp_1234567890abcdef powershell.exe failed",
|
||||
});
|
||||
assertCondition(failure.ok === false, "proxy failure summary must be failing", failure);
|
||||
assertCondition(failure.error === "notification-proxy-failed", "proxy failure summary must be structured", failure);
|
||||
assertCondition(failure.degradedReason === "microservice-proxy-failed", "proxy failure degraded reason must be microservice-proxy-failed", failure);
|
||||
assertCondition(asRecord(failure.rollback, "rollback").issueUpdateRolledBack === false, "proxy failure must not imply issue rollback", failure);
|
||||
assertCondition(!JSON.stringify(failure).includes("ghp_1234567890abcdef"), "proxy failure summary must redact secrets", failure);
|
||||
|
||||
const tmp = mkdtempSync(join(tmpdir(), "unidesk-gh-brief-notify-"));
|
||||
try {
|
||||
const bodyPath = join(tmp, "brief.md");
|
||||
writeFileSync(bodyPath, [
|
||||
"# 指挥简报",
|
||||
"",
|
||||
"## 常驻观察与长期建议",
|
||||
"",
|
||||
"- 保持监督。",
|
||||
"",
|
||||
"## 更新 2026-05-23 10:00 北京时间",
|
||||
"",
|
||||
"- 新增高风险审批路径 dry-run 预览。",
|
||||
"",
|
||||
].join("\n"), "utf8");
|
||||
const ghDryRun = dataOf(runCli([
|
||||
"gh",
|
||||
"issue",
|
||||
"edit",
|
||||
"24",
|
||||
"--body-file",
|
||||
bodyPath,
|
||||
"--notify-claudeqq-brief-diff",
|
||||
"--dry-run",
|
||||
], 0, {
|
||||
UNIDESK_COMMANDER_BRIEF_CLAUDEQQ_BASE_URL: "http://127.0.0.1:9082",
|
||||
}));
|
||||
const notification = asRecord(ghDryRun.commanderBriefNotification, "commanderBriefNotification");
|
||||
const ghClaudeqq = asRecord(notification.claudeqq, "commanderBriefNotification.claudeqq");
|
||||
assertCondition(ghClaudeqq.wouldSend === false, "dry-run helper must not allow non-proxy ClaudeQQ path", ghClaudeqq);
|
||||
assertCondition(ghClaudeqq.blockedReason === "notification-path-unavailable", "dry-run helper must structure non-proxy blocker", ghClaudeqq);
|
||||
assertCondition(String(ghClaudeqq.recommendedCommand).includes("microservice proxy claudeqq"), "dry-run helper must recommend repo-owned proxy command", ghClaudeqq);
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const hostDoc = readFileSync("docs/reference/host-codex-commander.md", "utf8");
|
||||
const supervisionDoc = readFileSync("docs/reference/code-queue-supervision.md", "utf8");
|
||||
for (const snippet of [
|
||||
"notification-path-unavailable",
|
||||
"microservice proxy claudeqq /api/push/text",
|
||||
"200 字以内中文纯文本",
|
||||
]) {
|
||||
assertCondition(hostDoc.includes(snippet), `host commander doc missing snippet: ${snippet}`);
|
||||
}
|
||||
for (const snippet of [
|
||||
"不超过 200 个中文字符",
|
||||
"不使用 Markdown 语法",
|
||||
"发送失败只记录到 #24 或对应 blocker issue",
|
||||
]) {
|
||||
assertCondition(supervisionDoc.includes(snippet), `code queue supervision doc missing snippet: ${snippet}`);
|
||||
}
|
||||
|
||||
process.stdout.write(`${JSON.stringify({
|
||||
ok: true,
|
||||
checks: [
|
||||
"commander approval dry-run survives without local powershell or ClaudeQQ skill server",
|
||||
"dry-run generates a <=200 char Chinese plain-text ClaudeQQ draft and does not send",
|
||||
"dry-run exposes notification-path-unavailable plus backend-core microservice proxy command",
|
||||
"microservice proxy failure summary is structured, redacted, and best-effort",
|
||||
"legacy gh issue edit notify dry-run rejects non-proxy ClaudeQQ base URLs",
|
||||
"reference docs state the high-risk ClaudeQQ approval notification path",
|
||||
],
|
||||
}, null, 2)}\n`);
|
||||
@@ -71,7 +71,9 @@ try {
|
||||
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 sessionPreview = asRecord(commanderSessionPreview(session), "session preview");
|
||||
const sessionPreviewNotes = Array.isArray(sessionPreview.notes) ? sessionPreview.notes.map((item) => String(item)) : [];
|
||||
assertCondition(sessionPreviewNotes.includes("<redacted>"), "session preview must redact notes", sessionPreview);
|
||||
|
||||
const trace = summarizeCommanderTrace({
|
||||
taskId: "task-123",
|
||||
@@ -117,7 +119,7 @@ try {
|
||||
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);
|
||||
assertCondition(traceBody.ok === true && Number(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",
|
||||
|
||||
@@ -25,7 +25,7 @@ function asRecord(value: unknown, label: string): JsonRecord {
|
||||
|
||||
function asArray(value: unknown, label: string): unknown[] {
|
||||
assertCondition(Array.isArray(value), `${label} must be an array`, value);
|
||||
return value;
|
||||
return value as unknown[];
|
||||
}
|
||||
|
||||
interface ContractCase {
|
||||
|
||||
@@ -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