@mirnoorata/codexa 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +634 -0
- package/dist/artifacts.d.ts +2 -0
- package/dist/artifacts.js +375 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/autonomy.d.ts +17 -0
- package/dist/autonomy.js +124 -0
- package/dist/autonomy.js.map +1 -0
- package/dist/autoverify/policy.d.ts +5 -0
- package/dist/autoverify/policy.js +18 -0
- package/dist/autoverify/policy.js.map +1 -0
- package/dist/autoverify.d.ts +45 -0
- package/dist/autoverify.js +1041 -0
- package/dist/autoverify.js.map +1 -0
- package/dist/cache-lock.d.ts +16 -0
- package/dist/cache-lock.js +181 -0
- package/dist/cache-lock.js.map +1 -0
- package/dist/cli/hooks.d.ts +5 -0
- package/dist/cli/hooks.js +264 -0
- package/dist/cli/hooks.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1034 -0
- package/dist/cli.js.map +1 -0
- package/dist/codex-contract.d.ts +2 -0
- package/dist/codex-contract.js +78 -0
- package/dist/codex-contract.js.map +1 -0
- package/dist/command.d.ts +34 -0
- package/dist/command.js +162 -0
- package/dist/command.js.map +1 -0
- package/dist/doctor.d.ts +112 -0
- package/dist/doctor.js +518 -0
- package/dist/doctor.js.map +1 -0
- package/dist/eval/baseline.d.ts +7 -0
- package/dist/eval/baseline.js +146 -0
- package/dist/eval/baseline.js.map +1 -0
- package/dist/eval/historical.d.ts +4 -0
- package/dist/eval/historical.js +663 -0
- package/dist/eval/historical.js.map +1 -0
- package/dist/eval/render.d.ts +2 -0
- package/dist/eval/render.js +53 -0
- package/dist/eval/render.js.map +1 -0
- package/dist/eval/scoring.d.ts +21 -0
- package/dist/eval/scoring.js +618 -0
- package/dist/eval/scoring.js.map +1 -0
- package/dist/eval/synthetic.d.ts +36 -0
- package/dist/eval/synthetic.js +107 -0
- package/dist/eval/synthetic.js.map +1 -0
- package/dist/eval/types.d.ts +36 -0
- package/dist/eval/types.js +2 -0
- package/dist/eval/types.js.map +1 -0
- package/dist/eval.d.ts +140 -0
- package/dist/eval.js +551 -0
- package/dist/eval.js.map +1 -0
- package/dist/git.d.ts +17 -0
- package/dist/git.js +189 -0
- package/dist/git.js.map +1 -0
- package/dist/github-release.d.ts +47 -0
- package/dist/github-release.js +610 -0
- package/dist/github-release.js.map +1 -0
- package/dist/github-sync.d.ts +68 -0
- package/dist/github-sync.js +345 -0
- package/dist/github-sync.js.map +1 -0
- package/dist/graph.d.ts +10 -0
- package/dist/graph.js +665 -0
- package/dist/graph.js.map +1 -0
- package/dist/indexer/aliases.d.ts +2 -0
- package/dist/indexer/aliases.js +190 -0
- package/dist/indexer/aliases.js.map +1 -0
- package/dist/indexer/artifact-writing.d.ts +3 -0
- package/dist/indexer/artifact-writing.js +79 -0
- package/dist/indexer/artifact-writing.js.map +1 -0
- package/dist/indexer/discovery.d.ts +2 -0
- package/dist/indexer/discovery.js +5 -0
- package/dist/indexer/discovery.js.map +1 -0
- package/dist/indexer/external-facts.d.ts +6 -0
- package/dist/indexer/external-facts.js +45 -0
- package/dist/indexer/external-facts.js.map +1 -0
- package/dist/indexer/freshness.d.ts +8 -0
- package/dist/indexer/freshness.js +56 -0
- package/dist/indexer/freshness.js.map +1 -0
- package/dist/indexer/graph-stage.d.ts +2 -0
- package/dist/indexer/graph-stage.js +21 -0
- package/dist/indexer/graph-stage.js.map +1 -0
- package/dist/indexer/parsing.d.ts +30 -0
- package/dist/indexer/parsing.js +177 -0
- package/dist/indexer/parsing.js.map +1 -0
- package/dist/indexer/pipeline.d.ts +5 -0
- package/dist/indexer/pipeline.js +8 -0
- package/dist/indexer/pipeline.js.map +1 -0
- package/dist/indexer/ranking.d.ts +4 -0
- package/dist/indexer/ranking.js +134 -0
- package/dist/indexer/ranking.js.map +1 -0
- package/dist/indexer.d.ts +13 -0
- package/dist/indexer.js +395 -0
- package/dist/indexer.js.map +1 -0
- package/dist/init.d.ts +24 -0
- package/dist/init.js +566 -0
- package/dist/init.js.map +1 -0
- package/dist/language.d.ts +8 -0
- package/dist/language.js +123 -0
- package/dist/language.js.map +1 -0
- package/dist/live-index.d.ts +68 -0
- package/dist/live-index.js +215 -0
- package/dist/live-index.js.map +1 -0
- package/dist/lsp/assist.d.ts +44 -0
- package/dist/lsp/assist.js +331 -0
- package/dist/lsp/assist.js.map +1 -0
- package/dist/lsp/client.d.ts +59 -0
- package/dist/lsp/client.js +208 -0
- package/dist/lsp/client.js.map +1 -0
- package/dist/mcp/compaction.d.ts +15 -0
- package/dist/mcp/compaction.js +1249 -0
- package/dist/mcp/compaction.js.map +1 -0
- package/dist/mcp/envelope.d.ts +44 -0
- package/dist/mcp/envelope.js +425 -0
- package/dist/mcp/envelope.js.map +1 -0
- package/dist/mcp/prompts.d.ts +2 -0
- package/dist/mcp/prompts.js +109 -0
- package/dist/mcp/prompts.js.map +1 -0
- package/dist/mcp/resources.d.ts +2 -0
- package/dist/mcp/resources.js +132 -0
- package/dist/mcp/resources.js.map +1 -0
- package/dist/mcp/runtime.d.ts +15 -0
- package/dist/mcp/runtime.js +122 -0
- package/dist/mcp/runtime.js.map +1 -0
- package/dist/mcp/session-memory.d.ts +3 -0
- package/dist/mcp/session-memory.js +61 -0
- package/dist/mcp/session-memory.js.map +1 -0
- package/dist/mcp/tool-registry.d.ts +269 -0
- package/dist/mcp/tool-registry.js +284 -0
- package/dist/mcp/tool-registry.js.map +1 -0
- package/dist/mcp/tools.d.ts +53 -0
- package/dist/mcp/tools.js +372 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/mcp-repo-root.d.ts +16 -0
- package/dist/mcp-repo-root.js +322 -0
- package/dist/mcp-repo-root.js.map +1 -0
- package/dist/mcp-tool-catalog.d.ts +2 -0
- package/dist/mcp-tool-catalog.js +2 -0
- package/dist/mcp-tool-catalog.js.map +1 -0
- package/dist/mcp.d.ts +11 -0
- package/dist/mcp.js +332 -0
- package/dist/mcp.js.map +1 -0
- package/dist/outcome-ranking.d.ts +5 -0
- package/dist/outcome-ranking.js +115 -0
- package/dist/outcome-ranking.js.map +1 -0
- package/dist/parser/context.d.ts +28 -0
- package/dist/parser/context.js +2 -0
- package/dist/parser/context.js.map +1 -0
- package/dist/parser/ecma.d.ts +5 -0
- package/dist/parser/ecma.js +388 -0
- package/dist/parser/ecma.js.map +1 -0
- package/dist/parser/facts.d.ts +12 -0
- package/dist/parser/facts.js +137 -0
- package/dist/parser/facts.js.map +1 -0
- package/dist/parser/json.d.ts +3 -0
- package/dist/parser/json.js +318 -0
- package/dist/parser/json.js.map +1 -0
- package/dist/parser/markdown.d.ts +3 -0
- package/dist/parser/markdown.js +180 -0
- package/dist/parser/markdown.js.map +1 -0
- package/dist/parser/nodes.d.ts +5 -0
- package/dist/parser/nodes.js +75 -0
- package/dist/parser/nodes.js.map +1 -0
- package/dist/parser/python.d.ts +2 -0
- package/dist/parser/python.js +307 -0
- package/dist/parser/python.js.map +1 -0
- package/dist/parser/references.d.ts +3 -0
- package/dist/parser/references.js +204 -0
- package/dist/parser/references.js.map +1 -0
- package/dist/parser/risks.d.ts +4 -0
- package/dist/parser/risks.js +62 -0
- package/dist/parser/risks.js.map +1 -0
- package/dist/parser/routes.d.ts +5 -0
- package/dist/parser/routes.js +97 -0
- package/dist/parser/routes.js.map +1 -0
- package/dist/parser/shallow.d.ts +3 -0
- package/dist/parser/shallow.js +545 -0
- package/dist/parser/shallow.js.map +1 -0
- package/dist/parser/source.d.ts +4 -0
- package/dist/parser/source.js +127 -0
- package/dist/parser/source.js.map +1 -0
- package/dist/parser.d.ts +2 -0
- package/dist/parser.js +2 -0
- package/dist/parser.js.map +1 -0
- package/dist/placeholder-signals.d.ts +15 -0
- package/dist/placeholder-signals.js +511 -0
- package/dist/placeholder-signals.js.map +1 -0
- package/dist/post-edit-outcomes.d.ts +167 -0
- package/dist/post-edit-outcomes.js +484 -0
- package/dist/post-edit-outcomes.js.map +1 -0
- package/dist/queries.d.ts +12 -0
- package/dist/queries.js +13 -0
- package/dist/queries.js.map +1 -0
- package/dist/query/change-plan.d.ts +48 -0
- package/dist/query/change-plan.js +858 -0
- package/dist/query/change-plan.js.map +1 -0
- package/dist/query/compact-data.d.ts +25 -0
- package/dist/query/compact-data.js +74 -0
- package/dist/query/compact-data.js.map +1 -0
- package/dist/query/context.d.ts +5 -0
- package/dist/query/context.js +1162 -0
- package/dist/query/context.js.map +1 -0
- package/dist/query/diff.d.ts +5 -0
- package/dist/query/diff.js +111 -0
- package/dist/query/diff.js.map +1 -0
- package/dist/query/edge-evidence.d.ts +3 -0
- package/dist/query/edge-evidence.js +36 -0
- package/dist/query/edge-evidence.js.map +1 -0
- package/dist/query/formatting.d.ts +14 -0
- package/dist/query/formatting.js +67 -0
- package/dist/query/formatting.js.map +1 -0
- package/dist/query/graph-traversal.d.ts +22 -0
- package/dist/query/graph-traversal.js +218 -0
- package/dist/query/graph-traversal.js.map +1 -0
- package/dist/query/graph.d.ts +14 -0
- package/dist/query/graph.js +102 -0
- package/dist/query/graph.js.map +1 -0
- package/dist/query/impact.d.ts +28 -0
- package/dist/query/impact.js +568 -0
- package/dist/query/impact.js.map +1 -0
- package/dist/query/inspection.d.ts +9 -0
- package/dist/query/inspection.js +290 -0
- package/dist/query/inspection.js.map +1 -0
- package/dist/query/next-tools.d.ts +3 -0
- package/dist/query/next-tools.js +25 -0
- package/dist/query/next-tools.js.map +1 -0
- package/dist/query/placeholders.d.ts +24 -0
- package/dist/query/placeholders.js +121 -0
- package/dist/query/placeholders.js.map +1 -0
- package/dist/query/post-edit/decision.d.ts +49 -0
- package/dist/query/post-edit/decision.js +130 -0
- package/dist/query/post-edit/decision.js.map +1 -0
- package/dist/query/post-edit/dirty-scope.d.ts +16 -0
- package/dist/query/post-edit/dirty-scope.js +21 -0
- package/dist/query/post-edit/dirty-scope.js.map +1 -0
- package/dist/query/post-edit/next-actions.d.ts +22 -0
- package/dist/query/post-edit/next-actions.js +44 -0
- package/dist/query/post-edit/next-actions.js.map +1 -0
- package/dist/query/post-edit/snapshot-contract.d.ts +8 -0
- package/dist/query/post-edit/snapshot-contract.js +111 -0
- package/dist/query/post-edit/snapshot-contract.js.map +1 -0
- package/dist/query/post-edit.d.ts +5 -0
- package/dist/query/post-edit.js +1108 -0
- package/dist/query/post-edit.js.map +1 -0
- package/dist/query/quality.d.ts +43 -0
- package/dist/query/quality.js +134 -0
- package/dist/query/quality.js.map +1 -0
- package/dist/query/raw-search.d.ts +23 -0
- package/dist/query/raw-search.js +147 -0
- package/dist/query/raw-search.js.map +1 -0
- package/dist/query/runtime.d.ts +11 -0
- package/dist/query/runtime.js +79 -0
- package/dist/query/runtime.js.map +1 -0
- package/dist/query/search.d.ts +25 -0
- package/dist/query/search.js +429 -0
- package/dist/query/search.js.map +1 -0
- package/dist/query/session-memory.d.ts +3 -0
- package/dist/query/session-memory.js +108 -0
- package/dist/query/session-memory.js.map +1 -0
- package/dist/query/session.d.ts +41 -0
- package/dist/query/session.js +90 -0
- package/dist/query/session.js.map +1 -0
- package/dist/query/targets.d.ts +25 -0
- package/dist/query/targets.js +97 -0
- package/dist/query/targets.js.map +1 -0
- package/dist/query/test-commands.d.ts +10 -0
- package/dist/query/test-commands.js +110 -0
- package/dist/query/test-commands.js.map +1 -0
- package/dist/query/test-plan.d.ts +6 -0
- package/dist/query/test-plan.js +104 -0
- package/dist/query/test-plan.js.map +1 -0
- package/dist/query/tests.d.ts +48 -0
- package/dist/query/tests.js +444 -0
- package/dist/query/tests.js.map +1 -0
- package/dist/query/verification/shell.d.ts +20 -0
- package/dist/query/verification/shell.js +164 -0
- package/dist/query/verification/shell.js.map +1 -0
- package/dist/query/verification.d.ts +47 -0
- package/dist/query/verification.js +1123 -0
- package/dist/query/verification.js.map +1 -0
- package/dist/query/workflow.d.ts +17 -0
- package/dist/query/workflow.js +252 -0
- package/dist/query/workflow.js.map +1 -0
- package/dist/query/workspace-guidance.d.ts +26 -0
- package/dist/query/workspace-guidance.js +214 -0
- package/dist/query/workspace-guidance.js.map +1 -0
- package/dist/query/worktree-state.d.ts +22 -0
- package/dist/query/worktree-state.js +32 -0
- package/dist/query/worktree-state.js.map +1 -0
- package/dist/query/worktree.d.ts +16 -0
- package/dist/query/worktree.js +194 -0
- package/dist/query/worktree.js.map +1 -0
- package/dist/query-data.d.ts +4 -0
- package/dist/query-data.js +112 -0
- package/dist/query-data.js.map +1 -0
- package/dist/repo-files.d.ts +24 -0
- package/dist/repo-files.js +105 -0
- package/dist/repo-files.js.map +1 -0
- package/dist/resolver.d.ts +9 -0
- package/dist/resolver.js +555 -0
- package/dist/resolver.js.map +1 -0
- package/dist/retrieval.d.ts +46 -0
- package/dist/retrieval.js +783 -0
- package/dist/retrieval.js.map +1 -0
- package/dist/risk-ingest.d.ts +16 -0
- package/dist/risk-ingest.js +458 -0
- package/dist/risk-ingest.js.map +1 -0
- package/dist/rules.d.ts +10 -0
- package/dist/rules.js +107 -0
- package/dist/rules.js.map +1 -0
- package/dist/semantic/python.d.ts +9 -0
- package/dist/semantic/python.js +817 -0
- package/dist/semantic/python.js.map +1 -0
- package/dist/semantic/typescript.d.ts +10 -0
- package/dist/semantic/typescript.js +714 -0
- package/dist/semantic/typescript.js.map +1 -0
- package/dist/semantic-retrieval.d.ts +53 -0
- package/dist/semantic-retrieval.js +673 -0
- package/dist/semantic-retrieval.js.map +1 -0
- package/dist/session-memory/derivation.d.ts +6 -0
- package/dist/session-memory/derivation.js +400 -0
- package/dist/session-memory/derivation.js.map +1 -0
- package/dist/session-memory/event-log.d.ts +23 -0
- package/dist/session-memory/event-log.js +126 -0
- package/dist/session-memory/event-log.js.map +1 -0
- package/dist/session-memory/formatting.d.ts +7 -0
- package/dist/session-memory/formatting.js +86 -0
- package/dist/session-memory/formatting.js.map +1 -0
- package/dist/session-memory/model.d.ts +94 -0
- package/dist/session-memory/model.js +17 -0
- package/dist/session-memory/model.js.map +1 -0
- package/dist/session-memory/runtime.d.ts +24 -0
- package/dist/session-memory/runtime.js +289 -0
- package/dist/session-memory/runtime.js.map +1 -0
- package/dist/session-memory/store.d.ts +27 -0
- package/dist/session-memory/store.js +447 -0
- package/dist/session-memory/store.js.map +1 -0
- package/dist/session-memory.d.ts +1 -0
- package/dist/session-memory.js +2 -0
- package/dist/session-memory.js.map +1 -0
- package/dist/static-analysis.d.ts +36 -0
- package/dist/static-analysis.js +505 -0
- package/dist/static-analysis.js.map +1 -0
- package/dist/symbol-report-ingest.d.ts +8 -0
- package/dist/symbol-report-ingest.js +504 -0
- package/dist/symbol-report-ingest.js.map +1 -0
- package/dist/task-snapshots.d.ts +41 -0
- package/dist/task-snapshots.js +430 -0
- package/dist/task-snapshots.js.map +1 -0
- package/dist/types.d.ts +848 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/dist/util.d.ts +11 -0
- package/dist/util.js +63 -0
- package/dist/util.js.map +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/package.json +81 -0
- package/plugins/codexa/.codex-plugin/plugin.json +38 -0
- package/plugins/codexa/.mcp.json +20 -0
- package/plugins/codexa/scripts/codexa-mcp.js +100 -0
- package/plugins/codexa/skills/codexa/SKILL.md +48 -0
|
@@ -0,0 +1,1041 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { execFile, spawn } from "node:child_process";
|
|
3
|
+
import { constants as fsConstants } from "node:fs";
|
|
4
|
+
import { promises as fs } from "node:fs";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { getGitStateAsync, repoRelativePath as gitRepoRelativePath } from "./git.js";
|
|
9
|
+
import { effectiveAutonomyMode } from "./autonomy.js";
|
|
10
|
+
import { isSourcePath, isTestPath, shouldSkipPath } from "./language.js";
|
|
11
|
+
import { AUTO_VERIFY_POLICY_DIGEST, AUTO_VERIFY_POLICY_ID, autoVerifyPolicySignature as policySignature, isTrustedAutoVerifyReport, markTrustedAutoVerifyReport } from "./autoverify/policy.js";
|
|
12
|
+
import { shellWords } from "./query/verification/shell.js";
|
|
13
|
+
import { isSubpath, stableId, uniqueSorted } from "./util.js";
|
|
14
|
+
const execFileAsync = promisify(execFile);
|
|
15
|
+
const DEFAULT_MAX_COMMANDS = 2;
|
|
16
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
17
|
+
const MAX_OUTPUT_SUMMARY = 500;
|
|
18
|
+
const MAX_OUTPUT_CAPTURE = 20_000;
|
|
19
|
+
export function isTrustedAutoVerifyCommandReport(report) {
|
|
20
|
+
return isTrustedAutoVerifyReport(report);
|
|
21
|
+
}
|
|
22
|
+
export function autoVerifyPolicySignature() {
|
|
23
|
+
return policySignature();
|
|
24
|
+
}
|
|
25
|
+
export async function runAutoVerifyForPostEdit(repoRoot, data) {
|
|
26
|
+
const candidates = autoVerifyCandidates(repoRoot, data);
|
|
27
|
+
const reports = [];
|
|
28
|
+
const attempted = [];
|
|
29
|
+
const skipped = [];
|
|
30
|
+
const trust = await autoVerifyTrusted(repoRoot);
|
|
31
|
+
if (!trust.enabled) {
|
|
32
|
+
return {
|
|
33
|
+
reports,
|
|
34
|
+
attempted,
|
|
35
|
+
skipped: candidates.map((candidate) => `${candidate.command} (${trust.reason})`)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
for (const candidate of candidates) {
|
|
39
|
+
if (reports.length >= DEFAULT_MAX_COMMANDS) {
|
|
40
|
+
skipped.push(`${candidate.command} (max auto-verify commands reached)`);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const safe = await safeAutoVerifyCommand(repoRoot, candidate);
|
|
44
|
+
if (!safe.ok) {
|
|
45
|
+
skipped.push(`${candidate.command} (${safe.reason})`);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
attempted.push(candidate.command);
|
|
49
|
+
const before = await runnerDirtyState(repoRoot, safe.command.candidate.protectedPaths);
|
|
50
|
+
const report = await runVerificationCommand(repoRoot, safe.command, before, DEFAULT_TIMEOUT_MS);
|
|
51
|
+
reports.push(report);
|
|
52
|
+
if (report.exitCode !== 0 || report.runner?.sourceMutationDetected) {
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { reports, attempted, skipped };
|
|
57
|
+
}
|
|
58
|
+
export function autoVerifyDirtyHashFromParts(input) {
|
|
59
|
+
return stableId("autoverify-dirty-tree", input.headCommit ?? "null", JSON.stringify({
|
|
60
|
+
dirtyFiles: [...input.dirtyFiles].sort(),
|
|
61
|
+
dirtyFileHashes: Object.fromEntries(Object.entries(input.dirtyFileHashes).sort(([a], [b]) => a.localeCompare(b)))
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
export function sanitizeAutoVerifyText(value, repoRoot) {
|
|
65
|
+
const clean = redactSecretText(value)
|
|
66
|
+
?.replaceAll(path.resolve(repoRoot), "<repo>")
|
|
67
|
+
.replace(/(^|[\s([,{])\/[^\s;|)\]'",]+/gu, "$1<abs-path>")
|
|
68
|
+
.replace(/(^|[\s([,{])(?:\.\.?\/)[^\s;|)\]'",]+/gu, "$1<rel-path>")
|
|
69
|
+
.replace(/\s+/gu, " ")
|
|
70
|
+
.trim();
|
|
71
|
+
if (!clean) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
return clean.length > MAX_OUTPUT_SUMMARY ? `${clean.slice(0, MAX_OUTPUT_SUMMARY - 3)}...` : clean;
|
|
75
|
+
}
|
|
76
|
+
async function autoVerifyTrusted(repoRoot) {
|
|
77
|
+
const autonomy = await effectiveAutonomyMode(repoRoot);
|
|
78
|
+
if (autonomy.mode === "full-access") {
|
|
79
|
+
return { enabled: true };
|
|
80
|
+
}
|
|
81
|
+
return { enabled: false, reason: `AutoVerify execution requires user full-access autonomy (current: ${autonomy.mode} via ${autonomy.source})` };
|
|
82
|
+
}
|
|
83
|
+
function autoVerifyCandidates(repoRoot, data) {
|
|
84
|
+
if (!data || typeof data !== "object") {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
const record = data;
|
|
88
|
+
const reviewPaths = pathsFromUnknown(record.reviewTargets);
|
|
89
|
+
const structured = Array.isArray(record.autoVerifyCandidates)
|
|
90
|
+
? record.autoVerifyCandidates
|
|
91
|
+
.map((item) => normalizeStructuredCandidate(repoRoot, item, reviewPaths))
|
|
92
|
+
.filter((candidate) => Boolean(candidate))
|
|
93
|
+
: [];
|
|
94
|
+
if (structured.length > 0) {
|
|
95
|
+
return uniqueCandidates(structured);
|
|
96
|
+
}
|
|
97
|
+
return uniqueCandidates(legacyAutoVerifyCandidates(repoRoot, record, reviewPaths));
|
|
98
|
+
}
|
|
99
|
+
function normalizeStructuredCandidate(repoRoot, item, reviewPaths) {
|
|
100
|
+
if (!item || typeof item !== "object") {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
const entry = item;
|
|
104
|
+
if (entry.schemaVersion !== 1 ||
|
|
105
|
+
typeof entry.taskId !== "string" ||
|
|
106
|
+
typeof entry.snapshotDigest !== "string" ||
|
|
107
|
+
typeof entry.commandId !== "string" ||
|
|
108
|
+
typeof entry.command !== "string" ||
|
|
109
|
+
typeof entry.commandExecutable !== "string" ||
|
|
110
|
+
!Array.isArray(entry.commandArgs) ||
|
|
111
|
+
!entry.commandArgs.every((arg) => typeof arg === "string") ||
|
|
112
|
+
typeof entry.commandCwd !== "string") {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
const targetPaths = pathsFromUnknown(entry.targetPaths);
|
|
116
|
+
return {
|
|
117
|
+
schemaVersion: 1,
|
|
118
|
+
taskId: entry.taskId,
|
|
119
|
+
snapshotDigest: entry.snapshotDigest,
|
|
120
|
+
commandId: entry.commandId,
|
|
121
|
+
command: materializeDisplayCommand(repoRoot, entry.command),
|
|
122
|
+
commandExecutable: entry.commandExecutable,
|
|
123
|
+
commandArgs: entry.commandArgs.map((arg) => materializePathValue(repoRoot, arg)),
|
|
124
|
+
commandCwd: materializePathValue(repoRoot, entry.commandCwd),
|
|
125
|
+
targetPaths,
|
|
126
|
+
source: entry.source ?? "legacy",
|
|
127
|
+
rank: typeof entry.rank === "number" ? entry.rank : 0,
|
|
128
|
+
protectedPaths: uniqueSorted([...reviewPaths, ...targetPaths])
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function legacyAutoVerifyCandidates(repoRoot, record, reviewPaths) {
|
|
132
|
+
const taskId = snapshotTaskId(record) ?? "latest";
|
|
133
|
+
const snapshotDigest = stableId("legacy-autoverify-snapshot", JSON.stringify(record.snapshot ?? record.snapshotLoad ?? {}));
|
|
134
|
+
const seen = new Set();
|
|
135
|
+
const candidates = [];
|
|
136
|
+
const add = (items, baseRank) => {
|
|
137
|
+
if (!Array.isArray(items)) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
items.forEach((item, index) => {
|
|
141
|
+
if (!item || typeof item !== "object") {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const entry = item;
|
|
145
|
+
const command = entry.command;
|
|
146
|
+
if (typeof command !== "string" || !command.trim() || seen.has(command)) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const commandExecutable = typeof entry.commandExecutable === "string" ? entry.commandExecutable : undefined;
|
|
150
|
+
const commandArgs = Array.isArray(entry.commandArgs) && entry.commandArgs.every((arg) => typeof arg === "string") ? entry.commandArgs : undefined;
|
|
151
|
+
const commandCwd = typeof entry.commandCwd === "string" ? entry.commandCwd : undefined;
|
|
152
|
+
if (!commandExecutable || !commandArgs || !commandCwd) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
seen.add(command);
|
|
156
|
+
const targetPaths = uniqueSorted([
|
|
157
|
+
...(typeof entry.path === "string" ? [entry.path] : []),
|
|
158
|
+
...provenanceTargetPaths(entry.provenance)
|
|
159
|
+
]);
|
|
160
|
+
const rank = typeof entry.rank === "number" ? entry.rank : baseRank - index;
|
|
161
|
+
const commandId = stableId("autoverify-command", taskId, command, commandCwd, JSON.stringify(commandArgs));
|
|
162
|
+
candidates.push({
|
|
163
|
+
schemaVersion: 1,
|
|
164
|
+
taskId,
|
|
165
|
+
snapshotDigest,
|
|
166
|
+
commandId,
|
|
167
|
+
command: materializeDisplayCommand(repoRoot, command),
|
|
168
|
+
commandExecutable,
|
|
169
|
+
commandArgs: commandArgs.map((arg) => materializePathValue(repoRoot, arg)),
|
|
170
|
+
commandCwd: materializePathValue(repoRoot, commandCwd),
|
|
171
|
+
targetPaths,
|
|
172
|
+
source: provenanceSource(entry.provenance),
|
|
173
|
+
rank,
|
|
174
|
+
protectedPaths: uniqueSorted([...reviewPaths, ...targetPaths])
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
add(record.testsNotRun, 2000);
|
|
179
|
+
add(record.missedLikelyTests, 1000);
|
|
180
|
+
return candidates.sort((a, b) => b.rank - a.rank || a.command.localeCompare(b.command));
|
|
181
|
+
}
|
|
182
|
+
function uniqueCandidates(candidates) {
|
|
183
|
+
const byCommand = new Map();
|
|
184
|
+
for (const candidate of candidates) {
|
|
185
|
+
const key = `${candidate.commandCwd}\0${candidate.commandExecutable}\0${candidate.commandArgs.join("\0")}`;
|
|
186
|
+
const existing = byCommand.get(key);
|
|
187
|
+
if (!existing || candidate.rank > existing.rank) {
|
|
188
|
+
byCommand.set(key, candidate);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return [...byCommand.values()].sort((a, b) => b.rank - a.rank || a.command.localeCompare(b.command));
|
|
192
|
+
}
|
|
193
|
+
function snapshotTaskId(record) {
|
|
194
|
+
const snapshot = record.snapshot;
|
|
195
|
+
if (snapshot && typeof snapshot === "object" && typeof snapshot.taskId === "string") {
|
|
196
|
+
return snapshot.taskId;
|
|
197
|
+
}
|
|
198
|
+
const snapshotLoad = record.snapshotLoad;
|
|
199
|
+
if (snapshotLoad && typeof snapshotLoad === "object" && typeof snapshotLoad.taskId === "string") {
|
|
200
|
+
return snapshotLoad.taskId;
|
|
201
|
+
}
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
function pathsFromUnknown(value) {
|
|
205
|
+
return Array.isArray(value) ? uniqueSorted(value.filter((entry) => typeof entry === "string")) : [];
|
|
206
|
+
}
|
|
207
|
+
function provenanceTargetPaths(value) {
|
|
208
|
+
if (!value || typeof value !== "object") {
|
|
209
|
+
return [];
|
|
210
|
+
}
|
|
211
|
+
return pathsFromUnknown(value.targetPaths);
|
|
212
|
+
}
|
|
213
|
+
function provenanceSource(value) {
|
|
214
|
+
if (!value || typeof value !== "object" || !Array.isArray(value.sources)) {
|
|
215
|
+
return "legacy";
|
|
216
|
+
}
|
|
217
|
+
const sources = value.sources;
|
|
218
|
+
if (sources.includes("explicit_target"))
|
|
219
|
+
return "explicit";
|
|
220
|
+
if (sources.includes("authoritative_test_edge"))
|
|
221
|
+
return "authoritative-test-edge";
|
|
222
|
+
if (sources.includes("derived_import") || sources.includes("derived_impact_expansion") || sources.includes("package_import") || sources.includes("outcome_history"))
|
|
223
|
+
return "derived-impact";
|
|
224
|
+
return "heuristic";
|
|
225
|
+
}
|
|
226
|
+
function materializeDisplayCommand(repoRoot, command) {
|
|
227
|
+
return command.replace(/<repo>(\/[^\s&|;]*)?/gu, (_match, suffix) => path.join(repoRoot, suffix ?? ""));
|
|
228
|
+
}
|
|
229
|
+
function materializePathValue(repoRoot, value) {
|
|
230
|
+
return value.replace(/<repo>(\/[^\s]*)?/gu, (_match, suffix) => path.join(repoRoot, suffix ?? ""));
|
|
231
|
+
}
|
|
232
|
+
async function safeAutoVerifyCommand(repoRoot, candidate) {
|
|
233
|
+
if (candidate.command.length > 1000 || /[\0\r\n]/u.test(candidate.command)) {
|
|
234
|
+
return { ok: false, reason: "unsupported command display" };
|
|
235
|
+
}
|
|
236
|
+
if (!safeExecutableName(candidate.commandExecutable) || candidate.commandArgs.some((arg) => /[\0\r\n]/u.test(arg))) {
|
|
237
|
+
return { ok: false, reason: "unsupported command argv" };
|
|
238
|
+
}
|
|
239
|
+
const repoRealRoot = await realpathOrUndefined(repoRoot);
|
|
240
|
+
if (!repoRealRoot) {
|
|
241
|
+
return { ok: false, reason: "repo root is unavailable" };
|
|
242
|
+
}
|
|
243
|
+
const cwd = path.isAbsolute(candidate.commandCwd) ? path.resolve(candidate.commandCwd) : path.resolve(repoRoot, candidate.commandCwd);
|
|
244
|
+
if (!isSubpath(cwd, repoRoot)) {
|
|
245
|
+
return { ok: false, reason: "command cwd is outside repo" };
|
|
246
|
+
}
|
|
247
|
+
const cwdReal = await realpathOrUndefined(cwd);
|
|
248
|
+
if (!cwdReal || !isSubpath(cwdReal, repoRealRoot)) {
|
|
249
|
+
return { ok: false, reason: "command cwd is outside repo" };
|
|
250
|
+
}
|
|
251
|
+
const words = [candidate.commandExecutable, ...candidate.commandArgs];
|
|
252
|
+
const safety = await safeRunnerInvocation(repoRoot, repoRealRoot, cwd, cwdReal, words);
|
|
253
|
+
if (!safety.ok) {
|
|
254
|
+
return safety;
|
|
255
|
+
}
|
|
256
|
+
const executable = safety.executable ?? candidate.commandExecutable;
|
|
257
|
+
const args = safety.args ?? candidate.commandArgs;
|
|
258
|
+
const resolvedExecutable = await resolveAllowlistedExecutable(executable, repoRealRoot, { packageBinRoot: safety.packageBinRoot, args });
|
|
259
|
+
if (!resolvedExecutable) {
|
|
260
|
+
return { ok: false, reason: "runner executable is unavailable or unsafe" };
|
|
261
|
+
}
|
|
262
|
+
const pathEnv = await safeRunnerPathEnv(repoRealRoot, cwdReal, resolvedExecutable.pathEnvExecutable);
|
|
263
|
+
return {
|
|
264
|
+
ok: true,
|
|
265
|
+
command: {
|
|
266
|
+
candidate,
|
|
267
|
+
command: candidate.command,
|
|
268
|
+
cwd,
|
|
269
|
+
spawnCwd: cwdReal,
|
|
270
|
+
executable,
|
|
271
|
+
spawnExecutable: resolvedExecutable.spawnExecutable,
|
|
272
|
+
spawnArgs: resolvedExecutable.spawnArgs,
|
|
273
|
+
args,
|
|
274
|
+
reportArgs: safety.reportArgs,
|
|
275
|
+
packageManager: safety.packageManager,
|
|
276
|
+
packageRoot: safety.packageRoot,
|
|
277
|
+
scriptName: safety.scriptName,
|
|
278
|
+
targetRealpaths: safety.targetRealpaths,
|
|
279
|
+
allowedBy: safety.allowedBy,
|
|
280
|
+
pathEnv
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
async function safeRunnerInvocation(repoRoot, repoRealRoot, cwd, cwdReal, words) {
|
|
285
|
+
const first = words[0];
|
|
286
|
+
if ((first === "npm" || first === "pnpm") && words[1] === "run" && words[2]) {
|
|
287
|
+
return safePackageTestScript(repoRoot, repoRealRoot, cwd, cwdReal, first, words[2], words.slice(3));
|
|
288
|
+
}
|
|
289
|
+
if ((first === "npm" || first === "pnpm") && (words[1] === "test" || words[1] === "t")) {
|
|
290
|
+
return safePackageTestScript(repoRoot, repoRealRoot, cwd, cwdReal, first, "test", words.slice(2));
|
|
291
|
+
}
|
|
292
|
+
if (first === "yarn" && words[1]) {
|
|
293
|
+
const scriptName = words[1] === "run" ? words[2] : words[1];
|
|
294
|
+
return scriptName ? safePackageTestScript(repoRoot, repoRealRoot, cwd, cwdReal, "yarn", scriptName, words.slice(words[1] === "run" ? 3 : 2)) : { ok: false, reason: "missing yarn script" };
|
|
295
|
+
}
|
|
296
|
+
if (first === "node" && words[1] === "--test") {
|
|
297
|
+
const target = await safeTargetArgs(repoRoot, repoRealRoot, cwd, cwdReal, words.slice(2), { runner: "node" });
|
|
298
|
+
return target.ok ? { ok: true, targetRealpaths: target.targetRealpaths, allowedBy: ["direct node --test target"], reportArgs: words.slice(1), packageManager: "node", scriptName: "node --test" } : target;
|
|
299
|
+
}
|
|
300
|
+
if (first === "vitest" || first === "jest") {
|
|
301
|
+
const runnerArgs = first === "vitest" && words[1] === "run" ? words.slice(2) : words.slice(1);
|
|
302
|
+
const target = await safeTargetArgs(repoRoot, repoRealRoot, cwd, cwdReal, runnerArgs, { runner: first });
|
|
303
|
+
return target.ok ? { ok: true, targetRealpaths: target.targetRealpaths, allowedBy: [`direct ${first} target`], reportArgs: words.slice(1), packageManager: first, scriptName: first } : target;
|
|
304
|
+
}
|
|
305
|
+
if (first === "pytest") {
|
|
306
|
+
const target = await safeTargetArgs(repoRoot, repoRealRoot, cwd, cwdReal, words.slice(1), { runner: "pytest" });
|
|
307
|
+
return target.ok ? { ok: true, targetRealpaths: target.targetRealpaths, allowedBy: ["direct pytest target"], reportArgs: words.slice(1), packageManager: "pytest", scriptName: "pytest" } : target;
|
|
308
|
+
}
|
|
309
|
+
if (first === "uv" && words[1] === "run" && words[2] === "pytest") {
|
|
310
|
+
const target = await safeTargetArgs(repoRoot, repoRealRoot, cwd, cwdReal, words.slice(3), { runner: "pytest" });
|
|
311
|
+
return target.ok ? { ok: true, targetRealpaths: target.targetRealpaths, allowedBy: ["direct uv pytest target"], reportArgs: words.slice(1), packageManager: "pytest", scriptName: "pytest" } : target;
|
|
312
|
+
}
|
|
313
|
+
if ((first === "python" || first === "python3") && words[1] === "-m" && words[2] === "pytest") {
|
|
314
|
+
const target = await safeTargetArgs(repoRoot, repoRealRoot, cwd, cwdReal, words.slice(3), { runner: "pytest" });
|
|
315
|
+
return target.ok ? { ok: true, targetRealpaths: target.targetRealpaths, allowedBy: [`direct ${first} -m pytest target`], reportArgs: words.slice(1), packageManager: "pytest", scriptName: "pytest" } : target;
|
|
316
|
+
}
|
|
317
|
+
return { ok: false, reason: "runner is not allowlisted" };
|
|
318
|
+
}
|
|
319
|
+
async function safePackageTestScript(repoRoot, repoRealRoot, cwd, cwdReal, packageManager, scriptName, args) {
|
|
320
|
+
if (!/^test(?::[\w:-]+)?$/u.test(scriptName)) {
|
|
321
|
+
return { ok: false, reason: "package script is not a test script" };
|
|
322
|
+
}
|
|
323
|
+
const forwarded = args[0] === "--" ? args.slice(1) : args;
|
|
324
|
+
const target = await safeTargetArgs(repoRoot, repoRealRoot, cwd, cwdReal, forwarded, { runner: "package" });
|
|
325
|
+
if (!target.ok) {
|
|
326
|
+
return target;
|
|
327
|
+
}
|
|
328
|
+
const scripts = await packageScripts(cwd);
|
|
329
|
+
const script = scripts?.[scriptName];
|
|
330
|
+
if (!script || typeof script !== "string") {
|
|
331
|
+
return { ok: false, reason: "package test script was not found" };
|
|
332
|
+
}
|
|
333
|
+
if (scripts?.[`pre${scriptName}`] || scripts?.[`post${scriptName}`]) {
|
|
334
|
+
return { ok: false, reason: "package test script has lifecycle hooks" };
|
|
335
|
+
}
|
|
336
|
+
if (!safeScriptBody(script)) {
|
|
337
|
+
return { ok: false, reason: "package test script is not safe to auto-run" };
|
|
338
|
+
}
|
|
339
|
+
const invocation = directPackageScriptInvocation(shellWords(script), forwarded);
|
|
340
|
+
if (!invocation) {
|
|
341
|
+
return { ok: false, reason: "package test script is not safe to auto-run" };
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
ok: true,
|
|
345
|
+
targetRealpaths: target.targetRealpaths,
|
|
346
|
+
allowedBy: [`${packageManager} ${scriptName} targeted test script via direct ${invocation.executable} runner`],
|
|
347
|
+
reportArgs: args,
|
|
348
|
+
executable: invocation.executable,
|
|
349
|
+
args: invocation.args,
|
|
350
|
+
packageBinRoot: cwdReal,
|
|
351
|
+
packageManager,
|
|
352
|
+
packageRoot: repoRelativeRealPath(repoRealRoot, cwdReal),
|
|
353
|
+
scriptName
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function directPackageScriptInvocation(words, forwarded) {
|
|
357
|
+
const first = words[0];
|
|
358
|
+
if (first === "node" && words[1] === "--test" && words.length === 2) {
|
|
359
|
+
return { executable: "node", args: ["--test", ...forwarded] };
|
|
360
|
+
}
|
|
361
|
+
if (first === "vitest" && (words.length === 1 || (words.length === 2 && words[1] === "run"))) {
|
|
362
|
+
return { executable: "vitest", args: ["run", ...forwarded] };
|
|
363
|
+
}
|
|
364
|
+
if ((first === "jest" || first === "pytest") && words.length === 1) {
|
|
365
|
+
return { executable: first, args: forwarded };
|
|
366
|
+
}
|
|
367
|
+
if ((first === "python" || first === "python3") && words.length === 3 && words[1] === "-m" && words[2] === "pytest") {
|
|
368
|
+
return { executable: first, args: ["-m", "pytest", ...forwarded] };
|
|
369
|
+
}
|
|
370
|
+
if (first === "uv" && words.length === 3 && words[1] === "run" && words[2] === "pytest") {
|
|
371
|
+
return { executable: "uv", args: ["run", "pytest", ...forwarded] };
|
|
372
|
+
}
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
async function packageScripts(cwd) {
|
|
376
|
+
const packagePath = path.join(cwd, "package.json");
|
|
377
|
+
try {
|
|
378
|
+
const parsed = JSON.parse(await fs.readFile(packagePath, "utf8"));
|
|
379
|
+
return parsed.scripts;
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function safeScriptBody(script) {
|
|
386
|
+
if (/[\0\r\n;&|`$<>]/u.test(script) || /\$\(/u.test(script)) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
const words = shellWords(script);
|
|
390
|
+
if (words.some((word) => /^[A-Za-z_][A-Za-z0-9_]*=/u.test(word))) {
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
const first = words[0];
|
|
394
|
+
if (first === "vitest") {
|
|
395
|
+
return words.length === 1 || (words.length === 2 && words[1] === "run");
|
|
396
|
+
}
|
|
397
|
+
if (first === "jest" || first === "pytest") {
|
|
398
|
+
return words.length === 1;
|
|
399
|
+
}
|
|
400
|
+
if (first === "node") {
|
|
401
|
+
return words.length === 2 && words[1] === "--test";
|
|
402
|
+
}
|
|
403
|
+
if (first === "python" || first === "python3") {
|
|
404
|
+
return words.length === 3 && words[1] === "-m" && words[2] === "pytest";
|
|
405
|
+
}
|
|
406
|
+
if (first === "uv") {
|
|
407
|
+
return words.length === 3 && words[1] === "run" && words[2] === "pytest";
|
|
408
|
+
}
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
async function safeTargetArgs(repoRoot, repoRealRoot, cwd, cwdReal, args, options) {
|
|
412
|
+
const targets = [];
|
|
413
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
414
|
+
const arg = args[index];
|
|
415
|
+
if (arg === "--") {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (arg.startsWith("-")) {
|
|
419
|
+
return { ok: false, reason: `${options.runner} command uses unsupported flag ${arg}` };
|
|
420
|
+
}
|
|
421
|
+
targets.push(arg);
|
|
422
|
+
}
|
|
423
|
+
if (targets.length === 0) {
|
|
424
|
+
return { ok: false, reason: `${options.runner} command is not targeted` };
|
|
425
|
+
}
|
|
426
|
+
const targetRealpaths = [];
|
|
427
|
+
for (const target of targets) {
|
|
428
|
+
const real = await repoTestTargetRealpath(repoRoot, repoRealRoot, cwd, cwdReal, target);
|
|
429
|
+
if (!real) {
|
|
430
|
+
return { ok: false, reason: `${options.runner} command is not targeted` };
|
|
431
|
+
}
|
|
432
|
+
targetRealpaths.push(real);
|
|
433
|
+
}
|
|
434
|
+
return { ok: true, targetRealpaths: uniqueSorted(targetRealpaths) };
|
|
435
|
+
}
|
|
436
|
+
async function repoTestTargetRealpath(repoRoot, repoRealRoot, cwd, cwdReal, value) {
|
|
437
|
+
if (/[\0\r\n;&|`$<>:*?[\]{}]/u.test(value)) {
|
|
438
|
+
return undefined;
|
|
439
|
+
}
|
|
440
|
+
const absolute = path.resolve(cwd, value);
|
|
441
|
+
if (!isSubpath(absolute, repoRoot)) {
|
|
442
|
+
return undefined;
|
|
443
|
+
}
|
|
444
|
+
const real = await realpathOrUndefined(path.resolve(cwdReal, path.relative(cwd, absolute)));
|
|
445
|
+
if (!real || !isSubpath(real, repoRealRoot)) {
|
|
446
|
+
return undefined;
|
|
447
|
+
}
|
|
448
|
+
const relative = path.relative(repoRoot, absolute).split(path.sep).join("/");
|
|
449
|
+
const realRelative = path.relative(repoRealRoot, real).split(path.sep).join("/");
|
|
450
|
+
return isTestPath(relative) || isTestPath(realRelative) ? real : undefined;
|
|
451
|
+
}
|
|
452
|
+
async function resolveAllowlistedExecutable(executable, repoRealRoot, options = {}) {
|
|
453
|
+
const args = options.args ?? [];
|
|
454
|
+
if (executable === "node") {
|
|
455
|
+
const nodeReal = await realpathOrUndefined(process.execPath);
|
|
456
|
+
return nodeReal && !isUnsafeExecutableRealpath(nodeReal, repoRealRoot) ? directExecutableResolution(nodeReal, args) : undefined;
|
|
457
|
+
}
|
|
458
|
+
if (options.packageBinRoot) {
|
|
459
|
+
const localBin = await packageLocalBinExecutable(executable, options.packageBinRoot);
|
|
460
|
+
if (localBin) {
|
|
461
|
+
return await packageLocalBinResolution(localBin, args, repoRealRoot);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const pathValue = process.env.PATH ?? "";
|
|
465
|
+
for (const dir of pathValue.split(path.delimiter)) {
|
|
466
|
+
if (!dir || !path.isAbsolute(dir)) {
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
const absoluteDir = path.resolve(dir);
|
|
470
|
+
if (isUnsafePathCandidate(absoluteDir, repoRealRoot)) {
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
if (await isUnsafeSearchDir(absoluteDir, repoRealRoot)) {
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
const candidate = path.join(absoluteDir, executable);
|
|
477
|
+
if (isUnsafePathCandidate(candidate, repoRealRoot)) {
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
const real = await executableRealpath(candidate);
|
|
481
|
+
if (!real || isUnsafeExecutableRealpath(real, repoRealRoot) || (await isWorldWritableDir(path.dirname(real)))) {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
return directExecutableResolution(real, args);
|
|
485
|
+
}
|
|
486
|
+
return undefined;
|
|
487
|
+
}
|
|
488
|
+
function directExecutableResolution(executablePath, args) {
|
|
489
|
+
return {
|
|
490
|
+
executablePath,
|
|
491
|
+
spawnExecutable: executablePath,
|
|
492
|
+
spawnArgs: args,
|
|
493
|
+
pathEnvExecutable: executablePath
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
async function packageLocalBinResolution(localBin, args, repoRealRoot) {
|
|
497
|
+
if (process.platform !== "win32" || path.extname(localBin).toLowerCase() !== ".cmd") {
|
|
498
|
+
return directExecutableResolution(localBin, args);
|
|
499
|
+
}
|
|
500
|
+
if (![localBin, ...args].every(isSafeWindowsCmdArgument)) {
|
|
501
|
+
return undefined;
|
|
502
|
+
}
|
|
503
|
+
const shell = await resolveWindowsCommandShell(repoRealRoot);
|
|
504
|
+
if (!shell) {
|
|
505
|
+
return undefined;
|
|
506
|
+
}
|
|
507
|
+
return {
|
|
508
|
+
executablePath: localBin,
|
|
509
|
+
spawnExecutable: shell,
|
|
510
|
+
spawnArgs: ["/d", "/v:off", "/c", "call", localBin, ...args],
|
|
511
|
+
pathEnvExecutable: shell
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
async function resolveWindowsCommandShell(repoRealRoot) {
|
|
515
|
+
const comspec = process.env.ComSpec;
|
|
516
|
+
if (comspec && path.isAbsolute(comspec)) {
|
|
517
|
+
const real = await executableRealpath(comspec);
|
|
518
|
+
if (real && !isUnsafeExecutableRealpath(real, repoRealRoot) && !(await isWorldWritableDir(path.dirname(real)))) {
|
|
519
|
+
return real;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
const pathValue = process.env.PATH ?? "";
|
|
523
|
+
for (const dir of pathValue.split(path.delimiter)) {
|
|
524
|
+
if (!dir || !path.isAbsolute(dir)) {
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
const absoluteDir = path.resolve(dir);
|
|
528
|
+
if (isUnsafePathCandidate(absoluteDir, repoRealRoot) || (await isUnsafeSearchDir(absoluteDir, repoRealRoot))) {
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
const candidate = path.join(absoluteDir, "cmd.exe");
|
|
532
|
+
const real = await executableRealpath(candidate);
|
|
533
|
+
if (real && !isUnsafeExecutableRealpath(real, repoRealRoot) && !(await isWorldWritableDir(path.dirname(real)))) {
|
|
534
|
+
return real;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return undefined;
|
|
538
|
+
}
|
|
539
|
+
function isSafeWindowsCmdArgument(value) {
|
|
540
|
+
return value.length > 0 && !/[\0\r\n"%!^]/u.test(value);
|
|
541
|
+
}
|
|
542
|
+
async function packageLocalBinExecutable(executable, packageRoot) {
|
|
543
|
+
const nodeModules = path.join(packageRoot, "node_modules");
|
|
544
|
+
const nodeModulesReal = await realpathOrUndefined(nodeModules);
|
|
545
|
+
if (!nodeModulesReal) {
|
|
546
|
+
return undefined;
|
|
547
|
+
}
|
|
548
|
+
const binCandidates = process.platform === "win32"
|
|
549
|
+
? [path.join(nodeModules, ".bin", `${executable}.cmd`), path.join(nodeModules, ".bin", executable)]
|
|
550
|
+
: [path.join(nodeModules, ".bin", executable)];
|
|
551
|
+
for (const candidate of binCandidates) {
|
|
552
|
+
const real = await executableRealpath(candidate);
|
|
553
|
+
if (real && isSubpath(real, nodeModulesReal)) {
|
|
554
|
+
return real;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return undefined;
|
|
558
|
+
}
|
|
559
|
+
function isUnsafePathCandidate(candidate, repoRealRoot) {
|
|
560
|
+
const normalized = normalizePathLike(candidate);
|
|
561
|
+
return isSubpath(candidate, repoRealRoot) || hasNodeModulesBinSegment(normalized);
|
|
562
|
+
}
|
|
563
|
+
function hasNodeModulesBinSegment(value) {
|
|
564
|
+
const segments = value.split("/");
|
|
565
|
+
return segments.some((segment, index) => segment === "node_modules" && segments[index + 1] === ".bin");
|
|
566
|
+
}
|
|
567
|
+
async function isUnsafeSearchDir(dir, repoRealRoot) {
|
|
568
|
+
const real = await realpathOrUndefined(dir);
|
|
569
|
+
return !real || isUnsafePathCandidate(real, repoRealRoot) || (await isWorldWritableDir(real));
|
|
570
|
+
}
|
|
571
|
+
async function isWorldWritableDir(dir) {
|
|
572
|
+
try {
|
|
573
|
+
const stat = await fs.stat(dir);
|
|
574
|
+
return stat.isDirectory() && (stat.mode & 0o002) !== 0;
|
|
575
|
+
}
|
|
576
|
+
catch {
|
|
577
|
+
return true;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
async function executableRealpath(candidate) {
|
|
581
|
+
try {
|
|
582
|
+
await fs.access(candidate, fsConstants.X_OK);
|
|
583
|
+
const stat = await fs.stat(candidate);
|
|
584
|
+
if (!stat.isFile()) {
|
|
585
|
+
return undefined;
|
|
586
|
+
}
|
|
587
|
+
return await fs.realpath(candidate);
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
return undefined;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
function isUnsafeExecutableRealpath(realpathValue, repoRealRoot) {
|
|
594
|
+
return isUnsafePathCandidate(realpathValue, repoRealRoot);
|
|
595
|
+
}
|
|
596
|
+
async function safeRunnerPathEnv(repoRealRoot, cwdReal, executableRealpathValue) {
|
|
597
|
+
const dirs = new Set([path.dirname(process.execPath)]);
|
|
598
|
+
const executableDir = path.dirname(executableRealpathValue);
|
|
599
|
+
if (!isUnsafePathCandidate(executableDir, repoRealRoot) && !(await isUnsafeSearchDir(executableDir, repoRealRoot))) {
|
|
600
|
+
dirs.add(executableDir);
|
|
601
|
+
}
|
|
602
|
+
for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
603
|
+
if (!dir || !path.isAbsolute(dir)) {
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
const absoluteDir = path.resolve(dir);
|
|
607
|
+
if (isUnsafePathCandidate(absoluteDir, repoRealRoot) || isSubpath(absoluteDir, cwdReal) || (await isUnsafeSearchDir(absoluteDir, repoRealRoot))) {
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
const real = await realpathOrUndefined(path.resolve(dir));
|
|
611
|
+
if (!real || isUnsafePathCandidate(real, repoRealRoot) || isSubpath(real, cwdReal) || (await isWorldWritableDir(real))) {
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
dirs.add(real);
|
|
615
|
+
}
|
|
616
|
+
return [...dirs].join(path.delimiter);
|
|
617
|
+
}
|
|
618
|
+
async function runVerificationCommand(repoRoot, command, before, timeoutMs) {
|
|
619
|
+
const startedAtMs = Date.now();
|
|
620
|
+
const startedAt = new Date(startedAtMs).toISOString();
|
|
621
|
+
const tempHome = await createRunnerHome();
|
|
622
|
+
let result;
|
|
623
|
+
try {
|
|
624
|
+
result = await new Promise((resolve) => {
|
|
625
|
+
const detached = process.platform !== "win32";
|
|
626
|
+
const child = spawn(command.spawnExecutable, command.spawnArgs, {
|
|
627
|
+
cwd: command.spawnCwd,
|
|
628
|
+
env: minimalChildEnv(tempHome, command.pathEnv),
|
|
629
|
+
detached,
|
|
630
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
631
|
+
});
|
|
632
|
+
let stdout = "";
|
|
633
|
+
let stderr = "";
|
|
634
|
+
let timedOut = false;
|
|
635
|
+
let closed = false;
|
|
636
|
+
let killTimer;
|
|
637
|
+
let hardKillTimer;
|
|
638
|
+
const killChild = (signal) => {
|
|
639
|
+
if (!child.pid || closed) {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
try {
|
|
643
|
+
process.kill(detached ? -child.pid : child.pid, signal);
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
// The process may already have exited; the close handler will settle the report.
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
killTimer = setTimeout(() => {
|
|
650
|
+
timedOut = true;
|
|
651
|
+
killChild("SIGTERM");
|
|
652
|
+
hardKillTimer = setTimeout(() => killChild("SIGKILL"), 2_000);
|
|
653
|
+
hardKillTimer.unref();
|
|
654
|
+
}, timeoutMs);
|
|
655
|
+
child.stdout.setEncoding("utf8");
|
|
656
|
+
child.stderr.setEncoding("utf8");
|
|
657
|
+
child.stdout.on("data", (chunk) => {
|
|
658
|
+
stdout = boundedAppend(stdout, chunk);
|
|
659
|
+
});
|
|
660
|
+
child.stderr.on("data", (chunk) => {
|
|
661
|
+
stderr = boundedAppend(stderr, chunk);
|
|
662
|
+
});
|
|
663
|
+
child.on("error", (error) => {
|
|
664
|
+
stderr = boundedAppend(stderr, error.message);
|
|
665
|
+
});
|
|
666
|
+
child.on("close", (code, signal) => {
|
|
667
|
+
closed = true;
|
|
668
|
+
if (killTimer)
|
|
669
|
+
clearTimeout(killTimer);
|
|
670
|
+
if (hardKillTimer)
|
|
671
|
+
clearTimeout(hardKillTimer);
|
|
672
|
+
resolve({ exitCode: timedOut ? 124 : code ?? 1, stdout, stderr, timedOut, signal: signal ?? undefined });
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
finally {
|
|
677
|
+
await fs.rm(tempHome, { recursive: true, force: true }).catch(() => undefined);
|
|
678
|
+
}
|
|
679
|
+
const after = await runnerDirtyState(repoRoot, command.candidate.protectedPaths);
|
|
680
|
+
const sourceMutationDetected = sourceMutationBetween(before, after, command.candidate.protectedPaths);
|
|
681
|
+
const finishedAt = new Date().toISOString();
|
|
682
|
+
const runnerBase = {
|
|
683
|
+
schemaVersion: 1,
|
|
684
|
+
reportKind: "codexa-autoverify-report",
|
|
685
|
+
runnerName: "codexa",
|
|
686
|
+
runnerVersion: process.env.npm_package_version ?? "0.0.0",
|
|
687
|
+
policyId: AUTO_VERIFY_POLICY_ID,
|
|
688
|
+
policyDigest: AUTO_VERIFY_POLICY_DIGEST,
|
|
689
|
+
taskId: command.candidate.taskId,
|
|
690
|
+
snapshotDigest: command.candidate.snapshotDigest,
|
|
691
|
+
commandId: command.candidate.commandId,
|
|
692
|
+
candidateDigest: candidateDigest(command.candidate),
|
|
693
|
+
headCommit: after.headCommit,
|
|
694
|
+
dirtyHashBefore: dirtyStateHash(before),
|
|
695
|
+
dirtyHashAfter: dirtyStateHash(after),
|
|
696
|
+
cwdRealpath: command.spawnCwd,
|
|
697
|
+
targetRealpaths: command.targetRealpaths,
|
|
698
|
+
envMode: "minimal",
|
|
699
|
+
allowedBy: command.allowedBy,
|
|
700
|
+
sourceMutationDetected,
|
|
701
|
+
timedOut: result.timedOut,
|
|
702
|
+
startedAt,
|
|
703
|
+
finishedAt,
|
|
704
|
+
signal: result.signal,
|
|
705
|
+
outputRedacted: true,
|
|
706
|
+
skippedReason: before.degradedReason ?? after.degradedReason
|
|
707
|
+
};
|
|
708
|
+
const stdoutSummary = summarizeOutput(result.stdout, repoRoot);
|
|
709
|
+
const stderrSummary = summarizeOutput(result.timedOut ? `${result.stderr}\nTimed out after ${timeoutMs}ms` : result.stderr, repoRoot);
|
|
710
|
+
const report = {
|
|
711
|
+
command: command.command,
|
|
712
|
+
cwd: command.cwd,
|
|
713
|
+
packageManager: command.packageManager,
|
|
714
|
+
packageRoot: command.packageRoot,
|
|
715
|
+
scriptName: command.scriptName,
|
|
716
|
+
args: command.reportArgs ?? command.args,
|
|
717
|
+
exitCode: result.exitCode,
|
|
718
|
+
durationMs: Date.now() - startedAtMs,
|
|
719
|
+
stdoutSummary,
|
|
720
|
+
stderrSummary,
|
|
721
|
+
runner: {
|
|
722
|
+
...runnerBase,
|
|
723
|
+
canonicalDigest: stableId("codexa-autoverify-report", command.command, result.exitCode, runnerBase.policyId, runnerBase.policyDigest, runnerBase.taskId, runnerBase.snapshotDigest, runnerBase.commandId, runnerBase.candidateDigest, runnerBase.headCommit ?? "null", runnerBase.dirtyHashBefore, runnerBase.dirtyHashAfter, runnerBase.cwdRealpath, JSON.stringify(runnerBase.targetRealpaths), runnerBase.envMode, JSON.stringify(runnerBase.allowedBy), runnerBase.sourceMutationDetected ? "mutated" : "clean", runnerBase.timedOut ? "timed-out" : "not-timed-out", runnerBase.outputRedacted ? "redacted" : "not-redacted", runnerBase.signal ?? "", runnerBase.skippedReason ?? "")
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
markTrustedAutoVerifyReport(report);
|
|
727
|
+
Object.freeze(report.runner.targetRealpaths);
|
|
728
|
+
Object.freeze(report.runner.allowedBy);
|
|
729
|
+
Object.freeze(report.runner);
|
|
730
|
+
if (report.args) {
|
|
731
|
+
Object.freeze(report.args);
|
|
732
|
+
}
|
|
733
|
+
Object.freeze(report);
|
|
734
|
+
return report;
|
|
735
|
+
}
|
|
736
|
+
async function runnerDirtyState(repoRoot, protectedPaths) {
|
|
737
|
+
const repo = path.resolve(repoRoot);
|
|
738
|
+
const [git, status] = await Promise.all([
|
|
739
|
+
getGitStateAsync(repo, { includeFiles: false, includeChurn: false }).catch((error) => ({ error })),
|
|
740
|
+
gitText(repo, ["status", "--porcelain=v1", "-z", "--untracked-files=all"], false)
|
|
741
|
+
]);
|
|
742
|
+
if ("error" in git) {
|
|
743
|
+
return {
|
|
744
|
+
headCommit: null,
|
|
745
|
+
dirtyFiles: [],
|
|
746
|
+
dirtyFileHashes: {},
|
|
747
|
+
allStatuses: new Map(),
|
|
748
|
+
protectedHashes: new Map(),
|
|
749
|
+
untrackedProtectedFiles: new Set(),
|
|
750
|
+
worktreeRoot: repo,
|
|
751
|
+
fullStatuses: new Map(),
|
|
752
|
+
fullProtectedHashes: new Map(),
|
|
753
|
+
fullUntrackedProtectedFiles: new Set(),
|
|
754
|
+
degradedReason: git.error instanceof Error ? git.error.message : "git state unavailable"
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
const relativePrefix = git.gitRoot ? normalizePathLike(path.relative(git.gitRoot, repo)) : "";
|
|
758
|
+
const rawStatuses = parsePorcelainStatus(status ?? "");
|
|
759
|
+
const statuses = normalizeStatusPaths(rawStatuses, git.gitRoot, relativePrefix);
|
|
760
|
+
const fullStatuses = normalizeFullStatusPaths(rawStatuses);
|
|
761
|
+
const worktreeRoot = git.gitRoot ? path.resolve(git.gitRoot) : repo;
|
|
762
|
+
const visibleDirtyFiles = git.dirtyFiles.filter((file) => !isCodexaGenerated(file)).sort();
|
|
763
|
+
const dirtyFileHashes = await hashFiles(repo, visibleDirtyFiles);
|
|
764
|
+
const protectedSet = new Set([
|
|
765
|
+
...protectedPaths.map(normalizePathLike),
|
|
766
|
+
...[...statuses.keys()].filter((file) => protectedMutationPath(file) && statuses.get(file) !== undefined)
|
|
767
|
+
]);
|
|
768
|
+
const fullProtectedSet = new Set([...fullStatuses.keys()].filter((file) => protectedMutationPath(file)));
|
|
769
|
+
const protectedHashes = await hashProtectedFiles(repo, [...protectedSet]);
|
|
770
|
+
const untrackedProtectedFiles = new Set([...statuses.entries()].filter(([file, statusValue]) => statusValue === "??" && protectedMutationPath(file)).map(([file]) => file));
|
|
771
|
+
const fullProtectedHashes = await hashProtectedFiles(worktreeRoot, [...fullProtectedSet]);
|
|
772
|
+
const fullUntrackedProtectedFiles = new Set([...fullStatuses.entries()].filter(([file, statusValue]) => statusValue === "??" && protectedMutationPath(file)).map(([file]) => file));
|
|
773
|
+
return {
|
|
774
|
+
headCommit: git.headCommit,
|
|
775
|
+
dirtyFiles: visibleDirtyFiles,
|
|
776
|
+
dirtyFileHashes,
|
|
777
|
+
allStatuses: statuses,
|
|
778
|
+
protectedHashes,
|
|
779
|
+
untrackedProtectedFiles,
|
|
780
|
+
worktreeRoot,
|
|
781
|
+
fullStatuses,
|
|
782
|
+
fullProtectedHashes,
|
|
783
|
+
fullUntrackedProtectedFiles,
|
|
784
|
+
degradedReason: status === null ? "git status unavailable" : undefined
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
function sourceMutationBetween(before, after, protectedPaths) {
|
|
788
|
+
if (before.degradedReason || after.degradedReason) {
|
|
789
|
+
return true;
|
|
790
|
+
}
|
|
791
|
+
const protectedSet = new Set(protectedPaths.map(normalizePathLike));
|
|
792
|
+
for (const [file, beforeStatus] of before.allStatuses) {
|
|
793
|
+
const afterStatus = after.allStatuses.get(file);
|
|
794
|
+
if (afterStatus !== beforeStatus && protectedMutationPath(file)) {
|
|
795
|
+
return true;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
for (const [file, afterStatus] of after.allStatuses) {
|
|
799
|
+
const beforeStatus = before.allStatuses.get(file);
|
|
800
|
+
if (beforeStatus !== afterStatus && protectedMutationPath(file)) {
|
|
801
|
+
return true;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
for (const [file, beforeHash] of before.protectedHashes) {
|
|
805
|
+
const afterHash = after.protectedHashes.get(file);
|
|
806
|
+
if (afterHash !== beforeHash && (protectedSet.has(file) || protectedMutationPath(file))) {
|
|
807
|
+
return true;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
for (const file of after.untrackedProtectedFiles) {
|
|
811
|
+
if (!before.untrackedProtectedFiles.has(file)) {
|
|
812
|
+
return true;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
for (const [file, beforeStatus] of before.fullStatuses) {
|
|
816
|
+
const afterStatus = after.fullStatuses.get(file);
|
|
817
|
+
if (afterStatus !== beforeStatus && protectedMutationPath(file)) {
|
|
818
|
+
return true;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
for (const [file, afterStatus] of after.fullStatuses) {
|
|
822
|
+
const beforeStatus = before.fullStatuses.get(file);
|
|
823
|
+
if (beforeStatus !== afterStatus && protectedMutationPath(file)) {
|
|
824
|
+
return true;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
for (const [file, beforeHash] of before.fullProtectedHashes) {
|
|
828
|
+
const afterHash = after.fullProtectedHashes.get(file);
|
|
829
|
+
if (afterHash !== beforeHash && protectedMutationPath(file)) {
|
|
830
|
+
return true;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
for (const file of after.fullUntrackedProtectedFiles) {
|
|
834
|
+
if (!before.fullUntrackedProtectedFiles.has(file)) {
|
|
835
|
+
return true;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return false;
|
|
839
|
+
}
|
|
840
|
+
function dirtyStateHash(state) {
|
|
841
|
+
return autoVerifyDirtyHashFromParts({
|
|
842
|
+
headCommit: state.headCommit,
|
|
843
|
+
dirtyFiles: state.dirtyFiles,
|
|
844
|
+
dirtyFileHashes: state.dirtyFileHashes
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
function candidateDigest(candidate) {
|
|
848
|
+
return stableId("autoverify-candidate", candidate.taskId, candidate.snapshotDigest, candidate.commandId, candidate.command, candidate.commandCwd, candidate.commandExecutable, JSON.stringify(candidate.commandArgs), JSON.stringify(candidate.targetPaths));
|
|
849
|
+
}
|
|
850
|
+
async function createRunnerHome() {
|
|
851
|
+
const home = await fs.mkdtemp(path.join(os.tmpdir(), "codexa-autoverify-home-"));
|
|
852
|
+
await fs.mkdir(path.join(home, ".config"), { recursive: true });
|
|
853
|
+
await fs.writeFile(path.join(home, ".npmrc"), "", "utf8");
|
|
854
|
+
return home;
|
|
855
|
+
}
|
|
856
|
+
function minimalChildEnv(home, pathEnv) {
|
|
857
|
+
const env = {};
|
|
858
|
+
for (const key of ["TMPDIR", "TEMP", "TMP", "SystemRoot", "WINDIR"]) {
|
|
859
|
+
if (process.env[key]) {
|
|
860
|
+
env[key] = process.env[key];
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
env.PATH = pathEnv;
|
|
864
|
+
env.HOME = home;
|
|
865
|
+
env.USERPROFILE = home;
|
|
866
|
+
env.XDG_CONFIG_HOME = path.join(home, ".config");
|
|
867
|
+
env.NPM_CONFIG_USERCONFIG = path.join(home, ".npmrc");
|
|
868
|
+
env.NPM_CONFIG_CACHE = path.join(home, ".npm-cache");
|
|
869
|
+
env.YARN_CACHE_FOLDER = path.join(home, ".yarn-cache");
|
|
870
|
+
env.PIP_CONFIG_FILE = os.devNull;
|
|
871
|
+
env.PYTHONNOUSERSITE = "1";
|
|
872
|
+
env.CI = "1";
|
|
873
|
+
env.NO_COLOR = "1";
|
|
874
|
+
env.CODEXA_VERIFY = "1";
|
|
875
|
+
return env;
|
|
876
|
+
}
|
|
877
|
+
async function gitText(repoRoot, args, trim = true) {
|
|
878
|
+
try {
|
|
879
|
+
const { stdout } = await execFileAsync("git", ["-C", repoRoot, ...args], {
|
|
880
|
+
encoding: "utf8",
|
|
881
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
882
|
+
timeout: 5_000,
|
|
883
|
+
env: minimalGitEnv()
|
|
884
|
+
});
|
|
885
|
+
return trim ? stdout.trim() : stdout;
|
|
886
|
+
}
|
|
887
|
+
catch {
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
function minimalGitEnv() {
|
|
892
|
+
const env = {};
|
|
893
|
+
for (const key of ["PATH", "HOME", "TMPDIR", "TEMP", "TMP", "SystemRoot", "WINDIR"]) {
|
|
894
|
+
if (process.env[key]) {
|
|
895
|
+
env[key] = process.env[key];
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
env.NO_COLOR = "1";
|
|
899
|
+
return env;
|
|
900
|
+
}
|
|
901
|
+
function parsePorcelainStatus(value) {
|
|
902
|
+
const entries = value.split("\0").filter(Boolean);
|
|
903
|
+
const statuses = new Map();
|
|
904
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
905
|
+
const entry = entries[index];
|
|
906
|
+
if (!entry || entry.length < 4) {
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
const status = entry.slice(0, 2);
|
|
910
|
+
const file = normalizePathLike(entry.slice(3));
|
|
911
|
+
if (status.includes("R") || status.includes("C")) {
|
|
912
|
+
const oldPath = normalizePathLike(entries[index + 1] ?? "");
|
|
913
|
+
statuses.set(file, status);
|
|
914
|
+
if (oldPath) {
|
|
915
|
+
statuses.set(oldPath, status);
|
|
916
|
+
}
|
|
917
|
+
index += 1;
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
statuses.set(file, status);
|
|
921
|
+
}
|
|
922
|
+
return statuses;
|
|
923
|
+
}
|
|
924
|
+
function normalizeStatusPaths(statuses, gitRoot, relativePrefix) {
|
|
925
|
+
const normalized = new Map();
|
|
926
|
+
for (const [file, status] of statuses) {
|
|
927
|
+
const relative = gitRepoRelativePath(file, gitRoot, relativePrefix);
|
|
928
|
+
if (relative) {
|
|
929
|
+
normalized.set(relative, status);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return normalized;
|
|
933
|
+
}
|
|
934
|
+
function normalizeFullStatusPaths(statuses) {
|
|
935
|
+
const normalized = new Map();
|
|
936
|
+
for (const [file, status] of statuses) {
|
|
937
|
+
normalized.set(normalizePathLike(file), status);
|
|
938
|
+
}
|
|
939
|
+
return normalized;
|
|
940
|
+
}
|
|
941
|
+
async function hashFiles(repoRoot, files) {
|
|
942
|
+
const entries = await Promise.all(files.map(async (file) => [file, await hashFile(repoRoot, file, { metadataForNonSource: true })]));
|
|
943
|
+
return Object.fromEntries(entries.sort(([a], [b]) => a.localeCompare(b)));
|
|
944
|
+
}
|
|
945
|
+
async function hashProtectedFiles(repoRoot, files) {
|
|
946
|
+
const entries = await Promise.all(files.map(async (file) => [file, await hashFile(repoRoot, file, { metadataForNonSource: false })]));
|
|
947
|
+
return new Map(entries);
|
|
948
|
+
}
|
|
949
|
+
async function hashFile(repoRoot, file, options) {
|
|
950
|
+
const absolute = path.join(repoRoot, file);
|
|
951
|
+
try {
|
|
952
|
+
const stat = await fs.lstat(absolute);
|
|
953
|
+
if (!stat.isFile()) {
|
|
954
|
+
return "non-file";
|
|
955
|
+
}
|
|
956
|
+
if (options.metadataForNonSource && (!isSourcePath(file) || stat.size > 2 * 1024 * 1024)) {
|
|
957
|
+
return `metadata:${stat.size}:${Math.trunc(stat.mtimeMs)}`;
|
|
958
|
+
}
|
|
959
|
+
if (stat.size > 2 * 1024 * 1024) {
|
|
960
|
+
return `metadata:${stat.size}:${Math.trunc(stat.mtimeMs)}`;
|
|
961
|
+
}
|
|
962
|
+
const content = await fs.readFile(absolute);
|
|
963
|
+
return createHash("sha1").update(content).digest("hex");
|
|
964
|
+
}
|
|
965
|
+
catch (error) {
|
|
966
|
+
const code = error?.code;
|
|
967
|
+
return code === "ENOENT" ? "missing" : `unreadable:${typeof code === "string" ? code : "unknown"}`;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
function isCodexaGenerated(file) {
|
|
971
|
+
const normalized = normalizePathLike(file);
|
|
972
|
+
if (normalized === ".codex/static-analysis" || normalized.startsWith(".codex/static-analysis/")) {
|
|
973
|
+
return false;
|
|
974
|
+
}
|
|
975
|
+
return normalized === ".codex" || normalized === ".codex/" || normalized.startsWith(".codex/");
|
|
976
|
+
}
|
|
977
|
+
function protectedMutationPath(file) {
|
|
978
|
+
const normalized = normalizePathLike(file);
|
|
979
|
+
if (codexaProvenancePath(normalized)) {
|
|
980
|
+
return true;
|
|
981
|
+
}
|
|
982
|
+
if (ignoredTestOutputPath(normalized)) {
|
|
983
|
+
return false;
|
|
984
|
+
}
|
|
985
|
+
return isSourcePath(normalized) || isTestPath(normalized) || codexaProvenancePath(normalized);
|
|
986
|
+
}
|
|
987
|
+
function ignoredTestOutputPath(file) {
|
|
988
|
+
return shouldSkipPath(file) || file === "coverage" || file.startsWith("coverage/") || file === ".coverage" || file.startsWith(".pytest_cache/") || file.startsWith(".nyc_output/");
|
|
989
|
+
}
|
|
990
|
+
function codexaProvenancePath(file) {
|
|
991
|
+
const normalized = normalizePathLike(file);
|
|
992
|
+
const codexPath = normalized.startsWith(".codex/")
|
|
993
|
+
? normalized
|
|
994
|
+
: normalized.includes("/.codex/")
|
|
995
|
+
? normalized.slice(normalized.indexOf("/.codex/") + 1)
|
|
996
|
+
: normalized;
|
|
997
|
+
return (codexPath === ".codex/config.toml" ||
|
|
998
|
+
codexPath.startsWith(".codex/cache/codexa-tasks/") ||
|
|
999
|
+
codexPath.startsWith(".codex/cache/codexa-task-snapshots/") ||
|
|
1000
|
+
codexPath.startsWith(".codex/cache/codexa-outcomes/") ||
|
|
1001
|
+
codexPath.startsWith(".codex/cache/codexa-hooks/") ||
|
|
1002
|
+
codexPath.startsWith(".codex/codebase/"));
|
|
1003
|
+
}
|
|
1004
|
+
async function realpathOrUndefined(value) {
|
|
1005
|
+
try {
|
|
1006
|
+
return await fs.realpath(value);
|
|
1007
|
+
}
|
|
1008
|
+
catch {
|
|
1009
|
+
return undefined;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
function summarizeOutput(value, repoRoot) {
|
|
1013
|
+
const clean = sanitizeAutoVerifyText(value
|
|
1014
|
+
.split(/\r?\n/u)
|
|
1015
|
+
.map((line) => line.trim())
|
|
1016
|
+
.filter(Boolean)
|
|
1017
|
+
.slice(-8)
|
|
1018
|
+
.join(" "), repoRoot);
|
|
1019
|
+
return clean;
|
|
1020
|
+
}
|
|
1021
|
+
function boundedAppend(current, chunk) {
|
|
1022
|
+
const next = current + chunk;
|
|
1023
|
+
return next.length > MAX_OUTPUT_CAPTURE ? next.slice(next.length - MAX_OUTPUT_CAPTURE) : next;
|
|
1024
|
+
}
|
|
1025
|
+
function safeExecutableName(value) {
|
|
1026
|
+
return /^(npm|pnpm|yarn|node|vitest|jest|pytest|uv|python|python3)$/u.test(value);
|
|
1027
|
+
}
|
|
1028
|
+
function normalizePathLike(value) {
|
|
1029
|
+
return value.split(path.sep).join("/").replace(/^\.\//u, "").replace(/\/+/gu, "/");
|
|
1030
|
+
}
|
|
1031
|
+
function repoRelativeRealPath(repoRealRoot, fileRealpath) {
|
|
1032
|
+
const relative = path.relative(repoRealRoot, fileRealpath).split(path.sep).join("/");
|
|
1033
|
+
return relative || ".";
|
|
1034
|
+
}
|
|
1035
|
+
function redactSecretText(value) {
|
|
1036
|
+
return value
|
|
1037
|
+
?.replace(/(^|[\s([,{])((?:--?[a-z0-9-]*(?:token|secret|password|passwd|pwd|api[-_]?key|access[-_]?key|auth|credential|cookie)[a-z0-9-]*)(?:=|\s+))([^\s;|)\]'",]+)/giu, "$1$2<redacted>")
|
|
1038
|
+
.replace(/(\b[A-Z_]*(?:TOKEN|SECRET|PASSWORD|PASSWD|PWD|API_?KEY|ACCESS_?KEY|AUTH|CREDENTIAL|COOKIE)[A-Z0-9_]*=)([^\s;|)\]'",]+)/gu, "$1<redacted>")
|
|
1039
|
+
.replace(/\b(Bearer)\s+[A-Za-z0-9._~+/-]+=*/giu, "$1 <redacted>");
|
|
1040
|
+
}
|
|
1041
|
+
//# sourceMappingURL=autoverify.js.map
|