@guilz-dev/belay 0.2.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/README.md +39 -12
- 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 +289 -23
- package/dist/bundle/claude-runtime.mjs +4435 -708
- package/dist/bundle/codex-runtime.mjs +4448 -710
- package/dist/bundle/cursor-runtime.mjs +4437 -710
- package/dist/cli.js +333 -11
- package/dist/commands/approve.d.ts +1 -0
- package/dist/commands/approve.js +56 -26
- package/dist/commands/config.d.ts +70 -0
- package/dist/commands/config.js +577 -0
- package/dist/commands/doctor.js +13 -2
- 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/health-snapshot.d.ts +6 -0
- package/dist/commands/health-snapshot.js +34 -0
- package/dist/commands/judge.d.ts +100 -0
- package/dist/commands/judge.js +304 -0
- 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/status.js +2 -2
- package/dist/commands/stdin-key.d.ts +1 -0
- package/dist/commands/stdin-key.js +8 -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/conformance/guarantee-table.js +12 -0
- package/dist/conformance/types.d.ts +2 -0
- 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-io.d.ts +2 -0
- package/dist/core/audit-io.js +14 -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 +4 -3
- package/dist/core/capability/index.js +3 -2
- package/dist/core/capability/paths.d.ts +2 -1
- package/dist/core/capability/paths.js +76 -8
- package/dist/core/capability/reasons.d.ts +3 -0
- package/dist/core/capability/reasons.js +8 -0
- 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 +129 -30
- package/dist/core/config-layers.js +2 -1
- package/dist/core/config.d.ts +40 -2
- package/dist/core/config.js +159 -11
- package/dist/core/credential-store.d.ts +11 -0
- package/dist/core/credential-store.js +60 -0
- package/dist/core/gate-engine.js +104 -13
- 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/integrity.d.ts +2 -0
- package/dist/core/integrity.js +13 -0
- package/dist/core/judge-api-key.d.ts +19 -0
- package/dist/core/judge-api-key.js +74 -0
- package/dist/core/judge-cloud-consent.d.ts +13 -0
- package/dist/core/judge-cloud-consent.js +38 -0
- package/dist/core/judge-config.d.ts +41 -4
- package/dist/core/judge-config.js +263 -57
- package/dist/core/judge-doctor.d.ts +9 -1
- package/dist/core/judge-doctor.js +199 -94
- package/dist/core/judge-model-discovery.d.ts +24 -0
- package/dist/core/judge-model-discovery.js +168 -0
- package/dist/core/judge-model-policy.d.ts +5 -0
- package/dist/core/judge-model-policy.js +21 -0
- package/dist/core/judge-runtime-detection.d.ts +9 -0
- package/dist/core/judge-runtime-detection.js +68 -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/transactional/diff-evaluator.js +1 -19
- package/dist/core/types.d.ts +17 -0
- package/dist/core/verdict/adapter.d.ts +1 -0
- package/dist/core/verdict/adapter.js +17 -2
- package/dist/core/verdict/containment.d.ts +8 -2
- package/dist/core/verdict/containment.js +45 -10
- 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-catalog.d.ts +40 -0
- package/dist/core/verdict/judge-catalog.js +148 -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 +46 -0
- package/dist/core/verdict/judge-cli.js +401 -0
- package/dist/core/verdict/judge-factory.d.ts +15 -4
- package/dist/core/verdict/judge-factory.js +127 -15
- 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 +33 -1
- package/dist/core/verdict/judge.js +84 -14
- package/dist/core/verdict/parser.js +6 -1
- package/dist/core/verdict/persistent-paths.d.ts +8 -0
- package/dist/core/verdict/persistent-paths.js +52 -0
- package/dist/core/verdict/types.d.ts +19 -2
- package/dist/core/verdict/verdict.js +166 -49
- 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 +69 -15
- 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/types.d.ts +7 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -3
- package/skills/belay/SKILL.md +15 -8
- package/skills/belay/belay-approve.md +17 -0
- package/dist/commands/init-wizard.d.ts +0 -21
- package/dist/commands/init-wizard.js +0 -63
|
@@ -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']);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { addPathToAllowlist, allPathsAllowlisted, fsScopeAllowlistPath, isPathAllowlisted, loadFsScopeAllowlist, loadFsScopeAllowlistSync, saveFsScopeAllowlist, } from './allowlist.js';
|
|
2
2
|
export { evaluateL1FullStatus, hasSandboxRuntime, isCapabilityBrokerDemotionActive, isSandboxBrokerEnabled, } from './broker.js';
|
|
3
|
-
export { collectOutsideRepoPaths } from './paths.js';
|
|
4
|
-
export { FS_SCOPE_REASONS, shouldSkipBrokerApprovedOnce } from './reasons.js';
|
|
5
|
-
export
|
|
3
|
+
export { collectOutsideRepoPaths, collectOutsideRepoPathsFromToolPayload } from './paths.js';
|
|
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';
|
|
6
|
+
export type { CapabilityApprovalScope, FsScopeAllowlistEntry, FsScopeAllowlistFile, TrustedWorkspaceRootEntry, TrustedWorkspaceRootsFile, } from './types.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { addPathToAllowlist, allPathsAllowlisted, fsScopeAllowlistPath, isPathAllowlisted, loadFsScopeAllowlist, loadFsScopeAllowlistSync, saveFsScopeAllowlist, } from './allowlist.js';
|
|
2
2
|
export { evaluateL1FullStatus, hasSandboxRuntime, isCapabilityBrokerDemotionActive, isSandboxBrokerEnabled, } from './broker.js';
|
|
3
|
-
export { collectOutsideRepoPaths } from './paths.js';
|
|
4
|
-
export { FS_SCOPE_REASONS, shouldSkipBrokerApprovedOnce } from './reasons.js';
|
|
3
|
+
export { collectOutsideRepoPaths, collectOutsideRepoPathsFromToolPayload } from './paths.js';
|
|
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 +1,2 @@
|
|
|
1
|
-
export declare function collectOutsideRepoPaths(command: string, 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,20 +1,88 @@
|
|
|
1
|
-
import
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { resolveMutationTarget, resolveWorkspaceRootMatch } from '../path-utils.js';
|
|
2
3
|
import { extractRedirectTargets, tokenizeShell } from '../shell-tokenizer.js';
|
|
3
|
-
|
|
4
|
+
function applyPatchTargets(patch) {
|
|
5
|
+
const targets = [];
|
|
6
|
+
for (const line of patch.split('\n')) {
|
|
7
|
+
const match = line.match(/^\*\*\* (?:Add|Delete|Update) File: (.+)$/);
|
|
8
|
+
if (match?.[1]) {
|
|
9
|
+
targets.push(match[1]);
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
const moveMatch = line.match(/^\*\*\* Move to: (.+)$/);
|
|
13
|
+
if (moveMatch?.[1]) {
|
|
14
|
+
targets.push(moveMatch[1]);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return targets;
|
|
18
|
+
}
|
|
19
|
+
function extractToolFilePath(payload) {
|
|
20
|
+
const toolInput = payload.tool_input;
|
|
21
|
+
if (!toolInput || typeof toolInput !== 'object') {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const input = toolInput;
|
|
25
|
+
for (const key of ['path', 'file_path', 'target_file', 'filePath']) {
|
|
26
|
+
if (typeof input[key] === 'string') {
|
|
27
|
+
return input[key];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
function extractToolPatch(payload) {
|
|
33
|
+
const toolInput = payload.tool_input;
|
|
34
|
+
if (!toolInput || typeof toolInput !== 'object') {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const input = toolInput;
|
|
38
|
+
for (const key of ['patch', 'input', 'text']) {
|
|
39
|
+
if (typeof input[key] === 'string' && input[key].trim()) {
|
|
40
|
+
return input[key];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function addOutsideRepoPath(paths, token, cwd, repoRoot, trustedWorkspaceRoots = []) {
|
|
46
|
+
const resolved = resolveMutationTarget(token, cwd);
|
|
47
|
+
if (resolved && resolveWorkspaceRootMatch(repoRoot, trustedWorkspaceRoots, resolved) === null) {
|
|
48
|
+
paths.add(resolved);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export function collectOutsideRepoPaths(command, cwd, repoRoot, trustedWorkspaceRoots = []) {
|
|
4
52
|
const tokens = tokenizeShell(command);
|
|
5
53
|
const redirects = extractRedirectTargets(tokens);
|
|
6
54
|
const paths = new Set();
|
|
7
55
|
for (const token of tokens.slice(1)) {
|
|
8
|
-
|
|
9
|
-
if (resolved && relativeWithinRepo(repoRoot, resolved) === null) {
|
|
10
|
-
paths.add(resolved);
|
|
11
|
-
}
|
|
56
|
+
addOutsideRepoPath(paths, token, cwd, repoRoot, trustedWorkspaceRoots);
|
|
12
57
|
}
|
|
13
58
|
for (const redirect of redirects) {
|
|
14
|
-
|
|
15
|
-
|
|
59
|
+
addOutsideRepoPath(paths, redirect, cwd, repoRoot, trustedWorkspaceRoots);
|
|
60
|
+
}
|
|
61
|
+
return [...paths];
|
|
62
|
+
}
|
|
63
|
+
export function collectOutsideRepoPathsFromToolPayload(payload, cwd, repoRoot, trustedWorkspaceRoots = []) {
|
|
64
|
+
const toolKind = String(payload.tool_name ?? '')
|
|
65
|
+
.trim()
|
|
66
|
+
.toLowerCase();
|
|
67
|
+
const paths = new Set();
|
|
68
|
+
const filePath = extractToolFilePath(payload);
|
|
69
|
+
if (filePath) {
|
|
70
|
+
const resolved = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
|
|
71
|
+
if (resolveWorkspaceRootMatch(repoRoot, trustedWorkspaceRoots, resolved) === null) {
|
|
16
72
|
paths.add(resolved);
|
|
17
73
|
}
|
|
74
|
+
return [...paths];
|
|
75
|
+
}
|
|
76
|
+
if (toolKind === 'apply_patch' || toolKind === 'applypatch') {
|
|
77
|
+
const patch = extractToolPatch(payload);
|
|
78
|
+
if (patch) {
|
|
79
|
+
for (const target of applyPatchTargets(patch)) {
|
|
80
|
+
const resolved = path.isAbsolute(target) ? target : path.resolve(cwd, target);
|
|
81
|
+
if (resolveWorkspaceRootMatch(repoRoot, trustedWorkspaceRoots, resolved) === null) {
|
|
82
|
+
paths.add(resolved);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
18
86
|
}
|
|
19
87
|
return [...paths];
|
|
20
88
|
}
|
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
export declare const FS_SCOPE_REASONS: Set<string>;
|
|
2
|
+
export declare const JUDGE_CLOUD_CONSENT_REASON: "judge_cloud_consent";
|
|
3
|
+
export declare const CAPABILITY_APPROVAL_REASONS: Set<string>;
|
|
2
4
|
export declare function shouldSkipBrokerApprovedOnce(brokerActive: boolean, reason: string): boolean;
|
|
5
|
+
export declare function shouldSkipBrokerApprovedRecord(brokerActive: boolean, approvalReason: string | undefined): boolean;
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
export const FS_SCOPE_REASONS = new Set(['outside_repo_mutation', 'outside_repo_redirect']);
|
|
2
|
+
export const JUDGE_CLOUD_CONSENT_REASON = 'judge_cloud_consent';
|
|
3
|
+
export const CAPABILITY_APPROVAL_REASONS = new Set([
|
|
4
|
+
...FS_SCOPE_REASONS,
|
|
5
|
+
JUDGE_CLOUD_CONSENT_REASON,
|
|
6
|
+
]);
|
|
2
7
|
export function shouldSkipBrokerApprovedOnce(brokerActive, reason) {
|
|
3
8
|
return brokerActive && FS_SCOPE_REASONS.has(reason);
|
|
4
9
|
}
|
|
10
|
+
export function shouldSkipBrokerApprovedRecord(brokerActive, approvalReason) {
|
|
11
|
+
return brokerActive && approvalReason !== undefined && FS_SCOPE_REASONS.has(approvalReason);
|
|
12
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type CapabilityApprovalScope = 'once' | 'path';
|
|
1
|
+
export type CapabilityApprovalScope = 'once' | 'path' | 'workspace-root';
|
|
2
2
|
export interface FsScopeAllowlistEntry {
|
|
3
3
|
path: string;
|
|
4
4
|
approvedAt: string;
|
|
@@ -8,3 +8,13 @@ export interface FsScopeAllowlistFile {
|
|
|
8
8
|
version: 1;
|
|
9
9
|
paths: FsScopeAllowlistEntry[];
|
|
10
10
|
}
|
|
11
|
+
export interface TrustedWorkspaceRootEntry {
|
|
12
|
+
path: string;
|
|
13
|
+
approvedAt: string;
|
|
14
|
+
approvalId: string;
|
|
15
|
+
source?: 'approval';
|
|
16
|
+
}
|
|
17
|
+
export interface TrustedWorkspaceRootsFile {
|
|
18
|
+
version: 1;
|
|
19
|
+
roots: TrustedWorkspaceRootEntry[];
|
|
20
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CapabilityApprovalScope } from './capability/types.js';
|
|
2
|
-
import type
|
|
2
|
+
import { type BelayConfigV3 } from './config.js';
|
|
3
3
|
import type { ApprovalStateFile } from './types.js';
|
|
4
4
|
export interface CapabilityApprovalStore {
|
|
5
5
|
loadPending: () => Promise<{
|
|
@@ -13,6 +13,7 @@ export interface CapabilityApprovalStore {
|
|
|
13
13
|
writePending: (filePath: string, state: ApprovalStateFile) => Promise<void>;
|
|
14
14
|
writeApproved: (filePath: string, state: ApprovalStateFile) => Promise<void>;
|
|
15
15
|
allowlistPath: string;
|
|
16
|
+
trustedRootsPath: string;
|
|
16
17
|
}
|
|
17
18
|
export declare function recordCapabilityApproval(params: {
|
|
18
19
|
approvalId: string;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { recordApproval } from './approval-service.js';
|
|
2
2
|
import { addPathToAllowlist, loadFsScopeAllowlist, normalizeAllowlistPath, saveFsScopeAllowlist, } from './capability/allowlist.js';
|
|
3
3
|
import { FS_SCOPE_REASONS } from './capability/reasons.js';
|
|
4
|
+
import { addTrustedWorkspaceRoot, loadTrustedWorkspaceRoots, normalizeTrustedWorkspaceRootPath, saveTrustedWorkspaceRoots, validateTrustedWorkspaceRootCandidate, } from './capability/trusted-workspace-roots.js';
|
|
5
|
+
import { resolveControlPlaneDir } from './config.js';
|
|
4
6
|
export async function recordCapabilityApproval(params) {
|
|
5
7
|
const pending = await params.store.loadPending();
|
|
6
8
|
const match = pending.state.approvals.find((approval) => approval.approvalId === params.approvalId);
|
|
7
9
|
if (params.scope === 'path') {
|
|
8
|
-
if (!match || !FS_SCOPE_REASONS.has(match.reason)) {
|
|
10
|
+
if (!match || match.kind !== 'shell' || !FS_SCOPE_REASONS.has(match.reason)) {
|
|
9
11
|
return {
|
|
10
12
|
ok: false,
|
|
11
13
|
message: 'Path scope approvals apply only to pending outside-repo shell actions (outside_repo_mutation / outside_repo_redirect).',
|
|
@@ -18,6 +20,85 @@ export async function recordCapabilityApproval(params) {
|
|
|
18
20
|
};
|
|
19
21
|
}
|
|
20
22
|
}
|
|
23
|
+
if (params.scope === 'workspace-root') {
|
|
24
|
+
if (!match || !FS_SCOPE_REASONS.has(match.reason)) {
|
|
25
|
+
return {
|
|
26
|
+
ok: false,
|
|
27
|
+
message: 'workspace-root scope approvals apply only to pending outside-repo actions (outside_repo_mutation / outside_repo_redirect).',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (!params.scopePath?.trim()) {
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
message: 'approve --scope workspace-root requires --path <absolute-directory-path>.',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (!match.scopeHint || match.scopeHint.scope !== 'workspace-root') {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
message: 'workspace-root approval requires a pending scope hint. Re-run the original blocked action to mint a scoped approval.',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const normalizedPath = normalizeTrustedWorkspaceRootPath(params.scopePath.trim());
|
|
43
|
+
const normalizedHint = normalizeTrustedWorkspaceRootPath(match.scopeHint.path);
|
|
44
|
+
if (normalizedPath !== normalizedHint) {
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
message: `workspace-root approval path must exactly match the suggested path: ${normalizedHint}`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const validation = validateTrustedWorkspaceRootCandidate({
|
|
51
|
+
candidatePath: normalizedPath,
|
|
52
|
+
repoRoot: match.repoRoot,
|
|
53
|
+
controlPlaneDir: params.config.controlPlane.enabled
|
|
54
|
+
? resolveControlPlaneDir(params.config)
|
|
55
|
+
: undefined,
|
|
56
|
+
requireExistingDirectory: true,
|
|
57
|
+
requireNonGit: true,
|
|
58
|
+
});
|
|
59
|
+
if (!validation.ok) {
|
|
60
|
+
if (validation.reason === 'not_directory') {
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
message: 'workspace-root approval requires an existing directory path.',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (validation.reason === 'broad_root') {
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
message: 'workspace-root approval rejected: broad roots (/, drive root, HOME) are not allowed.',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (validation.reason === 'high_stakes') {
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
message: 'workspace-root approval rejected: high-stakes directories cannot be trusted roots.',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (validation.reason === 'inside_repo') {
|
|
79
|
+
return {
|
|
80
|
+
ok: false,
|
|
81
|
+
message: 'workspace-root approval rejected: the path is already inside the repository root.',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (validation.reason === 'inside_git_repo') {
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
message: `workspace-root approval rejected: ${normalizedPath} is under a git repository.`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (validation.reason === 'control_plane_overlap') {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
message: 'workspace-root approval rejected: control plane paths cannot be trusted roots.',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
message: 'workspace-root approval rejected by trusted-root policy.',
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
21
102
|
const result = await recordApproval({
|
|
22
103
|
approvalId: params.approvalId,
|
|
23
104
|
config: params.config,
|
|
@@ -25,9 +106,26 @@ export async function recordCapabilityApproval(params) {
|
|
|
25
106
|
requireSignedToken: params.requireSignedToken ?? false,
|
|
26
107
|
store: params.store,
|
|
27
108
|
});
|
|
28
|
-
if (!result.ok ||
|
|
109
|
+
if (!result.ok ||
|
|
110
|
+
(params.scope !== 'path' && params.scope !== 'workspace-root') ||
|
|
111
|
+
!params.scopePath?.trim()) {
|
|
29
112
|
return { ok: result.ok, message: result.message };
|
|
30
113
|
}
|
|
114
|
+
if (params.scope === 'workspace-root') {
|
|
115
|
+
const trustedRoots = await loadTrustedWorkspaceRoots(params.store.trustedRootsPath);
|
|
116
|
+
const normalizedPath = normalizeTrustedWorkspaceRootPath(params.scopePath.trim());
|
|
117
|
+
const updated = addTrustedWorkspaceRoot(trustedRoots, {
|
|
118
|
+
path: normalizedPath,
|
|
119
|
+
approvedAt: new Date().toISOString(),
|
|
120
|
+
approvalId: params.approvalId,
|
|
121
|
+
source: 'approval',
|
|
122
|
+
});
|
|
123
|
+
await saveTrustedWorkspaceRoots(params.store.trustedRootsPath, updated);
|
|
124
|
+
return {
|
|
125
|
+
ok: true,
|
|
126
|
+
message: `${result.message} Trusted workspace root ${normalizedPath} added.`,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
31
129
|
const allowlist = await loadFsScopeAllowlist(params.store.allowlistPath);
|
|
32
130
|
const normalizedPath = normalizeAllowlistPath(params.scopePath.trim());
|
|
33
131
|
const updated = addPathToAllowlist(allowlist, {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { canonicalStringify, subagentFingerprint } from './fingerprint.js';
|
|
2
|
+
import { subagentFingerprintSource } from './replay-scrub.js';
|
|
2
3
|
import { scrubValue } from './scrub.js';
|
|
3
4
|
const EXTERNAL_TERMS = ['deploy', 'production', 'publish', 'release', 'ship', 'notify', 'email'];
|
|
4
5
|
function extractSubagentText(payload, options) {
|
|
@@ -22,26 +23,7 @@ function extractSubagentText(payload, options) {
|
|
|
22
23
|
return canonicalStringify(scrubValue(payload, options.scrubOptions));
|
|
23
24
|
}
|
|
24
25
|
function fingerprintSource(payload, options) {
|
|
25
|
-
|
|
26
|
-
if (toolInput && typeof toolInput === 'object') {
|
|
27
|
-
const input = toolInput;
|
|
28
|
-
return scrubValue({
|
|
29
|
-
description: input.description ?? '',
|
|
30
|
-
prompt: input.prompt ?? '',
|
|
31
|
-
}, options.scrubOptions);
|
|
32
|
-
}
|
|
33
|
-
const task = payload.task;
|
|
34
|
-
if (typeof task === 'string') {
|
|
35
|
-
return scrubValue({ task }, options.scrubOptions);
|
|
36
|
-
}
|
|
37
|
-
if (task && typeof task === 'object') {
|
|
38
|
-
const taskObj = task;
|
|
39
|
-
return scrubValue({
|
|
40
|
-
description: taskObj.description ?? '',
|
|
41
|
-
prompt: taskObj.prompt ?? '',
|
|
42
|
-
}, options.scrubOptions);
|
|
43
|
-
}
|
|
44
|
-
return scrubValue(payload, options.scrubOptions);
|
|
26
|
+
return subagentFingerprintSource(payload, options.scrubOptions ?? {});
|
|
45
27
|
}
|
|
46
28
|
export function classifySubagent(payload, repoRoot, options = {}) {
|
|
47
29
|
const kind = payload.tool_name === 'Task' ? 'Task' : String(payload.subagent_type ?? 'generalPurpose');
|