feat: add code queue prompt live auth lint
This commit is contained in:
committed by
unidesk-code-queue-runner
parent
67fee32beb
commit
e96a1acc9a
+282
-1
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user