@guilz-dev/belay 0.9.4 → 0.10.1
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 +3 -2
- package/dist/adapters/claude/runtime-entry.js +5 -18
- package/dist/adapters/codex/runtime-entry.js +10 -11
- package/dist/adapters/cursor/hook-dispatch-entry.d.ts +3 -0
- package/dist/adapters/cursor/hook-dispatch-entry.js +11 -0
- package/dist/adapters/cursor/hook-router.d.ts +1 -0
- package/dist/adapters/cursor/hook-router.js +39 -9
- package/dist/adapters/cursor/hook-routing-health.d.ts +1 -0
- package/dist/adapters/cursor/hook-routing-health.js +53 -0
- package/dist/adapters/cursor/hooks.d.ts +1 -1
- package/dist/adapters/cursor/hooks.js +16 -5
- package/dist/adapters/cursor/routing-config-trust.d.ts +1 -0
- package/dist/adapters/cursor/routing-config-trust.js +67 -0
- package/dist/adapters/cursor/runtime-entry.d.ts +2 -2
- package/dist/adapters/cursor/runtime-entry.js +35 -10
- package/dist/adapters/shared/gate-runtime.d.ts +0 -2
- package/dist/adapters/shared/gate-runtime.js +12 -52
- package/dist/bundle/claude-runtime.mjs +1083 -669
- package/dist/bundle/codex-runtime.mjs +1092 -698
- package/dist/bundle/cursor-dispatcher.mjs +119 -28
- package/dist/bundle/cursor-runtime.mjs +1132 -677
- package/dist/cli.js +53 -4
- package/dist/commands/approval-token.d.ts +10 -0
- package/dist/commands/approval-token.js +26 -0
- package/dist/commands/audit.d.ts +2 -1
- package/dist/commands/audit.js +2 -2
- package/dist/commands/config.d.ts +1 -1
- package/dist/commands/config.js +28 -2
- package/dist/commands/doctor.js +14 -54
- package/dist/commands/dogfood-check.d.ts +3 -0
- package/dist/commands/dogfood-check.js +116 -0
- package/dist/commands/dogfood.d.ts +1 -0
- package/dist/commands/dogfood.js +4 -3
- package/dist/commands/judge.js +2 -2
- package/dist/commands/recovery-checkpoints.js +2 -11
- package/dist/config-io.d.ts +1 -0
- package/dist/config-io.js +8 -1
- package/dist/core/capability/index.d.ts +1 -1
- package/dist/core/capability/index.js +1 -1
- package/dist/core/capability/policy-engine.d.ts +19 -0
- package/dist/core/capability/policy-engine.js +31 -0
- package/dist/core/classify-tool.js +372 -148
- package/dist/core/config.js +2 -2
- package/dist/core/dogfood-environment.d.ts +8 -0
- package/dist/core/dogfood-environment.js +55 -0
- package/dist/core/effect-ir/shell-lower/decoders/belay.js +12 -0
- package/dist/core/egress-approval.js +0 -18
- package/dist/core/gate-engine.js +2 -9
- package/dist/core/notify.d.ts +8 -2
- package/dist/core/notify.js +63 -7
- package/dist/core/recovery/operator-guidance.js +4 -4
- package/dist/core/repo-config-trust.d.ts +31 -0
- package/dist/core/repo-config-trust.js +152 -0
- package/dist/corpus/benign-probe-cores.d.ts +1 -1
- package/dist/corpus/benign-probe-cores.js +0 -1
- package/dist/defaults.js +0 -25
- package/dist/installer/scope-config.js +2 -2
- package/dist/installer.js +4 -4
- package/dist/types.d.ts +19 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -1
package/dist/config-io.js
CHANGED
|
@@ -7,6 +7,7 @@ import { compactApprovals, isExpired, mergeApprovalStates } from './core/approva
|
|
|
7
7
|
import { mutateApprovalStateWithRetry } from './core/capability/approval-state-mutation.js';
|
|
8
8
|
import { approvedApprovalsFile, belayStateDir, configuredControlPlaneDir, mergeConfig, pendingApprovalsFile, stripForbiddenShellOverrideLists, } from './core/config.js';
|
|
9
9
|
import { resolveLayeredConfig, teamConfigPath, } from './core/config-layers.js';
|
|
10
|
+
import { trustRepoConfig } from './core/repo-config-trust.js';
|
|
10
11
|
export function resolveAdapterName(config) {
|
|
11
12
|
if (config.adapter === 'claude') {
|
|
12
13
|
return 'claude';
|
|
@@ -221,6 +222,12 @@ export async function writeConfigFile(repoRoot, config, adapter = resolveAdapter
|
|
|
221
222
|
await unlink(temporaryPath).catch(() => undefined);
|
|
222
223
|
}
|
|
223
224
|
}
|
|
225
|
+
export async function writeTrustedConfigFile(repoRoot, config, adapter = resolveAdapterName(config)) {
|
|
226
|
+
await writeConfigFile(repoRoot, config, adapter);
|
|
227
|
+
const configPath = configPathFor(repoRoot, adapter);
|
|
228
|
+
const parsed = JSON.parse(await readFile(configPath, 'utf8'));
|
|
229
|
+
await trustRepoConfig(repoRoot, adapter, parsed);
|
|
230
|
+
}
|
|
224
231
|
export async function mergeAndWriteConfig(repoRoot, adapter = 'cursor') {
|
|
225
232
|
const layout = getAdapterLayout(adapter);
|
|
226
233
|
const configPath = layout.configPath(repoRoot);
|
|
@@ -229,7 +236,7 @@ export async function mergeAndWriteConfig(repoRoot, adapter = 'cursor') {
|
|
|
229
236
|
existing = JSON.parse(await readFile(configPath, 'utf8'));
|
|
230
237
|
}
|
|
231
238
|
const merged = mergeConfig(existing, layout.defaultConfig(repoRoot));
|
|
232
|
-
await
|
|
239
|
+
await writeTrustedConfigFile(repoRoot, merged, adapter);
|
|
233
240
|
await ensureBelayStateDir(merged, repoRoot);
|
|
234
241
|
if (merged.controlPlane.enabled) {
|
|
235
242
|
await migrateRepoLocalApprovalsToControlPlane(repoRoot, merged);
|
|
@@ -6,7 +6,7 @@ export { CAPABILITY_GRANT_VERSION, type CapabilityGrantV1, isGrantScopeTooBroad,
|
|
|
6
6
|
export { checkGatedActionLimits, MAX_SHELL_COMMAND_BYTES, MAX_TOOL_PAYLOAD_BYTES, } from './limits.js';
|
|
7
7
|
export { collectOutsideRepoPaths, collectOutsideRepoPathsFromToolPayload } from './paths.js';
|
|
8
8
|
export { type CapabilityAuthorizationMetadata, type PolicyLegacyReasonHints, policyDecisionToLegacyReason, policyReasonToLegacyReason, singleCapabilityMetadata, } from './policy-bridge.js';
|
|
9
|
-
export { buildFileMutationCapabilityRequest, buildShellCapabilityRequest, createTypeScriptPolicyEngine, evaluateFileMutationPolicy, evaluateShellPolicy, evaluateSubagentPolicy, type FileMutationCapabilityAnalysis, getDefaultPolicyEngine, policyDecisionRequiresAsk, type ShellCapabilityAnalysis, type SubagentCapabilityAnalysis, } from './policy-engine.js';
|
|
9
|
+
export { buildFileMutationCapabilityRequest, buildShellCapabilityRequest, createTypeScriptPolicyEngine, evaluateFileMutationPolicy, evaluateFileReadPolicy, evaluateShellPolicy, evaluateSubagentPolicy, type FileMutationCapabilityAnalysis, type FileReadCapabilityAnalysis, getDefaultPolicyEngine, policyDecisionRequiresAsk, type ShellCapabilityAnalysis, type SubagentCapabilityAnalysis, } from './policy-engine.js';
|
|
10
10
|
export type { AuthorizationContext, PolicyDecision, PolicyEngine, PolicyOutcome, } from './policy-types.js';
|
|
11
11
|
export { FS_SCOPE_REASONS, shouldSkipBrokerApprovedOnce, shouldSkipBrokerApprovedRecord, } from './reasons.js';
|
|
12
12
|
export { CAPABILITY_REQUEST_VERSION, type CapabilityAction, type CapabilityEvidence, type CapabilityEvidenceLevel, type CapabilityHookKind, type CapabilityPrincipal, type CapabilityRequestContext, type CapabilityRequestV1, type CapabilityResource, type GitRefScope, type NetworkMode, type NetworkPayload, type ProcessOperation, } from './request.js';
|
|
@@ -6,7 +6,7 @@ export { CAPABILITY_GRANT_VERSION, isGrantScopeTooBroad, } from './grant.js';
|
|
|
6
6
|
export { checkGatedActionLimits, MAX_SHELL_COMMAND_BYTES, MAX_TOOL_PAYLOAD_BYTES, } from './limits.js';
|
|
7
7
|
export { collectOutsideRepoPaths, collectOutsideRepoPathsFromToolPayload } from './paths.js';
|
|
8
8
|
export { policyDecisionToLegacyReason, policyReasonToLegacyReason, singleCapabilityMetadata, } from './policy-bridge.js';
|
|
9
|
-
export { buildFileMutationCapabilityRequest, buildShellCapabilityRequest, createTypeScriptPolicyEngine, evaluateFileMutationPolicy, evaluateShellPolicy, evaluateSubagentPolicy, getDefaultPolicyEngine, policyDecisionRequiresAsk, } from './policy-engine.js';
|
|
9
|
+
export { buildFileMutationCapabilityRequest, buildShellCapabilityRequest, createTypeScriptPolicyEngine, evaluateFileMutationPolicy, evaluateFileReadPolicy, evaluateShellPolicy, evaluateSubagentPolicy, getDefaultPolicyEngine, policyDecisionRequiresAsk, } from './policy-engine.js';
|
|
10
10
|
export { FS_SCOPE_REASONS, shouldSkipBrokerApprovedOnce, shouldSkipBrokerApprovedRecord, } from './reasons.js';
|
|
11
11
|
export { CAPABILITY_REQUEST_VERSION, } from './request.js';
|
|
12
12
|
export { addTrustedWorkspaceRoot, isBroadTrustedWorkspaceRoot, isHighStakesTrustedWorkspaceRoot, isPathWithinTrustedWorkspaceRoots, loadTrustedWorkspaceRoots, loadTrustedWorkspaceRootsSync, normalizeTrustedWorkspaceRootPath, saveTrustedWorkspaceRoots, trustedWorkspaceRootsPath, validateTrustedWorkspaceRootCandidate, } from './trusted-workspace-roots.js';
|
|
@@ -105,3 +105,22 @@ export declare function evaluateFileMutationPolicy(analysis: FileMutationCapabil
|
|
|
105
105
|
request: CapabilityRequestV1;
|
|
106
106
|
decision: PolicyDecision;
|
|
107
107
|
};
|
|
108
|
+
export interface FileReadCapabilityAnalysis {
|
|
109
|
+
hookKind: CapabilityHookKind;
|
|
110
|
+
toolKind: string;
|
|
111
|
+
filePath: string;
|
|
112
|
+
resolvedPath: string;
|
|
113
|
+
repoRoot: string;
|
|
114
|
+
cwd: string;
|
|
115
|
+
inputFingerprint: string;
|
|
116
|
+
signals: string[];
|
|
117
|
+
locationLabel: 'repo_local' | 'outside_repo' | 'sensitive_path' | 'control_plane';
|
|
118
|
+
trustedWorkspaceRoots?: string[];
|
|
119
|
+
sensitivePaths?: string[];
|
|
120
|
+
adapter?: string;
|
|
121
|
+
}
|
|
122
|
+
export declare function buildFileReadCapabilityRequest(analysis: FileReadCapabilityAnalysis): CapabilityRequestV1;
|
|
123
|
+
export declare function evaluateFileReadPolicy(analysis: FileReadCapabilityAnalysis, config: BelayConfigV4, auth?: PolicyAuthExtras): {
|
|
124
|
+
request: CapabilityRequestV1;
|
|
125
|
+
decision: PolicyDecision;
|
|
126
|
+
};
|
|
@@ -866,3 +866,34 @@ export function evaluateFileMutationPolicy(analysis, config, auth) {
|
|
|
866
866
|
const decision = getDefaultPolicyEngine().evaluate(request, buildAuthorizationContext(config, analysis.trustedWorkspaceRoots, enriched));
|
|
867
867
|
return { request, decision };
|
|
868
868
|
}
|
|
869
|
+
export function buildFileReadCapabilityRequest(analysis) {
|
|
870
|
+
return {
|
|
871
|
+
version: CAPABILITY_REQUEST_VERSION,
|
|
872
|
+
principal: {
|
|
873
|
+
adapter: analysis.adapter,
|
|
874
|
+
repoRoot: analysis.repoRoot,
|
|
875
|
+
sessionHash: hashSession(`${analysis.repoRoot}:${analysis.cwd}`),
|
|
876
|
+
},
|
|
877
|
+
action: 'fs.read',
|
|
878
|
+
resource: { kind: 'path', path: analysis.resolvedPath },
|
|
879
|
+
context: {
|
|
880
|
+
cwd: analysis.cwd,
|
|
881
|
+
inputFingerprint: analysis.inputFingerprint,
|
|
882
|
+
hookKind: analysis.hookKind,
|
|
883
|
+
analysisBasis: [`tool:${analysis.toolKind}`, `location:${analysis.locationLabel}`, 'read'],
|
|
884
|
+
},
|
|
885
|
+
evidence: {
|
|
886
|
+
level: analysis.locationLabel === 'repo_local' ? 'certain' : 'possible',
|
|
887
|
+
signals: [...analysis.signals, 'effect.fs_read'],
|
|
888
|
+
},
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
export function evaluateFileReadPolicy(analysis, config, auth) {
|
|
892
|
+
const request = buildFileReadCapabilityRequest(analysis);
|
|
893
|
+
const enriched = enrichAuthWithMaterializedGrants(request, config, {
|
|
894
|
+
...auth,
|
|
895
|
+
sensitivePaths: auth?.sensitivePaths ?? analysis.sensitivePaths,
|
|
896
|
+
});
|
|
897
|
+
const decision = getDefaultPolicyEngine().evaluate(request, buildAuthorizationContext(config, analysis.trustedWorkspaceRoots, enriched));
|
|
898
|
+
return { request, decision };
|
|
899
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { BOUNDARY_PROFILE_L3_L4_ONLY } from './capability/boundary-profile.js';
|
|
3
3
|
import { policyReasonToLegacyReason } from './capability/policy-bridge.js';
|
|
4
|
-
import { evaluateFileMutationPolicy, policyDecisionRequiresAsk, } from './capability/policy-engine.js';
|
|
4
|
+
import { evaluateFileMutationPolicy, evaluateFileReadPolicy, policyDecisionRequiresAsk, } from './capability/policy-engine.js';
|
|
5
5
|
import { canonicalStringify, toolFingerprint } from './fingerprint.js';
|
|
6
6
|
import { matchesSensitivePath } from './glob.js';
|
|
7
7
|
import { pathWithinRoot, resolveWorkspaceRootMatch } from './path-utils.js';
|
|
@@ -11,17 +11,44 @@ import { classifyShell, resolveClassifierTrustedCwd } from './verdict/adapter.js
|
|
|
11
11
|
import { isGitPath } from './verdict/containment.js';
|
|
12
12
|
import { mutationPrescanRequiresAsk } from './verdict/prescan.js';
|
|
13
13
|
const DEFAULT_SENSITIVE_PATHS = ['.env', '.env.*', '**/credentials/**'];
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
'
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
14
|
+
function toolInputRecord(payload) {
|
|
15
|
+
const toolInput = payload.tool_input;
|
|
16
|
+
if (!toolInput || typeof toolInput !== 'object' || Array.isArray(toolInput)) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return toolInput;
|
|
20
|
+
}
|
|
21
|
+
function hasMutationFields(input) {
|
|
22
|
+
return (typeof input.contents === 'string' ||
|
|
23
|
+
typeof input.old_string === 'string' ||
|
|
24
|
+
typeof input.new_string === 'string' ||
|
|
25
|
+
typeof input.newContents === 'string');
|
|
26
|
+
}
|
|
27
|
+
function inferToolPayloadEffect(payload, toolName) {
|
|
28
|
+
if (extractShellCommand(payload)) {
|
|
29
|
+
return 'shell';
|
|
30
|
+
}
|
|
31
|
+
const input = toolInputRecord(payload);
|
|
32
|
+
if (!input) {
|
|
33
|
+
return 'indeterminate';
|
|
34
|
+
}
|
|
35
|
+
if (hasMutationFields(input)) {
|
|
36
|
+
return 'file_mutation';
|
|
37
|
+
}
|
|
38
|
+
if (extractPatch(payload)) {
|
|
39
|
+
return 'apply_patch';
|
|
40
|
+
}
|
|
41
|
+
if (typeof input.pattern === 'string' || typeof input.glob_pattern === 'string') {
|
|
42
|
+
return 'search_read';
|
|
43
|
+
}
|
|
44
|
+
if (typeof input.file_path === 'string') {
|
|
45
|
+
return 'file_read';
|
|
46
|
+
}
|
|
47
|
+
if (typeof input.path === 'string' || typeof input.target_file === 'string') {
|
|
48
|
+
return normalizedToolName(toolName) === 'delete' ? 'file_delete' : 'indeterminate';
|
|
49
|
+
}
|
|
50
|
+
return 'indeterminate';
|
|
51
|
+
}
|
|
25
52
|
function policyAuth(options) {
|
|
26
53
|
if (!options.grants &&
|
|
27
54
|
options.attestation === undefined &&
|
|
@@ -201,6 +228,283 @@ function classifyFileMutationWithPolicy(params) {
|
|
|
201
228
|
boundaryProfile: params.options.boundaryProfile ?? BOUNDARY_PROFILE_L3_L4_ONLY,
|
|
202
229
|
};
|
|
203
230
|
}
|
|
231
|
+
function classifyFileReadWithPolicy(params) {
|
|
232
|
+
const fingerprint = toolFingerprint(params.toolName, { path: params.filePath }, params.repoRoot);
|
|
233
|
+
const { request, decision } = evaluateFileReadPolicy({
|
|
234
|
+
hookKind: 'tool',
|
|
235
|
+
toolKind: params.toolKind,
|
|
236
|
+
filePath: params.filePath,
|
|
237
|
+
resolvedPath: params.resolvedPath,
|
|
238
|
+
repoRoot: params.repoRoot,
|
|
239
|
+
cwd: params.cwd,
|
|
240
|
+
inputFingerprint: fingerprint,
|
|
241
|
+
signals: params.signals,
|
|
242
|
+
locationLabel: params.locationLabel,
|
|
243
|
+
trustedWorkspaceRoots: params.options.trustedWorkspaceRoots,
|
|
244
|
+
sensitivePaths: params.options.sensitivePaths ?? params.config.classifier.sensitivePaths,
|
|
245
|
+
}, params.config, policyAuth(params.options));
|
|
246
|
+
if (policyDecisionRequiresAsk(decision)) {
|
|
247
|
+
return {
|
|
248
|
+
verdict: 'deny_pending_approval',
|
|
249
|
+
reason: policyReasonToLegacyReason(decision),
|
|
250
|
+
summary: params.filePath,
|
|
251
|
+
fingerprint,
|
|
252
|
+
assessment: {
|
|
253
|
+
reversibility: 'reversible',
|
|
254
|
+
external: params.locationLabel === 'outside_repo',
|
|
255
|
+
blastRadius: params.locationLabel === 'outside_repo'
|
|
256
|
+
? 'outside the repository'
|
|
257
|
+
: 'sensitive repository file',
|
|
258
|
+
confidence: 0.85,
|
|
259
|
+
signals: [...params.signals, ...decision.signals],
|
|
260
|
+
},
|
|
261
|
+
capabilityRequests: [request],
|
|
262
|
+
authorizationDecision: decision,
|
|
263
|
+
boundaryProfile: params.options.boundaryProfile ?? BOUNDARY_PROFILE_L3_L4_ONLY,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
verdict: 'allow',
|
|
268
|
+
reason: 'effect.fs_read',
|
|
269
|
+
summary: params.filePath,
|
|
270
|
+
fingerprint,
|
|
271
|
+
assessment: {
|
|
272
|
+
reversibility: 'reversible',
|
|
273
|
+
external: false,
|
|
274
|
+
blastRadius: 'tool scope',
|
|
275
|
+
confidence: 0.88,
|
|
276
|
+
signals: [...params.signals, ...decision.signals],
|
|
277
|
+
},
|
|
278
|
+
capabilityRequests: [request],
|
|
279
|
+
authorizationDecision: decision,
|
|
280
|
+
boundaryProfile: params.options.boundaryProfile ?? BOUNDARY_PROFILE_L3_L4_ONLY,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function classifyFilePathMutation(params) {
|
|
284
|
+
const { toolName, toolKind, filePath, repoRoot, cwd, config, options, sensitivePaths, protectedRoots, isDelete, } = params;
|
|
285
|
+
const signals = [];
|
|
286
|
+
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
|
|
287
|
+
const hitsProtectedRoot = protectedRoots.some((root) => pathWithinRoot(root, resolvedPath));
|
|
288
|
+
if (hitsProtectedRoot) {
|
|
289
|
+
signals.push('control_plane_path');
|
|
290
|
+
return classifyFileMutationWithPolicy({
|
|
291
|
+
toolName,
|
|
292
|
+
toolKind,
|
|
293
|
+
filePath,
|
|
294
|
+
resolvedPath,
|
|
295
|
+
repoRoot,
|
|
296
|
+
cwd,
|
|
297
|
+
config,
|
|
298
|
+
options,
|
|
299
|
+
signals,
|
|
300
|
+
isDelete,
|
|
301
|
+
locationLabel: 'control_plane',
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
const workspaceMatch = resolveWorkspaceRootMatch(repoRoot, options.trustedWorkspaceRoots, resolvedPath);
|
|
305
|
+
if (workspaceMatch === null) {
|
|
306
|
+
signals.push('outside_repo_path');
|
|
307
|
+
return classifyFileMutationWithPolicy({
|
|
308
|
+
toolName,
|
|
309
|
+
toolKind,
|
|
310
|
+
filePath,
|
|
311
|
+
resolvedPath,
|
|
312
|
+
repoRoot,
|
|
313
|
+
cwd,
|
|
314
|
+
config,
|
|
315
|
+
options,
|
|
316
|
+
signals,
|
|
317
|
+
isDelete,
|
|
318
|
+
locationLabel: 'outside_repo',
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
if (workspaceMatch.kind === 'trusted') {
|
|
322
|
+
signals.push('trusted_workspace_root');
|
|
323
|
+
}
|
|
324
|
+
const trustedCwd = resolveClassifierTrustedCwd(cwd, options);
|
|
325
|
+
const workspacePrescan = mutationPrescanRequiresAsk({
|
|
326
|
+
targets: [filePath],
|
|
327
|
+
cwd,
|
|
328
|
+
repoRoot,
|
|
329
|
+
trustedCwd,
|
|
330
|
+
trustedWorkspaceRoots: options.trustedWorkspaceRoots,
|
|
331
|
+
sensitivePaths,
|
|
332
|
+
});
|
|
333
|
+
if (workspacePrescan) {
|
|
334
|
+
return classifyFileMutationWithPolicy({
|
|
335
|
+
toolName,
|
|
336
|
+
toolKind,
|
|
337
|
+
filePath,
|
|
338
|
+
resolvedPath,
|
|
339
|
+
repoRoot,
|
|
340
|
+
cwd,
|
|
341
|
+
config,
|
|
342
|
+
options,
|
|
343
|
+
signals,
|
|
344
|
+
isDelete,
|
|
345
|
+
locationLabel: 'sensitive_path',
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
const relativePath = workspaceMatch.relativePath;
|
|
349
|
+
if (matchesSensitivePath(relativePath, sensitivePaths)) {
|
|
350
|
+
signals.push('sensitive_path');
|
|
351
|
+
return classifyFileMutationWithPolicy({
|
|
352
|
+
toolName,
|
|
353
|
+
toolKind,
|
|
354
|
+
filePath,
|
|
355
|
+
resolvedPath,
|
|
356
|
+
repoRoot,
|
|
357
|
+
cwd,
|
|
358
|
+
config,
|
|
359
|
+
options,
|
|
360
|
+
signals,
|
|
361
|
+
isDelete,
|
|
362
|
+
locationLabel: 'sensitive_path',
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
if (isDelete) {
|
|
366
|
+
if (isGitPath(resolvedPath, repoRoot)) {
|
|
367
|
+
signals.push('protected_artifact');
|
|
368
|
+
return classifyFileMutationWithPolicy({
|
|
369
|
+
toolName,
|
|
370
|
+
toolKind,
|
|
371
|
+
filePath,
|
|
372
|
+
resolvedPath,
|
|
373
|
+
repoRoot,
|
|
374
|
+
cwd,
|
|
375
|
+
config,
|
|
376
|
+
options,
|
|
377
|
+
signals,
|
|
378
|
+
isDelete: true,
|
|
379
|
+
locationLabel: 'sensitive_path',
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
signals.push('file_delete');
|
|
383
|
+
return classifyFileMutationWithPolicy({
|
|
384
|
+
toolName,
|
|
385
|
+
toolKind,
|
|
386
|
+
filePath,
|
|
387
|
+
resolvedPath,
|
|
388
|
+
repoRoot,
|
|
389
|
+
cwd,
|
|
390
|
+
config,
|
|
391
|
+
options,
|
|
392
|
+
signals,
|
|
393
|
+
isDelete: true,
|
|
394
|
+
locationLabel: 'repo_local',
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
signals.push('file_mutation');
|
|
398
|
+
return classifyFileMutationWithPolicy({
|
|
399
|
+
toolName,
|
|
400
|
+
toolKind,
|
|
401
|
+
filePath,
|
|
402
|
+
resolvedPath,
|
|
403
|
+
repoRoot,
|
|
404
|
+
cwd,
|
|
405
|
+
config,
|
|
406
|
+
options,
|
|
407
|
+
signals,
|
|
408
|
+
isDelete: false,
|
|
409
|
+
locationLabel: 'repo_local',
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
function classifyFilePathRead(params) {
|
|
413
|
+
const { toolName, toolKind, filePath, repoRoot, cwd, config, options, sensitivePaths, protectedRoots, } = params;
|
|
414
|
+
const signals = ['effect.fs_read'];
|
|
415
|
+
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
|
|
416
|
+
if (protectedRoots.some((root) => pathWithinRoot(root, resolvedPath))) {
|
|
417
|
+
signals.push('control_plane_path');
|
|
418
|
+
return classifyFileReadWithPolicy({
|
|
419
|
+
toolName,
|
|
420
|
+
toolKind,
|
|
421
|
+
filePath,
|
|
422
|
+
resolvedPath,
|
|
423
|
+
repoRoot,
|
|
424
|
+
cwd,
|
|
425
|
+
config,
|
|
426
|
+
options,
|
|
427
|
+
signals,
|
|
428
|
+
locationLabel: 'control_plane',
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
const workspaceMatch = resolveWorkspaceRootMatch(repoRoot, options.trustedWorkspaceRoots, resolvedPath);
|
|
432
|
+
if (workspaceMatch === null) {
|
|
433
|
+
signals.push('outside_repo_path');
|
|
434
|
+
return classifyFileReadWithPolicy({
|
|
435
|
+
toolName,
|
|
436
|
+
toolKind,
|
|
437
|
+
filePath,
|
|
438
|
+
resolvedPath,
|
|
439
|
+
repoRoot,
|
|
440
|
+
cwd,
|
|
441
|
+
config,
|
|
442
|
+
options,
|
|
443
|
+
signals,
|
|
444
|
+
locationLabel: 'outside_repo',
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
const relativePath = workspaceMatch.relativePath;
|
|
448
|
+
if (matchesSensitivePath(relativePath, sensitivePaths)) {
|
|
449
|
+
signals.push('sensitive_path');
|
|
450
|
+
return classifyFileReadWithPolicy({
|
|
451
|
+
toolName,
|
|
452
|
+
toolKind,
|
|
453
|
+
filePath,
|
|
454
|
+
resolvedPath,
|
|
455
|
+
repoRoot,
|
|
456
|
+
cwd,
|
|
457
|
+
config,
|
|
458
|
+
options,
|
|
459
|
+
signals,
|
|
460
|
+
locationLabel: 'sensitive_path',
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
return classifyFileReadWithPolicy({
|
|
464
|
+
toolName,
|
|
465
|
+
toolKind,
|
|
466
|
+
filePath,
|
|
467
|
+
resolvedPath,
|
|
468
|
+
repoRoot,
|
|
469
|
+
cwd,
|
|
470
|
+
config,
|
|
471
|
+
options,
|
|
472
|
+
signals,
|
|
473
|
+
locationLabel: 'repo_local',
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
function indeterminateToolResult(toolName, payload, repoRoot, options) {
|
|
477
|
+
const summary = canonicalStringify(scrubPayload(payload.tool_input ?? {}, options));
|
|
478
|
+
const fingerprint = toolFingerprint(toolName, fingerprintPayload(payload, options), repoRoot);
|
|
479
|
+
if (options.unknownLocalEffect === 'deny') {
|
|
480
|
+
return {
|
|
481
|
+
verdict: 'deny_pending_approval',
|
|
482
|
+
reason: 'indeterminate_tool_effect',
|
|
483
|
+
summary,
|
|
484
|
+
fingerprint,
|
|
485
|
+
assessment: {
|
|
486
|
+
reversibility: 'irreversible',
|
|
487
|
+
external: false,
|
|
488
|
+
blastRadius: 'unknown tool action',
|
|
489
|
+
confidence: 0.5,
|
|
490
|
+
signals: ['indeterminate_tool_effect'],
|
|
491
|
+
},
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
verdict: 'allow_flagged',
|
|
496
|
+
reason: 'indeterminate_tool_effect',
|
|
497
|
+
summary,
|
|
498
|
+
fingerprint,
|
|
499
|
+
assessment: {
|
|
500
|
+
reversibility: 'recoverable_with_cost',
|
|
501
|
+
external: false,
|
|
502
|
+
blastRadius: 'tool scope',
|
|
503
|
+
confidence: 0.5,
|
|
504
|
+
signals: ['indeterminate_tool_effect'],
|
|
505
|
+
},
|
|
506
|
+
};
|
|
507
|
+
}
|
|
204
508
|
export async function classifyToolUse(payload, repoRoot, cwd, config, options = {}) {
|
|
205
509
|
const toolName = String(payload.tool_name ?? '');
|
|
206
510
|
const toolKind = normalizedToolName(toolName);
|
|
@@ -247,9 +551,8 @@ export async function classifyToolUse(payload, repoRoot, cwd, config, options =
|
|
|
247
551
|
summary: command,
|
|
248
552
|
};
|
|
249
553
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
FILE_DELETE_TOOL_NAMES.has(toolKind)) {
|
|
554
|
+
const effect = inferToolPayloadEffect(payload, toolName);
|
|
555
|
+
if (effect === 'file_mutation' || effect === 'file_delete') {
|
|
253
556
|
const filePath = extractFilePath(payload);
|
|
254
557
|
if (!filePath) {
|
|
255
558
|
if (options.unknownLocalEffect === 'deny') {
|
|
@@ -281,134 +584,20 @@ export async function classifyToolUse(payload, repoRoot, cwd, config, options =
|
|
|
281
584
|
},
|
|
282
585
|
};
|
|
283
586
|
}
|
|
284
|
-
|
|
285
|
-
const resolvedPath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
|
|
286
|
-
const hitsProtectedRoot = protectedRoots.some((root) => pathWithinRoot(root, resolvedPath));
|
|
287
|
-
if (hitsProtectedRoot) {
|
|
288
|
-
signals.push('control_plane_path');
|
|
289
|
-
return classifyFileMutationWithPolicy({
|
|
290
|
-
toolName,
|
|
291
|
-
toolKind,
|
|
292
|
-
filePath,
|
|
293
|
-
resolvedPath,
|
|
294
|
-
repoRoot,
|
|
295
|
-
cwd,
|
|
296
|
-
config,
|
|
297
|
-
options,
|
|
298
|
-
signals,
|
|
299
|
-
isDelete: FILE_DELETE_TOOL_NAMES.has(toolKind),
|
|
300
|
-
locationLabel: 'control_plane',
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
const workspaceMatch = resolveWorkspaceRootMatch(repoRoot, options.trustedWorkspaceRoots, resolvedPath);
|
|
304
|
-
if (workspaceMatch === null) {
|
|
305
|
-
signals.push('outside_repo_path');
|
|
306
|
-
return classifyFileMutationWithPolicy({
|
|
307
|
-
toolName,
|
|
308
|
-
toolKind,
|
|
309
|
-
filePath,
|
|
310
|
-
resolvedPath,
|
|
311
|
-
repoRoot,
|
|
312
|
-
cwd,
|
|
313
|
-
config,
|
|
314
|
-
options,
|
|
315
|
-
signals,
|
|
316
|
-
isDelete: FILE_DELETE_TOOL_NAMES.has(toolKind),
|
|
317
|
-
locationLabel: 'outside_repo',
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
if (workspaceMatch.kind === 'trusted') {
|
|
321
|
-
signals.push('trusted_workspace_root');
|
|
322
|
-
}
|
|
323
|
-
const trustedCwd = resolveClassifierTrustedCwd(cwd, options);
|
|
324
|
-
const workspacePrescan = mutationPrescanRequiresAsk({
|
|
325
|
-
targets: [filePath],
|
|
326
|
-
cwd,
|
|
327
|
-
repoRoot,
|
|
328
|
-
trustedCwd,
|
|
329
|
-
trustedWorkspaceRoots: options.trustedWorkspaceRoots,
|
|
330
|
-
sensitivePaths,
|
|
331
|
-
});
|
|
332
|
-
if (workspacePrescan) {
|
|
333
|
-
return classifyFileMutationWithPolicy({
|
|
334
|
-
toolName,
|
|
335
|
-
toolKind,
|
|
336
|
-
filePath,
|
|
337
|
-
resolvedPath,
|
|
338
|
-
repoRoot,
|
|
339
|
-
cwd,
|
|
340
|
-
config,
|
|
341
|
-
options,
|
|
342
|
-
signals,
|
|
343
|
-
isDelete: FILE_DELETE_TOOL_NAMES.has(toolKind),
|
|
344
|
-
locationLabel: 'sensitive_path',
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
const relativePath = workspaceMatch.relativePath;
|
|
348
|
-
if (matchesSensitivePath(relativePath, sensitivePaths)) {
|
|
349
|
-
signals.push('sensitive_path');
|
|
350
|
-
return classifyFileMutationWithPolicy({
|
|
351
|
-
toolName,
|
|
352
|
-
toolKind,
|
|
353
|
-
filePath,
|
|
354
|
-
resolvedPath,
|
|
355
|
-
repoRoot,
|
|
356
|
-
cwd,
|
|
357
|
-
config,
|
|
358
|
-
options,
|
|
359
|
-
signals,
|
|
360
|
-
isDelete: FILE_DELETE_TOOL_NAMES.has(toolKind),
|
|
361
|
-
locationLabel: 'sensitive_path',
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
if (FILE_DELETE_TOOL_NAMES.has(toolKind)) {
|
|
365
|
-
if (isGitPath(resolvedPath, repoRoot)) {
|
|
366
|
-
signals.push('protected_artifact');
|
|
367
|
-
return classifyFileMutationWithPolicy({
|
|
368
|
-
toolName,
|
|
369
|
-
toolKind,
|
|
370
|
-
filePath,
|
|
371
|
-
resolvedPath,
|
|
372
|
-
repoRoot,
|
|
373
|
-
cwd,
|
|
374
|
-
config,
|
|
375
|
-
options,
|
|
376
|
-
signals,
|
|
377
|
-
isDelete: true,
|
|
378
|
-
locationLabel: 'sensitive_path',
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
signals.push('file_delete');
|
|
382
|
-
return classifyFileMutationWithPolicy({
|
|
383
|
-
toolName,
|
|
384
|
-
toolKind,
|
|
385
|
-
filePath,
|
|
386
|
-
resolvedPath,
|
|
387
|
-
repoRoot,
|
|
388
|
-
cwd,
|
|
389
|
-
config,
|
|
390
|
-
options,
|
|
391
|
-
signals,
|
|
392
|
-
isDelete: true,
|
|
393
|
-
locationLabel: 'repo_local',
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
signals.push('file_mutation');
|
|
397
|
-
return classifyFileMutationWithPolicy({
|
|
587
|
+
return classifyFilePathMutation({
|
|
398
588
|
toolName,
|
|
399
589
|
toolKind,
|
|
400
590
|
filePath,
|
|
401
|
-
resolvedPath,
|
|
402
591
|
repoRoot,
|
|
403
592
|
cwd,
|
|
404
593
|
config,
|
|
405
594
|
options,
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
595
|
+
sensitivePaths,
|
|
596
|
+
protectedRoots,
|
|
597
|
+
isDelete: effect === 'file_delete',
|
|
409
598
|
});
|
|
410
599
|
}
|
|
411
|
-
if (
|
|
600
|
+
if (effect === 'apply_patch') {
|
|
412
601
|
const patch = extractPatch(payload);
|
|
413
602
|
const targets = patch ? applyPatchTargets(patch) : [];
|
|
414
603
|
if (targets.length === 0) {
|
|
@@ -445,7 +634,7 @@ export async function classifyToolUse(payload, repoRoot, cwd, config, options =
|
|
|
445
634
|
for (const target of targets) {
|
|
446
635
|
const result = await classifyToolUse({
|
|
447
636
|
tool_name: target.delete ? 'Delete' : 'Write',
|
|
448
|
-
tool_input: { path: target.path },
|
|
637
|
+
tool_input: target.delete ? { path: target.path } : { path: target.path, contents: ' ' },
|
|
449
638
|
}, repoRoot, cwd, config, options);
|
|
450
639
|
if (result.verdict === 'deny_pending_approval') {
|
|
451
640
|
return result;
|
|
@@ -466,17 +655,52 @@ export async function classifyToolUse(payload, repoRoot, cwd, config, options =
|
|
|
466
655
|
},
|
|
467
656
|
};
|
|
468
657
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
658
|
+
if (effect === 'file_read') {
|
|
659
|
+
const filePath = extractFilePath(payload);
|
|
660
|
+
if (!filePath) {
|
|
661
|
+
return indeterminateToolResult(toolName, payload, repoRoot, options);
|
|
662
|
+
}
|
|
663
|
+
return classifyFilePathRead({
|
|
664
|
+
toolName,
|
|
665
|
+
toolKind,
|
|
666
|
+
filePath,
|
|
667
|
+
repoRoot,
|
|
668
|
+
cwd,
|
|
669
|
+
config,
|
|
670
|
+
options,
|
|
671
|
+
sensitivePaths,
|
|
672
|
+
protectedRoots,
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
if (effect === 'search_read') {
|
|
676
|
+
return {
|
|
677
|
+
verdict: 'allow',
|
|
678
|
+
reason: 'effect.search_read',
|
|
679
|
+
summary: canonicalStringify(scrubPayload(payload.tool_input ?? {}, options)),
|
|
680
|
+
fingerprint: toolFingerprint(toolName, fingerprintPayload(payload, options), repoRoot),
|
|
681
|
+
assessment: {
|
|
682
|
+
reversibility: 'reversible',
|
|
683
|
+
external: false,
|
|
684
|
+
blastRadius: 'tool scope',
|
|
685
|
+
confidence: 0.88,
|
|
686
|
+
signals: ['effect.search_read'],
|
|
687
|
+
},
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
const filePath = extractFilePath(payload);
|
|
691
|
+
if (filePath) {
|
|
692
|
+
return classifyFilePathMutation({
|
|
693
|
+
toolName,
|
|
694
|
+
toolKind,
|
|
695
|
+
filePath,
|
|
696
|
+
repoRoot,
|
|
697
|
+
cwd,
|
|
698
|
+
config,
|
|
699
|
+
options,
|
|
700
|
+
sensitivePaths,
|
|
701
|
+
protectedRoots,
|
|
702
|
+
isDelete: toolKind === 'delete',
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
return indeterminateToolResult(toolName, payload, repoRoot, options);
|
|
482
706
|
}
|