@guilz-dev/belay 0.8.1 → 0.9.1
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 +67 -11
- package/dist/adapters/claude/runtime-entry.js +2 -0
- package/dist/adapters/codex/runtime-entry.js +2 -0
- package/dist/adapters/cursor/hooks.d.ts +5 -1
- package/dist/adapters/cursor/hooks.js +37 -2
- package/dist/adapters/cursor/runtime-entry.js +5 -0
- package/dist/adapters/shared/gate-runtime.d.ts +6 -0
- package/dist/adapters/shared/gate-runtime.js +260 -59
- package/dist/bundle/claude-runtime.mjs +7068 -3966
- package/dist/bundle/codex-runtime.mjs +7001 -3899
- package/dist/bundle/cursor-runtime.mjs +7004 -3899
- package/dist/commands/classify-for-report.js +5 -0
- package/dist/commands/doctor.js +88 -9
- package/dist/commands/explain.js +3 -2
- package/dist/commands/metrics.js +42 -0
- package/dist/commands/recovery-checkpoints.d.ts +8 -1
- package/dist/commands/recovery-checkpoints.js +44 -11
- package/dist/commands/session.d.ts +5 -0
- package/dist/commands/session.js +8 -1
- package/dist/config-io.js +2 -2
- package/dist/conformance/contained-execution-guarantee.d.ts +121 -0
- package/dist/conformance/contained-execution-guarantee.js +121 -0
- package/dist/conformance/guarantee-table.js +7 -0
- package/dist/core/audit-analysis.js +7 -4
- package/dist/core/audit-io.d.ts +1 -0
- package/dist/core/audit-io.js +16 -6
- package/dist/core/audit-legacy-archive.d.ts +6 -0
- package/dist/core/audit-legacy-archive.js +51 -0
- package/dist/core/audit-metrics.d.ts +15 -0
- package/dist/core/audit-metrics.js +16 -2
- package/dist/core/audit-query.d.ts +2 -0
- package/dist/core/audit-query.js +33 -9
- package/dist/core/audit-recovery-metrics.d.ts +33 -0
- package/dist/core/audit-recovery-metrics.js +146 -0
- package/dist/core/audit-serialize.d.ts +9 -0
- package/dist/core/audit-serialize.js +179 -0
- package/dist/core/audit-types.d.ts +8 -1
- package/dist/core/audit-types.js +1 -1
- package/dist/core/bounded-output.d.ts +11 -0
- package/dist/core/bounded-output.js +26 -0
- package/dist/core/capability/attestation.d.ts +56 -0
- package/dist/core/capability/attestation.js +110 -3
- package/dist/core/capability/boundary-session.d.ts +5 -0
- package/dist/core/capability/boundary-session.js +38 -1
- package/dist/core/capability/index.d.ts +1 -1
- package/dist/core/capability/index.js +1 -1
- package/dist/core/capability/policy-engine.js +32 -13
- package/dist/core/config.d.ts +18 -0
- package/dist/core/config.js +89 -10
- package/dist/core/contained-execution/docker-policy.d.ts +18 -0
- package/dist/core/contained-execution/docker-policy.js +97 -0
- package/dist/core/contained-execution/docker.d.ts +223 -0
- package/dist/core/contained-execution/docker.js +846 -0
- package/dist/core/contained-execution/eligibility.d.ts +9 -0
- package/dist/core/contained-execution/eligibility.js +103 -0
- package/dist/core/contained-execution/failure.d.ts +18 -0
- package/dist/core/contained-execution/failure.js +105 -0
- package/dist/core/contained-execution/mirror.d.ts +54 -0
- package/dist/core/contained-execution/mirror.js +587 -0
- package/dist/core/contained-execution/policy.d.ts +10 -0
- package/dist/core/contained-execution/policy.js +20 -0
- package/dist/core/decision-config-fingerprint.d.ts +3 -0
- package/dist/core/decision-config-fingerprint.js +6 -0
- package/dist/core/effect-ir/index.d.ts +1 -1
- package/dist/core/effect-ir/index.js +1 -1
- package/dist/core/effect-ir/normalize.d.ts +5 -0
- package/dist/core/effect-ir/normalize.js +17 -7
- package/dist/core/effect-ir/shell-build.js +2 -14
- package/dist/core/effect-ir/shell-lower.js +103 -25
- package/dist/core/gate-contract.d.ts +4 -1
- package/dist/core/gate-contract.js +2 -0
- package/dist/core/process-runner.d.ts +10 -3
- package/dist/core/process-runner.js +66 -10
- package/dist/core/recovery/checkpoint.d.ts +3 -0
- package/dist/core/recovery/checkpoint.js +33 -4
- package/dist/core/recovery/types.d.ts +1 -0
- package/dist/core/scrub.d.ts +17 -0
- package/dist/core/scrub.js +499 -5
- package/dist/core/standing-allow.d.ts +0 -20
- package/dist/core/standing-allow.js +0 -71
- package/dist/core/transactional/apply-observed-changes.d.ts +1 -0
- package/dist/core/transactional/apply-observed-changes.js +11 -0
- package/dist/core/transactional/backend-selector.d.ts +2 -1
- package/dist/core/transactional/backend-selector.js +32 -29
- package/dist/core/transactional/backend.d.ts +3 -0
- package/dist/core/transactional/file-checkpoint-backend.js +242 -54
- package/dist/core/transactional/file-checkpoint-git.d.ts +3 -0
- package/dist/core/transactional/file-checkpoint-git.js +53 -0
- package/dist/core/transactional/file-checkpoint-isolation.d.ts +3 -0
- package/dist/core/transactional/file-checkpoint-isolation.js +16 -0
- package/dist/core/transactional/git-worktree-backend.js +3 -0
- package/dist/core/transactional/git-worktree.d.ts +1 -0
- package/dist/core/transactional/git-worktree.js +1 -0
- package/dist/core/transactional/runner.js +59 -13
- package/dist/core/types.d.ts +13 -0
- package/dist/core/verdict/egress-classify.js +1 -0
- package/dist/core/verdict/parser.d.ts +10 -0
- package/dist/core/verdict/parser.js +271 -40
- package/dist/core/verdict/shell-semantics.js +21 -0
- package/dist/corpus/adversarial-probe.d.ts +1 -1
- package/dist/corpus/adversarial-probe.js +3 -3
- package/dist/corpus/benign-probe-cores.d.ts +6 -0
- package/dist/corpus/{must-allow-commands.js → benign-probe-cores.js} +8 -2
- package/dist/corpus/coverage-compare.d.ts +36 -0
- package/dist/corpus/coverage-compare.js +197 -0
- package/dist/corpus/coverage-contexts.d.ts +17 -0
- package/dist/corpus/coverage-contexts.js +88 -0
- package/dist/corpus/coverage-matrix.d.ts +37 -0
- package/dist/corpus/coverage-matrix.js +159 -0
- package/dist/corpus/coverage-probe.d.ts +91 -0
- package/dist/corpus/coverage-probe.js +341 -0
- package/dist/corpus/evaluate.d.ts +1 -1
- package/dist/corpus/evaluate.js +1 -1
- package/dist/corpus/structural-fixture-root.d.ts +2 -0
- package/dist/corpus/structural-fixture-root.js +5 -0
- package/dist/defaults.js +0 -8
- package/dist/egress-daemon.js +20 -4
- package/dist/installer.js +3 -0
- package/dist/runtime-provenance.d.ts +10 -0
- package/dist/runtime-provenance.js +63 -0
- package/dist/templates.js +12 -7
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -1
- package/skills/belay/SKILL.md +19 -0
- package/skills/belay/belay-status.md +5 -0
- package/dist/corpus/must-allow-commands.d.ts +0 -6
|
@@ -159,6 +159,7 @@ export async function applyWorktreeChanges(worktreePath, repoRoot, changes, opti
|
|
|
159
159
|
sourceRoot: worktreePath,
|
|
160
160
|
targetRoot: repoRoot,
|
|
161
161
|
changes: observed,
|
|
162
|
+
beforeMutation: options?.beforeMutation,
|
|
162
163
|
afterApply: options?.afterApply,
|
|
163
164
|
});
|
|
164
165
|
}
|
|
@@ -1,18 +1,33 @@
|
|
|
1
1
|
import { boundaryMountReadOnlyFromPrediction, runWithBoundaryRunnable, } from '../capability/boundary-run.js';
|
|
2
2
|
import { hostIntegrationBoundaryContext } from '../capability/boundary-session.js';
|
|
3
3
|
import { resolveGuestWorkdir } from '../capability/boundary-workspace-mount.js';
|
|
4
|
-
import {
|
|
4
|
+
import { canonicalPath, pathWithinRoot } from '../path-utils.js';
|
|
5
|
+
import { discardPreparedRecoveryCheckpoint, listRecoveryCheckpoints, markRecoveryCheckpointApplied, markRecoveryCheckpointApplying, markRecoveryCheckpointNeedsManualRepair, prepareRecoveryCheckpoint, reconcileRecoveryCheckpoint, } from '../recovery/checkpoint.js';
|
|
5
6
|
import { buildObservedChangesFromTransactional } from './apply-observed-changes.js';
|
|
6
7
|
import { selectTransactionalBackend } from './backend-selector.js';
|
|
7
8
|
import { evaluateTransactionalDiff } from './diff-evaluator.js';
|
|
8
9
|
import { FileCheckpointDiagnosticError } from './file-tree.js';
|
|
9
10
|
import { applyWorktreeChanges, resolveWorktreeCwd, TRANSACTIONAL_APPLY_ROLLBACK_FAILED, TRANSACTIONAL_APPLY_TOCTOU, } from './git-worktree.js';
|
|
10
11
|
import { TRANSACTIONAL_ALREADY_APPLIED, TRANSACTIONAL_APPLY_FAILED, TRANSACTIONAL_OBSERVED_RISK, } from './reasons.js';
|
|
12
|
+
const TRANSACTIONAL_RESOURCE_IDENTITY_CHANGED = 'transactional_resource_identity_changed';
|
|
13
|
+
function errorChainIncludes(error, message) {
|
|
14
|
+
const seen = new Set();
|
|
15
|
+
let current = error;
|
|
16
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
17
|
+
if (current.message === message)
|
|
18
|
+
return true;
|
|
19
|
+
seen.add(current);
|
|
20
|
+
current = current.cause;
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
11
24
|
export async function runTransactionalExecution(params) {
|
|
12
25
|
const { predicted, repoRoot, stateDir, command, cwd, timeoutMs, diffContext } = params;
|
|
26
|
+
const recoveryStateDir = canonicalPath(stateDir);
|
|
27
|
+
const recoveryStateOutsideResource = !pathWithinRoot(repoRoot, recoveryStateDir);
|
|
13
28
|
const backendContext = {
|
|
14
29
|
repoRoot,
|
|
15
|
-
stateDir,
|
|
30
|
+
stateDir: recoveryStateDir,
|
|
16
31
|
cwd,
|
|
17
32
|
dirtyIgnoreRoots: params.dirtyIgnoreRoots,
|
|
18
33
|
fileCheckpoint: params.fileCheckpoint,
|
|
@@ -32,11 +47,13 @@ export async function runTransactionalExecution(params) {
|
|
|
32
47
|
skipReason: skipReason ?? 'transactional_execution_failed',
|
|
33
48
|
predicted,
|
|
34
49
|
result: predicted,
|
|
50
|
+
transactionalBackend: selection.probe.backend,
|
|
51
|
+
resourceKind: selection.probe.resourceKind,
|
|
35
52
|
};
|
|
36
53
|
}
|
|
37
54
|
if (params.checkpoint?.enabled) {
|
|
38
55
|
try {
|
|
39
|
-
await listRecoveryCheckpoints(
|
|
56
|
+
await listRecoveryCheckpoints(recoveryStateDir, repoRoot);
|
|
40
57
|
}
|
|
41
58
|
catch (error) {
|
|
42
59
|
return {
|
|
@@ -45,6 +62,8 @@ export async function runTransactionalExecution(params) {
|
|
|
45
62
|
skipReason: error instanceof Error ? error.message : 'recovery_checkpoint_reconcile_failed',
|
|
46
63
|
predicted,
|
|
47
64
|
result: predicted,
|
|
65
|
+
transactionalBackend: selection.probe.backend,
|
|
66
|
+
resourceKind: selection.probe.resourceKind,
|
|
48
67
|
};
|
|
49
68
|
}
|
|
50
69
|
}
|
|
@@ -118,7 +137,7 @@ export async function runTransactionalExecution(params) {
|
|
|
118
137
|
const observedChanges = await buildObservedChangesFromTransactional(snapshot.baselineRoot ?? repoRoot, snapshot.executionRoot, changes);
|
|
119
138
|
const checkpoint = params.checkpoint?.enabled && changes.length > 0
|
|
120
139
|
? await prepareRecoveryCheckpoint({
|
|
121
|
-
stateDir,
|
|
140
|
+
stateDir: recoveryStateDir,
|
|
122
141
|
repoRoot,
|
|
123
142
|
baselinePath: snapshot.baselineRoot ?? repoRoot,
|
|
124
143
|
worktreePath: snapshot.executionRoot,
|
|
@@ -127,18 +146,30 @@ export async function runTransactionalExecution(params) {
|
|
|
127
146
|
protectedRoots: diffContext.protectedRoots,
|
|
128
147
|
config: params.checkpoint,
|
|
129
148
|
backend: snapshot.backend,
|
|
149
|
+
expectedResourceIdentity: snapshot.resourceIdentity,
|
|
130
150
|
})
|
|
131
151
|
: null;
|
|
132
152
|
try {
|
|
133
153
|
if (checkpoint) {
|
|
134
|
-
await markRecoveryCheckpointApplying(
|
|
154
|
+
await markRecoveryCheckpointApplying(recoveryStateDir, checkpoint);
|
|
135
155
|
}
|
|
136
156
|
let receipt;
|
|
157
|
+
const validateResourceIdentity = snapshot.validateResourceIdentity;
|
|
137
158
|
await applyWorktreeChanges(snapshot.executionRoot, repoRoot, changes, {
|
|
138
159
|
observedChanges,
|
|
160
|
+
beforeMutation: validateResourceIdentity
|
|
161
|
+
? async () => {
|
|
162
|
+
try {
|
|
163
|
+
await validateResourceIdentity();
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
throw new Error(TRANSACTIONAL_RESOURCE_IDENTITY_CHANGED, { cause: error });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
: undefined,
|
|
139
170
|
afterApply: checkpoint
|
|
140
171
|
? async () => {
|
|
141
|
-
receipt = await markRecoveryCheckpointApplied(
|
|
172
|
+
receipt = await markRecoveryCheckpointApplied(recoveryStateDir, checkpoint);
|
|
142
173
|
}
|
|
143
174
|
: undefined,
|
|
144
175
|
});
|
|
@@ -169,14 +200,25 @@ export async function runTransactionalExecution(params) {
|
|
|
169
200
|
};
|
|
170
201
|
}
|
|
171
202
|
catch (error) {
|
|
203
|
+
const toctou = error instanceof Error && error.message === TRANSACTIONAL_APPLY_TOCTOU;
|
|
204
|
+
const rollbackFailed = error instanceof Error && error.message === TRANSACTIONAL_APPLY_ROLLBACK_FAILED;
|
|
205
|
+
const resourceIdentityChanged = errorChainIncludes(error, TRANSACTIONAL_RESOURCE_IDENTITY_CHANGED);
|
|
206
|
+
const recoveryStateUnavailable = !recoveryStateOutsideResource && (rollbackFailed || resourceIdentityChanged);
|
|
172
207
|
if (checkpoint) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
208
|
+
if (recoveryStateUnavailable) {
|
|
209
|
+
// The resource path may now resolve to attacker-controlled replacement bytes.
|
|
210
|
+
// Leave the original artifact in applying state instead of touching that path.
|
|
211
|
+
}
|
|
212
|
+
else if (rollbackFailed) {
|
|
213
|
+
await markRecoveryCheckpointNeedsManualRepair(recoveryStateDir, checkpoint.checkpointId, TRANSACTIONAL_APPLY_ROLLBACK_FAILED);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
const recoveryState = await reconcileRecoveryCheckpoint(recoveryStateDir, checkpoint.checkpointId);
|
|
217
|
+
if (recoveryState === 'prepared') {
|
|
218
|
+
await discardPreparedRecoveryCheckpoint(recoveryStateDir, checkpoint.checkpointId);
|
|
219
|
+
}
|
|
176
220
|
}
|
|
177
221
|
}
|
|
178
|
-
const toctou = error instanceof Error && error.message === TRANSACTIONAL_APPLY_TOCTOU;
|
|
179
|
-
const rollbackFailed = error instanceof Error && error.message === TRANSACTIONAL_APPLY_ROLLBACK_FAILED;
|
|
180
222
|
const result = {
|
|
181
223
|
...predicted,
|
|
182
224
|
verdict: 'deny_pending_approval',
|
|
@@ -189,9 +231,10 @@ export async function runTransactionalExecution(params) {
|
|
|
189
231
|
...observed.assessment.signals,
|
|
190
232
|
rollbackFailed
|
|
191
233
|
? 'transactional_apply_rollback_failed'
|
|
192
|
-
: toctou
|
|
234
|
+
: toctou || resourceIdentityChanged
|
|
193
235
|
? 'transactional_apply_toctou'
|
|
194
236
|
: 'transactional_apply_failed',
|
|
237
|
+
...(recoveryStateUnavailable ? ['transactional_recovery_state_unavailable'] : []),
|
|
195
238
|
],
|
|
196
239
|
},
|
|
197
240
|
};
|
|
@@ -245,7 +288,10 @@ export async function runTransactionalExecution(params) {
|
|
|
245
288
|
snapshotCopyStrategy: snapshot.copyStrategy,
|
|
246
289
|
snapshotPrepareMs: snapshot.snapshotPrepareMs,
|
|
247
290
|
}
|
|
248
|
-
: {
|
|
291
|
+
: {
|
|
292
|
+
transactionalBackend: selection.probe.backend,
|
|
293
|
+
resourceKind: selection.probe.resourceKind,
|
|
294
|
+
}),
|
|
249
295
|
};
|
|
250
296
|
}
|
|
251
297
|
finally {
|
package/dist/core/types.d.ts
CHANGED
|
@@ -4,6 +4,17 @@ import type { PolicyDecision } from './capability/policy-types.js';
|
|
|
4
4
|
import type { CapabilityRequestV1 } from './capability/request.js';
|
|
5
5
|
import type { FsScopeAllowlistFile } from './capability/types.js';
|
|
6
6
|
import type { EffectPlan, EffectPlanPolicyProjection } from './effect-ir/types.js';
|
|
7
|
+
export interface MediatedExecutionResult {
|
|
8
|
+
exitCode: number | null;
|
|
9
|
+
signal: string | null;
|
|
10
|
+
timedOut: boolean;
|
|
11
|
+
stdout: string;
|
|
12
|
+
stderr: string;
|
|
13
|
+
stdoutTruncated: boolean;
|
|
14
|
+
stderrTruncated: boolean;
|
|
15
|
+
receiptHash: string;
|
|
16
|
+
workspaceChangesDiscarded: true;
|
|
17
|
+
}
|
|
7
18
|
export type BelayMode = 'enforce' | 'audit';
|
|
8
19
|
export type HookVerdict = 'allow' | 'allow_flagged' | 'deny_pending_approval';
|
|
9
20
|
export type Reversibility = 'reversible' | 'recoverable_with_cost' | 'irreversible';
|
|
@@ -45,6 +56,8 @@ export interface ClassifyResult {
|
|
|
45
56
|
effectPlanPolicyDecisions?: PolicyDecision[];
|
|
46
57
|
effectPlanProjection?: EffectPlanPolicyProjection;
|
|
47
58
|
boundaryProfile?: string;
|
|
59
|
+
wouldMediate?: boolean;
|
|
60
|
+
mediatedExecution?: MediatedExecutionResult;
|
|
48
61
|
}
|
|
49
62
|
export type UnknownLocalEffectPolicy = 'allow_flagged' | 'deny';
|
|
50
63
|
export type UnparseableShellPolicy = 'allow_flagged' | 'deny';
|
|
@@ -4,15 +4,25 @@ export interface ParsedSegment {
|
|
|
4
4
|
head: string;
|
|
5
5
|
key: string;
|
|
6
6
|
normalized: string;
|
|
7
|
+
encounteredXargs: boolean;
|
|
7
8
|
}
|
|
8
9
|
export declare function normalizeHead(token: string): string;
|
|
9
10
|
export declare function peelTransparentWrappers(tokens: string[]): {
|
|
10
11
|
tokens: string[];
|
|
11
12
|
xargsStdinOpaque: boolean;
|
|
13
|
+
encounteredXargs: boolean;
|
|
14
|
+
opaque: boolean;
|
|
12
15
|
};
|
|
13
16
|
export declare function isVariableIndirectHead(head: string): boolean;
|
|
14
17
|
export declare function extractEvalBody(tokens: string[]): string | null;
|
|
15
18
|
export declare function extractRecursiveScript(tokens: string[]): string | null;
|
|
19
|
+
/**
|
|
20
|
+
* True when a recursive script is evaluated from a command argument rather
|
|
21
|
+
* than expanded from a static launcher recipe. Callers use this semantic fact
|
|
22
|
+
* to preserve fail-closed policy for dynamic evaluation.
|
|
23
|
+
*/
|
|
24
|
+
export declare function isDynamicRecursiveEvaluation(tokens: string[]): boolean;
|
|
25
|
+
export declare function isCommandInspection(tokens: string[]): boolean;
|
|
16
26
|
export declare function isBareInterpreter(tokens: string[]): boolean;
|
|
17
27
|
export declare function splitTopLevelSegments(command: string): string[];
|
|
18
28
|
export declare function splitStructuralShellSegments(command: string): string[];
|
|
@@ -3,17 +3,7 @@ import { findCommandSubstitutions, findStructuralCommandSubstitutions, } from '.
|
|
|
3
3
|
import { commandKey, tokenizeShell } from '../shell-tokenizer.js';
|
|
4
4
|
import { detectUnparseableShell } from '../shell-unparseable.js';
|
|
5
5
|
const ENV_PREFIX_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*=(?:'[^']*'|"[^"]*"|\S+)$/;
|
|
6
|
-
const
|
|
7
|
-
'sudo',
|
|
8
|
-
'env',
|
|
9
|
-
'nohup',
|
|
10
|
-
'time',
|
|
11
|
-
'nice',
|
|
12
|
-
'ionice',
|
|
13
|
-
'stdbuf',
|
|
14
|
-
'setsid',
|
|
15
|
-
'xargs',
|
|
16
|
-
]);
|
|
6
|
+
const MAX_WRAPPER_PEEL_DEPTH = 32;
|
|
17
7
|
const SHELL_INTERPRETERS = new Set(['bash', 'sh', 'zsh', 'dash', 'fish']);
|
|
18
8
|
const CODE_INTERPRETERS = new Set(['python', 'python3', 'node', 'ruby', 'perl', 'osascript']);
|
|
19
9
|
const SCRIPT_FLAGS = new Set(['-c', '-lc', '-e', '--eval']);
|
|
@@ -37,7 +27,13 @@ export function normalizeHead(token) {
|
|
|
37
27
|
export function peelTransparentWrappers(tokens) {
|
|
38
28
|
let current = [...tokens];
|
|
39
29
|
let xargsStdinOpaque = false;
|
|
30
|
+
let encounteredXargs = false;
|
|
31
|
+
let peelDepth = 0;
|
|
40
32
|
while (current.length > 0) {
|
|
33
|
+
if (peelDepth >= MAX_WRAPPER_PEEL_DEPTH) {
|
|
34
|
+
return { tokens: current, xargsStdinOpaque: false, encounteredXargs, opaque: true };
|
|
35
|
+
}
|
|
36
|
+
peelDepth += 1;
|
|
41
37
|
while (current.length > 0 && ENV_PREFIX_PATTERN.test(current[0] ?? '')) {
|
|
42
38
|
current.shift();
|
|
43
39
|
}
|
|
@@ -45,38 +41,247 @@ export function peelTransparentWrappers(tokens) {
|
|
|
45
41
|
break;
|
|
46
42
|
}
|
|
47
43
|
const head = normalizeHead(current[0] ?? '');
|
|
48
|
-
if (
|
|
44
|
+
if (head === 'xargs') {
|
|
45
|
+
encounteredXargs = true;
|
|
46
|
+
const wrapper = peelXargsWrapper(current);
|
|
47
|
+
if (wrapper.kind === 'opaque') {
|
|
48
|
+
xargsStdinOpaque = current.length === 1;
|
|
49
|
+
return {
|
|
50
|
+
tokens: xargsStdinOpaque ? [] : current,
|
|
51
|
+
xargsStdinOpaque,
|
|
52
|
+
encounteredXargs,
|
|
53
|
+
opaque: true,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
current = wrapper.tokens;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const wrapper = peelTransparentWrapper(head, current);
|
|
60
|
+
if (!wrapper) {
|
|
49
61
|
break;
|
|
50
62
|
}
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
63
|
+
if (wrapper.kind === 'opaque') {
|
|
64
|
+
return { tokens: current, xargsStdinOpaque: false, encounteredXargs, opaque: true };
|
|
65
|
+
}
|
|
66
|
+
if (wrapper.kind === 'preserve') {
|
|
67
|
+
return { tokens: current, xargsStdinOpaque: false, encounteredXargs, opaque: false };
|
|
68
|
+
}
|
|
69
|
+
current = wrapper.tokens;
|
|
70
|
+
}
|
|
71
|
+
return { tokens: current, xargsStdinOpaque, encounteredXargs, opaque: false };
|
|
72
|
+
}
|
|
73
|
+
function peelTransparentWrapper(head, tokens) {
|
|
74
|
+
switch (head) {
|
|
75
|
+
case 'sudo':
|
|
76
|
+
return peelSudoWrapper(tokens);
|
|
77
|
+
case 'command':
|
|
78
|
+
case 'builtin':
|
|
79
|
+
case 'exec':
|
|
80
|
+
return peelShellInvocationWrapper(head, tokens);
|
|
81
|
+
case 'env':
|
|
82
|
+
return peelEnvWrapper(tokens);
|
|
83
|
+
case 'time':
|
|
84
|
+
return peelTimeWrapper(tokens);
|
|
85
|
+
case 'nice':
|
|
86
|
+
return peelNiceWrapper(tokens);
|
|
87
|
+
case 'ionice':
|
|
88
|
+
case 'stdbuf':
|
|
89
|
+
case 'setsid':
|
|
90
|
+
case 'nohup':
|
|
91
|
+
return peelNoOptionWrapper(tokens);
|
|
92
|
+
default:
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function peelXargsWrapper(tokens) {
|
|
97
|
+
const option = tokens[1] ?? '';
|
|
98
|
+
if (!option) {
|
|
99
|
+
return { kind: 'opaque' };
|
|
100
|
+
}
|
|
101
|
+
if (option === '-I') {
|
|
102
|
+
return isWrapperOperand(tokens[2]) ? targetFrom(tokens, 3) : { kind: 'opaque' };
|
|
103
|
+
}
|
|
104
|
+
if (option.startsWith('-I') && option.length > 2) {
|
|
105
|
+
return targetFrom(tokens, 2);
|
|
106
|
+
}
|
|
107
|
+
if (option.startsWith('-')) {
|
|
108
|
+
return { kind: 'opaque' };
|
|
109
|
+
}
|
|
110
|
+
return { kind: 'peeled', tokens: tokens.slice(1) };
|
|
111
|
+
}
|
|
112
|
+
function peelEnvWrapper(tokens) {
|
|
113
|
+
let index = 1;
|
|
114
|
+
while (index < tokens.length) {
|
|
115
|
+
const token = tokens[index] ?? '';
|
|
116
|
+
if (token === '--') {
|
|
117
|
+
index += 1;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
if (ENV_PREFIX_PATTERN.test(token)) {
|
|
121
|
+
index += 1;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (token === '-i' || token === '--ignore-environment') {
|
|
125
|
+
index += 1;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (token === '-u' || token === '--unset') {
|
|
129
|
+
if (!isWrapperOperand(tokens[index + 1])) {
|
|
130
|
+
return { kind: 'opaque' };
|
|
55
131
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
132
|
+
index += 2;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (token.startsWith('--unset=') && token.length > '--unset='.length) {
|
|
136
|
+
index += 1;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (token.startsWith('-')) {
|
|
140
|
+
return { kind: 'opaque' };
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
return targetFrom(tokens, index);
|
|
145
|
+
}
|
|
146
|
+
function peelTimeWrapper(tokens) {
|
|
147
|
+
if (tokens[1] === '-p') {
|
|
148
|
+
return targetFrom(tokens, 2);
|
|
149
|
+
}
|
|
150
|
+
return targetFrom(tokens, 1);
|
|
151
|
+
}
|
|
152
|
+
function peelNiceWrapper(tokens) {
|
|
153
|
+
if (tokens[1] === '-n') {
|
|
154
|
+
return isNiceAdjustment(tokens[2]) ? targetFrom(tokens, 3) : { kind: 'opaque' };
|
|
155
|
+
}
|
|
156
|
+
return targetFrom(tokens, 1);
|
|
157
|
+
}
|
|
158
|
+
function peelNoOptionWrapper(tokens) {
|
|
159
|
+
return targetFrom(tokens, 1);
|
|
160
|
+
}
|
|
161
|
+
function targetFrom(tokens, index) {
|
|
162
|
+
return isWrapperOperand(tokens[index])
|
|
163
|
+
? { kind: 'peeled', tokens: tokens.slice(index) }
|
|
164
|
+
: { kind: 'opaque' };
|
|
165
|
+
}
|
|
166
|
+
function isWrapperOperand(token) {
|
|
167
|
+
return Boolean(token && token !== '--' && !token.startsWith('-'));
|
|
168
|
+
}
|
|
169
|
+
function isNiceAdjustment(token) {
|
|
170
|
+
return Boolean(token && /^[+-]?\d+$/.test(token));
|
|
171
|
+
}
|
|
172
|
+
function peelSudoWrapper(tokens) {
|
|
173
|
+
let index = 1;
|
|
174
|
+
while (index < tokens.length) {
|
|
175
|
+
const token = tokens[index] ?? '';
|
|
176
|
+
if (token === '--') {
|
|
177
|
+
index += 1;
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
if (!token.startsWith('-') || token === '-') {
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
if (token === '-n' || token === '--non-interactive') {
|
|
184
|
+
index += 1;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (token === '-u' || token === '-g' || token === '--user' || token === '--group') {
|
|
188
|
+
if (!tokens[index + 1] || tokens[index + 1] === '--') {
|
|
189
|
+
return { kind: 'opaque' };
|
|
60
190
|
}
|
|
61
|
-
|
|
191
|
+
index += 2;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if ((token.startsWith('-u') || token.startsWith('-g')) && token.length > 2) {
|
|
195
|
+
index += 1;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if ((token.startsWith('--user=') || token.startsWith('--group=')) &&
|
|
199
|
+
token.slice(token.indexOf('=') + 1).length > 0) {
|
|
200
|
+
index += 1;
|
|
62
201
|
continue;
|
|
63
202
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
203
|
+
return { kind: 'opaque' };
|
|
204
|
+
}
|
|
205
|
+
return index < tokens.length
|
|
206
|
+
? { kind: 'peeled', tokens: tokens.slice(index) }
|
|
207
|
+
: { kind: 'opaque' };
|
|
208
|
+
}
|
|
209
|
+
function peelShellInvocationWrapper(head, tokens) {
|
|
210
|
+
switch (head) {
|
|
211
|
+
case 'command':
|
|
212
|
+
return peelCommandWrapper(tokens);
|
|
213
|
+
case 'builtin':
|
|
214
|
+
return peelBuiltinWrapper(tokens);
|
|
215
|
+
case 'exec':
|
|
216
|
+
return peelExecWrapper(tokens);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function peelCommandWrapper(tokens) {
|
|
220
|
+
let index = 1;
|
|
221
|
+
let inspection = false;
|
|
222
|
+
while (index < tokens.length) {
|
|
223
|
+
const token = tokens[index] ?? '';
|
|
224
|
+
if (token === '--') {
|
|
225
|
+
index += 1;
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
if (!token.startsWith('-') || token === '-') {
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
const flags = token.slice(1);
|
|
232
|
+
if (!flags || ![...flags].every((flag) => flag === 'p' || flag === 'v' || flag === 'V')) {
|
|
233
|
+
return { kind: 'opaque' };
|
|
234
|
+
}
|
|
235
|
+
inspection ||= flags.includes('v') || flags.includes('V');
|
|
236
|
+
index += 1;
|
|
237
|
+
}
|
|
238
|
+
if (inspection) {
|
|
239
|
+
return { kind: 'preserve' };
|
|
240
|
+
}
|
|
241
|
+
return index < tokens.length
|
|
242
|
+
? { kind: 'peeled', tokens: tokens.slice(index) }
|
|
243
|
+
: { kind: 'opaque' };
|
|
244
|
+
}
|
|
245
|
+
function peelBuiltinWrapper(tokens) {
|
|
246
|
+
const first = tokens[1];
|
|
247
|
+
if (first === '--') {
|
|
248
|
+
return tokens.length > 2 ? { kind: 'peeled', tokens: tokens.slice(2) } : { kind: 'opaque' };
|
|
249
|
+
}
|
|
250
|
+
if (!first || first.startsWith('-')) {
|
|
251
|
+
return { kind: 'opaque' };
|
|
252
|
+
}
|
|
253
|
+
return { kind: 'peeled', tokens: tokens.slice(1) };
|
|
254
|
+
}
|
|
255
|
+
function peelExecWrapper(tokens) {
|
|
256
|
+
let index = 1;
|
|
257
|
+
while (index < tokens.length) {
|
|
258
|
+
const token = tokens[index] ?? '';
|
|
259
|
+
if (token === '--') {
|
|
260
|
+
index += 1;
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
if (!token.startsWith('-') || token === '-') {
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
if (token === '-a') {
|
|
267
|
+
if (!tokens[index + 1]) {
|
|
268
|
+
return { kind: 'opaque' };
|
|
73
269
|
}
|
|
74
|
-
|
|
270
|
+
index += 2;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (token.startsWith('-a') && token.length > 2) {
|
|
274
|
+
index += 1;
|
|
75
275
|
continue;
|
|
76
276
|
}
|
|
77
|
-
|
|
277
|
+
if (![...token.slice(1)].every((flag) => flag === 'c' || flag === 'l')) {
|
|
278
|
+
return { kind: 'opaque' };
|
|
279
|
+
}
|
|
280
|
+
index += 1;
|
|
78
281
|
}
|
|
79
|
-
return
|
|
282
|
+
return index < tokens.length
|
|
283
|
+
? { kind: 'peeled', tokens: tokens.slice(index) }
|
|
284
|
+
: { kind: 'opaque' };
|
|
80
285
|
}
|
|
81
286
|
export function isVariableIndirectHead(head) {
|
|
82
287
|
return head.startsWith('$');
|
|
@@ -90,11 +295,15 @@ export function extractEvalBody(tokens) {
|
|
|
90
295
|
return body || null;
|
|
91
296
|
}
|
|
92
297
|
export function extractRecursiveScript(tokens) {
|
|
93
|
-
const
|
|
298
|
+
const { tokens: filtered, opaque } = peelTransparentWrappers(tokens);
|
|
299
|
+
if (opaque) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
94
302
|
const head = normalizeHead(filtered[0] ?? '');
|
|
95
303
|
const second = filtered[1] ?? '';
|
|
96
304
|
if (head === 'eval') {
|
|
97
|
-
|
|
305
|
+
const body = filtered.slice(1).join(' ').trim();
|
|
306
|
+
return body || null;
|
|
98
307
|
}
|
|
99
308
|
if (SHELL_INTERPRETERS.has(head) || CODE_INTERPRETERS.has(head)) {
|
|
100
309
|
const flagIndex = filtered.findIndex((token) => SCRIPT_FLAGS.has(token));
|
|
@@ -117,9 +326,29 @@ export function extractRecursiveScript(tokens) {
|
|
|
117
326
|
}
|
|
118
327
|
return null;
|
|
119
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* True when a recursive script is evaluated from a command argument rather
|
|
331
|
+
* than expanded from a static launcher recipe. Callers use this semantic fact
|
|
332
|
+
* to preserve fail-closed policy for dynamic evaluation.
|
|
333
|
+
*/
|
|
334
|
+
export function isDynamicRecursiveEvaluation(tokens) {
|
|
335
|
+
const { tokens: filtered, opaque } = peelTransparentWrappers(tokens);
|
|
336
|
+
if (opaque) {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
const head = normalizeHead(filtered[0] ?? '');
|
|
340
|
+
if (head === 'eval') {
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
return ((SHELL_INTERPRETERS.has(head) || CODE_INTERPRETERS.has(head)) &&
|
|
344
|
+
filtered.some((token) => SCRIPT_FLAGS.has(token)));
|
|
345
|
+
}
|
|
346
|
+
export function isCommandInspection(tokens) {
|
|
347
|
+
return (normalizeHead(tokens[0] ?? '') === 'command' && peelCommandWrapper(tokens).kind === 'preserve');
|
|
348
|
+
}
|
|
120
349
|
export function isBareInterpreter(tokens) {
|
|
121
|
-
const { tokens: peeled, xargsStdinOpaque } = peelTransparentWrappers(tokens);
|
|
122
|
-
if (xargsStdinOpaque) {
|
|
350
|
+
const { tokens: peeled, xargsStdinOpaque, opaque } = peelTransparentWrappers(tokens);
|
|
351
|
+
if (xargsStdinOpaque || opaque) {
|
|
123
352
|
return true;
|
|
124
353
|
}
|
|
125
354
|
if (peeled.length === 0) {
|
|
@@ -267,7 +496,8 @@ export function splitStructuralShellSegments(command) {
|
|
|
267
496
|
}
|
|
268
497
|
export function parseSegment(command) {
|
|
269
498
|
const tokens = tokenizeShell(command);
|
|
270
|
-
const
|
|
499
|
+
const peeledWrapper = peelTransparentWrappers(tokens);
|
|
500
|
+
const { tokens: peeled } = peeledWrapper;
|
|
271
501
|
const normalizedTokens = peeled.map((token) => normalizeHead(token));
|
|
272
502
|
const key = commandKey(peeled.map((token, index) => (index === 0 ? normalizeHead(token) : token)));
|
|
273
503
|
return {
|
|
@@ -275,6 +505,7 @@ export function parseSegment(command) {
|
|
|
275
505
|
head: normalizeHead(peeled[0] ?? ''),
|
|
276
506
|
key,
|
|
277
507
|
normalized: normalizedTokens.join(' ').trim(),
|
|
508
|
+
encounteredXargs: peeledWrapper.encounteredXargs,
|
|
278
509
|
};
|
|
279
510
|
}
|
|
280
511
|
export function segmentOpacity(command) {
|
|
@@ -282,8 +513,8 @@ export function segmentOpacity(command) {
|
|
|
282
513
|
return 'unparseable';
|
|
283
514
|
}
|
|
284
515
|
const tokens = tokenizeShell(command);
|
|
285
|
-
const { xargsStdinOpaque } = peelTransparentWrappers(tokens);
|
|
286
|
-
if (xargsStdinOpaque) {
|
|
516
|
+
const { xargsStdinOpaque, opaque } = peelTransparentWrappers(tokens);
|
|
517
|
+
if (xargsStdinOpaque || opaque) {
|
|
287
518
|
return 'opaque';
|
|
288
519
|
}
|
|
289
520
|
if (isBareInterpreter(tokens)) {
|
|
@@ -103,6 +103,18 @@ function filterRoutinePathTargets(head, targets) {
|
|
|
103
103
|
}
|
|
104
104
|
return targets.filter((target) => looksLikePathToken(target));
|
|
105
105
|
}
|
|
106
|
+
function tscDeclaresOutputPaths(args) {
|
|
107
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
108
|
+
const arg = args[index] ?? '';
|
|
109
|
+
if (/^--(?:outDir|outFile)=/.test(arg)) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
if ((arg === '--outDir' || arg === '--outFile') && args[index + 1]) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
106
118
|
function extractFilePathOperands(tokens) {
|
|
107
119
|
const redirects = extractRedirectTargets(tokens);
|
|
108
120
|
const args = [...redirects];
|
|
@@ -197,6 +209,15 @@ export function analyzeShellCommandSemantics(tokens, segment, cwd) {
|
|
|
197
209
|
if (head === 'rsync') {
|
|
198
210
|
return classifyRsync(tokens);
|
|
199
211
|
}
|
|
212
|
+
if (head === 'tsc' && tokens.includes('--noEmit') && !tscDeclaresOutputPaths(tokens.slice(1))) {
|
|
213
|
+
return {
|
|
214
|
+
effect: 'read_only',
|
|
215
|
+
pathTargets: [],
|
|
216
|
+
signals: ['tsc.typecheck'],
|
|
217
|
+
normalizedKey: segment.key,
|
|
218
|
+
isReadOnly: true,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
200
221
|
let effect = 'unknown';
|
|
201
222
|
let pathTargets = extractFilePathOperands(tokens);
|
|
202
223
|
pathTargets = filterRoutinePathTargets(head, pathTargets);
|
|
@@ -68,7 +68,7 @@ export declare function splitFixHoldout<T>(items: readonly T[], seed: number, ho
|
|
|
68
68
|
holdout: T[];
|
|
69
69
|
};
|
|
70
70
|
export declare function generateProbeCases(seed: number, maxCases?: number): MutatedCase[];
|
|
71
|
-
/** Benign
|
|
71
|
+
/** Benign probe cores × AUTO_LABEL mutators — FP over-stop probe (not for corpus auto-add). */
|
|
72
72
|
export declare function generateFpProbeCases(seed: number): MutatedCase[];
|
|
73
73
|
/** Ratio for overfitting detection; null when holdout unavailable or fix-perfect but holdout not. */
|
|
74
74
|
export declare function holdoutFixFnRateRatio(fixSetFnRate: number, holdoutFnRate: number | null): number | null;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { BENIGN_PROBE_CORES } from './benign-probe-cores.js';
|
|
4
5
|
import { evaluateCommandBatch, evaluateFpCommandBatch } from './evaluate.js';
|
|
5
|
-
import { MUST_ALLOW_SHELL_COMMANDS } from './must-allow-commands.js';
|
|
6
6
|
import { AUTO_LABEL_MUTATORS, CATASTROPHIC_CORES, generateMutatedCases, } from './mutators.js';
|
|
7
7
|
const MUST_ASK_VERDICT = 'deny_pending_approval';
|
|
8
8
|
export function probeCaseKey(entry) {
|
|
@@ -47,9 +47,9 @@ export function generateProbeCases(seed, maxCases) {
|
|
|
47
47
|
}
|
|
48
48
|
return shuffled.slice(0, maxCases);
|
|
49
49
|
}
|
|
50
|
-
/** Benign
|
|
50
|
+
/** Benign probe cores × AUTO_LABEL mutators — FP over-stop probe (not for corpus auto-add). */
|
|
51
51
|
export function generateFpProbeCases(seed) {
|
|
52
|
-
const all = generateMutatedCases([...
|
|
52
|
+
const all = generateMutatedCases([...BENIGN_PROBE_CORES], AUTO_LABEL_MUTATORS);
|
|
53
53
|
return shuffleWithSeed(all, seed + 1000);
|
|
54
54
|
}
|
|
55
55
|
function fnRate(fnCount, total) {
|