@guilz-dev/belay 0.3.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/cursor/runtime-entry.js +1 -0
- package/dist/adapters/layouts/scope.js +3 -2
- package/dist/adapters/shared/gate-runtime.d.ts +12 -16
- package/dist/adapters/shared/gate-runtime.js +499 -43
- package/dist/bundle/claude-runtime.mjs +8004 -2676
- package/dist/bundle/codex-runtime.mjs +8021 -2683
- package/dist/bundle/cursor-runtime.mjs +7993 -2667
- package/dist/cli.js +180 -6
- package/dist/commands/approve.d.ts +1 -0
- package/dist/commands/approve.js +45 -26
- package/dist/commands/classify-for-report.js +1 -1
- package/dist/commands/config.d.ts +8 -0
- package/dist/commands/config.js +264 -58
- package/dist/commands/doctor.js +47 -2
- package/dist/commands/explain.js +17 -2
- package/dist/commands/harvest.d.ts +29 -0
- package/dist/commands/harvest.js +72 -0
- package/dist/commands/judge.d.ts +11 -1
- package/dist/commands/judge.js +24 -4
- package/dist/commands/metrics.js +34 -2
- package/dist/commands/quality.d.ts +38 -0
- package/dist/commands/quality.js +105 -0
- package/dist/commands/session.d.ts +20 -0
- package/dist/commands/session.js +51 -0
- package/dist/commands/simulate.d.ts +1 -0
- package/dist/commands/simulate.js +7 -2
- package/dist/commands/standing-allow.d.ts +10 -0
- package/dist/commands/standing-allow.js +26 -0
- package/dist/commands/tui.d.ts +48 -0
- package/dist/commands/tui.js +150 -0
- package/dist/config-io.js +59 -17
- 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-replay-cli.d.ts +7 -0
- package/dist/core/approval-replay-cli.js +36 -0
- package/dist/core/approval-replay.d.ts +45 -0
- package/dist/core/approval-replay.js +141 -0
- package/dist/core/approval-service.d.ts +15 -0
- package/dist/core/approval-service.js +53 -4
- package/dist/core/approval.d.ts +26 -3
- package/dist/core/approval.js +61 -7
- package/dist/core/audit-analysis.d.ts +7 -1
- package/dist/core/audit-analysis.js +111 -0
- package/dist/core/audit-metrics.d.ts +7 -0
- package/dist/core/audit-metrics.js +24 -4
- package/dist/core/audit-query.d.ts +1 -0
- package/dist/core/audit-query.js +3 -0
- package/dist/core/audit-replay-context.d.ts +35 -0
- package/dist/core/audit-replay-context.js +88 -0
- package/dist/core/audit-types.d.ts +24 -1
- package/dist/core/audit-types.js +1 -1
- 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 +10 -1
- package/dist/core/capability/index.js +8 -0
- package/dist/core/capability/limits.d.ts +7 -0
- package/dist/core/capability/limits.js +55 -0
- package/dist/core/capability/paths.d.ts +2 -2
- package/dist/core/capability/paths.js +9 -9
- 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/capability/trusted-workspace-roots.d.ts +25 -0
- package/dist/core/capability/trusted-workspace-roots.js +149 -0
- package/dist/core/capability/types.d.ts +11 -1
- package/dist/core/capability-approval.d.ts +2 -1
- package/dist/core/capability-approval.js +100 -2
- package/dist/core/classify-subagent.d.ts +2 -1
- package/dist/core/classify-subagent.js +40 -22
- package/dist/core/classify-tool.js +157 -64
- package/dist/core/config.d.ts +32 -3
- package/dist/core/config.js +105 -3
- package/dist/core/gate-contract.d.ts +3 -0
- package/dist/core/gate-contract.js +3 -0
- package/dist/core/gate-engine.js +12 -7
- package/dist/core/harvest.d.ts +53 -0
- package/dist/core/harvest.js +276 -0
- package/dist/core/index.d.ts +9 -3
- package/dist/core/index.js +6 -2
- package/dist/core/judge-doctor.d.ts +3 -0
- package/dist/core/judge-doctor.js +90 -11
- 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 +12 -0
- package/dist/core/path-utils.js +60 -7
- package/dist/core/reclassify.d.ts +3 -0
- package/dist/core/reclassify.js +44 -14
- 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/replay-scrub.d.ts +9 -0
- package/dist/core/replay-scrub.js +43 -0
- package/dist/core/shell-tokenizer.d.ts +2 -0
- package/dist/core/shell-tokenizer.js +82 -23
- package/dist/core/standing-allow.d.ts +50 -0
- package/dist/core/standing-allow.js +178 -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 +36 -3
- package/dist/core/verdict/adapter.d.ts +3 -4
- package/dist/core/verdict/adapter.js +19 -6
- package/dist/core/verdict/containment.d.ts +4 -3
- package/dist/core/verdict/containment.js +17 -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.d.ts +1 -0
- package/dist/core/verdict/judge-audit.js +33 -1
- package/dist/core/verdict/judge-baseline.d.ts +20 -0
- package/dist/core/verdict/judge-baseline.js +49 -0
- package/dist/core/verdict/judge-broker-service.d.ts +42 -0
- package/dist/core/verdict/judge-broker-service.js +291 -0
- package/dist/core/verdict/judge-catalog.js +1 -0
- package/dist/core/verdict/judge-cli-fingerprint.d.ts +6 -0
- package/dist/core/verdict/judge-cli-fingerprint.js +67 -0
- package/dist/core/verdict/judge-cli.d.ts +23 -0
- package/dist/core/verdict/judge-cli.js +139 -16
- package/dist/core/verdict/judge-factory.js +13 -4
- package/dist/core/verdict/judge-provider-matrix.d.ts +20 -0
- package/dist/core/verdict/judge-provider-matrix.js +66 -0
- package/dist/core/verdict/judge-runtime-config.d.ts +44 -0
- package/dist/core/verdict/judge-runtime-config.js +96 -0
- package/dist/core/verdict/judge-session-broker.d.ts +48 -0
- package/dist/core/verdict/judge-session-broker.js +200 -0
- package/dist/core/verdict/judge-session-guard.d.ts +27 -0
- package/dist/core/verdict/judge-session-guard.js +91 -0
- package/dist/core/verdict/judge-session-kill-switch.d.ts +11 -0
- package/dist/core/verdict/judge-session-kill-switch.js +43 -0
- package/dist/core/verdict/judge-session-mutex.d.ts +9 -0
- package/dist/core/verdict/judge-session-mutex.js +23 -0
- package/dist/core/verdict/judge-shadow.d.ts +24 -0
- package/dist/core/verdict/judge-shadow.js +116 -0
- package/dist/core/verdict/judge-transport.d.ts +44 -0
- package/dist/core/verdict/judge-transport.js +268 -0
- package/dist/core/verdict/judge.d.ts +14 -14
- package/dist/core/verdict/judge.js +5 -67
- package/dist/core/verdict/launcher-resolve.d.ts +2 -0
- package/dist/core/verdict/launcher-resolve.js +16 -0
- package/dist/core/verdict/parser.js +6 -1
- 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 +26 -1
- package/dist/core/verdict/verdict.js +300 -179
- package/dist/corpus/adversarial-probe.d.ts +86 -0
- package/dist/corpus/adversarial-probe.js +220 -0
- package/dist/corpus/evaluate.d.ts +45 -12
- package/dist/corpus/evaluate.js +61 -8
- package/dist/corpus/gate-latency-budget.d.ts +39 -0
- package/dist/corpus/gate-latency-budget.js +33 -0
- package/dist/corpus/gates.d.ts +35 -0
- package/dist/corpus/gates.js +81 -0
- package/dist/corpus/judge-accuracy.d.ts +8 -0
- package/dist/corpus/judge-accuracy.js +39 -0
- package/dist/corpus/must-allow-commands.d.ts +5 -0
- package/dist/corpus/must-allow-commands.js +13 -0
- package/dist/corpus/mutators.d.ts +28 -0
- package/dist/corpus/mutators.js +162 -0
- package/dist/corpus/ratchet.d.ts +42 -0
- package/dist/corpus/ratchet.js +116 -0
- package/dist/corpus/runtime-match.d.ts +19 -0
- package/dist/corpus/runtime-match.js +43 -0
- package/dist/corpus/standing-allow-catalog.generated.d.ts +13 -0
- package/dist/corpus/standing-allow-catalog.generated.js +99 -0
- package/dist/corpus/types.d.ts +47 -0
- package/dist/corpus/types.js +157 -0
- package/dist/installer.js +4 -1
- package/dist/judge-broker-daemon.d.ts +1 -0
- package/dist/judge-broker-daemon.js +137 -0
- package/dist/services/sandbox-service.d.ts +5 -0
- package/dist/services/sandbox-service.js +30 -2
- package/dist/types.d.ts +2 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -3
- package/skills/belay/SKILL.md +7 -1
- package/skills/belay/belay-approve.md +17 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const INFRASTRUCTURE_FALLBACK_SUFFIXES = [
|
|
2
|
+
'_cli_nonzero',
|
|
3
|
+
'_cli_unavailable',
|
|
4
|
+
'_cli_parse_error',
|
|
5
|
+
'_cli_spawn_error',
|
|
6
|
+
];
|
|
7
|
+
const INFRASTRUCTURE_FALLBACK_REASONS = new Set([
|
|
8
|
+
'eval_timeout',
|
|
9
|
+
'connect_timeout',
|
|
10
|
+
'parse_timeout',
|
|
11
|
+
'session_unavailable',
|
|
12
|
+
'session_disabled',
|
|
13
|
+
'provider_not_allowlisted',
|
|
14
|
+
'guard_reset',
|
|
15
|
+
'kill_switch',
|
|
16
|
+
'non_json_response',
|
|
17
|
+
'parse_error',
|
|
18
|
+
'cli_error',
|
|
19
|
+
'timeout',
|
|
20
|
+
'version_mismatch',
|
|
21
|
+
'shadow_forced_spawn',
|
|
22
|
+
'unsafe_option_rejected',
|
|
23
|
+
'broker_busy_reset',
|
|
24
|
+
]);
|
|
25
|
+
function isInfrastructureFallbackReason(reason) {
|
|
26
|
+
if (INFRASTRUCTURE_FALLBACK_REASONS.has(reason)) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
return INFRASTRUCTURE_FALLBACK_SUFFIXES.some((suffix) => reason.endsWith(suffix));
|
|
30
|
+
}
|
|
31
|
+
function infrastructureSignal(signal) {
|
|
32
|
+
return isInfrastructureFallbackReason(signal);
|
|
33
|
+
}
|
|
34
|
+
export function extractJudgeFallbackReason(result) {
|
|
35
|
+
if (result.axes?.judgeFallbackReason) {
|
|
36
|
+
return result.axes.judgeFallbackReason;
|
|
37
|
+
}
|
|
38
|
+
return result.assessment.signals.find((signal) => isInfrastructureFallbackReason(signal));
|
|
39
|
+
}
|
|
40
|
+
export function isJudgeInfrastructureFailure(result) {
|
|
41
|
+
if (result.axes?.judgeProvider === 'fallback') {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
const fallbackReason = extractJudgeFallbackReason(result);
|
|
45
|
+
if (fallbackReason && isInfrastructureFallbackReason(fallbackReason)) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
return result.assessment.signals.some(infrastructureSignal);
|
|
49
|
+
}
|
|
50
|
+
export function inferProviderIdFromFallbackReason(fallbackReason, fallbackProviderId = 'cursor') {
|
|
51
|
+
if (!fallbackReason) {
|
|
52
|
+
return fallbackProviderId;
|
|
53
|
+
}
|
|
54
|
+
const match = fallbackReason.match(/^(cursor|claude|codex)_/);
|
|
55
|
+
return match?.[1] ?? fallbackProviderId;
|
|
56
|
+
}
|
|
57
|
+
export function formatJudgeRecoveryHint(providerId, fallbackReason) {
|
|
58
|
+
if (!fallbackReason) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
if (fallbackReason.endsWith('_cli_nonzero') || fallbackReason.endsWith('_cli_unavailable')) {
|
|
62
|
+
switch (providerId) {
|
|
63
|
+
case 'cursor':
|
|
64
|
+
return 'Judge CLI is not authenticated. Run: agent login';
|
|
65
|
+
case 'claude':
|
|
66
|
+
return 'Claude CLI failed. Run: claude auth login';
|
|
67
|
+
case 'codex':
|
|
68
|
+
return 'Codex CLI failed. Check codex auth';
|
|
69
|
+
default:
|
|
70
|
+
return `Judge CLI transport failed (${fallbackReason}). Check ${providerId} CLI auth`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (fallbackReason.endsWith('_cli_parse_error')) {
|
|
74
|
+
return `Judge CLI returned an invalid response (${fallbackReason}). Retry after updating the ${providerId} CLI`;
|
|
75
|
+
}
|
|
76
|
+
if (fallbackReason.endsWith('_cli_spawn_error')) {
|
|
77
|
+
return `Judge CLI could not be started (${fallbackReason}). Install or repair the ${providerId} CLI`;
|
|
78
|
+
}
|
|
79
|
+
switch (fallbackReason) {
|
|
80
|
+
case 'eval_timeout':
|
|
81
|
+
case 'connect_timeout':
|
|
82
|
+
case 'parse_timeout':
|
|
83
|
+
case 'timeout':
|
|
84
|
+
return 'Judge timed out. Check network or increase judge.timeoutMs';
|
|
85
|
+
case 'session_unavailable':
|
|
86
|
+
case 'session_disabled':
|
|
87
|
+
return 'Judge session transport is unavailable. Run: belay doctor';
|
|
88
|
+
case 'kill_switch':
|
|
89
|
+
return 'Judge session kill switch is active. Run: belay judge status';
|
|
90
|
+
default:
|
|
91
|
+
if (isInfrastructureFallbackReason(fallbackReason)) {
|
|
92
|
+
return `Judge transport failed (${fallbackReason}). Run: belay doctor`;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export function formatJudgeInfrastructureDenyMessage(params) {
|
|
98
|
+
const fallbackReason = params.fallbackReason ?? 'judge_transport_unavailable';
|
|
99
|
+
const recoveryHint = formatJudgeRecoveryHint(params.providerId, fallbackReason) ??
|
|
100
|
+
'Judge transport is unavailable. Run: belay doctor';
|
|
101
|
+
const commandHint = params.command
|
|
102
|
+
? ` For details: belay explain --command "${params.command}"`
|
|
103
|
+
: ' For details: belay explain';
|
|
104
|
+
return {
|
|
105
|
+
user_message: `Belay could not reach the Tier1 judge (${fallbackReason}). ${recoveryHint}. Then retry.${commandHint}`,
|
|
106
|
+
agent_message: `Belay denied this action because the Tier1 judge transport failed (${fallbackReason}). Repair judge infrastructure, then retry the exact same action.`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -5,8 +5,20 @@
|
|
|
5
5
|
* fs-scope allowlist matching).
|
|
6
6
|
*/
|
|
7
7
|
export declare function canonicalPath(targetPath: string): string;
|
|
8
|
+
export declare function isPathOutsideRoot(relativePath: string): boolean;
|
|
8
9
|
export declare function pathWithinRoot(root: string, targetPath: string): boolean;
|
|
9
10
|
export declare function relativeWithinRepo(repoRoot: string, targetPath: string): string | null;
|
|
11
|
+
export type WorkspaceRootMatch = {
|
|
12
|
+
kind: 'repo';
|
|
13
|
+
root: string;
|
|
14
|
+
relativePath: string;
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'trusted';
|
|
17
|
+
root: string;
|
|
18
|
+
relativePath: string;
|
|
19
|
+
};
|
|
20
|
+
export declare function resolveWorkspaceRootMatch(repoRoot: string, trustedRoots: string[] | undefined, targetPath: string): WorkspaceRootMatch | null;
|
|
10
21
|
export declare function normalizeToken(token: string, repoRoot: string): string;
|
|
11
22
|
export declare function resolveMutationTarget(token: string, cwd: string): string | null;
|
|
12
23
|
export declare function hasOutsideRepoPath(tokens: string[], cwd: string, repoRoot: string): boolean;
|
|
24
|
+
export declare function containingGitRoot(targetPath: string): string | null;
|
package/dist/core/path-utils.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { existsSync, realpathSync } from 'node:fs';
|
|
1
|
+
import { existsSync, realpathSync, statSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { isFdDuplication, isRedirectOperator } from './shell-tokenizer.js';
|
|
3
4
|
/**
|
|
4
5
|
* Resolve symlinks for the longest existing prefix of `targetPath`, then append
|
|
5
6
|
* any non-existent suffix without further resolution. Keeps path comparisons
|
|
@@ -35,6 +36,9 @@ export function canonicalPath(targetPath) {
|
|
|
35
36
|
}
|
|
36
37
|
return current;
|
|
37
38
|
}
|
|
39
|
+
export function isPathOutsideRoot(relativePath) {
|
|
40
|
+
return relativePath === '..' || relativePath.startsWith(`..${path.sep}`);
|
|
41
|
+
}
|
|
38
42
|
export function pathWithinRoot(root, targetPath) {
|
|
39
43
|
const resolvedRoot = canonicalPath(root);
|
|
40
44
|
const resolvedTarget = canonicalPath(targetPath);
|
|
@@ -42,20 +46,40 @@ export function pathWithinRoot(root, targetPath) {
|
|
|
42
46
|
if (relativePath === '') {
|
|
43
47
|
return true;
|
|
44
48
|
}
|
|
45
|
-
return !relativePath
|
|
49
|
+
return !isPathOutsideRoot(relativePath) && !path.isAbsolute(relativePath);
|
|
46
50
|
}
|
|
47
|
-
|
|
48
|
-
const resolvedRoot = canonicalPath(
|
|
51
|
+
function relativeWithinRoot(root, targetPath, options = {}) {
|
|
52
|
+
const resolvedRoot = options.canonicalizeRoot === false ? path.resolve(root) : canonicalPath(root);
|
|
49
53
|
const resolvedTarget = canonicalPath(targetPath);
|
|
50
54
|
const relativePath = path.relative(resolvedRoot, resolvedTarget);
|
|
51
55
|
if (relativePath === '') {
|
|
52
56
|
return '.';
|
|
53
57
|
}
|
|
54
|
-
if (relativePath.
|
|
58
|
+
if (isPathOutsideRoot(relativePath) || path.isAbsolute(relativePath)) {
|
|
55
59
|
return null;
|
|
56
60
|
}
|
|
57
61
|
return relativePath;
|
|
58
62
|
}
|
|
63
|
+
export function relativeWithinRepo(repoRoot, targetPath) {
|
|
64
|
+
return relativeWithinRoot(repoRoot, targetPath);
|
|
65
|
+
}
|
|
66
|
+
export function resolveWorkspaceRootMatch(repoRoot, trustedRoots = [], targetPath) {
|
|
67
|
+
const canonicalRepoRoot = canonicalPath(repoRoot);
|
|
68
|
+
const repoRelative = relativeWithinRoot(repoRoot, targetPath);
|
|
69
|
+
if (repoRelative !== null) {
|
|
70
|
+
return { kind: 'repo', root: canonicalRepoRoot, relativePath: repoRelative };
|
|
71
|
+
}
|
|
72
|
+
const normalizedTrustedRoots = [...new Set(trustedRoots.map((root) => path.resolve(root)))]
|
|
73
|
+
.filter((root) => root !== canonicalRepoRoot)
|
|
74
|
+
.sort((left, right) => right.length - left.length);
|
|
75
|
+
for (const root of normalizedTrustedRoots) {
|
|
76
|
+
const trustedRelative = relativeWithinRoot(root, targetPath, { canonicalizeRoot: false });
|
|
77
|
+
if (trustedRelative !== null) {
|
|
78
|
+
return { kind: 'trusted', root, relativePath: trustedRelative };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
59
83
|
export function normalizeToken(token, repoRoot) {
|
|
60
84
|
if (!path.isAbsolute(token)) {
|
|
61
85
|
return token;
|
|
@@ -67,7 +91,14 @@ export function resolveMutationTarget(token, cwd) {
|
|
|
67
91
|
if (!token || token === '--' || token.startsWith('-')) {
|
|
68
92
|
return null;
|
|
69
93
|
}
|
|
70
|
-
if (token
|
|
94
|
+
if (isRedirectOperator(token) ||
|
|
95
|
+
isFdDuplication(token) ||
|
|
96
|
+
token === '&&' ||
|
|
97
|
+
token === '||' ||
|
|
98
|
+
token === '|' ||
|
|
99
|
+
token === '|&' ||
|
|
100
|
+
token === ';' ||
|
|
101
|
+
token === '&') {
|
|
71
102
|
return null;
|
|
72
103
|
}
|
|
73
104
|
if (path.isAbsolute(token)) {
|
|
@@ -102,6 +133,28 @@ export function hasOutsideRepoPath(tokens, cwd, repoRoot) {
|
|
|
102
133
|
if (!resolved) {
|
|
103
134
|
return false;
|
|
104
135
|
}
|
|
105
|
-
return
|
|
136
|
+
return resolveWorkspaceRootMatch(repoRoot, [], resolved) === null;
|
|
106
137
|
});
|
|
107
138
|
}
|
|
139
|
+
export function containingGitRoot(targetPath) {
|
|
140
|
+
const resolved = canonicalPath(targetPath);
|
|
141
|
+
let current = resolved;
|
|
142
|
+
try {
|
|
143
|
+
if (!statSync(current).isDirectory()) {
|
|
144
|
+
current = path.dirname(current);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
current = path.dirname(current);
|
|
149
|
+
}
|
|
150
|
+
while (true) {
|
|
151
|
+
if (existsSync(path.join(current, '.git'))) {
|
|
152
|
+
return current;
|
|
153
|
+
}
|
|
154
|
+
const parent = path.dirname(current);
|
|
155
|
+
if (parent === current) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
current = parent;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -6,6 +6,8 @@ export interface ReclassifyDiff {
|
|
|
6
6
|
event?: string;
|
|
7
7
|
summary?: string;
|
|
8
8
|
fingerprint?: string;
|
|
9
|
+
replayCwd?: string;
|
|
10
|
+
replayKind?: string;
|
|
9
11
|
previousVerdict: string;
|
|
10
12
|
previousReason: string;
|
|
11
13
|
nextVerdict: string;
|
|
@@ -13,3 +15,4 @@ export interface ReclassifyDiff {
|
|
|
13
15
|
}
|
|
14
16
|
export declare function reclassifyAuditRecord(record: AuditRecord, config: BelayConfigV3, repoRoot: string): Promise<ClassifyResult | null>;
|
|
15
17
|
export declare function diffReclassification(record: AuditRecord, config: BelayConfigV3, repoRoot: string): Promise<ReclassifyDiff | null>;
|
|
18
|
+
export declare function countMissingActionSnapshots(records: AuditRecord[]): number;
|
package/dist/core/reclassify.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getAdapter } from '../adapters/registry.js';
|
|
2
2
|
import { repoShellClassifierOptions } from '../adapters/shared/gate-runtime.js';
|
|
3
3
|
import { detectAdapterName } from '../config-io.js';
|
|
4
|
+
import { hashReplayPayload, parseAuditActionSnapshot, parseAuditReplayContext, } from './audit-replay-context.js';
|
|
4
5
|
import { GATE_EVENTS } from './audit-types.js';
|
|
5
6
|
import { classifyGatedAction, normalizeGatedAction } from './gate-engine.js';
|
|
6
7
|
function shellCommandFromSummary(summary) {
|
|
@@ -11,47 +12,68 @@ function classifierOptionsForRepo(config, repoRoot) {
|
|
|
11
12
|
const adapter = getAdapter(config.adapter ?? detectAdapterName(repoRoot));
|
|
12
13
|
return repoShellClassifierOptions(config, repoRoot, adapter.layout);
|
|
13
14
|
}
|
|
15
|
+
function trustedReplayContext(snapshot, replay) {
|
|
16
|
+
if (!replay) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
if (snapshot?.payloadHash &&
|
|
20
|
+
replay.payload &&
|
|
21
|
+
hashReplayPayload(replay.payload) !== snapshot.payloadHash) {
|
|
22
|
+
return { ...replay, payload: undefined };
|
|
23
|
+
}
|
|
24
|
+
return replay;
|
|
25
|
+
}
|
|
14
26
|
export async function reclassifyAuditRecord(record, config, repoRoot) {
|
|
15
27
|
if (!record.event || !GATE_EVENTS.has(record.event)) {
|
|
16
28
|
return null;
|
|
17
29
|
}
|
|
18
|
-
const
|
|
30
|
+
const snapshot = parseAuditActionSnapshot(record);
|
|
31
|
+
const replay = trustedReplayContext(snapshot, parseAuditReplayContext(record));
|
|
32
|
+
const kind = snapshot?.kind ??
|
|
33
|
+
replay?.kind ??
|
|
34
|
+
(record.kind === 'tool' || record.kind === 'subagent' ? record.kind : 'shell');
|
|
19
35
|
const summary = record.summary ?? '';
|
|
36
|
+
const cwd = snapshot?.cwd ?? replay?.cwd ?? repoRoot;
|
|
20
37
|
try {
|
|
21
38
|
if (kind === 'shell') {
|
|
22
|
-
const command = shellCommandFromSummary(summary);
|
|
39
|
+
const command = snapshot?.normalizedAction ?? replay?.command ?? shellCommandFromSummary(summary);
|
|
23
40
|
if (!command) {
|
|
24
41
|
return null;
|
|
25
42
|
}
|
|
26
43
|
const action = normalizeGatedAction({
|
|
27
44
|
kind: 'shell',
|
|
28
45
|
repoRoot,
|
|
29
|
-
cwd
|
|
46
|
+
cwd,
|
|
30
47
|
command,
|
|
31
48
|
});
|
|
32
49
|
return await classifyGatedAction(action, config, classifierOptionsForRepo(config, repoRoot));
|
|
33
50
|
}
|
|
34
51
|
if (kind === 'subagent') {
|
|
52
|
+
const payload = replay?.payload ??
|
|
53
|
+
{
|
|
54
|
+
tool_name: 'Task',
|
|
55
|
+
tool_input: { description: summary },
|
|
56
|
+
};
|
|
35
57
|
const action = normalizeGatedAction({
|
|
36
58
|
kind: 'subagent',
|
|
37
59
|
repoRoot,
|
|
38
|
-
cwd
|
|
39
|
-
payload
|
|
40
|
-
tool_name: 'Task',
|
|
41
|
-
tool_input: { description: summary },
|
|
42
|
-
},
|
|
60
|
+
cwd,
|
|
61
|
+
payload,
|
|
43
62
|
});
|
|
44
63
|
return await classifyGatedAction(action, config, classifierOptionsForRepo(config, repoRoot));
|
|
45
64
|
}
|
|
65
|
+
const toolName = snapshot?.toolName ?? replay?.toolName ?? 'Shell';
|
|
66
|
+
const payload = replay?.payload ??
|
|
67
|
+
{
|
|
68
|
+
tool_name: toolName,
|
|
69
|
+
tool_input: { command: snapshot?.normalizedAction ?? replay?.command ?? summary },
|
|
70
|
+
};
|
|
46
71
|
const action = normalizeGatedAction({
|
|
47
72
|
kind: 'tool',
|
|
48
73
|
repoRoot,
|
|
49
|
-
cwd
|
|
50
|
-
toolName
|
|
51
|
-
payload
|
|
52
|
-
tool_name: 'Shell',
|
|
53
|
-
tool_input: { command: summary },
|
|
54
|
-
},
|
|
74
|
+
cwd,
|
|
75
|
+
toolName,
|
|
76
|
+
payload,
|
|
55
77
|
});
|
|
56
78
|
return await classifyGatedAction(action, config, classifierOptionsForRepo(config, repoRoot));
|
|
57
79
|
}
|
|
@@ -69,14 +91,22 @@ export async function diffReclassification(record, config, repoRoot) {
|
|
|
69
91
|
if (previousVerdict === next.verdict && previousReason === next.reason) {
|
|
70
92
|
return null;
|
|
71
93
|
}
|
|
94
|
+
const snapshot = parseAuditActionSnapshot(record);
|
|
95
|
+
const replay = parseAuditReplayContext(record);
|
|
72
96
|
return {
|
|
73
97
|
timestamp: record.timestamp,
|
|
74
98
|
event: record.event,
|
|
75
99
|
summary: record.summary,
|
|
76
100
|
fingerprint: record.fingerprint,
|
|
101
|
+
...(snapshot || replay
|
|
102
|
+
? { replayCwd: snapshot?.cwd ?? replay?.cwd, replayKind: snapshot?.kind ?? replay?.kind }
|
|
103
|
+
: {}),
|
|
77
104
|
previousVerdict,
|
|
78
105
|
previousReason,
|
|
79
106
|
nextVerdict: next.verdict,
|
|
80
107
|
nextReason: next.reason,
|
|
81
108
|
};
|
|
82
109
|
}
|
|
110
|
+
export function countMissingActionSnapshots(records) {
|
|
111
|
+
return records.filter((record) => record.event && GATE_EVENTS.has(record.event) && !parseAuditActionSnapshot(record)).length;
|
|
112
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const NON_RECOVERABLE_ACTIONS = new Set([
|
|
2
|
+
'network.connect',
|
|
3
|
+
'git.ref.write',
|
|
4
|
+
'control_plane.write',
|
|
5
|
+
'secret.read',
|
|
6
|
+
'indeterminate',
|
|
7
|
+
]);
|
|
8
|
+
/** Actions that cannot be closed by local git-worktree / file checkpoint recovery. */
|
|
9
|
+
export function capabilityRequestsBlockRecovery(requests) {
|
|
10
|
+
if (!requests?.length) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
for (const request of requests) {
|
|
14
|
+
if (NON_RECOVERABLE_ACTIONS.has(request.action)) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
if (request.evidence.level === 'indeterminate') {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ClassifyResult } from '../types.js';
|
|
2
|
+
export declare const RECOVERY_SUBSTRATE_UNAVAILABLE = "recovery_substrate_unavailable";
|
|
3
|
+
export declare const RECOVERY_EXECUTION_FAILED = "recovery_execution_failed";
|
|
4
|
+
export declare const RECOVERY_DIRTY_WORKTREE = "recovery_dirty_worktree";
|
|
5
|
+
export declare function recoveryFailReasonFromSkip(skipReason: string): string;
|
|
6
|
+
export declare function recoveryFailClosedResult(predicted: ClassifyResult, reason: string, signals?: string[]): ClassifyResult;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const RECOVERY_SUBSTRATE_UNAVAILABLE = 'recovery_substrate_unavailable';
|
|
2
|
+
export const RECOVERY_EXECUTION_FAILED = 'recovery_execution_failed';
|
|
3
|
+
export const RECOVERY_DIRTY_WORKTREE = 'recovery_dirty_worktree';
|
|
4
|
+
export function recoveryFailReasonFromSkip(skipReason) {
|
|
5
|
+
if (skipReason === 'transactional_command_failed' || skipReason === 'transactional_timed_out') {
|
|
6
|
+
return RECOVERY_EXECUTION_FAILED;
|
|
7
|
+
}
|
|
8
|
+
if (skipReason === 'dirty_worktree') {
|
|
9
|
+
return RECOVERY_DIRTY_WORKTREE;
|
|
10
|
+
}
|
|
11
|
+
return RECOVERY_SUBSTRATE_UNAVAILABLE;
|
|
12
|
+
}
|
|
13
|
+
export function recoveryFailClosedResult(predicted, reason, signals = []) {
|
|
14
|
+
return {
|
|
15
|
+
...predicted,
|
|
16
|
+
verdict: 'deny_pending_approval',
|
|
17
|
+
reason,
|
|
18
|
+
assessment: {
|
|
19
|
+
...predicted.assessment,
|
|
20
|
+
reversibility: 'irreversible',
|
|
21
|
+
confidence: 1,
|
|
22
|
+
signals: [...predicted.assessment.signals, 'recovery_fail_closed', ...signals],
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { capabilityRequestsBlockRecovery } from './capability.js';
|
|
2
|
+
export { RECOVERY_DIRTY_WORKTREE, RECOVERY_EXECUTION_FAILED, RECOVERY_SUBSTRATE_UNAVAILABLE, recoveryFailClosedResult, recoveryFailReasonFromSkip, } from './fail-closed.js';
|
|
3
|
+
export type { RecoveryBackend, RecoveryProofContext, RecoveryProofV1 } from './types.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { CapabilityRequestV1 } from '../capability/request.js';
|
|
2
|
+
/** Backends that can mint a verifiable RecoveryProof (ADR: proof-only execution). */
|
|
3
|
+
export type RecoveryBackend = 'git_worktree' | 'file_checkpoint';
|
|
4
|
+
export interface RecoveryProofV1 {
|
|
5
|
+
version: 1;
|
|
6
|
+
backend: RecoveryBackend;
|
|
7
|
+
inputFingerprint: string;
|
|
8
|
+
resourceScope: string;
|
|
9
|
+
baseStateHash: string;
|
|
10
|
+
effectClosure: 'repo_local_fs';
|
|
11
|
+
issuedAt: string;
|
|
12
|
+
expiresAt: string;
|
|
13
|
+
probeSignals: string[];
|
|
14
|
+
}
|
|
15
|
+
export interface RecoveryProofContext {
|
|
16
|
+
repoRoot: string;
|
|
17
|
+
cwd: string;
|
|
18
|
+
command: string;
|
|
19
|
+
requests: CapabilityRequestV1[];
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { GatedActionKind } from './gate-contract.js';
|
|
2
|
+
import type { ScrubOptions } from './types.js';
|
|
3
|
+
/** Subagent fingerprint input — must match classify-subagent `fingerprintSource`. */
|
|
4
|
+
export declare function subagentFingerprintSource(payload: Record<string, unknown>, scrubOptions: ScrubOptions): unknown;
|
|
5
|
+
/**
|
|
6
|
+
* Scrubbed payload used for replay envelope hashing — aligned with classifier fingerprints.
|
|
7
|
+
* Tool: scrubbed `tool_input`. Subagent: description/prompt subset. Other: full payload.
|
|
8
|
+
*/
|
|
9
|
+
export declare function fingerprintReplayPayload(kind: GatedActionKind, payload: Record<string, unknown> | undefined, scrubOptions: ScrubOptions): Record<string, unknown> | undefined;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { scrubValue } from './scrub.js';
|
|
2
|
+
/** Subagent fingerprint input — must match classify-subagent `fingerprintSource`. */
|
|
3
|
+
export function subagentFingerprintSource(payload, scrubOptions) {
|
|
4
|
+
const toolInput = payload.tool_input;
|
|
5
|
+
if (toolInput && typeof toolInput === 'object') {
|
|
6
|
+
const input = toolInput;
|
|
7
|
+
return scrubValue({
|
|
8
|
+
description: input.description ?? '',
|
|
9
|
+
prompt: input.prompt ?? '',
|
|
10
|
+
}, scrubOptions);
|
|
11
|
+
}
|
|
12
|
+
const task = payload.task;
|
|
13
|
+
if (typeof task === 'string') {
|
|
14
|
+
return scrubValue({ task }, scrubOptions);
|
|
15
|
+
}
|
|
16
|
+
if (task && typeof task === 'object') {
|
|
17
|
+
const taskObj = task;
|
|
18
|
+
return scrubValue({
|
|
19
|
+
description: taskObj.description ?? '',
|
|
20
|
+
prompt: taskObj.prompt ?? '',
|
|
21
|
+
}, scrubOptions);
|
|
22
|
+
}
|
|
23
|
+
return scrubValue(payload, scrubOptions);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Scrubbed payload used for replay envelope hashing — aligned with classifier fingerprints.
|
|
27
|
+
* Tool: scrubbed `tool_input`. Subagent: description/prompt subset. Other: full payload.
|
|
28
|
+
*/
|
|
29
|
+
export function fingerprintReplayPayload(kind, payload, scrubOptions) {
|
|
30
|
+
if (!payload) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
if (kind === 'tool') {
|
|
34
|
+
const toolInput = payload.tool_input;
|
|
35
|
+
if (toolInput && typeof toolInput === 'object') {
|
|
36
|
+
return scrubValue(toolInput, scrubOptions);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (kind === 'subagent') {
|
|
40
|
+
return subagentFingerprintSource(payload, scrubOptions);
|
|
41
|
+
}
|
|
42
|
+
return scrubValue(payload, scrubOptions);
|
|
43
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export declare function isRedirectOperator(token: string): boolean;
|
|
2
|
+
export declare function isFdDuplication(token: string): boolean;
|
|
1
3
|
export declare function tokenizeShell(input: string): string[];
|
|
2
4
|
export declare function normalizeShellCommand(command: string, repoRoot: string, normalizeToken: (t: string, r: string) => string): string;
|
|
3
5
|
export declare function splitTopLevelSegments(tokens: string[]): string[][];
|
|
@@ -1,4 +1,72 @@
|
|
|
1
1
|
const ENV_PREFIX_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*=(?:'[^']*'|"[^"]*"|\S+)$/;
|
|
2
|
+
const FD_DUPLICATION_PATTERN = /^\d+[<>]&(?:\d+|-)$/;
|
|
3
|
+
const FD_REDIRECT_PATTERN = /^\d+(?:>>?|<)$/;
|
|
4
|
+
function readDigits(input, index) {
|
|
5
|
+
let end = index;
|
|
6
|
+
while (end < input.length && /[0-9]/.test(input[end] ?? '')) {
|
|
7
|
+
end += 1;
|
|
8
|
+
}
|
|
9
|
+
return input.slice(index, end);
|
|
10
|
+
}
|
|
11
|
+
// This tokenizer only recognizes FD redirects when the digit run is immediately
|
|
12
|
+
// followed by `>` or `<` (for example `2>&1`, `3>file`, `12>>log`, `3<file`,
|
|
13
|
+
// `3<&1`). It does not try to recover bash's full word-boundary rules for cases
|
|
14
|
+
// like `foo12>>bar` or `foo12<bar`.
|
|
15
|
+
function readShellOperator(input, index) {
|
|
16
|
+
const char = input[index];
|
|
17
|
+
const next = input[index + 1] ?? '';
|
|
18
|
+
const digits = /[0-9]/.test(char) ? readDigits(input, index) : '';
|
|
19
|
+
const digitsLength = digits.length;
|
|
20
|
+
const afterDigits = digitsLength > 0 ? (input[index + digitsLength] ?? '') : '';
|
|
21
|
+
const afterDigitsNext = digitsLength > 0 ? (input[index + digitsLength + 1] ?? '') : '';
|
|
22
|
+
if (char === '&' && next === '&') {
|
|
23
|
+
return { token: '&&', length: 2 };
|
|
24
|
+
}
|
|
25
|
+
if (char === '|' && next === '|') {
|
|
26
|
+
return { token: '||', length: 2 };
|
|
27
|
+
}
|
|
28
|
+
if (char === '|' && next === '&') {
|
|
29
|
+
return { token: '|&', length: 2 };
|
|
30
|
+
}
|
|
31
|
+
if (char === '&' && next === '>') {
|
|
32
|
+
return { token: '&>', length: 2 };
|
|
33
|
+
}
|
|
34
|
+
if (digitsLength > 0 && (afterDigits === '>' || afterDigits === '<')) {
|
|
35
|
+
if (afterDigitsNext === '&') {
|
|
36
|
+
let end = index + digitsLength + 2;
|
|
37
|
+
while (end < input.length && /[0-9-]/.test(input[end] ?? '')) {
|
|
38
|
+
end += 1;
|
|
39
|
+
}
|
|
40
|
+
if (end > index + digitsLength + 2) {
|
|
41
|
+
return { token: input.slice(index, end), length: end - index };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (afterDigits === '>' && afterDigitsNext === '>') {
|
|
45
|
+
return { token: `${digits}>>`, length: digitsLength + 2 };
|
|
46
|
+
}
|
|
47
|
+
return { token: `${digits}${afterDigits}`, length: digitsLength + 1 };
|
|
48
|
+
}
|
|
49
|
+
if (char === '>' && next === '>') {
|
|
50
|
+
return { token: '>>', length: 2 };
|
|
51
|
+
}
|
|
52
|
+
if (char === '&') {
|
|
53
|
+
return { token: '&', length: 1 };
|
|
54
|
+
}
|
|
55
|
+
if (char === '|' || char === ';' || char === '>' || char === '<') {
|
|
56
|
+
return { token: char, length: 1 };
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
export function isRedirectOperator(token) {
|
|
61
|
+
return (token === '>' ||
|
|
62
|
+
token === '>>' ||
|
|
63
|
+
token === '<' ||
|
|
64
|
+
token === '&>' ||
|
|
65
|
+
FD_REDIRECT_PATTERN.test(token));
|
|
66
|
+
}
|
|
67
|
+
export function isFdDuplication(token) {
|
|
68
|
+
return FD_DUPLICATION_PATTERN.test(token);
|
|
69
|
+
}
|
|
2
70
|
export function tokenizeShell(input) {
|
|
3
71
|
const tokens = [];
|
|
4
72
|
let buffer = '';
|
|
@@ -12,7 +80,6 @@ export function tokenizeShell(input) {
|
|
|
12
80
|
};
|
|
13
81
|
for (let index = 0; index < input.length; index += 1) {
|
|
14
82
|
const char = input[index];
|
|
15
|
-
const next = input[index + 1] ?? '';
|
|
16
83
|
if (escaping) {
|
|
17
84
|
buffer += char;
|
|
18
85
|
escaping = false;
|
|
@@ -35,27 +102,11 @@ export function tokenizeShell(input) {
|
|
|
35
102
|
quote = char;
|
|
36
103
|
continue;
|
|
37
104
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
tokens.push('&&');
|
|
41
|
-
index += 1;
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
if (char === '|' && next === '|') {
|
|
45
|
-
flush();
|
|
46
|
-
tokens.push('||');
|
|
47
|
-
index += 1;
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
if (char === '>' && next === '>') {
|
|
105
|
+
const operator = readShellOperator(input, index);
|
|
106
|
+
if (operator) {
|
|
51
107
|
flush();
|
|
52
|
-
tokens.push(
|
|
53
|
-
index += 1;
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
if (char === '|' || char === ';' || char === '>' || char === '<') {
|
|
57
|
-
flush();
|
|
58
|
-
tokens.push(char);
|
|
108
|
+
tokens.push(operator.token);
|
|
109
|
+
index += operator.length - 1;
|
|
59
110
|
continue;
|
|
60
111
|
}
|
|
61
112
|
if (char === '\n' || char === '\r') {
|
|
@@ -84,7 +135,12 @@ export function splitTopLevelSegments(tokens) {
|
|
|
84
135
|
const segments = [];
|
|
85
136
|
let current = [];
|
|
86
137
|
for (const token of tokens) {
|
|
87
|
-
if (token === '&&' ||
|
|
138
|
+
if (token === '&&' ||
|
|
139
|
+
token === '||' ||
|
|
140
|
+
token === ';' ||
|
|
141
|
+
token === '|' ||
|
|
142
|
+
token === '&' ||
|
|
143
|
+
token === '|&') {
|
|
88
144
|
if (current.length > 0) {
|
|
89
145
|
segments.push(current);
|
|
90
146
|
}
|
|
@@ -118,7 +174,10 @@ export function extractRedirectTargets(tokens) {
|
|
|
118
174
|
const targets = [];
|
|
119
175
|
for (let index = 0; index < tokens.length; index += 1) {
|
|
120
176
|
const token = tokens[index];
|
|
121
|
-
if (token
|
|
177
|
+
if (isFdDuplication(token)) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (isRedirectOperator(token)) {
|
|
122
181
|
const next = tokens[index + 1];
|
|
123
182
|
if (next) {
|
|
124
183
|
targets.push(next);
|