@guilz-dev/belay 0.4.0 → 0.7.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 +27 -2
- package/dist/adapters/claude/runtime-entry.js +54 -25
- package/dist/adapters/layouts/scope.js +3 -2
- package/dist/adapters/shared/gate-runtime.d.ts +8 -2
- package/dist/adapters/shared/gate-runtime.js +579 -181
- package/dist/bundle/claude-runtime.mjs +13574 -6334
- package/dist/bundle/codex-runtime.mjs +13108 -5903
- package/dist/bundle/cursor-runtime.mjs +12676 -5476
- package/dist/cli.js +80 -2
- package/dist/commands/approve.js +34 -10
- package/dist/commands/classify-for-report.js +1 -1
- package/dist/commands/config.d.ts +8 -0
- package/dist/commands/config.js +134 -11
- package/dist/commands/doctor.js +87 -1
- package/dist/commands/explain.js +11 -1
- package/dist/commands/judge.js +3 -2
- package/dist/commands/recovery-checkpoints.d.ts +164 -0
- package/dist/commands/recovery-checkpoints.js +347 -0
- package/dist/commands/revoke.js +19 -7
- package/dist/commands/session.d.ts +20 -0
- package/dist/commands/session.js +51 -0
- package/dist/commands/status.js +7 -0
- package/dist/config-io.js +91 -23
- package/dist/conformance/guarantee-posture.d.ts +21 -0
- package/dist/conformance/guarantee-posture.js +46 -0
- package/dist/conformance/guarantee-table.d.ts +1 -0
- package/dist/conformance/guarantee-table.js +20 -9
- package/dist/conformance/types.d.ts +1 -0
- package/dist/core/approval-replay.d.ts +1 -1
- package/dist/core/approval-replay.js +4 -1
- package/dist/core/approval-service.d.ts +6 -1
- package/dist/core/approval-service.js +87 -24
- package/dist/core/approval.d.ts +4 -0
- package/dist/core/approval.js +19 -5
- package/dist/core/audit-types.d.ts +13 -0
- package/dist/core/capability/approval-state-mutation.d.ts +32 -0
- package/dist/core/capability/approval-state-mutation.js +165 -0
- package/dist/core/capability/approval-v3.d.ts +32 -0
- package/dist/core/capability/approval-v3.js +114 -0
- package/dist/core/capability/attestation.d.ts +16 -0
- package/dist/core/capability/attestation.js +62 -0
- package/dist/core/capability/boundary-attestation-sign.d.ts +21 -0
- package/dist/core/capability/boundary-attestation-sign.js +84 -0
- package/dist/core/capability/boundary-driver-container.d.ts +19 -0
- package/dist/core/capability/boundary-driver-container.js +189 -0
- package/dist/core/capability/boundary-driver.d.ts +29 -0
- package/dist/core/capability/boundary-driver.js +44 -0
- package/dist/core/capability/boundary-egress.d.ts +21 -0
- package/dist/core/capability/boundary-egress.js +109 -0
- package/dist/core/capability/boundary-grant-materialize.d.ts +14 -0
- package/dist/core/capability/boundary-grant-materialize.js +93 -0
- package/dist/core/capability/boundary-profile.d.ts +15 -0
- package/dist/core/capability/boundary-profile.js +25 -0
- package/dist/core/capability/boundary-run.d.ts +48 -0
- package/dist/core/capability/boundary-run.js +51 -0
- package/dist/core/capability/boundary-session.d.ts +61 -0
- package/dist/core/capability/boundary-session.js +138 -0
- package/dist/core/capability/boundary-workspace-mount.d.ts +5 -0
- package/dist/core/capability/boundary-workspace-mount.js +68 -0
- package/dist/core/capability/capability-request-hash.d.ts +2 -0
- package/dist/core/capability/capability-request-hash.js +8 -0
- package/dist/core/capability/gate-latency-ratchet.d.ts +7 -0
- package/dist/core/capability/gate-latency-ratchet.js +18 -0
- package/dist/core/capability/gate-policy-shadow.d.ts +25 -0
- package/dist/core/capability/gate-policy-shadow.js +130 -0
- package/dist/core/capability/gate-shadow-audit.d.ts +1 -0
- package/dist/core/capability/gate-shadow-audit.js +1 -0
- package/dist/core/capability/gate-shadow-ratchet.d.ts +14 -0
- package/dist/core/capability/gate-shadow-ratchet.js +81 -0
- package/dist/core/capability/grant-consumption.d.ts +4 -0
- package/dist/core/capability/grant-consumption.js +11 -0
- package/dist/core/capability/grant-lease.d.ts +44 -0
- package/dist/core/capability/grant-lease.js +243 -0
- package/dist/core/capability/grant-loader.d.ts +12 -0
- package/dist/core/capability/grant-loader.js +23 -0
- package/dist/core/capability/grant-match.d.ts +6 -0
- package/dist/core/capability/grant-match.js +96 -0
- package/dist/core/capability/grant.d.ts +16 -0
- package/dist/core/capability/grant.js +16 -0
- package/dist/core/capability/index.d.ts +8 -0
- package/dist/core/capability/index.js +7 -0
- package/dist/core/capability/limits.d.ts +7 -0
- package/dist/core/capability/limits.js +55 -0
- package/dist/core/capability/policy-bridge.d.ts +18 -0
- package/dist/core/capability/policy-bridge.js +51 -0
- package/dist/core/capability/policy-engine.d.ts +77 -0
- package/dist/core/capability/policy-engine.js +663 -0
- package/dist/core/capability/policy-types.d.ts +20 -0
- package/dist/core/capability/policy-types.js +1 -0
- package/dist/core/capability/request.d.ts +48 -0
- package/dist/core/capability/request.js +1 -0
- package/dist/core/capability/resolver.d.ts +4 -0
- package/dist/core/capability/resolver.js +4 -0
- package/dist/core/classify-subagent.d.ts +2 -1
- package/dist/core/classify-subagent.js +38 -2
- package/dist/core/classify-tool.js +126 -62
- package/dist/core/config.d.ts +34 -3
- package/dist/core/config.js +118 -3
- package/dist/core/effect-ir/audit.d.ts +9 -0
- package/dist/core/effect-ir/audit.js +86 -0
- package/dist/core/effect-ir/build.d.ts +31 -0
- package/dist/core/effect-ir/build.js +277 -0
- package/dist/core/effect-ir/index.d.ts +8 -0
- package/dist/core/effect-ir/index.js +7 -0
- package/dist/core/effect-ir/normalize.d.ts +4 -0
- package/dist/core/effect-ir/normalize.js +104 -0
- package/dist/core/effect-ir/package-exec-eval.d.ts +8 -0
- package/dist/core/effect-ir/package-exec-eval.js +152 -0
- package/dist/core/effect-ir/package-exec.d.ts +29 -0
- package/dist/core/effect-ir/package-exec.js +292 -0
- package/dist/core/effect-ir/policy.d.ts +13 -0
- package/dist/core/effect-ir/policy.js +77 -0
- package/dist/core/effect-ir/types.d.ts +55 -0
- package/dist/core/effect-ir/types.js +1 -0
- package/dist/core/egress/allowlist.d.ts +1 -0
- package/dist/core/egress/allowlist.js +8 -0
- package/dist/core/egress-approval.js +45 -51
- package/dist/core/gate-contract.d.ts +5 -0
- package/dist/core/gate-contract.js +5 -0
- package/dist/core/gate-engine.js +30 -8
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.js +2 -1
- package/dist/core/judge-cloud-consent.js +29 -15
- package/dist/core/judge-doctor.js +39 -14
- package/dist/core/judge-fallback-hints.d.ts +13 -0
- package/dist/core/judge-fallback-hints.js +108 -0
- package/dist/core/network-endpoint.d.ts +10 -0
- package/dist/core/network-endpoint.js +51 -0
- package/dist/core/path-utils.d.ts +1 -0
- package/dist/core/path-utils.js +5 -2
- package/dist/core/process-runner.d.ts +10 -0
- package/dist/core/process-runner.js +124 -0
- package/dist/core/recover-advice.js +2 -1
- package/dist/core/recovery/artifact-store.d.ts +29 -0
- package/dist/core/recovery/artifact-store.js +336 -0
- package/dist/core/recovery/capability.d.ts +3 -0
- package/dist/core/recovery/capability.js +22 -0
- package/dist/core/recovery/checkpoint.d.ts +25 -0
- package/dist/core/recovery/checkpoint.js +260 -0
- package/dist/core/recovery/fail-closed.d.ts +6 -0
- package/dist/core/recovery/fail-closed.js +25 -0
- package/dist/core/recovery/index.d.ts +5 -0
- package/dist/core/recovery/index.js +4 -0
- package/dist/core/recovery/operator-guidance.d.ts +7 -0
- package/dist/core/recovery/operator-guidance.js +39 -0
- package/dist/core/recovery/reconcile.d.ts +3 -0
- package/dist/core/recovery/reconcile.js +48 -0
- package/dist/core/recovery/resource-identity.d.ts +4 -0
- package/dist/core/recovery/resource-identity.js +34 -0
- package/dist/core/recovery/restore.d.ts +13 -0
- package/dist/core/recovery/restore.js +114 -0
- package/dist/core/recovery/snapshot-node.d.ts +27 -0
- package/dist/core/recovery/snapshot-node.js +261 -0
- package/dist/core/recovery/types.d.ts +104 -0
- package/dist/core/recovery/types.js +1 -0
- package/dist/core/standing-allow.js +3 -0
- package/dist/core/transactional/apply-observed-changes.d.ts +13 -0
- package/dist/core/transactional/apply-observed-changes.js +275 -0
- package/dist/core/transactional/backend-selector.d.ts +8 -0
- package/dist/core/transactional/backend-selector.js +118 -0
- package/dist/core/transactional/backend.d.ts +55 -0
- package/dist/core/transactional/backend.js +1 -0
- package/dist/core/transactional/diff-evaluator.js +4 -1
- package/dist/core/transactional/eligibility.js +4 -0
- package/dist/core/transactional/file-checkpoint-backend.d.ts +3 -0
- package/dist/core/transactional/file-checkpoint-backend.js +278 -0
- package/dist/core/transactional/file-checkpoint-git.d.ts +11 -0
- package/dist/core/transactional/file-checkpoint-git.js +217 -0
- package/dist/core/transactional/file-checkpoint-staging.d.ts +13 -0
- package/dist/core/transactional/file-checkpoint-staging.js +54 -0
- package/dist/core/transactional/file-clone.d.ts +15 -0
- package/dist/core/transactional/file-clone.js +139 -0
- package/dist/core/transactional/file-tree-path.d.ts +7 -0
- package/dist/core/transactional/file-tree-path.js +50 -0
- package/dist/core/transactional/file-tree.d.ts +41 -0
- package/dist/core/transactional/file-tree.js +186 -0
- package/dist/core/transactional/git-worktree-backend.d.ts +2 -0
- package/dist/core/transactional/git-worktree-backend.js +50 -0
- package/dist/core/transactional/git-worktree.d.ts +14 -9
- package/dist/core/transactional/git-worktree.js +62 -87
- package/dist/core/transactional/index.d.ts +11 -0
- package/dist/core/transactional/index.js +10 -0
- package/dist/core/transactional/reasons.d.ts +1 -0
- package/dist/core/transactional/reasons.js +5 -0
- package/dist/core/transactional/runner.js +171 -23
- package/dist/core/transactional/snapshot-node.d.ts +26 -0
- package/dist/core/transactional/snapshot-node.js +87 -0
- package/dist/core/transactional/types.d.ts +21 -1
- package/dist/core/types.d.ts +30 -3
- package/dist/core/verdict/adapter.d.ts +3 -4
- package/dist/core/verdict/adapter.js +24 -12
- package/dist/core/verdict/cursor-acp-client.d.ts +67 -0
- package/dist/core/verdict/cursor-acp-client.js +345 -0
- package/dist/core/verdict/egress-classify.d.ts +2 -2
- package/dist/core/verdict/egress-classify.js +2 -2
- package/dist/core/verdict/git-classifier.d.ts +26 -0
- package/dist/core/verdict/git-classifier.js +533 -0
- package/dist/core/verdict/judge-audit.js +1 -0
- package/dist/core/verdict/judge-baseline.d.ts +1 -1
- package/dist/core/verdict/judge-baseline.js +1 -0
- package/dist/core/verdict/judge-broker-service.js +12 -0
- package/dist/core/verdict/judge-catalog.js +1 -0
- package/dist/core/verdict/judge-cli-fingerprint.d.ts +1 -0
- package/dist/core/verdict/judge-cli-fingerprint.js +6 -0
- package/dist/core/verdict/judge-cli.js +5 -3
- package/dist/core/verdict/judge-provider-matrix.js +1 -1
- package/dist/core/verdict/judge-runtime-config.d.ts +2 -0
- package/dist/core/verdict/judge-runtime-config.js +10 -6
- package/dist/core/verdict/judge-session-broker.d.ts +1 -1
- package/dist/core/verdict/judge-session-broker.js +10 -5
- package/dist/core/verdict/judge-transport.d.ts +5 -1
- package/dist/core/verdict/judge-transport.js +24 -14
- package/dist/core/verdict/judge.d.ts +2 -15
- package/dist/core/verdict/judge.js +5 -68
- package/dist/core/verdict/launcher-resolve.d.ts +2 -0
- package/dist/core/verdict/launcher-resolve.js +14 -16
- package/dist/core/verdict/prescan.d.ts +15 -0
- package/dist/core/verdict/prescan.js +68 -0
- package/dist/core/verdict/shell-policy.d.ts +26 -0
- package/dist/core/verdict/shell-policy.js +40 -0
- package/dist/core/verdict/shell-semantics.d.ts +24 -0
- package/dist/core/verdict/shell-semantics.js +225 -0
- package/dist/core/verdict/types.d.ts +18 -1
- package/dist/core/verdict/verdict.js +313 -178
- package/dist/corpus/evaluate.js +3 -7
- package/dist/corpus/gate-latency-budget.d.ts +39 -0
- package/dist/corpus/gate-latency-budget.js +33 -0
- package/dist/corpus/mutators.d.ts +1 -1
- package/dist/corpus/mutators.js +11 -0
- package/dist/corpus/runtime-match.js +1 -3
- package/dist/corpus/standing-allow-catalog.generated.js +29 -9
- package/dist/judge-broker-daemon.js +15 -1
- package/dist/services/sandbox-service.d.ts +4 -0
- package/dist/services/sandbox-service.js +28 -2
- package/dist/types.d.ts +13 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/belay/SKILL.md +8 -3
- package/skills/belay/belay-approve.md +10 -7
|
@@ -1,12 +1,20 @@
|
|
|
1
|
-
import
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { policyDecisionToLegacyReason } from '../capability/policy-bridge.js';
|
|
3
|
+
import { combinePolicyDecisions, policyDecisionRequiresAsk } from '../capability/policy-engine.js';
|
|
4
|
+
import { mergeEffectPlans } from '../effect-ir/normalize.js';
|
|
5
|
+
import { isPackageExecLauncher } from '../effect-ir/package-exec.js';
|
|
6
|
+
import { evaluatePackageExecSegment } from '../effect-ir/package-exec-eval.js';
|
|
7
|
+
import { canonicalPath, resolveMutationTarget } from '../path-utils.js';
|
|
2
8
|
import { extractRedirectTargets, isFdDuplication, isRedirectOperator, tokenizeShell, } from '../shell-tokenizer.js';
|
|
3
9
|
import { analyzePathTargets, cwdRelative, isDestructiveHighStakesMutation, resolveTrustedPath, touchesProtectedRoot, } from './containment.js';
|
|
4
10
|
import { classifyEgressTool } from './egress-classify.js';
|
|
5
11
|
import { verdictFingerprint } from './fingerprint.js';
|
|
6
|
-
import {
|
|
7
|
-
import { isRoutineLauncher, resolveLauncherRecipe } from './launcher-resolve.js';
|
|
12
|
+
import { isReadOnlyLauncherInvocation, isRoutineLauncher, resolveLauncherRecipe, } from './launcher-resolve.js';
|
|
8
13
|
import { allowFromCustomOverride, askFromCustomExternal, customAllowMatch, customExternalMatch, } from './overrides.js';
|
|
9
14
|
import { extractRecursiveScript, isBareInterpreter, isVariableIndirectHead, parseSegment, peelTransparentWrappers, redactCommand, segmentOpacity, splitTopLevelSegments, substitutionInners, } from './parser.js';
|
|
15
|
+
import { mutationPrescanRequiresAsk, prescanInterpreterCode, tier1RequiresAsk } from './prescan.js';
|
|
16
|
+
import { evaluateSegmentShellPolicy, shellPolicyMetadata, } from './shell-policy.js';
|
|
17
|
+
import { analyzeShellCommandSemantics, isPureReadOnlySemantics, LOCAL_MUTATION_KEYS, } from './shell-semantics.js';
|
|
10
18
|
const DEFAULT_MAX_DEPTH = 8;
|
|
11
19
|
const TIER0_EXTERNAL_KEYS = new Set([
|
|
12
20
|
'git push',
|
|
@@ -20,7 +28,6 @@ const TIER0_EXTERNAL_KEYS = new Set([
|
|
|
20
28
|
'supabase',
|
|
21
29
|
'scp',
|
|
22
30
|
'ssh',
|
|
23
|
-
'rsync',
|
|
24
31
|
]);
|
|
25
32
|
const TIER0_EXTERNAL_HEADS = new Set([
|
|
26
33
|
'dropdb',
|
|
@@ -30,66 +37,6 @@ const TIER0_EXTERNAL_HEADS = new Set([
|
|
|
30
37
|
'mongosh',
|
|
31
38
|
'redis-cli',
|
|
32
39
|
]);
|
|
33
|
-
const READ_ONLY_KEYS = new Set([
|
|
34
|
-
'cat',
|
|
35
|
-
'cd',
|
|
36
|
-
'echo',
|
|
37
|
-
'git diff',
|
|
38
|
-
'git log',
|
|
39
|
-
'git rev-parse',
|
|
40
|
-
'git show',
|
|
41
|
-
'git status',
|
|
42
|
-
'head',
|
|
43
|
-
'ls',
|
|
44
|
-
'pwd',
|
|
45
|
-
'rg',
|
|
46
|
-
'sort',
|
|
47
|
-
'tail',
|
|
48
|
-
'wc',
|
|
49
|
-
'which',
|
|
50
|
-
'find',
|
|
51
|
-
]);
|
|
52
|
-
const PURE_READ_ONLY_KEYS = new Set([
|
|
53
|
-
'echo',
|
|
54
|
-
'git diff',
|
|
55
|
-
'git log',
|
|
56
|
-
'git rev-parse',
|
|
57
|
-
'git show',
|
|
58
|
-
'git status',
|
|
59
|
-
'pwd',
|
|
60
|
-
'which',
|
|
61
|
-
]);
|
|
62
|
-
const LOCAL_MUTATION_KEYS = new Set([
|
|
63
|
-
'chmod',
|
|
64
|
-
'cp',
|
|
65
|
-
'git add',
|
|
66
|
-
'git clean',
|
|
67
|
-
'git commit',
|
|
68
|
-
'git mv',
|
|
69
|
-
'git reset',
|
|
70
|
-
'mkdir',
|
|
71
|
-
'mv',
|
|
72
|
-
'rm',
|
|
73
|
-
'sed',
|
|
74
|
-
'tee',
|
|
75
|
-
'touch',
|
|
76
|
-
'truncate',
|
|
77
|
-
]);
|
|
78
|
-
/** Routine local build/test runners resolved from launcher recipes. */
|
|
79
|
-
const LOCAL_ROUTINE_HEADS = new Set([
|
|
80
|
-
'tsc',
|
|
81
|
-
'vitest',
|
|
82
|
-
'vite',
|
|
83
|
-
'webpack',
|
|
84
|
-
'esbuild',
|
|
85
|
-
'rollup',
|
|
86
|
-
'jest',
|
|
87
|
-
'mocha',
|
|
88
|
-
'cargo',
|
|
89
|
-
'go',
|
|
90
|
-
'make',
|
|
91
|
-
'cmake',
|
|
92
|
-
]);
|
|
93
40
|
const BELAY_SELF_COMMANDS = new Set(['approve', 'revoke']);
|
|
94
41
|
const FIND_DANGEROUS_FLAGS = new Set(['-delete', '-exec', '-execdir', '-ok', '-okdir']);
|
|
95
42
|
function isFindDangerous(tokens) {
|
|
@@ -126,7 +73,7 @@ async function evaluateSubstitutions(command, context, depth) {
|
|
|
126
73
|
}
|
|
127
74
|
worst = worst ? combineInternal(worst, innerVerdict) : innerVerdict;
|
|
128
75
|
}
|
|
129
|
-
if (!worst) {
|
|
76
|
+
if (!worst || worst.permission === 'allow') {
|
|
130
77
|
return null;
|
|
131
78
|
}
|
|
132
79
|
return {
|
|
@@ -150,6 +97,12 @@ function mergeLocation(left, right) {
|
|
|
150
97
|
return 'mixed';
|
|
151
98
|
}
|
|
152
99
|
function combineInternal(left, right) {
|
|
100
|
+
const policyDecisions = [
|
|
101
|
+
...(left.effectPlanPolicyDecisions ??
|
|
102
|
+
(left.authorizationDecision ? [left.authorizationDecision] : [])),
|
|
103
|
+
...(right.effectPlanPolicyDecisions ??
|
|
104
|
+
(right.authorizationDecision ? [right.authorizationDecision] : [])),
|
|
105
|
+
];
|
|
153
106
|
return {
|
|
154
107
|
permission: worsePermission(left.permission, right.permission),
|
|
155
108
|
location: mergeLocation(left.location, right.location),
|
|
@@ -177,19 +130,37 @@ function combineInternal(left, right) {
|
|
|
177
130
|
: right.reason,
|
|
178
131
|
signals: [...new Set([...left.signals, ...right.signals])],
|
|
179
132
|
judgeTrace: right.judgeTrace ?? left.judgeTrace,
|
|
133
|
+
capabilityRequests: [...(left.capabilityRequests ?? []), ...(right.capabilityRequests ?? [])],
|
|
134
|
+
authorizationDecision: policyDecisions.length > 0 ? combinePolicyDecisions(policyDecisions) : undefined,
|
|
135
|
+
effectPlan: mergeEffectPlans(left.effectPlan, right.effectPlan),
|
|
136
|
+
effectPlanPolicyDecisions: policyDecisions,
|
|
180
137
|
};
|
|
181
138
|
}
|
|
182
139
|
function askVerdict(params) {
|
|
183
140
|
return { ...params, permission: 'ask' };
|
|
184
141
|
}
|
|
185
|
-
function
|
|
186
|
-
|
|
142
|
+
function resolveShellPathTargets(pathArgs, context) {
|
|
143
|
+
if (!context.cwd) {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
return pathArgs.map((target) => {
|
|
147
|
+
const resolved = (context.trustedCwd ? resolveTrustedPath(target, context.cwd, context.trustedCwd) : null) ??
|
|
148
|
+
resolveMutationTarget(target, context.cwd);
|
|
149
|
+
if (!resolved) {
|
|
150
|
+
const joined = path.isAbsolute(target) ? target : path.join(context.cwd, target);
|
|
151
|
+
return canonicalPath(joined);
|
|
152
|
+
}
|
|
153
|
+
return canonicalPath(resolved);
|
|
154
|
+
});
|
|
187
155
|
}
|
|
188
|
-
function
|
|
189
|
-
if (
|
|
190
|
-
return
|
|
156
|
+
function withShellPolicyMetadata(input, existing = {}) {
|
|
157
|
+
if (existing.capabilityRequests?.length) {
|
|
158
|
+
return existing;
|
|
191
159
|
}
|
|
192
|
-
return
|
|
160
|
+
return shellPolicyMetadata(input);
|
|
161
|
+
}
|
|
162
|
+
function allowVerdict(params) {
|
|
163
|
+
return { ...params, permission: 'allow' };
|
|
193
164
|
}
|
|
194
165
|
function extractPathArgs(tokens) {
|
|
195
166
|
const redirects = extractRedirectTargets(tokens);
|
|
@@ -209,8 +180,15 @@ function extractPathArgs(tokens) {
|
|
|
209
180
|
function isVariableOrOpaquePathToken(token) {
|
|
210
181
|
return token.includes('$') || token.includes('`');
|
|
211
182
|
}
|
|
212
|
-
function
|
|
213
|
-
|
|
183
|
+
function pathContextFromSemantics(context, semantics) {
|
|
184
|
+
const cwd = semantics.gitWorkTree ?? semantics.effectiveCwd;
|
|
185
|
+
if (!cwd || cwd === context.cwd) {
|
|
186
|
+
return context;
|
|
187
|
+
}
|
|
188
|
+
return { ...context, cwd };
|
|
189
|
+
}
|
|
190
|
+
function isPureReadOnlySegment(segment, semantics) {
|
|
191
|
+
return isPureReadOnlySemantics(segment, semantics);
|
|
214
192
|
}
|
|
215
193
|
function updateChainState(command, state) {
|
|
216
194
|
const segment = parseSegment(command);
|
|
@@ -233,10 +211,13 @@ function updateChainState(command, state) {
|
|
|
233
211
|
trustedCwd: true,
|
|
234
212
|
};
|
|
235
213
|
}
|
|
236
|
-
function tier0ExternalMatch(key, head, tokens) {
|
|
214
|
+
function tier0ExternalMatch(key, head, tokens, signals = []) {
|
|
237
215
|
if (TIER0_EXTERNAL_KEYS.has(key)) {
|
|
238
216
|
return true;
|
|
239
217
|
}
|
|
218
|
+
if (signals.includes('git.push')) {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
240
221
|
if (TIER0_EXTERNAL_HEADS.has(head)) {
|
|
241
222
|
return true;
|
|
242
223
|
}
|
|
@@ -248,9 +229,6 @@ function tier0ExternalMatch(key, head, tokens) {
|
|
|
248
229
|
tokens.some((t) => t === '--push' || t.startsWith('--output=type=registry')))) {
|
|
249
230
|
return true;
|
|
250
231
|
}
|
|
251
|
-
if (head === 'git' && tokens[1] === 'push') {
|
|
252
|
-
return true;
|
|
253
|
-
}
|
|
254
232
|
if (head === 'terraform' && tokens[1] === 'apply') {
|
|
255
233
|
return true;
|
|
256
234
|
}
|
|
@@ -355,6 +333,39 @@ function tier0HighStakesRm(tokens, context) {
|
|
|
355
333
|
}
|
|
356
334
|
return null;
|
|
357
335
|
}
|
|
336
|
+
function verdictFromOpacityPolicy(params) {
|
|
337
|
+
const { requests, decision } = evaluateSegmentShellPolicy({
|
|
338
|
+
command: params.command,
|
|
339
|
+
segmentHead: params.segmentHead,
|
|
340
|
+
effect: params.effect,
|
|
341
|
+
location: params.location,
|
|
342
|
+
opacity: params.opacity,
|
|
343
|
+
pathArgs: params.pathArgs,
|
|
344
|
+
signals: params.signals,
|
|
345
|
+
context: params.context,
|
|
346
|
+
});
|
|
347
|
+
const base = {
|
|
348
|
+
location: params.location,
|
|
349
|
+
opacity: params.opacity,
|
|
350
|
+
effect: params.effect,
|
|
351
|
+
confidence: 'deterministic',
|
|
352
|
+
signals: [...params.signals, ...decision.signals],
|
|
353
|
+
capabilityRequests: requests,
|
|
354
|
+
authorizationDecision: decision,
|
|
355
|
+
};
|
|
356
|
+
if (policyDecisionRequiresAsk(decision)) {
|
|
357
|
+
return {
|
|
358
|
+
permission: 'ask',
|
|
359
|
+
reason: policyDecisionToLegacyReason(decision),
|
|
360
|
+
...base,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
permission: 'allow',
|
|
365
|
+
reason: policyDecisionToLegacyReason(decision),
|
|
366
|
+
...base,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
358
369
|
async function evaluateSegment(command, context, depth) {
|
|
359
370
|
const maxDepth = context.maxRecursionDepth ?? DEFAULT_MAX_DEPTH;
|
|
360
371
|
if (depth > maxDepth) {
|
|
@@ -369,23 +380,15 @@ async function evaluateSegment(command, context, depth) {
|
|
|
369
380
|
}
|
|
370
381
|
const opacity = segmentOpacity(command);
|
|
371
382
|
if (opacity === 'unparseable') {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
opacity: 'unparseable',
|
|
376
|
-
effect: 'unknown',
|
|
377
|
-
confidence: 'deterministic',
|
|
378
|
-
reason: 'unparseable_shell',
|
|
379
|
-
signals: ['unparseable_shell'],
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
return allowVerdict({
|
|
383
|
-
location: 'unknown',
|
|
383
|
+
return verdictFromOpacityPolicy({
|
|
384
|
+
command,
|
|
385
|
+
segmentHead: parseSegment(command).head,
|
|
384
386
|
opacity: 'unparseable',
|
|
385
387
|
effect: 'unknown',
|
|
386
|
-
|
|
387
|
-
|
|
388
|
+
location: 'unknown',
|
|
389
|
+
pathArgs: [],
|
|
388
390
|
signals: ['unparseable_shell'],
|
|
391
|
+
context,
|
|
389
392
|
});
|
|
390
393
|
}
|
|
391
394
|
const substitutionResult = await evaluateSubstitutions(command, context, depth);
|
|
@@ -395,13 +398,15 @@ async function evaluateSegment(command, context, depth) {
|
|
|
395
398
|
const tokens = tokenizeShell(command);
|
|
396
399
|
const { tokens: peeled, xargsStdinOpaque } = peelTransparentWrappers(tokens);
|
|
397
400
|
if (xargsStdinOpaque || isBareInterpreter(tokens)) {
|
|
398
|
-
return
|
|
399
|
-
|
|
401
|
+
return verdictFromOpacityPolicy({
|
|
402
|
+
command,
|
|
403
|
+
segmentHead: parseSegment(command).head,
|
|
400
404
|
opacity: 'opaque',
|
|
401
405
|
effect: 'unknown',
|
|
402
|
-
|
|
403
|
-
|
|
406
|
+
location: 'unknown',
|
|
407
|
+
pathArgs: [],
|
|
404
408
|
signals: ['opaque_execution'],
|
|
409
|
+
context,
|
|
405
410
|
});
|
|
406
411
|
}
|
|
407
412
|
const segment = parseSegment(command);
|
|
@@ -430,13 +435,28 @@ async function evaluateSegment(command, context, depth) {
|
|
|
430
435
|
if (recursiveScript) {
|
|
431
436
|
const prescan = prescanInterpreterCode(recursiveScript);
|
|
432
437
|
if (prescan && tier1RequiresAsk(prescan)) {
|
|
438
|
+
const { requests, decision } = evaluateSegmentShellPolicy({
|
|
439
|
+
command: recursiveScript,
|
|
440
|
+
segmentHead: segment.head,
|
|
441
|
+
effect: 'unknown',
|
|
442
|
+
location: 'unknown',
|
|
443
|
+
opacity: 'recursive',
|
|
444
|
+
pathArgs: [],
|
|
445
|
+
signals: ['interpreter_secret_prescan', prescan.reason],
|
|
446
|
+
context,
|
|
447
|
+
});
|
|
448
|
+
const legacyReason = policyDecisionRequiresAsk(decision)
|
|
449
|
+
? policyDecisionToLegacyReason(decision)
|
|
450
|
+
: 'interpreter_secret_prescan';
|
|
433
451
|
return askVerdict({
|
|
434
452
|
location: 'unknown',
|
|
435
453
|
opacity: 'recursive',
|
|
436
454
|
effect: 'unknown',
|
|
437
455
|
confidence: 'deterministic',
|
|
438
|
-
reason:
|
|
439
|
-
signals: ['interpreter_secret_prescan'],
|
|
456
|
+
reason: legacyReason,
|
|
457
|
+
signals: ['interpreter_secret_prescan', prescan.reason, ...decision.signals],
|
|
458
|
+
capabilityRequests: requests,
|
|
459
|
+
authorizationDecision: decision,
|
|
440
460
|
});
|
|
441
461
|
}
|
|
442
462
|
const innerVerdict = await evaluateSegment(recursiveScript, context, depth + 1);
|
|
@@ -452,6 +472,14 @@ async function evaluateSegment(command, context, depth) {
|
|
|
452
472
|
signals: [...innerVerdict.signals, 'recursive_wrapper'],
|
|
453
473
|
};
|
|
454
474
|
}
|
|
475
|
+
if (isPackageExecLauncher(peeled)) {
|
|
476
|
+
return evaluatePackageExecSegment({
|
|
477
|
+
command,
|
|
478
|
+
peeled,
|
|
479
|
+
context,
|
|
480
|
+
evaluateInner: (innerRecipe) => evaluateSegment(innerRecipe, context, depth + 1),
|
|
481
|
+
});
|
|
482
|
+
}
|
|
455
483
|
if (isRoutineLauncher(peeled)) {
|
|
456
484
|
const resolution = resolveLauncherRecipe({
|
|
457
485
|
tokens: peeled,
|
|
@@ -481,8 +509,10 @@ async function evaluateSegment(command, context, depth) {
|
|
|
481
509
|
}
|
|
482
510
|
let innerVerdict = null;
|
|
483
511
|
for (const recipe of resolution.recipes) {
|
|
484
|
-
const
|
|
485
|
-
|
|
512
|
+
for (const sub of splitTopLevelSegments(recipe)) {
|
|
513
|
+
const evaluated = await evaluateSegment(sub, context, depth + 1);
|
|
514
|
+
innerVerdict = innerVerdict ? combineInternal(innerVerdict, evaluated) : evaluated;
|
|
515
|
+
}
|
|
486
516
|
}
|
|
487
517
|
if (!innerVerdict) {
|
|
488
518
|
return askVerdict({
|
|
@@ -500,38 +530,78 @@ async function evaluateSegment(command, context, depth) {
|
|
|
500
530
|
signals: [...innerVerdict.signals, resolution.reason],
|
|
501
531
|
};
|
|
502
532
|
}
|
|
503
|
-
const
|
|
533
|
+
const semantics = analyzeShellCommandSemantics(peeled, segment, context.cwd);
|
|
534
|
+
const pathContext = pathContextFromSemantics(context, semantics);
|
|
535
|
+
if (semantics.requiresAsk) {
|
|
536
|
+
const { requests, decision } = evaluateSegmentShellPolicy({
|
|
537
|
+
command,
|
|
538
|
+
segmentHead: segment.head,
|
|
539
|
+
effect: 'local_mutation',
|
|
540
|
+
location: 'repo_local',
|
|
541
|
+
opacity: 'transparent',
|
|
542
|
+
pathArgs: semantics.pathTargets,
|
|
543
|
+
signals: semantics.requiresAsk.signals,
|
|
544
|
+
context: pathContext,
|
|
545
|
+
});
|
|
546
|
+
return askVerdict({
|
|
547
|
+
location: 'repo_local',
|
|
548
|
+
opacity: 'transparent',
|
|
549
|
+
effect: 'local_mutation',
|
|
550
|
+
confidence: 'deterministic',
|
|
551
|
+
reason: semantics.requiresAsk.reason,
|
|
552
|
+
signals: semantics.requiresAsk.signals,
|
|
553
|
+
capabilityRequests: requests,
|
|
554
|
+
authorizationDecision: decision,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
const egressClass = semantics.egressClass ?? classifyEgressTool(segment.head, peeled);
|
|
504
558
|
if (egressClass === 'destructive') {
|
|
559
|
+
const { requests, decision } = evaluateSegmentShellPolicy({
|
|
560
|
+
command,
|
|
561
|
+
segmentHead: segment.head,
|
|
562
|
+
effect: 'remote_mutation',
|
|
563
|
+
location: 'external',
|
|
564
|
+
opacity: 'transparent',
|
|
565
|
+
egressClass: 'destructive',
|
|
566
|
+
pathArgs: [],
|
|
567
|
+
signals: ['tier0_external', segment.head],
|
|
568
|
+
context,
|
|
569
|
+
});
|
|
505
570
|
return askVerdict({
|
|
506
571
|
location: 'external',
|
|
507
572
|
opacity: 'transparent',
|
|
508
573
|
effect: 'remote_mutation',
|
|
509
574
|
confidence: 'deterministic',
|
|
510
575
|
reason: 'tier0_external',
|
|
511
|
-
signals: ['tier0_external', segment.head],
|
|
576
|
+
signals: ['tier0_external', segment.head, ...decision.signals],
|
|
577
|
+
capabilityRequests: requests,
|
|
578
|
+
authorizationDecision: decision,
|
|
512
579
|
});
|
|
513
580
|
}
|
|
514
|
-
if (
|
|
515
|
-
|
|
581
|
+
if (tier0ExternalMatch(semantics.normalizedKey ?? segment.key, segment.head, peeled, semantics.signals)) {
|
|
582
|
+
const tier0Key = semantics.normalizedKey ?? segment.key;
|
|
583
|
+
const { requests, decision } = evaluateSegmentShellPolicy({
|
|
584
|
+
command,
|
|
585
|
+
segmentHead: segment.head,
|
|
586
|
+
effect: 'remote_mutation',
|
|
516
587
|
location: 'external',
|
|
517
588
|
opacity: 'transparent',
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
signals: ['egress_read', segment.head],
|
|
589
|
+
pathArgs: [],
|
|
590
|
+
signals: ['tier0_external', tier0Key, ...semantics.signals],
|
|
591
|
+
context: pathContext,
|
|
522
592
|
});
|
|
523
|
-
}
|
|
524
|
-
if (tier0ExternalMatch(segment.key, segment.head, peeled)) {
|
|
525
593
|
return askVerdict({
|
|
526
594
|
location: 'external',
|
|
527
595
|
opacity: 'transparent',
|
|
528
596
|
effect: 'remote_mutation',
|
|
529
597
|
confidence: 'deterministic',
|
|
530
598
|
reason: 'tier0_external',
|
|
531
|
-
signals: ['tier0_external',
|
|
599
|
+
signals: ['tier0_external', tier0Key, ...decision.signals],
|
|
600
|
+
capabilityRequests: requests,
|
|
601
|
+
authorizationDecision: decision,
|
|
532
602
|
});
|
|
533
603
|
}
|
|
534
|
-
const rmVerdict = tier0HighStakesRm(peeled,
|
|
604
|
+
const rmVerdict = tier0HighStakesRm(peeled, pathContext);
|
|
535
605
|
if (rmVerdict) {
|
|
536
606
|
return rmVerdict;
|
|
537
607
|
}
|
|
@@ -545,29 +615,29 @@ async function evaluateSegment(command, context, depth) {
|
|
|
545
615
|
signals: ['belay_control_plane_command', segment.head],
|
|
546
616
|
});
|
|
547
617
|
}
|
|
548
|
-
let effect =
|
|
549
|
-
if (
|
|
618
|
+
let effect = semantics.effect;
|
|
619
|
+
if (isReadOnlyLauncherInvocation(peeled)) {
|
|
550
620
|
effect = 'read_only';
|
|
551
621
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
}
|
|
555
|
-
else if (LOCAL_ROUTINE_HEADS.has(segment.head)) {
|
|
556
|
-
effect = 'local_mutation';
|
|
557
|
-
}
|
|
558
|
-
const pathArgs = extractPathArgs(peeled);
|
|
559
|
-
if (extractRedirectTargets(peeled).length > 0 && effect === 'read_only') {
|
|
560
|
-
effect = 'local_mutation';
|
|
561
|
-
}
|
|
622
|
+
const pathArgs = semantics.pathTargets;
|
|
623
|
+
const policyPathArgs = [...new Set([...pathArgs, ...(semantics.scopeTargets ?? [])])];
|
|
562
624
|
const pathAnalysis = analyzePathTargets({
|
|
563
|
-
targets:
|
|
564
|
-
cwd:
|
|
565
|
-
repoRoot:
|
|
566
|
-
trustedCwd:
|
|
567
|
-
trustedWorkspaceRoots:
|
|
568
|
-
sensitivePaths:
|
|
569
|
-
protectedArtifactRoots:
|
|
625
|
+
targets: policyPathArgs,
|
|
626
|
+
cwd: pathContext.cwd,
|
|
627
|
+
repoRoot: pathContext.repoRoot,
|
|
628
|
+
trustedCwd: pathContext.trustedCwd,
|
|
629
|
+
trustedWorkspaceRoots: pathContext.trustedWorkspaceRoots,
|
|
630
|
+
sensitivePaths: pathContext.sensitivePaths,
|
|
631
|
+
protectedArtifactRoots: pathContext.protectedArtifactRoots,
|
|
570
632
|
});
|
|
633
|
+
// Path-less read/mutation in a trusted cwd: assume repo-local (e.g. git status, tsc --noEmit).
|
|
634
|
+
const effectiveLocation = policyPathArgs.length === 0 &&
|
|
635
|
+
pathContext.trustedCwd &&
|
|
636
|
+
pathContext.cwd &&
|
|
637
|
+
(effect === 'read_only' || effect === 'local_mutation') &&
|
|
638
|
+
pathAnalysis.location === 'unknown'
|
|
639
|
+
? 'repo_local'
|
|
640
|
+
: pathAnalysis.location;
|
|
571
641
|
if (!context.trustedCwd || !context.cwd) {
|
|
572
642
|
if (opacity === 'opaque' || effect === 'unknown' || effect === 'local_mutation') {
|
|
573
643
|
return askVerdict({
|
|
@@ -579,7 +649,7 @@ async function evaluateSegment(command, context, depth) {
|
|
|
579
649
|
signals: ['missing_trusted_cwd'],
|
|
580
650
|
});
|
|
581
651
|
}
|
|
582
|
-
if (effect === 'read_only' && !isPureReadOnlySegment(segment)) {
|
|
652
|
+
if (effect === 'read_only' && !isPureReadOnlySegment(segment, semantics)) {
|
|
583
653
|
return askVerdict({
|
|
584
654
|
location: 'unknown',
|
|
585
655
|
opacity,
|
|
@@ -594,9 +664,9 @@ async function evaluateSegment(command, context, depth) {
|
|
|
594
664
|
context.protectedArtifactRoots &&
|
|
595
665
|
context.protectedArtifactRoots.length > 0) {
|
|
596
666
|
for (const target of pathArgs) {
|
|
597
|
-
const resolved = resolveTrustedPath(target,
|
|
598
|
-
resolveMutationTarget(target,
|
|
599
|
-
if (resolved && touchesProtectedRoot(resolved,
|
|
667
|
+
const resolved = resolveTrustedPath(target, pathContext.cwd, pathContext.trustedCwd) ??
|
|
668
|
+
resolveMutationTarget(target, pathContext.cwd);
|
|
669
|
+
if (resolved && touchesProtectedRoot(resolved, pathContext.protectedArtifactRoots ?? [])) {
|
|
600
670
|
return askVerdict({
|
|
601
671
|
location: pathAnalysis.location,
|
|
602
672
|
opacity: 'transparent',
|
|
@@ -611,10 +681,10 @@ async function evaluateSegment(command, context, depth) {
|
|
|
611
681
|
if (effect === 'local_mutation' || effect === 'unknown') {
|
|
612
682
|
let destructiveHighStakes = false;
|
|
613
683
|
for (const target of pathArgs) {
|
|
614
|
-
const resolved = resolveTrustedPath(target,
|
|
615
|
-
resolveMutationTarget(target,
|
|
684
|
+
const resolved = resolveTrustedPath(target, pathContext.cwd, pathContext.trustedCwd) ??
|
|
685
|
+
resolveMutationTarget(target, pathContext.cwd);
|
|
616
686
|
if (resolved &&
|
|
617
|
-
isDestructiveHighStakesMutation(segment.head, resolved,
|
|
687
|
+
isDestructiveHighStakesMutation(segment.head, resolved, pathContext.repoRoot, pathContext.sensitivePaths, pathContext.protectedArtifactRoots, pathContext.trustedWorkspaceRoots)) {
|
|
618
688
|
destructiveHighStakes = true;
|
|
619
689
|
break;
|
|
620
690
|
}
|
|
@@ -663,18 +733,43 @@ async function evaluateSegment(command, context, depth) {
|
|
|
663
733
|
signals: ['unknown_location_mutation'],
|
|
664
734
|
});
|
|
665
735
|
}
|
|
736
|
+
const resolvedPathTargets = resolveShellPathTargets(policyPathArgs, pathContext);
|
|
737
|
+
const semanticSignals = semantics.signals;
|
|
738
|
+
const buildPolicyInput = (overrides) => ({
|
|
739
|
+
command: recursiveScript ?? command,
|
|
740
|
+
segmentHead: segment.head,
|
|
741
|
+
opacity,
|
|
742
|
+
egressClass: egressClass ?? undefined,
|
|
743
|
+
pathArgs: policyPathArgs,
|
|
744
|
+
resolvedPathTargets,
|
|
745
|
+
signals: [...semanticSignals, ...pathAnalysis.signals],
|
|
746
|
+
context: pathContext,
|
|
747
|
+
...overrides,
|
|
748
|
+
});
|
|
666
749
|
const outsideMutation = pathAnalysis.location === 'repo_outside' || pathAnalysis.location === 'mixed';
|
|
667
750
|
const mutationPrescan = (effect === 'local_mutation' || effect === 'unknown') && pathArgs.length > 0
|
|
668
751
|
? mutationPrescanRequiresAsk({
|
|
669
752
|
targets: pathArgs,
|
|
670
|
-
cwd:
|
|
671
|
-
repoRoot:
|
|
672
|
-
trustedCwd:
|
|
673
|
-
trustedWorkspaceRoots:
|
|
674
|
-
sensitivePaths:
|
|
753
|
+
cwd: pathContext.cwd,
|
|
754
|
+
repoRoot: pathContext.repoRoot,
|
|
755
|
+
trustedCwd: pathContext.trustedCwd,
|
|
756
|
+
trustedWorkspaceRoots: pathContext.trustedWorkspaceRoots,
|
|
757
|
+
sensitivePaths: pathContext.sensitivePaths,
|
|
675
758
|
})
|
|
676
759
|
: null;
|
|
677
760
|
if (mutationPrescan) {
|
|
761
|
+
const { requests, decision } = evaluateSegmentShellPolicy(buildPolicyInput({
|
|
762
|
+
effect: 'local_mutation',
|
|
763
|
+
location: pathAnalysis.location === 'unknown'
|
|
764
|
+
? 'unknown'
|
|
765
|
+
: pathAnalysis.location === 'repo_outside' || pathAnalysis.location === 'mixed'
|
|
766
|
+
? pathAnalysis.location
|
|
767
|
+
: 'repo_local',
|
|
768
|
+
signals: ['tier1_catastrophic', mutationPrescan.reason, ...pathAnalysis.signals],
|
|
769
|
+
}));
|
|
770
|
+
const legacyReason = policyDecisionRequiresAsk(decision)
|
|
771
|
+
? policyDecisionToLegacyReason(decision, { outsideMutation, effect: 'local_mutation' })
|
|
772
|
+
: 'tier1_catastrophic';
|
|
678
773
|
return askVerdict({
|
|
679
774
|
location: pathAnalysis.location === 'unknown'
|
|
680
775
|
? 'unknown'
|
|
@@ -684,24 +779,28 @@ async function evaluateSegment(command, context, depth) {
|
|
|
684
779
|
opacity,
|
|
685
780
|
effect: 'local_mutation',
|
|
686
781
|
confidence: 'deterministic',
|
|
687
|
-
reason:
|
|
688
|
-
signals: ['tier1_catastrophic', mutationPrescan.reason],
|
|
782
|
+
reason: legacyReason,
|
|
783
|
+
signals: ['tier1_catastrophic', mutationPrescan.reason, ...decision.signals],
|
|
784
|
+
capabilityRequests: requests,
|
|
785
|
+
authorizationDecision: decision,
|
|
689
786
|
});
|
|
690
787
|
}
|
|
691
|
-
const
|
|
788
|
+
const needsPolicy = effect === 'unknown' ||
|
|
692
789
|
TIER0_EXTERNAL_HEADS.has(segment.head) ||
|
|
693
790
|
egressClass === 'ambiguous' ||
|
|
791
|
+
egressClass === 'read' ||
|
|
694
792
|
(outsideMutation && effect !== 'read_only');
|
|
695
|
-
let
|
|
696
|
-
if (
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
793
|
+
let policyMetadata = {};
|
|
794
|
+
if (needsPolicy) {
|
|
795
|
+
const { requests, decision } = evaluateSegmentShellPolicy(buildPolicyInput({
|
|
796
|
+
effect,
|
|
797
|
+
location: pathAnalysis.location,
|
|
798
|
+
}));
|
|
799
|
+
policyMetadata = {
|
|
800
|
+
capabilityRequests: requests,
|
|
801
|
+
authorizationDecision: decision,
|
|
802
|
+
};
|
|
803
|
+
if (policyDecisionRequiresAsk(decision)) {
|
|
705
804
|
return askVerdict({
|
|
706
805
|
location: pathAnalysis.location === 'unknown'
|
|
707
806
|
? 'unknown'
|
|
@@ -709,72 +808,104 @@ async function evaluateSegment(command, context, depth) {
|
|
|
709
808
|
? pathAnalysis.location
|
|
710
809
|
: 'repo_local',
|
|
711
810
|
opacity,
|
|
712
|
-
effect:
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
811
|
+
effect: decision.reason === 'external_effect' || decision.reason === 'network_connect'
|
|
812
|
+
? 'remote_mutation'
|
|
813
|
+
: effect,
|
|
814
|
+
confidence: 'deterministic',
|
|
815
|
+
reason: policyDecisionToLegacyReason(decision, { outsideMutation, effect }),
|
|
816
|
+
signals: ['policy_required', ...decision.signals],
|
|
817
|
+
...policyMetadata,
|
|
717
818
|
});
|
|
718
819
|
}
|
|
719
820
|
}
|
|
720
821
|
if (outsideMutation && effect !== 'read_only') {
|
|
721
|
-
|
|
822
|
+
const policyAllowsOutside = policyMetadata.authorizationDecision?.outcome === 'allow' &&
|
|
823
|
+
(policyMetadata.authorizationDecision.matchedRule === 'grant.exact' ||
|
|
824
|
+
policyMetadata.authorizationDecision.matchedRule === 'boundary.verified' ||
|
|
825
|
+
policyMetadata.authorizationDecision.matchedRule === 'builtin.trusted_workspace');
|
|
826
|
+
if (!policyAllowsOutside) {
|
|
827
|
+
const legacyReason = policyMetadata.authorizationDecision &&
|
|
828
|
+
policyDecisionRequiresAsk(policyMetadata.authorizationDecision)
|
|
829
|
+
? policyDecisionToLegacyReason(policyMetadata.authorizationDecision, {
|
|
830
|
+
outsideMutation,
|
|
831
|
+
effect,
|
|
832
|
+
})
|
|
833
|
+
: 'outside_repo_mutation';
|
|
834
|
+
return askVerdict({
|
|
835
|
+
location: pathAnalysis.location,
|
|
836
|
+
opacity,
|
|
837
|
+
effect: 'local_mutation',
|
|
838
|
+
confidence: 'deterministic',
|
|
839
|
+
reason: legacyReason,
|
|
840
|
+
signals: ['outside_repo_mutation', ...pathAnalysis.signals],
|
|
841
|
+
...policyMetadata,
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
return allowVerdict({
|
|
722
845
|
location: pathAnalysis.location,
|
|
723
846
|
opacity,
|
|
724
847
|
effect: 'local_mutation',
|
|
725
|
-
confidence:
|
|
848
|
+
confidence: 'deterministic',
|
|
726
849
|
reason: 'repo_outside_local_mutation',
|
|
727
850
|
signals: ['repo_outside_local_mutation', ...pathAnalysis.signals],
|
|
728
|
-
|
|
851
|
+
...policyMetadata,
|
|
852
|
+
});
|
|
729
853
|
}
|
|
730
|
-
if (
|
|
854
|
+
if (effectiveLocation === 'repo_local' &&
|
|
731
855
|
(effect === 'read_only' || effect === 'local_mutation') &&
|
|
732
856
|
opacity !== 'opaque') {
|
|
733
|
-
return
|
|
857
|
+
return allowVerdict({
|
|
734
858
|
location: 'repo_local',
|
|
735
859
|
opacity,
|
|
736
860
|
effect,
|
|
737
861
|
confidence: 'assumed_repo_local',
|
|
738
862
|
reason: effect === 'read_only' ? 'read_only' : 'repo_local_mutation',
|
|
739
863
|
signals: effect === 'read_only' ? ['read_only'] : ['repo_local_mutation'],
|
|
740
|
-
|
|
864
|
+
...withShellPolicyMetadata(buildPolicyInput({ effect, location: 'repo_local' }), policyMetadata),
|
|
865
|
+
});
|
|
741
866
|
}
|
|
742
867
|
if (effect === 'read_only') {
|
|
743
|
-
const readOnlyLocation =
|
|
744
|
-
?
|
|
868
|
+
const readOnlyLocation = pathContext.trustedCwd && pathContext.cwd
|
|
869
|
+
? effectiveLocation === 'unknown'
|
|
745
870
|
? 'repo_local'
|
|
746
|
-
:
|
|
871
|
+
: effectiveLocation
|
|
747
872
|
: 'unknown';
|
|
748
|
-
return
|
|
873
|
+
return allowVerdict({
|
|
749
874
|
location: readOnlyLocation,
|
|
750
875
|
opacity,
|
|
751
876
|
effect: 'read_only',
|
|
752
877
|
confidence: context.trustedCwd && context.cwd ? 'assumed_repo_local' : 'deterministic',
|
|
753
878
|
reason: 'read_only',
|
|
754
879
|
signals: ['read_only'],
|
|
755
|
-
|
|
880
|
+
...withShellPolicyMetadata(buildPolicyInput({ effect: 'read_only', location: readOnlyLocation }), policyMetadata),
|
|
881
|
+
});
|
|
756
882
|
}
|
|
757
883
|
if (allowOverride) {
|
|
758
|
-
return
|
|
884
|
+
return allowFromCustomOverride(opacity);
|
|
759
885
|
}
|
|
760
886
|
if (context.unknownLocalEffect === 'allow_flagged') {
|
|
761
|
-
return
|
|
762
|
-
location:
|
|
887
|
+
return allowVerdict({
|
|
888
|
+
location: effectiveLocation === 'unknown' ? 'repo_local' : effectiveLocation,
|
|
763
889
|
opacity,
|
|
764
890
|
effect: 'unknown',
|
|
765
891
|
confidence: 'assumed_repo_local',
|
|
766
892
|
reason: 'unknown_local_effect',
|
|
767
893
|
signals: ['unknown_local_effect'],
|
|
768
|
-
|
|
894
|
+
...withShellPolicyMetadata(buildPolicyInput({
|
|
895
|
+
effect: 'unknown',
|
|
896
|
+
location: effectiveLocation === 'unknown' ? 'repo_local' : effectiveLocation,
|
|
897
|
+
}), policyMetadata),
|
|
898
|
+
});
|
|
769
899
|
}
|
|
770
|
-
return
|
|
771
|
-
location:
|
|
900
|
+
return askVerdict({
|
|
901
|
+
location: effectiveLocation,
|
|
772
902
|
opacity,
|
|
773
903
|
effect,
|
|
774
904
|
confidence: 'deterministic',
|
|
775
905
|
reason: 'unknown_local_effect',
|
|
776
906
|
signals: ['unknown_local_effect'],
|
|
777
|
-
|
|
907
|
+
...withShellPolicyMetadata(buildPolicyInput({ effect, location: pathAnalysis.location }), policyMetadata),
|
|
908
|
+
});
|
|
778
909
|
}
|
|
779
910
|
function toVerdictResult(internal, command, context, fingerprintCwd = context.cwd) {
|
|
780
911
|
const commandRedacted = redactCommand(command);
|
|
@@ -790,6 +921,10 @@ function toVerdictResult(internal, command, context, fingerprintCwd = context.cw
|
|
|
790
921
|
fingerprint: verdictFingerprint(relative, commandRedacted),
|
|
791
922
|
signals: internal.signals,
|
|
792
923
|
judgeTrace: internal.judgeTrace,
|
|
924
|
+
capabilityRequests: internal.capabilityRequests,
|
|
925
|
+
authorizationDecision: internal.authorizationDecision,
|
|
926
|
+
effectPlan: internal.effectPlan,
|
|
927
|
+
effectPlanPolicyDecisions: internal.effectPlanPolicyDecisions,
|
|
793
928
|
};
|
|
794
929
|
}
|
|
795
930
|
export async function verdict(command, context) {
|