@guilz-dev/belay 0.3.0 → 0.4.0
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/dist/adapters/cursor/runtime-entry.js +1 -0
- package/dist/adapters/shared/gate-runtime.d.ts +11 -14
- package/dist/adapters/shared/gate-runtime.js +277 -19
- package/dist/bundle/claude-runtime.mjs +3138 -699
- package/dist/bundle/codex-runtime.mjs +3162 -711
- package/dist/bundle/cursor-runtime.mjs +3140 -701
- package/dist/cli.js +137 -5
- package/dist/commands/approve.d.ts +1 -0
- package/dist/commands/approve.js +45 -26
- package/dist/commands/config.js +135 -52
- package/dist/commands/doctor.js +11 -1
- package/dist/commands/explain.js +6 -1
- package/dist/commands/harvest.d.ts +29 -0
- package/dist/commands/harvest.js +72 -0
- package/dist/commands/judge.d.ts +11 -1
- package/dist/commands/judge.js +21 -2
- package/dist/commands/metrics.js +34 -2
- package/dist/commands/quality.d.ts +38 -0
- package/dist/commands/quality.js +105 -0
- package/dist/commands/simulate.d.ts +1 -0
- package/dist/commands/simulate.js +7 -2
- package/dist/commands/standing-allow.d.ts +10 -0
- package/dist/commands/standing-allow.js +26 -0
- package/dist/commands/tui.d.ts +48 -0
- package/dist/commands/tui.js +150 -0
- package/dist/config-io.js +10 -5
- package/dist/core/approval-replay-cli.d.ts +7 -0
- package/dist/core/approval-replay-cli.js +36 -0
- package/dist/core/approval-replay.d.ts +45 -0
- package/dist/core/approval-replay.js +141 -0
- package/dist/core/approval-service.d.ts +15 -0
- package/dist/core/approval-service.js +47 -2
- package/dist/core/approval.d.ts +24 -3
- package/dist/core/approval.js +47 -4
- package/dist/core/audit-analysis.d.ts +7 -1
- package/dist/core/audit-analysis.js +111 -0
- package/dist/core/audit-metrics.d.ts +7 -0
- package/dist/core/audit-metrics.js +24 -4
- package/dist/core/audit-query.d.ts +1 -0
- package/dist/core/audit-query.js +3 -0
- package/dist/core/audit-replay-context.d.ts +35 -0
- package/dist/core/audit-replay-context.js +88 -0
- package/dist/core/audit-types.d.ts +24 -1
- package/dist/core/audit-types.js +1 -1
- package/dist/core/capability/index.d.ts +2 -1
- package/dist/core/capability/index.js +1 -0
- package/dist/core/capability/paths.d.ts +2 -2
- package/dist/core/capability/paths.js +9 -9
- package/dist/core/capability/trusted-workspace-roots.d.ts +25 -0
- package/dist/core/capability/trusted-workspace-roots.js +149 -0
- package/dist/core/capability/types.d.ts +11 -1
- package/dist/core/capability-approval.d.ts +2 -1
- package/dist/core/capability-approval.js +100 -2
- package/dist/core/classify-subagent.js +2 -20
- package/dist/core/classify-tool.js +32 -3
- package/dist/core/config.d.ts +18 -0
- package/dist/core/config.js +49 -0
- package/dist/core/gate-engine.js +6 -6
- package/dist/core/harvest.d.ts +53 -0
- package/dist/core/harvest.js +276 -0
- package/dist/core/index.d.ts +9 -3
- package/dist/core/index.js +6 -2
- package/dist/core/judge-doctor.d.ts +3 -0
- package/dist/core/judge-doctor.js +54 -0
- package/dist/core/path-utils.d.ts +11 -0
- package/dist/core/path-utils.js +56 -6
- package/dist/core/reclassify.d.ts +3 -0
- package/dist/core/reclassify.js +44 -14
- package/dist/core/replay-scrub.d.ts +9 -0
- package/dist/core/replay-scrub.js +43 -0
- package/dist/core/shell-tokenizer.d.ts +2 -0
- package/dist/core/shell-tokenizer.js +82 -23
- package/dist/core/standing-allow.d.ts +50 -0
- package/dist/core/standing-allow.js +175 -0
- package/dist/core/types.d.ts +15 -0
- package/dist/core/verdict/adapter.js +10 -1
- package/dist/core/verdict/containment.d.ts +4 -3
- package/dist/core/verdict/containment.js +17 -12
- package/dist/core/verdict/judge-audit.d.ts +1 -0
- package/dist/core/verdict/judge-audit.js +32 -1
- package/dist/core/verdict/judge-baseline.d.ts +20 -0
- package/dist/core/verdict/judge-baseline.js +48 -0
- package/dist/core/verdict/judge-broker-service.d.ts +42 -0
- package/dist/core/verdict/judge-broker-service.js +279 -0
- package/dist/core/verdict/judge-cli-fingerprint.d.ts +5 -0
- package/dist/core/verdict/judge-cli-fingerprint.js +61 -0
- package/dist/core/verdict/judge-cli.d.ts +23 -0
- package/dist/core/verdict/judge-cli.js +137 -16
- package/dist/core/verdict/judge-factory.js +13 -4
- package/dist/core/verdict/judge-provider-matrix.d.ts +20 -0
- package/dist/core/verdict/judge-provider-matrix.js +66 -0
- package/dist/core/verdict/judge-runtime-config.d.ts +42 -0
- package/dist/core/verdict/judge-runtime-config.js +92 -0
- package/dist/core/verdict/judge-session-broker.d.ts +48 -0
- package/dist/core/verdict/judge-session-broker.js +195 -0
- package/dist/core/verdict/judge-session-guard.d.ts +27 -0
- package/dist/core/verdict/judge-session-guard.js +91 -0
- package/dist/core/verdict/judge-session-kill-switch.d.ts +11 -0
- package/dist/core/verdict/judge-session-kill-switch.js +43 -0
- package/dist/core/verdict/judge-session-mutex.d.ts +9 -0
- package/dist/core/verdict/judge-session-mutex.js +23 -0
- package/dist/core/verdict/judge-shadow.d.ts +24 -0
- package/dist/core/verdict/judge-shadow.js +116 -0
- package/dist/core/verdict/judge-transport.d.ts +40 -0
- package/dist/core/verdict/judge-transport.js +258 -0
- package/dist/core/verdict/judge.d.ts +14 -1
- package/dist/core/verdict/judge.js +6 -5
- package/dist/core/verdict/parser.js +6 -1
- package/dist/core/verdict/types.d.ts +13 -0
- package/dist/core/verdict/verdict.js +7 -4
- package/dist/corpus/adversarial-probe.d.ts +86 -0
- package/dist/corpus/adversarial-probe.js +220 -0
- package/dist/corpus/evaluate.d.ts +45 -12
- package/dist/corpus/evaluate.js +62 -5
- package/dist/corpus/gates.d.ts +35 -0
- package/dist/corpus/gates.js +81 -0
- package/dist/corpus/judge-accuracy.d.ts +8 -0
- package/dist/corpus/judge-accuracy.js +39 -0
- package/dist/corpus/must-allow-commands.d.ts +5 -0
- package/dist/corpus/must-allow-commands.js +13 -0
- package/dist/corpus/mutators.d.ts +28 -0
- package/dist/corpus/mutators.js +151 -0
- package/dist/corpus/ratchet.d.ts +42 -0
- package/dist/corpus/ratchet.js +116 -0
- package/dist/corpus/runtime-match.d.ts +19 -0
- package/dist/corpus/runtime-match.js +45 -0
- package/dist/corpus/standing-allow-catalog.generated.d.ts +13 -0
- package/dist/corpus/standing-allow-catalog.generated.js +79 -0
- package/dist/corpus/types.d.ts +47 -0
- package/dist/corpus/types.js +157 -0
- package/dist/installer.js +4 -1
- package/dist/judge-broker-daemon.d.ts +1 -0
- package/dist/judge-broker-daemon.js +123 -0
- package/dist/services/sandbox-service.d.ts +1 -0
- package/dist/services/sandbox-service.js +2 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -3
- package/skills/belay/SKILL.md +7 -1
- package/skills/belay/belay-approve.md +17 -0
|
@@ -145,3 +145,114 @@ export function countVerdicts(records) {
|
|
|
145
145
|
}
|
|
146
146
|
return counts;
|
|
147
147
|
}
|
|
148
|
+
export function computeWouldBlockByReason(records) {
|
|
149
|
+
const counts = {};
|
|
150
|
+
for (const record of records) {
|
|
151
|
+
if (!isGateRecord(record) || !inferWouldBlock(record)) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const reason = record.reason ?? 'unknown';
|
|
155
|
+
counts[reason] = (counts[reason] ?? 0) + 1;
|
|
156
|
+
}
|
|
157
|
+
return counts;
|
|
158
|
+
}
|
|
159
|
+
/** Approval ratios are candidate signals from operator behavior — not corpus ground truth. */
|
|
160
|
+
export function computeApprovalRatioByReason(records, roundTrips) {
|
|
161
|
+
const wouldBlockByReason = new Map();
|
|
162
|
+
const approvedByReason = new Map();
|
|
163
|
+
for (const record of records) {
|
|
164
|
+
if (!isGateRecord(record) || !inferWouldBlock(record)) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const reason = record.reason ?? 'unknown';
|
|
168
|
+
wouldBlockByReason.set(reason, (wouldBlockByReason.get(reason) ?? 0) + 1);
|
|
169
|
+
}
|
|
170
|
+
for (const trip of roundTrips) {
|
|
171
|
+
if (!trip.approvalTimestamp) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
approvedByReason.set(trip.reason, (approvedByReason.get(trip.reason) ?? 0) + 1);
|
|
175
|
+
}
|
|
176
|
+
return [...wouldBlockByReason.entries()]
|
|
177
|
+
.map(([reason, wouldBlockCount]) => {
|
|
178
|
+
const approvedAfterDenyCount = approvedByReason.get(reason) ?? 0;
|
|
179
|
+
return {
|
|
180
|
+
reason,
|
|
181
|
+
wouldBlockCount,
|
|
182
|
+
approvedAfterDenyCount,
|
|
183
|
+
approvalRate: wouldBlockCount > 0 ? approvedAfterDenyCount / wouldBlockCount : 0,
|
|
184
|
+
};
|
|
185
|
+
})
|
|
186
|
+
.sort((left, right) => right.wouldBlockCount - left.wouldBlockCount);
|
|
187
|
+
}
|
|
188
|
+
function judgeFallbackReason(record) {
|
|
189
|
+
return typeof record.judgeFallbackReason === 'string' ? record.judgeFallbackReason : '';
|
|
190
|
+
}
|
|
191
|
+
function isJudgeTimeoutFallback(fallback) {
|
|
192
|
+
return fallback.includes('timeout');
|
|
193
|
+
}
|
|
194
|
+
export function isAvailabilityCausedAsk(record) {
|
|
195
|
+
if (!isGateRecord(record) || !inferWouldBlock(record)) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
if (record.reason === 'missing_trusted_cwd') {
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
return judgeFallbackReason(record).length > 0;
|
|
202
|
+
}
|
|
203
|
+
export function computeAvailabilityAskCounts(records) {
|
|
204
|
+
const counts = {
|
|
205
|
+
total: 0,
|
|
206
|
+
missingTrustedCwd: 0,
|
|
207
|
+
judgeTimeout: 0,
|
|
208
|
+
judgeFallback: 0,
|
|
209
|
+
};
|
|
210
|
+
for (const record of records) {
|
|
211
|
+
if (!isGateRecord(record) || !inferWouldBlock(record)) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (record.reason === 'missing_trusted_cwd') {
|
|
215
|
+
counts.missingTrustedCwd += 1;
|
|
216
|
+
counts.total += 1;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const fallback = judgeFallbackReason(record);
|
|
220
|
+
if (!fallback) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (isJudgeTimeoutFallback(fallback)) {
|
|
224
|
+
counts.judgeTimeout += 1;
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
counts.judgeFallback += 1;
|
|
228
|
+
}
|
|
229
|
+
counts.total += 1;
|
|
230
|
+
}
|
|
231
|
+
return counts;
|
|
232
|
+
}
|
|
233
|
+
export function computeRepeatedFingerprintAsks(records, minCount = 2, limit = 10) {
|
|
234
|
+
const grouped = new Map();
|
|
235
|
+
for (const record of records) {
|
|
236
|
+
if (!isGateRecord(record) || !inferWouldBlock(record) || !record.fingerprint) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const fingerprint = record.fingerprint;
|
|
240
|
+
const existing = grouped.get(fingerprint);
|
|
241
|
+
if (existing) {
|
|
242
|
+
existing.askCount += 1;
|
|
243
|
+
existing.summary = record.summary ?? existing.summary;
|
|
244
|
+
existing.reason = record.reason ?? existing.reason;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
grouped.set(fingerprint, {
|
|
248
|
+
fingerprint,
|
|
249
|
+
summary: record.summary ?? '',
|
|
250
|
+
reason: record.reason ?? 'unknown',
|
|
251
|
+
askCount: 1,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
return [...grouped.values()]
|
|
255
|
+
.filter((entry) => entry.askCount >= minCount)
|
|
256
|
+
.sort((left, right) => right.askCount - left.askCount)
|
|
257
|
+
.slice(0, limit);
|
|
258
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { buildApprovalRoundTrips, filterAuditRecords, toAuditRecord } from './audit-query.js';
|
|
2
|
+
import type { AvailabilityAskCounts, ReasonApprovalRatio, RepeatedFingerprintAsk } from './audit-types.js';
|
|
2
3
|
/** Minimum gate events before recommending enforce with zero would-block rate. */
|
|
3
4
|
export declare const MIN_GATE_EVENTS_FOR_ENFORCE = 20;
|
|
4
5
|
export interface AuditMetricsReport {
|
|
@@ -9,6 +10,12 @@ export interface AuditMetricsReport {
|
|
|
9
10
|
gateEvents: number;
|
|
10
11
|
wouldBlockCount: number;
|
|
11
12
|
wouldBlockRate: number;
|
|
13
|
+
classifierWouldBlockCount: number;
|
|
14
|
+
classifierWouldBlockRate: number;
|
|
15
|
+
wouldBlockByReason: Record<string, number>;
|
|
16
|
+
approvalRatioByReason: ReasonApprovalRatio[];
|
|
17
|
+
availabilityAsks: AvailabilityAskCounts;
|
|
18
|
+
repeatedFingerprintAsks: RepeatedFingerprintAsk[];
|
|
12
19
|
byReason: Record<string, number>;
|
|
13
20
|
byKind: Record<string, number>;
|
|
14
21
|
byVerdict: Record<string, number>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { bucketGateEventsByDay, computeApprovalLatencyStats, countVerdicts, detectBypassAttempts, detectNoisyRules, } from './audit-analysis.js';
|
|
1
|
+
import { bucketGateEventsByDay, computeApprovalLatencyStats, computeApprovalRatioByReason, computeAvailabilityAskCounts, computeRepeatedFingerprintAsks, computeWouldBlockByReason, countVerdicts, detectBypassAttempts, detectNoisyRules, } from './audit-analysis.js';
|
|
2
2
|
import { buildApprovalRoundTrips, filterAuditRecords, inferWouldBlock, isApprovalRecorded, toAuditRecord, } from './audit-query.js';
|
|
3
3
|
import { AUDIT_METRICS_SCHEMA_VERSION, GATE_EVENTS } from './audit-types.js';
|
|
4
4
|
/** Minimum gate events before recommending enforce with zero would-block rate. */
|
|
@@ -78,7 +78,13 @@ export function computeAuditMetrics(records, options = {}) {
|
|
|
78
78
|
const approvalLatency = computeApprovalLatencyStats(roundTrips);
|
|
79
79
|
const bypassAttempts = detectBypassAttempts(auditRecords);
|
|
80
80
|
const noisyRuleCandidates = detectNoisyRules(auditRecords, roundTrips);
|
|
81
|
+
const wouldBlockByReason = computeWouldBlockByReason(auditRecords);
|
|
82
|
+
const approvalRatioByReason = computeApprovalRatioByReason(auditRecords, roundTrips);
|
|
83
|
+
const availabilityAsks = computeAvailabilityAskCounts(auditRecords);
|
|
84
|
+
const repeatedFingerprintAsks = computeRepeatedFingerprintAsks(auditRecords);
|
|
81
85
|
const wouldBlockRate = gateEvents > 0 ? wouldBlockCount / gateEvents : 0;
|
|
86
|
+
const classifierWouldBlockCount = Math.max(0, wouldBlockCount - availabilityAsks.total);
|
|
87
|
+
const classifierWouldBlockRate = gateEvents > 0 ? classifierWouldBlockCount / gateEvents : 0;
|
|
82
88
|
const topWouldBlockSummaries = [...summaryCounts.values()]
|
|
83
89
|
.sort((left, right) => right.count - left.count)
|
|
84
90
|
.slice(0, 10);
|
|
@@ -101,16 +107,16 @@ export function computeAuditMetrics(records, options = {}) {
|
|
|
101
107
|
}
|
|
102
108
|
}
|
|
103
109
|
else {
|
|
104
|
-
notes.push(`${wouldBlockCount} would-block event(s) (${(wouldBlockRate * 100).toFixed(1)}% of gate traffic). Review top summaries and add overrides.allow where appropriate.`);
|
|
110
|
+
notes.push(`${wouldBlockCount} would-block event(s) (${(wouldBlockRate * 100).toFixed(1)}% of gate traffic; classifier-quality ${(classifierWouldBlockRate * 100).toFixed(1)}%). Review top summaries and add overrides.allow where appropriate.`);
|
|
105
111
|
if (approvalRecordedCount > 0) {
|
|
106
112
|
notes.push(`${approvalRecordedCount} approval(s) recorded — these likely indicate actions operators wanted.`);
|
|
107
113
|
}
|
|
108
114
|
else {
|
|
109
115
|
notes.push('Review top would-block summaries and add overrides.allow for legitimate commands before switching to enforce.');
|
|
110
116
|
}
|
|
111
|
-
if (
|
|
117
|
+
if (classifierWouldBlockRate < 0.05 && gateEvents >= 20 && availabilityAsks.total === 0) {
|
|
112
118
|
readyForEnforce = true;
|
|
113
|
-
notes.push('
|
|
119
|
+
notes.push('Classifier-quality would-block rate is below 5% with sufficient sample size — consider enforce mode.');
|
|
114
120
|
}
|
|
115
121
|
}
|
|
116
122
|
}
|
|
@@ -120,6 +126,14 @@ export function computeAuditMetrics(records, options = {}) {
|
|
|
120
126
|
else {
|
|
121
127
|
notes.push('Set policy.unknownLocalEffect to "deny" to dogfood fail-closed defaults.');
|
|
122
128
|
}
|
|
129
|
+
if (availabilityAsks.total > 0) {
|
|
130
|
+
readyForEnforce = false;
|
|
131
|
+
notes.push(`${availabilityAsks.total} availability-caused ask(s) — tune infrastructure before corpus overrides.`);
|
|
132
|
+
notes.push('Ready for enforce withheld while availability-caused asks are present.');
|
|
133
|
+
}
|
|
134
|
+
if (repeatedFingerprintAsks.length > 0) {
|
|
135
|
+
notes.push(`${repeatedFingerprintAsks.length} repeated fingerprint ask pattern(s) — review standing-allow / cache candidates.`);
|
|
136
|
+
}
|
|
123
137
|
if (noisyRuleCandidates.length > 0) {
|
|
124
138
|
notes.push(`${noisyRuleCandidates.length} noisy rule candidate(s) — high deny-then-approve rate.`);
|
|
125
139
|
}
|
|
@@ -131,6 +145,12 @@ export function computeAuditMetrics(records, options = {}) {
|
|
|
131
145
|
gateEvents,
|
|
132
146
|
wouldBlockCount,
|
|
133
147
|
wouldBlockRate,
|
|
148
|
+
classifierWouldBlockCount,
|
|
149
|
+
classifierWouldBlockRate,
|
|
150
|
+
wouldBlockByReason,
|
|
151
|
+
approvalRatioByReason,
|
|
152
|
+
availabilityAsks,
|
|
153
|
+
repeatedFingerprintAsks,
|
|
134
154
|
byReason,
|
|
135
155
|
byKind,
|
|
136
156
|
byVerdict,
|
|
@@ -2,6 +2,7 @@ import type { ApprovalRoundTrip, AuditFilter, AuditRecord } from './audit-types.
|
|
|
2
2
|
export declare function toAuditRecord(value: Record<string, unknown>): AuditRecord;
|
|
3
3
|
export declare function parseTimestamp(value?: string): number | null;
|
|
4
4
|
export declare function isGateRecord(record: AuditRecord): boolean;
|
|
5
|
+
export declare function isShellGateRecord(record: AuditRecord): boolean;
|
|
5
6
|
export declare function isApprovalRecorded(record: AuditRecord): boolean;
|
|
6
7
|
export declare function inferWouldBlock(record: AuditRecord): boolean;
|
|
7
8
|
export declare function recordStringField(record: AuditRecord, field: 'effect' | 'reason' | 'permission' | 'verdict' | 'mode' | 'summary' | 'fingerprint' | 'location'): string;
|
package/dist/core/audit-query.js
CHANGED
|
@@ -16,6 +16,9 @@ export function parseTimestamp(value) {
|
|
|
16
16
|
export function isGateRecord(record) {
|
|
17
17
|
return typeof record.event === 'string' && GATE_EVENTS.has(record.event);
|
|
18
18
|
}
|
|
19
|
+
export function isShellGateRecord(record) {
|
|
20
|
+
return (isGateRecord(record) && (record.event === 'beforeShellExecution' || record.kind === 'shell'));
|
|
21
|
+
}
|
|
19
22
|
export function isApprovalRecorded(record) {
|
|
20
23
|
return ((record.event === 'approval' ||
|
|
21
24
|
(record.event === 'beforeSubmitPrompt' && record.reason === 'approval_recorded')) &&
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { GatedActionKind } from './gate-contract.js';
|
|
2
|
+
import type { ClassifyResult } from './types.js';
|
|
3
|
+
/** Preserved action context for simulate triage — not a safety gate. */
|
|
4
|
+
export interface AuditReplayContext {
|
|
5
|
+
cwd: string;
|
|
6
|
+
kind: GatedActionKind;
|
|
7
|
+
command?: string;
|
|
8
|
+
toolName?: string;
|
|
9
|
+
payload?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
/** Classifier-facing action snapshot for high-fidelity simulate replay. */
|
|
12
|
+
export interface AuditActionSnapshot {
|
|
13
|
+
schemaVersion: 1;
|
|
14
|
+
kind: GatedActionKind;
|
|
15
|
+
cwd: string;
|
|
16
|
+
normalizedAction: string;
|
|
17
|
+
toolName?: string;
|
|
18
|
+
payloadHash?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ReplayActionLike {
|
|
21
|
+
cwd?: string;
|
|
22
|
+
kind?: string;
|
|
23
|
+
command?: string;
|
|
24
|
+
toolName?: string;
|
|
25
|
+
payload?: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
export declare function hashReplayPayload(payload: Record<string, unknown>): string;
|
|
28
|
+
export declare function buildAuditActionSnapshot(kind: GatedActionKind, result: Pick<ClassifyResult, 'normalizedCommand' | 'summary'>, replayAction?: ReplayActionLike): AuditActionSnapshot | undefined;
|
|
29
|
+
export declare function parseAuditActionSnapshot(record: {
|
|
30
|
+
actionSnapshot?: unknown;
|
|
31
|
+
}): AuditActionSnapshot | null;
|
|
32
|
+
export declare function buildAuditReplayContext(kind: GatedActionKind, result: Pick<ClassifyResult, 'normalizedCommand' | 'summary'>, replayAction?: ReplayActionLike): AuditReplayContext | undefined;
|
|
33
|
+
export declare function parseAuditReplayContext(record: {
|
|
34
|
+
replayContext?: unknown;
|
|
35
|
+
}): AuditReplayContext | null;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
export function hashReplayPayload(payload) {
|
|
3
|
+
return createHash('sha256').update(JSON.stringify(payload)).digest('hex');
|
|
4
|
+
}
|
|
5
|
+
export function buildAuditActionSnapshot(kind, result, replayAction) {
|
|
6
|
+
if (!replayAction?.cwd) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
const replayKind = replayAction.kind;
|
|
10
|
+
const resolvedKind = replayKind === 'shell' || replayKind === 'tool' || replayKind === 'subagent' ? replayKind : kind;
|
|
11
|
+
const normalizedAction = result.normalizedCommand ?? replayAction.command ?? result.summary ?? '';
|
|
12
|
+
if (!normalizedAction.trim()) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
schemaVersion: 1,
|
|
17
|
+
kind: resolvedKind,
|
|
18
|
+
cwd: replayAction.cwd,
|
|
19
|
+
normalizedAction,
|
|
20
|
+
...(replayAction.toolName ? { toolName: replayAction.toolName } : {}),
|
|
21
|
+
...(replayAction.payload ? { payloadHash: hashReplayPayload(replayAction.payload) } : {}),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function parseAuditActionSnapshot(record) {
|
|
25
|
+
const raw = record.actionSnapshot;
|
|
26
|
+
if (!raw || typeof raw !== 'object') {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const snapshot = raw;
|
|
30
|
+
if (snapshot.schemaVersion !== 1) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
if (typeof snapshot.cwd !== 'string' || !snapshot.cwd.trim()) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const kind = snapshot.kind;
|
|
37
|
+
if (kind !== 'shell' && kind !== 'tool' && kind !== 'subagent') {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (typeof snapshot.normalizedAction !== 'string' || !snapshot.normalizedAction.trim()) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
schemaVersion: 1,
|
|
45
|
+
kind,
|
|
46
|
+
cwd: snapshot.cwd,
|
|
47
|
+
normalizedAction: snapshot.normalizedAction,
|
|
48
|
+
...(typeof snapshot.toolName === 'string' ? { toolName: snapshot.toolName } : {}),
|
|
49
|
+
...(typeof snapshot.payloadHash === 'string' ? { payloadHash: snapshot.payloadHash } : {}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function buildAuditReplayContext(kind, result, replayAction) {
|
|
53
|
+
if (!replayAction?.cwd) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const replayKind = replayAction.kind;
|
|
57
|
+
const resolvedKind = replayKind === 'shell' || replayKind === 'tool' || replayKind === 'subagent' ? replayKind : kind;
|
|
58
|
+
return {
|
|
59
|
+
cwd: replayAction.cwd,
|
|
60
|
+
kind: resolvedKind,
|
|
61
|
+
command: replayAction.command ?? result.normalizedCommand ?? result.summary,
|
|
62
|
+
...(replayAction.toolName ? { toolName: replayAction.toolName } : {}),
|
|
63
|
+
...(replayAction.payload ? { payload: replayAction.payload } : {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export function parseAuditReplayContext(record) {
|
|
67
|
+
const raw = record.replayContext;
|
|
68
|
+
if (!raw || typeof raw !== 'object') {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const ctx = raw;
|
|
72
|
+
if (typeof ctx.cwd !== 'string' || !ctx.cwd.trim()) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const kind = ctx.kind;
|
|
76
|
+
if (kind !== 'shell' && kind !== 'tool' && kind !== 'subagent') {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
cwd: ctx.cwd,
|
|
81
|
+
kind,
|
|
82
|
+
...(typeof ctx.command === 'string' ? { command: ctx.command } : {}),
|
|
83
|
+
...(typeof ctx.toolName === 'string' ? { toolName: ctx.toolName } : {}),
|
|
84
|
+
...(ctx.payload && typeof ctx.payload === 'object' && !Array.isArray(ctx.payload)
|
|
85
|
+
? { payload: ctx.payload }
|
|
86
|
+
: {}),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import type { AuditActionSnapshot, AuditReplayContext } from './audit-replay-context.js';
|
|
1
2
|
import type { Assessment } from './types.js';
|
|
2
|
-
export
|
|
3
|
+
export type { AuditActionSnapshot, AuditReplayContext } from './audit-replay-context.js';
|
|
4
|
+
export declare const AUDIT_METRICS_SCHEMA_VERSION = 3;
|
|
3
5
|
export declare const GATE_EVENTS: Set<string>;
|
|
4
6
|
export interface AuditRecord {
|
|
5
7
|
timestamp?: string;
|
|
@@ -11,6 +13,7 @@ export interface AuditRecord {
|
|
|
11
13
|
summary?: string;
|
|
12
14
|
approvalId?: string;
|
|
13
15
|
wouldBlock?: boolean;
|
|
16
|
+
judgeFallbackReason?: string;
|
|
14
17
|
permission?: string;
|
|
15
18
|
mode?: string;
|
|
16
19
|
assessment?: Assessment;
|
|
@@ -21,6 +24,8 @@ export interface AuditRecord {
|
|
|
21
24
|
transactionalCategories?: string[];
|
|
22
25
|
transactionalChangeCount?: number;
|
|
23
26
|
transactionalSkipReason?: string;
|
|
27
|
+
replayContext?: AuditReplayContext;
|
|
28
|
+
actionSnapshot?: AuditActionSnapshot;
|
|
24
29
|
[key: string]: unknown;
|
|
25
30
|
}
|
|
26
31
|
export interface AuditFilter {
|
|
@@ -63,3 +68,21 @@ export interface NoisyRuleCandidate {
|
|
|
63
68
|
approvedCount: number;
|
|
64
69
|
approvalRate: number;
|
|
65
70
|
}
|
|
71
|
+
export interface ReasonApprovalRatio {
|
|
72
|
+
reason: string;
|
|
73
|
+
wouldBlockCount: number;
|
|
74
|
+
approvedAfterDenyCount: number;
|
|
75
|
+
approvalRate: number;
|
|
76
|
+
}
|
|
77
|
+
export interface RepeatedFingerprintAsk {
|
|
78
|
+
fingerprint: string;
|
|
79
|
+
summary: string;
|
|
80
|
+
reason: string;
|
|
81
|
+
askCount: number;
|
|
82
|
+
}
|
|
83
|
+
export interface AvailabilityAskCounts {
|
|
84
|
+
total: number;
|
|
85
|
+
missingTrustedCwd: number;
|
|
86
|
+
judgeTimeout: number;
|
|
87
|
+
judgeFallback: number;
|
|
88
|
+
}
|
package/dist/core/audit-types.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const AUDIT_METRICS_SCHEMA_VERSION =
|
|
1
|
+
export const AUDIT_METRICS_SCHEMA_VERSION = 3;
|
|
2
2
|
export const GATE_EVENTS = new Set(['beforeShellExecution', 'preToolUse', 'subagentGate']);
|
|
@@ -2,4 +2,5 @@ export { addPathToAllowlist, allPathsAllowlisted, fsScopeAllowlistPath, isPathAl
|
|
|
2
2
|
export { evaluateL1FullStatus, hasSandboxRuntime, isCapabilityBrokerDemotionActive, isSandboxBrokerEnabled, } from './broker.js';
|
|
3
3
|
export { collectOutsideRepoPaths, collectOutsideRepoPathsFromToolPayload } from './paths.js';
|
|
4
4
|
export { FS_SCOPE_REASONS, shouldSkipBrokerApprovedOnce, shouldSkipBrokerApprovedRecord, } from './reasons.js';
|
|
5
|
-
export
|
|
5
|
+
export { addTrustedWorkspaceRoot, isBroadTrustedWorkspaceRoot, isHighStakesTrustedWorkspaceRoot, isPathWithinTrustedWorkspaceRoots, loadTrustedWorkspaceRoots, loadTrustedWorkspaceRootsSync, normalizeTrustedWorkspaceRootPath, saveTrustedWorkspaceRoots, trustedWorkspaceRootsPath, validateTrustedWorkspaceRootCandidate, } from './trusted-workspace-roots.js';
|
|
6
|
+
export type { CapabilityApprovalScope, FsScopeAllowlistEntry, FsScopeAllowlistFile, TrustedWorkspaceRootEntry, TrustedWorkspaceRootsFile, } from './types.js';
|
|
@@ -2,3 +2,4 @@ export { addPathToAllowlist, allPathsAllowlisted, fsScopeAllowlistPath, isPathAl
|
|
|
2
2
|
export { evaluateL1FullStatus, hasSandboxRuntime, isCapabilityBrokerDemotionActive, isSandboxBrokerEnabled, } from './broker.js';
|
|
3
3
|
export { collectOutsideRepoPaths, collectOutsideRepoPathsFromToolPayload } from './paths.js';
|
|
4
4
|
export { FS_SCOPE_REASONS, shouldSkipBrokerApprovedOnce, shouldSkipBrokerApprovedRecord, } from './reasons.js';
|
|
5
|
+
export { addTrustedWorkspaceRoot, isBroadTrustedWorkspaceRoot, isHighStakesTrustedWorkspaceRoot, isPathWithinTrustedWorkspaceRoots, loadTrustedWorkspaceRoots, loadTrustedWorkspaceRootsSync, normalizeTrustedWorkspaceRootPath, saveTrustedWorkspaceRoots, trustedWorkspaceRootsPath, validateTrustedWorkspaceRootCandidate, } from './trusted-workspace-roots.js';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare function collectOutsideRepoPaths(command: string, cwd: string, repoRoot: string): string[];
|
|
2
|
-
export declare function collectOutsideRepoPathsFromToolPayload(payload: Record<string, unknown>, cwd: string, repoRoot: string): string[];
|
|
1
|
+
export declare function collectOutsideRepoPaths(command: string, cwd: string, repoRoot: string, trustedWorkspaceRoots?: string[]): string[];
|
|
2
|
+
export declare function collectOutsideRepoPathsFromToolPayload(payload: Record<string, unknown>, cwd: string, repoRoot: string, trustedWorkspaceRoots?: string[]): string[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import {
|
|
2
|
+
import { resolveMutationTarget, resolveWorkspaceRootMatch } from '../path-utils.js';
|
|
3
3
|
import { extractRedirectTargets, tokenizeShell } from '../shell-tokenizer.js';
|
|
4
4
|
function applyPatchTargets(patch) {
|
|
5
5
|
const targets = [];
|
|
@@ -42,25 +42,25 @@ function extractToolPatch(payload) {
|
|
|
42
42
|
}
|
|
43
43
|
return null;
|
|
44
44
|
}
|
|
45
|
-
function addOutsideRepoPath(paths, token, cwd, repoRoot) {
|
|
45
|
+
function addOutsideRepoPath(paths, token, cwd, repoRoot, trustedWorkspaceRoots = []) {
|
|
46
46
|
const resolved = resolveMutationTarget(token, cwd);
|
|
47
|
-
if (resolved &&
|
|
47
|
+
if (resolved && resolveWorkspaceRootMatch(repoRoot, trustedWorkspaceRoots, resolved) === null) {
|
|
48
48
|
paths.add(resolved);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
export function collectOutsideRepoPaths(command, cwd, repoRoot) {
|
|
51
|
+
export function collectOutsideRepoPaths(command, cwd, repoRoot, trustedWorkspaceRoots = []) {
|
|
52
52
|
const tokens = tokenizeShell(command);
|
|
53
53
|
const redirects = extractRedirectTargets(tokens);
|
|
54
54
|
const paths = new Set();
|
|
55
55
|
for (const token of tokens.slice(1)) {
|
|
56
|
-
addOutsideRepoPath(paths, token, cwd, repoRoot);
|
|
56
|
+
addOutsideRepoPath(paths, token, cwd, repoRoot, trustedWorkspaceRoots);
|
|
57
57
|
}
|
|
58
58
|
for (const redirect of redirects) {
|
|
59
|
-
addOutsideRepoPath(paths, redirect, cwd, repoRoot);
|
|
59
|
+
addOutsideRepoPath(paths, redirect, cwd, repoRoot, trustedWorkspaceRoots);
|
|
60
60
|
}
|
|
61
61
|
return [...paths];
|
|
62
62
|
}
|
|
63
|
-
export function collectOutsideRepoPathsFromToolPayload(payload, cwd, repoRoot) {
|
|
63
|
+
export function collectOutsideRepoPathsFromToolPayload(payload, cwd, repoRoot, trustedWorkspaceRoots = []) {
|
|
64
64
|
const toolKind = String(payload.tool_name ?? '')
|
|
65
65
|
.trim()
|
|
66
66
|
.toLowerCase();
|
|
@@ -68,7 +68,7 @@ export function collectOutsideRepoPathsFromToolPayload(payload, cwd, repoRoot) {
|
|
|
68
68
|
const filePath = extractToolFilePath(payload);
|
|
69
69
|
if (filePath) {
|
|
70
70
|
const resolved = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
|
|
71
|
-
if (
|
|
71
|
+
if (resolveWorkspaceRootMatch(repoRoot, trustedWorkspaceRoots, resolved) === null) {
|
|
72
72
|
paths.add(resolved);
|
|
73
73
|
}
|
|
74
74
|
return [...paths];
|
|
@@ -78,7 +78,7 @@ export function collectOutsideRepoPathsFromToolPayload(payload, cwd, repoRoot) {
|
|
|
78
78
|
if (patch) {
|
|
79
79
|
for (const target of applyPatchTargets(patch)) {
|
|
80
80
|
const resolved = path.isAbsolute(target) ? target : path.resolve(cwd, target);
|
|
81
|
-
if (
|
|
81
|
+
if (resolveWorkspaceRootMatch(repoRoot, trustedWorkspaceRoots, resolved) === null) {
|
|
82
82
|
paths.add(resolved);
|
|
83
83
|
}
|
|
84
84
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { BelayConfigV3 } from '../config.js';
|
|
2
|
+
import type { TrustedWorkspaceRootEntry, TrustedWorkspaceRootsFile } from './types.js';
|
|
3
|
+
export declare function trustedWorkspaceRootsPath(config: BelayConfigV3, repoLocalStateDir: string): string;
|
|
4
|
+
export declare function loadTrustedWorkspaceRoots(filePath: string): Promise<TrustedWorkspaceRootsFile>;
|
|
5
|
+
export declare function loadTrustedWorkspaceRootsSync(filePath: string): TrustedWorkspaceRootsFile;
|
|
6
|
+
export declare function saveTrustedWorkspaceRoots(filePath: string, state: TrustedWorkspaceRootsFile): Promise<void>;
|
|
7
|
+
export declare function normalizeTrustedWorkspaceRootPath(targetPath: string): string;
|
|
8
|
+
export interface TrustedWorkspaceRootValidationParams {
|
|
9
|
+
candidatePath: string;
|
|
10
|
+
repoRoot: string;
|
|
11
|
+
controlPlaneDir?: string | null;
|
|
12
|
+
protectedRoots?: string[];
|
|
13
|
+
requireExistingDirectory?: boolean;
|
|
14
|
+
requireNonGit?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface TrustedWorkspaceRootValidationResult {
|
|
17
|
+
ok: boolean;
|
|
18
|
+
normalizedPath: string;
|
|
19
|
+
reason?: 'not_directory' | 'broad_root' | 'high_stakes' | 'inside_repo' | 'inside_git_repo' | 'control_plane_overlap' | 'protected_root_overlap';
|
|
20
|
+
}
|
|
21
|
+
export declare function isBroadTrustedWorkspaceRoot(targetPath: string): boolean;
|
|
22
|
+
export declare function isHighStakesTrustedWorkspaceRoot(targetPath: string): boolean;
|
|
23
|
+
export declare function validateTrustedWorkspaceRootCandidate(params: TrustedWorkspaceRootValidationParams): TrustedWorkspaceRootValidationResult;
|
|
24
|
+
export declare function isPathWithinTrustedWorkspaceRoots(absolutePath: string, roots: TrustedWorkspaceRootsFile): boolean;
|
|
25
|
+
export declare function addTrustedWorkspaceRoot(roots: TrustedWorkspaceRootsFile, entry: TrustedWorkspaceRootEntry): TrustedWorkspaceRootsFile;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { belayStateDir } from '../config.js';
|
|
5
|
+
import { canonicalPath, containingGitRoot, pathWithinRoot, resolveWorkspaceRootMatch, } from '../path-utils.js';
|
|
6
|
+
export function trustedWorkspaceRootsPath(config, repoLocalStateDir) {
|
|
7
|
+
return path.join(belayStateDir(config, repoLocalStateDir), 'trusted-workspace-roots.json');
|
|
8
|
+
}
|
|
9
|
+
export async function loadTrustedWorkspaceRoots(filePath) {
|
|
10
|
+
if (!existsSync(filePath)) {
|
|
11
|
+
return { version: 1, roots: [] };
|
|
12
|
+
}
|
|
13
|
+
const raw = JSON.parse(await readFile(filePath, 'utf8'));
|
|
14
|
+
return { version: 1, roots: sanitizeTrustedWorkspaceRootEntries(raw.roots) };
|
|
15
|
+
}
|
|
16
|
+
export function loadTrustedWorkspaceRootsSync(filePath) {
|
|
17
|
+
if (!existsSync(filePath)) {
|
|
18
|
+
return { version: 1, roots: [] };
|
|
19
|
+
}
|
|
20
|
+
const raw = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
21
|
+
return { version: 1, roots: sanitizeTrustedWorkspaceRootEntries(raw.roots) };
|
|
22
|
+
}
|
|
23
|
+
export async function saveTrustedWorkspaceRoots(filePath, state) {
|
|
24
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
25
|
+
await writeFile(filePath, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
|
|
26
|
+
}
|
|
27
|
+
export function normalizeTrustedWorkspaceRootPath(targetPath) {
|
|
28
|
+
return canonicalPath(targetPath);
|
|
29
|
+
}
|
|
30
|
+
function sanitizeTrustedWorkspaceRootEntries(input) {
|
|
31
|
+
if (!Array.isArray(input)) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
return input.flatMap((entry) => {
|
|
35
|
+
if (!entry || typeof entry !== 'object') {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
const record = entry;
|
|
39
|
+
if (typeof record.path !== 'string' || !record.path.trim()) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
const approvedAt = typeof record.approvedAt === 'string' ? record.approvedAt : new Date(0).toISOString();
|
|
43
|
+
const approvalId = typeof record.approvalId === 'string' ? record.approvalId : 'unknown';
|
|
44
|
+
const source = record.source === 'approval' ? 'approval' : undefined;
|
|
45
|
+
return [
|
|
46
|
+
{
|
|
47
|
+
path: path.resolve(record.path),
|
|
48
|
+
approvedAt,
|
|
49
|
+
approvalId,
|
|
50
|
+
...(source ? { source } : {}),
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const SYSTEM_HIGH_STAKES_PREFIXES = [
|
|
56
|
+
'/etc',
|
|
57
|
+
'/private/etc',
|
|
58
|
+
'/bin',
|
|
59
|
+
'/sbin',
|
|
60
|
+
'/usr',
|
|
61
|
+
'/opt',
|
|
62
|
+
'/System',
|
|
63
|
+
'/Library',
|
|
64
|
+
];
|
|
65
|
+
const HOME_HIGH_STAKES_SEGMENTS = ['.ssh', '.gnupg', '.aws', '.kube', '.docker', '.config'];
|
|
66
|
+
function isDirectoryPath(targetPath) {
|
|
67
|
+
try {
|
|
68
|
+
return statSync(targetPath).isDirectory();
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function isBroadTrustedWorkspaceRoot(targetPath) {
|
|
75
|
+
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
76
|
+
const root = normalizeTrustedWorkspaceRootPath(targetPath);
|
|
77
|
+
if (root === normalizeTrustedWorkspaceRootPath(path.parse(root).root)) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
if (home && normalizeTrustedWorkspaceRootPath(home) === root) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
export function isHighStakesTrustedWorkspaceRoot(targetPath) {
|
|
86
|
+
const normalized = normalizeTrustedWorkspaceRootPath(targetPath);
|
|
87
|
+
if (SYSTEM_HIGH_STAKES_PREFIXES.some((prefix) => normalized === normalizeTrustedWorkspaceRootPath(prefix) ||
|
|
88
|
+
pathWithinRoot(normalizeTrustedWorkspaceRootPath(prefix), normalized))) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
const home = process.env.HOME ?? process.env.USERPROFILE;
|
|
92
|
+
if (!home) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
const homeRoot = normalizeTrustedWorkspaceRootPath(home);
|
|
96
|
+
if (!pathWithinRoot(homeRoot, normalized)) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
return HOME_HIGH_STAKES_SEGMENTS.some((segment) => pathWithinRoot(path.join(homeRoot, segment), normalized));
|
|
100
|
+
}
|
|
101
|
+
export function validateTrustedWorkspaceRootCandidate(params) {
|
|
102
|
+
const normalizedPath = normalizeTrustedWorkspaceRootPath(params.candidatePath);
|
|
103
|
+
if (params.requireExistingDirectory !== false && !isDirectoryPath(normalizedPath)) {
|
|
104
|
+
return { ok: false, normalizedPath, reason: 'not_directory' };
|
|
105
|
+
}
|
|
106
|
+
if (isBroadTrustedWorkspaceRoot(normalizedPath)) {
|
|
107
|
+
return { ok: false, normalizedPath, reason: 'broad_root' };
|
|
108
|
+
}
|
|
109
|
+
if (isHighStakesTrustedWorkspaceRoot(normalizedPath)) {
|
|
110
|
+
return { ok: false, normalizedPath, reason: 'high_stakes' };
|
|
111
|
+
}
|
|
112
|
+
if (resolveWorkspaceRootMatch(params.repoRoot, [], normalizedPath) !== null) {
|
|
113
|
+
return { ok: false, normalizedPath, reason: 'inside_repo' };
|
|
114
|
+
}
|
|
115
|
+
if (params.requireNonGit !== false && containingGitRoot(normalizedPath)) {
|
|
116
|
+
return { ok: false, normalizedPath, reason: 'inside_git_repo' };
|
|
117
|
+
}
|
|
118
|
+
const normalizedControlPlane = params.controlPlaneDir?.trim()
|
|
119
|
+
? normalizeTrustedWorkspaceRootPath(params.controlPlaneDir)
|
|
120
|
+
: null;
|
|
121
|
+
if (normalizedControlPlane &&
|
|
122
|
+
(pathWithinRoot(normalizedControlPlane, normalizedPath) ||
|
|
123
|
+
pathWithinRoot(normalizedPath, normalizedControlPlane))) {
|
|
124
|
+
return { ok: false, normalizedPath, reason: 'control_plane_overlap' };
|
|
125
|
+
}
|
|
126
|
+
if ((params.protectedRoots ?? []).some((root) => {
|
|
127
|
+
const normalizedRoot = normalizeTrustedWorkspaceRootPath(root);
|
|
128
|
+
return (pathWithinRoot(normalizedRoot, normalizedPath) ||
|
|
129
|
+
pathWithinRoot(normalizedPath, normalizedRoot));
|
|
130
|
+
})) {
|
|
131
|
+
return { ok: false, normalizedPath, reason: 'protected_root_overlap' };
|
|
132
|
+
}
|
|
133
|
+
return { ok: true, normalizedPath };
|
|
134
|
+
}
|
|
135
|
+
export function isPathWithinTrustedWorkspaceRoots(absolutePath, roots) {
|
|
136
|
+
const resolved = normalizeTrustedWorkspaceRootPath(absolutePath);
|
|
137
|
+
return roots.roots.some((entry) => {
|
|
138
|
+
const root = path.resolve(entry.path);
|
|
139
|
+
return resolved === root || pathWithinRoot(root, resolved);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
export function addTrustedWorkspaceRoot(roots, entry) {
|
|
143
|
+
const normalized = normalizeTrustedWorkspaceRootPath(entry.path);
|
|
144
|
+
const filtered = roots.roots.filter((existing) => normalizeTrustedWorkspaceRootPath(existing.path) !== normalized);
|
|
145
|
+
return {
|
|
146
|
+
version: 1,
|
|
147
|
+
roots: [...filtered, { ...entry, path: normalized }],
|
|
148
|
+
};
|
|
149
|
+
}
|