@guilz-dev/belay 0.4.0 → 0.6.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 +7 -1
- package/dist/adapters/layouts/scope.js +3 -2
- package/dist/adapters/shared/gate-runtime.d.ts +1 -2
- package/dist/adapters/shared/gate-runtime.js +226 -28
- package/dist/bundle/claude-runtime.mjs +6967 -4078
- package/dist/bundle/codex-runtime.mjs +5951 -3064
- package/dist/bundle/cursor-runtime.mjs +5951 -3064
- package/dist/cli.js +43 -1
- 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 +36 -1
- package/dist/commands/explain.js +11 -1
- package/dist/commands/judge.js +3 -2
- package/dist/commands/session.d.ts +20 -0
- package/dist/commands/session.js +51 -0
- package/dist/config-io.js +49 -12
- 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 +14 -9
- package/dist/core/approval-service.js +6 -2
- package/dist/core/approval.d.ts +2 -0
- package/dist/core/approval.js +14 -3
- package/dist/core/capability/approval-state-mutation.d.ts +13 -0
- package/dist/core/capability/approval-state-mutation.js +92 -0
- package/dist/core/capability/approval-v3.d.ts +15 -0
- package/dist/core/capability/approval-v3.js +74 -0
- package/dist/core/capability/attestation.d.ts +13 -0
- package/dist/core/capability/attestation.js +55 -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 +7 -0
- package/dist/core/capability/boundary-driver-container.js +122 -0
- package/dist/core/capability/boundary-driver.d.ts +29 -0
- package/dist/core/capability/boundary-driver.js +43 -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 +13 -0
- package/dist/core/capability/boundary-grant-materialize.js +78 -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 +23 -0
- package/dist/core/capability/boundary-run.js +23 -0
- package/dist/core/capability/boundary-session.d.ts +57 -0
- package/dist/core/capability/boundary-session.js +129 -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 +108 -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-lease.d.ts +9 -0
- package/dist/core/capability/grant-lease.js +46 -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 +93 -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 +70 -0
- package/dist/core/capability/policy-engine.js +476 -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 +45 -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 +14 -3
- package/dist/core/config.js +56 -3
- package/dist/core/gate-contract.d.ts +3 -0
- package/dist/core/gate-contract.js +3 -0
- package/dist/core/gate-engine.js +6 -1
- 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/path-utils.d.ts +1 -0
- package/dist/core/path-utils.js +5 -2
- package/dist/core/recovery/capability.d.ts +3 -0
- package/dist/core/recovery/capability.js +22 -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 +3 -0
- package/dist/core/recovery/index.js +2 -0
- package/dist/core/recovery/types.d.ts +20 -0
- package/dist/core/recovery/types.js +1 -0
- package/dist/core/standing-allow.js +3 -0
- package/dist/core/transactional/diff-evaluator.js +4 -1
- package/dist/core/transactional/eligibility.js +4 -0
- package/dist/core/transactional/git-worktree.d.ts +10 -3
- package/dist/core/transactional/git-worktree.js +81 -11
- package/dist/core/transactional/reasons.d.ts +1 -0
- package/dist/core/transactional/reasons.js +4 -0
- package/dist/core/transactional/runner.js +22 -6
- package/dist/core/transactional/types.d.ts +4 -0
- package/dist/core/types.d.ts +21 -3
- package/dist/core/verdict/adapter.d.ts +3 -4
- package/dist/core/verdict/adapter.js +16 -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 +16 -0
- 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 +25 -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 +13 -1
- package/dist/core/verdict/verdict.js +296 -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 +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { boundaryMountReadOnlyFromPrediction, runWithBoundaryRunnable, } from '../capability/boundary-run.js';
|
|
2
|
+
import { hostIntegrationBoundaryContext } from '../capability/boundary-session.js';
|
|
1
3
|
import { evaluateTransactionalDiff } from './diff-evaluator.js';
|
|
2
|
-
import { applyWorktreeChanges, collectWorktreeChanges, createGitWorktreeSnapshot, isDirtyWorktree, isGitWorktreeAvailable, resolveWorktreeCwd,
|
|
4
|
+
import { applyWorktreeChanges, captureRepoFileHashes, collectWorktreeChanges, createGitWorktreeSnapshot, isDirtyWorktree, isGitWorktreeAvailable, resolveWorktreeCwd, TRANSACTIONAL_APPLY_TOCTOU, } from './git-worktree.js';
|
|
3
5
|
import { TRANSACTIONAL_ALREADY_APPLIED, TRANSACTIONAL_APPLY_FAILED, TRANSACTIONAL_OBSERVED_RISK, } from './reasons.js';
|
|
4
6
|
export async function runTransactionalExecution(params) {
|
|
5
7
|
const { predicted, repoRoot, stateDir, command, cwd, timeoutMs, diffContext } = params;
|
|
@@ -12,7 +14,7 @@ export async function runTransactionalExecution(params) {
|
|
|
12
14
|
result: predicted,
|
|
13
15
|
};
|
|
14
16
|
}
|
|
15
|
-
if (await isDirtyWorktree(repoRoot)) {
|
|
17
|
+
if (await isDirtyWorktree(repoRoot, { ignoreRoots: params.dirtyIgnoreRoots })) {
|
|
16
18
|
return {
|
|
17
19
|
ok: false,
|
|
18
20
|
skipped: true,
|
|
@@ -25,7 +27,16 @@ export async function runTransactionalExecution(params) {
|
|
|
25
27
|
try {
|
|
26
28
|
snapshot = await createGitWorktreeSnapshot(repoRoot, stateDir);
|
|
27
29
|
const execCwd = resolveWorktreeCwd(repoRoot, snapshot.worktreePath, cwd);
|
|
28
|
-
const
|
|
30
|
+
const boundaryContext = params.boundaryContext ?? hostIntegrationBoundaryContext(params.repoRoot);
|
|
31
|
+
const shellResult = await runWithBoundaryRunnable(boundaryContext.driver, {
|
|
32
|
+
prepareContext: boundaryContext.prepareContext,
|
|
33
|
+
command,
|
|
34
|
+
cwd: execCwd,
|
|
35
|
+
timeoutMs,
|
|
36
|
+
runOptions: {
|
|
37
|
+
mountReadOnly: boundaryMountReadOnlyFromPrediction(predicted),
|
|
38
|
+
},
|
|
39
|
+
});
|
|
29
40
|
if (shellResult.timedOut) {
|
|
30
41
|
return {
|
|
31
42
|
ok: false,
|
|
@@ -52,10 +63,12 @@ export async function runTransactionalExecution(params) {
|
|
|
52
63
|
const changes = await collectWorktreeChanges(snapshot.worktreePath);
|
|
53
64
|
const observed = evaluateTransactionalDiff(changes, diffContext);
|
|
54
65
|
if (observed.verdict === 'allow') {
|
|
66
|
+
const baseHashes = await captureRepoFileHashes(repoRoot, changes);
|
|
55
67
|
try {
|
|
56
|
-
await applyWorktreeChanges(snapshot.worktreePath, repoRoot, changes);
|
|
68
|
+
await applyWorktreeChanges(snapshot.worktreePath, repoRoot, changes, { baseHashes });
|
|
57
69
|
}
|
|
58
|
-
catch {
|
|
70
|
+
catch (error) {
|
|
71
|
+
const toctou = error instanceof Error && error.message === TRANSACTIONAL_APPLY_TOCTOU;
|
|
59
72
|
const result = {
|
|
60
73
|
...predicted,
|
|
61
74
|
verdict: 'deny_pending_approval',
|
|
@@ -64,7 +77,10 @@ export async function runTransactionalExecution(params) {
|
|
|
64
77
|
...observed.assessment,
|
|
65
78
|
reversibility: 'irreversible',
|
|
66
79
|
confidence: 1,
|
|
67
|
-
signals: [
|
|
80
|
+
signals: [
|
|
81
|
+
...observed.assessment.signals,
|
|
82
|
+
toctou ? 'transactional_apply_toctou' : 'transactional_apply_failed',
|
|
83
|
+
],
|
|
68
84
|
},
|
|
69
85
|
};
|
|
70
86
|
return {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ResolvedBoundaryDriverContext } from '../capability/boundary-session.js';
|
|
1
2
|
import type { Assessment, ClassifyResult, HookVerdict } from '../types.js';
|
|
2
3
|
export type TransactionalFileChangeKind = 'added' | 'modified' | 'deleted';
|
|
3
4
|
export interface TransactionalFileChange {
|
|
@@ -43,4 +44,7 @@ export interface TransactionalRunnerParams {
|
|
|
43
44
|
timeoutMs: number;
|
|
44
45
|
predicted: ClassifyResult;
|
|
45
46
|
diffContext: TransactionalDiffContext;
|
|
47
|
+
boundaryContext?: ResolvedBoundaryDriverContext;
|
|
48
|
+
/** Repo-local Belay paths excluded from dirty-worktree gating (init artifacts). */
|
|
49
|
+
dirtyIgnoreRoots?: string[];
|
|
46
50
|
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import type { BoundaryAttestation } from './capability/attestation.js';
|
|
2
|
+
import type { CapabilityGrantV1 } from './capability/grant.js';
|
|
3
|
+
import type { PolicyDecision } from './capability/policy-types.js';
|
|
4
|
+
import type { CapabilityRequestV1 } from './capability/request.js';
|
|
1
5
|
import type { FsScopeAllowlistFile } from './capability/types.js';
|
|
2
6
|
export type BelayMode = 'enforce' | 'audit';
|
|
3
7
|
export type HookVerdict = 'allow' | 'allow_flagged' | 'deny_pending_approval';
|
|
@@ -34,6 +38,9 @@ export interface ClassifyResult {
|
|
|
34
38
|
normalizedCommand?: string;
|
|
35
39
|
summary?: string;
|
|
36
40
|
axes?: VerdictAxes;
|
|
41
|
+
capabilityRequests?: CapabilityRequestV1[];
|
|
42
|
+
authorizationDecision?: PolicyDecision;
|
|
43
|
+
boundaryProfile?: string;
|
|
37
44
|
}
|
|
38
45
|
export type UnknownLocalEffectPolicy = 'allow_flagged' | 'deny';
|
|
39
46
|
export type UnparseableShellPolicy = 'allow_flagged' | 'deny';
|
|
@@ -68,10 +75,13 @@ export interface ClassifierOptions {
|
|
|
68
75
|
brokerFsScope?: boolean;
|
|
69
76
|
fsScopeAllowlist?: FsScopeAllowlistFile;
|
|
70
77
|
trustedWorkspaceRoots?: string[];
|
|
71
|
-
/** Test override: inject Tier1 judge without changing config.judge. */
|
|
72
|
-
tier1Judge?: import('./verdict/types.js').Tier1Judge;
|
|
73
78
|
/** When false, path resolution stays fail-closed (opaque cd chains). Default: Boolean(cwd). */
|
|
74
79
|
trustedCwd?: boolean;
|
|
80
|
+
grants?: CapabilityGrantV1[];
|
|
81
|
+
attestation?: BoundaryAttestation | null;
|
|
82
|
+
boundaryProfile?: string;
|
|
83
|
+
/** True when egress proxy is running and bound to this repository. */
|
|
84
|
+
egressProxyActive?: boolean;
|
|
75
85
|
}
|
|
76
86
|
export interface ApprovalScopeHint {
|
|
77
87
|
scope: 'workspace-root';
|
|
@@ -102,8 +112,16 @@ export interface ApprovalRecord {
|
|
|
102
112
|
payloadJson?: string;
|
|
103
113
|
/** Optional scope hint for structured capability approvals. */
|
|
104
114
|
scopeHint?: ApprovalScopeHint;
|
|
115
|
+
/** Approval state v3: normalized capability requests at ask time. */
|
|
116
|
+
capabilityRequests?: CapabilityRequestV1[];
|
|
117
|
+
/** Approval state v3: hash of capabilityRequests for replay binding. */
|
|
118
|
+
capabilityRequestHash?: string;
|
|
119
|
+
/** Approval state v3: minted grant after human approval. */
|
|
120
|
+
grant?: CapabilityGrantV1;
|
|
105
121
|
}
|
|
106
122
|
export interface ApprovalStateFile {
|
|
107
|
-
version: 1 | 2;
|
|
123
|
+
version: 1 | 2 | 3;
|
|
124
|
+
/** Optimistic concurrency revision (v3). */
|
|
125
|
+
revision?: number;
|
|
108
126
|
approvals: ApprovalRecord[];
|
|
109
127
|
}
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import type { BelayConfigV4 } from '../config.js';
|
|
2
2
|
import type { ClassifierOptions, ClassifyResult } from '../types.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type { VerdictContext, VerdictResult } from './types.js';
|
|
4
4
|
export declare function resolveClassifierTrustedCwd(cwd: string, options?: Pick<ClassifierOptions, 'trustedCwd'>, explicit?: boolean): boolean;
|
|
5
5
|
export declare function buildVerdictContext(params: {
|
|
6
6
|
cwd: string;
|
|
7
7
|
repoRoot: string;
|
|
8
8
|
config: BelayConfigV4;
|
|
9
9
|
options?: ClassifierOptions;
|
|
10
|
-
judge?: Tier1Judge;
|
|
11
10
|
trustedCwd?: boolean;
|
|
12
11
|
}): VerdictContext;
|
|
13
|
-
export declare function classifyShell(command: string, cwd: string, repoRoot: string, config: BelayConfigV4, options?: ClassifierOptions
|
|
14
|
-
export declare function verdictToClassifyResult(result: VerdictResult): ClassifyResult;
|
|
12
|
+
export declare function classifyShell(command: string, cwd: string, repoRoot: string, config: BelayConfigV4, options?: ClassifierOptions): Promise<ClassifyResult>;
|
|
13
|
+
export declare function verdictToClassifyResult(result: VerdictResult, config?: BelayConfigV4, options?: ClassifierOptions): ClassifyResult;
|
|
15
14
|
export declare function verdictAuditFields(result: VerdictResult): Record<string, unknown>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { BOUNDARY_PROFILE_L3_L4_ONLY, resolveBoundaryProfile, } from '../capability/boundary-profile.js';
|
|
1
2
|
import { judgeTraceAuditFields } from './judge-audit.js';
|
|
2
3
|
import { recordJudgeLatency } from './judge-baseline.js';
|
|
3
|
-
import { createJudgeFromConfig } from './judge-factory.js';
|
|
4
4
|
import { verdict } from './verdict.js';
|
|
5
5
|
export function resolveClassifierTrustedCwd(cwd, options, explicit) {
|
|
6
6
|
if (explicit !== undefined) {
|
|
@@ -16,29 +16,27 @@ export function buildVerdictContext(params) {
|
|
|
16
16
|
return {
|
|
17
17
|
cwd: params.cwd,
|
|
18
18
|
repoRoot: params.repoRoot,
|
|
19
|
+
config: params.config,
|
|
19
20
|
trustedCwd: resolveClassifierTrustedCwd(params.cwd, params.options, params.trustedCwd),
|
|
20
21
|
trustedWorkspaceRoots: params.options?.trustedWorkspaceRoots ?? [],
|
|
21
22
|
sensitivePaths: params.options?.sensitivePaths ?? params.config.classifier.sensitivePaths,
|
|
22
23
|
protectedArtifactRoots: protectedArtifactRoots.length > 0 ? [...new Set(protectedArtifactRoots)] : undefined,
|
|
23
24
|
customAllowCommands: params.options?.customAllowCommands ?? params.config.overrides.allow,
|
|
24
25
|
customExternalCommands: params.options?.customExternalCommands ?? params.config.overrides.external,
|
|
25
|
-
judge: params.judge ??
|
|
26
|
-
params.options?.tier1Judge ??
|
|
27
|
-
createJudgeFromConfig(params.config, { repoRoot: params.repoRoot }),
|
|
28
26
|
mode: params.config.mode,
|
|
29
27
|
unknownLocalEffect: params.options?.unknownLocalEffect ?? params.config.policy.unknownLocalEffect,
|
|
30
28
|
unparseableShell: params.options?.unparseableShell ?? params.config.policy.unparseableShell,
|
|
29
|
+
grants: params.options?.grants,
|
|
30
|
+
attestation: params.options?.attestation,
|
|
31
|
+
egressProxyActive: params.options?.egressProxyActive,
|
|
31
32
|
};
|
|
32
33
|
}
|
|
33
|
-
export async function classifyShell(command, cwd, repoRoot, config, options = {}
|
|
34
|
-
const context = buildVerdictContext({ cwd, repoRoot, config, options
|
|
34
|
+
export async function classifyShell(command, cwd, repoRoot, config, options = {}) {
|
|
35
|
+
const context = buildVerdictContext({ cwd, repoRoot, config, options });
|
|
35
36
|
const started = Date.now();
|
|
36
37
|
const result = await verdict(command, context);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
recordJudgeLatency('tier0', elapsed);
|
|
40
|
-
}
|
|
41
|
-
return verdictToClassifyResult(result);
|
|
38
|
+
recordJudgeLatency('gate', Date.now() - started);
|
|
39
|
+
return verdictToClassifyResult(result, config, options);
|
|
42
40
|
}
|
|
43
41
|
function mapLegacyReason(result) {
|
|
44
42
|
if (result.reason === 'repo_outside_mutation') {
|
|
@@ -71,7 +69,7 @@ function mapLegacyReason(result) {
|
|
|
71
69
|
}
|
|
72
70
|
return result.reason;
|
|
73
71
|
}
|
|
74
|
-
export function verdictToClassifyResult(result) {
|
|
72
|
+
export function verdictToClassifyResult(result, config, options = {}) {
|
|
75
73
|
const external = result.location === 'external' ||
|
|
76
74
|
result.location === 'repo_outside' ||
|
|
77
75
|
result.effect === 'remote_mutation';
|
|
@@ -120,6 +118,12 @@ export function verdictToClassifyResult(result) {
|
|
|
120
118
|
signals: result.signals,
|
|
121
119
|
...judgeTraceAuditFields(result.judgeTrace),
|
|
122
120
|
},
|
|
121
|
+
capabilityRequests: result.capabilityRequests,
|
|
122
|
+
authorizationDecision: result.authorizationDecision,
|
|
123
|
+
boundaryProfile: options.boundaryProfile ??
|
|
124
|
+
(config
|
|
125
|
+
? resolveBoundaryProfile({ config, attestation: options.attestation })
|
|
126
|
+
: BOUNDARY_PROFILE_L3_L4_ONLY),
|
|
123
127
|
};
|
|
124
128
|
}
|
|
125
129
|
export function verdictAuditFields(result) {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type ChildProcessWithoutNullStreams, type SpawnOptionsWithoutStdio } from 'node:child_process';
|
|
2
|
+
import type { CliJudgeRunCommand } from './judge-cli.js';
|
|
3
|
+
interface AcpSessionResponse {
|
|
4
|
+
sessionId: string;
|
|
5
|
+
models?: {
|
|
6
|
+
currentModelId?: string;
|
|
7
|
+
availableModels?: Array<{
|
|
8
|
+
modelId?: string;
|
|
9
|
+
}>;
|
|
10
|
+
};
|
|
11
|
+
configOptions?: Array<{
|
|
12
|
+
id?: string;
|
|
13
|
+
currentValue?: string;
|
|
14
|
+
options?: Array<{
|
|
15
|
+
value?: string;
|
|
16
|
+
}>;
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
export type CursorAcpSpawn = (binary: string, args: string[], options: SpawnOptionsWithoutStdio & {
|
|
20
|
+
stdio: ['pipe', 'pipe', 'pipe'];
|
|
21
|
+
}) => ChildProcessWithoutNullStreams;
|
|
22
|
+
export type CursorAcpErrorKind = 'connect_timeout' | 'eval_timeout' | 'process_exit' | 'protocol_error';
|
|
23
|
+
export declare class CursorAcpError extends Error {
|
|
24
|
+
readonly kind: CursorAcpErrorKind;
|
|
25
|
+
constructor(kind: CursorAcpErrorKind, message: string);
|
|
26
|
+
}
|
|
27
|
+
export interface CursorAcpJudgeRunnerOptions {
|
|
28
|
+
cwd: string;
|
|
29
|
+
connectTimeoutMs: number;
|
|
30
|
+
binary?: string;
|
|
31
|
+
spawnProcess?: CursorAcpSpawn;
|
|
32
|
+
}
|
|
33
|
+
export declare function resolveCursorAcpModelId(requested: string, session: AcpSessionResponse): string;
|
|
34
|
+
export declare class CursorAcpJudgeRunner {
|
|
35
|
+
private readonly options;
|
|
36
|
+
private readonly binary;
|
|
37
|
+
private readonly spawnProcess;
|
|
38
|
+
private child;
|
|
39
|
+
private startPromise;
|
|
40
|
+
private connected;
|
|
41
|
+
private nextId;
|
|
42
|
+
private stdoutBuffer;
|
|
43
|
+
private stderrTail;
|
|
44
|
+
private readonly pending;
|
|
45
|
+
private readonly messageBuffers;
|
|
46
|
+
private activeSessionId;
|
|
47
|
+
constructor(options: CursorAcpJudgeRunnerOptions);
|
|
48
|
+
get sessionId(): string | null;
|
|
49
|
+
readonly run: CliJudgeRunCommand;
|
|
50
|
+
private evaluate;
|
|
51
|
+
stop(): Promise<void>;
|
|
52
|
+
private ensureConnected;
|
|
53
|
+
private start;
|
|
54
|
+
private createSession;
|
|
55
|
+
private request;
|
|
56
|
+
private notify;
|
|
57
|
+
private handleStdout;
|
|
58
|
+
private handleMessage;
|
|
59
|
+
private respond;
|
|
60
|
+
private respondError;
|
|
61
|
+
private handleProcessExit;
|
|
62
|
+
private rejectPending;
|
|
63
|
+
private withStderr;
|
|
64
|
+
private terminateChild;
|
|
65
|
+
private signalAndWait;
|
|
66
|
+
}
|
|
67
|
+
export {};
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { spawn, } from 'node:child_process';
|
|
2
|
+
export class CursorAcpError extends Error {
|
|
3
|
+
kind;
|
|
4
|
+
constructor(kind, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.kind = kind;
|
|
7
|
+
this.name = 'CursorAcpError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const STDERR_TAIL_LIMIT = 2_048;
|
|
11
|
+
const TERMINATE_GRACE_MS = 250;
|
|
12
|
+
function invocationValue(invocation, flag) {
|
|
13
|
+
const index = invocation.args.indexOf(flag);
|
|
14
|
+
return index >= 0 ? invocation.args[index + 1] : undefined;
|
|
15
|
+
}
|
|
16
|
+
function promptFromInvocation(invocation) {
|
|
17
|
+
if (invocation.stdin !== undefined) {
|
|
18
|
+
return invocation.stdin;
|
|
19
|
+
}
|
|
20
|
+
return invocation.args[invocation.args.length - 1] ?? '';
|
|
21
|
+
}
|
|
22
|
+
function modelIdsFromSession(session) {
|
|
23
|
+
const fromModels = session.models?.availableModels
|
|
24
|
+
?.map((entry) => entry.modelId)
|
|
25
|
+
.filter((value) => Boolean(value));
|
|
26
|
+
if (fromModels?.length) {
|
|
27
|
+
return fromModels;
|
|
28
|
+
}
|
|
29
|
+
const modelOption = session.configOptions?.find((option) => option.id === 'model');
|
|
30
|
+
return (modelOption?.options
|
|
31
|
+
?.map((entry) => entry.value)
|
|
32
|
+
.filter((value) => Boolean(value)) ?? []);
|
|
33
|
+
}
|
|
34
|
+
export function resolveCursorAcpModelId(requested, session) {
|
|
35
|
+
const available = modelIdsFromSession(session);
|
|
36
|
+
if (available.includes(requested)) {
|
|
37
|
+
return requested;
|
|
38
|
+
}
|
|
39
|
+
const parameterized = available.find((modelId) => modelId.startsWith(`${requested}[`));
|
|
40
|
+
return parameterized ?? requested;
|
|
41
|
+
}
|
|
42
|
+
export class CursorAcpJudgeRunner {
|
|
43
|
+
options;
|
|
44
|
+
binary;
|
|
45
|
+
spawnProcess;
|
|
46
|
+
child = null;
|
|
47
|
+
startPromise = null;
|
|
48
|
+
connected = false;
|
|
49
|
+
nextId = 0;
|
|
50
|
+
stdoutBuffer = '';
|
|
51
|
+
stderrTail = '';
|
|
52
|
+
pending = new Map();
|
|
53
|
+
messageBuffers = new Map();
|
|
54
|
+
activeSessionId = null;
|
|
55
|
+
constructor(options) {
|
|
56
|
+
this.options = options;
|
|
57
|
+
this.binary = options.binary ?? 'cursor-agent';
|
|
58
|
+
this.spawnProcess = options.spawnProcess ?? spawn;
|
|
59
|
+
}
|
|
60
|
+
get sessionId() {
|
|
61
|
+
return this.activeSessionId;
|
|
62
|
+
}
|
|
63
|
+
run = async (invocation, timeoutMs) => {
|
|
64
|
+
if (invocation.binary !== this.binary && invocation.binary !== 'cursor-agent') {
|
|
65
|
+
throw new CursorAcpError('protocol_error', `Cursor ACP runner cannot execute ${invocation.binary}`);
|
|
66
|
+
}
|
|
67
|
+
let totalTimer = null;
|
|
68
|
+
try {
|
|
69
|
+
const evaluation = this.evaluate(invocation, timeoutMs);
|
|
70
|
+
const totalTimeout = new Promise((_resolve, reject) => {
|
|
71
|
+
totalTimer = setTimeout(() => {
|
|
72
|
+
reject(new CursorAcpError('eval_timeout', 'Cursor ACP evaluation timed out'));
|
|
73
|
+
}, timeoutMs);
|
|
74
|
+
});
|
|
75
|
+
return await Promise.race([evaluation, totalTimeout]);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error instanceof CursorAcpError && error.kind === 'eval_timeout') {
|
|
79
|
+
this.notify('session/cancel', { sessionId: this.activeSessionId });
|
|
80
|
+
}
|
|
81
|
+
await this.stop();
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
if (totalTimer) {
|
|
86
|
+
clearTimeout(totalTimer);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
async evaluate(invocation, timeoutMs) {
|
|
91
|
+
await this.ensureConnected();
|
|
92
|
+
const requestedResumeId = invocationValue(invocation, '--resume');
|
|
93
|
+
const model = invocationValue(invocation, '--model') ?? 'composer-2.5';
|
|
94
|
+
const canReuse = Boolean(requestedResumeId) && requestedResumeId === this.activeSessionId;
|
|
95
|
+
if (!canReuse) {
|
|
96
|
+
await this.createSession(model);
|
|
97
|
+
}
|
|
98
|
+
const sessionId = this.activeSessionId;
|
|
99
|
+
if (!sessionId) {
|
|
100
|
+
throw new CursorAcpError('protocol_error', 'Cursor ACP session was not created');
|
|
101
|
+
}
|
|
102
|
+
this.messageBuffers.set(sessionId, '');
|
|
103
|
+
try {
|
|
104
|
+
await this.request('session/prompt', {
|
|
105
|
+
sessionId,
|
|
106
|
+
prompt: [{ type: 'text', text: promptFromInvocation(invocation) }],
|
|
107
|
+
}, timeoutMs, 'eval_timeout');
|
|
108
|
+
const raw = this.messageBuffers.get(sessionId)?.trim() ?? '';
|
|
109
|
+
if (!raw) {
|
|
110
|
+
throw new CursorAcpError('protocol_error', 'Cursor ACP returned no agent message');
|
|
111
|
+
}
|
|
112
|
+
return raw;
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
this.messageBuffers.delete(sessionId);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async stop() {
|
|
119
|
+
const child = this.child;
|
|
120
|
+
this.child = null;
|
|
121
|
+
this.connected = false;
|
|
122
|
+
this.startPromise = null;
|
|
123
|
+
this.activeSessionId = null;
|
|
124
|
+
this.stdoutBuffer = '';
|
|
125
|
+
this.messageBuffers.clear();
|
|
126
|
+
this.rejectPending(new CursorAcpError('process_exit', this.withStderr('Cursor ACP process stopped')));
|
|
127
|
+
if (child && child.exitCode === null) {
|
|
128
|
+
await this.terminateChild(child);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async ensureConnected() {
|
|
132
|
+
if (this.connected && this.child) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (!this.startPromise) {
|
|
136
|
+
this.startPromise = this.start().finally(() => {
|
|
137
|
+
this.startPromise = null;
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
await this.startPromise;
|
|
141
|
+
}
|
|
142
|
+
async start() {
|
|
143
|
+
this.stderrTail = '';
|
|
144
|
+
const child = this.spawnProcess(this.binary, ['acp'], {
|
|
145
|
+
cwd: this.options.cwd,
|
|
146
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
147
|
+
});
|
|
148
|
+
this.child = child;
|
|
149
|
+
child.stdout.on('data', (chunk) => this.handleStdout(String(chunk)));
|
|
150
|
+
child.stderr.on('data', (chunk) => {
|
|
151
|
+
this.stderrTail = `${this.stderrTail}${String(chunk)}`.slice(-STDERR_TAIL_LIMIT);
|
|
152
|
+
});
|
|
153
|
+
child.once('error', (error) => {
|
|
154
|
+
this.handleProcessExit(new CursorAcpError('process_exit', this.withStderr(error.message)), child);
|
|
155
|
+
});
|
|
156
|
+
child.once('close', (code) => {
|
|
157
|
+
this.handleProcessExit(new CursorAcpError('process_exit', this.withStderr(`Cursor ACP exited with code ${code ?? 'unknown'}`)), child);
|
|
158
|
+
});
|
|
159
|
+
try {
|
|
160
|
+
await this.request('initialize', {
|
|
161
|
+
protocolVersion: 1,
|
|
162
|
+
clientCapabilities: {
|
|
163
|
+
fs: { readTextFile: false, writeTextFile: false },
|
|
164
|
+
terminal: false,
|
|
165
|
+
},
|
|
166
|
+
clientInfo: {
|
|
167
|
+
name: 'belay-judge',
|
|
168
|
+
title: 'Belay Judge',
|
|
169
|
+
version: '1',
|
|
170
|
+
},
|
|
171
|
+
}, this.options.connectTimeoutMs, 'connect_timeout');
|
|
172
|
+
await this.request('authenticate', { methodId: 'cursor_login' }, this.options.connectTimeoutMs, 'connect_timeout');
|
|
173
|
+
this.connected = true;
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
await this.stop();
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async createSession(model) {
|
|
181
|
+
const previous = this.activeSessionId;
|
|
182
|
+
this.activeSessionId = null;
|
|
183
|
+
if (previous) {
|
|
184
|
+
await this.request('session/close', { sessionId: previous }, Math.min(this.options.connectTimeoutMs, 1_000), 'connect_timeout').catch(() => undefined);
|
|
185
|
+
}
|
|
186
|
+
const session = (await this.request('session/new', { cwd: this.options.cwd, mcpServers: [] }, this.options.connectTimeoutMs, 'connect_timeout'));
|
|
187
|
+
if (!session?.sessionId) {
|
|
188
|
+
throw new CursorAcpError('protocol_error', 'Cursor ACP session/new omitted sessionId');
|
|
189
|
+
}
|
|
190
|
+
this.activeSessionId = session.sessionId;
|
|
191
|
+
await this.request('session/set_config_option', { sessionId: session.sessionId, configId: 'mode', value: 'ask' }, this.options.connectTimeoutMs, 'connect_timeout');
|
|
192
|
+
await this.request('session/set_config_option', {
|
|
193
|
+
sessionId: session.sessionId,
|
|
194
|
+
configId: 'model',
|
|
195
|
+
value: resolveCursorAcpModelId(model, session),
|
|
196
|
+
}, this.options.connectTimeoutMs, 'connect_timeout');
|
|
197
|
+
}
|
|
198
|
+
request(method, params, timeoutMs, timeoutKind) {
|
|
199
|
+
const child = this.child;
|
|
200
|
+
if (!child?.stdin.writable) {
|
|
201
|
+
return Promise.reject(new CursorAcpError('process_exit', this.withStderr('Cursor ACP stdin is unavailable')));
|
|
202
|
+
}
|
|
203
|
+
const id = ++this.nextId;
|
|
204
|
+
return new Promise((resolve, reject) => {
|
|
205
|
+
const timer = setTimeout(() => {
|
|
206
|
+
this.pending.delete(id);
|
|
207
|
+
reject(new CursorAcpError(timeoutKind, `Cursor ACP ${method} timed out`));
|
|
208
|
+
}, timeoutMs);
|
|
209
|
+
this.pending.set(id, { method, resolve, reject, timer });
|
|
210
|
+
child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
notify(method, params) {
|
|
214
|
+
if (!this.child?.stdin.writable) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
this.child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', method, params })}\n`);
|
|
218
|
+
}
|
|
219
|
+
handleStdout(chunk) {
|
|
220
|
+
this.stdoutBuffer += chunk;
|
|
221
|
+
const lines = this.stdoutBuffer.split('\n');
|
|
222
|
+
this.stdoutBuffer = lines.pop() ?? '';
|
|
223
|
+
for (const line of lines) {
|
|
224
|
+
if (!line.trim()) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
this.handleMessage(JSON.parse(line));
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
const child = this.child;
|
|
232
|
+
this.handleProcessExit(new CursorAcpError('protocol_error', 'Cursor ACP emitted invalid JSON-RPC'), child);
|
|
233
|
+
if (child?.exitCode === null) {
|
|
234
|
+
void this.terminateChild(child);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
handleMessage(message) {
|
|
240
|
+
if (message.id !== undefined && !message.method) {
|
|
241
|
+
const pending = this.pending.get(message.id);
|
|
242
|
+
if (!pending) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
clearTimeout(pending.timer);
|
|
246
|
+
this.pending.delete(message.id);
|
|
247
|
+
if (message.error !== undefined) {
|
|
248
|
+
pending.reject(new CursorAcpError('protocol_error', `Cursor ACP ${pending.method} failed: ${JSON.stringify(message.error)}`));
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
pending.resolve(message.result);
|
|
252
|
+
}
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (message.method === 'session/update') {
|
|
256
|
+
const sessionId = message.params?.sessionId;
|
|
257
|
+
const update = message.params?.update;
|
|
258
|
+
if (typeof sessionId !== 'string' || !update || typeof update !== 'object') {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const record = update;
|
|
262
|
+
const content = record.content;
|
|
263
|
+
if (record.sessionUpdate === 'agent_message_chunk' &&
|
|
264
|
+
content &&
|
|
265
|
+
typeof content === 'object' &&
|
|
266
|
+
typeof content.text === 'string') {
|
|
267
|
+
const text = content.text;
|
|
268
|
+
this.messageBuffers.set(sessionId, `${this.messageBuffers.get(sessionId) ?? ''}${text}`);
|
|
269
|
+
}
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (message.id !== undefined && message.method === 'session/request_permission') {
|
|
273
|
+
this.respond(message.id, { outcome: { outcome: 'cancelled' } });
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (message.id !== undefined && message.method) {
|
|
277
|
+
this.respondError(message.id, -32601, 'Belay Judge does not expose client tools');
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
respond(id, result) {
|
|
281
|
+
this.child?.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, result })}\n`);
|
|
282
|
+
}
|
|
283
|
+
respondError(id, code, message) {
|
|
284
|
+
this.child?.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } })}\n`);
|
|
285
|
+
}
|
|
286
|
+
handleProcessExit(error, exitedChild = this.child) {
|
|
287
|
+
if (exitedChild && this.child !== exitedChild) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
this.child = null;
|
|
291
|
+
this.connected = false;
|
|
292
|
+
this.activeSessionId = null;
|
|
293
|
+
this.messageBuffers.clear();
|
|
294
|
+
this.rejectPending(error);
|
|
295
|
+
}
|
|
296
|
+
rejectPending(error) {
|
|
297
|
+
for (const pending of this.pending.values()) {
|
|
298
|
+
clearTimeout(pending.timer);
|
|
299
|
+
pending.reject(error);
|
|
300
|
+
}
|
|
301
|
+
this.pending.clear();
|
|
302
|
+
}
|
|
303
|
+
withStderr(message) {
|
|
304
|
+
const stderr = this.stderrTail.trim();
|
|
305
|
+
return stderr ? `${message}: ${stderr}` : message;
|
|
306
|
+
}
|
|
307
|
+
async terminateChild(child) {
|
|
308
|
+
if (child.exitCode !== null) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (await this.signalAndWait(child, 'SIGTERM')) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
await this.signalAndWait(child, 'SIGKILL');
|
|
315
|
+
}
|
|
316
|
+
signalAndWait(child, signal) {
|
|
317
|
+
if (child.exitCode !== null) {
|
|
318
|
+
return Promise.resolve(true);
|
|
319
|
+
}
|
|
320
|
+
return new Promise((resolve) => {
|
|
321
|
+
let settled = false;
|
|
322
|
+
const finish = (exited) => {
|
|
323
|
+
if (settled)
|
|
324
|
+
return;
|
|
325
|
+
settled = true;
|
|
326
|
+
clearTimeout(timer);
|
|
327
|
+
child.off('close', onExit);
|
|
328
|
+
child.off('error', onExit);
|
|
329
|
+
resolve(exited);
|
|
330
|
+
};
|
|
331
|
+
const onExit = () => finish(true);
|
|
332
|
+
const timer = setTimeout(() => finish(child.exitCode !== null), TERMINATE_GRACE_MS);
|
|
333
|
+
child.once('close', onExit);
|
|
334
|
+
child.once('error', onExit);
|
|
335
|
+
try {
|
|
336
|
+
if (!child.kill(signal)) {
|
|
337
|
+
finish(child.exitCode !== null);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
finish(child.exitCode !== null);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* destructive →
|
|
2
|
+
* Egress tool classification (SPEC v2.1.3 R33–R34).
|
|
3
|
+
* destructive → ask | read → allow at egress layer | ambiguous → require_approval via PolicyEngine
|
|
4
4
|
*/
|
|
5
5
|
export type EgressClassification = 'destructive' | 'read' | 'ambiguous';
|
|
6
6
|
export declare function isEgressToolHead(head: string): boolean;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* destructive →
|
|
2
|
+
* Egress tool classification (SPEC v2.1.3 R33–R34).
|
|
3
|
+
* destructive → ask | read → allow at egress layer | ambiguous → require_approval via PolicyEngine
|
|
4
4
|
*/
|
|
5
5
|
const EGRESS_TOOL_HEADS = new Set([
|
|
6
6
|
'aws',
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { VerdictEffect } from './types.js';
|
|
2
|
+
export interface NormalizedGitInvocation {
|
|
3
|
+
subcommand: string;
|
|
4
|
+
args: string[];
|
|
5
|
+
effectiveCwd?: string;
|
|
6
|
+
gitDir?: string;
|
|
7
|
+
workTree?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface GitCommandSemantics {
|
|
10
|
+
effect: VerdictEffect;
|
|
11
|
+
pathTargets: string[];
|
|
12
|
+
scopeTargets: string[];
|
|
13
|
+
signals: string[];
|
|
14
|
+
egressClass?: 'read' | 'destructive' | 'ambiguous';
|
|
15
|
+
requiresAsk?: {
|
|
16
|
+
reason: string;
|
|
17
|
+
signals: string[];
|
|
18
|
+
};
|
|
19
|
+
effectiveCwd?: string;
|
|
20
|
+
gitWorkTree?: string;
|
|
21
|
+
normalizedKey: string;
|
|
22
|
+
isReadOnly: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare function normalizeGitInvocation(tokens: string[], baseCwd: string): NormalizedGitInvocation | null;
|
|
25
|
+
export declare function isGitReadOnlyInvocation(tokens: string[], baseCwd: string): boolean;
|
|
26
|
+
export declare function classifyGitCommand(tokens: string[], baseCwd: string): GitCommandSemantics | null;
|