@kaydash9999/banker-plugins 0.21.0 → 0.23.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +69 -0
- package/PRIVACY.md +8 -3
- package/README.md +78 -15
- package/bin/banker.js +154 -32
- package/bin/lib/codex-hooks.mjs +123 -20
- package/bin/lib/codex-names.mjs +4 -0
- package/bin/lib/codex-routes.mjs +204 -0
- package/bin/lib/command-skill.cjs +50 -0
- package/bin/lib/telemetry-config.mjs +2 -1
- package/codex/command-skills/front-qa/SKILL.md +56 -0
- package/codex/command-skills/setup/SKILL.md +87 -0
- package/codex/hooks.json +29 -0
- package/codex/manifest.json +18 -15
- package/codex/skills/graceful-pause/SKILL.md +2 -2
- package/codex/skills/progress/scripts/progress.mjs +44 -14
- package/codex/skills/teamwork/SKILL.md +137 -0
- package/codex/skills/teamwork/scripts/teamwork.mjs +957 -0
- package/codex/transform-matrix.md +5 -3
- package/commands/setup.md +2 -2
- package/hooks/obsidize-hook.mjs +75 -32
- package/hooks/telemetry-count-codex.mjs +303 -35
- package/hooks/update-fetch.mjs +2 -1
- package/hooks/update-notify.mjs +146 -13
- package/package.json +3 -1
- package/skills/3d-intro-build/SKILL.md +101 -20
- package/skills/3d-intro-build/references/NOTICE.md +9 -2
- package/skills/3d-intro-build/references/assemble.mjs +113 -22
- package/skills/3d-intro-build/references/azure-adapter.mjs +110 -24
- package/skills/3d-intro-build/references/clip-post.mjs +443 -0
- package/skills/3d-intro-build/references/dive-mode.mjs +299 -0
- package/skills/3d-intro-build/references/intro-fixes.css +11 -0
- package/skills/3d-intro-build/references/prompts.md +51 -4
- package/skills/3d-intro-build/references/scrub-engine.js +1 -0
- package/skills/3d-intro-build/references/step-nav.css +29 -0
- package/skills/3d-intro-build/references/step-nav.js +277 -106
- package/skills/3d-intro-build/references/verify-intro.mjs +387 -32
- package/skills/3d-intro-build/references/verify.md +6 -0
- package/skills/3d-intro-build/references/video-pool.mjs +142 -12
- package/skills/3d-intro-setup/SKILL.md +20 -7
- package/skills/3d-intro-setup/references/azure-adapter.mjs +110 -24
- package/skills/3d-intro-setup/references/video-pool.mjs +142 -12
- package/skills/all-in-one/SKILL.md +125 -86
- package/skills/anti-drm/SKILL.md +1 -1
- package/skills/anti-drm/scripts/instruction-toggle.mjs +57 -4
- package/skills/arch-diagram/build_pptx_template.py +47 -28
- package/skills/audit-web-page/SKILL.md +1 -1
- package/skills/audit-web-page/harness.py +0 -1
- package/skills/lineage/lineage.py +1435 -533
- package/skills/motion-graphic-make/SKILL.md +1 -1
- package/skills/obsidizer/SKILL.md +20 -5
- package/skills/omc-patch/SKILL.md +1 -1
- package/skills/payload-mon/SKILL.md +2 -2
- package/skills/play-qa/SKILL.md +7 -5
- package/skills/progress-mon/README.md +162 -0
- package/skills/progress-mon/SKILL.md +165 -0
- package/skills/progress-mon/assets/fixture/README.md +1 -0
- package/skills/progress-mon/assets/fixture/arch.json +33 -0
- package/skills/progress-mon/assets/fixture/commits.json +7 -0
- package/skills/progress-mon/assets/fixture/config.json +30 -0
- package/skills/progress-mon/assets/fixture/events.jsonl +7 -0
- package/skills/progress-mon/assets/fixture/flow.json +44 -0
- package/skills/progress-mon/assets/fixture/items.json +14 -0
- package/skills/progress-mon/assets/fixture/req-spec.json +31 -0
- package/skills/progress-mon/assets/fixture/reqs.json +11 -0
- package/skills/progress-mon/assets/fixture/variants/arch-auto/arch.json +58 -0
- package/skills/progress-mon/assets/fixture/variants/check-auto/arch.json +15 -0
- package/skills/progress-mon/assets/fixture/variants/check-overlap/arch.json +14 -0
- package/skills/progress-mon/assets/fixture/variants/history-manual/config.json +13 -0
- package/skills/progress-mon/assets/fixture/variants/history-manual/items.json +7 -0
- package/skills/progress-mon/assets/fixture/variants/workflow-wide/flow.json +35 -0
- package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a01.jsonl +1 -0
- package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a02.jsonl +1 -0
- package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a03.jsonl +1 -0
- package/skills/progress-mon/assets/fixture/workflows/wf_ex1/agent-a04.jsonl +1 -0
- package/skills/progress-mon/assets/fixture/workflows/wf_ex1/journal.jsonl +7 -0
- package/skills/progress-mon/references/arch.md +280 -0
- package/skills/progress-mon/references/authoring.md +199 -0
- package/skills/progress-mon/references/config.md +115 -0
- package/skills/progress-mon/references/flow.md +75 -0
- package/skills/progress-mon/references/inventory.md +58 -0
- package/skills/progress-mon/references/items.md +74 -0
- package/skills/progress-mon/references/req-spec.md +74 -0
- package/skills/progress-mon/references/reqs.md +30 -0
- package/skills/progress-mon/scripts/index.js +31 -0
- package/skills/progress-mon/scripts/pm-check.mjs +526 -0
- package/skills/progress-mon/scripts/pm-config.mjs +341 -0
- package/skills/progress-mon/scripts/pm-data.mjs +394 -0
- package/skills/progress-mon/scripts/pm-html.mjs +77 -0
- package/skills/progress-mon/scripts/pm-memory.mjs +308 -0
- package/skills/progress-mon/scripts/pm-render.mjs +1644 -0
- package/skills/progress-mon/scripts/pm-server.mjs +735 -0
- package/skills/progress-mon/scripts/pm-sources.mjs +501 -0
- package/skills/progress-mon/scripts/progress-mon.mjs +167 -0
- package/skills/ralph-qa/SKILL.md +105 -16
- package/skills/ralph-qa/references/gate-evidence.mjs +457 -0
- package/skills/ralph-qa/references/payload-scan.mjs +19 -2
- package/skills/ralph-qa/references/run-folder.mjs +299 -0
- package/skills/ralph-qa/references/seat-payload.mjs +60 -13
- package/skills/ralph-qa/references/seat-run.mjs +159 -155
- package/skills/remains/SKILL.md +2 -1
- package/skills/remains/scripts/boxes.mjs +6 -2
- package/skills/rfp-author/scripts/md2docx.py +63 -34
- package/skills/setup-bypass-permissions/SKILL.md +2 -2
- package/skills/setup-bypass-permissions/scripts/codex-config.mjs +19 -8
- package/skills/setup-bypass-permissions/scripts/fallback/bypass-permissions.py +66 -15
- package/skills/setup-mcp/SKILL.md +1 -1
- package/skills/setup-omc-hud/SKILL.md +2 -1
- package/skills/setup-omc-hud/assets/codex-config.mjs +19 -8
- package/skills/smart-compact/SKILL.md +1 -1
- package/skills/smart-compact/scripts/codex-gate.mjs +2 -1
- package/skills/theme-selection/SKILL.md +1 -1
- package/skills/tone-compact/SKILL.md +1 -1
- package/skills/tone-compact/scripts/instruction-toggle.mjs +57 -4
- package/skills/ultra-chain/SKILL.md +34 -16
- package/skills/ultra-chain/references/chain-ledger.mjs +111 -3
- package/skills/ultra-init/SKILL.md +30 -22
- package/skills/ultra-ui-qa/SKILL.md +1 -1
- package/skills/ultraqa/NOTICE.md +34 -0
- package/skills/ultraqa/SKILL.md +196 -0
- package/skills/ultraqa/references/gate-evidence.mjs +457 -0
- package/skills/ultraqa/references/run-folder.mjs +299 -0
- package/skills/ultraqa/references/tmux-drive.md +52 -0
- package/skills/ultraqa/references/uqa-run.mjs +339 -0
- package/skills/update-banker/SKILL.md +32 -7
- package/skills/vertical-pptx/SKILL.md +1 -1
- package/skills/vertical-pptx/build_a4p.js +20 -2
- package/skills/vertical-pptx/build_a4p.py +560 -359
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* gate-evidence: a gate command run by this program, not by the model, and what it saw, as the evidence ralph-qa
|
|
4
|
+
* (--gates), ultraqa, all-in-one, ultra-init and ultra-chain judge by. The source of truth is
|
|
5
|
+
* skills/ralph-qa/references/gate-evidence.mjs; skills/ultraqa/references holds a byte-identical copy
|
|
6
|
+
* (scripts/sync-adapter.js).
|
|
7
|
+
*
|
|
8
|
+
* node gate-evidence.mjs init [--cwd <repo>] the gate folder <g> of one ralph-qa run (ralph-qa-gates.<letters and digits>): made
|
|
9
|
+
* once before the baseline, kept across the iterations, apart from the payload folder seat-run.mjs makes anew
|
|
10
|
+
* each iteration; seat-run's day rule never takes it (another name, another marker). init and run stop with
|
|
11
|
+
* not-a-git-tree when the repo (--cwd) is no git work tree: its fingerprint would see no change.
|
|
12
|
+
* node gate-evidence.mjs cleanup --dir <g> that gate folder alone, once the run has its end word
|
|
13
|
+
* node gate-evidence.mjs run --dir <d> --id <id> --cmd <command> --timeout <s> [--iter <n>] [--cwd <repo>]
|
|
14
|
+
* one command (one gate, never several joined), POSIX `sh -c`, Windows PowerShell 7 (pwsh) when on PATH,
|
|
15
|
+
* else 5.1, with an exit that keeps the command's code; its output on the descriptor of a new file
|
|
16
|
+
* <d>/gate-<id>-<iter>-<seq>.md (no Node pipes); past the limit its whole tree ends (POSIX: its process group, TERM then
|
|
17
|
+
* KILL; Windows: taskkill /T /F); after it ends on its own the group is sent KILL too (POSIX). One
|
|
18
|
+
* `observed` line goes to <d>/evidence.md: the command, folder, times, exit or signal, timedOut, the output
|
|
19
|
+
* file and its sha256, the work tree fingerprint after it, the verdict and the failure signature. Each seq is
|
|
20
|
+
* claimed with a file of its own (<d>/.seq-<n>.md, made with "wx"), so two runs at once never share one.
|
|
21
|
+
* node gate-evidence.mjs note --dir <d> --id <id> --text <what> [--iter <n>] [--exit <code>]
|
|
22
|
+
* a `reported` line: what someone said (a model, an OMX report). Never a verdict's evidence.
|
|
23
|
+
* node gate-evidence.mjs gate --dir <d> --gates <a,b,...> --iter <n> [--cwd <repo>]
|
|
24
|
+
* gateConjunct over the evidence and the fingerprint now; exit 0 when it holds, 1 when not.
|
|
25
|
+
* node gate-evidence.mjs verify --dir <d> each observed output file against its recorded sha256; 1 on a mismatch
|
|
26
|
+
* node gate-evidence.mjs fingerprint [--cwd <repo>]
|
|
27
|
+
*
|
|
28
|
+
* <d> is a gate folder (init), an ultraqa run folder, or a ralph-qa folder an older version made with seat-run.mjs init
|
|
29
|
+
* (run-folder.mjs). Iteration 0 is the baseline, before any fix. Out: one
|
|
30
|
+
* JSON object on stdout; exit 2 for a usage or internal error, 3 for a stop. Every file name ends in .md (a closed
|
|
31
|
+
* network's DRM encrypts files by their extension and leaves .md readable).
|
|
32
|
+
*/
|
|
33
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
34
|
+
import { createHash } from "node:crypto";
|
|
35
|
+
import { appendFileSync, closeSync, existsSync, fstatSync, openSync, readFileSync, readSync, realpathSync, statSync } from "node:fs";
|
|
36
|
+
import { delimiter, dirname, join, resolve, win32 } from "node:path";
|
|
37
|
+
import { fileURLToPath } from "node:url";
|
|
38
|
+
import { Stop, hashFile, makeRunFolder, removeRunFolder, requireGitTree, runFolder, snapshot } from "./run-folder.mjs";
|
|
39
|
+
|
|
40
|
+
const GATES_KIND = "ralph-qa-gates";
|
|
41
|
+
const KINDS = [GATES_KIND, "ralph-qa", "ultraqa"];
|
|
42
|
+
const EVIDENCE = "evidence.md";
|
|
43
|
+
const GRACE_MS = 2000;
|
|
44
|
+
// The output text the verdict and the signature read: whole up to 64 MiB, past that its first and last 32 MiB.
|
|
45
|
+
const TEXT_MAX = 64 << 20;
|
|
46
|
+
const sha256 = (b) => createHash("sha256").update(b).digest("hex");
|
|
47
|
+
|
|
48
|
+
// The nine adversarial classes (OMX ultraqa's), the lens of ralph-qa's adversarial seat and the rows of ultraqa's matrix.
|
|
49
|
+
export const CLASSES = [
|
|
50
|
+
["malformed-input", "잘못된 입력"], ["repeated-interruption", "반복 중단"], ["prompt-injection", "프롬프트 주입"],
|
|
51
|
+
["cancel-resume", "취소와 재개"], ["stale-state", "낡은 상태"], ["dirty-worktree", "더러운 작업 트리"],
|
|
52
|
+
["hung-command", "멈춘 명령"], ["flaky-test", "흔들리는 시험"], ["misleading-output", "오도하는 성공 출력"],
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
// ---- verdict and signature (pure)
|
|
56
|
+
|
|
57
|
+
// Exit 0 whose output still says a test failed or was skipped: node:test, TAP, Jest, Vitest, pytest, Go test.
|
|
58
|
+
const MISLEADING = [
|
|
59
|
+
["node:test 실패", /^ℹ (?:fail|cancelled) [1-9]/m], ["node:test 건너뜀", /^ℹ (?:skipped|todo) [1-9]/m],
|
|
60
|
+
["TAP 실패나 건너뜀", /^# (?:fail|skip|todo|cancelled) [1-9]/m],
|
|
61
|
+
["Jest/Vitest 실패", /^\s*Tests?:?\s+.*?\b[1-9]\d* failed\b/m], ["Jest/Vitest 건너뜀", /^\s*Tests?:?\s+.*?\b[1-9]\d* (?:skipped|todo|pending)\b/m],
|
|
62
|
+
["pytest 실패", /^=+ .*?\b[1-9]\d* (?:failed|errors?)\b.*=+\s*$/m], ["pytest 건너뜀", /^=+ .*?\b[1-9]\d* skipped\b.*=+\s*$/m],
|
|
63
|
+
["Go test 실패", /^(?:--- FAIL: |FAIL\s)/m], ["Go test 건너뜀", /^\s*--- SKIP: /m],
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
export function judge({ exit, signal, timedOut, text }) {
|
|
67
|
+
if (timedOut) return { verdict: "timeout", why: "시간 제한" };
|
|
68
|
+
if (exit !== 0) return { verdict: "fail", why: exit === null ? `신호 ${signal}` : `종료 ${exit}` };
|
|
69
|
+
const hit = MISLEADING.find(([, re]) => re.test(text));
|
|
70
|
+
return hit ? { verdict: "misleading", why: hit[0] } : { verdict: "pass", why: null };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const FAILURE_LINE = /(?:^|[\s(])(?:not ok|✖|FAIL(?:ED)?|Error|ERROR|error|[Ff]ailed|AssertionError|panic:|Traceback)\b/;
|
|
74
|
+
// What does not tell two failures apart: colours, times, numbers, long hex, temporary folders.
|
|
75
|
+
const normalized = (line) => line.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "").replace(/(?:\/tmp|\/var\/folders|\\Temp|\/private\/var)[^\s'"]*/gi, "<tmp>")
|
|
76
|
+
.replace(/\b[0-9a-f]{8,}\b/gi, "<hex>").replace(/\d+(?:\.\d+)?/g, "#").replace(/\s+/g, " ").trim();
|
|
77
|
+
|
|
78
|
+
// Where the command ran, as <repo>: the same failure in the repo and in a clean worktree copy of it (the baseline of a
|
|
79
|
+
// tree with changes) names two folders. Each root (never a file system root) in its own form and with / (a Windows
|
|
80
|
+
// file URL), longest first, as one pattern.
|
|
81
|
+
function rootsOut(roots) {
|
|
82
|
+
const forms = [...new Set(roots.filter((r) => r && dirname(r) !== r).flatMap((r) => [r, r.replace(/\\/g, "/")]))].sort((x, y) => y.length - x.length);
|
|
83
|
+
if (!forms.length) return (line) => line;
|
|
84
|
+
const re = new RegExp(forms.map((f) => f.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"), "gi");
|
|
85
|
+
return (line) => line.replace(re, "<repo>");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// The id with the sha256 (16 hex) of the normalized failure lines, or of the last 20 lines when none reads as one.
|
|
89
|
+
// `roots` are the folders the command ran in (its cwd, the repo root): written as <repo>.
|
|
90
|
+
export function signatureOf(id, verdict, text, roots = []) {
|
|
91
|
+
const lines = String(text).split(/\r?\n/);
|
|
92
|
+
const failing = lines.filter((l) => FAILURE_LINE.test(l));
|
|
93
|
+
const out = rootsOut(roots);
|
|
94
|
+
const picked = (failing.length ? failing : lines.filter((l) => l.trim()).slice(-20)).map((l) => normalized(out(l)));
|
|
95
|
+
return `${id}:${sha256(`${verdict}\n${[...new Set(picked)].slice(0, 200).join("\n")}`).slice(0, 16)}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---- the gate conjunct (pure)
|
|
99
|
+
|
|
100
|
+
const OK_STATES = new Set(["pass", "baseline", "misleading"]);
|
|
101
|
+
const PASSES_AFTER_FAIL = 3;
|
|
102
|
+
|
|
103
|
+
// `scenarios` stands for every scenario-* gate the evidence holds.
|
|
104
|
+
function expand(gates, observed) {
|
|
105
|
+
const scenarioIds = [...new Set(observed.map((r) => r.id).filter((id) => id.startsWith("scenario-")))];
|
|
106
|
+
return gates.flatMap((g) => (g !== "scenarios" ? [g] : scenarioIds.length ? scenarioIds : ["scenarios"]));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// A pass right after a failure counts once it has two more passes on the same tree.
|
|
110
|
+
function afterFailure(recs, last, fingerprint) {
|
|
111
|
+
const lastBad = recs.reduce((at, r, i) => (r.verdict !== "pass" ? i : at), -1);
|
|
112
|
+
if (lastBad === -1) return { state: "pass", seq: last.seq };
|
|
113
|
+
const passes = recs.slice(lastBad + 1).filter((r) => r.fingerprint === fingerprint).length;
|
|
114
|
+
return passes >= PASSES_AFTER_FAIL ? { state: "pass", seq: last.seq } : { state: "flaky", seq: last.seq, need: PASSES_AFTER_FAIL - passes };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// The baseline record (iteration 0 of the gate) failed the same way: the same command and failure signature.
|
|
118
|
+
const sameAsBaseline = (base, r) => Boolean(base) && base.verdict !== "pass" && base.cmd === r.cmd && base.signature === r.signature;
|
|
119
|
+
|
|
120
|
+
// A failure the baseline (iteration 0, the same command) had with the same signature is not this change's.
|
|
121
|
+
function failed(recs, last) {
|
|
122
|
+
const same = sameAsBaseline(recs.find((r) => r.iter === 0), last);
|
|
123
|
+
return same ? { state: "baseline", seq: last.seq, signature: last.signature } : { state: last.verdict, seq: last.seq, signature: last.signature };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function gateState(recs, id, fingerprint, iter) {
|
|
127
|
+
const now = recs.filter((r) => r.iter === iter);
|
|
128
|
+
if (!now.length) return { id, state: "missing" };
|
|
129
|
+
const last = now[now.length - 1];
|
|
130
|
+
// A tree with no git has one fingerprint whatever changes: no record of it shows the tree it was judged on.
|
|
131
|
+
if (last.git === false) return { id, state: "unverifiable", seq: last.seq, why: "git 저장소가 아니라 작업 트리 변경을 볼 수 없다" };
|
|
132
|
+
if (last.fingerprint !== fingerprint) return { id, state: "stale", seq: last.seq };
|
|
133
|
+
if (last.verdict === "misleading") return { id, state: "misleading", seq: last.seq, why: last.why };
|
|
134
|
+
return { id, ...(last.verdict === "pass" ? afterFailure(recs, last, fingerprint) : failed(recs, last)) };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// The quorum's gate conjunct: every declared gate has an observed record of this iteration on the tree as it is now,
|
|
138
|
+
// and passes (a pass after a failure: two more), or fails as the baseline did. `reported` lines never count.
|
|
139
|
+
export function gateConjunct(evidence, gates, fingerprint, iter) {
|
|
140
|
+
const observed = evidence.filter((r) => r && r.kind === "observed");
|
|
141
|
+
const results = expand(gates, observed).map((g) => gateState(observed.filter((r) => r.id === g), g, fingerprint, iter));
|
|
142
|
+
return { ok: results.length > 0 && results.every((x) => OK_STATES.has(x.state)), gates: results };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Keys seen in `n` or more different iterations: the same failure (a gate's signature) or the same issue.
|
|
146
|
+
export function repeated(items, n = 3) {
|
|
147
|
+
const iters = new Map();
|
|
148
|
+
for (const { key, iter } of items) iters.set(key, (iters.get(key) || new Set()).add(iter));
|
|
149
|
+
return [...iters].filter(([, s]) => s.size >= n).map(([key]) => key);
|
|
150
|
+
}
|
|
151
|
+
// A gate's same failure, from iteration 1 on: the baseline (iteration 0) is no review iteration, and a failure the
|
|
152
|
+
// baseline had (same command, same signature; `baseline` in the conjunct, never this change's to fix) is not counted.
|
|
153
|
+
export function repeatedFailures(evidence, n = 3) {
|
|
154
|
+
const observed = evidence.filter((r) => r?.kind === "observed");
|
|
155
|
+
const own = observed.filter((r) => r.signature && r.iter !== 0 && !sameAsBaseline(observed.find((b) => b.id === r.id && b.iter === 0), r));
|
|
156
|
+
return repeated(own.map((r) => ({ key: r.signature, iter: r.iter })), n);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ralph-qa's same issue: (seat id, the path or criterion ID it cites, its title normalized).
|
|
160
|
+
export const issueKey = (seat, cite, title) => [seat, cite, String(title).toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim()].join("\u0000");
|
|
161
|
+
|
|
162
|
+
// How a run that spent --max ends: blockers left are INCONCLUSIVE; a run that only the gates kept from APPROVE says so.
|
|
163
|
+
export function exhausted({ blockers, quorumWithoutGate, gateOk }) {
|
|
164
|
+
if (blockers > 0) return "INCONCLUSIVE";
|
|
165
|
+
return quorumWithoutGate && gateOk === false ? "ITERATE(한도 소진, 원인 gate)" : "ITERATE(한도 소진)";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---- running one command
|
|
169
|
+
|
|
170
|
+
function onPath(name, env) {
|
|
171
|
+
const key = Object.keys(env).find((k) => k.toUpperCase() === "PATH");
|
|
172
|
+
for (const dir of String(key ? env[key] : "").split(delimiter).filter((x) => win32.isAbsolute(x))) {
|
|
173
|
+
const p = win32.join(dir, name);
|
|
174
|
+
if (existsSync(p) && statSync(p).isFile()) return p;
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// The shell: POSIX sh, or PowerShell with an exit that keeps a native command's code (5.1 makes any failure 1) and
|
|
180
|
+
// turns a failed cmdlet into 1. A newline before it, so a trailing comment in the command cannot swallow it.
|
|
181
|
+
export function shellFor(cmd, ctx) {
|
|
182
|
+
if (ctx.platform !== "win32") return { name: "sh", exe: "/bin/sh", args: ["-c", cmd] };
|
|
183
|
+
const pwsh = onPath("pwsh.exe", ctx.env);
|
|
184
|
+
const exe = pwsh || win32.join(ctx.env.SystemRoot || "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
185
|
+
const tail = "\n$rqOk = $?; if ($null -ne $LASTEXITCODE -and $LASTEXITCODE -ne 0) { exit $LASTEXITCODE }; if (-not $rqOk) { exit 1 }; exit 0";
|
|
186
|
+
return { name: pwsh ? "pwsh" : "powershell", exe, args: ["-NoProfile", "-NonInteractive", "-Command", `${cmd}${tail}`] };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const killGroup = (pid, sig) => {
|
|
190
|
+
try {
|
|
191
|
+
process.kill(-pid, sig);
|
|
192
|
+
} catch {
|
|
193
|
+
/* the group is gone */
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
const taskkill = (ctx, pid) => spawnSync(win32.join(ctx.env.SystemRoot || "C:\\Windows", "System32", "taskkill.exe"), ["/T", "/F", "/PID", String(pid)], { stdio: "ignore", windowsHide: true });
|
|
197
|
+
|
|
198
|
+
function endTree(ctx, child) {
|
|
199
|
+
if (ctx.platform === "win32") return void taskkill(ctx, child.pid);
|
|
200
|
+
killGroup(child.pid, "SIGTERM");
|
|
201
|
+
setTimeout(() => killGroup(child.pid, "SIGKILL"), GRACE_MS).unref();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Runs `cmd` with its output on `fd` (a new output file, closed here); resolves { exit, signal, timedOut, secs, shell }.
|
|
205
|
+
// The child writes to the output file's descriptor itself, not through Node's pipes: those are AF_UNIX socket pairs,
|
|
206
|
+
// and in the Codex Linux sandbox their output can be lost (zero bytes recorded for a command that printed).
|
|
207
|
+
export function execute({ cmd, cwd, limitMs, fd }, ctx) {
|
|
208
|
+
const sh = shellFor(cmd, ctx);
|
|
209
|
+
const t0 = Date.now();
|
|
210
|
+
return new Promise((done) => {
|
|
211
|
+
const child = spawn(sh.exe, sh.args, { cwd, env: ctx.env, stdio: ["ignore", fd, fd], detached: ctx.platform !== "win32", windowsHide: true });
|
|
212
|
+
let timedOut = false;
|
|
213
|
+
let settled = false;
|
|
214
|
+
const finish = (status, error) => {
|
|
215
|
+
if (settled) return;
|
|
216
|
+
settled = true;
|
|
217
|
+
clearTimeout(limit);
|
|
218
|
+
closeSync(fd);
|
|
219
|
+
const { code = null, signal = null } = status || {};
|
|
220
|
+
done({ exit: error ? null : code, signal, timedOut, secs: Math.round((Date.now() - t0) / 100) / 10, shell: sh.name, error: error || null });
|
|
221
|
+
};
|
|
222
|
+
const limit = setTimeout(() => {
|
|
223
|
+
timedOut = true;
|
|
224
|
+
endTree(ctx, child);
|
|
225
|
+
}, limitMs);
|
|
226
|
+
child.on("error", (e) => (child.pid === undefined ? finish(null, e.code || "spawn") : null));
|
|
227
|
+
// After the shell ends, what it left in its group ends too (POSIX).
|
|
228
|
+
child.on("exit", (code, signal) => {
|
|
229
|
+
if (ctx.platform !== "win32") killGroup(child.pid, "SIGKILL");
|
|
230
|
+
finish({ code, signal });
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// A new file (never one already there): its descriptor, or null when one is there; a folder the sandbox does not let
|
|
236
|
+
// it write is a stop with what to do.
|
|
237
|
+
function openNew(p) {
|
|
238
|
+
try {
|
|
239
|
+
return openSync(p, "wx", 0o600);
|
|
240
|
+
} catch (e) {
|
|
241
|
+
if (e.code === "EEXIST") return null;
|
|
242
|
+
if (!["EACCES", "EPERM", "EROFS"].includes(e.code)) throw e;
|
|
243
|
+
throw new Stop("no-write", `게이트 기록 파일을 만들지 못했다(${e.code}): ${p}. 실행 폴더가 샌드박스 쓰기 범위 밖이면 이 명령을 승인으로 다시 실행한다`, [p]);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// The next seq: past every recorded one (a record cut short leaves no line) and claimed by its own file made with
|
|
248
|
+
// "wx", so two runs in one folder at once never share one.
|
|
249
|
+
function claimSeq(d) {
|
|
250
|
+
const recorded = readEvidence(d).map((r) => (Number.isInteger(r?.seq) ? r.seq : 0));
|
|
251
|
+
for (let seq = Math.max(recorded.length, ...recorded) + 1; ; seq += 1) {
|
|
252
|
+
const fd = openNew(join(d, `.seq-${seq}.md`));
|
|
253
|
+
if (fd !== null) return (closeSync(fd), seq);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// A claimed seq whose output file is new (one a run of an older version left behind takes the next seq).
|
|
258
|
+
function newOutput(d, id, iter) {
|
|
259
|
+
for (;;) {
|
|
260
|
+
const seq = claimSeq(d);
|
|
261
|
+
const output = join(d, `gate-${id}-${iter}-${seq}.md`);
|
|
262
|
+
const fd = openNew(output);
|
|
263
|
+
if (fd !== null) return { seq, output, fd };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// What the verdict and the signature read: the whole output up to TEXT_MAX, past it the first and last half (a
|
|
268
|
+
// test runner's counts are at its end) with `cut`.
|
|
269
|
+
export function outputText(p, max = TEXT_MAX) {
|
|
270
|
+
const fd = openSync(p, "r");
|
|
271
|
+
try {
|
|
272
|
+
const size = fstatSync(fd).size;
|
|
273
|
+
if (size <= max) return { text: readFileSync(fd).toString("utf8"), size, cut: false };
|
|
274
|
+
const part = (at, n) => {
|
|
275
|
+
const b = Buffer.alloc(n);
|
|
276
|
+
return b.subarray(0, readSync(fd, b, 0, n, at));
|
|
277
|
+
};
|
|
278
|
+
return { text: Buffer.concat([part(0, max / 2), Buffer.from("\n"), part(size - max / 2, max / 2)]).toString("utf8"), size, cut: true };
|
|
279
|
+
} finally {
|
|
280
|
+
closeSync(fd);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// The verdict of one run; a pass whose output was too large to read whole is misleading (the seats judge it).
|
|
285
|
+
function verdictOf(r, out) {
|
|
286
|
+
if (r.error) return { verdict: "fail", why: `시작 실패(${r.error})` };
|
|
287
|
+
const v = judge({ ...r, text: out.text });
|
|
288
|
+
return v.verdict === "pass" && out.cut ? { verdict: "misleading", why: `출력 ${out.size} 바이트가 ${TEXT_MAX} 바이트를 넘어 앞뒤만 봄` } : v;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// ---- commands
|
|
292
|
+
|
|
293
|
+
class Usage extends Error {}
|
|
294
|
+
const FLAGS = {
|
|
295
|
+
init: ["cwd"], cleanup: ["dir"], run: ["dir", "id", "cmd", "timeout", "iter", "cwd"], note: ["dir", "id", "text", "iter", "exit"],
|
|
296
|
+
gate: ["dir", "gates", "iter", "cwd"], verify: ["dir"], fingerprint: ["cwd"],
|
|
297
|
+
};
|
|
298
|
+
const REQUIRED = { init: [], cleanup: ["dir"], run: ["dir", "id", "cmd", "timeout"], note: ["dir", "id", "text"], gate: ["dir", "gates", "iter"], verify: ["dir"], fingerprint: [] };
|
|
299
|
+
|
|
300
|
+
export function parseArgs(argv) {
|
|
301
|
+
const [command, ...rest] = argv;
|
|
302
|
+
if (!Object.prototype.hasOwnProperty.call(FLAGS, command)) throw new Usage(`모르는 명령이다: ${command ?? "(없음)"}`);
|
|
303
|
+
const o = {};
|
|
304
|
+
for (let i = 0; i < rest.length; ) i = readFlag(FLAGS[command], rest, i, o);
|
|
305
|
+
for (const r of REQUIRED[command]) if (!(r in o)) throw new Usage(`--${r} 가 필요하다`);
|
|
306
|
+
return { command, o };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// One `--name value` or `--name=value` at rest[i] into o; gives the index of the next one.
|
|
310
|
+
function readFlag(spec, rest, i, o) {
|
|
311
|
+
const m = /^--([a-z]+)(?:=([\s\S]*))?$/.exec(rest[i]);
|
|
312
|
+
if (!m || !spec.includes(m[1])) throw new Usage(`모르는 인자다: ${String(rest[i]).slice(0, 80)}`);
|
|
313
|
+
if (m[1] in o) throw new Usage(`--${m[1]} 를 두 번 줬다`);
|
|
314
|
+
const value = m[2] !== undefined ? m[2] : rest[i + 1];
|
|
315
|
+
if (value === undefined) throw new Usage(`--${m[1]} 에 값이 없다`);
|
|
316
|
+
o[m[1]] = value;
|
|
317
|
+
return m[2] !== undefined ? i + 1 : i + 2;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const gateId = (id) => {
|
|
321
|
+
if (!/^[a-z0-9][a-z0-9-]{0,47}$/.test(String(id))) throw new Usage(`게이트 id 는 영소문자, 숫자, - 로 48자까지다: ${String(id).slice(0, 60)}`);
|
|
322
|
+
return id;
|
|
323
|
+
};
|
|
324
|
+
const whole = (v, name, min, max) => {
|
|
325
|
+
const n = Number(v);
|
|
326
|
+
if (!Number.isInteger(n) || n < min || n > max) throw new Usage(`--${name} 는 ${min}~${max} 정수다: ${String(v).slice(0, 20)}`);
|
|
327
|
+
return n;
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
export function readEvidence(d) {
|
|
331
|
+
let text = "";
|
|
332
|
+
try {
|
|
333
|
+
text = readFileSync(join(d, EVIDENCE), "utf8");
|
|
334
|
+
} catch {
|
|
335
|
+
return [];
|
|
336
|
+
}
|
|
337
|
+
return text.split("\n").filter(Boolean).map((l) => {
|
|
338
|
+
try {
|
|
339
|
+
return JSON.parse(l);
|
|
340
|
+
} catch {
|
|
341
|
+
return { kind: "unreadable" };
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
const append = (d, record) => appendFileSync(join(d, EVIDENCE), `${JSON.stringify(record)}\n`, { mode: 0o600 });
|
|
346
|
+
|
|
347
|
+
function init(o, ctx) {
|
|
348
|
+
requireGitTree(o.cwd || process.cwd(), ctx.env);
|
|
349
|
+
const { dir, base } = makeRunFolder(ctx, GATES_KIND);
|
|
350
|
+
return [{ ok: true, dir, base }, 0];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// The gate folder alone (an older ralph-qa.* folder goes with seat-run.mjs cleanup, which knows its answer folder).
|
|
354
|
+
function cleanup(o, ctx) {
|
|
355
|
+
return [{ ok: true, ...removeRunFolder(o.dir, ctx, [GATES_KIND]) }, 0];
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function run(o, ctx) {
|
|
359
|
+
const { d } = runFolder(o.dir, ctx, KINDS);
|
|
360
|
+
const [id, iter, limit] = [gateId(o.id), whole(o.iter ?? 0, "iter", 0, 999), whole(o.timeout, "timeout", 1, 86400)];
|
|
361
|
+
const cwd = resolve(o.cwd || process.cwd());
|
|
362
|
+
if (!existsSync(cwd)) throw new Stop("no-cwd", `--cwd 폴더가 없다: ${cwd}`);
|
|
363
|
+
requireGitTree(cwd, ctx.env);
|
|
364
|
+
const { seq, output, fd } = newOutput(d, id, iter);
|
|
365
|
+
const startedAt = new Date().toISOString();
|
|
366
|
+
const r = await execute({ cmd: o.cmd, cwd, limitMs: limit * 1000, fd }, ctx);
|
|
367
|
+
const out = outputText(output);
|
|
368
|
+
const snap = await snapshot(cwd, ctx.env);
|
|
369
|
+
const v = verdictOf(r, out);
|
|
370
|
+
const record = {
|
|
371
|
+
kind: "observed", id, iter, seq, cmd: o.cmd, cwd, shell: r.shell, startedAt, endedAt: new Date().toISOString(), secs: r.secs,
|
|
372
|
+
exit: r.exit, signal: r.signal, timedOut: r.timedOut, timeout: limit, output, outputSha256: hashFile(createHash, output), bytes: out.size,
|
|
373
|
+
git: snap.git, fingerprint: snap.fingerprint, verdict: v.verdict, why: v.why,
|
|
374
|
+
signature: v.verdict === "pass" ? null : signatureOf(id, v.verdict, out.text, [cwd, snap.root]),
|
|
375
|
+
};
|
|
376
|
+
append(d, record);
|
|
377
|
+
return [record, 0];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function note(o, ctx) {
|
|
381
|
+
const { d } = runFolder(o.dir, ctx, KINDS);
|
|
382
|
+
const record = { kind: "reported", id: gateId(o.id), iter: whole(o.iter ?? 0, "iter", 0, 999), exit: o.exit === undefined ? null : Number(o.exit),
|
|
383
|
+
text: String(o.text).slice(0, 4000), at: new Date().toISOString(), seq: claimSeq(d) };
|
|
384
|
+
append(d, record);
|
|
385
|
+
return [record, 0];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async function gate(o, ctx) {
|
|
389
|
+
const { d } = runFolder(o.dir, ctx, KINDS);
|
|
390
|
+
const gates = String(o.gates).split(",").map((g) => g.trim()).filter(Boolean).map((g) => gateId(g.replace(/^custom:.*$/s, "custom")));
|
|
391
|
+
const evidence = readEvidence(d);
|
|
392
|
+
const snap = await snapshot(resolve(o.cwd || process.cwd()), ctx.env);
|
|
393
|
+
const result = gateConjunct(evidence, gates, snap.fingerprint, whole(o.iter, "iter", 0, 999));
|
|
394
|
+
return [{ ...result, fingerprint: snap.fingerprint, repeatedFailures: repeatedFailures(evidence) }, result.ok ? 0 : 1];
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function verify(o, ctx) {
|
|
398
|
+
const { d } = runFolder(o.dir, ctx, KINDS);
|
|
399
|
+
const checked = readEvidence(d).filter((r) => r.kind === "observed").map((r) => {
|
|
400
|
+
let now = null;
|
|
401
|
+
try {
|
|
402
|
+
now = hashFile(createHash, r.output);
|
|
403
|
+
} catch {
|
|
404
|
+
/* gone */
|
|
405
|
+
}
|
|
406
|
+
return { seq: r.seq, id: r.id, output: r.output, state: now === null ? "missing" : now === r.outputSha256 ? "same" : "changed" };
|
|
407
|
+
});
|
|
408
|
+
const bad = checked.filter((c) => c.state !== "same");
|
|
409
|
+
return [{ ok: bad.length === 0, checked: checked.length, bad }, bad.length ? 1 : 0];
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function fingerprint(o, ctx) {
|
|
413
|
+
const snap = await snapshot(resolve(o.cwd || process.cwd()), ctx.env);
|
|
414
|
+
return [snap, 0];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const COMMANDS = { init, cleanup, run, note, gate, verify, fingerprint };
|
|
418
|
+
|
|
419
|
+
// A stop as JSON: what it is, its line, its paths, and for not-a-git-tree the folder it checked.
|
|
420
|
+
const stopJson = (e) => ({ ok: false, stop: e.code, message: `gate-evidence: ${e.message}`, detail: e.detail, ...(e.checked ? { checked: e.checked } : {}) });
|
|
421
|
+
|
|
422
|
+
// A usage error and an internal one on stderr (2), a stop as JSON on stdout (3).
|
|
423
|
+
function failure(e, out, err) {
|
|
424
|
+
if (e instanceof Stop) {
|
|
425
|
+
out.write(`${JSON.stringify(stopJson(e), null, 2)}\n`);
|
|
426
|
+
return 3;
|
|
427
|
+
}
|
|
428
|
+
err.write(e instanceof Usage ? `gate-evidence: ${e.message}\n` : `gate-evidence: 내부 오류(${e?.code || e?.name || "error"}): ${String(e?.message || e).split("\n")[0]}\n`);
|
|
429
|
+
return 2;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export async function main(argv, io = {}) {
|
|
433
|
+
const ctx = { env: io.env || process.env, platform: io.platform || process.platform };
|
|
434
|
+
const out = io.stdout || process.stdout;
|
|
435
|
+
try {
|
|
436
|
+
const { command, o } = parseArgs(argv);
|
|
437
|
+
const [result, code] = await COMMANDS[command](o, ctx);
|
|
438
|
+
out.write(`${JSON.stringify({ command, ...result }, null, 2)}\n`);
|
|
439
|
+
return code;
|
|
440
|
+
} catch (e) {
|
|
441
|
+
return failure(e, out, io.stderr || process.stderr);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const invokedDirectly = () => {
|
|
446
|
+
try {
|
|
447
|
+
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
|
448
|
+
} catch {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
if (invokedDirectly()) {
|
|
454
|
+
main(process.argv.slice(2)).then((code) => {
|
|
455
|
+
process.exitCode = code;
|
|
456
|
+
});
|
|
457
|
+
}
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
* word after a space, a quote, `(` or `=` as in `env`, `docker -e`, `--from-literal=`, or after a Dockerfile
|
|
15
15
|
* `ENV KEY value`; the shell's own PWD or OLDPWD holding a path is no password),
|
|
16
16
|
* secrets given as command-line flags, curl's user and password flag,
|
|
17
|
-
* hex keys, Basic and Bearer credentials, credentials in URLs
|
|
17
|
+
* hex keys, Basic and Bearer credentials, credentials in URLs, a .netrc password (on its machine line or on a line
|
|
18
|
+
* of its own), a .pgpass line, docker's config.json "auth" (base64 of user:password) and
|
|
18
19
|
* long high-entropy strings (a lockfile's sha512- integrity hash and a Go or Terraform h1: hash
|
|
19
20
|
* aside); they do not prove a payload clean. A Docker secret file's path after a *_FILE key is
|
|
20
21
|
* not read as the password it points to, and a value masked as a whole `[REDACTED]` word (the mask
|
|
@@ -74,8 +75,22 @@ export const PATTERNS = [
|
|
|
74
75
|
// that would let admin:admin through.
|
|
75
76
|
["curl-user", /(?<!\S)(?:-u\s*|--user(?:\s+|=))(?!(["']?)\d{1,10}:\d{1,10}\1(?=["'`\s;|&)]|$))(?!["']?\+%|\))[^\s:=]{1,256}:(?!\/)\S{3,256}/],
|
|
76
77
|
["assigned-hex", /(?:key|auth|token|secret)[\w-]{0,64}["']?\s*[:=]\s*["']?[0-9a-f]{32,65536}(?![0-9a-z])/i],
|
|
78
|
+
// .netrc: `machine <host> ... password <value>` on one line, or a `password <value>` line of its own (the file's
|
|
79
|
+
// other layout, one token pair a line); a value that is a reference or a placeholder is none
|
|
80
|
+
["netrc-password", /^[+\- ]?\s*(?:(?:machine\s+\S{1,256}|default)(?:\s+\S{1,256}){0,16}?\s+password\s+(?![$<{%])\S|password\s+(?![$<{%])\S{1,256}\s*$)/],
|
|
81
|
+
// .pgpass: host:port:database:user:password, the port a number or *, no empty field. The host is not digits alone
|
|
82
|
+
// and the password holds no colon but an escaped one (\:), so a MAC of digits or a time (12:30:45:00) is no hit.
|
|
83
|
+
["pgpass-line", /^[+\- ]?(?=[^\s:#]*[^\d\s:#])[^\s:#+\-][^\s:#]{0,255}:(?:\d{1,5}|\*):[^\s:]{1,256}:[^\s:]{1,256}:(?!\*?$)(?:\\.|[^\s:\\]){1,256}$/],
|
|
84
|
+
// docker's config.json: "auth" holds base64 of user:password (checked by decoding it, below)
|
|
85
|
+
["docker-auth", /"auth"\s*:\s*"[A-Za-z0-9+/]{4,65536}={0,2}"/],
|
|
77
86
|
];
|
|
78
87
|
|
|
88
|
+
// A kind whose pattern alone reads too wide: the line counts only when this holds as well.
|
|
89
|
+
const DOCKER_AUTH = /"auth"\s*:\s*"([A-Za-z0-9+/]{4,65536}={0,2})"/;
|
|
90
|
+
const CONFIRM = {
|
|
91
|
+
"docker-auth": (line) => Buffer.from(DOCKER_AUTH.exec(line)[1], "base64").toString("latin1").includes(":"),
|
|
92
|
+
};
|
|
93
|
+
|
|
79
94
|
// Long runs of base64-like characters whose Shannon entropy is that of random data, as
|
|
80
95
|
// lineage's redaction measures them (32 characters or more, 4.5 bits per character or more).
|
|
81
96
|
const LONG_RUN = /[A-Za-z0-9+/=]{32,65536}/g;
|
|
@@ -113,6 +128,8 @@ const KEY_END = new RegExp(`-----END ${PRIVATE_KEY}`);
|
|
|
113
128
|
// The mask SKILL.md asks for, as a word of its own. One glued to the rest of a value is no mask: what is left is read.
|
|
114
129
|
const WHOLE_MASK = /(?<!\w)\[REDACTED\](?!\w)/g;
|
|
115
130
|
|
|
131
|
+
const caught = (kind, re, plain, keyed) => re.test(KEYED.has(kind) ? keyed : plain) && (!CONFIRM[kind] || CONFIRM[kind](plain));
|
|
132
|
+
|
|
116
133
|
// Each line's secret-like kinds, once per kind and line. A private key closes at the first END
|
|
117
134
|
// marker at or after its line: the END lines are found once and walked with the lines, so many
|
|
118
135
|
// BEGIN lines without an END still take linear time.
|
|
@@ -126,7 +143,7 @@ export function scan(text) {
|
|
|
126
143
|
const plain = line.replace(WHOLE_MASK, "-");
|
|
127
144
|
const keyed = plain.replace(SECRET_FILE_PATH, "$1-").replace(SHELL_DIR, "$1-");
|
|
128
145
|
for (const [kind, re] of PATTERNS) {
|
|
129
|
-
if (!
|
|
146
|
+
if (!caught(kind, re, plain, keyed)) continue;
|
|
130
147
|
hits.push(kind === "private-key" ? { line: i + 1, kind, end: ends[next] ?? null } : { line: i + 1, kind });
|
|
131
148
|
}
|
|
132
149
|
if (highEntropy(plain)) hits.push({ line: i + 1, kind: "high-entropy" });
|