@mutmutco/cursor-plugin 4.2.7 → 4.3.1
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/.cursor-plugin/plugin.json +2 -3
- package/package.json +1 -1
- package/scripts/edit-tool-paths.mjs +4 -4
- package/skills/bootstrap/SKILL.md +2 -2
- package/skills/bootstrap/seeds/README.template.md +2 -2
- package/skills/bootstrap/seeds/gate.template.yml +5 -5
- package/skills/bootstrap/seeds/manifest.json +1 -0
- package/skills/bootstrap/seeds/test-policy.template.json +4 -0
- package/skills/hotfix/SKILL.md +1 -1
- package/skills/rcand/SKILL.md +1 -1
- package/skills/release/SKILL.md +38 -9
- package/skills/secrets/SKILL.md +1 -1
- package/skills/stage/SKILL.md +1 -1
- package/bin/mmi-hook +0 -2
- package/bin/mmi-hook-console.cmd +0 -16
- package/bin/mmi-hook.exe +0 -0
- package/hooks/cursor-hooks.json +0 -26
- package/scripts/command-ladder-core.mjs +0 -339
- package/scripts/command-ladder-gate.mjs +0 -126
- package/scripts/deny-gate-crash.mjs +0 -179
- package/scripts/env-write-lint.mjs +0 -146
- package/scripts/hook-io.mjs +0 -22
- package/scripts/hook-policy.mjs +0 -78
- package/scripts/hook-run.mjs +0 -434
- package/scripts/hook-trace.mjs +0 -151
- package/scripts/pretooluse-shell-gates.mjs +0 -720
- package/scripts/secret-echo-lint.mjs +0 -177
- package/scripts/test-command-policy-core.mjs +0 -294
- package/scripts/throttle-core.mjs +0 -332
- package/scripts/vault-edit-gate.mjs +0 -94
- package/skills/browser-automation/SKILL.md +0 -122
- package/skills/mmi/SKILL.md +0 -544
- package/skills/mmi-doctor/SKILL.md +0 -66
- package/skills/mmi-resume/SKILL.md +0 -123
- package/skills/onboard/SKILL.md +0 -72
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
// Secret-echo PreToolUse lint (#2611): zero-LLM guard against printing secrets-inserted env
|
|
2
|
-
// vars to the transcript. Runs before every Bash / PowerShell tool use (PreToolUse hook).
|
|
3
|
-
// Flags echo/printf/printenv of env vars whose name matches the secret-name pattern, and
|
|
4
|
-
// whole-environment dumps (bare env, printenv, set, Get-ChildItem env:). Never crashes a
|
|
5
|
-
// turn — every failure path exits 0 and allows the call.
|
|
6
|
-
//
|
|
7
|
-
// MODE: advisory by default (warn on stderr, exit 0). MMI_SECRET_ECHO_LINT=block flips to
|
|
8
|
-
// emit the PreToolUse deny JSON. Ship: advisory.
|
|
9
|
-
const MODE = process.env.MMI_SECRET_ECHO_LINT ?? 'advisory';
|
|
10
|
-
|
|
11
|
-
import { readHookInput } from './hook-io.mjs';
|
|
12
|
-
import { appendHookActivity } from './hook-trace.mjs';
|
|
13
|
-
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
// Pure detection logic — exported for tests (no IO here).
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Secret-name pattern reused from scripts/sensitive-value-mask.mjs secret-assignment matcher.
|
|
20
|
-
*/
|
|
21
|
-
const SECRET_NAME_RE = /(?:API[_-]?KEY|SECRET|TOKEN|PASSWORD|PASSWD|PASSPHRASE|PRIVATE[_-]?KEY|CLIENT[_-]?SECRET|ACCESS[_-]?KEY|CREDENTIALS?)/i;
|
|
22
|
-
|
|
23
|
-
function isSecretName(name) {
|
|
24
|
-
return SECRET_NAME_RE.test(name);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Whole-environment dump patterns (per-segment anchors)
|
|
28
|
-
const BARE_ENV = /^\s*env\s*$/;
|
|
29
|
-
const BARE_PRINTENV = /^\s*printenv\s*$/;
|
|
30
|
-
const BARE_SET = /^\s*set\s*$/;
|
|
31
|
-
const PS_ENV_DUMP = /\b(?:Get-ChildItem|gci|ls|dir)\s+env:/i;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Inspect a shell command for secret-named env var print or whole-environment dumps.
|
|
35
|
-
* Returns null (clean) or { block: true, reason }.
|
|
36
|
-
*
|
|
37
|
-
* @param {string} command
|
|
38
|
-
* @returns {{ block: boolean, reason: string } | null}
|
|
39
|
-
*/
|
|
40
|
-
export function analyze(command) {
|
|
41
|
-
if (!command || typeof command !== 'string') return null;
|
|
42
|
-
const trimmed = command.trim();
|
|
43
|
-
if (!trimmed) return null;
|
|
44
|
-
|
|
45
|
-
// Split first so `secrets use` exemptions and dump checks are per-segment
|
|
46
|
-
// (closes compound bypass + newline-separated dump miss; lone-CR line breaks split too).
|
|
47
|
-
const segments = trimmed.split(/[;&|]|[\r\n]+/);
|
|
48
|
-
for (const raw of segments) {
|
|
49
|
-
const seg = raw.trim();
|
|
50
|
-
if (!seg) continue;
|
|
51
|
-
if (/^secrets\s+use\b/.test(seg)) continue;
|
|
52
|
-
if (/^(?:export|local|declare|typeset)\s/.test(seg)) continue;
|
|
53
|
-
|
|
54
|
-
if (BARE_ENV.test(seg)) {
|
|
55
|
-
return { block: true, reason: 'bare `env` dumps all environment variables including secrets-sealed values' };
|
|
56
|
-
}
|
|
57
|
-
if (BARE_PRINTENV.test(seg)) {
|
|
58
|
-
return { block: true, reason: 'bare `printenv` dumps all environment variables including secrets-sealed values' };
|
|
59
|
-
}
|
|
60
|
-
if (BARE_SET.test(seg)) {
|
|
61
|
-
return { block: true, reason: 'bare `set` dumps all environment variables including secrets-sealed values' };
|
|
62
|
-
}
|
|
63
|
-
if (PS_ENV_DUMP.test(seg)) {
|
|
64
|
-
return { block: true, reason: '`Get-ChildItem env:` dumps all environment variables including secrets-sealed values' };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const hit = checkBashPrint(seg) || checkPsPrint(seg);
|
|
68
|
-
if (hit) return hit;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function checkBashPrint(segment) {
|
|
75
|
-
// printenv SECRET_NAME
|
|
76
|
-
const peMatch = segment.match(/(?:^|\s)printenv\s+(\w+)(?:\s|$)/);
|
|
77
|
-
if (peMatch && isSecretName(peMatch[1])) {
|
|
78
|
-
return { block: true, reason: `printenv of $${peMatch[1]} prints a secret-named env var` };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// echo/printf with $SECRET_VAR
|
|
82
|
-
if (/\b(?:echo|printf)\b/.test(segment)) {
|
|
83
|
-
for (const m of segment.matchAll(/\$[\{]?(\w+)/g)) {
|
|
84
|
-
if (isSecretName(m[1])) {
|
|
85
|
-
return { block: true, reason: `echo/printf of $${m[1]} prints a secret-named env var` };
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function checkPsPrint(segment) {
|
|
94
|
-
// PowerShell accepts both $env:SECRET_VAR and ${env:SECRET_VAR}; keep one matcher so the braced form
|
|
95
|
-
// cannot bypass the same output checks as the ordinary form.
|
|
96
|
-
const envReference = /\$(?:\{env:(\w+)\}|env:(\w+))/gi;
|
|
97
|
-
const envName = (match) => match[1] ?? match[2];
|
|
98
|
-
|
|
99
|
-
// Write-Output / Write-Host / echo with $env:SECRET_VAR
|
|
100
|
-
if (/\b(?:Write-Output|Write-Host|echo)\b/i.test(segment)) {
|
|
101
|
-
for (const m of segment.matchAll(envReference)) {
|
|
102
|
-
const name = envName(m);
|
|
103
|
-
if (isSecretName(name)) {
|
|
104
|
-
return { block: true, reason: `Write/echo of $env:${name} prints a secret-named env var` };
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// $env:SECRET_VAR in output position (start of command or after pipe, not assignment LHS)
|
|
110
|
-
for (const m of segment.matchAll(new RegExp(`(?:^|[;&|]\\s*)${envReference.source}`, 'gi'))) {
|
|
111
|
-
const name = envName(m);
|
|
112
|
-
const after = segment.slice(m.index + m[0].length);
|
|
113
|
-
if (/^\s*=(?!=)/.test(after)) continue; // assignment LHS, not output
|
|
114
|
-
if (isSecretName(name)) {
|
|
115
|
-
return { block: true, reason: `$env:${name} in output position prints a secret-named env var` };
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// ---------------------------------------------------------------------------
|
|
123
|
-
// IO + main — only runs when this file is the entry point.
|
|
124
|
-
// ---------------------------------------------------------------------------
|
|
125
|
-
|
|
126
|
-
function preToolUseDeny(reason) {
|
|
127
|
-
return JSON.stringify({
|
|
128
|
-
hookSpecificOutput: {
|
|
129
|
-
hookEventName: 'PreToolUse',
|
|
130
|
-
permissionDecision: 'deny',
|
|
131
|
-
permissionDecisionReason: reason,
|
|
132
|
-
},
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async function main() {
|
|
137
|
-
let input;
|
|
138
|
-
try {
|
|
139
|
-
input = await readHookInput();
|
|
140
|
-
} catch {
|
|
141
|
-
appendHookActivity({
|
|
142
|
-
event: 'PreToolUse',
|
|
143
|
-
script: 'secret-echo-lint',
|
|
144
|
-
outcome: 'failed',
|
|
145
|
-
action: 'could not read hook input',
|
|
146
|
-
});
|
|
147
|
-
process.exit(0);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const result = analyze(input?.tool_input?.command);
|
|
151
|
-
|
|
152
|
-
appendHookActivity({
|
|
153
|
-
event: 'PreToolUse',
|
|
154
|
-
script: 'secret-echo-lint',
|
|
155
|
-
outcome: result?.block ? (MODE === 'block' ? 'deny' : 'observe') : 'ran',
|
|
156
|
-
action: result?.block ? result.reason : 'clean',
|
|
157
|
-
tool: input?.tool_name,
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
if (result?.block) {
|
|
161
|
-
if (MODE === 'block') {
|
|
162
|
-
process.stdout.write(preToolUseDeny(result.reason) + '\n');
|
|
163
|
-
} else {
|
|
164
|
-
process.stderr.write(`[mmi-secret-echo-lint] would-block: ${result.reason}\n`);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
process.exit(0);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (
|
|
172
|
-
process.argv[1] &&
|
|
173
|
-
(process.argv[1].endsWith('secret-echo-lint.mjs') ||
|
|
174
|
-
process.argv[1].replace(/\\/g, '/').endsWith('scripts/secret-echo-lint.mjs'))
|
|
175
|
-
) {
|
|
176
|
-
main().catch(() => process.exit(0));
|
|
177
|
-
}
|
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
// test-command-policy-core.mjs — shared verdict for "may this diff run tests?" (#5519), and the
|
|
2
|
-
// ONE `Test-Policy-Override` reader both surfaces share (#5804).
|
|
3
|
-
//
|
|
4
|
-
// `mmi-cli tests policy` and the PreToolUse test-command gate both answer that question. Before
|
|
5
|
-
// #5519 they answered it separately: the CLI summary printed the repository's CONFIGURED mandatory
|
|
6
|
-
// glob count on an OK line, while the gate independently matched the task diff against those globs
|
|
7
|
-
// and refused a focused test when none hit. Agents read "8 mandatory glob(s)" as permission, then
|
|
8
|
-
// hit TEST-POLICY TEST COMMAND REFUSED on the next line. Delegated workers and the parent hook also
|
|
9
|
-
// diverged when they did not share an evaluator.
|
|
10
|
-
//
|
|
11
|
-
// #5804 reopened the same divergence along the waiver axis: the CLI honoured a valid override
|
|
12
|
-
// trailer on the findings layer and printed OK, while the gate — fed the same diff without the
|
|
13
|
-
// waiver — refused the matching focused test on the next line. So the reader lives HERE, next to
|
|
14
|
-
// the verdict it feeds, and both surfaces pass the SAME honoured waiver into `evaluateTestCommandPolicy`.
|
|
15
|
-
// The reader is the only git-IO in this module, and it is git's own trailer parser plus the exact
|
|
16
|
-
// guards #3628/#3637 established — the hook must never grow a second, looser trailer parser.
|
|
17
|
-
|
|
18
|
-
import { execFileSync } from 'node:child_process';
|
|
19
|
-
|
|
20
|
-
/** Command class the PreToolUse gate regulates. Non-test verification is never refused here. */
|
|
21
|
-
export const TEST_COMMAND_CLASS = 'test';
|
|
22
|
-
|
|
23
|
-
/** The commit trailer that carries a waiver. Live here so the reader and every message built from
|
|
24
|
-
* it spell the key identically. */
|
|
25
|
-
export const TRAILER_KEY = 'Test-Policy-Override';
|
|
26
|
-
/** NOT the authority on what is honoured — {@link readOverride} asks git for that. This is the
|
|
27
|
-
* DISAGREEMENT detector: a line shaped like the trailer that git's parser does not report is the
|
|
28
|
-
* defect worth surfacing, because it waives the gate while leaving the audit trail empty. */
|
|
29
|
-
const OVERRIDE_RE = /^Test-Policy-Override:\s*(.+)$/im;
|
|
30
|
-
/** Record and field separators for the one `git log` call that reads sha, trailer and body at once. */
|
|
31
|
-
const REC = '\u001e';
|
|
32
|
-
const FLD = '\u001f';
|
|
33
|
-
|
|
34
|
-
/** The kinds a `Test-Policy-Override` trailer may waive — the diff rules and nothing else. The
|
|
35
|
-
* three refusal kinds (unresolvable-base, untrusted-range, malformed-override-trailer) are
|
|
36
|
-
* deliberately absent: a waiver read out of a range the gate cannot trust would be the original
|
|
37
|
-
* defect wearing the fix's clothes. */
|
|
38
|
-
export const WAIVABLE_KINDS = [
|
|
39
|
-
'mandatory-zone-untested',
|
|
40
|
-
'unrequested-test-file',
|
|
41
|
-
'protected-removed',
|
|
42
|
-
'stale-protected-entry',
|
|
43
|
-
'stale-satisfied-by',
|
|
44
|
-
];
|
|
45
|
-
|
|
46
|
-
/** The one waivable kind whose finding is a VERDICT ON OUT-OF-ZONE TEST WORK ITSELF (#5804). Rule 1
|
|
47
|
-
* always co-occurs with a matched glob (test commands already allowed there), and the deletion and
|
|
48
|
-
* staleness kinds speak to policy hygiene, not to running tests — waiving them buys the deletion,
|
|
49
|
-
* never the command class. `unrequested-test-file` is different: its finding says exactly what the
|
|
50
|
-
* command gate refuses, "this test work was never asked for". A waiver over it IS the human's
|
|
51
|
-
* "this test is requested" decision, so it — and only it — authorizes the `test` class. */
|
|
52
|
-
const TEST_WORK_KIND = 'unrequested-test-file';
|
|
53
|
-
|
|
54
|
-
/** What counts as a test FILE. Lives here with the verdict that reads it, and is re-imported by
|
|
55
|
-
* cli/src/test-policy-core.ts, for the reason the module header gives: two copies of a predicate
|
|
56
|
-
* both surfaces answer with is how #5519 and #5804 each started. */
|
|
57
|
-
export const TEST_FILE_RE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
58
|
-
export const PY_TEST_FILE_RE = /(?:^|\/)test_[^/]*\.py$|_test\.py$/;
|
|
59
|
-
|
|
60
|
-
/** Is this path a test file by name? JS/TS and Python, because the guard regulates both estates. */
|
|
61
|
-
export function isTestPath(path) {
|
|
62
|
-
return typeof path === 'string' && (TEST_FILE_RE.test(path) || PY_TEST_FILE_RE.test(path));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Does this diff EDIT a test file that already existed at the base? (#5842)
|
|
66
|
-
*
|
|
67
|
-
* A zone outside the mandatory globs means no test is REQUIRED there. It never meant no test may be
|
|
68
|
-
* RUN. Those read the same to a guard that only looks at globs, and the difference is what the
|
|
69
|
-
* refusal cost: a repo whose policy deliberately leaves `scripts/**` unmandated still runs
|
|
70
|
-
* `scripts/*.test.mjs` in its REQUIRED gate, so a fix to one of those tests could go red in CI while
|
|
71
|
-
* every local attempt to run that exact file was denied — a three-minute round trip per attempt,
|
|
72
|
-
* with the file's own fixtures unverifiable any other way.
|
|
73
|
-
*
|
|
74
|
-
* ADDED test files are deliberately excluded, and that exclusion is the whole reason this is not
|
|
75
|
-
* simply "is a test file in the diff". An out-of-zone test the diff CREATES is precisely what
|
|
76
|
-
* `unrequested-test-file` refuses and what a `Test-Policy-Override` waiver exists to authorize
|
|
77
|
-
* (#5804). If writing one also bought permission to run it, the waiver would authorize nothing that
|
|
78
|
-
* was not already free, and "tests are opt-in" would be enforced only at PR time. Editing a test
|
|
79
|
-
* that is already in the tree asserts nothing about whether new test work was requested — the file
|
|
80
|
-
* is there, the required gate already runs it, and the only question left is whether the author may
|
|
81
|
-
* watch it pass. A rename counts as added: the new name did not exist at the base either. */
|
|
82
|
-
export function editsExistingTest(paths, addedPaths = []) {
|
|
83
|
-
const added = new Set(Array.isArray(addedPaths) ? addedPaths : []);
|
|
84
|
-
const list = Array.isArray(paths) ? paths : [];
|
|
85
|
-
return list.some((path) => isTestPath(path) && !added.has(path));
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Glob body → regex body. Supports `**`, `*`, and `{a,b}` (including wildcards inside braces).
|
|
90
|
-
* Kept byte-compatible with {@link globToRegExp} in cli/src/test-policy-core.ts so rule matching
|
|
91
|
-
* and the command guard cannot drift on the same policy file.
|
|
92
|
-
*/
|
|
93
|
-
export function translateGlob(glob) {
|
|
94
|
-
let out = '';
|
|
95
|
-
for (let i = 0; i < glob.length; i += 1) {
|
|
96
|
-
const char = glob[i];
|
|
97
|
-
if (char === '*') {
|
|
98
|
-
if (glob[i + 1] === '*') {
|
|
99
|
-
if (glob[i + 2] === '/') {
|
|
100
|
-
out += '(?:.*/)?';
|
|
101
|
-
i += 2;
|
|
102
|
-
} else {
|
|
103
|
-
out += '.*';
|
|
104
|
-
i += 1;
|
|
105
|
-
}
|
|
106
|
-
} else out += '[^/]*';
|
|
107
|
-
} else if (char === '{') {
|
|
108
|
-
const close = glob.indexOf('}', i);
|
|
109
|
-
if (close === -1) out += '\\{';
|
|
110
|
-
else {
|
|
111
|
-
out += `(?:${glob.slice(i + 1, close).split(',').map(translateGlob).join('|')})`;
|
|
112
|
-
i = close;
|
|
113
|
-
}
|
|
114
|
-
} else {
|
|
115
|
-
out += /[.+?^${}()|[\]\\]/.test(char) ? `\\${char}` : char;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return out;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export function globToRegExp(glob) {
|
|
122
|
-
return new RegExp(`^${translateGlob(glob)}$`);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function mandatoryGlobList(mandatory) {
|
|
126
|
-
if (!Array.isArray(mandatory)) return [];
|
|
127
|
-
return mandatory.map((entry) => (typeof entry === 'string' ? entry : entry?.glob)).filter((glob) => typeof glob === 'string');
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** Mandatory globs that match at least one path in `paths`. Order follows the policy declaration. */
|
|
131
|
-
export function matchedMandatoryGlobs(paths, mandatory) {
|
|
132
|
-
const globs = mandatoryGlobList(mandatory);
|
|
133
|
-
const list = Array.isArray(paths) ? paths : [];
|
|
134
|
-
return globs.filter((glob) => {
|
|
135
|
-
const re = globToRegExp(glob);
|
|
136
|
-
return list.some((path) => re.test(path));
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Decide whether test commands are allowed against a path set, a mandatory zone, and the waiver the
|
|
142
|
-
* caller has already read for this range (#5804).
|
|
143
|
-
*
|
|
144
|
-
* @param {{ paths: string[], mandatory?: unknown, regulated?: boolean,
|
|
145
|
-
* override?: { kinds?: string[] } | null }} input
|
|
146
|
-
* `regulated: false` — no test-policy.json (estate default); tests are not gated.
|
|
147
|
-
* `regulated: true` (default) — a declared policy applies; zero matched globs refuses `test`.
|
|
148
|
-
* `override` — the waiver honoured for this range, as {@link readOverride} returned it, or null.
|
|
149
|
-
* A waiver whose kinds cover {@link TEST_WORK_KIND} authorizes the `test` class even with zero
|
|
150
|
-
* matched globs: reporting and enforcement then state ONE decision, because both feed this
|
|
151
|
-
* function the same receipt.
|
|
152
|
-
* `addedPaths` — the subset of `paths` this diff CREATED. #5842: an edit to a test that already
|
|
153
|
-
* existed authorizes the class; creating one does not — see {@link editsExistingTest}.
|
|
154
|
-
*/
|
|
155
|
-
export function evaluateTestCommandPolicy({ paths, mandatory, regulated = true, override = null, addedPaths = [] } = {}) {
|
|
156
|
-
const configuredMandatoryCount = mandatoryGlobList(mandatory).length;
|
|
157
|
-
if (!regulated) {
|
|
158
|
-
return {
|
|
159
|
-
configuredMandatoryCount: 0,
|
|
160
|
-
matchedMandatoryGlobs: [],
|
|
161
|
-
matchedMandatoryCount: 0,
|
|
162
|
-
testCommandsAllowed: true,
|
|
163
|
-
editsExistingTest: false,
|
|
164
|
-
reasonId: null,
|
|
165
|
-
commandClasses: { allowed: [TEST_COMMAND_CLASS], refused: [] },
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
const matched = matchedMandatoryGlobs(paths, mandatory);
|
|
169
|
-
const overrideAuthorizes = Array.isArray(override?.kinds) && override.kinds.includes(TEST_WORK_KIND);
|
|
170
|
-
const existingTestEdited = editsExistingTest(paths, addedPaths);
|
|
171
|
-
const testCommandsAllowed = matched.length > 0 || overrideAuthorizes || existingTestEdited;
|
|
172
|
-
return {
|
|
173
|
-
configuredMandatoryCount,
|
|
174
|
-
matchedMandatoryGlobs: matched,
|
|
175
|
-
matchedMandatoryCount: matched.length,
|
|
176
|
-
testCommandsAllowed,
|
|
177
|
-
/** #5842: true when the diff edits a test file that already existed — one of the facts that can
|
|
178
|
-
* authorize the class, named so a refusal can say what was missing. */
|
|
179
|
-
editsExistingTest: existingTestEdited,
|
|
180
|
-
reasonId: testCommandsAllowed ? null : 'test-command-outside-mandatory-zone',
|
|
181
|
-
commandClasses: {
|
|
182
|
-
allowed: testCommandsAllowed ? [TEST_COMMAND_CLASS] : [],
|
|
183
|
-
refused: testCommandsAllowed ? [] : [TEST_COMMAND_CLASS],
|
|
184
|
-
},
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function git(args, cwd) {
|
|
189
|
-
return execFileSync('git', args, { windowsHide: true, cwd, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 });
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/** Split an optional `[kind, kind]` scope off the front of a trailer value. */
|
|
193
|
-
function parseScope(value) {
|
|
194
|
-
const scoped = /^\[([^\]]*)\]\s*([\s\S]*)$/.exec(value);
|
|
195
|
-
if (!scoped) return { kinds: [...WAIVABLE_KINDS], reason: value, unknown: [] };
|
|
196
|
-
const named = scoped[1].split(',').map((k) => k.trim()).filter(Boolean);
|
|
197
|
-
return {
|
|
198
|
-
kinds: named,
|
|
199
|
-
reason: scoped[2].trim(),
|
|
200
|
-
unknown: named.filter((k) => !WAIVABLE_KINDS.includes(k)),
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function invisibleTrailer(sha, line) {
|
|
205
|
-
return {
|
|
206
|
-
kind: 'malformed-override-trailer',
|
|
207
|
-
paths: [],
|
|
208
|
-
detail:
|
|
209
|
-
`OVERRIDE TRAILER GIT CANNOT SEE — commit ${sha.slice(0, 8)} carries\n`
|
|
210
|
-
+ ` ${line}\n`
|
|
211
|
-
+ ` but \`git log --format='%(trailers:key=${TRAILER_KEY})'\` reports nothing for it, so the waiver would\n`
|
|
212
|
-
+ ' exist only to the regex that granted it. The trailer was chosen over a flag because it can be\n'
|
|
213
|
-
+ ' AUDITED, and a reason the auditor cannot see is not a reason this gate accepts (#3628).\n'
|
|
214
|
-
+ ' Git reads trailers from the LAST paragraph only, and every line of that paragraph must be a\n'
|
|
215
|
-
+ ' trailer or an indented continuation — one bare line such as `Closes #123` disqualifies the whole\n'
|
|
216
|
-
+ ' block. Indent the continuation lines, and leave nothing but trailers in that paragraph.\n'
|
|
217
|
-
+ ' This is fixable forward: push a LATER commit on this branch carrying a well-formed trailer.\n'
|
|
218
|
-
+ ' The nearest waiver wins, and it supersedes this one — no force-push, no re-filed branch.',
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function unknownScope(sha, unknown) {
|
|
223
|
-
return {
|
|
224
|
-
kind: 'malformed-override-trailer',
|
|
225
|
-
paths: [],
|
|
226
|
-
detail:
|
|
227
|
-
`UNKNOWN OVERRIDE SCOPE — commit ${sha.slice(0, 8)} scopes its waiver to ${unknown.join(', ')}, which this\n`
|
|
228
|
-
+ ' gate cannot report.\n'
|
|
229
|
-
+ ` Waivable kinds: ${WAIVABLE_KINDS.join(', ')}.\n`
|
|
230
|
-
+ ' Refused rather than widened: treating a typo as "waive everything" is how a scoped waiver turns\n'
|
|
231
|
-
+ ' into a blanket exemption without anyone deciding it should.',
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Whether this clone's history is grafted. Anything other than a plain `false` — including a git that
|
|
237
|
-
* cannot answer — counts as shallow: the question being asked is "may I trust a commit range", and
|
|
238
|
-
* "I could not tell" is not a yes.
|
|
239
|
-
*/
|
|
240
|
-
export function isShallowRepository(cwd) {
|
|
241
|
-
try {
|
|
242
|
-
return git(['rev-parse', '--is-shallow-repository'], cwd).trim() !== 'false';
|
|
243
|
-
} catch {
|
|
244
|
-
return true;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Read the waiver out of `base..HEAD` with GIT's own trailer parser, and refuse the disagreements.
|
|
250
|
-
*
|
|
251
|
-
* The regex is kept, but demoted to a cross-check: what it sees and what git sees must be the same
|
|
252
|
-
* set, and where they differ the difference IS the finding. On Jerv-PowerTools@e3d4b8ba the regex
|
|
253
|
-
* granted a waiver git reported no trailer for, and stored the first line of it — `"src/commands/
|
|
254
|
-
* grind.ts is outside test-policy.json's"` — as the justification, argument discarded.
|
|
255
|
-
*
|
|
256
|
-
* Moved here from cli/src/test-policy-core.ts (#5804) so the PreToolUse gate reads the SAME waiver
|
|
257
|
-
* the CLI reports — git's parser, the folded-whole value, the sha, the `[kind]` scoping, and the
|
|
258
|
-
* nearest-waiver-wins precedence — rather than a parallel line regex that would reintroduce the
|
|
259
|
-
* #3628 class on the execution side.
|
|
260
|
-
*
|
|
261
|
-
* @param {string} base merge-base of the change set; the waiver is read from `base..HEAD` only
|
|
262
|
-
* @param {string} cwd repository root the git commands run in
|
|
263
|
-
* @returns {{ override: { sha: string, reason: string, kinds: string[] } | null,
|
|
264
|
-
* refusals: Array<{ kind: string, detail: string, paths: string[] }> }}
|
|
265
|
-
* A non-empty `refusals` lists override-shaped problems from commits NEWER than the winning
|
|
266
|
-
* waiver; the caller must treat the run as refused, never honour `override` past them.
|
|
267
|
-
*/
|
|
268
|
-
export function readOverride(base, cwd) {
|
|
269
|
-
const format = `%H${FLD}%(trailers:key=${TRAILER_KEY},valueonly,unfold)${FLD}%B${REC}`;
|
|
270
|
-
const refusals = [];
|
|
271
|
-
let override = null;
|
|
272
|
-
for (const record of git(['log', `${base}..HEAD`, `--format=${format}`], cwd).split(REC)) {
|
|
273
|
-
const [sha, trailer, body] = record.replace(/^\s+/, '').split(FLD);
|
|
274
|
-
if (!sha) continue;
|
|
275
|
-
// git log is newest-first, so the nearest waiver wins — as it always did — and once one is held
|
|
276
|
-
// every remaining record is OLDER than it and cannot change the answer. Stopping here is what
|
|
277
|
-
// gives a malformed trailer a forward-only remedy (#3637): the disagreement check below used to
|
|
278
|
-
// sit above this line, so an unparseable trailer refused the PR forever while the honoured waiver
|
|
279
|
-
// came from a commit after it. Rewriting that commit needs a force-push this estate does not do,
|
|
280
|
-
// which left re-filing the whole branch as the only exit. Precedence already applied to values;
|
|
281
|
-
// it now applies to the refusals read out of the same range.
|
|
282
|
-
if (override) break;
|
|
283
|
-
const value = (trailer ?? '').trim();
|
|
284
|
-
if (!value) {
|
|
285
|
-
const shaped = OVERRIDE_RE.exec(body ?? '');
|
|
286
|
-
if (shaped) refusals.push(invisibleTrailer(sha, shaped[0].trim()));
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
const { kinds, reason, unknown } = parseScope(value);
|
|
290
|
-
if (unknown.length > 0) refusals.push(unknownScope(sha, unknown));
|
|
291
|
-
else override = { sha, reason, kinds };
|
|
292
|
-
}
|
|
293
|
-
return { override, refusals };
|
|
294
|
-
}
|