@haposoft/cafekit 0.16.0-rc.3 → 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 +531 -29
- package/bin/__tests__/codex-native.test.js +228 -7
- package/bin/__tests__/develop-contract.test.js +653 -185
- package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +342 -0
- package/bin/__tests__/installer-safety.test.js +39 -0
- package/bin/__tests__/opencode-spec-gate.test.js +24 -16
- package/bin/__tests__/package-inventory.test.js +705 -13
- 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 +468 -0
- package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +665 -0
- package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +1132 -0
- package/bin/__tests__/test-runtime-dependency-closure.cjs +182 -0
- package/bin/__tests__/validator-grounding.test.js +2013 -221
- package/bin/lib/managed-writer.js +12 -3
- package/bin/phases/codex-runtime.js +17 -1
- package/package.json +5 -2
- package/src/claude/CLAUDE.md +6 -0
- package/src/claude/agents/code-auditor.md +20 -0
- package/src/claude/agents/spec-maker.md +143 -99
- package/src/claude/hooks/__tests__/completion-authority.test.js +321 -58
- package/src/claude/hooks/__tests__/privacy-block.test.js +254 -0
- package/src/claude/hooks/__tests__/semantic-review-authority.test.js +257 -0
- package/src/claude/hooks/__tests__/spec-gate.test.js +115 -40
- package/src/claude/hooks/completion-authority-check.cjs +37 -288
- package/src/claude/hooks/completion-authority-state.cjs +10 -8
- package/src/claude/hooks/completion-authority.cjs +13 -7
- package/src/claude/hooks/docs-sync.cjs +5 -2
- package/src/claude/hooks/lib/privacy-command-analysis.cjs +393 -0
- package/src/claude/hooks/lib/runtime-path-safety.cjs +119 -0
- package/src/claude/hooks/privacy-block.cjs +99 -28
- package/src/claude/hooks/semantic-review-authority.cjs +152 -0
- package/src/claude/hooks/spec-gate.cjs +47 -117
- package/src/claude/hooks/spec-state.cjs +8 -2
- package/src/claude/migration-manifest.json +9 -1
- package/src/claude/rules/state-sync.md +52 -21
- package/src/claude/rules/workflow.md +17 -9
- 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 +290 -0
- package/src/claude/scripts/spec-ground.cjs +571 -173
- package/src/claude/scripts/spec-readiness.cjs +425 -0
- package/src/claude/scripts/spec-receipt.cjs +162 -0
- package/src/claude/scripts/spec-resolver.cjs +67 -16
- package/src/claude/scripts/spec-scaffold.cjs +1594 -139
- package/src/claude/scripts/spec-semantic-model.cjs +396 -0
- package/src/claude/scripts/validate-spec-output.cjs +3110 -218
- package/src/claude/scripts/workflow-policy.cjs +501 -183
- package/src/claude/settings/settings.json +4 -0
- package/src/claude/skills/code-review/SKILL.md +12 -3
- package/src/claude/skills/code-review/references/spec-compliance-review.md +10 -2
- package/src/claude/skills/code-review/references/verification-gate.md +13 -0
- package/src/claude/skills/develop/SKILL.md +47 -60
- package/src/claude/skills/develop/references/parallel-waves.md +9 -6
- package/src/claude/skills/develop/references/quality-gate.md +19 -12
- package/src/claude/skills/specs/SKILL.md +183 -177
- package/src/claude/skills/specs/references/ask-user-question-gates.md +27 -18
- package/src/claude/skills/specs/references/cross-spec-dependency.md +8 -4
- package/src/claude/skills/specs/references/research-strategy.md +4 -1
- package/src/claude/skills/specs/references/review.md +214 -262
- package/src/claude/skills/specs/references/scope-inquiry.md +18 -5
- package/src/claude/skills/specs/rules/design-discovery-full.md +33 -81
- package/src/claude/skills/specs/rules/design-discovery-light.md +21 -42
- package/src/claude/skills/specs/rules/design-principles.md +136 -180
- package/src/claude/skills/specs/rules/design-review.md +42 -97
- package/src/claude/skills/specs/rules/ears-format.md +42 -50
- package/src/claude/skills/specs/rules/phase-decision-matrix.md +42 -41
- package/src/claude/skills/specs/rules/task-scoring-rubric.md +2 -1
- package/src/claude/skills/specs/rules/tasks-generation.md +189 -300
- package/src/claude/skills/specs/templates/design.md +49 -263
- package/src/claude/skills/specs/templates/requirements.md +26 -36
- package/src/claude/skills/specs/templates/research.md +9 -98
- package/src/claude/skills/specs/templates/spec-state.json +35 -85
- package/src/claude/skills/specs/templates/task.md +30 -102
- package/src/claude/skills/sync/SKILL.md +33 -10
- package/src/claude/skills/sync/references/sync-protocols.md +31 -13
- package/src/claude/skills/test/SKILL.md +17 -7
- package/src/codex/AGENTS.md +6 -0
- package/src/codex/hooks/completion-authority-check.cjs +43 -240
- package/src/codex/hooks/completion-authority-state.cjs +10 -8
- package/src/codex/hooks/completion-authority.cjs +13 -7
- package/src/codex/hooks/lib/hook-context.cjs +16 -2
- package/src/codex/hooks/lib/privacy-command-analysis.cjs +3 -0
- package/src/codex/hooks/lib/privacy-state.cjs +61 -38
- package/src/codex/hooks/lib/runtime-path-safety.cjs +3 -0
- package/src/codex/hooks/lib/spec-receipt.cjs +72 -134
- package/src/codex/hooks/lib/spec-utils.cjs +56 -5
- package/src/codex/hooks/privacy-approval.cjs +6 -2
- package/src/codex/hooks/privacy-block.cjs +94 -15
- package/src/codex/hooks/semantic-review-authority.cjs +130 -0
- package/src/codex/hooks/spec-gate.cjs +49 -34
- package/src/codex/hooks/spec-state.cjs +10 -3
- package/src/codex/hooks/state.cjs +2 -0
- package/src/codex/hooks.json +6 -1
- package/src/codex/rules/state-sync.md +47 -27
- package/src/common/AGENTS.md +1 -1
|
@@ -1,260 +1,63 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const crypto = require('crypto');
|
|
4
3
|
const fs = require('fs');
|
|
5
4
|
const path = require('path');
|
|
6
5
|
const STATE = require('./completion-authority-state.cjs');
|
|
7
|
-
const {
|
|
8
|
-
const
|
|
6
|
+
const { checkFeatureReceipt, checkReceiptDetails } = require('./lib/spec-receipt.cjs');
|
|
7
|
+
const RESOLVER = require('./lib/spec-utils.cjs');
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
function inside(root, target) {
|
|
15
|
-
const relative = path.relative(path.resolve(root), path.resolve(target));
|
|
16
|
-
return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
17
|
-
}
|
|
9
|
+
const installedScripts = path.join(__dirname, '..', 'scripts');
|
|
10
|
+
const scripts = fs.existsSync(path.join(installedScripts, 'spec-final-state.cjs'))
|
|
11
|
+
? installedScripts
|
|
12
|
+
: path.resolve(__dirname, '..', '..', 'claude', 'scripts');
|
|
18
13
|
|
|
19
|
-
function
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
14
|
+
function runtimeModule(root, name) {
|
|
15
|
+
const target = path.join(root, name);
|
|
16
|
+
const stat = fs.lstatSync(target);
|
|
17
|
+
if (stat.isSymbolicLink() || !stat.isFile()) throw new Error(`runtime dependency must be a regular non-symlink file: ${name}`);
|
|
18
|
+
const canonicalRoot = fs.realpathSync(root);
|
|
19
|
+
const canonical = fs.realpathSync(target);
|
|
20
|
+
const relative = path.relative(canonicalRoot, canonical);
|
|
21
|
+
if (relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) throw new Error(`runtime dependency escaped scripts root: ${name}`);
|
|
22
|
+
return require(canonical);
|
|
28
23
|
}
|
|
29
24
|
|
|
30
|
-
|
|
31
|
-
const target = path.resolve(featureDir, taskPath);
|
|
32
|
-
if (!inside(featureDir, target)) return null;
|
|
33
|
-
const bytes = readBoundFile(featureDir, target);
|
|
34
|
-
if (!bytes) return null;
|
|
35
|
-
return { bytes, text: bytes.toString('utf8'), path: fs.realpathSync(target) };
|
|
36
|
-
}
|
|
25
|
+
const FINAL_STATE = runtimeModule(scripts, 'spec-final-state.cjs');
|
|
37
26
|
|
|
38
|
-
function
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
for (const line of body.split('\n')) {
|
|
42
|
-
if (!/^\s*Artifacts?\s*:/i.test(line) && !/^\s*Artifact\s+produced\b/i.test(line)) continue;
|
|
43
|
-
const hashes = [...line.matchAll(/\b(?:artifact[_-])?sha-?256\s*:\s*([^\s),;`]+)/gi)].map((match) => match[1]);
|
|
44
|
-
const raw = line.replace(/^\s*Artifacts?\s*:\s*/i, '').replace(/^\s*Artifact\s+produced\s*:?[ \t]*/i, '').replace(/\b(?:artifact[_-])?sha-?256\s*:\s*[^\s),;`]+/gi, '').replace(/[()[\]{}]/g, ' ').trim();
|
|
45
|
-
const paths = raw.split(/\s*(?:\+|,|;|\band\b)\s*/i).map((value) => value.trim()).filter(Boolean);
|
|
46
|
-
paths.forEach((artifactPath, index) => references.push({ path: artifactPath, hash: hashes[index] || null }));
|
|
27
|
+
function explicitTarget(payload) {
|
|
28
|
+
for (const key of ['featureName', 'feature', 'explicitFeature']) {
|
|
29
|
+
if (Object.prototype.hasOwnProperty.call(payload, key)) return { explicitFeature: payload[key] };
|
|
47
30
|
}
|
|
48
|
-
|
|
31
|
+
for (const key of ['specPath', 'spec_path', 'featurePath']) {
|
|
32
|
+
if (Object.prototype.hasOwnProperty.call(payload, key)) return { explicitPath: payload[key] };
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
49
35
|
}
|
|
50
36
|
|
|
51
37
|
function resolveCandidate({ projectRoot, runtime, payload }) {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
const candidates = findAllSpecCandidates(projectRoot, runtime);
|
|
38
|
+
const target = explicitTarget(payload);
|
|
39
|
+
if (target) return RESOLVER.resolveActiveSpec({ projectRoot, runtime, ...target });
|
|
40
|
+
const candidates = RESOLVER.findAllSpecCandidates(projectRoot, runtime);
|
|
55
41
|
if (candidates.length > 1) return { error: 'multiple_persisted', candidates: candidates.map((candidate) => candidate.featureName) };
|
|
56
|
-
|
|
57
|
-
if (explicitFeature || explicitPath) return { error: 'explicit_not_found', explicitFeature, explicitPath, reason: 'no globally persisted spec matches the explicit target' };
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
if (!explicitFeature && !explicitPath) return candidates[0];
|
|
61
|
-
const targeted = resolveActiveSpec({ projectRoot, runtime, explicitFeature, explicitPath });
|
|
62
|
-
if (!targeted || targeted.error) {
|
|
63
|
-
return {
|
|
64
|
-
error: 'explicit_mismatch',
|
|
65
|
-
explicitFeature,
|
|
66
|
-
explicitPath,
|
|
67
|
-
reason: targeted?.reason || 'explicit target does not resolve to the persisted candidate',
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
if (targeted.featureName !== candidates[0].featureName || path.resolve(targeted.specFile) !== path.resolve(candidates[0].specFile)) {
|
|
71
|
-
return {
|
|
72
|
-
error: 'explicit_mismatch',
|
|
73
|
-
explicitFeature,
|
|
74
|
-
explicitPath,
|
|
75
|
-
reason: 'explicit target does not match the sole globally persisted candidate',
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
return candidates[0];
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function policyRank(policy, lane) {
|
|
82
|
-
const lanes = Array.isArray(policy.LANES) ? policy.LANES : ['Direct', 'Standard', 'Critical'];
|
|
83
|
-
return lanes.indexOf(lane);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function policyMonotonicFailures(policy, baseline, current) {
|
|
87
|
-
const failures = [];
|
|
88
|
-
if (baseline.version !== current.version) failures.push('workflow_policy.version changed');
|
|
89
|
-
if (policyRank(policy, current.lane) < policyRank(policy, baseline.lane)) failures.push(`workflow_policy.lane downgraded from ${baseline.lane} to ${current.lane}`);
|
|
90
|
-
if (policyRank(policy, current.automatic_lane) < policyRank(policy, baseline.automatic_lane)) failures.push(`workflow_policy.automatic_lane downgraded from ${baseline.automatic_lane} to ${current.automatic_lane}`);
|
|
91
|
-
const currentRisks = new Set(current.risks);
|
|
92
|
-
for (const risk of baseline.risks) if (!currentRisks.has(risk)) failures.push(`workflow_policy risk ${risk} was removed`);
|
|
93
|
-
const currentObligations = new Set(current.proof_obligations);
|
|
94
|
-
for (const obligation of baseline.proof_obligations) if (!currentObligations.has(obligation)) failures.push(`workflow_policy obligation ${obligation} was removed`);
|
|
95
|
-
return failures;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function mergePolicyFloor(policy, floor, current) {
|
|
99
|
-
const merged = { ...current };
|
|
100
|
-
if (policyRank(policy, floor.lane) > policyRank(policy, current.lane)) merged.lane = floor.lane;
|
|
101
|
-
if (policyRank(policy, floor.automatic_lane) > policyRank(policy, current.automatic_lane)) merged.automatic_lane = floor.automatic_lane;
|
|
102
|
-
merged.risks = [...new Set([...current.risks, ...floor.risks])];
|
|
103
|
-
const proofObligations = [];
|
|
104
|
-
const actorNeeds = [];
|
|
105
|
-
for (const source of [current, floor]) {
|
|
106
|
-
for (let index = 0; index < source.proof_obligations.length; index += 1) {
|
|
107
|
-
const obligation = source.proof_obligations[index];
|
|
108
|
-
if (proofObligations.includes(obligation)) continue;
|
|
109
|
-
proofObligations.push(obligation);
|
|
110
|
-
actorNeeds.push(source.actor_needs[index]);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
merged.proof_obligations = proofObligations;
|
|
114
|
-
merged.actor_needs = actorNeeds;
|
|
115
|
-
return merged;
|
|
42
|
+
return candidates[0] || null;
|
|
116
43
|
}
|
|
117
44
|
|
|
118
|
-
function
|
|
119
|
-
if (!
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
let identity;
|
|
126
|
-
try {
|
|
127
|
-
identity = {
|
|
128
|
-
project_root: fs.realpathSync(projectRoot),
|
|
129
|
-
spec_file: fs.realpathSync(candidate.specFile),
|
|
130
|
-
feature_name: candidate.featureName,
|
|
131
|
-
};
|
|
132
|
-
state.ensureKey();
|
|
133
|
-
let authorityState = state.readState(projectRoot);
|
|
134
|
-
if (authorityState.malformed || authorityState.keyError || authorityState.keyMissing) {
|
|
135
|
-
return { ok: false, reason: authorityState.reason || 'completion authority state is unavailable' };
|
|
136
|
-
}
|
|
137
|
-
if (!authorityState.floor) {
|
|
138
|
-
if (authorityState.baselines.length > 0 || authorityState.pending || authorityState.grant) {
|
|
139
|
-
return { ok: false, reason: 'project policy floor is missing for persisted authority state' };
|
|
140
|
-
}
|
|
141
|
-
state.writePolicyFloor(projectRoot, current);
|
|
142
|
-
authorityState = state.readState(projectRoot);
|
|
143
|
-
if (authorityState.malformed || authorityState.keyError || authorityState.keyMissing || !authorityState.floor) {
|
|
144
|
-
return { ok: false, reason: authorityState.reason || 'project policy floor could not be persisted' };
|
|
145
|
-
}
|
|
146
|
-
} else {
|
|
147
|
-
const floorValidation = policy.validateWorkflowPolicySnapshot(authorityState.floor.policy);
|
|
148
|
-
if (!floorValidation.valid) return { ok: false, reason: `persisted project policy floor is malformed: ${floorValidation.errors.join('; ')}` };
|
|
149
|
-
const floorFailures = policyMonotonicFailures(policy, authorityState.floor.policy, current);
|
|
150
|
-
if (floorFailures.length) return { ok: false, reason: `project policy floor is monotonic and cannot be downgraded (${floorFailures.join('; ')})` };
|
|
151
|
-
const mergedFloor = mergePolicyFloor(policy, authorityState.floor.policy, current);
|
|
152
|
-
if (state.digest(authorityState.floor.policy) !== state.digest(mergedFloor)) state.writePolicyFloor(projectRoot, mergedFloor);
|
|
153
|
-
}
|
|
154
|
-
const baseline = state.readBaseline(projectRoot, identity);
|
|
155
|
-
if (!baseline) {
|
|
156
|
-
state.writeBaseline(projectRoot, identity, current);
|
|
157
|
-
return { ok: true };
|
|
158
|
-
}
|
|
159
|
-
const baselineValidation = policy.validateWorkflowPolicySnapshot(baseline.policy);
|
|
160
|
-
if (!baselineValidation.valid) return { ok: false, reason: `persisted workflow policy baseline is malformed: ${baselineValidation.errors.join('; ')}` };
|
|
161
|
-
const failures = policyMonotonicFailures(policy, baseline.policy, current);
|
|
162
|
-
if (failures.length) return { ok: false, reason: `workflow_policy baseline is monotonic and cannot be downgraded (${failures.join('; ')})` };
|
|
163
|
-
if (state.digest(baseline.policy) !== state.digest(current)) state.writeBaseline(projectRoot, identity, current);
|
|
164
|
-
return { ok: true };
|
|
165
|
-
} catch (error) {
|
|
166
|
-
return { ok: false, reason: `workflow_policy baseline unavailable: ${error.message}` };
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function evaluateCloseout({ policy, projectRoot, runtime, payload }) {
|
|
171
|
-
const candidate = resolveCandidate({ projectRoot, runtime, payload });
|
|
172
|
-
if (!candidate) return { ok: true, candidate: null };
|
|
173
|
-
if (candidate.error) {
|
|
174
|
-
const detail = Array.isArray(candidate.candidates) ? candidate.candidates.join(', ') : (candidate.reason || candidate.error);
|
|
175
|
-
return { ok: false, candidate, reason: `${candidate.error}: ${detail}` };
|
|
176
|
-
}
|
|
177
|
-
const sessionId = payload.session_id;
|
|
178
|
-
if (typeof sessionId !== 'string' || !sessionId.trim()) return { ok: false, candidate, reason: 'trusted host session ID is unavailable' };
|
|
179
|
-
const baseline = observePolicyBaseline({ STATE, policy, projectRoot, candidate });
|
|
180
|
-
if (!baseline.ok) return { ok: false, candidate, reason: baseline.reason };
|
|
181
|
-
if (runtime.spec?.completion_gate !== undefined && runtime.spec.completion_gate !== true) return { ok: false, candidate, reason: 'runtime.spec.completion_gate is worker-writable and cannot authorize completion' };
|
|
182
|
-
const status = candidate.spec.status;
|
|
183
|
-
if (!['in_progress', 'in-progress', 'completed', 'complete'].includes(status)) return { ok: false, candidate, reason: `spec status is invalid: ${String(status)}` };
|
|
184
|
-
const registry = candidate.spec.task_registry;
|
|
185
|
-
if (!plain(registry) || Object.keys(registry).length === 0) return { ok: false, candidate, reason: 'task_registry is missing or empty' };
|
|
186
|
-
|
|
187
|
-
let runtimeContext;
|
|
188
|
-
try {
|
|
189
|
-
runtimeContext = policy.deriveRuntimeContext({
|
|
190
|
-
projectRoot,
|
|
191
|
-
specsRoot: candidate.specsDir,
|
|
192
|
-
specFile: candidate.specFile,
|
|
193
|
-
featureName: candidate.featureName,
|
|
194
|
-
runtimeSession: sessionId,
|
|
195
|
-
});
|
|
196
|
-
} catch (error) {
|
|
197
|
-
return { ok: false, candidate, reason: `runtime provenance unavailable: ${error.message}` };
|
|
198
|
-
}
|
|
199
|
-
if (!policy.isTrustedRuntimeContext(runtimeContext)) return { ok: false, candidate, reason: 'fresh trusted runtime Base/Head/context is unavailable' };
|
|
200
|
-
|
|
201
|
-
const featureDir = path.join(candidate.specsDir, candidate.featureName);
|
|
202
|
-
const receiptBodies = new Map();
|
|
203
|
-
const taskFiles = [];
|
|
204
|
-
const failures = [];
|
|
205
|
-
for (const [taskPath, task] of Object.entries(registry)) {
|
|
206
|
-
if (!plain(task) || task.status !== 'done') {
|
|
207
|
-
failures.push(`${taskPath}: status is not done`);
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
|
-
if (policy.isStaleFlashDone(task)) failures.push(`${taskPath}: FLASH_UNVERIFIED`);
|
|
211
|
-
const file = readTask(featureDir, taskPath);
|
|
212
|
-
const body = file ? evidenceBody(file.text) : null;
|
|
213
|
-
if (body !== null) receiptBodies.set(taskPath, body);
|
|
214
|
-
const receiptFailures = checkReceipt(featureDir, taskPath, task, runtimeContext);
|
|
215
|
-
if (receiptFailures.length) failures.push(`${taskPath}: checks ${receiptFailures.join(',')}`);
|
|
216
|
-
if (file) taskFiles.push({ path: taskPath, digest: crypto.createHash('sha256').update(file.bytes).digest('hex') });
|
|
217
|
-
}
|
|
218
|
-
if (failures.length) return { ok: false, candidate, reason: `technical completion proof is incomplete (${failures.slice(0, 4).join('; ')})` };
|
|
219
|
-
|
|
220
|
-
const firstTask = Object.keys(registry).find((taskPath) => receiptBodies.has(taskPath));
|
|
221
|
-
let completion;
|
|
222
|
-
try {
|
|
223
|
-
completion = policy.completionDecisionForSpec(candidate.spec, {
|
|
224
|
-
runtimeContext,
|
|
225
|
-
executionReceipt: firstTask ? receiptBodies.get(firstTask) : null,
|
|
226
|
-
taskContext: firstTask ? registry[firstTask] : null,
|
|
227
|
-
});
|
|
228
|
-
} catch (error) {
|
|
229
|
-
return { ok: false, candidate, reason: `workflow completion proof could not be evaluated: ${error.message}` };
|
|
230
|
-
}
|
|
231
|
-
if (completion.completion !== 'complete' && completion.completion !== 'not_applicable') {
|
|
232
|
-
return { ok: false, candidate, reason: completion.blocker || 'workflow completion proof is incomplete' };
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const artifactRefs = [...receiptBodies.values()].flatMap(artifactReferences).sort((left, right) => `${left.path}:${left.hash}`.localeCompare(`${right.path}:${right.hash}`));
|
|
236
|
-
const artifactFiles = artifactRefs.map((reference) => ({
|
|
237
|
-
...reference,
|
|
238
|
-
digest: crypto.createHash('sha256').update(readBoundFile(projectRoot, reference.path) || Buffer.from('<missing>')).digest('hex'),
|
|
239
|
-
}));
|
|
240
|
-
const specBytes = readBoundFile(projectRoot, candidate.specFile);
|
|
241
|
-
const binding = {
|
|
242
|
-
project_root: runtimeContext.project_root,
|
|
243
|
-
platform: process.platform,
|
|
244
|
-
session_id: sessionId,
|
|
245
|
-
feature_name: candidate.featureName,
|
|
246
|
-
spec_file: candidate.specFile,
|
|
247
|
-
runtime: { base: runtimeContext.base, head: runtimeContext.head, context_id: runtimeContext.context_id },
|
|
248
|
-
spec_digest: STATE.digest(candidate.spec),
|
|
249
|
-
spec_bytes_digest: crypto.createHash('sha256').update(specBytes || Buffer.from('<missing>')).digest('hex'),
|
|
250
|
-
policy_digest: STATE.digest(candidate.spec.workflow_policy ?? null),
|
|
251
|
-
task_registry_digest: STATE.digest(registry),
|
|
252
|
-
task_digest: STATE.digest(taskFiles),
|
|
253
|
-
artifact_digest: STATE.digest(artifactFiles),
|
|
254
|
-
provenance_digest: STATE.digest({ runtime: runtimeContext, receipts: [...receiptBodies.values()].map((body) => body.match(/^\s*(?:Base|Head|base_sha|head_sha)\s*:.*$/gim) || []) }),
|
|
45
|
+
function dependencies(overrides = {}) {
|
|
46
|
+
if (!overrides.grounder) runtimeModule(scripts, 'spec-semantic-model.cjs');
|
|
47
|
+
const grounder = overrides.grounder || runtimeModule(scripts, 'spec-ground.cjs');
|
|
48
|
+
return {
|
|
49
|
+
validator: overrides.validator || runtimeModule(scripts, 'validate-spec-output.cjs'),
|
|
50
|
+
grounder,
|
|
51
|
+
semanticAuthority: overrides.semanticAuthority || require('./semantic-review-authority.cjs'),
|
|
255
52
|
};
|
|
256
|
-
binding.binding_digest = STATE.digest(binding);
|
|
257
|
-
return { ok: true, candidate, runtimeContext, binding };
|
|
258
53
|
}
|
|
259
54
|
|
|
260
|
-
module.exports = {
|
|
55
|
+
module.exports = FINAL_STATE.createAuthority({
|
|
56
|
+
state: STATE,
|
|
57
|
+
resolveCandidate,
|
|
58
|
+
dependencies,
|
|
59
|
+
receipts: {
|
|
60
|
+
task: (featureDir, taskPath, task, context) => checkReceiptDetails(featureDir, taskPath, task, context),
|
|
61
|
+
feature: (featureDir, context) => checkFeatureReceipt(featureDir, context),
|
|
62
|
+
},
|
|
63
|
+
});
|
|
@@ -10,7 +10,8 @@ const NONCE_RE = /^[a-f0-9]{24}$/;
|
|
|
10
10
|
const HASH_RE = /^[a-f0-9]{64}$/;
|
|
11
11
|
const TTL_MS = 5 * 60 * 1000;
|
|
12
12
|
const KEY_BYTES = 32;
|
|
13
|
-
const SCHEMA_VERSION =
|
|
13
|
+
const SCHEMA_VERSION = 3;
|
|
14
|
+
const LEGACY_SCHEMA_VERSION = 2;
|
|
14
15
|
const EPHEMERAL_FILE_RE = /^(pending|grant)-([a-f0-9]{24})\.json$/;
|
|
15
16
|
const BASELINE_FILE_RE = /^baseline-([a-f0-9]{64})\.json$/;
|
|
16
17
|
const FLOOR_FILE = 'policy-floor.json';
|
|
@@ -222,7 +223,7 @@ function validateEphemeral(record, kind, nonce, now, key) {
|
|
|
222
223
|
: ['schema_version', 'kind', 'nonce', 'binding', 'issued_at', 'expires_at', 'approved_at', 'mac'];
|
|
223
224
|
if (!plain(record) || !exactKeys(record, expected)) return { ok: false, reason: 'malformed authority record' };
|
|
224
225
|
if (!macMatches(record, key)) return { ok: false, reason: 'authority record MAC is invalid' };
|
|
225
|
-
if (record.schema_version
|
|
226
|
+
if (![LEGACY_SCHEMA_VERSION, SCHEMA_VERSION].includes(record.schema_version) || record.kind !== kind || record.nonce !== nonce || !NONCE_RE.test(nonce)) return { ok: false, reason: 'authority record identity is invalid' };
|
|
226
227
|
if (!plain(record.binding) || typeof record.binding.session_id !== 'string' || !record.binding.session_id.trim()) return { ok: false, reason: 'authority record binding is invalid' };
|
|
227
228
|
if (!validTimestamp(record.issued_at) || !Number.isFinite(record.expires_at) || record.expires_at <= Date.parse(record.issued_at)) return { ok: false, reason: 'authority record timestamps are invalid' };
|
|
228
229
|
if (kind === 'grant' && !validTimestamp(record.approved_at)) return { ok: false, reason: 'authority grant approval timestamp is invalid' };
|
|
@@ -234,7 +235,7 @@ function validateBaseline(record, filename, projectRoot, key) {
|
|
|
234
235
|
const expected = ['schema_version', 'kind', 'identity', 'policy', 'issued_at', 'updated_at', 'mac'];
|
|
235
236
|
if (!plain(record) || !exactKeys(record, expected)) return { ok: false, reason: 'malformed policy baseline' };
|
|
236
237
|
if (!macMatches(record, key)) return { ok: false, reason: 'policy baseline MAC is invalid' };
|
|
237
|
-
if (record.schema_version
|
|
238
|
+
if (![LEGACY_SCHEMA_VERSION, SCHEMA_VERSION].includes(record.schema_version) || record.kind !== 'baseline' || !validTimestamp(record.issued_at) || !validTimestamp(record.updated_at)) return { ok: false, reason: 'policy baseline identity or timestamps are invalid' };
|
|
238
239
|
let identity;
|
|
239
240
|
try { identity = normalizeIdentity(record.identity, { allowMissingSpec: true }); } catch (error) { return { ok: false, reason: error.message }; }
|
|
240
241
|
if (filename !== `baseline-${identityDigest(identity)}.json` || identity.project_root !== canonicalProjectRoot(projectRoot)) return { ok: false, reason: 'policy baseline namespace is invalid' };
|
|
@@ -246,7 +247,7 @@ function validatePolicyFloor(record, filename, key) {
|
|
|
246
247
|
const expected = ['schema_version', 'kind', 'policy', 'issued_at', 'updated_at', 'mac'];
|
|
247
248
|
if (!plain(record) || !exactKeys(record, expected)) return { ok: false, reason: 'malformed project policy floor' };
|
|
248
249
|
if (!macMatches(record, key)) return { ok: false, reason: 'project policy floor MAC is invalid' };
|
|
249
|
-
if (filename !== FLOOR_FILE || record.schema_version
|
|
250
|
+
if (filename !== FLOOR_FILE || ![LEGACY_SCHEMA_VERSION, SCHEMA_VERSION].includes(record.schema_version) || record.kind !== 'policy-floor' || !validTimestamp(record.issued_at) || !validTimestamp(record.updated_at)) {
|
|
250
251
|
return { ok: false, reason: 'project policy floor identity or timestamps are invalid' };
|
|
251
252
|
}
|
|
252
253
|
if (!plain(record.policy)) return { ok: false, reason: 'project policy floor policy is malformed' };
|
|
@@ -344,10 +345,6 @@ function readState(projectRoot, now = Date.now()) {
|
|
|
344
345
|
}
|
|
345
346
|
if (!state.floor) {
|
|
346
347
|
state.floorMissing = true;
|
|
347
|
-
if (state.pending || state.grant || state.baselines.length > 0) {
|
|
348
|
-
state.malformed = true;
|
|
349
|
-
state.reason ||= 'project policy floor is missing';
|
|
350
|
-
}
|
|
351
348
|
}
|
|
352
349
|
return state;
|
|
353
350
|
}
|
|
@@ -509,6 +506,11 @@ module.exports = {
|
|
|
509
506
|
FLOOR_FILE,
|
|
510
507
|
stableStringify,
|
|
511
508
|
digest,
|
|
509
|
+
canonicalProjectRoot,
|
|
510
|
+
atomicWrite,
|
|
511
|
+
readKey,
|
|
512
|
+
recordMac,
|
|
513
|
+
macMatches,
|
|
512
514
|
userStateRoot,
|
|
513
515
|
stateDir,
|
|
514
516
|
clearState,
|
|
@@ -45,6 +45,7 @@ function approve(payload) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
function stop(payload) {
|
|
48
|
+
if (payload.stop_hook_active === true) return;
|
|
48
49
|
const loaded = loadSharedPolicy();
|
|
49
50
|
if (!loaded.policy) {
|
|
50
51
|
emitBlock(`Completion authority unavailable: shared workflow policy could not be loaded (${loaded.error.message}). Repair ${loaded.path} before completing tasks`);
|
|
@@ -69,6 +70,7 @@ function stop(payload) {
|
|
|
69
70
|
}
|
|
70
71
|
return;
|
|
71
72
|
}
|
|
73
|
+
if (result.active === false) return;
|
|
72
74
|
if (!result.ok) {
|
|
73
75
|
STATE.clearState(projectRoot);
|
|
74
76
|
emitBlock(`Completion authority: ${result.reason}`);
|
|
@@ -84,13 +86,17 @@ let mode = process.argv[2] || null;
|
|
|
84
86
|
try {
|
|
85
87
|
const payload = readPayload();
|
|
86
88
|
mode = mode || (payload.hook_event_name === 'UserPromptSubmit' ? '--approve' : '--stop');
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
89
|
+
if (mode === '--stop' && payload.stop_hook_active === true) {
|
|
90
|
+
// Host loop prevention is always silent, including when dependencies are unavailable.
|
|
91
|
+
} else {
|
|
92
|
+
const dependencyError = loadAuthority();
|
|
93
|
+
if (dependencyError) {
|
|
94
|
+
if (mode === '--approve') context(`CafeKit completion approval failed safely; authority dependency unavailable (${dependencyError.message}).`);
|
|
95
|
+
else emitBlock(`Completion authority unavailable: ${dependencyError.message}. Repair the installed authority before completing tasks`);
|
|
96
|
+
} else if (mode === '--approve') approve(payload);
|
|
97
|
+
else if (mode === '--stop') stop(payload);
|
|
98
|
+
else throw new Error('completion authority mode is required');
|
|
99
|
+
}
|
|
94
100
|
} catch (error) {
|
|
95
101
|
if (mode === '--approve') context(`CafeKit completion approval failed safely; no grant was issued (${error.message}).`);
|
|
96
102
|
else {
|
|
@@ -5,7 +5,18 @@ const fs = require('fs');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
|
|
7
7
|
// Installed layout: <project>/.codex/hooks/lib/hook-context.cjs
|
|
8
|
-
|
|
8
|
+
// Canonicalize installed project root via realpath where available; lexical fallback keeps source-tree tests working.
|
|
9
|
+
// This ensures a symlink project root (e.g., /tmp/link -> /real/project) is resolved to its real location,
|
|
10
|
+
// keeping assertConfiguredSpecsPath and state paths consistent with resolveSessionCwd's realpath.
|
|
11
|
+
// Installed-root authority stays PROJECT_ROOT (now canonical); do not change without updating state/approval paths.
|
|
12
|
+
const PROJECT_ROOT = (() => {
|
|
13
|
+
const lexical = path.resolve(__dirname, '..', '..', '..');
|
|
14
|
+
try {
|
|
15
|
+
return fs.realpathSync(lexical);
|
|
16
|
+
} catch {
|
|
17
|
+
return lexical;
|
|
18
|
+
}
|
|
19
|
+
})();
|
|
9
20
|
|
|
10
21
|
function readPayload() {
|
|
11
22
|
const raw = fs.readFileSync(0, 'utf8').trim();
|
|
@@ -104,7 +115,10 @@ function atomicWrite(file, content, mode = 0o600) {
|
|
|
104
115
|
|
|
105
116
|
function logCrash(hook, error) {
|
|
106
117
|
try {
|
|
107
|
-
const
|
|
118
|
+
const isSourceTree = PROJECT_ROOT.includes(`${path.sep}packages${path.sep}spec${path.sep}src`);
|
|
119
|
+
const dir = isSourceTree
|
|
120
|
+
? path.join(require('os').tmpdir(), 'cafekit-hook-logs', 'codex', hook)
|
|
121
|
+
: path.join(PROJECT_ROOT, '.codex', 'hooks', '.logs');
|
|
108
122
|
fs.mkdirSync(dir, { recursive: true });
|
|
109
123
|
fs.appendFileSync(
|
|
110
124
|
path.join(dir, 'hook-log.jsonl'),
|
|
@@ -3,12 +3,36 @@
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
const PATH_SAFETY = require('./runtime-path-safety.cjs');
|
|
7
|
+
|
|
8
|
+
// Codex cannot raise an interactive prompt from a hook -- PreToolUse honours no
|
|
9
|
+
// "ask" decision -- so approval costs a full round trip: the agent has to relay
|
|
10
|
+
// a request id and the user has to type it back. A five-minute window expired
|
|
11
|
+
// mid-conversation, leaving the agent repeating a request id that could no
|
|
12
|
+
// longer be approved. An hour outlives a working session's train of thought.
|
|
13
|
+
const TTL_MS = 60 * 60 * 1000;
|
|
8
14
|
const APPROVAL_PREFIX = 'APPROVE CAFEKIT PRIVACY ';
|
|
9
15
|
function stateDir(projectRoot) {
|
|
10
16
|
return path.join(projectRoot, '.codex', 'hooks', '.privacy');
|
|
11
17
|
}
|
|
18
|
+
function secureStateDir(projectRoot, create = false) {
|
|
19
|
+
const root = PATH_SAFETY.canonicalDirectory(projectRoot, 'privacy project root');
|
|
20
|
+
const hooks = path.join(root, '.codex', 'hooks');
|
|
21
|
+
try { PATH_SAFETY.assertComponentPath(root, hooks); }
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (!create && error.code === 'ENOENT') return null;
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
const dir = path.join(hooks, '.privacy');
|
|
27
|
+
if (create && !fs.existsSync(dir)) fs.mkdirSync(dir, { mode: 0o700 });
|
|
28
|
+
try { PATH_SAFETY.assertComponentPath(root, dir); }
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (!create && error.code === 'ENOENT') return null;
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
if (!fs.lstatSync(dir).isDirectory()) throw new Error('privacy state must be a regular directory');
|
|
34
|
+
return dir;
|
|
35
|
+
}
|
|
12
36
|
function hash(value) {
|
|
13
37
|
return crypto.createHash('sha256').update(String(value)).digest('hex');
|
|
14
38
|
}
|
|
@@ -44,16 +68,16 @@ function displayNames(filePaths) {
|
|
|
44
68
|
: `${names.slice(0, 3).join(', ')} (+${names.length - 3} more)`;
|
|
45
69
|
}
|
|
46
70
|
|
|
47
|
-
function atomicWrite(file, payload) {
|
|
48
|
-
|
|
49
|
-
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
71
|
+
function atomicWrite(dir, file, payload) {
|
|
72
|
+
PATH_SAFETY.assertComponentPath(dir, path.dirname(file));
|
|
50
73
|
const temp = `${file}.${process.pid}.${crypto.randomBytes(6).toString('hex')}.tmp`;
|
|
51
74
|
fs.writeFileSync(temp, `${JSON.stringify(payload)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
52
75
|
fs.renameSync(temp, file);
|
|
53
76
|
}
|
|
54
77
|
|
|
55
|
-
function readJson(file) {
|
|
78
|
+
function readJson(dir, file) {
|
|
56
79
|
try {
|
|
80
|
+
PATH_SAFETY.assertComponentPath(dir, file);
|
|
57
81
|
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
58
82
|
} catch {
|
|
59
83
|
return null;
|
|
@@ -61,12 +85,12 @@ function readJson(file) {
|
|
|
61
85
|
}
|
|
62
86
|
|
|
63
87
|
function cleanup(projectRoot, now = Date.now()) {
|
|
64
|
-
const dir =
|
|
65
|
-
if (!
|
|
88
|
+
const dir = secureStateDir(projectRoot);
|
|
89
|
+
if (!dir) return;
|
|
66
90
|
for (const name of fs.readdirSync(dir)) {
|
|
67
91
|
if (!/^(pending|token)-[a-f0-9]+\.json$/.test(name)) continue;
|
|
68
92
|
const file = path.join(dir, name);
|
|
69
|
-
const record = readJson(file);
|
|
93
|
+
const record = readJson(dir, file);
|
|
70
94
|
if (!record || Number(record.expiresAt) <= now) {
|
|
71
95
|
try {
|
|
72
96
|
fs.unlinkSync(file);
|
|
@@ -78,9 +102,9 @@ function cleanup(projectRoot, now = Date.now()) {
|
|
|
78
102
|
}
|
|
79
103
|
|
|
80
104
|
function clearState(projectRoot) {
|
|
81
|
-
const dir = stateDir(projectRoot);
|
|
82
105
|
try {
|
|
83
|
-
|
|
106
|
+
const dir = secureStateDir(projectRoot);
|
|
107
|
+
if (!dir) return;
|
|
84
108
|
for (const name of fs.readdirSync(dir)) {
|
|
85
109
|
if (/^(pending|token)-[a-f0-9]+\.json$/.test(name)) {
|
|
86
110
|
try { fs.unlinkSync(path.join(dir, name)); } catch { /* concurrently consumed */ }
|
|
@@ -96,8 +120,7 @@ function createPending({
|
|
|
96
120
|
sessionCwd,
|
|
97
121
|
sessionId,
|
|
98
122
|
filePaths,
|
|
99
|
-
filePath
|
|
100
|
-
toolName
|
|
123
|
+
filePath
|
|
101
124
|
}) {
|
|
102
125
|
cleanup(projectRoot);
|
|
103
126
|
if (!sessionId) throw new Error('A Codex session ID is required');
|
|
@@ -110,10 +133,10 @@ function createPending({
|
|
|
110
133
|
sessionId: String(sessionId || ''),
|
|
111
134
|
pathKeys: requestPathKeys(paths, sessionCwd),
|
|
112
135
|
displayName: displayNames(paths),
|
|
113
|
-
toolName: String(toolName || ''),
|
|
114
136
|
expiresAt: Date.now() + TTL_MS
|
|
115
137
|
};
|
|
116
|
-
|
|
138
|
+
const dir = secureStateDir(projectRoot, true);
|
|
139
|
+
atomicWrite(dir, path.join(dir, `pending-${requestId}.json`), record);
|
|
117
140
|
return record;
|
|
118
141
|
}
|
|
119
142
|
|
|
@@ -122,8 +145,10 @@ function approvePending({ projectRoot, sessionId, requestId }) {
|
|
|
122
145
|
if (!sessionId) return { ok: false, reason: 'session' };
|
|
123
146
|
if (!/^[a-f0-9]{24}$/.test(requestId)) return { ok: false, reason: 'invalid' };
|
|
124
147
|
|
|
125
|
-
const
|
|
126
|
-
|
|
148
|
+
const dir = secureStateDir(projectRoot);
|
|
149
|
+
if (!dir) return { ok: false, reason: 'missing' };
|
|
150
|
+
const pendingFile = path.join(dir, `pending-${requestId}.json`);
|
|
151
|
+
const record = readJson(dir, pendingFile);
|
|
127
152
|
if (!record) return { ok: false, reason: 'missing' };
|
|
128
153
|
if (record.sessionId !== String(sessionId || '')) return { ok: false, reason: 'session' };
|
|
129
154
|
if (Number(record.expiresAt) <= Date.now()) return { ok: false, reason: 'expired' };
|
|
@@ -134,55 +159,53 @@ function approvePending({ projectRoot, sessionId, requestId }) {
|
|
|
134
159
|
return { ok: false, reason: 'consumed' };
|
|
135
160
|
}
|
|
136
161
|
|
|
137
|
-
|
|
162
|
+
// The grant window starts when the user approves, not when the request was
|
|
163
|
+
// raised. Carrying the pending record's deadline over meant a late approval
|
|
164
|
+
// bought only the minutes left on it.
|
|
165
|
+
atomicWrite(dir, path.join(dir, `token-${requestId}.json`), {
|
|
138
166
|
version: 2,
|
|
139
167
|
requestId,
|
|
140
168
|
sessionId: record.sessionId,
|
|
141
169
|
pathKeys: record.pathKeys,
|
|
142
|
-
|
|
143
|
-
expiresAt: Math.min(record.expiresAt, Date.now() + TTL_MS)
|
|
170
|
+
expiresAt: Date.now() + TTL_MS
|
|
144
171
|
});
|
|
145
172
|
return { ok: true, displayName: record.displayName };
|
|
146
173
|
}
|
|
147
174
|
|
|
148
|
-
|
|
175
|
+
/**
|
|
176
|
+
* A grant is session-bound, path-bound and time-limited, but not single-use:
|
|
177
|
+
* one approval covers repeated access to the same paths until it expires.
|
|
178
|
+
* Consuming it on first use made every re-read a fresh round trip, and the tool
|
|
179
|
+
* name is not part of the identity either -- reading a file with `cat` and with
|
|
180
|
+
* a native read is the same access to the same secret.
|
|
181
|
+
*/
|
|
182
|
+
function hasGrant({
|
|
149
183
|
projectRoot,
|
|
150
184
|
sessionCwd,
|
|
151
185
|
sessionId,
|
|
152
186
|
filePaths,
|
|
153
|
-
filePath
|
|
154
|
-
toolName
|
|
187
|
+
filePath
|
|
155
188
|
}) {
|
|
156
189
|
cleanup(projectRoot);
|
|
157
190
|
if (!sessionId) return false;
|
|
158
|
-
const dir =
|
|
159
|
-
if (!
|
|
191
|
+
const dir = secureStateDir(projectRoot);
|
|
192
|
+
if (!dir) return false;
|
|
160
193
|
const paths = requestPaths(filePaths, filePath);
|
|
161
194
|
if (paths.length === 0) return false;
|
|
162
195
|
const expectedPaths = requestPathKeys(paths, sessionCwd);
|
|
163
196
|
|
|
164
197
|
for (const name of fs.readdirSync(dir)) {
|
|
165
198
|
if (!/^token-[a-f0-9]{24}\.json$/.test(name)) continue;
|
|
166
|
-
const
|
|
167
|
-
const token = readJson(tokenFile);
|
|
199
|
+
const token = readJson(dir, path.join(dir, name));
|
|
168
200
|
if (
|
|
169
201
|
!token ||
|
|
170
202
|
token.sessionId !== String(sessionId || '') ||
|
|
171
203
|
!samePathKeys(token.pathKeys, expectedPaths) ||
|
|
172
|
-
token.toolName !== String(toolName || '') ||
|
|
173
204
|
Number(token.expiresAt) <= Date.now()
|
|
174
205
|
) {
|
|
175
206
|
continue;
|
|
176
207
|
}
|
|
177
|
-
|
|
178
|
-
const claimed = `${tokenFile}.claimed-${process.pid}-${crypto.randomBytes(4).toString('hex')}`;
|
|
179
|
-
try {
|
|
180
|
-
fs.renameSync(tokenFile, claimed);
|
|
181
|
-
fs.unlinkSync(claimed);
|
|
182
|
-
return true;
|
|
183
|
-
} catch {
|
|
184
|
-
// Only one concurrent tool invocation can rename and consume this token.
|
|
185
|
-
}
|
|
208
|
+
return true;
|
|
186
209
|
}
|
|
187
210
|
return false;
|
|
188
211
|
}
|
|
@@ -193,7 +216,7 @@ module.exports = {
|
|
|
193
216
|
pathKey,
|
|
194
217
|
createPending,
|
|
195
218
|
approvePending,
|
|
196
|
-
|
|
219
|
+
hasGrant,
|
|
197
220
|
cleanup,
|
|
198
221
|
clearState
|
|
199
222
|
};
|