@guilz-dev/belay 0.6.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 +39 -20
- package/dist/adapters/claude/runtime-entry.js +54 -25
- package/dist/adapters/shared/gate-runtime.d.ts +7 -0
- package/dist/adapters/shared/gate-runtime.js +443 -225
- package/dist/bundle/claude-runtime.mjs +13027 -7271
- package/dist/bundle/codex-runtime.mjs +12700 -6977
- package/dist/bundle/cursor-runtime.mjs +12695 -6977
- package/dist/cli.js +37 -1
- package/dist/commands/approve.js +34 -10
- package/dist/commands/doctor.js +58 -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/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/status.js +7 -0
- package/dist/config-io.js +49 -18
- package/dist/conformance/guarantee-table.js +137 -9
- package/dist/conformance/types.d.ts +2 -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 +86 -27
- package/dist/core/approval.d.ts +2 -0
- package/dist/core/approval.js +6 -3
- package/dist/core/audit-metrics.d.ts +1 -0
- package/dist/core/audit-metrics.js +12 -4
- package/dist/core/audit-types.d.ts +16 -0
- package/dist/core/capability/approval-state-mutation.d.ts +19 -0
- package/dist/core/capability/approval-state-mutation.js +76 -3
- package/dist/core/capability/approval-v3.d.ts +18 -1
- package/dist/core/capability/approval-v3.js +63 -23
- package/dist/core/capability/attestation.d.ts +3 -0
- package/dist/core/capability/attestation.js +7 -0
- package/dist/core/capability/boundary-driver-container.d.ts +12 -0
- package/dist/core/capability/boundary-driver-container.js +101 -34
- package/dist/core/capability/boundary-driver.js +1 -0
- package/dist/core/capability/boundary-grant-materialize.d.ts +1 -0
- package/dist/core/capability/boundary-grant-materialize.js +15 -0
- package/dist/core/capability/boundary-run.d.ts +25 -0
- package/dist/core/capability/boundary-run.js +37 -9
- package/dist/core/capability/boundary-session.d.ts +4 -0
- package/dist/core/capability/boundary-session.js +9 -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/gate-policy-shadow.js +23 -1
- 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 +36 -1
- package/dist/core/capability/grant-lease.js +213 -16
- package/dist/core/capability/grant-match.js +3 -0
- package/dist/core/capability/index.d.ts +2 -2
- package/dist/core/capability/index.js +1 -1
- package/dist/core/capability/policy-engine.d.ts +38 -1
- package/dist/core/capability/policy-engine.js +388 -15
- package/dist/core/capability/request.d.ts +12 -0
- package/dist/core/capability/resolver.d.ts +0 -1
- package/dist/core/capability/resolver.js +0 -1
- package/dist/core/config.d.ts +20 -0
- package/dist/core/config.js +62 -2
- package/dist/core/effect-ir/audit.d.ts +9 -0
- package/dist/core/effect-ir/audit.js +87 -0
- package/dist/core/effect-ir/build.d.ts +31 -0
- package/dist/core/effect-ir/build.js +281 -0
- package/dist/core/effect-ir/index.d.ts +13 -0
- package/dist/core/effect-ir/index.js +9 -0
- package/dist/core/effect-ir/normalize.d.ts +4 -0
- package/dist/core/effect-ir/normalize.js +110 -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 +14 -0
- package/dist/core/effect-ir/policy.js +164 -0
- 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 +62 -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 +2 -0
- package/dist/core/gate-contract.js +2 -0
- package/dist/core/gate-engine.js +82 -25
- 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 +5 -1
- package/dist/core/index.js +3 -1
- package/dist/core/judge-cloud-consent.js +29 -15
- package/dist/core/network-endpoint.d.ts +10 -0
- package/dist/core/network-endpoint.js +51 -0
- package/dist/core/path-utils.js +42 -23
- 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/checkpoint.d.ts +25 -0
- package/dist/core/recovery/checkpoint.js +260 -0
- package/dist/core/recovery/index.d.ts +3 -1
- package/dist/core/recovery/index.js +2 -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 +85 -1
- 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/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/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 +9 -11
- package/dist/core/transactional/git-worktree.js +24 -119
- package/dist/core/transactional/index.d.ts +11 -0
- package/dist/core/transactional/index.js +10 -0
- package/dist/core/transactional/reasons.js +1 -0
- package/dist/core/transactional/runner.js +157 -25
- 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 +17 -1
- package/dist/core/types.d.ts +13 -1
- package/dist/core/verdict/adapter.js +23 -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 +79 -37
- package/dist/core/verdict/parser.d.ts +2 -0
- package/dist/core/verdict/parser.js +95 -1
- package/dist/core/verdict/types.d.ts +6 -2
- package/dist/core/verdict/verdict.js +103 -935
- 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/types.d.ts +11 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/skills/belay/SKILL.md +8 -3
- package/skills/belay/belay-approve.md +10 -7
- 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 -25
- 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
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { CapabilityAction, CapabilityEvidenceLevel, CapabilityResource } from '../capability/request.js';
|
|
2
|
+
import type { VerdictOpacity } from '../verdict/types.js';
|
|
3
|
+
export declare const EFFECT_PLAN_VERSION: 1;
|
|
4
|
+
export type EffectTag = 'fs.read' | 'fs.write' | 'process.exec' | 'network.connect' | 'network.acquire' | 'git.ref.write' | 'secret.read' | 'control_plane.write' | 'read_only' | 'indeterminate';
|
|
5
|
+
export type LauncherPhase = 'invoke' | 'resolve' | 'acquire' | 'cache_read' | 'cache_write' | 'delegate';
|
|
6
|
+
export type PackageExecLauncher = 'npx' | 'npm' | 'pnpm';
|
|
7
|
+
export type EffectPlanDisposition = 'effects' | 'effect_free';
|
|
8
|
+
export type AnalysisCompleteness = 'complete' | 'partial';
|
|
9
|
+
export interface EffectPlanPolicyProjection {
|
|
10
|
+
permission: 'allow' | 'ask';
|
|
11
|
+
hookVerdict: 'allow' | 'allow_flagged' | 'deny_pending_approval';
|
|
12
|
+
reason: string;
|
|
13
|
+
}
|
|
14
|
+
export interface EffectEvidence {
|
|
15
|
+
level: CapabilityEvidenceLevel;
|
|
16
|
+
signals: readonly string[];
|
|
17
|
+
basis: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
export interface EffectProvenance {
|
|
20
|
+
segment?: string;
|
|
21
|
+
launcher?: PackageExecLauncher;
|
|
22
|
+
phase?: LauncherPhase;
|
|
23
|
+
innerCommand?: string;
|
|
24
|
+
innerArgv?: readonly string[];
|
|
25
|
+
}
|
|
26
|
+
export interface EffectRequirement {
|
|
27
|
+
tag: EffectTag;
|
|
28
|
+
action: CapabilityAction;
|
|
29
|
+
resource: CapabilityResource;
|
|
30
|
+
evidence: EffectEvidence;
|
|
31
|
+
provenance: EffectProvenance;
|
|
32
|
+
/** All contributing sources after requirement de-duplication. */
|
|
33
|
+
provenances?: readonly EffectProvenance[];
|
|
34
|
+
}
|
|
35
|
+
export interface ExecEffectNode {
|
|
36
|
+
kind: 'exec';
|
|
37
|
+
commandRedacted: string;
|
|
38
|
+
segmentHead: string;
|
|
39
|
+
requirements: readonly EffectRequirement[];
|
|
40
|
+
}
|
|
41
|
+
export interface LauncherEffectNode {
|
|
42
|
+
kind: 'launcher';
|
|
43
|
+
launcher: PackageExecLauncher | 'npm' | 'pnpm' | 'make';
|
|
44
|
+
phase: LauncherPhase;
|
|
45
|
+
opaque: boolean;
|
|
46
|
+
reason: string;
|
|
47
|
+
children: readonly EffectNode[];
|
|
48
|
+
}
|
|
49
|
+
export interface MergeEffectNode {
|
|
50
|
+
kind: 'merge';
|
|
51
|
+
children: readonly EffectNode[];
|
|
52
|
+
}
|
|
53
|
+
export type EffectNode = ExecEffectNode | LauncherEffectNode | MergeEffectNode;
|
|
54
|
+
export interface EffectPlan {
|
|
55
|
+
version: typeof EFFECT_PLAN_VERSION;
|
|
56
|
+
root: EffectNode;
|
|
57
|
+
inputFingerprint: string;
|
|
58
|
+
opacity: VerdictOpacity;
|
|
59
|
+
disposition: EffectPlanDisposition;
|
|
60
|
+
completeness: AnalysisCompleteness;
|
|
61
|
+
signals: readonly string[];
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const EFFECT_PLAN_VERSION = 1;
|
|
@@ -3,5 +3,6 @@ import type { EgressAllowlistEntry, EgressAllowlistFile } from './types.js';
|
|
|
3
3
|
export declare function egressAllowlistPath(config: BelayConfigV3, repoLocalStateDir: string): string;
|
|
4
4
|
export declare function loadEgressAllowlist(filePath: string): Promise<EgressAllowlistFile>;
|
|
5
5
|
export declare function saveEgressAllowlist(filePath: string, state: EgressAllowlistFile): Promise<void>;
|
|
6
|
+
export declare function mutateEgressAllowlist(filePath: string, mutate: (state: EgressAllowlistFile) => EgressAllowlistFile): Promise<EgressAllowlistFile>;
|
|
6
7
|
export declare function isHostAllowlisted(host: string, allowlist: EgressAllowlistFile): boolean;
|
|
7
8
|
export declare function addDomainToAllowlist(allowlist: EgressAllowlistFile, entry: EgressAllowlistEntry): EgressAllowlistFile;
|
|
@@ -1,6 +1,7 @@
|
|
|
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 { withStateFileLock } from '../capability/approval-state-mutation.js';
|
|
4
5
|
import { belayStateDir } from '../config.js';
|
|
5
6
|
export function egressAllowlistPath(config, repoLocalStateDir) {
|
|
6
7
|
return path.join(belayStateDir(config, repoLocalStateDir), 'egress-allowlist.json');
|
|
@@ -19,6 +20,13 @@ export async function saveEgressAllowlist(filePath, state) {
|
|
|
19
20
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
20
21
|
await writeFile(filePath, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
|
|
21
22
|
}
|
|
23
|
+
export async function mutateEgressAllowlist(filePath, mutate) {
|
|
24
|
+
return withStateFileLock(filePath, async () => {
|
|
25
|
+
const updated = mutate(await loadEgressAllowlist(filePath));
|
|
26
|
+
await saveEgressAllowlist(filePath, updated);
|
|
27
|
+
return updated;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
22
30
|
export function isHostAllowlisted(host, allowlist) {
|
|
23
31
|
const normalized = host.toLowerCase();
|
|
24
32
|
return allowlist.domains.some((entry) => entry.host.toLowerCase() === normalized);
|
|
@@ -1,23 +1,15 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { compactApprovals, createApprovalRecord } from './approval.js';
|
|
3
3
|
import { issueApprovalToken } from './approval-token.js';
|
|
4
|
+
import { mutateApprovalStateWithRetry } from './capability/approval-state-mutation.js';
|
|
4
5
|
import { configuredControlPlaneDir } from './config.js';
|
|
5
|
-
import { addDomainToAllowlist,
|
|
6
|
+
import { addDomainToAllowlist, mutateEgressAllowlist } from './egress/allowlist.js';
|
|
6
7
|
import { parseHostFromSummary } from './egress/fingerprint.js';
|
|
7
8
|
import { notifyDeny } from './notify.js';
|
|
8
9
|
export async function ensurePendingEgressApproval(params) {
|
|
9
10
|
const { config, repoRoot, policyResult, store } = params;
|
|
10
|
-
const pending = await store.loadPending();
|
|
11
|
-
pending.state = compactApprovals(pending.state);
|
|
12
|
-
const existing = pending.state.approvals.find((approval) => approval.kind === 'egress' &&
|
|
13
|
-
approval.fingerprint === policyResult.fingerprint &&
|
|
14
|
-
approval.repoRoot === repoRoot);
|
|
15
|
-
if (existing) {
|
|
16
|
-
await store.writePending(pending.filePath, pending.state);
|
|
17
|
-
return { approvalId: existing.approvalId, approval: existing, created: false };
|
|
18
|
-
}
|
|
19
11
|
const approvalId = `belay_${randomUUID().replaceAll('-', '').slice(0, 12)}`;
|
|
20
|
-
const
|
|
12
|
+
const candidate = createApprovalRecord({
|
|
21
13
|
kind: 'egress',
|
|
22
14
|
fingerprint: policyResult.fingerprint,
|
|
23
15
|
repoRoot,
|
|
@@ -26,44 +18,47 @@ export async function ensurePendingEgressApproval(params) {
|
|
|
26
18
|
approvalTtlMinutes: config.approvalTtlMinutes,
|
|
27
19
|
approvalId,
|
|
28
20
|
});
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
21
|
+
const outcome = await mutateApprovalStateWithRetry({
|
|
22
|
+
load: store.loadPending,
|
|
23
|
+
write: store.writePending,
|
|
24
|
+
mutate: (state) => {
|
|
25
|
+
const compacted = compactApprovals(state);
|
|
26
|
+
const existing = compacted.approvals.find((approval) => approval.kind === 'egress' &&
|
|
27
|
+
approval.fingerprint === policyResult.fingerprint &&
|
|
28
|
+
approval.repoRoot === repoRoot);
|
|
29
|
+
if (existing) {
|
|
30
|
+
return {
|
|
31
|
+
state: compacted,
|
|
32
|
+
result: { approvalId: existing.approvalId, approval: existing, created: false },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
compacted.approvals.push(candidate);
|
|
36
|
+
return {
|
|
37
|
+
state: compacted,
|
|
38
|
+
result: { approvalId, approval: candidate, created: true },
|
|
39
|
+
};
|
|
40
|
+
},
|
|
39
41
|
});
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
return await fn();
|
|
44
|
-
}
|
|
45
|
-
finally {
|
|
46
|
-
release();
|
|
47
|
-
if (consumeLocks.get(key) === gate) {
|
|
48
|
-
consumeLocks.delete(key);
|
|
49
|
-
}
|
|
42
|
+
if (!outcome) {
|
|
43
|
+
throw new Error('Failed to persist pending egress approval');
|
|
50
44
|
}
|
|
45
|
+
return outcome;
|
|
51
46
|
}
|
|
52
47
|
export async function consumeApprovedEgress(params) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
approval.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
48
|
+
return mutateApprovalStateWithRetry({
|
|
49
|
+
load: params.store.loadApproved,
|
|
50
|
+
write: params.store.writeApproved,
|
|
51
|
+
mutate: (state) => {
|
|
52
|
+
const compacted = compactApprovals(state);
|
|
53
|
+
const index = compacted.approvals.findIndex((approval) => approval.kind === 'egress' &&
|
|
54
|
+
approval.fingerprint === params.fingerprint &&
|
|
55
|
+
approval.repoRoot === params.repoRoot);
|
|
56
|
+
if (index === -1) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const [approval] = compacted.approvals.splice(index, 1);
|
|
60
|
+
return { state: compacted, result: approval ?? null };
|
|
61
|
+
},
|
|
67
62
|
});
|
|
68
63
|
}
|
|
69
64
|
export async function notifyEgressDeny(params) {
|
|
@@ -98,8 +93,8 @@ export async function recordEgressApproval(params) {
|
|
|
98
93
|
const { recordApproval } = await import('./approval-service.js');
|
|
99
94
|
const pending = await params.store.loadPending();
|
|
100
95
|
const match = pending.state.approvals.find((approval) => approval.approvalId === params.approvalId);
|
|
101
|
-
const
|
|
102
|
-
if (params.scope === 'domain' && match && !
|
|
96
|
+
const pendingHost = match ? parseHostFromSummary(match.summary) : null;
|
|
97
|
+
if (params.scope === 'domain' && match && !pendingHost) {
|
|
103
98
|
return {
|
|
104
99
|
ok: false,
|
|
105
100
|
message: `Cannot add domain to egress allowlist: could not parse host from summary "${match.summary}".`,
|
|
@@ -112,16 +107,15 @@ export async function recordEgressApproval(params) {
|
|
|
112
107
|
requireSignedToken: params.requireSignedToken ?? false,
|
|
113
108
|
store: params.store,
|
|
114
109
|
});
|
|
110
|
+
const host = result.approval ? parseHostFromSummary(result.approval.summary) : pendingHost;
|
|
115
111
|
if (!result.ok || params.scope !== 'domain' || !host) {
|
|
116
112
|
return { ok: result.ok, message: result.message };
|
|
117
113
|
}
|
|
118
|
-
|
|
119
|
-
const updated = addDomainToAllowlist(allowlist, {
|
|
114
|
+
await mutateEgressAllowlist(params.store.allowlistPath, (allowlist) => addDomainToAllowlist(allowlist, {
|
|
120
115
|
host,
|
|
121
116
|
approvedAt: new Date().toISOString(),
|
|
122
117
|
approvalId: params.approvalId,
|
|
123
|
-
});
|
|
124
|
-
await saveEgressAllowlist(params.store.allowlistPath, updated);
|
|
118
|
+
}));
|
|
125
119
|
return {
|
|
126
120
|
ok: true,
|
|
127
121
|
message: `${result.message} Domain ${host} added to egress allowlist.`,
|
|
@@ -31,6 +31,8 @@ export interface GateVerdict extends GatePermissionResponse {
|
|
|
31
31
|
axes?: ClassifyResult['axes'];
|
|
32
32
|
capabilityRequests?: ClassifyResult['capabilityRequests'];
|
|
33
33
|
authorizationDecision?: ClassifyResult['authorizationDecision'];
|
|
34
|
+
effectPlan?: ClassifyResult['effectPlan'];
|
|
35
|
+
effectPlanPolicyDecisions?: ClassifyResult['effectPlanPolicyDecisions'];
|
|
34
36
|
boundaryProfile?: string;
|
|
35
37
|
}
|
|
36
38
|
export declare function isGatedAction(value: unknown): value is GatedAction;
|
|
@@ -28,6 +28,8 @@ export function classifyResultToGateVerdict(params) {
|
|
|
28
28
|
axes: result.axes,
|
|
29
29
|
capabilityRequests: result.capabilityRequests,
|
|
30
30
|
authorizationDecision: result.authorizationDecision,
|
|
31
|
+
effectPlan: result.effectPlan,
|
|
32
|
+
effectPlanPolicyDecisions: result.effectPlanPolicyDecisions,
|
|
31
33
|
boundaryProfile: result.boundaryProfile,
|
|
32
34
|
};
|
|
33
35
|
}
|
package/dist/core/gate-engine.js
CHANGED
|
@@ -4,9 +4,12 @@ import { collectOutsideRepoPaths, collectOutsideRepoPathsFromToolPayload, } from
|
|
|
4
4
|
import { classifySubagent } from './classify-subagent.js';
|
|
5
5
|
import { classifyToolUse } from './classify-tool.js';
|
|
6
6
|
import { classifierOptionsFromConfig } from './config.js';
|
|
7
|
+
import { buildCapabilityEffectPlan } from './effect-ir/build.js';
|
|
8
|
+
import { evaluateEffectPlanPolicy } from './effect-ir/policy.js';
|
|
9
|
+
import { buildShellEffectPlan } from './effect-ir/shell-build.js';
|
|
7
10
|
import { GATE_CONTRACT_VERSION } from './gate-contract.js';
|
|
8
11
|
import { mergeAgentAssessment } from './judgment.js';
|
|
9
|
-
import { classifyShell } from './verdict/adapter.js';
|
|
12
|
+
import { buildVerdictContext, classifyShell } from './verdict/adapter.js';
|
|
10
13
|
export class GateNormalizationError extends Error {
|
|
11
14
|
reason = 'normalization_failed';
|
|
12
15
|
constructor(message) {
|
|
@@ -167,16 +170,6 @@ function applyFsScopePeripheralPolicy(outsideRepoPaths, result, options) {
|
|
|
167
170
|
}
|
|
168
171
|
return result;
|
|
169
172
|
}
|
|
170
|
-
function applySandboxOutsideBoundary(command, action, result, options) {
|
|
171
|
-
const outsideRepoPaths = collectOutsideRepoPaths(command, action.cwd, action.repoRoot, options.trustedWorkspaceRoots);
|
|
172
|
-
return applySandboxFsScopeBoundary(outsideRepoPaths, result, options, {
|
|
173
|
-
redirect: command.includes('>'),
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
function applyShellPeripheralPolicy(command, action, result, options) {
|
|
177
|
-
const outsideRepoPaths = collectOutsideRepoPaths(command, action.cwd, action.repoRoot, options.trustedWorkspaceRoots);
|
|
178
|
-
return applyFsScopePeripheralPolicy(outsideRepoPaths, result, options);
|
|
179
|
-
}
|
|
180
173
|
function outsideRepoPathsForToolAction(action, options) {
|
|
181
174
|
const payload = action.payload ?? {};
|
|
182
175
|
const paths = new Set(collectOutsideRepoPathsFromToolPayload(payload, action.cwd, action.repoRoot, options.trustedWorkspaceRoots));
|
|
@@ -198,50 +191,114 @@ function applyToolSandboxPolicies(action, result, options) {
|
|
|
198
191
|
return next;
|
|
199
192
|
}
|
|
200
193
|
export async function classifyGatedAction(action, config, extraOptions = {}) {
|
|
194
|
+
const options = { ...classifierOptionsFromConfig(config), ...extraOptions };
|
|
201
195
|
const limitResult = checkGatedActionLimits(action);
|
|
202
196
|
if (limitResult) {
|
|
203
|
-
|
|
197
|
+
if (action.kind === 'shell') {
|
|
198
|
+
return projectShellLimitResult(action, limitResult, config, options);
|
|
199
|
+
}
|
|
200
|
+
return attachEffectPlan(action, limitResult);
|
|
204
201
|
}
|
|
205
|
-
const options = { ...classifierOptionsFromConfig(config), ...extraOptions };
|
|
206
202
|
if (action.kind === 'shell') {
|
|
207
203
|
const command = action.command ?? shellCommandFromPayload(action.payload ?? {});
|
|
208
204
|
if (!command) {
|
|
209
205
|
throw new GateNormalizationError('Shell gated action requires a command.');
|
|
210
206
|
}
|
|
211
|
-
|
|
212
|
-
result = applySandboxOutsideBoundary(command, action, result, options);
|
|
213
|
-
result = applyShellPeripheralPolicy(command, action, result, options);
|
|
207
|
+
const result = await classifyShell(command, action.cwd, action.repoRoot, config, options);
|
|
214
208
|
if (!action.agentAssessment) {
|
|
215
209
|
return result;
|
|
216
210
|
}
|
|
217
211
|
const merged = mergeAgentAssessment(result.assessment, action.agentAssessment);
|
|
218
|
-
if (!merged.mismatch) {
|
|
219
|
-
return { ...result, assessment: merged.assessment };
|
|
220
|
-
}
|
|
221
212
|
return {
|
|
222
213
|
...result,
|
|
223
|
-
verdict: 'deny_pending_approval',
|
|
224
|
-
reason: 'agent_assessment_mismatch',
|
|
225
214
|
assessment: merged.assessment,
|
|
226
215
|
};
|
|
227
216
|
}
|
|
228
217
|
if (action.kind === 'subagent') {
|
|
229
|
-
return classifySubagent(action.payload ?? {}, action.repoRoot, options, config);
|
|
218
|
+
return attachEffectPlan(action, classifySubagent(action.payload ?? {}, action.repoRoot, options, config));
|
|
230
219
|
}
|
|
231
220
|
let result = await classifyToolUse(action.payload ?? {}, action.repoRoot, action.cwd, config, options);
|
|
232
221
|
result = applyToolSandboxPolicies(action, result, options);
|
|
233
222
|
if (!action.agentAssessment) {
|
|
234
|
-
return result;
|
|
223
|
+
return attachEffectPlan(action, result);
|
|
235
224
|
}
|
|
236
225
|
const merged = mergeAgentAssessment(result.assessment, action.agentAssessment);
|
|
237
226
|
if (!merged.mismatch) {
|
|
238
|
-
return { ...result, assessment: merged.assessment };
|
|
227
|
+
return attachEffectPlan(action, { ...result, assessment: merged.assessment });
|
|
239
228
|
}
|
|
240
|
-
return {
|
|
229
|
+
return attachEffectPlan(action, {
|
|
241
230
|
...result,
|
|
242
231
|
verdict: 'deny_pending_approval',
|
|
243
232
|
reason: 'agent_assessment_mismatch',
|
|
244
233
|
assessment: merged.assessment,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
function projectShellLimitResult(action, result, config, options) {
|
|
237
|
+
const summary = result.summary ?? 'oversized shell input';
|
|
238
|
+
const effectPlan = buildShellEffectPlan({
|
|
239
|
+
inputFingerprint: result.fingerprint,
|
|
240
|
+
segments: [
|
|
241
|
+
{
|
|
242
|
+
commandRedacted: summary,
|
|
243
|
+
segmentHead: 'input-limit',
|
|
244
|
+
requirements: [
|
|
245
|
+
{
|
|
246
|
+
tag: 'indeterminate',
|
|
247
|
+
action: 'indeterminate',
|
|
248
|
+
resource: { kind: 'unknown' },
|
|
249
|
+
evidence: {
|
|
250
|
+
level: 'indeterminate',
|
|
251
|
+
signals: ['input_too_large', 'analysis_budget_exceeded'],
|
|
252
|
+
basis: ['gate:input_limit'],
|
|
253
|
+
},
|
|
254
|
+
provenance: { segment: summary },
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
completeness: 'partial',
|
|
258
|
+
opacity: 'opaque',
|
|
259
|
+
signals: ['input_too_large', 'analysis_budget_exceeded'],
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
});
|
|
263
|
+
const policy = evaluateEffectPlanPolicy(effectPlan, buildVerdictContext({
|
|
264
|
+
cwd: action.cwd,
|
|
265
|
+
repoRoot: action.repoRoot,
|
|
266
|
+
config,
|
|
267
|
+
options,
|
|
268
|
+
}));
|
|
269
|
+
return {
|
|
270
|
+
...result,
|
|
271
|
+
verdict: policy.projection.hookVerdict,
|
|
272
|
+
axes: {
|
|
273
|
+
location: 'unknown',
|
|
274
|
+
opacity: effectPlan.opacity,
|
|
275
|
+
effect: 'unknown',
|
|
276
|
+
confidence: 'deterministic',
|
|
277
|
+
would: policy.projection.permission,
|
|
278
|
+
by: 'effect_plan',
|
|
279
|
+
signals: [...effectPlan.signals],
|
|
280
|
+
},
|
|
281
|
+
capabilityRequests: policy.capabilityRequests,
|
|
282
|
+
authorizationDecision: policy.authorizationDecision,
|
|
283
|
+
effectPlan,
|
|
284
|
+
effectPlanPolicyDecisions: policy.decisions,
|
|
285
|
+
effectPlanProjection: policy.projection,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
function attachEffectPlan(action, result) {
|
|
289
|
+
if (result.effectPlan) {
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
const summary = result.normalizedCommand ?? result.summary ?? action.command ?? action.kind;
|
|
293
|
+
return {
|
|
294
|
+
...result,
|
|
295
|
+
effectPlan: buildCapabilityEffectPlan({
|
|
296
|
+
actionKind: action.kind,
|
|
297
|
+
summary,
|
|
298
|
+
inputFingerprint: result.fingerprint,
|
|
299
|
+
requests: result.capabilityRequests ?? [],
|
|
300
|
+
effectFree: result.reason === 'read_only',
|
|
301
|
+
}),
|
|
245
302
|
};
|
|
246
303
|
}
|
|
247
304
|
export async function classifyGatedActionAsync(action, config, extraOptions = {}) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface GitResourceIdentity {
|
|
2
|
+
repositoryRoot: string;
|
|
3
|
+
gitDir: string;
|
|
4
|
+
commonDir: string;
|
|
5
|
+
gitEntryPath: string;
|
|
6
|
+
}
|
|
7
|
+
export type GitResourceIdentityInspection = {
|
|
8
|
+
status: 'resolved';
|
|
9
|
+
identity: GitResourceIdentity;
|
|
10
|
+
} | {
|
|
11
|
+
status: 'absent';
|
|
12
|
+
} | {
|
|
13
|
+
status: 'invalid';
|
|
14
|
+
boundaryPath: string;
|
|
15
|
+
metadataRoots: string[];
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Inspects the Git boundary containing a path without executing Git or a shell.
|
|
19
|
+
* Malformed or unreadable metadata is distinct from an absent Git boundary.
|
|
20
|
+
*/
|
|
21
|
+
export declare function inspectGitResourceIdentity(targetPath: string): GitResourceIdentityInspection;
|
|
22
|
+
/** Resolves only structurally valid Git metadata; otherwise returns null. */
|
|
23
|
+
export declare function resolveGitResourceIdentity(targetPath: string): GitResourceIdentity | null;
|
|
24
|
+
export declare function sameGitResourceIdentity(leftPath: string, rightPath: string): boolean;
|
|
25
|
+
/** Protects control data for any proven repository and any malformed Git boundary. */
|
|
26
|
+
export declare function isGitMetadataPath(targetPath: string, _repositoryPath?: string): boolean;
|