feat: 封装NapCat Codex审计执行器
This commit is contained in:
parent
99408d3cec
commit
3f7ed99aec
@ -1,4 +1,4 @@
|
|||||||
import { mkdirSync, writeFileSync } from "node:fs";
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -41,6 +41,7 @@ import {
|
|||||||
upstreamAuditOutputSchema,
|
upstreamAuditOutputSchema,
|
||||||
} from "./tools/napcatAutomation.types.js";
|
} from "./tools/napcatAutomation.types.js";
|
||||||
import {
|
import {
|
||||||
|
buildNapcatCodexExecCommand,
|
||||||
buildNapcatUpstreamAudit,
|
buildNapcatUpstreamAudit,
|
||||||
classifyNapcatUpstreamAudit,
|
classifyNapcatUpstreamAudit,
|
||||||
} from "./tools/napcatAutomation.js";
|
} from "./tools/napcatAutomation.js";
|
||||||
@ -119,6 +120,66 @@ export async function runSelfTest(): Promise<void> {
|
|||||||
"NapCat upstream audit dry-run artifact plan self-check failed",
|
"NapCat upstream audit dry-run artifact plan self-check failed",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const codexCommand = buildNapcatCodexExecCommand({
|
||||||
|
artifactRoot: "/vol1/docker/kt-codex/artifacts/smoke",
|
||||||
|
contextPacketPath:
|
||||||
|
"/vol1/docker/kt-codex/artifacts/smoke/context-packet.md",
|
||||||
|
outputSchemaPath:
|
||||||
|
"/vol1/docker/kt-codex/workspace/KT/mcp/ktWorkflow/prompts/napcat/schemas/upstream-audit.schema.json",
|
||||||
|
promptName: "upstream-audit",
|
||||||
|
workspaceRoot: "/vol1/docker/kt-codex/workspace/KT",
|
||||||
|
});
|
||||||
|
if (!codexCommand.includes("--ask-for-approval never")) {
|
||||||
|
throw new Error("NapCat Codex runner approval policy self-check failed");
|
||||||
|
}
|
||||||
|
for (const expected of [
|
||||||
|
"CODEX_HOME='/vol1/docker/kt-codex/home/.codex'",
|
||||||
|
"--cd '/vol1/docker/kt-codex/workspace/KT'",
|
||||||
|
"--profile-v2 automation",
|
||||||
|
"--sandbox workspace-write",
|
||||||
|
"--json",
|
||||||
|
"--ephemeral",
|
||||||
|
"--output-schema '/vol1/docker/kt-codex/workspace/KT/mcp/ktWorkflow/prompts/napcat/schemas/upstream-audit.schema.json'",
|
||||||
|
"--output-last-message '/vol1/docker/kt-codex/artifacts/smoke/codex-upstream-audit.md'",
|
||||||
|
"\"$(cat '/vol1/docker/kt-codex/artifacts/smoke/context-packet.md')\"",
|
||||||
|
]) {
|
||||||
|
if (!codexCommand.includes(expected)) {
|
||||||
|
throw new Error(`NapCat Codex runner command missing: ${expected}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (codexCommand.includes("git push") || codexCommand.includes("kubectl")) {
|
||||||
|
throw new Error("NapCat Codex runner mutation guard self-check failed");
|
||||||
|
}
|
||||||
|
const codexExecutionArtifactRoot = resolveInsideRoot(
|
||||||
|
".kt-workspace/test-artifacts/napcat-codex-runner-self-test",
|
||||||
|
);
|
||||||
|
const codexInvalidSchemaAudit = buildNapcatUpstreamAudit({
|
||||||
|
artifactRoot: codexExecutionArtifactRoot,
|
||||||
|
codexExecutor: () => ({
|
||||||
|
ok: true,
|
||||||
|
stderr: "",
|
||||||
|
stdout: "{\"classification\":\"surprising\"}\n",
|
||||||
|
}),
|
||||||
|
dryMergeConflict: false,
|
||||||
|
execute: true,
|
||||||
|
forkPatchFiles: [],
|
||||||
|
upstreamChangedFiles: [],
|
||||||
|
useCodex: true,
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
codexInvalidSchemaAudit.classification !== "blocked" ||
|
||||||
|
!codexInvalidSchemaAudit.reasonCodes.includes("CODEX_SCHEMA_INVALID")
|
||||||
|
) {
|
||||||
|
throw new Error("NapCat Codex invalid schema fallback self-check failed");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!existsSync(
|
||||||
|
path.join(codexExecutionArtifactRoot, "codex-upstream-audit.jsonl"),
|
||||||
|
) ||
|
||||||
|
!existsSync(path.join(codexExecutionArtifactRoot, "context-packet.md"))
|
||||||
|
) {
|
||||||
|
throw new Error("NapCat Codex artifact capture self-check failed");
|
||||||
|
}
|
||||||
const dryRunAuditCommands: string[] = [];
|
const dryRunAuditCommands: string[] = [];
|
||||||
for (const command of dryRunAudit.commands) {
|
for (const command of dryRunAudit.commands) {
|
||||||
if (!command.readOnly) {
|
if (!command.readOnly) {
|
||||||
|
|||||||
@ -1,8 +1,18 @@
|
|||||||
|
import { execSync } from 'node:child_process';
|
||||||
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import { upstreamAuditOutputSchema } from './napcatAutomation.types.js';
|
||||||
|
import { loadNapcatAutomationPrompt } from './napcatAutomation.prompts.js';
|
||||||
import type {
|
import type {
|
||||||
NapcatAuditClassificationInput,
|
NapcatAuditClassificationInput,
|
||||||
NapcatAuditClassificationResult,
|
NapcatAuditClassificationResult,
|
||||||
NapcatAuditCommand,
|
NapcatAuditCommand,
|
||||||
NapcatAutomationReasonCode,
|
NapcatAutomationReasonCode,
|
||||||
|
NapcatCodexExecCommandInput,
|
||||||
|
NapcatCodexExecResult,
|
||||||
|
NapcatCodexExecutor,
|
||||||
|
NapcatUpstreamAuditInput,
|
||||||
NapcatUpstreamAuditResult,
|
NapcatUpstreamAuditResult,
|
||||||
} from './napcatAutomation.types.js';
|
} from './napcatAutomation.types.js';
|
||||||
|
|
||||||
@ -75,6 +85,330 @@ const hotZoneMatchers: Array<{ pattern: string; regex: RegExp }> = [
|
|||||||
{ pattern: 'vite*.ts', regex: /(?:^|\/)vite[^/]*\.ts$/i },
|
{ pattern: 'vite*.ts', regex: /(?:^|\/)vite[^/]*\.ts$/i },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Quotes a POSIX shell path argument for NAS-side Codex automation commands.
|
||||||
|
* @param value - Absolute or repo-relative filesystem path that will be interpreted by a Linux shell.
|
||||||
|
* @returns A single-quoted shell token with embedded single quotes escaped.
|
||||||
|
*/
|
||||||
|
function shellQuotePath(value: string): string {
|
||||||
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derives the default trusted Codex home from the NAS workspace root layout.
|
||||||
|
* @param workspaceRoot - Linux workspace root passed to `codex exec --cd`, usually `/vol1/docker/kt-codex/workspace/KT`.
|
||||||
|
* @returns The colocated Codex home path, or the stabilized NAS default when the workspace root does not match the expected layout.
|
||||||
|
*/
|
||||||
|
function deriveCodexHome(workspaceRoot: string): string {
|
||||||
|
const marker = '/workspace/';
|
||||||
|
const markerIndex = workspaceRoot.indexOf(marker);
|
||||||
|
if (markerIndex >= 0) {
|
||||||
|
return path.posix.join(workspaceRoot.slice(0, markerIndex), 'home/.codex');
|
||||||
|
}
|
||||||
|
return '/vol1/docker/kt-codex/home/.codex';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the bounded Codex CLI command used by NapCat upstream audit automation.
|
||||||
|
* @param input - Linux paths for artifact capture, the context packet prompt, JSON schema validation, and the trusted KT workspace root; `codexHome` may override the derived NAS default.
|
||||||
|
* @returns A POSIX shell command that runs `codex exec` with workspace-write sandboxing, never approval, JSONL output, and last-message artifact capture.
|
||||||
|
*/
|
||||||
|
export function buildNapcatCodexExecCommand(
|
||||||
|
input: NapcatCodexExecCommandInput,
|
||||||
|
): string {
|
||||||
|
const outputLastMessagePath = path.posix.join(
|
||||||
|
input.artifactRoot,
|
||||||
|
`codex-${input.promptName}.md`,
|
||||||
|
);
|
||||||
|
const codexHome = input.codexHome ?? deriveCodexHome(input.workspaceRoot);
|
||||||
|
|
||||||
|
return [
|
||||||
|
`CODEX_HOME=${shellQuotePath(codexHome)}`,
|
||||||
|
'codex exec',
|
||||||
|
`--cd ${shellQuotePath(input.workspaceRoot)}`,
|
||||||
|
'--profile-v2 automation',
|
||||||
|
'--sandbox workspace-write',
|
||||||
|
'--ask-for-approval never',
|
||||||
|
'--json',
|
||||||
|
'--ephemeral',
|
||||||
|
`--output-schema ${shellQuotePath(input.outputSchemaPath)}`,
|
||||||
|
`--output-last-message ${shellQuotePath(outputLastMessagePath)}`,
|
||||||
|
`"$(cat ${shellQuotePath(input.contextPacketPath)})"`,
|
||||||
|
].join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a POSIX-style artifact child path while preserving deterministic command text for NAS execution.
|
||||||
|
* @param artifactRoot - Artifact directory supplied by automation input or the default KT test-artifact root.
|
||||||
|
* @param fileName - File name to place directly under the artifact root.
|
||||||
|
* @returns A stable path string that can be used by both local self-tests and Linux-side command construction.
|
||||||
|
*/
|
||||||
|
function joinNapcatArtifactPath(artifactRoot: string, fileName: string): string {
|
||||||
|
return path.posix.join(artifactRoot, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the artifact root for dry-run and execute-mode NapCat upstream audit outputs.
|
||||||
|
* @param artifactRoot - Optional caller-provided artifact directory; when omitted the standard KT test-artifact folder is used.
|
||||||
|
* @returns A non-empty artifact root path.
|
||||||
|
*/
|
||||||
|
function resolveNapcatArtifactRoot(artifactRoot?: string): string {
|
||||||
|
return (
|
||||||
|
artifactRoot?.trim() ||
|
||||||
|
'.kt-workspace/test-artifacts/napcat-upstream-audit'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the Linux workspace root used by the NAS Codex automation runner.
|
||||||
|
* @param workspaceRoot - Optional trusted KT workspace root override.
|
||||||
|
* @returns The configured workspace root or the stabilized NAS kt-codex workspace path.
|
||||||
|
*/
|
||||||
|
function resolveNapcatCodexWorkspaceRoot(workspaceRoot?: string): string {
|
||||||
|
return workspaceRoot?.trim() || '/vol1/docker/kt-codex/workspace/KT';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the JSON schema path passed to `codex exec --output-schema`.
|
||||||
|
* @param input - Upstream audit input containing an optional schema path and workspace root.
|
||||||
|
* @returns The caller-provided schema path or the source-controlled upstream-audit schema under the trusted workspace root.
|
||||||
|
*/
|
||||||
|
function resolveNapcatCodexOutputSchemaPath(
|
||||||
|
input: NapcatUpstreamAuditInput,
|
||||||
|
): string {
|
||||||
|
if (input.outputSchemaPath?.trim()) {
|
||||||
|
return input.outputSchemaPath.trim();
|
||||||
|
}
|
||||||
|
return path.posix.join(
|
||||||
|
resolveNapcatCodexWorkspaceRoot(input.workspaceRoot),
|
||||||
|
'mcp/ktWorkflow/prompts/napcat/schemas/upstream-audit.schema.json',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a deterministic Markdown context packet for Codex upstream audit reasoning.
|
||||||
|
* @param input - NapCat upstream audit metadata and optional repo/ref hints; values are serialized as context only and must not be executed.
|
||||||
|
* @param classification - Deterministic local classifier output that Codex can compare against upstream risk evidence.
|
||||||
|
* @returns Markdown prompt content with the source-controlled prompt, sanitized metadata, and explicit no-mutation constraints.
|
||||||
|
*/
|
||||||
|
function buildNapcatCodexContextPacket(
|
||||||
|
input: NapcatUpstreamAuditInput,
|
||||||
|
classification: NapcatAuditClassificationResult,
|
||||||
|
): string {
|
||||||
|
const promptText = loadNapcatAutomationPrompt('upstream-audit').trim();
|
||||||
|
const context = {
|
||||||
|
classification,
|
||||||
|
constraints: {
|
||||||
|
forbiddenCommands: ['git push', 'kubectl', 'docker build', 'deploy'],
|
||||||
|
outputSchema: 'upstream-audit.schema.json',
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
createCandidateBranch: input.createCandidateBranch === true,
|
||||||
|
dryMergeConflict: input.dryMergeConflict,
|
||||||
|
forkBranch: input.forkBranch,
|
||||||
|
forkPatchFiles: input.forkPatchFiles,
|
||||||
|
forkRepo: redactUrlCredentials(input.forkRepo),
|
||||||
|
lastAcceptedUpstreamBase: input.lastAcceptedUpstreamBase,
|
||||||
|
upstreamChangedFiles: input.upstreamChangedFiles,
|
||||||
|
upstreamReleaseTag: input.upstreamReleaseTag,
|
||||||
|
upstreamRepo: redactUrlCredentials(input.upstreamRepo),
|
||||||
|
},
|
||||||
|
schemaVersion: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
return [
|
||||||
|
'# NapCat Upstream Audit Codex Context',
|
||||||
|
'',
|
||||||
|
'## Source Prompt',
|
||||||
|
'',
|
||||||
|
promptText,
|
||||||
|
'',
|
||||||
|
'## Context',
|
||||||
|
'',
|
||||||
|
'```json',
|
||||||
|
JSON.stringify(context, null, 2),
|
||||||
|
'```',
|
||||||
|
'',
|
||||||
|
'## Safety',
|
||||||
|
'',
|
||||||
|
'- Do not edit files.',
|
||||||
|
'- Do not run git push, kubectl, docker build, deploy, or release mutation commands.',
|
||||||
|
'- Return only JSON that matches the supplied output schema.',
|
||||||
|
'',
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redacts credentials from URL-shaped repository hints before they enter context artifacts.
|
||||||
|
* @param value - Optional repo URL or short repository identifier from automation input.
|
||||||
|
* @returns The original value when no URL credentials are present; otherwise the same URL with userinfo removed.
|
||||||
|
*/
|
||||||
|
function redactUrlCredentials(value?: string): string | undefined {
|
||||||
|
if (!value) return undefined;
|
||||||
|
return value.replace(/:\/\/[^/@\s]+@/g, '://');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the Codex CLI command with the task-required ten minute timeout.
|
||||||
|
* @param command - POSIX shell command produced by `buildNapcatCodexExecCommand`.
|
||||||
|
* @returns Captured stdout/stderr and an ok flag; failures are returned so callers can preserve artifacts and classify as blocked.
|
||||||
|
*/
|
||||||
|
function executeNapcatCodexCommand(command: string): NapcatCodexExecResult {
|
||||||
|
try {
|
||||||
|
const stdout = execSync(command, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
maxBuffer: 10 * 1024 * 1024,
|
||||||
|
shell: process.platform === 'win32' ? undefined : '/bin/bash',
|
||||||
|
timeout: 10 * 60 * 1000,
|
||||||
|
});
|
||||||
|
return { ok: true, stderr: '', stdout };
|
||||||
|
} catch (error) {
|
||||||
|
const execError = error as {
|
||||||
|
stderr?: Buffer | string;
|
||||||
|
stdout?: Buffer | string;
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
stderr: bufferLikeToString(execError.stderr),
|
||||||
|
stdout: bufferLikeToString(execError.stdout),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts optional exec output into UTF-8 text for artifact preservation.
|
||||||
|
* @param value - Buffer or string captured from a child-process result.
|
||||||
|
* @returns String output, or an empty string when the stream was not captured.
|
||||||
|
*/
|
||||||
|
function bufferLikeToString(value?: Buffer | string): string {
|
||||||
|
if (!value) return '';
|
||||||
|
return Buffer.isBuffer(value) ? value.toString('utf8') : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses Codex stdout and last-message content into the strict upstream audit schema.
|
||||||
|
* @param stdout - JSONL stdout emitted by `codex exec --json`; the final schema payload may be a raw line or nested text field.
|
||||||
|
* @param lastMessage - Optional content from `--output-last-message` when Codex wrote one.
|
||||||
|
* @returns Parsed classification result, or null when no schema-valid payload can be found.
|
||||||
|
*/
|
||||||
|
function parseNapcatCodexAuditOutput(
|
||||||
|
stdout: string,
|
||||||
|
lastMessage: string,
|
||||||
|
): NapcatAuditClassificationResult | null {
|
||||||
|
const candidates = [
|
||||||
|
...stdout
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.reverse(),
|
||||||
|
lastMessage,
|
||||||
|
stdout,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
const parsed = parseNapcatCodexAuditCandidate(candidate);
|
||||||
|
if (parsed) return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to parse one raw Codex output candidate, including nested string payloads.
|
||||||
|
* @param candidate - Raw JSON, JSONL line, markdown fenced JSON, or nested Codex event text.
|
||||||
|
* @returns A schema-valid upstream audit result, or null when the candidate does not contain one.
|
||||||
|
*/
|
||||||
|
function parseNapcatCodexAuditCandidate(
|
||||||
|
candidate: string,
|
||||||
|
): NapcatAuditClassificationResult | null {
|
||||||
|
const trimmed = candidate.trim();
|
||||||
|
if (!trimmed) return null;
|
||||||
|
|
||||||
|
for (const jsonText of collectJsonTextCandidates(trimmed)) {
|
||||||
|
try {
|
||||||
|
const value = JSON.parse(jsonText) as unknown;
|
||||||
|
const direct = upstreamAuditOutputSchema.safeParse(value);
|
||||||
|
if (direct.success) return direct.data;
|
||||||
|
|
||||||
|
const nested = parseNestedCodexAuditCandidate(value);
|
||||||
|
if (nested) return nested;
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts direct and fenced JSON snippets from Codex text output.
|
||||||
|
* @param value - Raw Codex event text or last-message Markdown.
|
||||||
|
* @returns Candidate JSON strings in most-specific-first order.
|
||||||
|
*/
|
||||||
|
function collectJsonTextCandidates(value: string): string[] {
|
||||||
|
const candidates = [value];
|
||||||
|
const fencedJson = value.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1]?.trim();
|
||||||
|
if (fencedJson) {
|
||||||
|
candidates.unshift(fencedJson);
|
||||||
|
}
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches common Codex JSONL event shapes for a nested schema payload string.
|
||||||
|
* @param value - Parsed JSONL event or raw nested JSON value from Codex stdout.
|
||||||
|
* @returns A schema-valid upstream audit result, or null when no nested payload matches.
|
||||||
|
*/
|
||||||
|
function parseNestedCodexAuditCandidate(
|
||||||
|
value: unknown,
|
||||||
|
): NapcatAuditClassificationResult | null {
|
||||||
|
if (!value || typeof value !== 'object') return null;
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
for (const item of [...value].reverse()) {
|
||||||
|
const parsed = parseNestedCodexAuditCandidate(item);
|
||||||
|
if (parsed) return parsed;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = value as Record<string, unknown>;
|
||||||
|
for (const key of ['content', 'message', 'output', 'text', 'lastMessage']) {
|
||||||
|
const nestedValue = record[key];
|
||||||
|
if (typeof nestedValue === 'string') {
|
||||||
|
const parsed = parseNapcatCodexAuditCandidate(nestedValue);
|
||||||
|
if (parsed) return parsed;
|
||||||
|
} else if (nestedValue && typeof nestedValue === 'object') {
|
||||||
|
const parsed = parseNestedCodexAuditCandidate(nestedValue);
|
||||||
|
if (parsed) return parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the blocked result used when Codex execution or schema parsing fails.
|
||||||
|
* @param artifacts - Artifact paths already written or reserved for the failed execution.
|
||||||
|
* @param commands - Command metadata returned to callers for auditability.
|
||||||
|
* @returns A blocked upstream audit result with the `CODEX_SCHEMA_INVALID` reason code.
|
||||||
|
*/
|
||||||
|
function buildCodexSchemaInvalidResult(
|
||||||
|
artifacts: NapcatUpstreamAuditResult['artifacts'],
|
||||||
|
commands: NapcatAuditCommand[],
|
||||||
|
): NapcatUpstreamAuditResult {
|
||||||
|
return {
|
||||||
|
artifacts,
|
||||||
|
classification: 'blocked',
|
||||||
|
commands,
|
||||||
|
execute: true,
|
||||||
|
hotZonePatterns: [...napcatUpstreamHotZonePatterns],
|
||||||
|
reasonCodes: ['CODEX_SCHEMA_INVALID'],
|
||||||
|
recommendedAction: 'block',
|
||||||
|
summary:
|
||||||
|
'Codex upstream audit execution did not produce schema-valid JSON; inspect context, JSONL stdout, and last-message artifacts before continuing.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether Git audit metadata contains a repo-relative path that the classifier can trust.
|
* Checks whether Git audit metadata contains a repo-relative path that the classifier can trust.
|
||||||
* @param value - Raw path string from fork/upstream Git diff collectors; empty entries are allowed because collectors may emit blank lines.
|
* @param value - Raw path string from fork/upstream Git diff collectors; empty entries are allowed because collectors may emit blank lines.
|
||||||
@ -289,14 +623,15 @@ export function classifyNapcatUpstreamAudit(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a read-only upstream audit plan for NapCatQQ release sync automation.
|
* Builds or executes a NapCatQQ upstream audit plan.
|
||||||
* @param input - Same deterministic classifier input supplied by the dry-run collector; values are embedded only in the returned classification, never executed.
|
* @param input - Deterministic collector metadata plus optional Codex automation settings; execute mode writes context/stdout artifacts and runs the bounded Codex command only when both `execute` and `useCodex` are true.
|
||||||
* @returns Dry-run commands and artifact paths for a future collector, with no Git merge/push, Docker, Kubernetes, or release mutations.
|
* @returns Dry-run classification and commands, or Codex schema output with artifact paths; failures are blocked with `CODEX_SCHEMA_INVALID` and preserved artifacts.
|
||||||
*/
|
*/
|
||||||
export function buildNapcatUpstreamAudit(
|
export function buildNapcatUpstreamAudit(
|
||||||
input: NapcatAuditClassificationInput,
|
input: NapcatUpstreamAuditInput,
|
||||||
): NapcatUpstreamAuditResult {
|
): NapcatUpstreamAuditResult {
|
||||||
const classification = classifyNapcatUpstreamAudit(input);
|
const classification = classifyNapcatUpstreamAudit(input);
|
||||||
|
const artifactRoot = resolveNapcatArtifactRoot(input.artifactRoot);
|
||||||
const commands: NapcatAuditCommand[] = [
|
const commands: NapcatAuditCommand[] = [
|
||||||
{
|
{
|
||||||
command: 'git status --short --branch',
|
command: 'git status --short --branch',
|
||||||
@ -335,19 +670,99 @@ export function buildNapcatUpstreamAudit(
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (input.execute === true && input.useCodex === true) {
|
||||||
|
const promptName = input.promptName ?? 'upstream-audit';
|
||||||
|
const contextPacketPath = joinNapcatArtifactPath(
|
||||||
|
artifactRoot,
|
||||||
|
'context-packet.md',
|
||||||
|
);
|
||||||
|
const jsonlPath = joinNapcatArtifactPath(
|
||||||
|
artifactRoot,
|
||||||
|
'codex-upstream-audit.jsonl',
|
||||||
|
);
|
||||||
|
const lastMessagePath = joinNapcatArtifactPath(
|
||||||
|
artifactRoot,
|
||||||
|
`codex-${promptName}.md`,
|
||||||
|
);
|
||||||
|
const outputSchemaPath = resolveNapcatCodexOutputSchemaPath(input);
|
||||||
|
const workspaceRoot = resolveNapcatCodexWorkspaceRoot(input.workspaceRoot);
|
||||||
|
const codexCommand = buildNapcatCodexExecCommand({
|
||||||
|
artifactRoot,
|
||||||
|
codexHome: input.codexHome,
|
||||||
|
contextPacketPath,
|
||||||
|
outputSchemaPath,
|
||||||
|
promptName,
|
||||||
|
workspaceRoot,
|
||||||
|
});
|
||||||
|
const codexArtifacts: NapcatUpstreamAuditResult['artifacts'] = [
|
||||||
|
{ path: contextPacketPath, type: 'context' },
|
||||||
|
{ path: jsonlPath, type: 'jsonl' },
|
||||||
|
{ path: lastMessagePath, type: 'markdown' },
|
||||||
|
];
|
||||||
|
const codexCommands: NapcatAuditCommand[] = [
|
||||||
|
{
|
||||||
|
command: codexCommand,
|
||||||
|
description:
|
||||||
|
'Run Codex CLI upstream audit with bounded sandboxing and capture JSONL/last-message artifacts.',
|
||||||
|
readOnly: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
mkdirSync(artifactRoot, { recursive: true });
|
||||||
|
writeFileSync(
|
||||||
|
contextPacketPath,
|
||||||
|
buildNapcatCodexContextPacket(input, classification),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
writeFileSync(lastMessagePath, '', 'utf8');
|
||||||
|
|
||||||
|
let codexResult: NapcatCodexExecResult;
|
||||||
|
try {
|
||||||
|
const executor: NapcatCodexExecutor =
|
||||||
|
input.codexExecutor ?? executeNapcatCodexCommand;
|
||||||
|
codexResult = executor(codexCommand);
|
||||||
|
} catch (error) {
|
||||||
|
codexResult = {
|
||||||
|
ok: false,
|
||||||
|
stderr: String(error),
|
||||||
|
stdout: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(jsonlPath, codexResult.stdout, 'utf8');
|
||||||
|
const lastMessage = existsSync(lastMessagePath)
|
||||||
|
? readFileSync(lastMessagePath, 'utf8')
|
||||||
|
: '';
|
||||||
|
const parsed = codexResult.ok
|
||||||
|
? parseNapcatCodexAuditOutput(codexResult.stdout, lastMessage)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!parsed) {
|
||||||
|
return buildCodexSchemaInvalidResult(codexArtifacts, codexCommands);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...parsed,
|
||||||
|
artifacts: codexArtifacts,
|
||||||
|
commands: codexCommands,
|
||||||
|
execute: true,
|
||||||
|
hotZonePatterns: [...napcatUpstreamHotZonePatterns],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...classification,
|
...classification,
|
||||||
artifacts: [
|
artifacts: [
|
||||||
{
|
{
|
||||||
path: '.kt-workspace/test-artifacts/napcat-upstream-audit/context.md',
|
path: joinNapcatArtifactPath(artifactRoot, 'context.md'),
|
||||||
type: 'markdown',
|
type: 'markdown',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '.kt-workspace/test-artifacts/napcat-upstream-audit/classification.json',
|
path: joinNapcatArtifactPath(artifactRoot, 'classification.json'),
|
||||||
type: 'json',
|
type: 'json',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '.kt-workspace/test-artifacts/napcat-upstream-audit/files.json',
|
path: joinNapcatArtifactPath(artifactRoot, 'files.json'),
|
||||||
type: 'json',
|
type: 'json',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@ -56,16 +56,41 @@ export interface NapcatAuditCommand {
|
|||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NapcatUpstreamAuditInput {
|
export interface NapcatCodexExecCommandInput {
|
||||||
|
artifactRoot: string;
|
||||||
|
codexHome?: string;
|
||||||
|
contextPacketPath: string;
|
||||||
|
outputSchemaPath: string;
|
||||||
|
promptName: 'upstream-audit';
|
||||||
|
workspaceRoot: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NapcatCodexExecResult {
|
||||||
|
ok: boolean;
|
||||||
|
stderr: string;
|
||||||
|
stdout: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NapcatCodexExecutor = (
|
||||||
|
command: string,
|
||||||
|
) => NapcatCodexExecResult;
|
||||||
|
|
||||||
|
export interface NapcatUpstreamAuditInput
|
||||||
|
extends NapcatAuditClassificationInput {
|
||||||
artifactRoot?: string;
|
artifactRoot?: string;
|
||||||
|
codexExecutor?: NapcatCodexExecutor;
|
||||||
|
codexHome?: string;
|
||||||
createCandidateBranch?: boolean;
|
createCandidateBranch?: boolean;
|
||||||
execute?: boolean;
|
execute?: boolean;
|
||||||
forkBranch?: string;
|
forkBranch?: string;
|
||||||
forkRepo?: string;
|
forkRepo?: string;
|
||||||
lastAcceptedUpstreamBase?: string;
|
lastAcceptedUpstreamBase?: string;
|
||||||
|
outputSchemaPath?: string;
|
||||||
|
promptName?: 'upstream-audit';
|
||||||
upstreamReleaseTag?: string;
|
upstreamReleaseTag?: string;
|
||||||
upstreamRepo?: string;
|
upstreamRepo?: string;
|
||||||
useCodex?: boolean;
|
useCodex?: boolean;
|
||||||
|
workspaceRoot?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NapcatAutomationArtifact {
|
export interface NapcatAutomationArtifact {
|
||||||
@ -85,7 +110,7 @@ export interface NapcatUpstreamAuditResult
|
|||||||
extends NapcatAuditClassificationResult {
|
extends NapcatAuditClassificationResult {
|
||||||
artifacts: NapcatAutomationArtifact[];
|
artifacts: NapcatAutomationArtifact[];
|
||||||
commands: NapcatAuditCommand[];
|
commands: NapcatAuditCommand[];
|
||||||
execute: false;
|
execute: boolean;
|
||||||
hotZonePatterns: string[];
|
hotZonePatterns: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user