@haposoft/cafekit 0.18.3 → 0.18.4
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/README.md +1 -1
- package/bin/__tests__/adv-skill.test.js +1 -2
- package/bin/__tests__/claude-plugin-build.test.js +2 -2
- package/bin/__tests__/codex-hook-runtime-fixture.cjs +26 -0
- package/bin/__tests__/codex-hooks-ownership.test.js +8 -11
- package/bin/__tests__/codex-hooks.test.js +49 -79
- package/bin/__tests__/codex-implicit-policy.test.js +2 -2
- package/bin/__tests__/codex-native-hook-boundaries.test.js +2 -4
- package/bin/__tests__/codex-native.test.js +125 -2294
- package/bin/__tests__/codex-quiet-hooks.test.js +2 -8
- package/bin/__tests__/codex-skill-projections.test.js +101 -0
- package/bin/__tests__/develop-contract.test.js +100 -874
- package/bin/__tests__/orca-session.test.js +2 -16
- package/bin/__tests__/package-inventory.test.js +228 -2044
- package/bin/__tests__/secret-scanner.test.js +73 -0
- package/bin/__tests__/skill-routing-source.test.js +6 -0
- package/bin/__tests__/toaa-skill.test.js +195 -0
- package/bin/__tests__/validator-grounding.test.js +0 -198
- package/bin/__tests__/vibe-skill.test.js +1 -2
- package/bin/__tests__/website-docs.test.js +58 -0
- package/bin/lib/codex-install.js +5 -3
- package/package.json +1 -1
- package/src/claude/hooks/__tests__/privacy-block.test.js +0 -26
- package/src/claude/hooks/__tests__/spec-gate.test.js +0 -47
- package/src/claude/hooks/__tests__/state.test.js +21 -0
- package/src/claude/hooks/state.cjs +22 -0
- package/src/claude/migration-manifest.json +1 -0
- package/src/claude/scripts/scan-staged-secrets.cjs +49 -3
- package/src/claude/skills/develop/SKILL.md +6 -0
- package/src/claude/skills/sync/SKILL.md +4 -0
- package/src/claude/skills/toaa/SKILL.md +82 -0
- package/src/claude/skills/toaa/references/autonomous-orchestration.md +54 -0
- package/src/claude/skills/toaa/references/prerequisites-checklist.md +37 -0
- package/src/codex/hooks/lib/hook-context.cjs +1 -0
- package/src/codex/hooks/lib/state-store.cjs +27 -3
- package/bin/__tests__/codex-research-projection.test.js +0 -70
- package/bin/__tests__/codex-specs-projection.test.js +0 -76
- package/bin/__tests__/specs-v2-execution-closeout.test.js +0 -58
- package/src/claude/skills/chrome-devtools/scripts/__tests__/error-handling.test.js +0 -102
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Native spec-driven workflow and runtime bundle for Claude Code and Codex CLI.
|
|
4
4
|
|
|
5
|
-
[](https://github.com/haposoft/cafekit)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://claude.ai/code)
|
|
8
8
|
[](https://developers.openai.com/codex)
|
|
@@ -75,11 +75,10 @@ function assertLinksResolve(baseDir, file) {
|
|
|
75
75
|
return links.length;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
test('manifest requires adv
|
|
78
|
+
test('manifest requires adv', () => {
|
|
79
79
|
// Without the entry the installer never copies the skill and nothing else fails.
|
|
80
80
|
const required = loadClaudeMigrationManifest().skills.required;
|
|
81
81
|
assert.equal(required.includes('adv'), true);
|
|
82
|
-
assert.deepEqual([...required], [...required].sort());
|
|
83
82
|
});
|
|
84
83
|
|
|
85
84
|
test('frontmatter carries the catalog identity, manual-only invocation, and a routable description', () => {
|
|
@@ -29,10 +29,10 @@ before(async () => {
|
|
|
29
29
|
|
|
30
30
|
after(() => fs.rmSync(work, { recursive: true, force: true }));
|
|
31
31
|
|
|
32
|
-
test('inventory:
|
|
32
|
+
test('inventory: 18 in-scope skills, 14 agents, no excluded component', () => {
|
|
33
33
|
const skills = fs.readdirSync(path.join(tree, 'skills')).sort();
|
|
34
34
|
const agents = fs.readdirSync(path.join(tree, 'agents')).map((f) => f.replace(/\.md$/, '')).sort();
|
|
35
|
-
assert.equal(skills.length,
|
|
35
|
+
assert.equal(skills.length, 18);
|
|
36
36
|
assert.equal(agents.length, 14);
|
|
37
37
|
for (const name of lib.EXCLUDED_SKILLS) assert.ok(!skills.includes(name), `excluded skill shipped: ${name}`);
|
|
38
38
|
for (const name of lib.EXCLUDED_AGENTS) assert.ok(!agents.includes(name), `excluded agent shipped: ${name}`);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { codexRuntimeLayout } = require('../phases/codex-runtime');
|
|
6
|
+
|
|
7
|
+
// Groups the installer materializes under .codex that Codex hooks load at run time.
|
|
8
|
+
const HOOK_RUNTIME_GROUPS = new Set(['hooks', 'schema', 'shared-lib']);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Lay the Codex hook runtime into `<root>/.codex` exactly as the installer does,
|
|
12
|
+
* driven by the canonical layout so a new hook or shared library cannot be missed.
|
|
13
|
+
* Returns the hooks directory.
|
|
14
|
+
*/
|
|
15
|
+
function installCodexHookRuntime(root) {
|
|
16
|
+
const runtimeRoot = path.join(root, '.codex');
|
|
17
|
+
for (const entry of codexRuntimeLayout()) {
|
|
18
|
+
if (!HOOK_RUNTIME_GROUPS.has(entry.group)) continue;
|
|
19
|
+
const target = path.join(runtimeRoot, entry.dest);
|
|
20
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
21
|
+
fs.copyFileSync(entry.src, target);
|
|
22
|
+
}
|
|
23
|
+
return path.join(runtimeRoot, 'hooks');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { installCodexHookRuntime };
|
|
@@ -79,17 +79,6 @@ function addUserHooks(root) {
|
|
|
79
79
|
fs.writeFileSync(hooksPath(root), `${JSON.stringify(config, null, 2)}\n`);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
test('a fresh install registers the precompact capture hook', () => {
|
|
83
|
-
inTempProject((root) => {
|
|
84
|
-
assert.ok(managedScripts().PreCompact?.includes('precompact.cjs'));
|
|
85
|
-
const result = install(root);
|
|
86
|
-
assert.equal(result.status, 0, result.stderr);
|
|
87
|
-
const scripts = (readHooks(root).hooks.PreCompact || []).flatMap(group => group.hooks || []).map(handler => hookScript(handler.command));
|
|
88
|
-
assert.ok(scripts.includes('precompact.cjs'));
|
|
89
|
-
assert.ok(fs.existsSync(path.join(root, '.codex/hooks/precompact.cjs')));
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
93
82
|
test('a fresh install registers every CafeKit hook under its own event', () => {
|
|
94
83
|
inTempProject((root) => {
|
|
95
84
|
const result = install(root);
|
|
@@ -103,7 +92,10 @@ test('a fresh install registers every CafeKit hook under its own event', () => {
|
|
|
103
92
|
.filter(Boolean);
|
|
104
93
|
for (const script of scripts) {
|
|
105
94
|
assert.ok(registered.includes(script), `${script} missing from ${eventName}`);
|
|
95
|
+
assert.ok(fs.existsSync(path.join(root, '.codex/hooks', script)), `${script} not installed`);
|
|
106
96
|
}
|
|
97
|
+
const template = JSON.parse(fs.readFileSync(TEMPLATE, 'utf8')).hooks[eventName];
|
|
98
|
+
assert.deepEqual(installed.hooks[eventName].map((group) => group.matcher), template.map((group) => group.matcher));
|
|
107
99
|
}
|
|
108
100
|
});
|
|
109
101
|
});
|
|
@@ -119,6 +111,11 @@ test('a reinstall preserves user-added hooks instead of overwriting them', () =>
|
|
|
119
111
|
const commands = allCommands(readHooks(root));
|
|
120
112
|
assert.ok(commands.includes(USER_STOP_COMMAND), 'user Stop hook was erased by the reinstall');
|
|
121
113
|
assert.ok(commands.includes(USER_PRETOOL_COMMAND), 'user PreToolUse hook was erased by the reinstall');
|
|
114
|
+
const after = readHooks(root).hooks;
|
|
115
|
+
const userGroup = (event, command) => after[event].find((group) => group.hooks.some((handler) => handler.command === command));
|
|
116
|
+
assert.deepEqual(userGroup('Stop', USER_STOP_COMMAND), { matcher: '*', hooks: [{ type: 'command', command: USER_STOP_COMMAND }] });
|
|
117
|
+
assert.deepEqual(userGroup('PreToolUse', USER_PRETOOL_COMMAND), { matcher: 'Bash', hooks: [{ type: 'command', command: USER_PRETOOL_COMMAND }] });
|
|
118
|
+
assert.doesNotMatch(second.stdout + second.stderr, /matcher preserved/);
|
|
122
119
|
});
|
|
123
120
|
});
|
|
124
121
|
|
|
@@ -7,6 +7,7 @@ const os = require('node:os');
|
|
|
7
7
|
const path = require('node:path');
|
|
8
8
|
const { spawnSync, spawn } = require('node:child_process');
|
|
9
9
|
const { test } = require('node:test');
|
|
10
|
+
const { installCodexHookRuntime } = require('./codex-hook-runtime-fixture.cjs');
|
|
10
11
|
|
|
11
12
|
const OLD_INSPECTION_MATCHER = 'Bash|exec_command|apply_patch|Edit|Write|Read|Glob|Grep|mcp__.*|shell';
|
|
12
13
|
const NEW_INSPECTION_MATCHER = 'Bash|exec_command|shell|apply_patch|mcp__.*|collaborationspawn_agent|collaborationwait_agent';
|
|
@@ -44,31 +45,6 @@ test('upgrade preserves a mixed user group and warns', () => matcherFixture(f =>
|
|
|
44
45
|
assert.ok(f.read().hooks.PreToolUse[0].hooks.some(h => h.command === 'echo user-owned'));
|
|
45
46
|
assert.ok(f.warnings.some(line => /matcher.*preserv/i.test(line)));
|
|
46
47
|
}));
|
|
47
|
-
test('reinstall keeps purely user groups unchanged without matcher warnings', () => matcherFixture(f => {
|
|
48
|
-
const userPre = { matcher: 'Bash', hooks: [{ command: 'my-linter' }] };
|
|
49
|
-
const userPost = { hooks: [{ command: 'my-logger' }] };
|
|
50
|
-
f.config.hooks = { PreToolUse: [userPre], PostToolUse: [userPost] }; f.save();
|
|
51
|
-
const installs = Array.from({ length: 2 }, () => {
|
|
52
|
-
const result = spawnSync(process.execPath, [path.resolve(__dirname, '../install.js'), '--platform', 'codex', '-y'], {
|
|
53
|
-
cwd: f.root, encoding: 'utf8',
|
|
54
|
-
});
|
|
55
|
-
return { result, config: f.read() };
|
|
56
|
-
});
|
|
57
|
-
for (const { result, config } of installs) {
|
|
58
|
-
assert.equal(result.status, 0, result.stderr);
|
|
59
|
-
for (const [event, original] of [['PreToolUse', userPre], ['PostToolUse', userPost]]) {
|
|
60
|
-
const group = config.hooks[event].find(g => g.hooks.some(h => h.command === original.hooks[0].command));
|
|
61
|
-
assert.equal(JSON.stringify(group), JSON.stringify(original));
|
|
62
|
-
}
|
|
63
|
-
assert.doesNotMatch(result.stdout + result.stderr, /matcher preserved/);
|
|
64
|
-
}
|
|
65
|
-
}));
|
|
66
|
-
test('fresh hook registration uses every template matcher', () => matcherFixture(f => {
|
|
67
|
-
f.merge();
|
|
68
|
-
for (const [event, groups] of Object.entries(f.config.hooks)) {
|
|
69
|
-
assert.deepEqual(f.read().hooks[event].map(g => g.matcher), groups.map(g => g.matcher));
|
|
70
|
-
}
|
|
71
|
-
}));
|
|
72
48
|
test('upgrade preserves a user-selected managed matcher and warns', () => matcherFixture(f => {
|
|
73
49
|
f.config.hooks.PreToolUse[0].matcher = 'Read|Grep'; f.save(); f.merge();
|
|
74
50
|
assert.equal(f.read().hooks.PreToolUse[0].matcher, 'Read|Grep');
|
|
@@ -93,12 +69,18 @@ const { stateDir: codexStateDir } = require(
|
|
|
93
69
|
path.join(CODEX_HOOKS, 'lib', 'state-store.cjs')
|
|
94
70
|
);
|
|
95
71
|
|
|
72
|
+
// A hook child that never exits must fail its test, not hang the whole suite.
|
|
73
|
+
const HOOK_TIMEOUT_MS = 30000;
|
|
74
|
+
|
|
96
75
|
function runHook(file, cwd, payload) {
|
|
97
|
-
|
|
76
|
+
const result = spawnSync(process.execPath, [file], {
|
|
98
77
|
cwd,
|
|
99
78
|
input: JSON.stringify(payload),
|
|
100
|
-
encoding: 'utf8'
|
|
79
|
+
encoding: 'utf8',
|
|
80
|
+
timeout: HOOK_TIMEOUT_MS
|
|
101
81
|
});
|
|
82
|
+
assert.equal(result.error, undefined, `${path.basename(file)} did not exit: ${result.error?.message}`);
|
|
83
|
+
return result;
|
|
102
84
|
}
|
|
103
85
|
|
|
104
86
|
function runtimeContext(root, feature = 'auth', session = 'session-a') {
|
|
@@ -145,26 +127,7 @@ function inHookFixture(run, options = {}) {
|
|
|
145
127
|
const hooks = path.join(root, '.codex', 'hooks');
|
|
146
128
|
let deferred = false;
|
|
147
129
|
try {
|
|
148
|
-
|
|
149
|
-
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/spec-touch.cjs'), path.join(hooks, 'lib/spec-touch.cjs'));
|
|
150
|
-
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/secret-keywords.cjs'), path.join(hooks, 'lib/secret-keywords.cjs'));
|
|
151
|
-
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/update-check.cjs'), path.join(hooks, 'lib/update-check.cjs'));
|
|
152
|
-
fs.copyFileSync(
|
|
153
|
-
path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/inspect-paths.cjs'),
|
|
154
|
-
path.join(hooks, 'lib/inspect-paths.cjs'),
|
|
155
|
-
);
|
|
156
|
-
fs.copyFileSync(
|
|
157
|
-
path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/privacy-command-analysis.cjs'),
|
|
158
|
-
path.join(hooks, 'lib/privacy-command-analysis.cjs'),
|
|
159
|
-
);
|
|
160
|
-
fs.copyFileSync(
|
|
161
|
-
path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/privacy-paths.cjs'),
|
|
162
|
-
path.join(hooks, 'lib/privacy-paths.cjs'),
|
|
163
|
-
);
|
|
164
|
-
fs.copyFileSync(
|
|
165
|
-
path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/runtime-path-safety.cjs'),
|
|
166
|
-
path.join(hooks, 'lib/runtime-path-safety.cjs'),
|
|
167
|
-
);
|
|
130
|
+
installCodexHookRuntime(root);
|
|
168
131
|
fs.mkdirSync(path.join(root, '.codex', 'scripts'), { recursive: true });
|
|
169
132
|
if (options.policy !== 'missing') {
|
|
170
133
|
if (options.policy === 'malformed') {
|
|
@@ -216,15 +179,7 @@ function inNoCommitCodexFixture(run) {
|
|
|
216
179
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cafekit-codex-no-commit-'));
|
|
217
180
|
const hooks = path.join(root, '.codex/hooks');
|
|
218
181
|
try {
|
|
219
|
-
|
|
220
|
-
for (const name of ['privacy-paths.cjs', 'privacy-command-analysis.cjs']) {
|
|
221
|
-
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib', name), path.join(hooks, 'lib', name));
|
|
222
|
-
}
|
|
223
|
-
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/spec-touch.cjs'), path.join(hooks, 'lib/spec-touch.cjs'));
|
|
224
|
-
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/secret-keywords.cjs'), path.join(hooks, 'lib/secret-keywords.cjs'));
|
|
225
|
-
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/update-check.cjs'), path.join(hooks, 'lib/update-check.cjs'));
|
|
226
|
-
fs.copyFileSync(path.join(PACKAGE_ROOT, 'src/claude/hooks/lib/runtime-path-safety.cjs'),
|
|
227
|
-
path.join(hooks, 'lib/runtime-path-safety.cjs'));
|
|
182
|
+
installCodexHookRuntime(root);
|
|
228
183
|
const scripts = path.join(root, '.codex/scripts');
|
|
229
184
|
fs.mkdirSync(scripts, { recursive: true });
|
|
230
185
|
for (const name of ['workflow-policy.cjs', 'provenance.cjs', 'spec-receipt.cjs', 'spec-resolver.cjs']) {
|
|
@@ -539,7 +494,7 @@ for (const method of ['spawnSync', 'execFileSync']) {
|
|
|
539
494
|
}
|
|
540
495
|
`);
|
|
541
496
|
const result = spawnSync(process.execPath, [path.join(hooks, 'spec-state.cjs')], {
|
|
542
|
-
cwd: root, encoding: 'utf8',
|
|
497
|
+
cwd: root, encoding: 'utf8', timeout: HOOK_TIMEOUT_MS,
|
|
543
498
|
env: { ...process.env, NODE_OPTIONS: `${process.env.NODE_OPTIONS || ''} --require ${JSON.stringify(preload)}` },
|
|
544
499
|
input: JSON.stringify({ cwd: root, featureName: 'auth', hook_event_name: 'UserPromptSubmit',
|
|
545
500
|
session_id: 'one-snapshot', prompt: 'Continue specs/auth' }),
|
|
@@ -754,7 +709,7 @@ test('Codex secret guardrail runs from source with its shared keyword shim', ()
|
|
|
754
709
|
delete env.CLAUDE_PROJECT_DIR;
|
|
755
710
|
delete env.PROJECT_ROOT;
|
|
756
711
|
const result = spawnSync(process.execPath, [path.join(CODEX_HOOKS, 'secret-output-guardrail.cjs')], {
|
|
757
|
-
cwd: PACKAGE_ROOT, env, encoding: 'utf8', input: JSON.stringify({ prompt: 'Explain secret handling.' }),
|
|
712
|
+
cwd: PACKAGE_ROOT, env, encoding: 'utf8', timeout: HOOK_TIMEOUT_MS, input: JSON.stringify({ prompt: 'Explain secret handling.' }),
|
|
758
713
|
});
|
|
759
714
|
assert.equal(result.status, 0);
|
|
760
715
|
assert.equal(result.stderr, '');
|
|
@@ -855,7 +810,7 @@ test('Codex compact releases only its own rules reservation', () => {
|
|
|
855
810
|
test('Codex precompact ignores empty stdin without recording a crash', () => {
|
|
856
811
|
inHookFixture((root, hooks) => {
|
|
857
812
|
const result = spawnSync(process.execPath, [path.join(hooks, 'precompact.cjs')], {
|
|
858
|
-
cwd: root, input: '', encoding: 'utf8',
|
|
813
|
+
cwd: root, input: '', encoding: 'utf8', timeout: HOOK_TIMEOUT_MS,
|
|
859
814
|
});
|
|
860
815
|
assert.equal(result.status, 0);
|
|
861
816
|
assert.equal(result.stderr, '');
|
|
@@ -900,7 +855,8 @@ function sessionWithoutUpdateNetwork(root, hooks, session_id) {
|
|
|
900
855
|
throw new Error('network disabled in update test');
|
|
901
856
|
};\n`);
|
|
902
857
|
const result = spawnSync(process.execPath, ['--require', preload, path.join(hooks, 'session.cjs')], {
|
|
903
|
-
cwd: root, encoding: 'utf8',
|
|
858
|
+
cwd: root, encoding: 'utf8', timeout: HOOK_TIMEOUT_MS,
|
|
859
|
+
input: JSON.stringify({ cwd: root, hook_event_name: 'SessionStart', source: 'startup', session_id }),
|
|
904
860
|
});
|
|
905
861
|
assert.equal(fs.existsSync(marker), false, 'the cached update check must not attempt a network request');
|
|
906
862
|
return result;
|
|
@@ -976,7 +932,7 @@ function shortPrivacyFixture(check) {
|
|
|
976
932
|
hook_event_name: 'PreToolUse', tool_name: 'Bash', tool_input: { command: `cat ${file}` } });
|
|
977
933
|
const block = (file, id) => runHook(path.join(hooks, 'privacy-block.cjs'), root, payload(file, id));
|
|
978
934
|
const submit = (prompt, session_id = session, env = process.env) => spawnSync(process.execPath,
|
|
979
|
-
[path.join(hooks, 'privacy-approval.cjs')], { cwd: root, encoding: 'utf8', env,
|
|
935
|
+
[path.join(hooks, 'privacy-approval.cjs')], { cwd: root, encoding: 'utf8', env, timeout: HOOK_TIMEOUT_MS,
|
|
980
936
|
input: JSON.stringify({ cwd: root, session_id, hook_event_name: 'UserPromptSubmit', prompt }) });
|
|
981
937
|
const files = prefix => fs.existsSync(dir) ? fs.readdirSync(dir).filter(name => name.startsWith(prefix)) : [];
|
|
982
938
|
const id = result => {
|
|
@@ -1682,6 +1638,7 @@ test('empty or forged Codex hook authority fails closed', () => {
|
|
|
1682
1638
|
env: { ...process.env },
|
|
1683
1639
|
input,
|
|
1684
1640
|
encoding: 'utf8',
|
|
1641
|
+
timeout: HOOK_TIMEOUT_MS,
|
|
1685
1642
|
});
|
|
1686
1643
|
assert.equal(result.status, 0, `${JSON.stringify(input)}: ${result.stderr}`);
|
|
1687
1644
|
assert.equal(result.stderr, '');
|
|
@@ -2390,7 +2347,37 @@ test('Codex hook-context canonicalizes symlink project root (regression)', () =>
|
|
|
2390
2347
|
});
|
|
2391
2348
|
});
|
|
2392
2349
|
|
|
2393
|
-
test('Codex
|
|
2350
|
+
test('Codex session state refuses a symlinked state path and writes nothing outside the project', () => {
|
|
2351
|
+
inHookFixture((root, hooks) => {
|
|
2352
|
+
const stateStore = require(path.join(hooks, 'lib', 'state-store.cjs'));
|
|
2353
|
+
const sessionId = 'sess-symlink';
|
|
2354
|
+
const dir = stateStore.stateDir(root, sessionId);
|
|
2355
|
+
const sessionState = path.dirname(dir);
|
|
2356
|
+
// A repository can commit either the shared state folder or one session folder as a link.
|
|
2357
|
+
for (const link of [sessionState, dir]) {
|
|
2358
|
+
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-state-outside-'));
|
|
2359
|
+
try {
|
|
2360
|
+
fs.rmSync(sessionState, { recursive: true, force: true });
|
|
2361
|
+
fs.mkdirSync(path.dirname(link), { recursive: true });
|
|
2362
|
+
fs.symlinkSync(outside, link);
|
|
2363
|
+
let called = false;
|
|
2364
|
+
const ok = stateStore.withStateLock(root, sessionId, (stateDir) => {
|
|
2365
|
+
called = true;
|
|
2366
|
+
stateStore.writeData(stateDir, { todos: [] });
|
|
2367
|
+
});
|
|
2368
|
+
assert.equal(ok, false, `${path.relative(root, link)} link must be refused`);
|
|
2369
|
+
assert.equal(called, false);
|
|
2370
|
+
assert.deepEqual(fs.readdirSync(outside), [], 'nothing may be written outside the project');
|
|
2371
|
+
} finally {
|
|
2372
|
+
fs.rmSync(sessionState, { recursive: true, force: true });
|
|
2373
|
+
fs.rmSync(outside, { recursive: true, force: true });
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
assert.equal(stateStore.withStateLock(root, sessionId, () => {}), true, 'a plain state folder still locks');
|
|
2377
|
+
});
|
|
2378
|
+
});
|
|
2379
|
+
|
|
2380
|
+
test('Codex resolver rejects symlink escape', () => {
|
|
2394
2381
|
inHookFixture((root, hooks) => {
|
|
2395
2382
|
const specUtils = require(path.join(hooks, 'lib', 'spec-utils.cjs'));
|
|
2396
2383
|
const resolverPath = path.join(root, '.codex', 'scripts', 'spec-resolver.cjs');
|
|
@@ -2418,23 +2405,6 @@ test('Codex resolver and state reject symlink escape', () => {
|
|
|
2418
2405
|
);
|
|
2419
2406
|
fs.rmSync(outsideScripts, { recursive: true, force: true });
|
|
2420
2407
|
}
|
|
2421
|
-
// State dir symlink escape: create symlink for session-state dir
|
|
2422
|
-
const stateStore = require(path.join(hooks, 'lib', 'state-store.cjs'));
|
|
2423
|
-
const sessionId = 'sess-symlink';
|
|
2424
|
-
const dir = stateStore.stateDir(root, sessionId);
|
|
2425
|
-
fs.mkdirSync(path.dirname(dir), { recursive: true });
|
|
2426
|
-
// Create a symlink that points outside project
|
|
2427
|
-
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'outside-'));
|
|
2428
|
-
try {
|
|
2429
|
-
if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
|
|
2430
|
-
fs.symlinkSync(outside, dir);
|
|
2431
|
-
// withStateLock should handle symlink safely (no crash, no write outside)
|
|
2432
|
-
const ok = stateStore.withStateLock(root, sessionId, () => {});
|
|
2433
|
-
assert.equal(typeof ok, 'boolean');
|
|
2434
|
-
} finally {
|
|
2435
|
-
try { fs.unlinkSync(dir); } catch {}
|
|
2436
|
-
fs.rmSync(outside, { recursive: true, force: true });
|
|
2437
|
-
}
|
|
2438
2408
|
});
|
|
2439
2409
|
});
|
|
2440
2410
|
|
|
@@ -55,8 +55,8 @@ function catalog(root, script, skillsRoot) {
|
|
|
55
55
|
return JSON.parse(result.stdout);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
test('the explicit-only skills are
|
|
59
|
-
assert.deepEqual(explicitOnly.sort(), ['adv', 'vibe']);
|
|
58
|
+
test('the explicit-only skills are adv, toaa and vibe', () => {
|
|
59
|
+
assert.deepEqual(explicitOnly.sort(), ['adv', 'toaa', 'vibe']);
|
|
60
60
|
});
|
|
61
61
|
|
|
62
62
|
test('a Codex install writes the policy file for every explicit-only skill, owned, and for no other skill', () => {
|
|
@@ -6,16 +6,14 @@ const os = require('node:os');
|
|
|
6
6
|
const path = require('node:path');
|
|
7
7
|
const { spawnSync } = require('node:child_process');
|
|
8
8
|
const { test } = require('node:test');
|
|
9
|
+
const { installCodexHookRuntime } = require('./codex-hook-runtime-fixture.cjs');
|
|
9
10
|
const SOURCE = path.resolve(__dirname, '../../src');
|
|
10
11
|
|
|
11
12
|
function fixture(run) {
|
|
12
13
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ck-native-boundaries-'));
|
|
13
14
|
const hooks = path.join(root, '.codex/hooks');
|
|
14
15
|
try {
|
|
15
|
-
|
|
16
|
-
for (const name of ['runtime-path-safety', 'privacy-command-analysis', 'privacy-paths', 'secret-keywords', 'inspect-paths', 'update-check']) {
|
|
17
|
-
fs.copyFileSync(path.join(SOURCE, `claude/hooks/lib/${name}.cjs`), path.join(hooks, `lib/${name}.cjs`));
|
|
18
|
-
}
|
|
16
|
+
installCodexHookRuntime(root);
|
|
19
17
|
fs.cpSync(path.join(SOURCE, 'claude/scripts'), path.join(root, '.codex/scripts'), { recursive: true });
|
|
20
18
|
fs.writeFileSync(path.join(root, '.gitignore'), '.codex/\n');
|
|
21
19
|
for (const args of [['init', '-q'], ['config', 'user.email', 'probe@example.invalid'], ['config', 'user.name', 'Probe'], ['add', '.gitignore'], ['commit', '-qm', 'base']]) {
|