@guilz-dev/belay 0.7.0 → 0.8.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 +19 -19
- package/dist/adapters/shared/gate-runtime.js +23 -5
- package/dist/bundle/claude-runtime.mjs +4909 -3504
- package/dist/bundle/codex-runtime.mjs +5050 -3645
- package/dist/bundle/cursor-runtime.mjs +5050 -3645
- package/dist/commands/doctor.js +7 -0
- package/dist/commands/dogfood.js +1 -1
- package/dist/commands/explain.js +2 -2
- package/dist/commands/harvest.d.ts +0 -1
- package/dist/commands/harvest.js +1 -4
- package/dist/commands/metrics.js +6 -0
- package/dist/commands/quality.js +1 -3
- package/dist/conformance/guarantee-table.js +133 -11
- package/dist/conformance/types.d.ts +1 -0
- package/dist/core/audit-metrics.d.ts +1 -0
- package/dist/core/audit-metrics.js +12 -4
- package/dist/core/audit-types.d.ts +3 -0
- package/dist/core/capability/index.d.ts +1 -1
- package/dist/core/capability/policy-engine.d.ts +31 -1
- package/dist/core/capability/policy-engine.js +186 -0
- package/dist/core/capability/request.d.ts +9 -0
- package/dist/core/capability/resolver.d.ts +0 -1
- package/dist/core/capability/resolver.js +0 -1
- package/dist/core/config.js +0 -2
- package/dist/core/effect-ir/audit.js +1 -0
- package/dist/core/effect-ir/build.js +5 -1
- package/dist/core/effect-ir/index.d.ts +6 -1
- package/dist/core/effect-ir/index.js +2 -0
- package/dist/core/effect-ir/normalize.js +9 -3
- package/dist/core/effect-ir/policy.d.ts +2 -1
- package/dist/core/effect-ir/policy.js +93 -6
- package/dist/core/effect-ir/shell-build.d.ts +43 -0
- package/dist/core/effect-ir/shell-build.js +77 -0
- package/dist/core/effect-ir/shell-lower.d.ts +14 -0
- package/dist/core/effect-ir/shell-lower.js +1509 -0
- package/dist/core/effect-ir/types.d.ts +9 -2
- package/dist/core/gate-engine.js +63 -23
- package/dist/core/git-resource-identity.d.ts +26 -0
- package/dist/core/git-resource-identity.js +284 -0
- package/dist/core/harvest.d.ts +3 -7
- package/dist/core/harvest.js +4 -27
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +1 -0
- package/dist/core/path-utils.js +42 -23
- package/dist/core/shell-substitution.d.ts +1 -0
- package/dist/core/shell-substitution.js +61 -0
- package/dist/core/standing-allow.d.ts +1 -2
- package/dist/core/standing-allow.js +3 -20
- package/dist/core/types.d.ts +4 -1
- package/dist/core/verdict/adapter.js +15 -15
- package/dist/core/verdict/containment.js +4 -0
- package/dist/core/verdict/egress-classify.d.ts +12 -0
- package/dist/core/verdict/egress-classify.js +746 -0
- package/dist/core/verdict/git-classifier.d.ts +12 -0
- package/dist/core/verdict/git-classifier.js +320 -0
- package/dist/core/verdict/launcher-resolve.js +77 -17
- package/dist/core/verdict/parser.d.ts +2 -0
- package/dist/core/verdict/parser.js +95 -1
- package/dist/core/verdict/types.d.ts +2 -3
- package/dist/core/verdict/verdict.js +103 -952
- package/dist/corpus/must-allow-commands.d.ts +2 -1
- package/dist/corpus/must-allow-commands.js +2 -1
- package/dist/corpus/runtime-match.d.ts +2 -1
- package/dist/corpus/runtime-match.js +2 -1
- package/dist/corpus/types.d.ts +2 -2
- package/dist/templates.js +7 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/dist/core/verdict/overrides.d.ts +0 -7
- package/dist/core/verdict/overrides.js +0 -37
- package/dist/core/verdict/shell-policy.d.ts +0 -26
- package/dist/core/verdict/shell-policy.js +0 -40
- package/dist/corpus/standing-allow-catalog.generated.d.ts +0 -13
- package/dist/corpus/standing-allow-catalog.generated.js +0 -99
|
@@ -68,6 +68,67 @@ export function findCommandSubstitutions(command) {
|
|
|
68
68
|
}
|
|
69
69
|
return results;
|
|
70
70
|
}
|
|
71
|
+
export function findStructuralCommandSubstitutions(command) {
|
|
72
|
+
const results = [];
|
|
73
|
+
let index = 0;
|
|
74
|
+
let inSingle = false;
|
|
75
|
+
let inDouble = false;
|
|
76
|
+
let escaping = false;
|
|
77
|
+
while (index < command.length) {
|
|
78
|
+
const char = command[index];
|
|
79
|
+
if (escaping) {
|
|
80
|
+
escaping = false;
|
|
81
|
+
index += 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (char === '\\' && !inSingle) {
|
|
85
|
+
escaping = true;
|
|
86
|
+
index += 1;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!inDouble && char === "'") {
|
|
90
|
+
inSingle = !inSingle;
|
|
91
|
+
index += 1;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (!inSingle && char === '"') {
|
|
95
|
+
inDouble = !inDouble;
|
|
96
|
+
index += 1;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (inSingle) {
|
|
100
|
+
index += 1;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (char === '`') {
|
|
104
|
+
const end = findClosingBacktick(command, index + 1);
|
|
105
|
+
if (end === -1) {
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
const inner = command.slice(index + 1, end).trim();
|
|
109
|
+
if (inner) {
|
|
110
|
+
results.push(inner);
|
|
111
|
+
}
|
|
112
|
+
index = end + 1;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (char === '$' && command[index + 1] === '(') {
|
|
116
|
+
const closed = extractBalancedParenContent(command, index + 2);
|
|
117
|
+
if (!closed) {
|
|
118
|
+
index += 1;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const inner = closed.content.trim();
|
|
122
|
+
if (inner) {
|
|
123
|
+
results.push(inner);
|
|
124
|
+
}
|
|
125
|
+
index = closed.endIndex;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
index += 1;
|
|
129
|
+
}
|
|
130
|
+
return results;
|
|
131
|
+
}
|
|
71
132
|
function findClosingBacktick(command, start) {
|
|
72
133
|
let index = start;
|
|
73
134
|
while (index < command.length) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { BelayConfigV4 } from './config.js';
|
|
2
2
|
import type { GatedActionKind } from './gate-contract.js';
|
|
3
3
|
import type { ClassifyResult } from './types.js';
|
|
4
|
-
export type StandingAllowSource = '
|
|
4
|
+
export type StandingAllowSource = 'operator' | 'availability-reconfirmed';
|
|
5
5
|
export interface StandingAllowEntry {
|
|
6
6
|
kind: GatedActionKind;
|
|
7
7
|
fingerprint: string;
|
|
@@ -18,7 +18,6 @@ export interface StandingAllowFile {
|
|
|
18
18
|
}
|
|
19
19
|
export interface StandingAllowMatch {
|
|
20
20
|
source: StandingAllowSource;
|
|
21
|
-
catalogCommand?: string;
|
|
22
21
|
entryId?: string;
|
|
23
22
|
}
|
|
24
23
|
/** Default TTL for operator / availability-reconfirmed standing-allow entries. */
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { STANDING_ALLOW_CATALOG } from '../corpus/standing-allow-catalog.generated.js';
|
|
5
4
|
import { belayStateDir } from './config.js';
|
|
6
5
|
const EMPTY_STANDING_ALLOW = {
|
|
7
6
|
version: 1,
|
|
@@ -9,8 +8,6 @@ const EMPTY_STANDING_ALLOW = {
|
|
|
9
8
|
};
|
|
10
9
|
/** Default TTL for operator / availability-reconfirmed standing-allow entries. */
|
|
11
10
|
export const DEFAULT_STANDING_ALLOW_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
12
|
-
const PROVABLY_BENIGN_COMMANDS = new Set(STANDING_ALLOW_CATALOG.shell.provablyBenign.map((entry) => entry.normalizedCommand));
|
|
13
|
-
const MUST_ALLOW_COMMANDS = new Set(STANDING_ALLOW_CATALOG.shell.mustAllow.map((entry) => entry.normalizedCommand));
|
|
14
11
|
/** Reasons that must never be silenced via standing-allow (defense in depth with signal checks). */
|
|
15
12
|
const STANDING_ALLOW_BLOCKED_REASONS = new Set([
|
|
16
13
|
'external_effect',
|
|
@@ -95,18 +92,6 @@ export async function saveStandingAllow(filePath, state) {
|
|
|
95
92
|
const compacted = compactStandingAllow(state);
|
|
96
93
|
await writeFile(filePath, `${JSON.stringify(compacted, null, 2)}\n`, 'utf8');
|
|
97
94
|
}
|
|
98
|
-
function matchBundledCatalog(kind, normalizedCommand) {
|
|
99
|
-
if (kind !== 'shell' || !normalizedCommand) {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
if (PROVABLY_BENIGN_COMMANDS.has(normalizedCommand)) {
|
|
103
|
-
return { source: 'provably-benign-corpus', catalogCommand: normalizedCommand };
|
|
104
|
-
}
|
|
105
|
-
if (MUST_ALLOW_COMMANDS.has(normalizedCommand)) {
|
|
106
|
-
return { source: 'must-allow-catalog', catalogCommand: normalizedCommand };
|
|
107
|
-
}
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
95
|
function matchStateEntry(params) {
|
|
111
96
|
const now = params.now ?? Date.now();
|
|
112
97
|
const match = params.state.entries.find((entry) => entry.kind === params.kind &&
|
|
@@ -119,17 +104,15 @@ function matchStateEntry(params) {
|
|
|
119
104
|
return { source: match.source, entryId: match.fingerprint };
|
|
120
105
|
}
|
|
121
106
|
export function resolveStandingAllowMatch(params) {
|
|
107
|
+
if (params.kind === 'shell') {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
122
110
|
if (params.result.verdict !== 'deny_pending_approval') {
|
|
123
111
|
return null;
|
|
124
112
|
}
|
|
125
113
|
if (isTier0StandingAllowBlocked(params.result)) {
|
|
126
114
|
return null;
|
|
127
115
|
}
|
|
128
|
-
const normalizedCommand = params.result.normalizedCommand ?? params.result.summary ?? '';
|
|
129
|
-
const catalogMatch = matchBundledCatalog(params.kind, normalizedCommand);
|
|
130
|
-
if (catalogMatch) {
|
|
131
|
-
return catalogMatch;
|
|
132
|
-
}
|
|
133
116
|
return matchStateEntry({
|
|
134
117
|
kind: params.kind,
|
|
135
118
|
fingerprint: params.result.fingerprint,
|
package/dist/core/types.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { CapabilityGrantV1 } from './capability/grant.js';
|
|
|
3
3
|
import type { PolicyDecision } from './capability/policy-types.js';
|
|
4
4
|
import type { CapabilityRequestV1 } from './capability/request.js';
|
|
5
5
|
import type { FsScopeAllowlistFile } from './capability/types.js';
|
|
6
|
-
import type { EffectPlan } from './effect-ir/types.js';
|
|
6
|
+
import type { EffectPlan, EffectPlanPolicyProjection } from './effect-ir/types.js';
|
|
7
7
|
export type BelayMode = 'enforce' | 'audit';
|
|
8
8
|
export type HookVerdict = 'allow' | 'allow_flagged' | 'deny_pending_approval';
|
|
9
9
|
export type Reversibility = 'reversible' | 'recoverable_with_cost' | 'irreversible';
|
|
@@ -43,6 +43,7 @@ export interface ClassifyResult {
|
|
|
43
43
|
authorizationDecision?: PolicyDecision;
|
|
44
44
|
effectPlan?: EffectPlan;
|
|
45
45
|
effectPlanPolicyDecisions?: PolicyDecision[];
|
|
46
|
+
effectPlanProjection?: EffectPlanPolicyProjection;
|
|
46
47
|
boundaryProfile?: string;
|
|
47
48
|
}
|
|
48
49
|
export type UnknownLocalEffectPolicy = 'allow_flagged' | 'deny';
|
|
@@ -61,7 +62,9 @@ export interface ConfidenceThresholds {
|
|
|
61
62
|
}
|
|
62
63
|
export interface ClassifierOptions {
|
|
63
64
|
strictChains?: boolean;
|
|
65
|
+
/** @deprecated Accepted for API compatibility but ignored by shell EffectPlan authorization. */
|
|
64
66
|
customExternalCommands?: string[];
|
|
67
|
+
/** @deprecated Accepted for API compatibility but ignored by shell EffectPlan authorization. */
|
|
65
68
|
customAllowCommands?: string[];
|
|
66
69
|
sensitivePaths?: string[];
|
|
67
70
|
unknownLocalEffect?: UnknownLocalEffectPolicy;
|
|
@@ -22,8 +22,6 @@ export function buildVerdictContext(params) {
|
|
|
22
22
|
trustedWorkspaceRoots: params.options?.trustedWorkspaceRoots ?? [],
|
|
23
23
|
sensitivePaths: params.options?.sensitivePaths ?? params.config.classifier.sensitivePaths,
|
|
24
24
|
protectedArtifactRoots: protectedArtifactRoots.length > 0 ? [...new Set(protectedArtifactRoots)] : undefined,
|
|
25
|
-
customAllowCommands: params.options?.customAllowCommands ?? params.config.overrides.allow,
|
|
26
|
-
customExternalCommands: params.options?.customExternalCommands ?? params.config.overrides.external,
|
|
27
25
|
mode: params.config.mode,
|
|
28
26
|
unknownLocalEffect: params.options?.unknownLocalEffect ?? params.config.policy.unknownLocalEffect,
|
|
29
27
|
unparseableShell: params.options?.unparseableShell ?? params.config.policy.unparseableShell,
|
|
@@ -75,14 +73,15 @@ export function verdictToClassifyResult(result, config, options = {}) {
|
|
|
75
73
|
result.location === 'repo_outside' ||
|
|
76
74
|
result.effect === 'remote_mutation';
|
|
77
75
|
const legacyReason = mapLegacyReason(result);
|
|
78
|
-
const hookVerdict = result.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
legacyReason === '
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
76
|
+
const hookVerdict = result.effectPlanProjection?.hookVerdict ??
|
|
77
|
+
(result.permission === 'ask'
|
|
78
|
+
? 'deny_pending_approval'
|
|
79
|
+
: legacyReason === 'command_substitution' ||
|
|
80
|
+
legacyReason === 'unknown_local_effect' ||
|
|
81
|
+
legacyReason === 'unparseable_shell' ||
|
|
82
|
+
result.effect === 'local_mutation'
|
|
83
|
+
? 'allow_flagged'
|
|
84
|
+
: 'allow');
|
|
86
85
|
const assessment = {
|
|
87
86
|
reversibility: result.effect === 'read_only'
|
|
88
87
|
? 'reversible'
|
|
@@ -91,11 +90,11 @@ export function verdictToClassifyResult(result, config, options = {}) {
|
|
|
91
90
|
: 'irreversible',
|
|
92
91
|
external,
|
|
93
92
|
blastRadius: result.location,
|
|
94
|
-
confidence:
|
|
95
|
-
? 0.
|
|
96
|
-
: result.confidence === '
|
|
97
|
-
? 0.
|
|
98
|
-
:
|
|
93
|
+
confidence: hookVerdict === 'allow_flagged'
|
|
94
|
+
? 0.75
|
|
95
|
+
: result.confidence === 'deterministic'
|
|
96
|
+
? 0.95
|
|
97
|
+
: result.confidence === 'llm'
|
|
99
98
|
? 0.75
|
|
100
99
|
: 0.7,
|
|
101
100
|
signals: result.signals,
|
|
@@ -123,6 +122,7 @@ export function verdictToClassifyResult(result, config, options = {}) {
|
|
|
123
122
|
authorizationDecision: result.authorizationDecision,
|
|
124
123
|
effectPlan: result.effectPlan,
|
|
125
124
|
effectPlanPolicyDecisions: result.effectPlanPolicyDecisions,
|
|
125
|
+
effectPlanProjection: result.effectPlanProjection,
|
|
126
126
|
boundaryProfile: options.boundaryProfile ??
|
|
127
127
|
(config
|
|
128
128
|
? resolveBoundaryProfile({ config, attestation: options.attestation })
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { isGitMetadataPath } from '../git-resource-identity.js';
|
|
2
3
|
import { matchesSensitivePath } from '../glob.js';
|
|
3
4
|
import { canonicalPath, pathWithinRoot, relativeWithinRepo, resolveMutationTarget, resolveWorkspaceRootMatch, } from '../path-utils.js';
|
|
4
5
|
import { isOutsideRepoSecretCredentialPath } from './persistent-paths.js';
|
|
@@ -36,6 +37,9 @@ export function locationForPath(resolvedPath, repoRoot, trustedWorkspaceRoots =
|
|
|
36
37
|
return 'repo_outside';
|
|
37
38
|
}
|
|
38
39
|
export function isGitPath(resolvedPath, repoRoot) {
|
|
40
|
+
if (isGitMetadataPath(resolvedPath, repoRoot)) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
39
43
|
const relative = relativeWithinRepo(repoRoot, resolvedPath);
|
|
40
44
|
if (!relative) {
|
|
41
45
|
return false;
|
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
* Egress tool classification (SPEC v2.1.3 R33–R34).
|
|
3
3
|
* destructive → ask | read → allow at egress layer | ambiguous → require_approval via PolicyEngine
|
|
4
4
|
*/
|
|
5
|
+
import type { ShellEffectRequirement } from '../effect-ir/shell-build.js';
|
|
5
6
|
export type EgressClassification = 'destructive' | 'read' | 'ambiguous';
|
|
7
|
+
export interface DecodeEgressEffectsParams {
|
|
8
|
+
tokens: string[];
|
|
9
|
+
cwd: string;
|
|
10
|
+
segment: string;
|
|
11
|
+
}
|
|
6
12
|
export declare function isEgressToolHead(head: string): boolean;
|
|
7
13
|
export declare function classifyEgressTool(head: string, tokens: string[]): EgressClassification | null;
|
|
14
|
+
/**
|
|
15
|
+
* Decode curl/wget/gh grammar into typed effects. This API deliberately has no
|
|
16
|
+
* permission or verdict output; uncertainty is represented by indeterminate
|
|
17
|
+
* requirements alongside every effect that was still recoverable.
|
|
18
|
+
*/
|
|
19
|
+
export declare function decodeEgressEffects(params: DecodeEgressEffectsParams): ShellEffectRequirement[] | null;
|