@devrik-tools/claude-gates 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +7 -1
- package/cli/__tests__/config.test.mjs +113 -101
- package/cli/__tests__/install.test.mjs +102 -0
- package/cli/__tests__/materialize.test.mjs +95 -0
- package/cli/__tests__/registry-gates-consistency.test.mjs +52 -44
- package/cli/__tests__/task.test.mjs +124 -0
- package/cli/__tests__/version-consistency.test.mjs +44 -0
- package/cli/config.mjs +124 -113
- package/cli/index.mjs +100 -97
- package/cli/init.mjs +259 -256
- package/cli/install.mjs +150 -80
- package/cli/materialize.mjs +102 -51
- package/cli/registry.mjs +136 -127
- package/cli/task.mjs +140 -0
- package/package.json +2 -1
- package/plugins/gates/.claude-plugin/plugin.json +8 -2
- package/plugins/gates/hooks/__tests__/ask-adoption.test.mjs +83 -0
- package/plugins/gates/hooks/__tests__/doctor.test.mjs +85 -0
- package/plugins/gates/hooks/__tests__/wiring-check.test.mjs +65 -0
- package/plugins/gates/hooks/ask-adoption.mjs +147 -0
- package/plugins/gates/hooks/doctor.mjs +207 -0
- package/plugins/gates/hooks/gates/audit-before-build/audit-before-build.edge.test.mjs +83 -0
- package/plugins/gates/hooks/gates/audit-before-build/index.mjs +88 -88
- package/plugins/gates/hooks/gates/audit-before-build/test.mjs +117 -92
- package/plugins/gates/hooks/gates/autonomous-mode/autonomous.edge.test.mjs +104 -0
- package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +48 -45
- package/plugins/gates/hooks/gates/bash-commands/bash-commands.edge.test.mjs +165 -0
- package/plugins/gates/hooks/gates/bash-commands/index.mjs +285 -268
- package/plugins/gates/hooks/gates/brief-before-delegate/brief-before-delegate.edge.test.mjs +151 -0
- package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +265 -177
- package/plugins/gates/hooks/gates/circuit-breaker/circuit-breaker.edge.test.mjs +207 -0
- package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +501 -456
- package/plugins/gates/hooks/gates/circuit-breaker/test.mjs +161 -143
- package/plugins/gates/hooks/gates/dependency-skills/dependency-skills.edge.test.mjs +69 -0
- package/plugins/gates/hooks/gates/dependency-skills/index.mjs +126 -118
- package/plugins/gates/hooks/gates/diagnosis-before-patch/diagnosis-before-patch.edge.test.mjs +68 -0
- package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +43 -49
- package/plugins/gates/hooks/gates/feature-catalog/edge.edge.test.mjs +83 -0
- package/plugins/gates/hooks/gates/feature-catalog/index.mjs +83 -100
- package/plugins/gates/hooks/gates/force-parallel/index.mjs +119 -0
- package/plugins/gates/hooks/gates/force-parallel/test.mjs +88 -0
- package/plugins/gates/hooks/gates/forge-flow/forge-flow.edge.test.mjs +155 -0
- package/plugins/gates/hooks/gates/forge-flow/index.mjs +134 -112
- package/plugins/gates/hooks/gates/implementation-pipeline/edge.edge.test.mjs +66 -0
- package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +187 -186
- package/plugins/gates/hooks/gates/intent-flow/index.mjs +260 -238
- package/plugins/gates/hooks/gates/intent-flow/intent-flow.edge.test.mjs +96 -0
- package/plugins/gates/hooks/gates/intent-flow/test.mjs +165 -136
- package/plugins/gates/hooks/gates/lint-commit/index.mjs +149 -0
- package/plugins/gates/hooks/gates/lint-commit/test.mjs +85 -0
- package/plugins/gates/hooks/gates/mandatory-flow/edge.edge.test.mjs +100 -0
- package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +180 -166
- package/plugins/gates/hooks/gates/neutral-spanish/index.mjs +79 -85
- package/plugins/gates/hooks/gates/neutral-spanish/neutral-spanish.edge.test.mjs +80 -0
- package/plugins/gates/hooks/gates/never-assume/index.mjs +58 -55
- package/plugins/gates/hooks/gates/never-assume/never-assume.edge.test.mjs +71 -0
- package/plugins/gates/hooks/gates/never-assume/test.mjs +95 -78
- package/plugins/gates/hooks/gates/no-blocking/index.mjs +148 -142
- package/plugins/gates/hooks/gates/no-blocking/no-blocking.edge.test.mjs +109 -0
- package/plugins/gates/hooks/gates/no-memory-dependency/index.mjs +123 -120
- package/plugins/gates/hooks/gates/no-memory-dependency/no-memory-dependency.edge.test.mjs +83 -0
- package/plugins/gates/hooks/gates/no-reconfirm/no-reconfirm.edge.test.mjs +91 -0
- package/plugins/gates/hooks/gates/protected-paths/index.mjs +144 -147
- package/plugins/gates/hooks/gates/protected-paths/protected-paths.edge.test.mjs +126 -0
- package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +89 -64
- package/plugins/gates/hooks/gates/recurrence-lock/recurrence-lock.edge.test.mjs +102 -0
- package/plugins/gates/hooks/gates/reuse-before-build/edge.edge.test.mjs +94 -0
- package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +159 -150
- package/plugins/gates/hooks/gates/risk-level/index.mjs +263 -203
- package/plugins/gates/hooks/gates/risk-level/risk-level.edge.test.mjs +116 -0
- package/plugins/gates/hooks/gates/risk-level/test.mjs +153 -125
- package/plugins/gates/hooks/gates/root-cause-first/index.mjs +56 -50
- package/plugins/gates/hooks/gates/root-cause-first/root-cause-first.edge.test.mjs +58 -0
- package/plugins/gates/hooks/gates/root-whitelist/index.mjs +116 -111
- package/plugins/gates/hooks/gates/root-whitelist/root-whitelist.edge.test.mjs +97 -0
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +184 -99
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/rule-skill-autodiscovery.edge.test.mjs +126 -0
- package/plugins/gates/hooks/gates/sdd-specs/edge.edge.test.mjs +130 -0
- package/plugins/gates/hooks/gates/sdd-specs/index.mjs +256 -251
- package/plugins/gates/hooks/gates/stop-pending/index.mjs +159 -0
- package/plugins/gates/hooks/gates/stop-pending/test.mjs +114 -0
- package/plugins/gates/hooks/gates/test-after-implementation/index.mjs +101 -101
- package/plugins/gates/hooks/gates/test-after-implementation/test-after-implementation.edge.test.mjs +72 -0
- package/plugins/gates/hooks/gates/test-matrix/edge.edge.test.mjs +63 -0
- package/plugins/gates/hooks/gates/test-matrix/index.mjs +187 -181
- package/plugins/gates/hooks/gates/test-matrix/test.mjs +114 -87
- package/plugins/gates/hooks/gates/tool-map/edge.edge.test.mjs +62 -0
- package/plugins/gates/hooks/gates/tool-map/index.mjs +143 -140
- package/plugins/gates/hooks/hooks.json +346 -286
- package/plugins/gates/hooks/lib/__tests__/config.test.mjs +173 -154
- package/plugins/gates/hooks/lib/__tests__/hook-io.test.mjs +210 -154
- package/plugins/gates/hooks/lib/__tests__/signals.test.mjs +114 -0
- package/plugins/gates/hooks/lib/config.mjs +172 -165
- package/plugins/gates/hooks/lib/hook-io-config.edge.test.mjs +189 -0
- package/plugins/gates/hooks/lib/hook-io.mjs +318 -208
- package/plugins/gates/hooks/lib/signals.mjs +127 -0
- package/plugins/gates/hooks/wiring-check.mjs +227 -0
- package/plugins/tasks/.claude-plugin/plugin.json +14 -0
- package/plugins/tasks/hooks/__tests__/register-requests.test.mjs +100 -0
- package/plugins/tasks/hooks/__tests__/session-tasks.test.mjs +95 -0
- package/plugins/tasks/hooks/hooks.json +26 -0
- package/plugins/tasks/hooks/lib/__tests__/task-store.test.mjs +180 -132
- package/plugins/tasks/hooks/lib/task-store.mjs +197 -159
- package/plugins/tasks/hooks/register-requests.mjs +141 -108
- package/plugins/tasks/hooks/session-tasks.mjs +108 -0
- package/registry.json +793 -686
package/plugins/gates/hooks/gates/rule-skill-autodiscovery/rule-skill-autodiscovery.edge.test.mjs
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Edge-case probes for sdd-specs. Previously: (1) an Edit (new_string) bypassed the
|
|
2
|
+
// content check the same way as feature-catalog; (2) malformed JSON content failed
|
|
3
|
+
// open (parseJsonOrNull -> null -> features=[] -> nothing to check); (3) the catalog
|
|
4
|
+
// lookup was relative to process.cwd() only, so a monorepo subpackage's own
|
|
5
|
+
// .ai/feature_list.json living outside cwd was invisible to findCatalog. All three are
|
|
6
|
+
// now fixed: writtenContentOf()/writtenPathOf() read every field shape, invalid JSON on
|
|
7
|
+
// a catalog write now denies (fail-closed) instead of silently allowing, and
|
|
8
|
+
// findCatalog also resolves catalogLocations relative to the written file's own
|
|
9
|
+
// directory.
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import { execFileSync } from 'node:child_process';
|
|
12
|
+
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { tmpdir } from 'node:os';
|
|
14
|
+
import { dirname, join } from 'node:path';
|
|
15
|
+
import { test } from 'node:test';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
const GATE = join(dirname(fileURLToPath(import.meta.url)), 'index.mjs');
|
|
19
|
+
|
|
20
|
+
function makeProject() {
|
|
21
|
+
const project = mkdtempSync(join(tmpdir(), 'sdd-specs-edge-'));
|
|
22
|
+
mkdirSync(join(project, '.git'));
|
|
23
|
+
return project;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function writeProjectConfig(project, config) {
|
|
27
|
+
mkdirSync(join(project, '.ai'), { recursive: true });
|
|
28
|
+
writeFileSync(join(project, '.ai', 'config.json'), JSON.stringify(config));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function runGate(project, payload) {
|
|
32
|
+
const out = execFileSync(process.execPath, [GATE], {
|
|
33
|
+
input: JSON.stringify(payload),
|
|
34
|
+
encoding: 'utf8',
|
|
35
|
+
cwd: project,
|
|
36
|
+
env: { ...process.env, HOME: project, USERPROFILE: project },
|
|
37
|
+
});
|
|
38
|
+
return out.trim() ? JSON.parse(out.trim()) : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isDeny(result) {
|
|
42
|
+
return result?.hookSpecificOutput?.permissionDecision === 'deny';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function enableGate(project) {
|
|
46
|
+
writeProjectConfig(project, { gates: { requireSpecBeforeImplementing: true } });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
test('FIXED: an Edit (new_string) moving a feature to spec_ready with no contract is now detected', () => {
|
|
50
|
+
const project = makeProject();
|
|
51
|
+
enableGate(project);
|
|
52
|
+
writeFileSync(join(project, '.ai', 'feature_list.json'), JSON.stringify({ features: [] }));
|
|
53
|
+
mkdirSync(join(project, '.ai', 'features', 'checkout'), { recursive: true });
|
|
54
|
+
const payload = {
|
|
55
|
+
tool_name: 'Edit',
|
|
56
|
+
tool_input: {
|
|
57
|
+
file_path: join(project, '.ai', 'feature_list.json'),
|
|
58
|
+
old_string: 'x',
|
|
59
|
+
new_string: JSON.stringify({
|
|
60
|
+
features: [{ name: 'checkout', status: 'spec_ready' }],
|
|
61
|
+
}),
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
assert.ok(
|
|
65
|
+
isDeny(runGate(project, payload)),
|
|
66
|
+
'writtenContentOf reads new_string and catches the spec_ready-with-no-contract write',
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('FIXED: malformed/invalid JSON content on a catalog write now denies (fail-closed)', () => {
|
|
71
|
+
const project = makeProject();
|
|
72
|
+
enableGate(project);
|
|
73
|
+
writeFileSync(join(project, '.ai', 'feature_list.json'), JSON.stringify({ features: [] }));
|
|
74
|
+
mkdirSync(join(project, '.ai', 'features', 'checkout'), { recursive: true });
|
|
75
|
+
const payload = {
|
|
76
|
+
tool_name: 'Write',
|
|
77
|
+
tool_input: {
|
|
78
|
+
file_path: join(project, '.ai', 'feature_list.json'),
|
|
79
|
+
// deliberately invalid JSON (unquoted keys): the gate can no longer verify the
|
|
80
|
+
// spec-contract invariant for this write, so it denies instead of allowing.
|
|
81
|
+
content:
|
|
82
|
+
'{features: [{name: "checkout", status: "spec_ready"}]}',
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
assert.ok(
|
|
86
|
+
isDeny(runGate(project, payload)),
|
|
87
|
+
'malformed JSON targeting the catalog must deny, not fail open',
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('FIXED: a catalog living outside process.cwd() is now found via the written path', () => {
|
|
92
|
+
const project = makeProject();
|
|
93
|
+
enableGate(project);
|
|
94
|
+
// The catalog lives under sub/.ai/feature_list.json, not the project root's .ai/ --
|
|
95
|
+
// findCatalog now also resolves catalogLocations relative to dirname(writtenPath).
|
|
96
|
+
mkdirSync(join(project, 'sub', '.ai', 'features', 'checkout'), { recursive: true });
|
|
97
|
+
writeFileSync(
|
|
98
|
+
join(project, 'sub', '.ai', 'feature_list.json'),
|
|
99
|
+
JSON.stringify({ features: [{ name: 'checkout', status: 'spec_ready' }] }),
|
|
100
|
+
);
|
|
101
|
+
const payload = {
|
|
102
|
+
tool_name: 'Write',
|
|
103
|
+
tool_input: {
|
|
104
|
+
file_path: join(project, 'sub', '.ai', 'feature_list.json'),
|
|
105
|
+
content: JSON.stringify({
|
|
106
|
+
features: [{ name: 'checkout', status: 'spec_ready' }],
|
|
107
|
+
}),
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
assert.ok(
|
|
111
|
+
isDeny(runGate(project, payload)),
|
|
112
|
+
'a subpackage catalog reached via its own written path must now be inspected',
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('OK: the equivalent Write-tool payload at the project root IS caught (control)', () => {
|
|
117
|
+
const project = makeProject();
|
|
118
|
+
enableGate(project);
|
|
119
|
+
writeFileSync(join(project, '.ai', 'feature_list.json'), JSON.stringify({ features: [] }));
|
|
120
|
+
const payload = {
|
|
121
|
+
tool_name: 'Write',
|
|
122
|
+
tool_input: {
|
|
123
|
+
file_path: join(project, '.ai', 'feature_list.json'),
|
|
124
|
+
content: JSON.stringify({
|
|
125
|
+
features: [{ name: 'checkout', status: 'spec_ready' }],
|
|
126
|
+
}),
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
assert.ok(isDeny(runGate(project, payload)), 'control failed');
|
|
130
|
+
});
|