@haposoft/cafekit 0.16.0-rc.4 → 0.16.0-rc.5
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/bin/__tests__/change-firewall.semantic-firewall.test.js +2134 -0
- package/bin/__tests__/codex-hooks.test.js +136 -64
- package/bin/__tests__/develop-contract.test.js +55 -17
- package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +342 -0
- package/bin/__tests__/package-inventory.test.js +29 -11
- package/bin/__tests__/release-preflight.semantic-firewall.test.js +578 -0
- package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +378 -0
- package/bin/__tests__/specs-v2-execution-closeout.test.js +59 -12
- package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +78 -14
- package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +222 -42
- package/bin/__tests__/validator-grounding.test.js +103 -4
- package/package.json +5 -2
- package/src/claude/hooks/__tests__/completion-authority.test.js +31 -1
- package/src/claude/hooks/__tests__/privacy-block.test.js +111 -29
- package/src/claude/hooks/privacy-block.cjs +38 -14
- package/src/claude/scripts/change-firewall.cjs +1748 -0
- package/src/claude/scripts/spec-authoring-digest.cjs +151 -0
- package/src/claude/scripts/spec-authoring-validation.cjs +126 -0
- package/src/claude/scripts/spec-final-state.cjs +46 -0
- package/src/claude/scripts/spec-ground.cjs +46 -10
- package/src/claude/scripts/spec-readiness.cjs +324 -17
- package/src/claude/scripts/spec-scaffold.cjs +268 -28
- package/src/claude/scripts/validate-spec-output.cjs +262 -10
- package/src/claude/skills/specs/references/review.md +14 -2
- package/src/claude/skills/specs/templates/spec-state.json +14 -2
- package/src/codex/hooks/lib/privacy-state.cjs +23 -22
- package/src/codex/hooks/privacy-approval.cjs +7 -3
- package/src/codex/hooks/privacy-block.cjs +45 -22
|
@@ -128,7 +128,7 @@ function inHookFixture(run, options = {}) {
|
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
test('Codex privacy approval is exact, session-bound, and
|
|
131
|
+
test('Codex privacy approval is exact, session-bound, and reusable within its window', () => {
|
|
132
132
|
inHookFixture((root, hooks) => {
|
|
133
133
|
const nested = path.join(root, 'packages', 'app');
|
|
134
134
|
fs.mkdirSync(nested, { recursive: true });
|
|
@@ -173,31 +173,36 @@ test('Codex privacy approval is exact, session-bound, and one-time', () => {
|
|
|
173
173
|
});
|
|
174
174
|
assert.match(wrongSession.stdout, /rejected/);
|
|
175
175
|
|
|
176
|
+
// A pasted prompt carries the terminal's surrounding whitespace. The
|
|
177
|
+
// exactness that matters is the request id, not the padding.
|
|
176
178
|
const padded = runHook(approve, nested, {
|
|
177
179
|
cwd: nested,
|
|
178
180
|
session_id: 'session-a',
|
|
179
181
|
hook_event_name: 'UserPromptSubmit',
|
|
180
182
|
prompt: ` ${prompt}\n`
|
|
181
183
|
});
|
|
182
|
-
assert.
|
|
183
|
-
|
|
184
|
-
const approved = runHook(approve, nested, {
|
|
185
|
-
cwd: nested,
|
|
186
|
-
session_id: 'session-a',
|
|
187
|
-
hook_event_name: 'UserPromptSubmit',
|
|
188
|
-
prompt
|
|
189
|
-
});
|
|
190
|
-
assert.match(approved.stdout, /approved one retry/);
|
|
184
|
+
assert.match(padded.stdout, /User approved/, 'whitespace padding must not void the approval');
|
|
191
185
|
|
|
186
|
+
// The grant now covers repeated access to the same paths, and is not bound
|
|
187
|
+
// to the tool that first tripped the gate.
|
|
192
188
|
const allowedOnce = runHook(block, nested, request);
|
|
193
189
|
assert.equal(allowedOnce.stdout, '');
|
|
194
190
|
|
|
195
|
-
const
|
|
196
|
-
assert.equal(
|
|
191
|
+
const allowedAgain = runHook(block, nested, request);
|
|
192
|
+
assert.equal(allowedAgain.stdout, '', 'one approval covers a re-read');
|
|
197
193
|
|
|
194
|
+
const allowedOtherTool = runHook(block, nested, {
|
|
195
|
+
...request,
|
|
196
|
+
tool_name: 'exec_command'
|
|
197
|
+
});
|
|
198
|
+
assert.equal(allowedOtherTool.stdout, '', 'grant is not bound to a tool name');
|
|
199
|
+
|
|
200
|
+
// A fresh session, so the single-path grant above cannot mask the exact-set
|
|
201
|
+
// matching under test.
|
|
198
202
|
fs.writeFileSync(path.join(nested, 'credentials.json'), '{"token":"test"}\n');
|
|
199
203
|
const multiRequest = {
|
|
200
204
|
...request,
|
|
205
|
+
session_id: 'session-c',
|
|
201
206
|
tool_input: { command: 'cat .env credentials.json' }
|
|
202
207
|
};
|
|
203
208
|
const multiDenied = runHook(block, nested, multiRequest);
|
|
@@ -210,31 +215,65 @@ test('Codex privacy approval is exact, session-bound, and one-time', () => {
|
|
|
210
215
|
|
|
211
216
|
const multiApproved = runHook(approve, nested, {
|
|
212
217
|
cwd: nested,
|
|
213
|
-
session_id: 'session-
|
|
218
|
+
session_id: 'session-c',
|
|
214
219
|
hook_event_name: 'UserPromptSubmit',
|
|
215
220
|
prompt: multiPrompt
|
|
216
221
|
});
|
|
217
|
-
assert.match(multiApproved.stdout, /approved
|
|
222
|
+
assert.match(multiApproved.stdout, /User approved/);
|
|
218
223
|
|
|
219
|
-
// A
|
|
220
|
-
//
|
|
221
|
-
const subsetDenied = runHook(block, nested, request);
|
|
224
|
+
// A grant for two paths authorizes exactly that set: neither a subset nor a
|
|
225
|
+
// superset, in either order.
|
|
226
|
+
const subsetDenied = runHook(block, nested, { ...request, session_id: 'session-c' });
|
|
222
227
|
assert.equal(JSON.parse(subsetDenied.stdout).hookSpecificOutput.permissionDecision, 'deny');
|
|
223
228
|
fs.writeFileSync(path.join(nested, 'tokens.json'), '{"token":"test"}\n');
|
|
224
229
|
const supersetDenied = runHook(block, nested, {
|
|
225
230
|
...request,
|
|
231
|
+
session_id: 'session-c',
|
|
226
232
|
tool_input: { command: 'cat .env credentials.json tokens.json' }
|
|
227
233
|
});
|
|
228
234
|
assert.equal(JSON.parse(supersetDenied.stdout).hookSpecificOutput.permissionDecision, 'deny');
|
|
229
235
|
const reordered = {
|
|
230
236
|
...request,
|
|
237
|
+
session_id: 'session-c',
|
|
231
238
|
tool_input: { command: 'cat credentials.json .env' }
|
|
232
239
|
};
|
|
233
240
|
assert.equal(runHook(block, nested, reordered).stdout, '');
|
|
234
|
-
assert.equal(
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
);
|
|
241
|
+
assert.equal(runHook(block, nested, multiRequest).stdout, '', 'the set stays granted');
|
|
242
|
+
|
|
243
|
+
// Session binding survives the change: another session sees no grant.
|
|
244
|
+
const otherSession = runHook(block, nested, { ...request, session_id: 'session-d' });
|
|
245
|
+
assert.equal(JSON.parse(otherSession.stdout).hookSpecificOutput.permissionDecision, 'deny');
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('Codex privacy grant window is one hour and restarts at approval', () => {
|
|
250
|
+
inHookFixture((root, hooks) => {
|
|
251
|
+
const state = require(path.join(hooks, 'lib', 'privacy-state.cjs'));
|
|
252
|
+
const HOUR = 60 * 60 * 1000;
|
|
253
|
+
assert.equal(state.TTL_MS, HOUR, 'a five-minute window expired mid-conversation');
|
|
254
|
+
|
|
255
|
+
fs.writeFileSync(path.join(root, '.env'), 'SECRET=1\n');
|
|
256
|
+
const pending = state.createPending({
|
|
257
|
+
projectRoot: root,
|
|
258
|
+
sessionCwd: root,
|
|
259
|
+
sessionId: 'session-ttl',
|
|
260
|
+
filePaths: ['.env'],
|
|
261
|
+
});
|
|
262
|
+
assert.ok(pending.expiresAt - Date.now() > HOUR - 5000, 'pending must live an hour');
|
|
263
|
+
|
|
264
|
+
const approved = state.approvePending({
|
|
265
|
+
projectRoot: root,
|
|
266
|
+
sessionId: 'session-ttl',
|
|
267
|
+
requestId: pending.requestId,
|
|
268
|
+
});
|
|
269
|
+
assert.equal(approved.ok, true);
|
|
270
|
+
|
|
271
|
+
const stateDir = path.join(root, '.codex', 'hooks', '.privacy');
|
|
272
|
+
const tokenFile = fs.readdirSync(stateDir).find((name) => name.startsWith('token-'));
|
|
273
|
+
assert.ok(tokenFile, 'approval must write a grant');
|
|
274
|
+
const grant = JSON.parse(fs.readFileSync(path.join(stateDir, tokenFile), 'utf8'));
|
|
275
|
+
assert.ok(grant.expiresAt - Date.now() > HOUR - 5000, 'the grant window starts at approval');
|
|
276
|
+
assert.equal(grant.toolName, undefined, 'a grant is not bound to a tool name');
|
|
238
277
|
});
|
|
239
278
|
});
|
|
240
279
|
|
|
@@ -909,14 +948,14 @@ test('Codex adapter enforces declared task artifact SHA-256 with missing, invali
|
|
|
909
948
|
}
|
|
910
949
|
});
|
|
911
950
|
|
|
912
|
-
test('Codex privacy denies obfuscated
|
|
951
|
+
test('Codex privacy denies obfuscated secret reads, allows benign and runtime state', () => {
|
|
913
952
|
inHookFixture((root, hooks) => {
|
|
914
953
|
const block = path.join(hooks, 'privacy-block.cjs');
|
|
915
954
|
const denied = [
|
|
916
955
|
{ command: "cat .e''nv", tool: 'exec_command' },
|
|
917
|
-
{ command:
|
|
918
|
-
{ command:
|
|
919
|
-
{ command:
|
|
956
|
+
{ command: 'cat ~/.cafekit/completion-authority/hmac.key', tool: 'exec_command' },
|
|
957
|
+
{ command: 'cat ~/.ssh/id_rsa', tool: 'Bash' },
|
|
958
|
+
{ command: 'docker compose --env-file .env up', tool: 'exec_command' },
|
|
920
959
|
{ command: "python3 -c \"open('.env').read()\"", tool: 'exec_command' },
|
|
921
960
|
{ command: "node -e \"fs.readFileSync('.env')\"", tool: 'Bash' },
|
|
922
961
|
{ command: "bash -c 'cat .e''nv'", tool: 'exec_command' },
|
|
@@ -926,10 +965,18 @@ test('Codex privacy denies obfuscated and session paths, allows benign', () => {
|
|
|
926
965
|
const res = runHook(block, root, { cwd: root, session_id: 's1', hook_event_name: 'PreToolUse', tool_name: tool, tool_input: { command } });
|
|
927
966
|
assert.equal(JSON.parse(res.stdout).hookSpecificOutput.permissionDecision, 'deny', command);
|
|
928
967
|
}
|
|
968
|
+
// Runtime state and transcripts are CafeKit's own bookkeeping, not secrets.
|
|
969
|
+
// Guarding them by substring also matched every source file and doc that
|
|
970
|
+
// merely spelled `sessions` or `session-state`, so the gate fired on the
|
|
971
|
+
// repo it was installed in. The one real secret there, hmac.key, is still
|
|
972
|
+
// covered above by the `.key` rule.
|
|
929
973
|
const allowed = [
|
|
930
974
|
{ command: "echo hello", tool: 'exec_command' },
|
|
931
975
|
{ command: "cat README.md", tool: 'exec_command' },
|
|
932
976
|
{ command: "cat src/app.js", tool: 'Bash' },
|
|
977
|
+
{ command: 'cat .codex/sessions/test.json', tool: 'exec_command' },
|
|
978
|
+
{ command: 'cat .codex/session-state/latest.md', tool: 'Bash' },
|
|
979
|
+
{ command: 'cat docs/sessions-guide.md', tool: 'exec_command' },
|
|
933
980
|
];
|
|
934
981
|
for (const { command, tool } of allowed) {
|
|
935
982
|
const res = runHook(block, root, { cwd: root, session_id: 's1', hook_event_name: 'PreToolUse', tool_name: tool, tool_input: { command } });
|
|
@@ -938,45 +985,27 @@ test('Codex privacy denies obfuscated and session paths, allows benign', () => {
|
|
|
938
985
|
});
|
|
939
986
|
});
|
|
940
987
|
|
|
941
|
-
test('Codex privacy denies
|
|
988
|
+
test('Codex privacy denies sensitive names it can resolve, allows pure indirection', () => {
|
|
942
989
|
inHookFixture((root, hooks) => {
|
|
943
990
|
const block = path.join(hooks, 'privacy-block.cjs');
|
|
944
991
|
const denied = [
|
|
945
|
-
{ command: "cat $FILE", tool: 'Bash' },
|
|
946
|
-
{ command: 'cat "$FILE"', tool: 'exec_command' },
|
|
947
|
-
{ command: "cat ${FILE}", tool: 'Bash' },
|
|
948
|
-
{ command: "cat $1", tool: 'exec_command' },
|
|
949
|
-
{ command: "cat $(echo .env)", tool: 'Bash' },
|
|
950
|
-
{ command: "cat `echo .env`", tool: 'Bash' },
|
|
951
|
-
{ command: "head $SECRET", tool: 'exec_command' },
|
|
952
|
-
{ command: "tail ${MY_FILE}", tool: 'Bash' },
|
|
953
|
-
{ command: "cat README.md $EXTRA", tool: 'Bash' },
|
|
954
|
-
{ command: "head README.md $UNRELATED", tool: 'exec_command' },
|
|
955
|
-
{ command: "echo ok; cat README.md $FILE", tool: 'Bash' },
|
|
956
|
-
{ command: "cat README.md | head $FILE", tool: 'exec_command' },
|
|
957
|
-
{ command: "bash -c 'cat $FILE'", tool: 'Bash' },
|
|
958
|
-
{ command: "echo $(cat $FILE)", tool: 'exec_command' },
|
|
959
|
-
{ command: 'cat README.md "$(printf safe)"', tool: 'Bash' },
|
|
960
|
-
{ command: 'cat README.md *', tool: 'Bash' },
|
|
961
|
-
{ command: 'cat README.md ~/README.md', tool: 'exec_command' },
|
|
962
|
-
{ command: 'cat README.md {README.md,LICENSE}', tool: 'Bash' },
|
|
963
992
|
{ command: 'cat .e\\\nnv', tool: 'Bash' },
|
|
964
993
|
{ command: "cat $'\\x2e\\x65\\x6e\\x76'", tool: 'exec_command' },
|
|
965
|
-
{ command: '
|
|
966
|
-
{ command: '
|
|
967
|
-
{ command: '
|
|
968
|
-
{ command:
|
|
969
|
-
{ command:
|
|
970
|
-
{ command: '
|
|
971
|
-
{ command: '
|
|
972
|
-
{ command: 'python3 -c \'open(os.environ["FILE"]).read()\'', tool: 'exec_command' },
|
|
973
|
-
{ command: 'node -e \'fs.readFileSync(process.env.FILE)\'', tool: 'Bash' },
|
|
974
|
-
{ command: 'cat < "$FILE"', tool: 'exec_command' },
|
|
994
|
+
{ command: 'cat .env*', tool: 'Bash' },
|
|
995
|
+
{ command: 'cat certs/*.pem', tool: 'exec_command' },
|
|
996
|
+
{ command: 'cat ~/.aws/credentials', tool: 'Bash' },
|
|
997
|
+
{ command: 'ssh -i ~/.ssh/id_ed25519 host', tool: 'exec_command' },
|
|
998
|
+
{ command: 'curl --config ~/.netrc https://example.com', tool: 'Bash' },
|
|
999
|
+
{ command: 'kubectl --kubeconfig ~/.kube/kubeconfig get pods', tool: 'exec_command' },
|
|
1000
|
+
{ command: 'openssl rsa -in server.key', tool: 'Bash' },
|
|
975
1001
|
];
|
|
976
1002
|
for (const { command, tool } of denied) {
|
|
977
1003
|
const res = runHook(block, root, { cwd: root, session_id: 's1', hook_event_name: 'PreToolUse', tool_name: tool, tool_input: { command } });
|
|
978
1004
|
assert.equal(JSON.parse(res.stdout).hookSpecificOutput.permissionDecision, 'deny', `should deny ${command}`);
|
|
979
1005
|
}
|
|
1006
|
+
// Accepted gap: the target of `$FILE` is unknowable without running the
|
|
1007
|
+
// shell, and every denial here costs a round trip through a one-shot
|
|
1008
|
+
// approval prompt. `less "$HOME/.env"` is the edge of that decision.
|
|
980
1009
|
const allowed = [
|
|
981
1010
|
{ command: "echo $VAR", tool: 'exec_command' },
|
|
982
1011
|
{ command: 'echo "$HOME"', tool: 'Bash' },
|
|
@@ -995,11 +1024,37 @@ test('Codex privacy denies active read expansion, preserves literal dollars and
|
|
|
995
1024
|
{ command: 'echo not.env', tool: 'Bash' },
|
|
996
1025
|
{ command: 'echo .env', tool: 'exec_command' },
|
|
997
1026
|
{ command: "printf '%s\\n' .env", tool: 'Bash' },
|
|
1027
|
+
{ command: 'git log -- .env', tool: 'Bash' },
|
|
998
1028
|
{ command: 'git status sessions.md', tool: 'exec_command' },
|
|
999
1029
|
{ command: 'python3 -c "print(os.environ[\'HOME\'])"', tool: 'Bash' },
|
|
1000
1030
|
{ command: 'head -n "$COUNT" README.md', tool: 'exec_command' },
|
|
1001
1031
|
{ command: 'grep .env README.md', tool: 'Bash' },
|
|
1002
1032
|
{ command: "sed -n '/.env/p' README.md", tool: 'exec_command' },
|
|
1033
|
+
{ command: 'cat $FILE', tool: 'Bash' },
|
|
1034
|
+
{ command: 'cat "$FILE"', tool: 'exec_command' },
|
|
1035
|
+
{ command: 'cat ${FILE}', tool: 'Bash' },
|
|
1036
|
+
{ command: 'cat $1', tool: 'exec_command' },
|
|
1037
|
+
{ command: 'cat $(echo .env)', tool: 'Bash' },
|
|
1038
|
+
{ command: 'cat `echo .env`', tool: 'Bash' },
|
|
1039
|
+
{ command: 'head $SECRET', tool: 'exec_command' },
|
|
1040
|
+
{ command: 'tail ${MY_FILE}', tool: 'Bash' },
|
|
1041
|
+
{ command: 'less "$HOME/.env"', tool: 'Bash' },
|
|
1042
|
+
{ command: 'cat README.md $EXTRA', tool: 'Bash' },
|
|
1043
|
+
{ command: 'echo ok; cat README.md $FILE', tool: 'Bash' },
|
|
1044
|
+
{ command: 'cat README.md | head $FILE', tool: 'exec_command' },
|
|
1045
|
+
{ command: "bash -c 'cat $FILE'", tool: 'Bash' },
|
|
1046
|
+
{ command: 'cat README.md *', tool: 'Bash' },
|
|
1047
|
+
{ command: 'cat ~/.zshrc', tool: 'exec_command' },
|
|
1048
|
+
{ command: 'du -sh */', tool: 'Bash' },
|
|
1049
|
+
{ command: 'env -u UNUSED cat "$FILE"', tool: 'Bash' },
|
|
1050
|
+
{ command: 'timeout 1 cat "$FILE"', tool: 'exec_command' },
|
|
1051
|
+
{ command: "eval 'cat \"$FILE\"'", tool: 'exec_command' },
|
|
1052
|
+
{ command: "find . -exec sh -c 'cat \"$FILE\"' \\;", tool: 'Bash' },
|
|
1053
|
+
{ command: 'source "$FILE"', tool: 'exec_command' },
|
|
1054
|
+
{ command: 'xargs -a "$LIST" cat', tool: 'Bash' },
|
|
1055
|
+
{ command: 'python3 -c \'open(os.environ["FILE"]).read()\'', tool: 'exec_command' },
|
|
1056
|
+
{ command: 'node -e \'fs.readFileSync(process.env.FILE)\'', tool: 'Bash' },
|
|
1057
|
+
{ command: 'cat < "$FILE"', tool: 'exec_command' },
|
|
1003
1058
|
];
|
|
1004
1059
|
for (const { command, tool } of allowed) {
|
|
1005
1060
|
const res = runHook(block, root, { cwd: root, session_id: 's1', hook_event_name: 'PreToolUse', tool_name: tool, tool_input: { command } });
|
|
@@ -1007,11 +1062,24 @@ test('Codex privacy denies active read expansion, preserves literal dollars and
|
|
|
1007
1062
|
}
|
|
1008
1063
|
const assignmentOnly = runHook(block, root, { cwd: root, session_id: 's1', hook_event_name: 'PreToolUse', tool_name: 'Bash', tool_input: { command: 'FILE=.env; cat README.md' } });
|
|
1009
1064
|
assert.equal(assignmentOnly.stdout, '');
|
|
1010
|
-
const assignmentRead = runHook(block, root, { cwd: root, session_id: 's1', hook_event_name: 'PreToolUse', tool_name: 'Bash', tool_input: { command: 'FILE=.env; cat
|
|
1065
|
+
const assignmentRead = runHook(block, root, { cwd: root, session_id: 's1', hook_event_name: 'PreToolUse', tool_name: 'Bash', tool_input: { command: 'FILE=.env; cat .env' } });
|
|
1011
1066
|
assert.equal(JSON.parse(assignmentRead.stdout).hookSpecificOutput.permissionDecision, 'deny');
|
|
1012
1067
|
});
|
|
1013
1068
|
});
|
|
1014
1069
|
|
|
1070
|
+
test('Codex privacy reads an inert heredoc body as prose, not shell', () => {
|
|
1071
|
+
inHookFixture((root, hooks) => {
|
|
1072
|
+
const block = path.join(hooks, 'privacy-block.cjs');
|
|
1073
|
+
const inert = "gh pr create --body \"$(cat <<'BODY'\nRun `cat .env` to inspect config\nBODY\n)\"";
|
|
1074
|
+
const allowed = runHook(block, root, { cwd: root, session_id: 's1', hook_event_name: 'PreToolUse', tool_name: 'Bash', tool_input: { command: inert } });
|
|
1075
|
+
assert.equal(allowed.stdout, '', 'quoted delimiter expands nothing');
|
|
1076
|
+
|
|
1077
|
+
const expanding = 'bash -c "cat <<BODY\n$(cat .env)\nBODY"';
|
|
1078
|
+
const denied = runHook(block, root, { cwd: root, session_id: 's1', hook_event_name: 'PreToolUse', tool_name: 'Bash', tool_input: { command: expanding } });
|
|
1079
|
+
assert.equal(JSON.parse(denied.stdout).hookSpecificOutput.permissionDecision, 'deny', 'unquoted delimiter expands');
|
|
1080
|
+
});
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1015
1083
|
test('Claude and Codex privacy lexers agree on literals, delimiters, and quote expansion', () => {
|
|
1016
1084
|
inHookFixture((root, hooks) => {
|
|
1017
1085
|
const claude = path.join(PACKAGE_ROOT, 'src/claude/hooks/privacy-block.cjs');
|
|
@@ -1019,8 +1087,8 @@ test('Claude and Codex privacy lexers agree on literals, delimiters, and quote e
|
|
|
1019
1087
|
const vectors = [
|
|
1020
1088
|
["cat .e''nv", true],
|
|
1021
1089
|
["python3 -c \"open('.env').read()\"", true],
|
|
1022
|
-
['cat README.md $FILE',
|
|
1023
|
-
['echo ok; cat README.md $FILE',
|
|
1090
|
+
['cat README.md $FILE', false],
|
|
1091
|
+
['echo ok; cat README.md $FILE', false],
|
|
1024
1092
|
["cat '$FILE'", false],
|
|
1025
1093
|
["echo '$(cat $FILE)'", false],
|
|
1026
1094
|
["cat README.md '*'", false],
|
|
@@ -1030,13 +1098,17 @@ test('Claude and Codex privacy lexers agree on literals, delimiters, and quote e
|
|
|
1030
1098
|
['echo .envoy', false],
|
|
1031
1099
|
['cat .e\\\nnv', true],
|
|
1032
1100
|
["cat $'\\x2e\\x65\\x6e\\x76'", true],
|
|
1033
|
-
['env -u UNUSED cat "$FILE"',
|
|
1034
|
-
['timeout 1 cat "$FILE"',
|
|
1035
|
-
["eval 'cat \"$FILE\"'",
|
|
1036
|
-
["find . -exec sh -c 'cat \"$FILE\"' \\;",
|
|
1037
|
-
['source "$FILE"',
|
|
1038
|
-
['xargs -a "$LIST" cat',
|
|
1039
|
-
['python3 -c \'open(os.environ["FILE"]).read()\'',
|
|
1101
|
+
['env -u UNUSED cat "$FILE"', false],
|
|
1102
|
+
['timeout 1 cat "$FILE"', false],
|
|
1103
|
+
["eval 'cat \"$FILE\"'", false],
|
|
1104
|
+
["find . -exec sh -c 'cat \"$FILE\"' \\;", false],
|
|
1105
|
+
['source "$FILE"', false],
|
|
1106
|
+
['xargs -a "$LIST" cat', false],
|
|
1107
|
+
['python3 -c \'open(os.environ["FILE"]).read()\'', false],
|
|
1108
|
+
['cat ~/.ssh/id_rsa', true],
|
|
1109
|
+
['docker compose --env-file .env up', true],
|
|
1110
|
+
['cat ~/.zshrc', false],
|
|
1111
|
+
['git log -- .env', false],
|
|
1040
1112
|
['echo .env', false],
|
|
1041
1113
|
["printf '%s\\n' .env", false],
|
|
1042
1114
|
['git status sessions.md', false],
|
|
@@ -17,6 +17,7 @@ const VALIDATOR = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/validate-s
|
|
|
17
17
|
const GROUNDER = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-ground.cjs'));
|
|
18
18
|
const SEMANTIC_AUTHORITY = require(path.join(PACKAGE_ROOT, 'src/claude/hooks/semantic-review-authority.cjs'));
|
|
19
19
|
const READINESS = require(path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-readiness.cjs'));
|
|
20
|
+
const AUTHORING_VALIDATOR = path.join(PACKAGE_ROOT, 'src/claude/scripts/spec-authoring-validation.cjs');
|
|
20
21
|
const { copyClaudeTestRuntime } = require('./test-runtime-dependency-closure.cjs');
|
|
21
22
|
const DEVELOP = path.join(PACKAGE_ROOT, 'src/claude/skills/develop/SKILL.md');
|
|
22
23
|
const SPECS = path.join(PACKAGE_ROOT, 'src/claude/skills/specs/SKILL.md');
|
|
@@ -183,23 +184,38 @@ The result contains the literal deleted state.
|
|
|
183
184
|
## Verification Definitions
|
|
184
185
|
- **V1**: Criteria R1.1; Owner A-D-01; Decision refs D1, I1, C1; Method command \`node --test test/entry.test.js\`; Expected exit 0 and the service reports deleted state; Negative/failure a missing record returns not-found without deleted state; Reachability/grounding entrypoint \`src/entry.js\` via A-D-01.
|
|
185
186
|
`);
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
187
|
+
const semantic = runNode(root, SCAFFOLD, ['ordinary', '--sync-semantic-model']);
|
|
188
|
+
assert.equal(semantic.status, 0, commandOutput(semantic));
|
|
189
|
+
const authoring = runNode(root, AUTHORING_VALIDATOR, [specDir]);
|
|
190
|
+
assert.equal(authoring.status, 0, commandOutput(authoring));
|
|
190
191
|
initFixtureGit(root);
|
|
191
192
|
return { root, specDir, specPath };
|
|
192
193
|
}
|
|
193
194
|
|
|
194
195
|
function ordinaryReview() {
|
|
195
|
-
return {
|
|
196
|
-
reviewed_criteria: ['R1.1'],
|
|
197
|
-
counterexamples: [{
|
|
196
|
+
return canonicalPassReview(['R1.1'], [{
|
|
198
197
|
criterion: 'R1.1', case_kind: 'failure',
|
|
199
198
|
scenario: 'Deletion targets a record that does not exist in the current store.',
|
|
200
199
|
expected: 'The service returns not-found and never reports the record as deleted.',
|
|
201
200
|
decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
|
|
202
|
-
}]
|
|
201
|
+
}]);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function canonicalPassReview(reviewedCriteria, counterexamples) {
|
|
205
|
+
return {
|
|
206
|
+
verdict: 'PASS',
|
|
207
|
+
findings: [],
|
|
208
|
+
unresolved_decisions: [],
|
|
209
|
+
graph_coverage: [
|
|
210
|
+
'criterion_local', 'cross_criterion', 'runtime_path',
|
|
211
|
+
'assumption_provenance', 'compatibility_migration',
|
|
212
|
+
].map((surface) => ({
|
|
213
|
+
surface, covered: true,
|
|
214
|
+
notes: 'The review covers this semantic surface against the canonical model.',
|
|
215
|
+
})),
|
|
216
|
+
reviewed_criteria: reviewedCriteria,
|
|
217
|
+
counterexamples,
|
|
218
|
+
reviewer_evidence: null,
|
|
203
219
|
};
|
|
204
220
|
}
|
|
205
221
|
|
|
@@ -209,16 +225,12 @@ function promoteCanonicalModel(fixture) {
|
|
|
209
225
|
}
|
|
210
226
|
|
|
211
227
|
function makeCanonicalReady(fixture) {
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
spec.authoring.design = 'validated';
|
|
215
|
-
fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
228
|
+
const authoring = runNode(fixture.root, AUTHORING_VALIDATOR, [fixture.specDir]);
|
|
229
|
+
assert.equal(authoring.status, 0, commandOutput(authoring));
|
|
216
230
|
const result = READINESS.finalizeReadiness({
|
|
217
231
|
specDir: fixture.specDir,
|
|
218
232
|
projectRoot: fixture.root,
|
|
219
|
-
reviewResult: {
|
|
220
|
-
reviewed_criteria: ['R1.1', 'R1.2'],
|
|
221
|
-
counterexamples: [{
|
|
233
|
+
reviewResult: canonicalPassReview(['R1.1', 'R1.2'], [{
|
|
222
234
|
criterion: 'R1.1', case_kind: 'failure',
|
|
223
235
|
scenario: 'The service receives invalid input without its required discriminator.',
|
|
224
236
|
expected: 'The service returns rejected state and never emits enabled true.',
|
|
@@ -228,8 +240,7 @@ function makeCanonicalReady(fixture) {
|
|
|
228
240
|
scenario: 'The service reports enabled state without producing separate proof evidence.',
|
|
229
241
|
expected: 'Verification fails because the independent proof artifact is absent.',
|
|
230
242
|
decision_refs: ['D1', 'I1', 'C1'], verification_ref: 'V1',
|
|
231
|
-
}],
|
|
232
|
-
},
|
|
243
|
+
}]),
|
|
233
244
|
});
|
|
234
245
|
return result.spec;
|
|
235
246
|
}
|
|
@@ -875,6 +886,8 @@ test('auto authoring reaches readiness only after promotion, validation, groundi
|
|
|
875
886
|
assert.ok(validation.errors.some((error) => error.includes('readiness requires validated')));
|
|
876
887
|
assert.ok(validation.errors.some((error) => error.includes('requires completed semantic review')));
|
|
877
888
|
|
|
889
|
+
spec.ready_for_implementation = false;
|
|
890
|
+
fs.writeFileSync(fixture.specPath, `${JSON.stringify(spec, null, 2)}\n`);
|
|
878
891
|
spec = makeCanonicalReady(fixture);
|
|
879
892
|
validation = VALIDATOR.validateSpec(fixture.specDir);
|
|
880
893
|
assert.deepEqual(validation.errors, [], validation.errors.join('\n'));
|
|
@@ -1025,6 +1038,31 @@ test('done is a final-state request bound to current semantic model and digest',
|
|
|
1025
1038
|
assert.equal(current.ok, true, current.reason);
|
|
1026
1039
|
assert.equal(current.semanticDigest, spec.validation.semantic_review.semantic_digest);
|
|
1027
1040
|
|
|
1041
|
+
const failedReceipt = structuredClone(spec);
|
|
1042
|
+
failedReceipt.validation.semantic_review.verdict = 'FAIL';
|
|
1043
|
+
const receiptMutation = FINAL_STATE.validateCanonicalFinalState({
|
|
1044
|
+
policy: POLICY,
|
|
1045
|
+
projectRoot: fixture.root,
|
|
1046
|
+
candidate: { ...candidate, spec: failedReceipt },
|
|
1047
|
+
dependencies,
|
|
1048
|
+
});
|
|
1049
|
+
assert.equal(receiptMutation.ok, false);
|
|
1050
|
+
assert.match(receiptMutation.reason, /PASS CONTINUE/);
|
|
1051
|
+
|
|
1052
|
+
const mismatchedHistory = structuredClone(spec);
|
|
1053
|
+
const latest = mismatchedHistory.validation.semantic_review_history.entries[
|
|
1054
|
+
mismatchedHistory.validation.semantic_review_history.entries.length - 1
|
|
1055
|
+
];
|
|
1056
|
+
latest.review_receipt_digest = `sha256:${'0'.repeat(64)}`;
|
|
1057
|
+
const digestMismatch = FINAL_STATE.validateCanonicalFinalState({
|
|
1058
|
+
policy: POLICY,
|
|
1059
|
+
projectRoot: fixture.root,
|
|
1060
|
+
candidate: { ...candidate, spec: mismatchedHistory },
|
|
1061
|
+
dependencies,
|
|
1062
|
+
});
|
|
1063
|
+
assert.equal(digestMismatch.ok, false);
|
|
1064
|
+
assert.match(digestMismatch.reason, /review_receipt_digest|canonical receipt/);
|
|
1065
|
+
|
|
1028
1066
|
const requirementsPath = path.join(fixture.specDir, 'requirements.md');
|
|
1029
1067
|
const requirements = fs.readFileSync(requirementsPath, 'utf8');
|
|
1030
1068
|
fs.writeFileSync(requirementsPath, requirements.replace('invalid input shall return rejected state', 'invalid input shall return a deterministic rejected state'));
|