@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
|
@@ -11,6 +11,7 @@ const fs = require('fs');
|
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const POLICY = require('./workflow-policy.cjs');
|
|
13
13
|
const SEMANTIC = require('./spec-semantic-model.cjs');
|
|
14
|
+
const AUTHORING_DIGEST = require('./spec-authoring-digest.cjs');
|
|
14
15
|
const {
|
|
15
16
|
canonicalProjectRoot,
|
|
16
17
|
groundSpec,
|
|
@@ -74,6 +75,41 @@ const SPEC_21_REQUIRED_FIELDS = Object.freeze([
|
|
|
74
75
|
]);
|
|
75
76
|
const SPEC_21_OPTIONAL_FIELDS = Object.freeze(['research', 'task_files', 'task_registry', 'decisions']);
|
|
76
77
|
|
|
78
|
+
// C2 SemanticReviewReceipt (R1-01, frozen) — separate reviewer verdict from
|
|
79
|
+
// lifecycle_disposition; repair_round is the finalizer-derived C13 attempt_index.
|
|
80
|
+
// This is the sole canonical field-list authority for C2's exact key set;
|
|
81
|
+
// every other consumer (e.g. run-skill-self-tests.mjs's template contract
|
|
82
|
+
// check) imports this constant rather than maintaining its own literal copy.
|
|
83
|
+
const C2_FIELDS = Object.freeze([
|
|
84
|
+
'status', 'semantic_digest', 'verdict', 'lifecycle_disposition', 'findings',
|
|
85
|
+
'unresolved_decisions', 'graph_coverage', 'repair_round', 'reviewed_criteria',
|
|
86
|
+
'counterexamples', 'reviewer_evidence',
|
|
87
|
+
]);
|
|
88
|
+
const C2_VERDICTS = new Set(['PASS', 'FAIL']);
|
|
89
|
+
const C2_LIFECYCLE_DISPOSITIONS = new Set(['CONTINUE', 'BLOCKED']);
|
|
90
|
+
const C2_FINDING_FIELDS = ['id', 'severity', 'location', 'summary', 'blocking', 'disposition'];
|
|
91
|
+
const C2_FINDING_SEVERITIES = new Set(['Critical', 'High', 'Medium', 'Low']);
|
|
92
|
+
const C2_FINDING_DISPOSITIONS = new Set(['open', 'accepted', 'mitigated']);
|
|
93
|
+
const C2_UNRESOLVED_DECISION_FIELDS = ['id', 'summary', 'blocking'];
|
|
94
|
+
const C2_GRAPH_COVERAGE_FIELDS = ['surface', 'covered', 'notes'];
|
|
95
|
+
const C2_GRAPH_COVERAGE_SURFACES = [
|
|
96
|
+
'criterion_local', 'cross_criterion', 'runtime_path', 'assumption_provenance', 'compatibility_migration',
|
|
97
|
+
];
|
|
98
|
+
const C2_REVIEWER_EVIDENCE_FIELDS = ['assurance', 'summary'];
|
|
99
|
+
const C2_REVIEWER_EVIDENCE_ASSURANCES = new Set(['Routine', 'Elevated', 'Strict']);
|
|
100
|
+
// C13 SemanticReviewHistory (R1-01, frozen) — durable, append-only, whole-lineage-deduped.
|
|
101
|
+
const C13_HISTORY_FIELDS = ['lineage_id', 'entries'];
|
|
102
|
+
const C13_ENTRY_FIELDS = [
|
|
103
|
+
'sequence', 'semantic_digest', 'review_receipt_digest', 'verdict',
|
|
104
|
+
'lifecycle_disposition', 'blocking_count', 'attempt_index', 'review_epoch',
|
|
105
|
+
];
|
|
106
|
+
// C16 AuthoringValidationReceipt (R1-01, frozen) — sole freshness evidence for
|
|
107
|
+
// an authoring stage's `validated` reading (I20); computed via the shared,
|
|
108
|
+
// R0-01-owned spec-authoring-digest.cjs primitive (never reimplemented here).
|
|
109
|
+
const C16_FIELDS = ['schema_version', 'requirements', 'design', 'research', 'tasks'];
|
|
110
|
+
const C16_STAGE_FIELDS = ['digest', 'validated_at'];
|
|
111
|
+
const VALIDATION_21_FIELDS = ['status', 'semantic_review', 'semantic_review_history', 'authoring_validation'];
|
|
112
|
+
|
|
77
113
|
function usage() {
|
|
78
114
|
console.error('Usage: node .claude/scripts/validate-spec-output.cjs specs/<feature> [--semantic-digest]');
|
|
79
115
|
}
|
|
@@ -2551,7 +2587,7 @@ function hasConcreteTaskProse(section) {
|
|
|
2551
2587
|
));
|
|
2552
2588
|
}
|
|
2553
2589
|
|
|
2554
|
-
function validateTask21(taskPath, content, entry, errors) {
|
|
2590
|
+
function validateTask21(taskPath, content, entry, specReady, errors) {
|
|
2555
2591
|
for (const section of TASK_21_SECTIONS) {
|
|
2556
2592
|
if (headingCount(content, section) !== 1) errors.push(`${taskPath}: requires exactly one ## ${section} section`);
|
|
2557
2593
|
}
|
|
@@ -2561,7 +2597,7 @@ function validateTask21(taskPath, content, entry, errors) {
|
|
|
2561
2597
|
}
|
|
2562
2598
|
validateTaskIdentity(taskPath, content, entry, errors);
|
|
2563
2599
|
validateTaskDependencyHeader(taskPath, content, entry, true, errors);
|
|
2564
|
-
validateTaskStatusHeader(taskPath, content, entry,
|
|
2600
|
+
validateTaskStatusHeader(taskPath, content, entry, specReady, errors);
|
|
2565
2601
|
validateVerificationPlan(taskPath, content, errors);
|
|
2566
2602
|
validateTaskPlaceholders(taskPath, content, errors, []);
|
|
2567
2603
|
const outcome = extractSection(content, 'Outcome') || '';
|
|
@@ -2702,17 +2738,94 @@ function computeSemanticDigest21(specDir, spec) {
|
|
|
2702
2738
|
return { errors, digest: semanticDigest21(specDir, digestSpec, taskFiles) };
|
|
2703
2739
|
}
|
|
2704
2740
|
|
|
2741
|
+
function validateSemanticReviewFindings(review, errors) {
|
|
2742
|
+
if (!Array.isArray(review.findings)) {
|
|
2743
|
+
errors.push('spec.json.validation.semantic_review.findings: must be an array');
|
|
2744
|
+
return;
|
|
2745
|
+
}
|
|
2746
|
+
const findingIds = new Set();
|
|
2747
|
+
for (const [index, finding] of review.findings.entries()) {
|
|
2748
|
+
const label = `semantic_review.findings[${index}]`;
|
|
2749
|
+
if (!isPlainObject(finding) || Object.keys(finding).sort().join(',') !== [...C2_FINDING_FIELDS].sort().join(',')) {
|
|
2750
|
+
errors.push(`${label}: fields must be exactly ${C2_FINDING_FIELDS.join(', ')}`); continue;
|
|
2751
|
+
}
|
|
2752
|
+
if (typeof finding.id !== 'string' || finding.id === '' || findingIds.has(finding.id)) errors.push(`${label}.id: must be a unique non-empty string`);
|
|
2753
|
+
findingIds.add(finding.id);
|
|
2754
|
+
if (!C2_FINDING_SEVERITIES.has(finding.severity)) errors.push(`${label}.severity: must be Critical, High, Medium, or Low`);
|
|
2755
|
+
if (typeof finding.location !== 'string' || finding.location.trim() === '') errors.push(`${label}.location: must be a non-empty string`);
|
|
2756
|
+
if (!concreteSemanticReviewText(finding.summary)) errors.push(`${label}.summary: must be concrete with minimum length 8`);
|
|
2757
|
+
if (typeof finding.blocking !== 'boolean') errors.push(`${label}.blocking: must be boolean`);
|
|
2758
|
+
if (!C2_FINDING_DISPOSITIONS.has(finding.disposition)) errors.push(`${label}.disposition: must be open, accepted, or mitigated`);
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
function validateSemanticReviewUnresolvedDecisions(review, errors) {
|
|
2763
|
+
if (!Array.isArray(review.unresolved_decisions)) {
|
|
2764
|
+
errors.push('spec.json.validation.semantic_review.unresolved_decisions: must be an array');
|
|
2765
|
+
return;
|
|
2766
|
+
}
|
|
2767
|
+
const decisionIds = new Set();
|
|
2768
|
+
for (const [index, decision] of review.unresolved_decisions.entries()) {
|
|
2769
|
+
const label = `semantic_review.unresolved_decisions[${index}]`;
|
|
2770
|
+
if (!isPlainObject(decision) || Object.keys(decision).sort().join(',') !== [...C2_UNRESOLVED_DECISION_FIELDS].sort().join(',')) {
|
|
2771
|
+
errors.push(`${label}: fields must be exactly ${C2_UNRESOLVED_DECISION_FIELDS.join(', ')}`); continue;
|
|
2772
|
+
}
|
|
2773
|
+
if (typeof decision.id !== 'string' || decision.id === '' || decisionIds.has(decision.id)) errors.push(`${label}.id: must be a unique non-empty string`);
|
|
2774
|
+
decisionIds.add(decision.id);
|
|
2775
|
+
if (!concreteSemanticReviewText(decision.summary)) errors.push(`${label}.summary: must be concrete with minimum length 8`);
|
|
2776
|
+
if (typeof decision.blocking !== 'boolean') errors.push(`${label}.blocking: must be boolean`);
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
function validateSemanticReviewGraphCoverage(review, errors) {
|
|
2781
|
+
if (!Array.isArray(review.graph_coverage)) {
|
|
2782
|
+
errors.push('spec.json.validation.semantic_review.graph_coverage: must be an array');
|
|
2783
|
+
return;
|
|
2784
|
+
}
|
|
2785
|
+
const coveredSurfaces = new Set();
|
|
2786
|
+
for (const [index, coverage] of review.graph_coverage.entries()) {
|
|
2787
|
+
const label = `semantic_review.graph_coverage[${index}]`;
|
|
2788
|
+
if (!isPlainObject(coverage) || Object.keys(coverage).sort().join(',') !== [...C2_GRAPH_COVERAGE_FIELDS].sort().join(',')) {
|
|
2789
|
+
errors.push(`${label}: fields must be exactly ${C2_GRAPH_COVERAGE_FIELDS.join(', ')}`); continue;
|
|
2790
|
+
}
|
|
2791
|
+
if (!C2_GRAPH_COVERAGE_SURFACES.includes(coverage.surface)) errors.push(`${label}.surface: must be a canonical graph coverage surface`);
|
|
2792
|
+
else coveredSurfaces.add(coverage.surface);
|
|
2793
|
+
if (typeof coverage.covered !== 'boolean') errors.push(`${label}.covered: must be boolean`);
|
|
2794
|
+
if (typeof coverage.notes !== 'string') errors.push(`${label}.notes: must be a string`);
|
|
2795
|
+
}
|
|
2796
|
+
for (const surface of C2_GRAPH_COVERAGE_SURFACES) if (!coveredSurfaces.has(surface)) {
|
|
2797
|
+
errors.push(`spec.json.validation.semantic_review.graph_coverage: missing required surface ${surface}`);
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
function validateSemanticReviewerEvidence(review, errors) {
|
|
2802
|
+
if (review.reviewer_evidence === null) return;
|
|
2803
|
+
if (!isPlainObject(review.reviewer_evidence)
|
|
2804
|
+
|| Object.keys(review.reviewer_evidence).sort().join(',') !== [...C2_REVIEWER_EVIDENCE_FIELDS].sort().join(',')) {
|
|
2805
|
+
errors.push(`spec.json.validation.semantic_review.reviewer_evidence: fields must be exactly ${C2_REVIEWER_EVIDENCE_FIELDS.join(', ')} or null`);
|
|
2806
|
+
return;
|
|
2807
|
+
}
|
|
2808
|
+
if (!C2_REVIEWER_EVIDENCE_ASSURANCES.has(review.reviewer_evidence.assurance)) {
|
|
2809
|
+
errors.push('spec.json.validation.semantic_review.reviewer_evidence.assurance: must be Routine, Elevated, or Strict');
|
|
2810
|
+
}
|
|
2811
|
+
if (!concreteSemanticReviewText(review.reviewer_evidence.summary)) {
|
|
2812
|
+
errors.push('spec.json.validation.semantic_review.reviewer_evidence.summary: must be concrete with minimum length 8');
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2705
2816
|
function validateSemanticReview21(specDir, spec, criteria, designIds, verificationDefs, errors) {
|
|
2706
2817
|
const review = spec.validation?.semantic_review;
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
errors.push(`spec.json.validation.semantic_review: schema 2.1 fields must be exactly ${fields.join(', ')}`);
|
|
2818
|
+
if (!isPlainObject(review) || Object.keys(review).sort().join(',') !== [...C2_FIELDS].sort().join(',')) {
|
|
2819
|
+
errors.push(`spec.json.validation.semantic_review: schema 2.1 fields must be exactly ${C2_FIELDS.join(', ')}`);
|
|
2710
2820
|
return;
|
|
2711
2821
|
}
|
|
2712
2822
|
if (!['not-run', 'completed'].includes(review.status)) errors.push('spec.json.validation.semantic_review.status: must be not-run or completed');
|
|
2713
2823
|
if (review.status === 'not-run') {
|
|
2714
|
-
if (review.semantic_digest !== null || review.
|
|
2715
|
-
|
|
2824
|
+
if (review.semantic_digest !== null || review.verdict !== null || review.lifecycle_disposition !== null
|
|
2825
|
+
|| review.reviewer_evidence !== null || review.repair_round !== 0
|
|
2826
|
+
|| review.findings?.length || review.unresolved_decisions?.length
|
|
2827
|
+
|| review.graph_coverage?.length || review.reviewed_criteria?.length || review.counterexamples?.length) {
|
|
2828
|
+
errors.push('spec.json.validation.semantic_review: not-run receipt must keep verdict, lifecycle_disposition, and reviewer_evidence null, repair_round 0, and every array empty');
|
|
2716
2829
|
}
|
|
2717
2830
|
if (spec.ready_for_implementation === true) errors.push('spec.json.ready_for_implementation: requires completed semantic review');
|
|
2718
2831
|
return;
|
|
@@ -2723,6 +2836,18 @@ function validateSemanticReview21(specDir, spec, criteria, designIds, verificati
|
|
|
2723
2836
|
} else if (review.semantic_digest !== computed.digest) {
|
|
2724
2837
|
errors.push('spec.json.validation.semantic_review.semantic_digest: stale for current authoring, policy, topology, or research');
|
|
2725
2838
|
}
|
|
2839
|
+
if (typeof review.semantic_digest !== 'string' || !SHA256_RE.test(review.semantic_digest)) {
|
|
2840
|
+
errors.push('spec.json.validation.semantic_review.semantic_digest: must be sha256:<64 lowercase hex> when completed');
|
|
2841
|
+
}
|
|
2842
|
+
if (!C2_VERDICTS.has(review.verdict)) errors.push('spec.json.validation.semantic_review.verdict: must be PASS or FAIL when completed');
|
|
2843
|
+
if (!C2_LIFECYCLE_DISPOSITIONS.has(review.lifecycle_disposition)) errors.push('spec.json.validation.semantic_review.lifecycle_disposition: must be CONTINUE or BLOCKED when completed');
|
|
2844
|
+
if (!Number.isInteger(review.repair_round) || review.repair_round < 0 || review.repair_round > 2) {
|
|
2845
|
+
errors.push('spec.json.validation.semantic_review.repair_round: must be an integer 0..2 when completed');
|
|
2846
|
+
}
|
|
2847
|
+
validateSemanticReviewFindings(review, errors);
|
|
2848
|
+
validateSemanticReviewUnresolvedDecisions(review, errors);
|
|
2849
|
+
validateSemanticReviewGraphCoverage(review, errors);
|
|
2850
|
+
validateSemanticReviewerEvidence(review, errors);
|
|
2726
2851
|
if (!Array.isArray(review.reviewed_criteria) || new Set(review.reviewed_criteria).size !== review.reviewed_criteria.length) {
|
|
2727
2852
|
errors.push('spec.json.validation.semantic_review.reviewed_criteria: must be a unique array');
|
|
2728
2853
|
} else {
|
|
@@ -2788,6 +2913,91 @@ function validateSemanticReview21(specDir, spec, criteria, designIds, verificati
|
|
|
2788
2913
|
}
|
|
2789
2914
|
}
|
|
2790
2915
|
|
|
2916
|
+
function computeLineageId(featureName, createdAt) {
|
|
2917
|
+
return `sha256:${crypto.createHash('sha256').update(SEMANTIC.stableJson({ feature_name: featureName, created_at: createdAt })).digest('hex')}`;
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
// C13 SemanticReviewHistory: structural completeness, types, nullability,
|
|
2921
|
+
// enums, cardinality, and whole-lineage digest dedupe only — never semantic
|
|
2922
|
+
// judgment of the epoch/attempt-index derivation itself (that is the R1-02
|
|
2923
|
+
// finalizer's own write-time responsibility, see C3/D4).
|
|
2924
|
+
function validateSemanticReviewHistory21(spec, errors) {
|
|
2925
|
+
const history = spec.validation?.semantic_review_history;
|
|
2926
|
+
if (!isPlainObject(history) || Object.keys(history).sort().join(',') !== [...C13_HISTORY_FIELDS].sort().join(',')) {
|
|
2927
|
+
errors.push(`spec.json.validation.semantic_review_history: fields must be exactly ${C13_HISTORY_FIELDS.join(', ')}`);
|
|
2928
|
+
return;
|
|
2929
|
+
}
|
|
2930
|
+
const expectedLineageId = computeLineageId(spec.feature_name, spec.created_at);
|
|
2931
|
+
if (history.lineage_id !== expectedLineageId) {
|
|
2932
|
+
errors.push('spec.json.validation.semantic_review_history.lineage_id: must be sha256 of feature_name and created_at; callers never set it directly');
|
|
2933
|
+
}
|
|
2934
|
+
if (!Array.isArray(history.entries)) {
|
|
2935
|
+
errors.push('spec.json.validation.semantic_review_history.entries: must be an array');
|
|
2936
|
+
return;
|
|
2937
|
+
}
|
|
2938
|
+
const digestsSeen = new Set();
|
|
2939
|
+
for (const [index, entry] of history.entries.entries()) {
|
|
2940
|
+
const label = `semantic_review_history.entries[${index}]`;
|
|
2941
|
+
if (!isPlainObject(entry) || Object.keys(entry).sort().join(',') !== [...C13_ENTRY_FIELDS].sort().join(',')) {
|
|
2942
|
+
errors.push(`${label}: fields must be exactly ${C13_ENTRY_FIELDS.join(', ')}`); continue;
|
|
2943
|
+
}
|
|
2944
|
+
if (entry.sequence !== index) errors.push(`${label}.sequence: must start at 0 and increase by exactly 1 per real append`);
|
|
2945
|
+
if (typeof entry.semantic_digest !== 'string' || !SHA256_RE.test(entry.semantic_digest)) errors.push(`${label}.semantic_digest: must be sha256:<64 lowercase hex>`);
|
|
2946
|
+
if (typeof entry.review_receipt_digest !== 'string' || !SHA256_RE.test(entry.review_receipt_digest)) {
|
|
2947
|
+
errors.push(`${label}.review_receipt_digest: must be sha256:<64 lowercase hex>`);
|
|
2948
|
+
} else if (digestsSeen.has(entry.review_receipt_digest)) {
|
|
2949
|
+
errors.push(`${label}.review_receipt_digest: replays an existing entry's digest anywhere in the lineage; a replay must not be appended twice`);
|
|
2950
|
+
} else digestsSeen.add(entry.review_receipt_digest);
|
|
2951
|
+
if (!C2_VERDICTS.has(entry.verdict)) errors.push(`${label}.verdict: must be PASS or FAIL`);
|
|
2952
|
+
if (!C2_LIFECYCLE_DISPOSITIONS.has(entry.lifecycle_disposition)) errors.push(`${label}.lifecycle_disposition: must be CONTINUE or BLOCKED`);
|
|
2953
|
+
if (!Number.isInteger(entry.blocking_count) || entry.blocking_count < 0) errors.push(`${label}.blocking_count: must be a non-negative integer`);
|
|
2954
|
+
if (!Number.isInteger(entry.attempt_index) || entry.attempt_index < 0 || entry.attempt_index > 2) errors.push(`${label}.attempt_index: must be an integer 0..2`);
|
|
2955
|
+
if (!Number.isInteger(entry.review_epoch) || entry.review_epoch < 0) errors.push(`${label}.review_epoch: must be a non-negative integer`);
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
// C16 AuthoringValidationReceipt: structural completeness plus the fail-closed
|
|
2960
|
+
// freshness check (I20/R3.8) — an authoring.<field> reading `validated`
|
|
2961
|
+
// without a matching fresh digest is a hard structural failure here, not a
|
|
2962
|
+
// silent downgrade to draft.
|
|
2963
|
+
function validateAuthoringValidation21(specDir, spec, errors) {
|
|
2964
|
+
const receipt = spec.validation?.authoring_validation;
|
|
2965
|
+
if (receipt !== null) {
|
|
2966
|
+
if (!isPlainObject(receipt) || Object.keys(receipt).sort().join(',') !== [...C16_FIELDS].sort().join(',')) {
|
|
2967
|
+
errors.push(`spec.json.validation.authoring_validation: fields must be exactly ${C16_FIELDS.join(', ')} or null`);
|
|
2968
|
+
return;
|
|
2969
|
+
}
|
|
2970
|
+
if (receipt.schema_version !== '1') errors.push('spec.json.validation.authoring_validation.schema_version: must be exactly "1"');
|
|
2971
|
+
for (const stage of ['requirements', 'design', 'research', 'tasks']) {
|
|
2972
|
+
const label = `spec.json.validation.authoring_validation.${stage}`;
|
|
2973
|
+
const entry = receipt[stage];
|
|
2974
|
+
if (!isPlainObject(entry) || Object.keys(entry).sort().join(',') !== [...C16_STAGE_FIELDS].sort().join(',')) {
|
|
2975
|
+
errors.push(`${label}: fields must be exactly ${C16_STAGE_FIELDS.join(', ')}`); continue;
|
|
2976
|
+
}
|
|
2977
|
+
const sentinel = stage === 'research' ? AUTHORING_DIGEST.RESEARCH_ABSENT_DIGEST
|
|
2978
|
+
: stage === 'tasks' ? AUTHORING_DIGEST.TASKS_ABSENT_DIGEST : null;
|
|
2979
|
+
const digestValid = typeof entry.digest === 'string'
|
|
2980
|
+
&& (SHA256_RE.test(entry.digest) || (sentinel !== null && entry.digest === sentinel));
|
|
2981
|
+
if (!digestValid) errors.push(`${label}.digest: must be sha256:<64 lowercase hex>${sentinel ? ` or ${sentinel}` : ''}`);
|
|
2982
|
+
if (!isIsoTimestamp(entry.validated_at)) errors.push(`${label}.validated_at: must be an ISO timestamp`);
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
let freshness;
|
|
2986
|
+
try {
|
|
2987
|
+
freshness = AUTHORING_DIGEST.assertAuthoringFresh({
|
|
2988
|
+
specDir, authoring: spec.authoring, receipt, fields: ['requirements', 'design', 'research', 'tasks'],
|
|
2989
|
+
});
|
|
2990
|
+
} catch (error) {
|
|
2991
|
+
errors.push(`spec.json.validation.authoring_validation: cannot recompute current authoring digests (${error.message})`);
|
|
2992
|
+
return;
|
|
2993
|
+
}
|
|
2994
|
+
for (const field of ['requirements', 'design', 'research', 'tasks']) {
|
|
2995
|
+
if (spec.authoring?.[field] === 'validated' && !freshness[field]?.fresh) {
|
|
2996
|
+
errors.push(`spec.json.authoring.${field}: reads validated but validation.authoring_validation is absent, missing this field's entry, or digest-mismatched against current bytes (I20/C16)`);
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
|
|
2791
3001
|
function validateDecisionState21(spec, errors) {
|
|
2792
3002
|
if (!Object.prototype.hasOwnProperty.call(spec, 'decisions')) return;
|
|
2793
3003
|
if (!Array.isArray(spec.decisions) || spec.decisions.length === 0) {
|
|
@@ -3026,6 +3236,36 @@ function validateBoundaries21(spec, taskFiles, registry, anchorsByTask, criteria
|
|
|
3026
3236
|
}
|
|
3027
3237
|
}
|
|
3028
3238
|
}
|
|
3239
|
+
// D10/I7/R8.3: global ownership-contention scan. Candidate pairs are derived
|
|
3240
|
+
// directly from the complete anchorsByTask write-anchor projection — every
|
|
3241
|
+
// task's every write anchor — never only from declared write_sets or from
|
|
3242
|
+
// iterating already-declared boundaries. Any single existing dependency
|
|
3243
|
+
// boundary between a pair covers every target that pair shares, regardless
|
|
3244
|
+
// of which target it names as its own deliverable; a same-target write pair
|
|
3245
|
+
// with no dependency boundary at all (even one never named in any boundary)
|
|
3246
|
+
// is exactly the defect this scan exists to catch.
|
|
3247
|
+
const connectedPairs = new Set(dependencyEdges.map(([producer, consumer]) => [producer, consumer].sort().join('\0')));
|
|
3248
|
+
const writersByTarget = new Map();
|
|
3249
|
+
for (const taskPath of taskFiles) {
|
|
3250
|
+
const taskId = registry[taskPath]?.id;
|
|
3251
|
+
if (!taskId) continue;
|
|
3252
|
+
for (const anchor of anchorsByTask.get(taskPath) || []) {
|
|
3253
|
+
if (anchor.access !== 'write') continue;
|
|
3254
|
+
if (!writersByTarget.has(anchor.target)) writersByTarget.set(anchor.target, new Set());
|
|
3255
|
+
writersByTarget.get(anchor.target).add(taskId);
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
const flaggedContentionPairs = new Set();
|
|
3259
|
+
for (const [target, writers] of writersByTarget) {
|
|
3260
|
+
const writerIds = [...writers].sort();
|
|
3261
|
+
for (let left = 0; left < writerIds.length; left += 1) for (let right = left + 1; right < writerIds.length; right += 1) {
|
|
3262
|
+
const a = writerIds[left]; const b = writerIds[right];
|
|
3263
|
+
const pairKey = [a, b].sort().join('\0');
|
|
3264
|
+
if (connectedPairs.has(pairKey) || flaggedContentionPairs.has(pairKey)) continue;
|
|
3265
|
+
flaggedContentionPairs.add(pairKey);
|
|
3266
|
+
errors.push(`spec.json.coordination.boundaries: ${a} and ${b} both hold a write anchor for ${target} with no dependency boundary connecting them`);
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3029
3269
|
const participation = new Map([...taskIds].map((taskId) => [taskId, 0]));
|
|
3030
3270
|
for (const boundary of spec.coordination.boundaries) {
|
|
3031
3271
|
for (const taskId of new Set(boundaryParticipants(boundary))) if (participation.has(taskId)) {
|
|
@@ -3161,8 +3401,8 @@ function validateSpec21(specDir, canonicalSpecDir, spec, errors, warnings) {
|
|
|
3161
3401
|
if (spec.scope_lock.expansion_policy !== 'requires-user-approval') errors.push('spec.json.scope_lock.expansion_policy: must be requires-user-approval');
|
|
3162
3402
|
}
|
|
3163
3403
|
if (isPlainObject(spec.validation)
|
|
3164
|
-
&& Object.keys(spec.validation).sort().join(',') !== '
|
|
3165
|
-
errors.push(
|
|
3404
|
+
&& Object.keys(spec.validation).sort().join(',') !== [...VALIDATION_21_FIELDS].sort().join(',')) {
|
|
3405
|
+
errors.push(`spec.json.validation: fields must be exactly ${VALIDATION_21_FIELDS.join(', ')}`);
|
|
3166
3406
|
}
|
|
3167
3407
|
const taskFiles = listTaskFiles(specDir, canonicalSpecDir, errors);
|
|
3168
3408
|
validateCoreMachineState(spec, taskFiles, errors, { schema21: true });
|
|
@@ -3206,7 +3446,13 @@ function validateSpec21(specDir, canonicalSpecDir, spec, errors, warnings) {
|
|
|
3206
3446
|
const taskContents = new Map();
|
|
3207
3447
|
for (const taskPath of taskFiles) {
|
|
3208
3448
|
const content = fs.readFileSync(path.join(specDir, taskPath), 'utf8');
|
|
3209
|
-
const task = validateTask21(
|
|
3449
|
+
const task = validateTask21(
|
|
3450
|
+
taskPath,
|
|
3451
|
+
content,
|
|
3452
|
+
spec.task_registry?.[taskPath],
|
|
3453
|
+
spec.ready_for_implementation === true,
|
|
3454
|
+
errors,
|
|
3455
|
+
);
|
|
3210
3456
|
taskContents.set(taskPath, content);
|
|
3211
3457
|
anchorsByTask.set(taskPath, task.anchors);
|
|
3212
3458
|
criteriaByTask.set(taskPath, task.criteria);
|
|
@@ -3241,6 +3487,8 @@ function validateSpec21(specDir, canonicalSpecDir, spec, errors, warnings) {
|
|
|
3241
3487
|
designText, verificationDefinitions, errors,
|
|
3242
3488
|
);
|
|
3243
3489
|
validateSemanticReview21(specDir, spec, criteria, designIds, verificationDefinitions, errors);
|
|
3490
|
+
validateSemanticReviewHistory21(spec, errors);
|
|
3491
|
+
validateAuthoringValidation21(specDir, spec, errors);
|
|
3244
3492
|
if (spec.ready_for_implementation === true) {
|
|
3245
3493
|
for (const artifact of ['requirements', 'design']) if (spec.authoring?.[artifact] !== 'validated') errors.push(`spec.json.authoring.${artifact}: readiness requires validated`);
|
|
3246
3494
|
if (taskFiles.length > 0 && spec.authoring?.tasks !== 'validated') errors.push('spec.json.authoring.tasks: readiness requires validated task projection');
|
|
@@ -3685,5 +3933,9 @@ function main() {
|
|
|
3685
3933
|
}
|
|
3686
3934
|
|
|
3687
3935
|
module.exports = { validateSpec, computeSemanticDigest21 };
|
|
3936
|
+
Object.defineProperty(module.exports, 'C2_FIELDS', {
|
|
3937
|
+
value: C2_FIELDS,
|
|
3938
|
+
enumerable: false,
|
|
3939
|
+
});
|
|
3688
3940
|
|
|
3689
3941
|
if (require.main === module) main();
|
|
@@ -137,8 +137,20 @@ fix. Scope or product decisions require explicit user approval before editing.
|
|
|
137
137
|
## Deterministic and grounding gates
|
|
138
138
|
|
|
139
139
|
Finalize requirements, design, optional research, task files, and canonical
|
|
140
|
-
`spec.json` topology first.
|
|
141
|
-
|
|
140
|
+
`spec.json` topology first. Every required authoring stage (`requirements`,
|
|
141
|
+
`design`, and, when present, `research`/`tasks`) must already read `validated`
|
|
142
|
+
via a fresh run of `spec-authoring-validation.cjs` — the sole atomic writer of
|
|
143
|
+
that transition and its C16 `AuthoringValidationReceipt` — before the digest
|
|
144
|
+
below is computed; a `validated` reading with no matching fresh receipt digest
|
|
145
|
+
is treated as `draft` everywhere it is read. Compute the candidate digest
|
|
146
|
+
read-only, then provide a review JSON object with exactly `verdict`,
|
|
147
|
+
`findings`, `unresolved_decisions`, `graph_coverage`, `reviewed_criteria`,
|
|
148
|
+
`counterexamples`, and `reviewer_evidence` — the frozen C2 fields the reviewer
|
|
149
|
+
itself supplies. `lifecycle_disposition` and `repair_round` are never authored
|
|
150
|
+
in this JSON: the finalizer derives both (repair_round is the C13 `attempt_index`
|
|
151
|
+
for the current review_epoch; lifecycle_disposition is CONTINUE unless a
|
|
152
|
+
non-PASS at the epoch's third attempt forces BLOCKED) and appends the completed
|
|
153
|
+
receipt to the durable, append-only `semantic_review_history` lineage. The
|
|
142
154
|
author must not fabricate or directly write authority state.
|
|
143
155
|
|
|
144
156
|
The digest projects task registry semantics as `id`, `dependencies`, and optional
|
|
@@ -25,9 +25,21 @@
|
|
|
25
25
|
"semantic_review": {
|
|
26
26
|
"status": "not-run",
|
|
27
27
|
"semantic_digest": null,
|
|
28
|
+
"verdict": null,
|
|
29
|
+
"lifecycle_disposition": null,
|
|
30
|
+
"findings": [],
|
|
31
|
+
"unresolved_decisions": [],
|
|
32
|
+
"graph_coverage": [],
|
|
33
|
+
"repair_round": 0,
|
|
28
34
|
"reviewed_criteria": [],
|
|
29
|
-
"counterexamples": []
|
|
30
|
-
|
|
35
|
+
"counterexamples": [],
|
|
36
|
+
"reviewer_evidence": null
|
|
37
|
+
},
|
|
38
|
+
"semantic_review_history": {
|
|
39
|
+
"lineage_id": "{{LINEAGE_ID}}",
|
|
40
|
+
"entries": []
|
|
41
|
+
},
|
|
42
|
+
"authoring_validation": null
|
|
31
43
|
},
|
|
32
44
|
"semantic_model": null,
|
|
33
45
|
"ready_for_implementation": false,
|
|
@@ -5,7 +5,12 @@ const fs = require('fs');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const PATH_SAFETY = require('./runtime-path-safety.cjs');
|
|
7
7
|
|
|
8
|
-
|
|
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;
|
|
9
14
|
const APPROVAL_PREFIX = 'APPROVE CAFEKIT PRIVACY ';
|
|
10
15
|
function stateDir(projectRoot) {
|
|
11
16
|
return path.join(projectRoot, '.codex', 'hooks', '.privacy');
|
|
@@ -115,8 +120,7 @@ function createPending({
|
|
|
115
120
|
sessionCwd,
|
|
116
121
|
sessionId,
|
|
117
122
|
filePaths,
|
|
118
|
-
filePath
|
|
119
|
-
toolName
|
|
123
|
+
filePath
|
|
120
124
|
}) {
|
|
121
125
|
cleanup(projectRoot);
|
|
122
126
|
if (!sessionId) throw new Error('A Codex session ID is required');
|
|
@@ -129,7 +133,6 @@ function createPending({
|
|
|
129
133
|
sessionId: String(sessionId || ''),
|
|
130
134
|
pathKeys: requestPathKeys(paths, sessionCwd),
|
|
131
135
|
displayName: displayNames(paths),
|
|
132
|
-
toolName: String(toolName || ''),
|
|
133
136
|
expiresAt: Date.now() + TTL_MS
|
|
134
137
|
};
|
|
135
138
|
const dir = secureStateDir(projectRoot, true);
|
|
@@ -156,24 +159,32 @@ function approvePending({ projectRoot, sessionId, requestId }) {
|
|
|
156
159
|
return { ok: false, reason: 'consumed' };
|
|
157
160
|
}
|
|
158
161
|
|
|
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.
|
|
159
165
|
atomicWrite(dir, path.join(dir, `token-${requestId}.json`), {
|
|
160
166
|
version: 2,
|
|
161
167
|
requestId,
|
|
162
168
|
sessionId: record.sessionId,
|
|
163
169
|
pathKeys: record.pathKeys,
|
|
164
|
-
|
|
165
|
-
expiresAt: Math.min(record.expiresAt, Date.now() + TTL_MS)
|
|
170
|
+
expiresAt: Date.now() + TTL_MS
|
|
166
171
|
});
|
|
167
172
|
return { ok: true, displayName: record.displayName };
|
|
168
173
|
}
|
|
169
174
|
|
|
170
|
-
|
|
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({
|
|
171
183
|
projectRoot,
|
|
172
184
|
sessionCwd,
|
|
173
185
|
sessionId,
|
|
174
186
|
filePaths,
|
|
175
|
-
filePath
|
|
176
|
-
toolName
|
|
187
|
+
filePath
|
|
177
188
|
}) {
|
|
178
189
|
cleanup(projectRoot);
|
|
179
190
|
if (!sessionId) return false;
|
|
@@ -185,26 +196,16 @@ function consumeToken({
|
|
|
185
196
|
|
|
186
197
|
for (const name of fs.readdirSync(dir)) {
|
|
187
198
|
if (!/^token-[a-f0-9]{24}\.json$/.test(name)) continue;
|
|
188
|
-
const
|
|
189
|
-
const token = readJson(dir, tokenFile);
|
|
199
|
+
const token = readJson(dir, path.join(dir, name));
|
|
190
200
|
if (
|
|
191
201
|
!token ||
|
|
192
202
|
token.sessionId !== String(sessionId || '') ||
|
|
193
203
|
!samePathKeys(token.pathKeys, expectedPaths) ||
|
|
194
|
-
token.toolName !== String(toolName || '') ||
|
|
195
204
|
Number(token.expiresAt) <= Date.now()
|
|
196
205
|
) {
|
|
197
206
|
continue;
|
|
198
207
|
}
|
|
199
|
-
|
|
200
|
-
const claimed = `${tokenFile}.claimed-${process.pid}-${crypto.randomBytes(4).toString('hex')}`;
|
|
201
|
-
try {
|
|
202
|
-
fs.renameSync(tokenFile, claimed);
|
|
203
|
-
fs.unlinkSync(claimed);
|
|
204
|
-
return true;
|
|
205
|
-
} catch {
|
|
206
|
-
// Only one concurrent tool invocation can rename and consume this token.
|
|
207
|
-
}
|
|
208
|
+
return true;
|
|
208
209
|
}
|
|
209
210
|
return false;
|
|
210
211
|
}
|
|
@@ -215,7 +216,7 @@ module.exports = {
|
|
|
215
216
|
pathKey,
|
|
216
217
|
createPending,
|
|
217
218
|
approvePending,
|
|
218
|
-
|
|
219
|
+
hasGrant,
|
|
219
220
|
cleanup,
|
|
220
221
|
clearState
|
|
221
222
|
};
|
|
@@ -24,7 +24,11 @@ try {
|
|
|
24
24
|
const data = readPayload();
|
|
25
25
|
if (!data) process.exit(0);
|
|
26
26
|
const { projectRoot } = getHookContext(data);
|
|
27
|
-
|
|
27
|
+
// A pasted prompt arrives with whatever whitespace the terminal added around
|
|
28
|
+
// it. Rejecting on a trailing newline made the approval look broken while the
|
|
29
|
+
// id it named was still perfectly valid; the exactness that matters is the
|
|
30
|
+
// request id, checked below.
|
|
31
|
+
const prompt = String(data.prompt || '').trim();
|
|
28
32
|
cleanup(projectRoot);
|
|
29
33
|
|
|
30
34
|
if (!prompt.startsWith(APPROVAL_PREFIX)) process.exit(0);
|
|
@@ -49,8 +53,8 @@ try {
|
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
context(
|
|
52
|
-
`User approved
|
|
53
|
-
`The
|
|
56
|
+
`User approved the pending sensitive access (${result.displayName}). ` +
|
|
57
|
+
`The grant is session-bound, bound to exactly those paths, and expires in one hour.`
|
|
54
58
|
);
|
|
55
59
|
process.exit(0);
|
|
56
60
|
} catch {
|
|
@@ -6,16 +6,13 @@ const path = require('path');
|
|
|
6
6
|
const {
|
|
7
7
|
APPROVAL_PREFIX,
|
|
8
8
|
createPending,
|
|
9
|
-
|
|
9
|
+
hasGrant
|
|
10
10
|
} = require('./lib/privacy-state.cjs');
|
|
11
11
|
const {
|
|
12
12
|
getHookContext,
|
|
13
13
|
readPayload
|
|
14
14
|
} = require('./lib/hook-context.cjs');
|
|
15
|
-
const {
|
|
16
|
-
DYNAMIC_READ_PATH,
|
|
17
|
-
commandAccess,
|
|
18
|
-
} = require('./lib/privacy-command-analysis.cjs');
|
|
15
|
+
const { commandAccess } = require('./lib/privacy-command-analysis.cjs');
|
|
19
16
|
|
|
20
17
|
const RESTRICTED = [
|
|
21
18
|
/^\.env(?:[.\[*?{]|$)/i,
|
|
@@ -32,13 +29,7 @@ const RESTRICTED = [
|
|
|
32
29
|
/\.keystore$/i,
|
|
33
30
|
/\.jks$/i,
|
|
34
31
|
/auth\.json$/i,
|
|
35
|
-
/token(s)?\.json$/i
|
|
36
|
-
/\.cafekit/i,
|
|
37
|
-
/hmac\.key/i,
|
|
38
|
-
/completion-authority/i,
|
|
39
|
-
/session-state/i,
|
|
40
|
-
/hook-log/i,
|
|
41
|
-
/sessions/i
|
|
32
|
+
/token(s)?\.json$/i
|
|
42
33
|
];
|
|
43
34
|
|
|
44
35
|
const EXEMPT = [/\.env\.(example|sample|template|test)$/i];
|
|
@@ -58,7 +49,6 @@ function isSafe(filePath) {
|
|
|
58
49
|
}
|
|
59
50
|
|
|
60
51
|
function isSensitive(filePath) {
|
|
61
|
-
if (filePath === DYNAMIC_READ_PATH) return true;
|
|
62
52
|
return matchesAny(filePath, RESTRICTED);
|
|
63
53
|
}
|
|
64
54
|
|
|
@@ -120,11 +110,44 @@ function isSymlinkToSensitive(filePath, cwd) {
|
|
|
120
110
|
}
|
|
121
111
|
}
|
|
122
112
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
113
|
+
function extractCommandPaths(command) {
|
|
114
|
+
// The analyzer parses the command and returns the operands of every reading
|
|
115
|
+
// command it recognises, nested shells and `find -exec` included. Treating
|
|
116
|
+
// every unresolved expansion as sensitive, as this did before, denied
|
|
117
|
+
// `cat $FILE`, `wc -l *.cjs` and `cat ~/.zshrc` alike -- and every denial here
|
|
118
|
+
// costs a round trip through a one-shot approval prompt.
|
|
119
|
+
// A heredoc whose delimiter is quoted (<<'BODY') is inert: the shell expands
|
|
120
|
+
// nothing inside it, so no word in the body is ever read. The analyzer has no
|
|
121
|
+
// notion of heredocs and parses the body as shell, so prose that quotes a
|
|
122
|
+
// command -- a PR body carrying `cat .env` in backticks -- came back as a real
|
|
123
|
+
// command substitution. Blank those bodies out first. An unquoted delimiter
|
|
124
|
+
// (<<BODY) does expand, so it is left to the analyzer.
|
|
125
|
+
const scrubbed = String(command).replace(
|
|
126
|
+
/(<<-?[ \t]*)(['"])([A-Za-z_]\w*)\2[^\n]*\n[\s\S]*?\n[ \t]*\3\b/g,
|
|
127
|
+
'$1$2$3$2'
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const paths = new Set(commandAccess(scrubbed).paths);
|
|
131
|
+
|
|
132
|
+
// Two shapes the analyzer cannot return. It drops any word carrying an
|
|
133
|
+
// expansion, so `~/.ssh/id_rsa` and `certs/*.pem` still have to be matched by
|
|
134
|
+
// name. And it only walks commands it recognises, so an unknown tool handed a
|
|
135
|
+
// secret through a file-shaped option, such as
|
|
136
|
+
// `docker compose --env-file .env`, is invisible to it.
|
|
137
|
+
const FILE_OPTION = /^--?[\w-]*(?:file|config|key|cert|identity)$/i;
|
|
138
|
+
const tokens = scrubbed.split(/[\s|;&<>"'()`]+/);
|
|
139
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
140
|
+
const token = tokens[index];
|
|
141
|
+
const shaped = /^~|[*?[{]/.test(token);
|
|
142
|
+
const optionFed = index > 0 && FILE_OPTION.test(tokens[index - 1]);
|
|
143
|
+
if (!shaped && !optionFed) continue;
|
|
144
|
+
const value = token.replace(/^~/, '').replace(/^["'()`]+|["'(),]+$/g, '');
|
|
145
|
+
if (value && isSensitive(value) && !isSafe(value)) paths.add(value);
|
|
126
146
|
}
|
|
127
147
|
|
|
148
|
+
return [...paths];
|
|
149
|
+
}
|
|
150
|
+
|
|
128
151
|
function extractPatchPaths(patch) {
|
|
129
152
|
return [...String(patch).matchAll(
|
|
130
153
|
/^\*\*\* (?:(?:Add|Update|Delete) File:|Move to:)\s+(.+)$/gm
|
|
@@ -216,19 +239,19 @@ try {
|
|
|
216
239
|
);
|
|
217
240
|
process.exit(0);
|
|
218
241
|
}
|
|
219
|
-
const
|
|
242
|
+
const grantInput = {
|
|
220
243
|
projectRoot,
|
|
221
244
|
sessionCwd,
|
|
222
245
|
sessionId: data.session_id,
|
|
223
|
-
filePaths: effectivePaths
|
|
224
|
-
toolName: data.tool_name
|
|
246
|
+
filePaths: effectivePaths
|
|
225
247
|
};
|
|
226
|
-
if (
|
|
248
|
+
if (hasGrant(grantInput)) process.exit(0);
|
|
227
249
|
|
|
228
|
-
const pending = createPending(
|
|
250
|
+
const pending = createPending(grantInput);
|
|
229
251
|
deny(
|
|
230
252
|
`Sensitive file access denied: ${pending.displayName}. ` +
|
|
231
|
-
`
|
|
253
|
+
`To approve access to exactly these paths for the next hour of this Codex ` +
|
|
254
|
+
`session, send this exact user prompt: ` +
|
|
232
255
|
`${APPROVAL_PREFIX}${pending.requestId}`
|
|
233
256
|
);
|
|
234
257
|
process.exit(0);
|