feat: add code queue prompt live auth lint

This commit is contained in:
Codex
2026-05-23 02:49:40 +00:00
committed by unidesk-code-queue-runner
parent 67fee32beb
commit e96a1acc9a
7 changed files with 488 additions and 4 deletions
+282 -1
View File
@@ -67,6 +67,71 @@ interface CodexJudgeOptions {
includePrompt: boolean;
}
type LiveTestAuthorizationClass = "read-only" | "live-read" | "live-mutating";
type PromptLintSeverity = "info" | "warning" | "block";
type PromptLintDisposition = "ready" | "review" | "needs-authorization";
interface CodexPromptLintOptions {
prompt: string;
}
interface PromptLintSignal {
id: string;
severity: PromptLintSeverity;
matched: boolean;
evidence: string[];
message: string;
}
interface PromptLiveAuthorizationLint {
ok: boolean;
dryRun: true;
mutation: false;
dispatchDisposition: PromptLintDisposition;
declaredClass: LiveTestAuthorizationClass | null;
effectiveClass: LiveTestAuthorizationClass;
requiredClass: LiveTestAuthorizationClass;
defaultedReadOnly: boolean;
liveMutationAuthorized: boolean;
promptShape: {
chars: number;
lines: number;
textEchoed: false;
};
requiredPromptFields: {
devTestClass: {
present: boolean;
value: LiveTestAuthorizationClass | null;
allowedValues: LiveTestAuthorizationClass[];
};
allowedLiveMutation: {
present: boolean;
nonNone: boolean;
requiredWhen: "live-mutating";
};
forbiddenActions: {
present: boolean;
};
closeoutFields: {
present: boolean;
};
};
signals: PromptLintSignal[];
missingOrContradictory: string[];
policy: {
defaultWhenUnclassified: "read-only";
promptLintOnly: true;
accessesLiveService: false;
printsPromptText: false;
reference: string;
};
commands: {
lintFile: string;
submitDryRun: string;
steerDryRun: string;
};
}
interface CodexSubmitOptions {
prompt: string;
queueId: string | undefined;
@@ -892,6 +957,196 @@ function routeSignal(id: string, severity: SubmitRouteSignalSeverity, evidence:
return { id, severity, matched: evidence.length > 0, evidence, message };
}
function promptLintSignal(id: string, severity: PromptLintSeverity, evidence: string[], message: string): PromptLintSignal {
return { id, severity, matched: evidence.length > 0, evidence, message };
}
function declaredLiveTestClass(prompt: string): LiveTestAuthorizationClass | null {
const patterns: Array<[LiveTestAuthorizationClass, RegExp[]]> = [
["live-mutating", [
/\bDEV\s+test\s+class\s*[:]\s*`?live-mutating`?/iu,
/\blive\s+test\s+class\s*[:]\s*`?live-mutating`?/iu,
/\btest\s+class\s*[:]\s*`?live-mutating`?/iu,
/\bDEV\s+(?:)?\s*[:]\s*`?live-mutating`?/iu,
]],
["live-read", [
/\bDEV\s+test\s+class\s*[:]\s*`?live-read`?/iu,
/\blive\s+test\s+class\s*[:]\s*`?live-read`?/iu,
/\btest\s+class\s*[:]\s*`?live-read`?/iu,
/\bDEV\s+(?:)?\s*[:]\s*`?live-read`?/iu,
]],
["read-only", [
/\bDEV\s+test\s+class\s*[:]\s*`?read-only`?/iu,
/\blive\s+test\s+class\s*[:]\s*`?read-only`?/iu,
/\btest\s+class\s*[:]\s*`?read-only`?/iu,
/\bDEV\s+(?:)?\s*[:]\s*`?read-only`?/iu,
]],
];
for (const [value, valuePatterns] of patterns) {
if (valuePatterns.some((pattern) => pattern.test(prompt))) return value;
}
return null;
}
function liveClassRank(value: LiveTestAuthorizationClass): number {
if (value === "read-only") return 0;
if (value === "live-read") return 1;
return 2;
}
function hasPromptField(prompt: string, patterns: RegExp[]): boolean {
return patterns.some((pattern) => pattern.test(prompt));
}
function sanitizePromptLintEvidence(evidence: string[]): string[] {
return evidence.map((item) => item
.replace(/([?&](?:token|api[_-]?key|secret|password|credential)=)[^&\s]+/giu, "$1<redacted>")
.replace(/((?:token|api[_-]?key|secret|password|credential)\s*[:=]\s*)[^\s,;]+/giu, "$1<redacted>")
.replace(/(Bearer\s+)[A-Za-z0-9._~+/-]+=*/giu, "$1<redacted>")
.slice(0, 160));
}
function buildPromptLiveAuthorizationLint(prompt: string): PromptLiveAuthorizationLint {
const declaredClass = declaredLiveTestClass(prompt);
const effectiveClass = declaredClass ?? "read-only";
const allowedClasses: LiveTestAuthorizationClass[] = ["read-only", "live-read", "live-mutating"];
const liveReadEvidence = sanitizePromptLintEvidence(regexEvidenceWithoutNegatedContext(prompt, [
/\blive[- ]read\b/giu,
/\blive\s+(?:dev\s+)?(?:service|runtime|endpoint|health|status|logs?|metrics?)\b/giu,
/\bGET\s+\/(?:health|status|live|metrics|api\/diagnostics)\b/gu,
/\bkubectl\s+(?:get|describe|logs)\b/giu,
/\bmicroservice\s+(?:health|status|diagnostics)\b/giu,
/\bdiagnostics\b|\bstatus\b|\bmetrics\b|\blogs?\b/giu,
/(?:||||)/gu,
/\s*(?:DEV|live||||)/giu,
]));
const liveMutationEvidence = sanitizePromptLintEvidence(regexEvidenceWithoutNegatedContext(prompt, [
/\blive-mutating\b/giu,
/\bDEV\s+smoke\b|\blive\s+smoke\b|\bM3\s+smoke\b/giu,
/\bdeploy\s+apply\b|\brollout\s+restart\b|\bkubectl\s+(?:apply|delete|patch|rollout)\b/giu,
/\b(?:POST|PUT|PATCH|DELETE)\s+\/[A-Za-z0-9_./:-]*/gu,
/\bcodex\s+(?:submit|steer|interrupt|cancel)\b/giu,
/\btask\s+(?:submit|steer|retry|trigger)\b/giu,
/\btrigger\s+(?:schedule|job|task|operation|audit|evidence)\b/giu,
/\bschedule\s+(?:run|retry-run|delete)\b/giu,
/\b(?:create|write|post|put|patch)\b[^\n]{0,40}\b(?:operation|audit|evidence)\b/giu,
/\b(?:operation|audit|evidence)\s+(?:id|record|write|create)\b/giu,
/\bDO\d+\b|\bDI\d+\b|\bres_boxsimu_\d+\b|\bhwlab-patch-panel\b/giu,
/||||||(?:|operation|audit|evidence)||/gu,
]));
const prodMutationEvidence = sanitizePromptLintEvidence(regexEvidenceWithoutNegatedContext(prompt, [
/\bprod(?:uction)?\b[^\n]*(?:deploy|restart|write|mutation|mutating|apply|rollout|delete|patch)\b/giu,
/\b(?:deploy|restart|write|mutation|mutating|apply|rollout|delete|patch)\b[^\n]*\bprod(?:uction)?\b/giu,
/[^\n]*(?:|||||)/gu,
]));
const requiredClass = liveMutationEvidence.length > 0 || prodMutationEvidence.length > 0
? "live-mutating"
: liveReadEvidence.length > 0
? "live-read"
: "read-only";
const allowedLiveMutationPresent = hasPromptField(prompt, [
/\ballowed\s+live\s+mutation\s*[:]/iu,
/\s*live\s*mutation\s*[:]/iu,
/(?:||)?(?:||mutation)\s*[:]/iu,
]);
const allowedLiveMutationNone = hasPromptField(prompt, [
/\ballowed\s+live\s+mutation\s*[:]\s*(?:`?none`?||)(?:\s|$|[.;,])/iu,
/\s*live\s*mutation\s*[:]\s*(?:`?none`?||)(?:\s|$|[.;,])/iu,
/(?:||)?(?:||mutation)\s*[:]\s*(?:`?none`?||)(?:\s|$|[.;,])/iu,
]);
const forbiddenActionsPresent = hasPromptField(prompt, [
/\bforbidden\s+actions?\s*[:]/iu,
/\s*[:]/u,
/\s*[:]/u,
]);
const closeoutFieldsPresent = hasPromptField(prompt, [
/\bcloseout\s+fields?\s*[:]/iu,
/\bfinal\s+response\b[^\n]*(?:must|include|report)/iu,
/\b\s*[:]/u,
/\bfinal\s+response\b[^\n]*/iu,
]);
const effectiveInsufficient = liveClassRank(effectiveClass) < liveClassRank(requiredClass);
const liveMutationAuthorized = effectiveClass === "live-mutating" && allowedLiveMutationPresent && !allowedLiveMutationNone;
const contradictionEvidence = [
...(effectiveClass === "read-only" && liveMutationEvidence.length > 0 ? ["declares/read-only but prompt contains live mutation signals"] : []),
...(effectiveClass === "live-read" && liveMutationEvidence.length > 0 ? ["declares/live-read but prompt contains live mutation signals"] : []),
...(effectiveClass === "live-mutating" && allowedLiveMutationNone ? ["declares/live-mutating but allowed live mutation is none"] : []),
...(prodMutationEvidence.length > 0 ? prodMutationEvidence.map((item) => `prod mutation signal: ${item}`) : []),
];
const missingOrContradictory = [
...(declaredClass === null ? ["missing DEV test class; defaulting to read-only"] : []),
...(effectiveInsufficient ? [`effective class ${effectiveClass} is below required ${requiredClass}`] : []),
...(requiredClass === "live-mutating" && !allowedLiveMutationPresent ? ["live-mutating prompt must include allowed live mutation"] : []),
...(requiredClass === "live-mutating" && allowedLiveMutationNone ? ["live-mutating prompt cannot set allowed live mutation to none"] : []),
...(!forbiddenActionsPresent ? ["missing forbidden actions"] : []),
...(!closeoutFieldsPresent ? ["missing closeout fields"] : []),
...contradictionEvidence,
];
const signals = [
promptLintSignal("declared-dev-test-class", "info", declaredClass === null ? [] : [declaredClass], "Prompt explicitly declares DEV test class."),
promptLintSignal("live-read-signal", "warning", liveReadEvidence, "Prompt appears to read live DEV service state, logs, health, status, metrics, or Kubernetes objects."),
promptLintSignal("live-mutation-signal", "block", liveMutationEvidence, "Prompt appears to trigger runtime writes, deployment, task control, operation/audit/evidence creation, or HWLAB DO/DI activity."),
promptLintSignal("prod-mutation-signal", "block", prodMutationEvidence, "Prompt appears to mention production mutation; Code Queue runner prompts must not implicitly authorize this."),
promptLintSignal("allowed-live-mutation-field", requiredClass === "live-mutating" ? "block" : "info", allowedLiveMutationPresent && !allowedLiveMutationNone ? ["present"] : [], "live-mutating prompts must enumerate allowed live mutation commands and target state changes."),
promptLintSignal("forbidden-actions-field", "warning", forbiddenActionsPresent ? ["present"] : [], "Prompt should list forbidden high-risk actions."),
promptLintSignal("closeout-fields-field", "warning", closeoutFieldsPresent ? ["present"] : [], "Prompt should require final closeout fields for class, mutation, commands, targets, evidence, and residual risk."),
];
const ok = missingOrContradictory.length === 0;
const dispatchDisposition: PromptLintDisposition = ok
? "ready"
: requiredClass === "live-mutating" || effectiveInsufficient || contradictionEvidence.length > 0
? "needs-authorization"
: "review";
return {
ok,
dryRun: true,
mutation: false,
dispatchDisposition,
declaredClass,
effectiveClass,
requiredClass,
defaultedReadOnly: declaredClass === null,
liveMutationAuthorized,
promptShape: {
chars: prompt.length,
lines: prompt.split(/\r\n|\r|\n/u).length,
textEchoed: false,
},
requiredPromptFields: {
devTestClass: {
present: declaredClass !== null,
value: declaredClass,
allowedValues: allowedClasses,
},
allowedLiveMutation: {
present: allowedLiveMutationPresent,
nonNone: allowedLiveMutationPresent && !allowedLiveMutationNone,
requiredWhen: "live-mutating",
},
forbiddenActions: {
present: forbiddenActionsPresent,
},
closeoutFields: {
present: closeoutFieldsPresent,
},
},
signals,
missingOrContradictory,
policy: {
defaultWhenUnclassified: "read-only",
promptLintOnly: true,
accessesLiveService: false,
printsPromptText: false,
reference: "docs/reference/code-queue-supervision.md#dev-测试授权分级",
},
commands: {
lintFile: "bun scripts/cli.ts codex prompt-lint --prompt-file <path>",
submitDryRun: "bun scripts/cli.ts codex submit --prompt-file <path> --dry-run",
steerDryRun: "bun scripts/cli.ts codex steer <taskId> --prompt-file <path> --dry-run",
},
};
}
function submitPolicyContract(): SubmitRoutingRecommendation["policyContract"] {
return {
selectionPrinciples: [
@@ -3318,6 +3573,16 @@ function parseSteerOptions(args: string[]): CodexSteerOptions {
};
}
function parsePromptLintOptions(args: string[]): CodexPromptLintOptions {
assertKnownOptions(args, {
flags: ["--prompt-stdin", "--stdin"],
valueOptions: ["--prompt-file", "--file"],
}, "codex prompt-lint");
return {
prompt: promptFromArgs(args, "codex prompt-lint", steerPromptValueOptions),
};
}
function submitPayload(options: CodexSubmitOptions): Record<string, unknown> {
return {
prompt: options.prompt,
@@ -4986,17 +5251,28 @@ export function codexSubmitRoutingRecommendationForTest(prompt: string, model?:
});
}
export function codexPromptLiveAuthorizationLintForTest(prompt: string): PromptLiveAuthorizationLint {
return buildPromptLiveAuthorizationLint(prompt);
}
export function codexSubmitModelRegistryForTest(models: string[] = sharedDefaultCodeModels): ReturnType<typeof submitModelRegistry> {
return submitModelRegistry(models);
}
function codexPromptLintTask(args: string[]): unknown {
const options = parsePromptLintOptions(args);
return buildPromptLiveAuthorizationLint(options.prompt);
}
function codexSubmitTask(args: string[]): unknown {
const options = parseSubmitOptions(args);
const payload = submitPayload(options);
const promptLint = buildPromptLiveAuthorizationLint(options.prompt);
if (options.dryRun) {
return {
ok: true,
dryRun: true,
promptLint,
routingRecommendation: submitRoutingRecommendation(options),
modelRegistry: submitModelRegistry(),
request: {
@@ -5027,6 +5303,7 @@ function codexInterruptTask(taskId: string): unknown {
function codexSteerTask(taskId: string, args: string[], fetcher: CodexResponseFetcher = coreInternalFetch): unknown {
const options = parseSteerOptions(args);
const promptLint = buildPromptLiveAuthorizationLint(options.prompt);
const targetPath = `/api/tasks/${encodeURIComponent(taskId)}/steer`;
const stableProxyPath = codeQueueProxyPath(targetPath);
const rawProxyEquivalent = codeQueueProxyEquivalentCommand(targetPath, "{\"prompt\":\"...\"}");
@@ -5053,6 +5330,7 @@ function codexSteerTask(taskId: string, args: string[], fetcher: CodexResponseFe
return {
ok: true,
dryRun: true,
promptLint,
request,
commands: {
run: `bun scripts/cli.ts codex steer ${taskId} --prompt-file <path>`,
@@ -5164,6 +5442,9 @@ function codexSteerTask(taskId: string, args: string[], fetcher: CodexResponseFe
export async function runCodeQueueCommand(config: UniDeskConfig, args: string[]): Promise<unknown> {
const [action = "task", taskIdArg] = args;
if (action === "prompt-lint" || action === "lint-prompt") {
return codexPromptLintTask(args.slice(1));
}
if (action === "submit" || action === "enqueue") {
return codexSubmitTask(args.slice(1));
}
@@ -5214,5 +5495,5 @@ export async function runCodeQueueCommand(config: UniDeskConfig, args: string[])
const taskId = requireTaskId(taskIdArg, "codex steer");
return codexSteerTask(taskId, args.slice(2));
}
throw new Error("codex command must be one of: submit, enqueue, task, summary, show, tasks, overview, output, judge, read, mark-read, dev-ready, health, skills-sync, pr-preflight, runtime-preflight, queues, queue list, queue create, queue merge, move, steer, interrupt, cancel");
throw new Error("codex command must be one of: prompt-lint, submit, enqueue, task, summary, show, tasks, overview, output, judge, read, mark-read, dev-ready, health, skills-sync, pr-preflight, runtime-preflight, queues, queue list, queue create, queue merge, move, steer, interrupt, cancel");
}