@haposoft/cafekit 0.18.0 → 0.18.2
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 +12 -1
- package/bin/__tests__/claude-settings.test.js +123 -0
- package/bin/__tests__/codex-ask-guard.test.js +123 -0
- package/bin/__tests__/codex-ask-projection.test.js +51 -0
- package/bin/__tests__/codex-hooks-ownership.test.js +80 -1
- package/bin/__tests__/codex-hooks.test.js +938 -237
- package/bin/__tests__/codex-native-hook-boundaries.test.js +136 -0
- package/bin/__tests__/codex-native.test.js +222 -149
- package/bin/__tests__/codex-projection-parity.test.js +314 -0
- package/bin/__tests__/codex-quiet-hooks.test.js +170 -0
- package/bin/__tests__/codex-research-projection.test.js +70 -0
- package/bin/__tests__/codex-specs-projection.test.js +76 -0
- package/bin/__tests__/develop-contract.test.js +102 -1362
- package/bin/__tests__/docs-sync-on-demand.test.js +25 -20
- package/bin/__tests__/inspect-paths.test.js +183 -0
- package/bin/__tests__/installer-safety.test.js +335 -0
- package/bin/__tests__/legacy-authority-decoupled.test.js +14 -19
- package/bin/__tests__/legacy-hooks-retired.test.js +18 -8
- package/bin/__tests__/omp-bridge.test.js +66 -12
- package/bin/__tests__/omp-hooks.test.js +11 -5
- package/bin/__tests__/orca-session.test.js +7 -0
- package/bin/__tests__/package-inventory.test.js +1 -395
- package/bin/__tests__/privacy-paths.test.js +317 -0
- package/bin/__tests__/spec-narrowing.test.js +30 -134
- package/bin/__tests__/specs-v2-execution-closeout.test.js +0 -372
- package/bin/__tests__/test-runtime-dependency-closure.cjs +1 -2
- package/bin/__tests__/usage-hook-retired.test.js +5 -3
- package/bin/__tests__/validator-grounding.test.js +17 -2929
- package/bin/install.js +25 -0
- package/bin/lib/codex-install.js +134 -12
- package/bin/lib/context.js +1 -1
- package/bin/lib/managed-writer.js +2 -3
- package/bin/phases/claude-runtime.js +91 -25
- package/bin/phases/claude-settings.js +22 -1
- package/bin/phases/codex-hooks.js +96 -33
- package/bin/phases/codex-runtime.js +45 -49
- package/bin/phases/copy-payload.js +11 -28
- package/bin/phases/omp-runtime.js +6 -3
- package/package.json +2 -3
- package/src/claude/CLAUDE.md +4 -0
- package/src/claude/agents/code-auditor.md +9 -6
- package/src/claude/agents/debugger.md +3 -9
- package/src/claude/agents/docs-keeper.md +4 -7
- package/src/claude/agents/git-ops.md +2 -4
- package/src/claude/agents/implementer.md +6 -7
- package/src/claude/agents/inspector.md +2 -0
- package/src/claude/agents/project-manager.md +2 -2
- package/src/claude/agents/spec-maker.md +4 -4
- package/src/claude/agents/test-runner.md +8 -11
- package/src/claude/agents/ui-ux-designer.md +0 -1
- package/src/claude/gitignore +3 -0
- package/src/claude/hooks/__tests__/installed-root.test.js +111 -0
- package/src/claude/hooks/__tests__/precompact.test.js +206 -10
- package/src/claude/hooks/__tests__/privacy-block.test.js +27 -0
- package/src/claude/hooks/__tests__/session-writeenv.test.js +25 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +311 -340
- package/src/claude/hooks/__tests__/spec-state-touched.test.js +230 -8
- package/src/claude/hooks/__tests__/state.test.js +207 -7
- package/src/claude/hooks/__tests__/update-check.test.js +88 -0
- package/src/claude/hooks/agent.cjs +7 -6
- package/src/claude/hooks/inspect-block.cjs +15 -63
- package/src/claude/hooks/lib/inspect-paths.cjs +304 -0
- package/src/claude/hooks/lib/parser.cjs +1 -1
- package/src/claude/hooks/lib/privacy-command-analysis.cjs +191 -17
- package/src/claude/hooks/lib/privacy-paths.cjs +313 -0
- package/src/claude/hooks/lib/project-root.cjs +15 -0
- package/src/claude/hooks/lib/rules-reservation.cjs +36 -0
- package/src/claude/hooks/lib/spec-touch.cjs +67 -0
- package/src/claude/hooks/lib/update-check.cjs +77 -0
- package/src/claude/hooks/precompact.cjs +8 -15
- package/src/claude/hooks/privacy-block.cjs +23 -205
- package/src/claude/hooks/rules.cjs +6 -28
- package/src/claude/hooks/session.cjs +34 -117
- package/src/claude/hooks/spec-gate.cjs +17 -89
- package/src/claude/hooks/spec-state.cjs +89 -96
- package/src/claude/hooks/state.cjs +96 -51
- package/src/claude/migration-manifest.json +20 -8
- package/src/claude/output-styles/coding-level-0-eli5.md +9 -9
- package/src/claude/output-styles/coding-level-1-junior.md +3 -3
- package/src/claude/output-styles/coding-level-2-mid.md +3 -3
- package/src/claude/output-styles/coding-level-3-senior.md +2 -2
- package/src/claude/output-styles/coding-level-4-lead.md +5 -5
- package/src/claude/output-styles/coding-level-5-god.md +6 -6
- package/src/claude/rules/ai-dev-rules.md +1 -2
- package/src/claude/rules/hook-protocols.md +1 -1
- package/src/claude/rules/workflow.md +1 -1
- package/src/claude/runtime.json +0 -3
- package/src/claude/scripts/generate-skill-catalog.cjs +2 -2
- package/src/claude/scripts/provenance.cjs +14 -0
- package/src/claude/scripts/spec-receipt.cjs +0 -114
- package/src/claude/scripts/spec-resolver.cjs +41 -457
- package/src/claude/scripts/workflow-policy.cjs +3 -1801
- package/src/claude/settings/settings.json +1 -1
- package/src/claude/skills/agent-browser/SKILL.md +1 -1
- package/src/claude/skills/ai-multimodal/scripts/check_setup.py +8 -68
- package/src/claude/skills/ai-multimodal/scripts/gemini_batch_process.py +6 -28
- package/src/claude/skills/chrome-devtools/SKILL.md +14 -31
- package/src/claude/skills/code-review/SKILL.md +25 -108
- package/src/claude/skills/code-review/references/spec-compliance-review.md +4 -7
- package/src/claude/skills/develop/SKILL.md +22 -70
- package/src/claude/skills/develop/references/subagent-patterns.md +0 -6
- package/src/claude/skills/git/SKILL.md +1 -1
- package/src/claude/skills/git/references/worktree-blueprint.md +1 -1
- package/src/claude/skills/research/SKILL.md +24 -55
- package/src/claude/skills/sync/SKILL.md +12 -34
- package/src/claude/skills/sync/references/rebind-and-audit.md +2 -2
- package/src/claude/skills/test/SKILL.md +39 -123
- package/src/claude/skills/test/references/execution-strategy.md +11 -22
- package/src/claude/skills/ui-ux/SKILL.md +1 -1
- package/src/claude/skills/ui-ux-pro-max/SKILL.md +24 -79
- package/src/claude/skills/web-testing/SKILL.md +2 -20
- package/src/codex/AGENTS.md +8 -0
- package/src/codex/hooks/ask-guard.cjs +54 -0
- package/src/codex/hooks/inspect-block.cjs +11 -7
- package/src/codex/hooks/lib/inspect-paths.cjs +1 -199
- package/src/codex/hooks/lib/privacy-paths.cjs +1 -0
- package/src/codex/hooks/lib/privacy-state.cjs +98 -16
- package/src/codex/hooks/lib/rules-reservation.cjs +38 -0
- package/src/codex/hooks/lib/secret-keywords.cjs +1 -0
- package/src/codex/hooks/lib/spec-receipt.cjs +2 -30
- package/src/codex/hooks/lib/spec-session-touch.cjs +100 -0
- package/src/codex/hooks/lib/spec-touch.cjs +3 -0
- package/src/codex/hooks/lib/spec-utils.cjs +6 -45
- package/src/codex/hooks/lib/update-check.cjs +1 -0
- package/src/codex/hooks/precompact.cjs +39 -0
- package/src/codex/hooks/privacy-approval.cjs +28 -17
- package/src/codex/hooks/privacy-block.cjs +9 -215
- package/src/codex/hooks/rules.cjs +4 -30
- package/src/codex/hooks/secret-output-guardrail.cjs +1 -1
- package/src/codex/hooks/session.cjs +47 -11
- package/src/codex/hooks/spec-gate.cjs +32 -74
- package/src/codex/hooks/spec-state.cjs +68 -46
- package/src/codex/hooks/state.cjs +25 -4
- package/src/codex/hooks.json +39 -7
- package/src/codex/rules/hook-protocols.md +13 -6
- package/src/common/AGENTS.md +4 -10
- package/src/omp/extensions/cafekit-bridge.mjs +12 -4
- package/src/omp/hooks/privacy-block.cjs +24 -206
- package/bin/__tests__/change-firewall.semantic-firewall.test.js +0 -2133
- package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +0 -342
- package/bin/__tests__/release-preflight.semantic-firewall.test.js +0 -586
- package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +0 -378
- package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +0 -665
- package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +0 -1132
- package/src/claude/hooks/lib/context.cjs +0 -527
- package/src/claude/hooks/lib/detect.cjs +0 -474
- package/src/claude/scripts/change-firewall.cjs +0 -1748
- package/src/claude/scripts/spec-authoring-digest.cjs +0 -151
- package/src/claude/scripts/spec-authoring-validation.cjs +0 -126
- package/src/claude/scripts/spec-final-state.cjs +0 -290
- package/src/claude/scripts/spec-ground.cjs +0 -650
- package/src/claude/scripts/spec-readiness.cjs +0 -425
- package/src/claude/scripts/spec-scaffold.cjs +0 -2317
- package/src/claude/scripts/spec-semantic-model.cjs +0 -396
- package/src/claude/scripts/validate-spec-output.cjs +0 -3914
- package/src/claude/skills/code-review/references/verification-gate.md +0 -41
- package/src/claude/skills/specs/templates/design.md +0 -82
- package/src/claude/skills/specs/templates/requirements-init.md +0 -8
- package/src/claude/skills/specs/templates/requirements.md +0 -42
- package/src/claude/skills/specs/templates/spec-state.json +0 -56
- package/src/claude/skills/specs/templates/task.md +0 -47
- package/src/codex/rules/state-sync.md +0 -60
|
@@ -4,6 +4,7 @@ const crypto = require('crypto');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const PATH_SAFETY = require('./runtime-path-safety.cjs');
|
|
7
|
+
const { displayNames } = require('./privacy-paths.cjs');
|
|
7
8
|
|
|
8
9
|
// Codex cannot raise an interactive prompt from a hook -- PreToolUse honours no
|
|
9
10
|
// "ask" decision -- so approval costs a full round trip: the agent has to relay
|
|
@@ -61,13 +62,6 @@ function samePathKeys(left, right) {
|
|
|
61
62
|
&& left.every((value, index) => value === right[index]);
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
function displayNames(filePaths) {
|
|
65
|
-
const names = [...new Set(filePaths.map((filePath) => path.basename(filePath)))].sort();
|
|
66
|
-
return names.length <= 3
|
|
67
|
-
? names.join(', ')
|
|
68
|
-
: `${names.slice(0, 3).join(', ')} (+${names.length - 3} more)`;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
65
|
function atomicWrite(dir, file, payload) {
|
|
72
66
|
PATH_SAFETY.assertComponentPath(dir, path.dirname(file));
|
|
73
67
|
const temp = `${file}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`;
|
|
@@ -84,14 +78,81 @@ function readJson(dir, file) {
|
|
|
84
78
|
}
|
|
85
79
|
}
|
|
86
80
|
|
|
81
|
+
function validPending(record, sessionId, requestId, now = Date.now()) {
|
|
82
|
+
return record?.requestId === requestId && record.sessionId === sessionId
|
|
83
|
+
&& Number.isFinite(record.expiresAt) && record.expiresAt > now
|
|
84
|
+
&& typeof record.displayName === 'string' && record.displayName.length > 0
|
|
85
|
+
&& Array.isArray(record.pathKeys) && record.pathKeys.length > 0
|
|
86
|
+
&& record.pathKeys.every(key => typeof key === 'string' && /^[a-f0-9]{64}$/.test(key));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function writeShortMarker(dir, record) {
|
|
90
|
+
const { requestId, sessionId, expiresAt } = record;
|
|
91
|
+
fs.writeFileSync(path.join(dir, `short-${hash(sessionId)}-${requestId}.json`),
|
|
92
|
+
`${JSON.stringify({ requestId, sessionId, expiresAt })}\n`,
|
|
93
|
+
{ encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Each prompt owns only markers it successfully renamed, never a shared mutable flag. */
|
|
97
|
+
function claimShortMarkers(projectRoot, sessionId) {
|
|
98
|
+
const requestIds = [];
|
|
99
|
+
if (typeof sessionId !== 'string' || !sessionId.trim()) return { ok: true, requestIds };
|
|
100
|
+
const dir = secureStateDir(projectRoot);
|
|
101
|
+
if (!dir) return { ok: true, requestIds };
|
|
102
|
+
const prefix = `short-${hash(sessionId)}-`;
|
|
103
|
+
let ok = true;
|
|
104
|
+
for (const name of fs.readdirSync(dir)) {
|
|
105
|
+
if (!name.startsWith(prefix) || !name.endsWith('.json')) continue;
|
|
106
|
+
const file = path.join(dir, name);
|
|
107
|
+
const claimed = path.join(dir, `claim-${crypto.randomBytes(12).toString('hex')}.tmp`);
|
|
108
|
+
const requestId = name.slice(prefix.length, -'.json'.length);
|
|
109
|
+
let moved = false;
|
|
110
|
+
try {
|
|
111
|
+
PATH_SAFETY.assertComponentPath(dir, file);
|
|
112
|
+
fs.renameSync(file, claimed);
|
|
113
|
+
moved = true;
|
|
114
|
+
requestIds.push(requestId);
|
|
115
|
+
PATH_SAFETY.assertComponentPath(dir, claimed);
|
|
116
|
+
const marker = JSON.parse(fs.readFileSync(claimed, 'utf8'));
|
|
117
|
+
if (!/^[a-f0-9]{24}$/.test(requestId) || marker.requestId !== requestId
|
|
118
|
+
|| marker.sessionId !== sessionId || !Number.isFinite(marker.expiresAt)
|
|
119
|
+
|| marker.expiresAt <= Date.now()) throw new Error('Invalid short approval marker');
|
|
120
|
+
} catch {
|
|
121
|
+
ok = false;
|
|
122
|
+
if (!moved) {
|
|
123
|
+
// A failed claim cancels the shortcut; deletion never counts as ownership.
|
|
124
|
+
try { fs.unlinkSync(file); }
|
|
125
|
+
catch {
|
|
126
|
+
let descriptor;
|
|
127
|
+
try {
|
|
128
|
+
PATH_SAFETY.canonicalRegularFile(dir, file, 'short approval marker');
|
|
129
|
+
descriptor = fs.openSync(file, fs.constants.O_WRONLY | fs.constants.O_NOFOLLOW);
|
|
130
|
+
fs.ftruncateSync(descriptor, 0);
|
|
131
|
+
} catch { /* no permission follows a failed invalidation */ }
|
|
132
|
+
finally {
|
|
133
|
+
if (descriptor !== undefined) {
|
|
134
|
+
try { fs.closeSync(descriptor); } catch { /* already rejected */ }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} finally {
|
|
140
|
+
if (moved) {
|
|
141
|
+
try { fs.unlinkSync(claimed); } catch { ok = false; }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { ok, requestIds };
|
|
146
|
+
}
|
|
147
|
+
|
|
87
148
|
function cleanup(projectRoot, now = Date.now()) {
|
|
88
149
|
const dir = secureStateDir(projectRoot);
|
|
89
150
|
if (!dir) return;
|
|
90
151
|
for (const name of fs.readdirSync(dir)) {
|
|
91
|
-
if (!/^(pending|token)-[a-f0-9]
|
|
152
|
+
if (!/^(?:(?:pending|token)-[a-f0-9]+|short-[a-f0-9]{64}-[a-f0-9]{24})\.json$/.test(name)) continue;
|
|
92
153
|
const file = path.join(dir, name);
|
|
93
154
|
const record = readJson(dir, file);
|
|
94
|
-
if (!record || Number(record.expiresAt) <= now) {
|
|
155
|
+
if (!record || !Number.isFinite(record.expiresAt) || record.expiresAt <= now) {
|
|
95
156
|
try {
|
|
96
157
|
fs.unlinkSync(file);
|
|
97
158
|
} catch {
|
|
@@ -101,13 +162,20 @@ function cleanup(projectRoot, now = Date.now()) {
|
|
|
101
162
|
}
|
|
102
163
|
}
|
|
103
164
|
|
|
104
|
-
function clearState(projectRoot) {
|
|
165
|
+
function clearState(projectRoot, sessionId) {
|
|
105
166
|
try {
|
|
167
|
+
if (typeof sessionId !== 'string' || !sessionId.trim()) return;
|
|
106
168
|
const dir = secureStateDir(projectRoot);
|
|
107
169
|
if (!dir) return;
|
|
108
170
|
for (const name of fs.readdirSync(dir)) {
|
|
171
|
+
if (name.startsWith(`short-${hash(sessionId)}-`) && name.endsWith('.json')) {
|
|
172
|
+
try { fs.unlinkSync(path.join(dir, name)); } catch { /* concurrently claimed */ }
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
109
175
|
if (/^(pending|token)-[a-f0-9]+\.json$/.test(name)) {
|
|
110
|
-
|
|
176
|
+
const file = path.join(dir, name);
|
|
177
|
+
if (readJson(dir, file)?.sessionId !== sessionId) continue;
|
|
178
|
+
try { fs.unlinkSync(file); } catch { /* concurrently consumed */ }
|
|
111
179
|
}
|
|
112
180
|
}
|
|
113
181
|
} catch {
|
|
@@ -123,26 +191,38 @@ function createPending({
|
|
|
123
191
|
filePath
|
|
124
192
|
}) {
|
|
125
193
|
cleanup(projectRoot);
|
|
126
|
-
if (!sessionId) throw new Error('A Codex session ID is required');
|
|
194
|
+
if (typeof sessionId !== 'string' || !sessionId.trim()) throw new Error('A Codex session ID is required');
|
|
127
195
|
const paths = requestPaths(filePaths, filePath);
|
|
128
196
|
if (paths.length === 0) throw new Error('At least one sensitive path is required');
|
|
197
|
+
const pathKeys = requestPathKeys(paths, sessionCwd);
|
|
198
|
+
const dir = secureStateDir(projectRoot, true);
|
|
199
|
+
for (const name of fs.readdirSync(dir)) {
|
|
200
|
+
const match = name.match(/^pending-([a-f0-9]{24})\.json$/);
|
|
201
|
+
if (!match) continue;
|
|
202
|
+
const pending = readJson(dir, path.join(dir, name));
|
|
203
|
+
if (validPending(pending, sessionId, match[1]) && samePathKeys(pending.pathKeys, pathKeys)) {
|
|
204
|
+
try { writeShortMarker(dir, pending); }
|
|
205
|
+
catch (error) { if (error.code !== 'EEXIST') throw error; }
|
|
206
|
+
return pending;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
129
209
|
const requestId = crypto.randomBytes(12).toString('hex');
|
|
130
210
|
const record = {
|
|
131
211
|
version: 2,
|
|
132
212
|
requestId,
|
|
133
213
|
sessionId: String(sessionId || ''),
|
|
134
|
-
pathKeys
|
|
214
|
+
pathKeys,
|
|
135
215
|
displayName: displayNames(paths),
|
|
136
216
|
expiresAt: Date.now() + TTL_MS
|
|
137
217
|
};
|
|
138
|
-
const dir = secureStateDir(projectRoot, true);
|
|
139
218
|
atomicWrite(dir, path.join(dir, `pending-${requestId}.json`), record);
|
|
219
|
+
writeShortMarker(dir, record);
|
|
140
220
|
return record;
|
|
141
221
|
}
|
|
142
222
|
|
|
143
223
|
function approvePending({ projectRoot, sessionId, requestId }) {
|
|
144
224
|
cleanup(projectRoot);
|
|
145
|
-
if (!sessionId) return { ok: false, reason: 'session' };
|
|
225
|
+
if (typeof sessionId !== 'string' || !sessionId.trim()) return { ok: false, reason: 'session' };
|
|
146
226
|
if (!/^[a-f0-9]{24}$/.test(requestId)) return { ok: false, reason: 'invalid' };
|
|
147
227
|
|
|
148
228
|
const dir = secureStateDir(projectRoot);
|
|
@@ -152,6 +232,7 @@ function approvePending({ projectRoot, sessionId, requestId }) {
|
|
|
152
232
|
if (!record) return { ok: false, reason: 'missing' };
|
|
153
233
|
if (record.sessionId !== String(sessionId || '')) return { ok: false, reason: 'session' };
|
|
154
234
|
if (Number(record.expiresAt) <= Date.now()) return { ok: false, reason: 'expired' };
|
|
235
|
+
if (!validPending(record, sessionId, requestId)) return { ok: false, reason: 'invalid' };
|
|
155
236
|
|
|
156
237
|
try {
|
|
157
238
|
fs.unlinkSync(pendingFile);
|
|
@@ -218,5 +299,6 @@ module.exports = {
|
|
|
218
299
|
approvePending,
|
|
219
300
|
hasGrant,
|
|
220
301
|
cleanup,
|
|
221
|
-
clearState
|
|
302
|
+
clearState,
|
|
303
|
+
claimShortMarkers
|
|
222
304
|
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { hookStateDir } = require('./hook-context.cjs');
|
|
7
|
+
|
|
8
|
+
function reservationFile(projectRoot, sessionId) {
|
|
9
|
+
const key = crypto.createHash('sha256')
|
|
10
|
+
.update(String(sessionId || 'unknown'))
|
|
11
|
+
.digest('hex')
|
|
12
|
+
.slice(0, 16);
|
|
13
|
+
return path.join(hookStateDir(projectRoot), `rules-${key}.json`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Reserve one injection slot per session. Returns null on reservation errors. */
|
|
17
|
+
function reserveSession(projectRoot, sessionId) {
|
|
18
|
+
if (!sessionId) return null;
|
|
19
|
+
try {
|
|
20
|
+
const file = reservationFile(projectRoot, sessionId);
|
|
21
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
22
|
+
fs.writeFileSync(file, JSON.stringify({ sessionId: String(sessionId) }), {
|
|
23
|
+
encoding: 'utf8',
|
|
24
|
+
flag: 'wx',
|
|
25
|
+
mode: 0o600
|
|
26
|
+
});
|
|
27
|
+
return true;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
return error.code === 'EEXIST' ? false : null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function clearSessionReservation(projectRoot, sessionId) {
|
|
34
|
+
if (!sessionId) return;
|
|
35
|
+
try { fs.rmSync(reservationFile(projectRoot, sessionId), { force: true }); } catch { /* fail-open */ }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { reserveSession, clearSessionReservation };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('../../../claude/hooks/lib/secret-keywords.cjs');
|
|
@@ -37,8 +37,8 @@ function loadSharedReceipt() {
|
|
|
37
37
|
'spec-receipt.cjs',
|
|
38
38
|
'spec-receipt.cjs',
|
|
39
39
|
(receipt) => {
|
|
40
|
-
if (typeof receipt?.
|
|
41
|
-
throw new Error('shared spec receipt helper has no
|
|
40
|
+
if (typeof receipt?.checkWorkflowTaskReceipt !== 'function') {
|
|
41
|
+
throw new Error('shared spec receipt helper has no checkWorkflowTaskReceipt function');
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
'shared spec receipt helper is missing'
|
|
@@ -55,7 +55,6 @@ function receiptHelper() {
|
|
|
55
55
|
return loaded.receipt;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
function evidenceBody(text) { return receiptHelper().evidenceBody(text, getSharedPolicy() || {}); }
|
|
59
58
|
function safeTaskFile(featureDir, taskPath) {
|
|
60
59
|
const result = receiptHelper().safeRead(featureDir, taskPath);
|
|
61
60
|
return result.status === 'ok' ? result.path : null;
|
|
@@ -64,11 +63,6 @@ function validateCanonicalReceipt(body, options = {}) {
|
|
|
64
63
|
const validate = getSharedValidate();
|
|
65
64
|
return validate ? validate(body, options) : ['shared_validator'];
|
|
66
65
|
}
|
|
67
|
-
function checkReceiptDetails(featureDir, taskPath, task, runtimeContext) {
|
|
68
|
-
const policy = getSharedPolicy();
|
|
69
|
-
if (!policy) return { failures: ['shared_validator'], status: 'missing' };
|
|
70
|
-
return receiptHelper().checkTaskReceipt(featureDir, taskPath, task, runtimeContext, policy);
|
|
71
|
-
}
|
|
72
66
|
function checkWorkflowReceiptDetails(featureDir, taskPath, runtimeContext) {
|
|
73
67
|
const policy = getSharedPolicy();
|
|
74
68
|
const receipt = receiptHelper();
|
|
@@ -85,40 +79,18 @@ function checkWorkflowReceiptSet(candidates, projectRoot, runtimeSession) {
|
|
|
85
79
|
}
|
|
86
80
|
return receipt.checkWorkflowReceiptSet(candidates, projectRoot, runtimeSession, policy);
|
|
87
81
|
}
|
|
88
|
-
function checkReceipt(featureDir, taskPath, task, runtimeContext) {
|
|
89
|
-
const map = {
|
|
90
|
-
unsafe_path: 'a', missing_receipt: 'b', task_status: 'a', completed_at: 'd',
|
|
91
|
-
verification_state: 'c', placeholder: 'c', validator_unavailable: 'c',
|
|
92
|
-
command: 'e', command_identity: 'e', exit_result: 'f', provenance: 'g',
|
|
93
|
-
artifact_hash: 'h', artifact_declaration: 'h',
|
|
94
|
-
};
|
|
95
|
-
return [...new Set(checkReceiptDetails(featureDir, taskPath, task, runtimeContext).failures.map((failure) => map[failure] || failure))];
|
|
96
|
-
}
|
|
97
|
-
function checkFeatureReceipt(featureDir, runtimeContext) {
|
|
98
|
-
const policy = getSharedPolicy();
|
|
99
|
-
if (!policy) return { failures: ['shared_validator'], status: 'missing' };
|
|
100
|
-
return receiptHelper().checkFeatureReceipt(featureDir, runtimeContext, policy);
|
|
101
|
-
}
|
|
102
82
|
function receiptFixHint(failures, body) {
|
|
103
83
|
const helper = receiptHelper();
|
|
104
84
|
return typeof helper.receiptFixHint === 'function' ? helper.receiptFixHint(failures, body) : null;
|
|
105
85
|
}
|
|
106
|
-
function readTaskProof(featureDir, taskPath) {
|
|
107
|
-
return receiptHelper().readTaskProof(featureDir, taskPath, getSharedPolicy() || {});
|
|
108
|
-
}
|
|
109
86
|
|
|
110
87
|
module.exports = {
|
|
111
|
-
checkFeatureReceipt,
|
|
112
|
-
checkReceipt,
|
|
113
|
-
checkReceiptDetails,
|
|
114
88
|
checkWorkflowReceiptDetails,
|
|
115
89
|
checkWorkflowReceiptSet,
|
|
116
|
-
evidenceBody,
|
|
117
90
|
getSharedPolicy,
|
|
118
91
|
getSharedValidate,
|
|
119
92
|
loadSharedPolicy,
|
|
120
93
|
loadSharedReceipt,
|
|
121
|
-
readTaskProof,
|
|
122
94
|
receiptFixHint,
|
|
123
95
|
safeTaskFile,
|
|
124
96
|
validateCanonicalReceipt,
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { packetAt, namedPackets, explicitFailure } = require('./spec-touch.cjs');
|
|
7
|
+
const { atomicWrite, assertConfiguredSpecsPath, hookStateDir } = require('./hook-context.cjs');
|
|
8
|
+
|
|
9
|
+
function sessionIdentity(payload) {
|
|
10
|
+
return [payload.session_id, payload.sessionId, payload.sessionID, payload.session?.id]
|
|
11
|
+
.find((value) => typeof value === 'string' && value.trim()) || null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function patchPaths(command) {
|
|
15
|
+
if (typeof command !== 'string') return [];
|
|
16
|
+
const lines = command.trim().split(/\r?\n/);
|
|
17
|
+
if (lines.shift() !== '*** Begin Patch' || lines.pop() !== '*** End Patch') return [];
|
|
18
|
+
const paths = [];
|
|
19
|
+
let operation = null;
|
|
20
|
+
let body = false;
|
|
21
|
+
for (const line of lines) {
|
|
22
|
+
const header = line.match(/^\*\*\* (Add|Update|Delete) File: (.+)$/);
|
|
23
|
+
if (header) {
|
|
24
|
+
if (operation && operation !== 'Delete' && !body) return [];
|
|
25
|
+
operation = header[1]; body = operation === 'Delete';
|
|
26
|
+
paths.push(header[2]);
|
|
27
|
+
} else if (/^\*\*\* Move to: .+/.test(line) && operation === 'Update' && !body) {
|
|
28
|
+
paths.push(line.slice('*** Move to: '.length));
|
|
29
|
+
} else if (operation === 'Add' && /^\+/.test(line)) {
|
|
30
|
+
body = true;
|
|
31
|
+
} else if (operation === 'Update' && /^(?:@@|[ +\-]|\*\*\* End of File$)/.test(line)) {
|
|
32
|
+
body = true;
|
|
33
|
+
} else {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return operation && body ? paths : [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function responseText(response) {
|
|
41
|
+
if (typeof response === 'string') return response;
|
|
42
|
+
if (!response || typeof response !== 'object') return '';
|
|
43
|
+
const value = response.output ?? response.result ?? response.message ?? response.content;
|
|
44
|
+
if (typeof value === 'string') return value;
|
|
45
|
+
return Array.isArray(value) ? value.map((item) => item?.text || '').join('\n') : '';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function successfulMutation(payload) {
|
|
49
|
+
const response = payload.tool_response;
|
|
50
|
+
if (payload.hook_event_name !== 'PostToolUse' || explicitFailure(payload) || response == null) return false;
|
|
51
|
+
const text = responseText(response).trim();
|
|
52
|
+
if (/^(?:error\b|failed\b|failure\b|apply_patch verification failed)/im.test(text)) return false;
|
|
53
|
+
// Contract: github.com/openai/codex/blob/main/codex-rs/core/src/tools/context.rs
|
|
54
|
+
// Native ApplyPatchToolOutput emits a string summary to PostToolUse, whereas its
|
|
55
|
+
// code-mode return is {}. Unknown/empty output is not observed successful mutation.
|
|
56
|
+
const nativeSuccess = /^Success\. Updated the following files:\r?\n[AMD] .+/m.test(text);
|
|
57
|
+
if (payload.tool_name === 'apply_patch') return nativeSuccess;
|
|
58
|
+
return nativeSuccess || response?.ok === true || response?.success === true
|
|
59
|
+
|| /^(?:success\b|.*\bupdated successfully[.!]?\s*$)/i.test(text);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function mutationPaths(payload) {
|
|
63
|
+
if (!successfulMutation(payload)) return [];
|
|
64
|
+
const input = payload.tool_input || {};
|
|
65
|
+
if (payload.tool_name === 'apply_patch') return patchPaths(input.command);
|
|
66
|
+
if (!['Edit', 'Write', 'MultiEdit'].includes(payload.tool_name)) return [];
|
|
67
|
+
return [input.file_path, input.path, ...(Array.isArray(input.edits) ? input.edits.map((edit) => edit?.file_path || edit?.path) : [])]
|
|
68
|
+
.filter((value) => typeof value === 'string');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function touchSession(payload, context) {
|
|
72
|
+
const sessionId = sessionIdentity(payload);
|
|
73
|
+
const specsRoot = assertConfiguredSpecsPath(context.projectRoot, context.runtime);
|
|
74
|
+
if (!sessionId) return { sessionId, names: [], touched: [], specsRoot };
|
|
75
|
+
const key = crypto.createHash('sha256').update(`${context.projectRoot}\0${sessionId}`).digest('hex');
|
|
76
|
+
const file = path.join(hookStateDir(context.projectRoot), `spec-touched-${key}.json`);
|
|
77
|
+
let previous = [];
|
|
78
|
+
try { previous = JSON.parse(fs.readFileSync(file, 'utf8')); } catch { /* first touch */ }
|
|
79
|
+
const touched = new Set(Array.isArray(previous) ? previous.filter((name) => typeof name === 'string') : []);
|
|
80
|
+
let names;
|
|
81
|
+
if (payload.hook_event_name === 'PostToolUse') {
|
|
82
|
+
names = mutationPaths(payload).map((value) => packetAt(value, specsRoot, context.sessionCwd)).filter(Boolean);
|
|
83
|
+
} else {
|
|
84
|
+
let packets = [];
|
|
85
|
+
try { packets = fs.readdirSync(specsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name); } catch { /* absent root */ }
|
|
86
|
+
names = namedPackets(payload.prompt || payload.user_prompt, packets, specsRoot, context.projectRoot);
|
|
87
|
+
const feature = payload.explicitFeature ?? payload.featureName ?? payload.feature;
|
|
88
|
+
if (typeof feature === 'string' && feature.trim() && !/[\\/]/.test(feature) && !['.', '..', '_shared'].includes(feature)) names.push(feature);
|
|
89
|
+
const explicitPath = payload.explicitPath ?? payload.specPath ?? payload.spec_path ?? payload.featurePath;
|
|
90
|
+
const fromPath = packetAt(explicitPath, specsRoot, context.projectRoot);
|
|
91
|
+
if (fromPath) names.push(fromPath);
|
|
92
|
+
}
|
|
93
|
+
for (const name of names) touched.add(name);
|
|
94
|
+
if (names.length) {
|
|
95
|
+
try { atomicWrite(file, `${JSON.stringify([...touched].sort())}\n`); } catch { /* no shared fallback */ }
|
|
96
|
+
}
|
|
97
|
+
return { sessionId, names: [...new Set(names)], touched: [...touched], specsRoot };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = { touchSession, sessionIdentity };
|
|
@@ -66,8 +66,8 @@ function sharedResolver() {
|
|
|
66
66
|
// Use canonical realpath for require to avoid symlink hijack
|
|
67
67
|
const realCandidate = canonicalRealpath(candidate);
|
|
68
68
|
const resolver = require(realCandidate);
|
|
69
|
-
if (typeof resolver?.
|
|
70
|
-
throw new Error('shared spec resolver has no
|
|
69
|
+
if (typeof resolver?.resolveWorkflowCandidate !== 'function') {
|
|
70
|
+
throw new Error('shared spec resolver has no resolveWorkflowCandidate function');
|
|
71
71
|
}
|
|
72
72
|
return resolver;
|
|
73
73
|
} catch (error) {
|
|
@@ -80,31 +80,13 @@ function specsDirectory(projectRoot, runtime) {
|
|
|
80
80
|
return sharedResolver().specsDirectory(projectRoot, runtime);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
function
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
function findAllSpecCandidates(projectRoot, runtime) {
|
|
88
|
-
return sharedResolver().findAllSpecCandidates(projectRoot, runtime);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function resolveActiveSpec(projectRoot, runtime, explicitFeature, explicitPath) {
|
|
92
|
-
if (projectRoot && typeof projectRoot === 'object') {
|
|
93
|
-
return sharedResolver().resolveActiveSpec(projectRoot);
|
|
94
|
-
}
|
|
95
|
-
return sharedResolver().resolveActiveSpec({
|
|
96
|
-
projectRoot,
|
|
97
|
-
runtime,
|
|
98
|
-
explicitFeature,
|
|
99
|
-
explicitPath,
|
|
100
|
-
});
|
|
83
|
+
function findLegacyPackets(projectRoot, runtime) {
|
|
84
|
+
const resolver = sharedResolver();
|
|
85
|
+
return typeof resolver.findLegacyPackets === 'function' ? resolver.findLegacyPackets(projectRoot, runtime) : [];
|
|
101
86
|
}
|
|
102
87
|
|
|
103
88
|
function resolveWorkflowCandidate(projectRoot, runtime, explicitFeature, explicitPath) {
|
|
104
89
|
const resolver = sharedResolver();
|
|
105
|
-
if (typeof resolver.resolveWorkflowCandidate !== 'function') {
|
|
106
|
-
return resolveActiveSpec(projectRoot, runtime, explicitFeature, explicitPath);
|
|
107
|
-
}
|
|
108
90
|
if (projectRoot && typeof projectRoot === 'object') {
|
|
109
91
|
return resolver.resolveWorkflowCandidate(projectRoot);
|
|
110
92
|
}
|
|
@@ -123,23 +105,6 @@ function refineWorkflowGateResolution(resolved) {
|
|
|
123
105
|
: resolved;
|
|
124
106
|
}
|
|
125
107
|
|
|
126
|
-
function findActiveSpec(projectRoot, runtime) {
|
|
127
|
-
const resolved = resolveActiveSpec(projectRoot, runtime);
|
|
128
|
-
if (!resolved) return null;
|
|
129
|
-
if (resolved.error === 'multiple_active' || resolved.error === 'invalid_specs') return resolved;
|
|
130
|
-
if (resolved.error) return null;
|
|
131
|
-
return resolved;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function taskStatusMap(spec) {
|
|
135
|
-
return Object.fromEntries(
|
|
136
|
-
Object.entries(spec.task_registry || {}).map(([taskPath, task]) => [
|
|
137
|
-
taskPath,
|
|
138
|
-
task?.status || 'pending',
|
|
139
|
-
]),
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
108
|
function extractExplicitTarget(...sources) {
|
|
144
109
|
const resolver = sharedResolver();
|
|
145
110
|
if (typeof resolver.extractExplicitTarget !== 'function') return null;
|
|
@@ -153,14 +118,10 @@ function readActiveFeatureTarget(options) {
|
|
|
153
118
|
}
|
|
154
119
|
|
|
155
120
|
module.exports = {
|
|
156
|
-
findActiveSpec,
|
|
157
121
|
extractExplicitTarget,
|
|
122
|
+
findLegacyPackets,
|
|
158
123
|
readActiveFeatureTarget,
|
|
159
|
-
findAllActiveSpecs,
|
|
160
|
-
findAllSpecCandidates,
|
|
161
|
-
resolveActiveSpec,
|
|
162
124
|
resolveWorkflowCandidate,
|
|
163
125
|
refineWorkflowGateResolution,
|
|
164
126
|
specsDirectory,
|
|
165
|
-
taskStatusMap,
|
|
166
127
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('../../../claude/hooks/lib/update-check.cjs');
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { execFileSync } = require('child_process');
|
|
7
|
+
const { PROJECT_ROOT, atomicWrite, hookStateDir, logCrash, readPayload } = require('./lib/hook-context.cjs');
|
|
8
|
+
|
|
9
|
+
function gitLine(args) {
|
|
10
|
+
try {
|
|
11
|
+
return execFileSync('git', args, {
|
|
12
|
+
cwd: PROJECT_ROOT, encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'ignore'],
|
|
13
|
+
}).trim() || null;
|
|
14
|
+
} catch { return null; }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const payload = readPayload();
|
|
19
|
+
if (!payload) process.exit(0);
|
|
20
|
+
const sessionId = [payload.session_id, payload.sessionId, payload.sessionID, payload.session?.id]
|
|
21
|
+
.find(value => typeof value === 'string' && value.trim());
|
|
22
|
+
if (!sessionId) process.exit(0);
|
|
23
|
+
const status = gitLine(['status', '--porcelain']);
|
|
24
|
+
// These anchors are re-derivable facts, never proof or authorization.
|
|
25
|
+
const record = {
|
|
26
|
+
captured_at: new Date().toISOString(),
|
|
27
|
+
session_id: sessionId,
|
|
28
|
+
trigger: typeof payload.trigger === 'string' ? payload.trigger : null,
|
|
29
|
+
project_root: PROJECT_ROOT,
|
|
30
|
+
branch: gitLine(['rev-parse', '--abbrev-ref', 'HEAD']),
|
|
31
|
+
head: gitLine(['rev-parse', '--short', 'HEAD']),
|
|
32
|
+
dirty_count: status === null ? null : status.split('\n').filter(line => line.trim()).length,
|
|
33
|
+
};
|
|
34
|
+
const key = crypto.createHash('sha256').update(sessionId).digest('hex');
|
|
35
|
+
atomicWrite(path.join(hookStateDir(PROJECT_ROOT), `compact-recovery-${key}.json`), `${JSON.stringify(record)}\n`);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (error.message === 'hook payload is empty') process.exit(0);
|
|
38
|
+
logCrash('precompact', error);
|
|
39
|
+
}
|
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
const {
|
|
5
|
-
APPROVAL_PREFIX,
|
|
6
|
-
approvePending,
|
|
7
|
-
cleanup
|
|
8
|
-
} = require('./lib/privacy-state.cjs');
|
|
9
|
-
const {
|
|
10
|
-
getHookContext,
|
|
11
|
-
readPayload
|
|
12
|
-
} = require('./lib/hook-context.cjs');
|
|
13
|
-
|
|
14
4
|
function context(message) {
|
|
15
5
|
process.stdout.write(`${JSON.stringify({
|
|
16
6
|
hookSpecificOutput: {
|
|
@@ -20,10 +10,20 @@ function context(message) {
|
|
|
20
10
|
})}\n`);
|
|
21
11
|
}
|
|
22
12
|
|
|
13
|
+
let projectRoot, sessionId, state;
|
|
23
14
|
try {
|
|
15
|
+
const { PROJECT_ROOT, readPayload } = require('./lib/hook-context.cjs');
|
|
24
16
|
const data = readPayload();
|
|
25
17
|
if (!data) process.exit(0);
|
|
26
|
-
|
|
18
|
+
projectRoot = PROJECT_ROOT;
|
|
19
|
+
sessionId = data.session_id;
|
|
20
|
+
let claimed = { ok: false, requestIds: [] };
|
|
21
|
+
try {
|
|
22
|
+
state = require('./lib/privacy-state.cjs');
|
|
23
|
+
claimed = state.claimShortMarkers(projectRoot, sessionId);
|
|
24
|
+
} catch { /* the outer catch retries invalidation, never approval */ }
|
|
25
|
+
if (!claimed.ok) throw new Error('Short approval markers could not be claimed safely');
|
|
26
|
+
const { APPROVAL_PREFIX, approvePending, cleanup } = state;
|
|
27
27
|
// A pasted prompt arrives with whatever whitespace the terminal added around
|
|
28
28
|
// it. Rejecting on a trailing newline made the approval look broken while the
|
|
29
29
|
// id it named was still perfectly valid; the exactness that matters is the
|
|
@@ -31,20 +31,25 @@ try {
|
|
|
31
31
|
const prompt = String(data.prompt || '').trim();
|
|
32
32
|
cleanup(projectRoot);
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
if (!
|
|
34
|
+
const shortApproval = new Set(['y', 'yes', 'có']).has(prompt.normalize('NFC').toLowerCase());
|
|
35
|
+
if (!shortApproval && !prompt.startsWith(APPROVAL_PREFIX)) process.exit(0);
|
|
36
|
+
if (typeof sessionId !== 'string' || !sessionId.trim()) {
|
|
36
37
|
context('CafeKit privacy approval rejected: this hook invocation had no Codex session ID.');
|
|
37
38
|
process.exit(0);
|
|
38
39
|
}
|
|
39
|
-
|
|
40
|
-
if (
|
|
40
|
+
let requestId;
|
|
41
|
+
if (shortApproval) {
|
|
42
|
+
if (claimed.requestIds.length !== 1) process.exit(0);
|
|
43
|
+
requestId = claimed.requestIds[0];
|
|
44
|
+
} else requestId = prompt.slice(APPROVAL_PREFIX.length);
|
|
45
|
+
if (!/^[a-f0-9]{24}$/.test(requestId)) {
|
|
41
46
|
context('CafeKit privacy approval rejected: the prompt did not exactly match a pending request.');
|
|
42
47
|
process.exit(0);
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
const result = approvePending({
|
|
46
51
|
projectRoot,
|
|
47
|
-
sessionId
|
|
52
|
+
sessionId,
|
|
48
53
|
requestId
|
|
49
54
|
});
|
|
50
55
|
if (!result.ok) {
|
|
@@ -54,10 +59,16 @@ try {
|
|
|
54
59
|
|
|
55
60
|
context(
|
|
56
61
|
`User approved the pending sensitive access (${result.displayName}). ` +
|
|
57
|
-
`The grant is session-bound, bound to exactly those paths, and expires in one hour
|
|
62
|
+
`The grant is session-bound, bound to exactly those paths, and expires in one hour. ` +
|
|
63
|
+
`Tell the user which paths were just granted (${result.displayName}) before reading them.`
|
|
58
64
|
);
|
|
59
65
|
process.exit(0);
|
|
60
66
|
} catch {
|
|
67
|
+
try {
|
|
68
|
+
if (projectRoot && typeof sessionId === 'string' && sessionId.trim()) {
|
|
69
|
+
(state || require('./lib/privacy-state.cjs')).claimShortMarkers(projectRoot, sessionId);
|
|
70
|
+
}
|
|
71
|
+
} catch { /* no approval follows an invalidation failure */ }
|
|
61
72
|
context('CafeKit privacy approval failed safely; no token was issued.');
|
|
62
73
|
process.exit(0);
|
|
63
74
|
}
|