@devrik-tools/claude-gates 0.2.0 → 0.3.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 +2 -2
- package/README.es.md +7 -6
- package/README.md +16 -7
- package/cli/index.mjs +4 -0
- package/cli/init.mjs +18 -1
- package/cli/install.mjs +26 -1
- package/package.json +5 -2
- package/plugins/gates/.claude-plugin/plugin.json +1 -1
- package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +8 -6
- package/plugins/gates/hooks/gates/autonomous-mode/stop.mjs +78 -0
- package/plugins/gates/hooks/gates/bash-commands/index.mjs +11 -81
- package/plugins/gates/hooks/gates/block-remote-publish/index.mjs +140 -0
- package/plugins/gates/hooks/gates/dependency-skills/index.mjs +130 -126
- package/plugins/gates/hooks/gates/neutral-spanish/index.mjs +104 -79
- package/plugins/gates/hooks/gates/no-memory-dependency/index.mjs +138 -123
- package/plugins/gates/hooks/gates/root-whitelist/index.mjs +59 -44
- package/plugins/gates/hooks/gates/test-after-implementation/index.mjs +124 -101
- package/plugins/gates/hooks/hooks.json +365 -346
- package/plugins/gates/hooks/lib/hook-io.mjs +39 -0
- package/plugins/tasks/.claude-plugin/plugin.json +1 -1
- package/plugins/tasks/hooks/register-requests.mjs +7 -3
- package/registry.json +738 -793
- package/cli/__tests__/config.test.mjs +0 -113
- package/cli/__tests__/init-flags.test.mjs +0 -111
- package/cli/__tests__/install.test.mjs +0 -102
- package/cli/__tests__/materialize.test.mjs +0 -95
- package/cli/__tests__/registry-gates-consistency.test.mjs +0 -52
- package/cli/__tests__/selection.test.mjs +0 -200
- package/cli/__tests__/task.test.mjs +0 -124
- package/cli/__tests__/version-consistency.test.mjs +0 -44
- package/plugins/gates/hooks/__tests__/ask-adoption.test.mjs +0 -83
- package/plugins/gates/hooks/__tests__/doctor.test.mjs +0 -85
- package/plugins/gates/hooks/__tests__/wiring-check.test.mjs +0 -65
- package/plugins/gates/hooks/gates/audit-before-build/audit-before-build.edge.test.mjs +0 -83
- package/plugins/gates/hooks/gates/audit-before-build/test.mjs +0 -117
- package/plugins/gates/hooks/gates/autonomous-mode/autonomous.edge.test.mjs +0 -104
- package/plugins/gates/hooks/gates/autonomous-mode/test.mjs +0 -58
- package/plugins/gates/hooks/gates/bash-commands/bash-commands.edge.test.mjs +0 -165
- package/plugins/gates/hooks/gates/bash-commands/test.mjs +0 -126
- package/plugins/gates/hooks/gates/brief-before-delegate/brief-before-delegate.edge.test.mjs +0 -151
- package/plugins/gates/hooks/gates/brief-before-delegate/test.mjs +0 -111
- package/plugins/gates/hooks/gates/circuit-breaker/circuit-breaker.edge.test.mjs +0 -207
- package/plugins/gates/hooks/gates/circuit-breaker/test.mjs +0 -161
- package/plugins/gates/hooks/gates/dependency-skills/dependency-skills.edge.test.mjs +0 -69
- package/plugins/gates/hooks/gates/dependency-skills/test.mjs +0 -109
- package/plugins/gates/hooks/gates/diagnosis-before-patch/diagnosis-before-patch.edge.test.mjs +0 -68
- package/plugins/gates/hooks/gates/diagnosis-before-patch/test.mjs +0 -68
- package/plugins/gates/hooks/gates/feature-catalog/edge.edge.test.mjs +0 -83
- package/plugins/gates/hooks/gates/feature-catalog/test.mjs +0 -97
- package/plugins/gates/hooks/gates/force-parallel/test.mjs +0 -88
- package/plugins/gates/hooks/gates/forge-flow/forge-flow.edge.test.mjs +0 -155
- package/plugins/gates/hooks/gates/forge-flow/test.mjs +0 -135
- package/plugins/gates/hooks/gates/implementation-pipeline/edge.edge.test.mjs +0 -66
- package/plugins/gates/hooks/gates/implementation-pipeline/test.mjs +0 -86
- package/plugins/gates/hooks/gates/intent-flow/intent-flow.edge.test.mjs +0 -96
- package/plugins/gates/hooks/gates/intent-flow/test.mjs +0 -165
- package/plugins/gates/hooks/gates/lint-commit/test.mjs +0 -85
- package/plugins/gates/hooks/gates/mandatory-flow/edge.edge.test.mjs +0 -100
- package/plugins/gates/hooks/gates/mandatory-flow/test.mjs +0 -119
- package/plugins/gates/hooks/gates/neutral-spanish/neutral-spanish.edge.test.mjs +0 -80
- package/plugins/gates/hooks/gates/neutral-spanish/test.mjs +0 -65
- package/plugins/gates/hooks/gates/never-assume/never-assume.edge.test.mjs +0 -71
- package/plugins/gates/hooks/gates/never-assume/test.mjs +0 -95
- package/plugins/gates/hooks/gates/no-blocking/no-blocking.edge.test.mjs +0 -109
- package/plugins/gates/hooks/gates/no-blocking/test.mjs +0 -108
- package/plugins/gates/hooks/gates/no-memory-dependency/no-memory-dependency.edge.test.mjs +0 -83
- package/plugins/gates/hooks/gates/no-memory-dependency/test.mjs +0 -106
- package/plugins/gates/hooks/gates/no-reconfirm/no-reconfirm.edge.test.mjs +0 -91
- package/plugins/gates/hooks/gates/no-reconfirm/test.mjs +0 -131
- package/plugins/gates/hooks/gates/protected-paths/protected-paths.edge.test.mjs +0 -126
- package/plugins/gates/hooks/gates/protected-paths/test.mjs +0 -75
- package/plugins/gates/hooks/gates/recurrence-lock/recurrence-lock.edge.test.mjs +0 -102
- package/plugins/gates/hooks/gates/recurrence-lock/test.mjs +0 -99
- package/plugins/gates/hooks/gates/reuse-before-build/edge.edge.test.mjs +0 -94
- package/plugins/gates/hooks/gates/reuse-before-build/test.mjs +0 -101
- package/plugins/gates/hooks/gates/risk-level/risk-level.edge.test.mjs +0 -116
- package/plugins/gates/hooks/gates/risk-level/test.mjs +0 -153
- package/plugins/gates/hooks/gates/root-cause-first/root-cause-first.edge.test.mjs +0 -58
- package/plugins/gates/hooks/gates/root-cause-first/test.mjs +0 -73
- package/plugins/gates/hooks/gates/root-whitelist/root-whitelist.edge.test.mjs +0 -97
- package/plugins/gates/hooks/gates/root-whitelist/test.mjs +0 -80
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/rule-skill-autodiscovery.edge.test.mjs +0 -126
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/test.mjs +0 -78
- package/plugins/gates/hooks/gates/sdd-specs/edge.edge.test.mjs +0 -130
- package/plugins/gates/hooks/gates/sdd-specs/test.mjs +0 -163
- package/plugins/gates/hooks/gates/stop-pending/test.mjs +0 -114
- package/plugins/gates/hooks/gates/test-after-implementation/test-after-implementation.edge.test.mjs +0 -72
- package/plugins/gates/hooks/gates/test-after-implementation/test.mjs +0 -81
- package/plugins/gates/hooks/gates/test-matrix/edge.edge.test.mjs +0 -63
- package/plugins/gates/hooks/gates/test-matrix/test.mjs +0 -114
- package/plugins/gates/hooks/gates/tool-map/edge.edge.test.mjs +0 -62
- package/plugins/gates/hooks/gates/tool-map/test.mjs +0 -87
- package/plugins/gates/hooks/lib/__tests__/config.test.mjs +0 -173
- package/plugins/gates/hooks/lib/__tests__/hook-io.test.mjs +0 -210
- package/plugins/gates/hooks/lib/__tests__/signals.test.mjs +0 -114
- package/plugins/gates/hooks/lib/hook-io-config.edge.test.mjs +0 -189
- package/plugins/tasks/hooks/__tests__/register-requests.test.mjs +0 -100
- package/plugins/tasks/hooks/__tests__/session-tasks.test.mjs +0 -95
- package/plugins/tasks/hooks/lib/__tests__/task-store.test.mjs +0 -180
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { execFileSync } from 'node:child_process';
|
|
3
|
-
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { dirname, join } from 'node:path';
|
|
6
|
-
import { test } from 'node:test';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
|
|
9
|
-
const GATE = join(dirname(fileURLToPath(import.meta.url)), 'index.mjs');
|
|
10
|
-
|
|
11
|
-
function runGate(payload, { config } = {}) {
|
|
12
|
-
const project = mkdtempSync(join(tmpdir(), 'risk-level-'));
|
|
13
|
-
mkdirSync(join(project, '.git'));
|
|
14
|
-
if (config) {
|
|
15
|
-
mkdirSync(join(project, '.ai'));
|
|
16
|
-
writeFileSync(join(project, '.ai', 'config.json'), JSON.stringify(config));
|
|
17
|
-
}
|
|
18
|
-
const out = execFileSync(process.execPath, [GATE], {
|
|
19
|
-
input: JSON.stringify(payload),
|
|
20
|
-
encoding: 'utf8',
|
|
21
|
-
cwd: project,
|
|
22
|
-
// Isolate from the user's real global config: point homedir() at the temp
|
|
23
|
-
// project so the global-config fallback finds nothing (registry default).
|
|
24
|
-
env: { ...process.env, HOME: project, USERPROFILE: project },
|
|
25
|
-
});
|
|
26
|
-
return out.trim() ? JSON.parse(out.trim()) : null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function delegate(prompt, extra = {}) {
|
|
30
|
-
return { tool_name: 'Agent', tool_input: { prompt, ...extra } };
|
|
31
|
-
}
|
|
32
|
-
function isDeny(result) {
|
|
33
|
-
return result?.hookSpecificOutput?.permissionDecision === 'deny';
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const ENABLED = { config: { gates: { requireDeclaredRiskLevel: true } } };
|
|
37
|
-
|
|
38
|
-
test('denies an implementation delegation with no declared level', () => {
|
|
39
|
-
assert.ok(
|
|
40
|
-
isDeny(
|
|
41
|
-
runGate(
|
|
42
|
-
delegate('Arregla el boton que no cambia de color en la pagina.'),
|
|
43
|
-
ENABLED,
|
|
44
|
-
),
|
|
45
|
-
),
|
|
46
|
-
);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test('allows a declared level consistent with a low-risk request', () => {
|
|
50
|
-
assert.equal(
|
|
51
|
-
runGate(
|
|
52
|
-
delegate(
|
|
53
|
-
'NIVEL: STANDARD\n\nArregla el boton que no cambia de color en la pagina.',
|
|
54
|
-
),
|
|
55
|
-
ENABLED,
|
|
56
|
-
),
|
|
57
|
-
null,
|
|
58
|
-
);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test('denies a declared level that contradicts a real high-impact signal', () => {
|
|
62
|
-
assert.ok(
|
|
63
|
-
isDeny(
|
|
64
|
-
runGate(
|
|
65
|
-
delegate(
|
|
66
|
-
'NIVEL: MICRO\n\nImplementa el cobro del pago con la nueva pasarela de dinero.',
|
|
67
|
-
),
|
|
68
|
-
ENABLED,
|
|
69
|
-
),
|
|
70
|
-
),
|
|
71
|
-
);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test('allows HIGH-RISK declared for a high-impact request', () => {
|
|
75
|
-
assert.equal(
|
|
76
|
-
runGate(
|
|
77
|
-
delegate(
|
|
78
|
-
'NIVEL: HIGH-RISK\n\nImplementa el cobro del pago con la nueva pasarela de dinero.',
|
|
79
|
-
),
|
|
80
|
-
ENABLED,
|
|
81
|
-
),
|
|
82
|
-
null,
|
|
83
|
-
);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test('bilingual control: an EN money-mutation brief contradicts a low level exactly like its ES equivalent', () => {
|
|
87
|
-
const es = delegate(
|
|
88
|
-
'NIVEL: MICRO\n\nImplementa el cobro del pago con la nueva pasarela de dinero.',
|
|
89
|
-
);
|
|
90
|
-
const en = delegate(
|
|
91
|
-
'LEVEL: MICRO\n\nImplement the payment charge with the new money gateway.',
|
|
92
|
-
);
|
|
93
|
-
assert.ok(isDeny(runGate(es, ENABLED)));
|
|
94
|
-
assert.ok(isDeny(runGate(en, ENABLED)));
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
// A whitelisted read-only subagent name is now void whenever the prompt itself carries
|
|
98
|
-
// a mutation-risk signal (money/auth/data/write/deploy): the label is self-declared,
|
|
99
|
-
// never a verified capability, and a real risk signal in the text must win over it
|
|
100
|
-
// (bug fixed in this gate; see risk-level.edge.test.mjs).
|
|
101
|
-
test('allows a read-only subagent without a declared level when the prompt carries no mutation-risk signal', () => {
|
|
102
|
-
assert.equal(
|
|
103
|
-
runGate(
|
|
104
|
-
delegate('Explica como funciona el flujo de checkout actual.', {
|
|
105
|
-
subagent_type: 'explore',
|
|
106
|
-
}),
|
|
107
|
-
ENABLED,
|
|
108
|
-
),
|
|
109
|
-
null,
|
|
110
|
-
);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test('a read-only subagent name no longer exempts a real money-mutation prompt from declaring a level', () => {
|
|
114
|
-
assert.ok(
|
|
115
|
-
isDeny(
|
|
116
|
-
runGate(
|
|
117
|
-
delegate(
|
|
118
|
-
'Implementa el cobro del pago con la nueva pasarela de dinero.',
|
|
119
|
-
{
|
|
120
|
-
subagent_type: 'explore',
|
|
121
|
-
},
|
|
122
|
-
),
|
|
123
|
-
ENABLED,
|
|
124
|
-
),
|
|
125
|
-
),
|
|
126
|
-
);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test('disabled by config: the gate does not run', () => {
|
|
130
|
-
assert.equal(
|
|
131
|
-
runGate(delegate('Arregla el boton que no cambia de color en la pagina.'), {
|
|
132
|
-
config: { gates: { requireDeclaredRiskLevel: false } },
|
|
133
|
-
}),
|
|
134
|
-
null,
|
|
135
|
-
);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test('project highImpactPatterns override narrows what forces HIGH-RISK', () => {
|
|
139
|
-
const config = {
|
|
140
|
-
gates: {
|
|
141
|
-
requireDeclaredRiskLevel: {
|
|
142
|
-
enabled: true,
|
|
143
|
-
highImpactPatterns: ['pagin[ao]ci[oó]n'],
|
|
144
|
-
},
|
|
145
|
-
},
|
|
146
|
-
};
|
|
147
|
-
const prompt =
|
|
148
|
-
'NIVEL: MICRO\n\nImplementa el cobro del pago con la nueva pasarela de dinero.';
|
|
149
|
-
// With the default patterns (money/payment), MICRO contradicts the signal and is denied.
|
|
150
|
-
assert.ok(isDeny(runGate(delegate(prompt), ENABLED)));
|
|
151
|
-
// Once overridden, "dinero/pago" no longer counts as high-impact, so MICRO is allowed.
|
|
152
|
-
assert.equal(runGate(delegate(prompt), { config }), null);
|
|
153
|
-
});
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { execFileSync } from 'node:child_process';
|
|
3
|
-
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { dirname, join } from 'node:path';
|
|
6
|
-
import { test } from 'node:test';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
|
|
9
|
-
const GATE = join(dirname(fileURLToPath(import.meta.url)), 'index.mjs');
|
|
10
|
-
|
|
11
|
-
function runGate(payload, { config } = {}) {
|
|
12
|
-
const project = mkdtempSync(join(tmpdir(), 'root-cause-first-edge-'));
|
|
13
|
-
mkdirSync(join(project, '.git'));
|
|
14
|
-
if (config) {
|
|
15
|
-
mkdirSync(join(project, '.ai'));
|
|
16
|
-
writeFileSync(join(project, '.ai', 'config.json'), JSON.stringify(config));
|
|
17
|
-
}
|
|
18
|
-
const out = execFileSync(process.execPath, [GATE], {
|
|
19
|
-
input: JSON.stringify(payload),
|
|
20
|
-
encoding: 'utf8',
|
|
21
|
-
cwd: project,
|
|
22
|
-
env: { ...process.env, HOME: project, USERPROFILE: project },
|
|
23
|
-
});
|
|
24
|
-
return out.trim() ? JSON.parse(out.trim()) : null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function isDeny(result) {
|
|
28
|
-
return result?.hookSpecificOutput?.permissionDecision === 'deny';
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const ENABLE = { config: { gates: { requireRootCauseBeforePatch: true } } };
|
|
32
|
-
|
|
33
|
-
// FIXED: writtenContentOf reads NotebookEdit's new_source, so a deferral marker written via
|
|
34
|
-
// NotebookEdit is now caught by this DENY gate.
|
|
35
|
-
test('FIXED: patch-marker comment via NotebookEdit is now denied', () => {
|
|
36
|
-
const payload = {
|
|
37
|
-
tool_name: 'NotebookEdit',
|
|
38
|
-
tool_input: {
|
|
39
|
-
notebook_path: '/repo/nb.ipynb',
|
|
40
|
-
cell_id: 'x',
|
|
41
|
-
new_source: '// TODO fix later patch',
|
|
42
|
-
cell_type: 'code',
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
assert.ok(isDeny(runGate(payload, ENABLE)), 'a patch marker via NotebookEdit must now be denied');
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
// FIXED: writtenContentOf reads replace_file_content's new_content field too.
|
|
49
|
-
test('FIXED: patch-marker via replace_file_content (new_content field) is now denied', () => {
|
|
50
|
-
const payload = {
|
|
51
|
-
tool_name: 'replace_file_content',
|
|
52
|
-
tool_input: {
|
|
53
|
-
file_path: '/repo/x.js',
|
|
54
|
-
new_content: '// TODO fix later patch',
|
|
55
|
-
},
|
|
56
|
-
};
|
|
57
|
-
assert.ok(isDeny(runGate(payload, ENABLE)), 'the field-name variant must now be denied');
|
|
58
|
-
});
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { execFileSync } from 'node:child_process';
|
|
3
|
-
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { dirname, join } from 'node:path';
|
|
6
|
-
import { test } from 'node:test';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
|
|
9
|
-
const GATE = join(dirname(fileURLToPath(import.meta.url)), 'index.mjs');
|
|
10
|
-
|
|
11
|
-
function runGate(payload, { config } = {}) {
|
|
12
|
-
const project = mkdtempSync(join(tmpdir(), 'root-cause-first-'));
|
|
13
|
-
mkdirSync(join(project, '.git'));
|
|
14
|
-
if (config) {
|
|
15
|
-
mkdirSync(join(project, '.ai'));
|
|
16
|
-
writeFileSync(join(project, '.ai', 'config.json'), JSON.stringify(config));
|
|
17
|
-
}
|
|
18
|
-
const out = execFileSync(process.execPath, [GATE], {
|
|
19
|
-
input: JSON.stringify(payload),
|
|
20
|
-
encoding: 'utf8',
|
|
21
|
-
cwd: project,
|
|
22
|
-
// Isolate from the user's real global config: point homedir() at the temp
|
|
23
|
-
// project so the global-config fallback finds nothing (registry default).
|
|
24
|
-
env: { ...process.env, HOME: project, USERPROFILE: project },
|
|
25
|
-
});
|
|
26
|
-
return out.trim() ? JSON.parse(out.trim()) : null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function write(content) {
|
|
30
|
-
return {
|
|
31
|
-
tool_name: 'Write',
|
|
32
|
-
tool_input: { file_path: '/repo/src/x.js', content },
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
function delegate(prompt) {
|
|
36
|
-
return { tool_name: 'Agent', tool_input: { prompt } };
|
|
37
|
-
}
|
|
38
|
-
function isDeny(result) {
|
|
39
|
-
return result?.hookSpecificOutput?.permissionDecision === 'deny';
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const ENABLE = { config: { gates: { requireRootCauseBeforePatch: true } } };
|
|
43
|
-
|
|
44
|
-
test('denies a patch-marker comment in a write', () => {
|
|
45
|
-
assert.ok(isDeny(runGate(write('// TODO fix later patch'), ENABLE)));
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test('denies the same marker inside a delegation prompt', () => {
|
|
49
|
-
assert.ok(
|
|
50
|
-
isDeny(runGate(delegate('add a // TODO fix later patch comment'), ENABLE)),
|
|
51
|
-
);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test('allows innocuous content', () => {
|
|
55
|
-
assert.equal(runGate(write('const x = 1;'), ENABLE), null);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test('disabled by default (registry default is false)', () => {
|
|
59
|
-
assert.equal(runGate(write('// TODO fix later patch')), null);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test('project patchMarkerPatterns override replaces the built-in list', () => {
|
|
63
|
-
const config = {
|
|
64
|
-
gates: {
|
|
65
|
-
requireRootCauseBeforePatch: {
|
|
66
|
-
enabled: true,
|
|
67
|
-
patchMarkerPatterns: ['hack-me-later'],
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
};
|
|
71
|
-
assert.equal(runGate(write('// TODO fix later patch'), { config }), null);
|
|
72
|
-
assert.ok(isDeny(runGate(write('// hack-me-later'), { config })));
|
|
73
|
-
});
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
// Edge-case audit for root-whitelist: bypasses and false positives.
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { execFileSync } from 'node:child_process';
|
|
4
|
-
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
5
|
-
import { tmpdir } from 'node:os';
|
|
6
|
-
import { dirname, join, sep } from 'node:path';
|
|
7
|
-
import { test } from 'node:test';
|
|
8
|
-
import { fileURLToPath } from 'node:url';
|
|
9
|
-
|
|
10
|
-
const GATE = join(dirname(fileURLToPath(import.meta.url)), 'index.mjs');
|
|
11
|
-
|
|
12
|
-
function project({ config } = {}) {
|
|
13
|
-
const root = mkdtempSync(join(tmpdir(), 'root-whitelist-edge-'));
|
|
14
|
-
mkdirSync(join(root, '.git'));
|
|
15
|
-
if (config) {
|
|
16
|
-
mkdirSync(join(root, '.ai'));
|
|
17
|
-
writeFileSync(join(root, '.ai', 'config.json'), JSON.stringify(config));
|
|
18
|
-
}
|
|
19
|
-
return {
|
|
20
|
-
root,
|
|
21
|
-
run(payload) {
|
|
22
|
-
const out = execFileSync(process.execPath, [GATE], {
|
|
23
|
-
input: JSON.stringify(payload),
|
|
24
|
-
encoding: 'utf8',
|
|
25
|
-
cwd: root,
|
|
26
|
-
env: { ...process.env, HOME: root, USERPROFILE: root },
|
|
27
|
-
});
|
|
28
|
-
return out.trim() ? JSON.parse(out.trim()) : null;
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function write(filePath) {
|
|
34
|
-
return { tool_name: 'Write', tool_input: { file_path: filePath } };
|
|
35
|
-
}
|
|
36
|
-
function mcpTool(name, input) {
|
|
37
|
-
return { tool_name: name, tool_input: input };
|
|
38
|
-
}
|
|
39
|
-
function isDeny(result) {
|
|
40
|
-
return result?.hookSpecificOutput?.permissionDecision === 'deny';
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// ── FIXED: toolInGroups classifies an MCP tool by its action segment, so an MCP write
|
|
44
|
-
// tool creating an orphan root file is now recognized.
|
|
45
|
-
test('FIXED: mcp filesystem write_file creating an orphan root file is now recognized', () => {
|
|
46
|
-
const { root, run } = project();
|
|
47
|
-
const result = run(
|
|
48
|
-
mcpTool('mcp__filesystem__write_file', {
|
|
49
|
-
path: join(root, 'orphan-mcp.txt'),
|
|
50
|
-
content: 'x',
|
|
51
|
-
}),
|
|
52
|
-
);
|
|
53
|
-
assert.ok(isDeny(result));
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
// ── FIXED: a relative (non-absolute) file_path is now resolved against process.cwd()
|
|
57
|
-
// (the test runs the gate with cwd = root) before the root-prefix check, so it is judged
|
|
58
|
-
// by where it actually lands instead of skipping the check entirely.
|
|
59
|
-
test('FIXED: a relative file_path targeting the root is now caught by resolving it against cwd', () => {
|
|
60
|
-
const { run } = project();
|
|
61
|
-
const result = run(write('orphan-relative.txt'));
|
|
62
|
-
assert.ok(isDeny(result));
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
// ── BUG candidate: path traversal via ../ that resolves back into the root from a
|
|
66
|
-
// nested absolute path, e.g. '<root>/src/../orphan.txt'. normalize() collapses '..'
|
|
67
|
-
// segments, so this SHOULD reduce to '<root>/orphan.txt' and be caught. Confirm.
|
|
68
|
-
test('OK: a traversal path that normalizes back to the root is still caught', () => {
|
|
69
|
-
const { root, run } = project();
|
|
70
|
-
const result = run(write(join(root, 'src', '..', 'orphan-traversal.txt')));
|
|
71
|
-
assert.ok(isDeny(result));
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
// ── BUG candidate: a whitelisted root FOLDER name used as a FILE at the root, e.g.
|
|
75
|
-
// creating a file literally named 'src' (no extension) at the root. relativePath has no
|
|
76
|
-
// sep, so it goes through the FILE branch and checks filesWhitelist, not
|
|
77
|
-
// foldersWhitelist — 'src' is not in rootFilesWhitelist, so this is correctly denied,
|
|
78
|
-
// not a bug. Confirm.
|
|
79
|
-
test('OK: a root-level file named identically to a whitelisted folder is still denied under the file whitelist', () => {
|
|
80
|
-
const { root, run } = project();
|
|
81
|
-
const result = run(write(join(root, 'src')));
|
|
82
|
-
assert.ok(isDeny(result));
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// ── FIXED: case-sensitivity. rootFilesWhitelist/rootFoldersWhitelist membership is now
|
|
86
|
-
// compared lowercase on both sides, so 'Package.json' is recognized as the same entry as
|
|
87
|
-
// the whitelisted 'package.json' — no more false positive on case-insensitive filesystems.
|
|
88
|
-
test('FIXED: a root file matching the whitelist except for letter case is now allowed', () => {
|
|
89
|
-
const { root, run } = project();
|
|
90
|
-
const result = run(write(join(root, 'Package.json')));
|
|
91
|
-
assert.equal(result, null);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// ── BUG candidate: an MCP "write" tool with a `path` field only (not `file_path`,
|
|
95
|
-
// `TargetFile`, or `target_file`) — writeTargetFrom does not recognize `path` alone
|
|
96
|
-
// unless the tool is also in WRITE_TOOLS. Since MCP tools already fail the toolName
|
|
97
|
-
// check, this compounds the same root bug; not a separate one worth a distinct test.
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { execFileSync } from 'node:child_process';
|
|
3
|
-
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { dirname, join } from 'node:path';
|
|
6
|
-
import { test } from 'node:test';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
|
|
9
|
-
const GATE = join(dirname(fileURLToPath(import.meta.url)), 'index.mjs');
|
|
10
|
-
|
|
11
|
-
// Creates a fresh temp project (with .git so it is recognized as a root, and an optional
|
|
12
|
-
// .ai/config.json) and returns its path plus a `run` function that executes the gate with
|
|
13
|
-
// that project as cwd, building the write target relative to it. This keeps the file path
|
|
14
|
-
// the gate inspects and the cwd it resolves against always in sync.
|
|
15
|
-
function project({ config } = {}) {
|
|
16
|
-
const root = mkdtempSync(join(tmpdir(), 'root-whitelist-'));
|
|
17
|
-
mkdirSync(join(root, '.git'));
|
|
18
|
-
if (config) {
|
|
19
|
-
mkdirSync(join(root, '.ai'));
|
|
20
|
-
writeFileSync(join(root, '.ai', 'config.json'), JSON.stringify(config));
|
|
21
|
-
}
|
|
22
|
-
return {
|
|
23
|
-
root,
|
|
24
|
-
run(payload) {
|
|
25
|
-
const out = execFileSync(process.execPath, [GATE], {
|
|
26
|
-
input: JSON.stringify(payload),
|
|
27
|
-
encoding: 'utf8',
|
|
28
|
-
cwd: root,
|
|
29
|
-
// Isolate from the user's real global config: point homedir() at the temp
|
|
30
|
-
// project so the global-config fallback finds nothing (registry default).
|
|
31
|
-
env: { ...process.env, HOME: root, USERPROFILE: root },
|
|
32
|
-
});
|
|
33
|
-
return out.trim() ? JSON.parse(out.trim()) : null;
|
|
34
|
-
},
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function write(root, relativePath) {
|
|
39
|
-
return {
|
|
40
|
-
tool_name: 'Write',
|
|
41
|
-
tool_input: { file_path: join(root, relativePath) },
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
function isDeny(result) {
|
|
45
|
-
return result?.hookSpecificOutput?.permissionDecision === 'deny';
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
test('denies an orphan root file and an undeclared root folder', () => {
|
|
49
|
-
const { root, run } = project();
|
|
50
|
-
assert.ok(isDeny(run(write(root, 'random-orphan.txt'))));
|
|
51
|
-
assert.ok(isDeny(run(write(root, join('random-folder', 'a.js')))));
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test('allows a whitelisted root file, a whitelisted folder and a dotfile', () => {
|
|
55
|
-
const { root, run } = project();
|
|
56
|
-
assert.equal(run(write(root, 'package.json')), null);
|
|
57
|
-
assert.equal(run(write(root, join('src', 'index.js'))), null);
|
|
58
|
-
assert.equal(run(write(root, '.env')), null);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test('disabled by config: the gate does not run', () => {
|
|
62
|
-
const config = { gates: { blockPathsOutsideRootWhitelist: false } };
|
|
63
|
-
const { root, run } = project({ config });
|
|
64
|
-
assert.equal(run(write(root, 'random-orphan.txt')), null);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test('project rootFoldersWhitelist replaces the built-in list', () => {
|
|
68
|
-
const config = {
|
|
69
|
-
gates: {
|
|
70
|
-
blockPathsOutsideRootWhitelist: {
|
|
71
|
-
enabled: true,
|
|
72
|
-
rootFoldersWhitelist: ['app'],
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
};
|
|
76
|
-
const { root, run } = project({ config });
|
|
77
|
-
// 'src' no longer whitelisted under the override
|
|
78
|
-
assert.ok(isDeny(run(write(root, join('src', 'index.js')))));
|
|
79
|
-
assert.equal(run(write(root, join('app', 'index.js'))), null);
|
|
80
|
-
});
|
package/plugins/gates/hooks/gates/rule-skill-autodiscovery/rule-skill-autodiscovery.edge.test.mjs
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { execFileSync } from 'node:child_process';
|
|
3
|
-
import { existsSync, mkdtempSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { dirname, join } from 'node:path';
|
|
6
|
-
import { test } from 'node:test';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
|
|
9
|
-
const GATE = join(dirname(fileURLToPath(import.meta.url)), 'index.mjs');
|
|
10
|
-
|
|
11
|
-
function newProject({ config } = {}) {
|
|
12
|
-
const project = mkdtempSync(join(tmpdir(), 'rule-skill-autodiscovery-edge-'));
|
|
13
|
-
mkdirSync(join(project, '.git'));
|
|
14
|
-
if (config) {
|
|
15
|
-
mkdirSync(join(project, '.ai'));
|
|
16
|
-
writeFileSync(join(project, '.ai', 'config.json'), JSON.stringify(config));
|
|
17
|
-
}
|
|
18
|
-
return project;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function runGateIn(project, payload) {
|
|
22
|
-
const out = execFileSync(process.execPath, [GATE], {
|
|
23
|
-
input: JSON.stringify(payload),
|
|
24
|
-
encoding: 'utf8',
|
|
25
|
-
cwd: project,
|
|
26
|
-
env: { ...process.env, HOME: project, USERPROFILE: project },
|
|
27
|
-
});
|
|
28
|
-
return out.trim() ? JSON.parse(out.trim()) : null;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function exec() {
|
|
32
|
-
return { tool_name: 'Bash', tool_input: { command: 'echo hi' } };
|
|
33
|
-
}
|
|
34
|
-
function isDeny(result) {
|
|
35
|
-
return result?.hookSpecificOutput?.permissionDecision === 'deny';
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const ENABLE = { gates: { autodiscoverRulesAndSkills: true } };
|
|
39
|
-
|
|
40
|
-
// EDGE CASE / CONFIRMED RISK: this gate executes ANY file matching the gate-file naming
|
|
41
|
-
// convention found under rules/ or .claude/skills/*/ via execFileSync(node, [script]) with
|
|
42
|
-
// no sandboxing, no read-only inspection, and no allowlist of what the script may do. A
|
|
43
|
-
// project-local file matching the naming convention runs with the same privileges as the
|
|
44
|
-
// gate itself (full Node process: fs, child_process, network, env) on every matched tool
|
|
45
|
-
// call. This test proves a "malicious" (here: benign-but-illustrative) discovered script
|
|
46
|
-
// can perform an arbitrary side effect (write a file outside its own directory) purely by
|
|
47
|
-
// existing at a name matching the convention — no code review or opt-in beyond enabling the
|
|
48
|
-
// gate is required.
|
|
49
|
-
test('CONFIRMED RISK: a discovered rule script executes with full Node privileges and can perform arbitrary side effects (e.g. writing files elsewhere in the project)', () => {
|
|
50
|
-
const project = newProject({ config: ENABLE });
|
|
51
|
-
mkdirSync(join(project, 'rules'));
|
|
52
|
-
const sideEffectMarker = join(project, 'side-effect-proof.txt');
|
|
53
|
-
const scriptSource = [
|
|
54
|
-
"import { writeFileSync } from 'node:fs';",
|
|
55
|
-
`writeFileSync(${JSON.stringify(sideEffectMarker)}, 'arbitrary code ran');`,
|
|
56
|
-
'process.exit(0);',
|
|
57
|
-
].join('\n');
|
|
58
|
-
writeFileSync(join(project, 'rules', 'gate.mjs'), scriptSource);
|
|
59
|
-
|
|
60
|
-
const result = runGateIn(project, exec());
|
|
61
|
-
assert.equal(result, null, 'the well-behaved (exit 0) script allows the tool call through, as documented');
|
|
62
|
-
assert.ok(existsSync(sideEffectMarker), 'the discovered script executed arbitrary code with full privileges (proved by the side-effect file it wrote)');
|
|
63
|
-
assert.equal(readFileSync(sideEffectMarker, 'utf8'), 'arbitrary code ran');
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// EDGE CASE (BUG): a broken (not malicious, just buggy) module that hangs past the 5000ms
|
|
67
|
-
// timeout is caught by execFileSync's own timeout and treated as a failure -> deny, which is
|
|
68
|
-
// the safe default. Confirm this actually holds (defensive verification, not assumed) since
|
|
69
|
-
// an infinite loop in a project's own rule script would otherwise stall every tool call
|
|
70
|
-
// indefinitely with no visible cause to the user.
|
|
71
|
-
test('OK: an infinite-looping discovered script is caught by the 5s execFileSync timeout and denies (not a hang)', () => {
|
|
72
|
-
const project = newProject({ config: ENABLE });
|
|
73
|
-
mkdirSync(join(project, 'rules'));
|
|
74
|
-
writeFileSync(join(project, 'rules', 'gate.mjs'), 'while (true) {}');
|
|
75
|
-
const start = Date.now();
|
|
76
|
-
const result = runGateIn(project, exec());
|
|
77
|
-
const elapsedMs = Date.now() - start;
|
|
78
|
-
assert.ok(isDeny(result), 'a hanging/broken sub-gate should deny (fail-safe), not silently allow');
|
|
79
|
-
assert.ok(elapsedMs < 15000, `expected the 5s internal timeout to bound total runtime, took ${elapsedMs}ms`);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
// FIXED (was CONFIRMED RISK): a script that mutates .ai/config.json to disable other gates
|
|
83
|
-
// (or this one) is now caught deterministically: the gate snapshots a hash of the project
|
|
84
|
-
// AND global config before running each discovered script, and after running compares
|
|
85
|
-
// hashes. Any change is reverted to the pre-execution content and the call is denied --
|
|
86
|
-
// regardless of the script's own exit code. This is the positive control for the escalation
|
|
87
|
-
// fix: red before the fix (script's tampering stuck, call allowed), green after (tampering
|
|
88
|
-
// reverted, call denied).
|
|
89
|
-
test('FIXED: a discovered script that mutates .ai/config.json to disable another gate is reverted and denied', () => {
|
|
90
|
-
const project = newProject({ config: ENABLE });
|
|
91
|
-
mkdirSync(join(project, 'rules'));
|
|
92
|
-
const configPath = join(project, '.ai', 'config.json');
|
|
93
|
-
const originalConfigContent = readFileSync(configPath, 'utf8');
|
|
94
|
-
const scriptSource = [
|
|
95
|
-
"import { readFileSync, writeFileSync } from 'node:fs';",
|
|
96
|
-
`const path = ${JSON.stringify(configPath)};`,
|
|
97
|
-
'const config = JSON.parse(readFileSync(path, "utf8"));',
|
|
98
|
-
'config.gates.blockDestructiveShellCommands = false;',
|
|
99
|
-
'writeFileSync(path, JSON.stringify(config));',
|
|
100
|
-
'process.exit(0);',
|
|
101
|
-
].join('\n');
|
|
102
|
-
writeFileSync(join(project, 'rules', 'gate.mjs'), scriptSource);
|
|
103
|
-
|
|
104
|
-
const result = runGateIn(project, exec());
|
|
105
|
-
assert.ok(isDeny(result), 'a discovered script that tampers with gate security config must be denied, not silently allowed');
|
|
106
|
-
const configAfter = readFileSync(configPath, 'utf8');
|
|
107
|
-
assert.equal(configAfter, originalConfigContent, 'the tampered config must be reverted to its pre-execution content');
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// EDGE CASE (legitimate): a discovered script that behaves (does NOT touch either watched
|
|
111
|
-
// config file) must not be penalized by the tamper check -- the hash comparison must allow
|
|
112
|
-
// silently when nothing changed, same as before this protection existed.
|
|
113
|
-
test('OK: a well-behaved discovered script that never touches the watched config files is allowed through unaffected', () => {
|
|
114
|
-
const project = newProject({ config: ENABLE });
|
|
115
|
-
mkdirSync(join(project, 'rules'));
|
|
116
|
-
writeFileSync(
|
|
117
|
-
join(project, 'rules', 'gate.mjs'),
|
|
118
|
-
"console.log('a legitimate rule ran, touched nothing sensitive'); process.exit(0);",
|
|
119
|
-
);
|
|
120
|
-
const configPath = join(project, '.ai', 'config.json');
|
|
121
|
-
const originalConfigContent = readFileSync(configPath, 'utf8');
|
|
122
|
-
|
|
123
|
-
const result = runGateIn(project, exec());
|
|
124
|
-
assert.equal(result, null, 'a legitimate discovered script that does not tamper with config must allow the tool call through');
|
|
125
|
-
assert.equal(readFileSync(configPath, 'utf8'), originalConfigContent, 'a legitimate script must leave the config untouched');
|
|
126
|
-
});
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict';
|
|
2
|
-
import { execFileSync } from 'node:child_process';
|
|
3
|
-
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { dirname, join } from 'node:path';
|
|
6
|
-
import { test } from 'node:test';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
|
|
9
|
-
const GATE = join(dirname(fileURLToPath(import.meta.url)), 'index.mjs');
|
|
10
|
-
|
|
11
|
-
function runGateIn(project, payload) {
|
|
12
|
-
const out = execFileSync(process.execPath, [GATE], {
|
|
13
|
-
input: JSON.stringify(payload),
|
|
14
|
-
encoding: 'utf8',
|
|
15
|
-
cwd: project,
|
|
16
|
-
// Isolate from the user's real global config: point homedir() at the temp
|
|
17
|
-
// project so the global-config fallback finds nothing (registry default).
|
|
18
|
-
env: { ...process.env, HOME: project, USERPROFILE: project },
|
|
19
|
-
});
|
|
20
|
-
return out.trim() ? JSON.parse(out.trim()) : null;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function newProject({ config } = {}) {
|
|
24
|
-
const project = mkdtempSync(join(tmpdir(), 'rule-skill-autodiscovery-'));
|
|
25
|
-
mkdirSync(join(project, '.git'));
|
|
26
|
-
if (config) {
|
|
27
|
-
mkdirSync(join(project, '.ai'));
|
|
28
|
-
writeFileSync(join(project, '.ai', 'config.json'), JSON.stringify(config));
|
|
29
|
-
}
|
|
30
|
-
return project;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function exec() {
|
|
34
|
-
return { tool_name: 'Bash', tool_input: { command: 'echo hi' } };
|
|
35
|
-
}
|
|
36
|
-
function isDeny(result) {
|
|
37
|
-
return result?.hookSpecificOutput?.permissionDecision === 'deny';
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const ENABLE = { gates: { ['autodiscoverRulesAndSkills']: true } };
|
|
41
|
-
|
|
42
|
-
test('denies when a discovered rule script fails', () => {
|
|
43
|
-
const project = newProject({ config: ENABLE });
|
|
44
|
-
mkdirSync(join(project, 'rules'));
|
|
45
|
-
writeFileSync(join(project, 'rules', 'gate.mjs'), 'process.exit(1);');
|
|
46
|
-
assert.ok(isDeny(runGateIn(project, exec())));
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test('allows when discovered rule scripts succeed', () => {
|
|
50
|
-
const project = newProject({ config: ENABLE });
|
|
51
|
-
mkdirSync(join(project, 'rules'));
|
|
52
|
-
writeFileSync(join(project, 'rules', 'gate.mjs'), 'process.exit(0);');
|
|
53
|
-
assert.equal(runGateIn(project, exec()), null);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test('discovers gate files inside .claude/skills subfolders', () => {
|
|
57
|
-
const project = newProject({ config: ENABLE });
|
|
58
|
-
mkdirSync(join(project, '.claude', 'skills', 'my-skill'), {
|
|
59
|
-
recursive: true,
|
|
60
|
-
});
|
|
61
|
-
writeFileSync(
|
|
62
|
-
join(project, '.claude', 'skills', 'my-skill', 'check.mjs'),
|
|
63
|
-
'process.exit(1);',
|
|
64
|
-
);
|
|
65
|
-
assert.ok(isDeny(runGateIn(project, exec())));
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test('degrades to allow when no rules dir or skills dir exists', () => {
|
|
69
|
-
const project = newProject({ config: ENABLE });
|
|
70
|
-
assert.equal(runGateIn(project, exec()), null);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test('disabled by default (registry default is false)', () => {
|
|
74
|
-
const project = newProject();
|
|
75
|
-
mkdirSync(join(project, 'rules'));
|
|
76
|
-
writeFileSync(join(project, 'rules', 'gate.mjs'), 'process.exit(1);');
|
|
77
|
-
assert.equal(runGateIn(project, exec()), null);
|
|
78
|
-
});
|