@guilz-dev/belay 0.9.0 → 0.9.2
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/claude/runtime-entry.js +2 -0
- package/dist/adapters/codex/runtime-entry.js +2 -0
- package/dist/adapters/cursor/hooks.d.ts +6 -1
- package/dist/adapters/cursor/hooks.js +23 -0
- package/dist/adapters/cursor/runtime-entry.js +5 -0
- package/dist/adapters/shared/gate-runtime.d.ts +1 -0
- package/dist/adapters/shared/gate-runtime.js +49 -52
- package/dist/bundle/claude-runtime.mjs +4295 -3624
- package/dist/bundle/codex-runtime.mjs +4136 -3465
- package/dist/bundle/cursor-runtime.mjs +4137 -3463
- package/dist/commands/doctor.js +21 -3
- package/dist/core/audit-analysis.js +7 -4
- package/dist/core/audit-io.d.ts +1 -0
- package/dist/core/audit-io.js +8 -6
- package/dist/core/audit-legacy-archive.d.ts +7 -0
- package/dist/core/audit-legacy-archive.js +56 -0
- package/dist/core/audit-metrics.d.ts +4 -0
- package/dist/core/audit-metrics.js +2 -2
- package/dist/core/audit-query.d.ts +2 -0
- package/dist/core/audit-query.js +33 -9
- package/dist/core/audit-recovery-metrics.js +2 -2
- package/dist/core/audit-serialize.d.ts +9 -0
- package/dist/core/audit-serialize.js +179 -0
- package/dist/core/audit-types.d.ts +1 -0
- package/dist/core/capability/policy-engine.js +32 -13
- package/dist/core/decision-config-fingerprint.d.ts +3 -0
- package/dist/core/decision-config-fingerprint.js +6 -0
- package/dist/core/effect-ir/shell-lower.js +208 -2
- package/dist/core/verdict/egress-classify.js +1 -0
- package/dist/core/verdict/launcher-resolve.js +51 -10
- package/dist/core/verdict/makefile-expand.d.ts +4 -0
- package/dist/core/verdict/makefile-expand.js +151 -0
- package/dist/core/verdict/parser.d.ts +1 -0
- package/dist/core/verdict/parser.js +24 -12
- package/dist/corpus/benign-probe-cores.d.ts +1 -1
- package/dist/corpus/benign-probe-cores.js +2 -0
- package/dist/defaults.js +5 -4
- 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 +2 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { lstatSync, realpathSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { inspectGitResourceIdentity } from '../git-resource-identity.js';
|
|
4
|
+
import { canonicalPath, pathWithinRoot } from '../path-utils.js';
|
|
4
5
|
import { isFdDuplication, isRedirectOperator, tokenizeShell } from '../shell-tokenizer.js';
|
|
5
6
|
import { decodeEgressEffects } from '../verdict/egress-classify.js';
|
|
6
7
|
import { decodeGitEffects } from '../verdict/git-classifier.js';
|
|
7
8
|
import { resolveLauncherRecipe } from '../verdict/launcher-resolve.js';
|
|
8
|
-
import { extractRecursiveScript, isCommandInspection, isDynamicRecursiveEvaluation, parseSegment, redactCommand, segmentOpacity, splitStructuralShellSegments, structuralSubstitutionInners, } from '../verdict/parser.js';
|
|
9
|
+
import { extractDockerComposeRunScript, extractRecursiveScript, isCommandInspection, isDynamicRecursiveEvaluation, parseSegment, redactCommand, segmentOpacity, splitStructuralShellSegments, structuralSubstitutionInners, } from '../verdict/parser.js';
|
|
9
10
|
import { joinEffectOpacity } from './normalize.js';
|
|
10
11
|
import { classifyPackageAcquisitionSpec, innerRecipeFromPeel, peelPackageExecArgv, resolveLocalBin, } from './package-exec.js';
|
|
11
12
|
import { buildShellEffectPlan, } from './shell-build.js';
|
|
@@ -130,7 +131,12 @@ function lowerSegment(command, context) {
|
|
|
130
131
|
const environment = extractEnvironment(rawTokens, context.env);
|
|
131
132
|
const env = environment.env;
|
|
132
133
|
const parsed = parseSegment(command);
|
|
133
|
-
const
|
|
134
|
+
const parsedTokens = environment.commandTokens ?? parsed.tokens;
|
|
135
|
+
const tokens = stripRedirects(parsedTokens.length === 0 &&
|
|
136
|
+
rawTokens.length > 0 &&
|
|
137
|
+
rawTokens.every((token) => ENV_PREFIX_PATTERN.test(token))
|
|
138
|
+
? rawTokens
|
|
139
|
+
: parsedTokens).map((token) => expandKnownVariables(token, env));
|
|
134
140
|
const head = path.basename(tokens[0] ?? parsed.head);
|
|
135
141
|
let opacity = segmentOpacity(command);
|
|
136
142
|
const signals = new Set();
|
|
@@ -210,6 +216,25 @@ function lowerSegment(command, context) {
|
|
|
210
216
|
}
|
|
211
217
|
return shellSegment(commandRedacted, head, requirements, 'recursive', signals);
|
|
212
218
|
}
|
|
219
|
+
const dockerComposeScript = extractDockerComposeRunScript(tokens);
|
|
220
|
+
if (dockerComposeScript && opacity !== 'opaque' && opacity !== 'unparseable') {
|
|
221
|
+
requirements.push(processRequirement(head, 'spawn', commandRedacted, ['process.docker_compose_run']));
|
|
222
|
+
const nested = lowerTopLevelSegments(dockerComposeScript, {
|
|
223
|
+
...context,
|
|
224
|
+
command: dockerComposeScript,
|
|
225
|
+
env,
|
|
226
|
+
depth: context.depth + 1,
|
|
227
|
+
});
|
|
228
|
+
for (const nestedSegment of nested) {
|
|
229
|
+
requirements.push(...nestedSegment.requirements.map((entry) => withInnerProvenance(entry, dockerComposeScript, head, commandRedacted)));
|
|
230
|
+
for (const signal of nestedSegment.signals) {
|
|
231
|
+
signals.add(signal);
|
|
232
|
+
}
|
|
233
|
+
opacity = joinNestedOpacity(opacity, nestedSegment);
|
|
234
|
+
}
|
|
235
|
+
signals.add('process.docker_compose_run');
|
|
236
|
+
return shellSegment(commandRedacted, head, requirements, 'recursive', signals);
|
|
237
|
+
}
|
|
213
238
|
const launcher = resolveLauncherRecipe({
|
|
214
239
|
tokens,
|
|
215
240
|
cwd: context.cwd,
|
|
@@ -381,6 +406,101 @@ function railsReadOnlySubcommand(args) {
|
|
|
381
406
|
}
|
|
382
407
|
return RAILS_READ_ONLY_SUBCOMMANDS.has(subcommand);
|
|
383
408
|
}
|
|
409
|
+
function isRubyTestScript(scriptPath) {
|
|
410
|
+
const base = path.basename(scriptPath);
|
|
411
|
+
return base.endsWith('_test.rb') || base.endsWith('_spec.rb');
|
|
412
|
+
}
|
|
413
|
+
function parseRubyTestInvocation(args) {
|
|
414
|
+
const includePaths = [];
|
|
415
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
416
|
+
const arg = args[index] ?? '';
|
|
417
|
+
if (arg === '-e' || arg === '-r') {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
if (arg === '-I') {
|
|
421
|
+
const includePath = args[index + 1];
|
|
422
|
+
if (!includePath) {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
includePaths.push(includePath);
|
|
426
|
+
index += 1;
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
if (arg.startsWith('-I') && arg.length > 2) {
|
|
430
|
+
includePaths.push(arg.slice(2));
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
if (arg.startsWith('-')) {
|
|
434
|
+
if (arg === '-n') {
|
|
435
|
+
if (!args[index + 1]) {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
index += 1;
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (arg.startsWith('-n')) {
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
if (isRubyTestScript(arg)) {
|
|
447
|
+
return { includePaths, scriptPath: arg };
|
|
448
|
+
}
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
function isRubocopMutating(args) {
|
|
454
|
+
return args.some((arg) => arg === '-A' ||
|
|
455
|
+
arg === '-a' ||
|
|
456
|
+
arg === '--auto-correct' ||
|
|
457
|
+
arg === '--autocorrect' ||
|
|
458
|
+
arg.startsWith('--auto-correct-all') ||
|
|
459
|
+
arg.startsWith('--autocorrect-all'));
|
|
460
|
+
}
|
|
461
|
+
function decodeBundleExecInner(innerHead, innerArgs, segment) {
|
|
462
|
+
const innerBase = executableBaseName(innerHead);
|
|
463
|
+
if (innerBase === 'rubocop') {
|
|
464
|
+
const mutating = isRubocopMutating(innerArgs);
|
|
465
|
+
return [
|
|
466
|
+
processRequirement(innerHead, mutating ? 'spawn' : 'inspect', segment, mutating ? ['process.linter.mutating'] : ['process.inspect.linter']),
|
|
467
|
+
];
|
|
468
|
+
}
|
|
469
|
+
if (innerBase === 'rspec') {
|
|
470
|
+
const targetArgs = innerArgs.filter((arg) => !arg.startsWith('-'));
|
|
471
|
+
if (targetArgs.length === 0) {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
return [processRequirement(innerHead, 'spawn', segment, ['process.test_runner.rspec'])];
|
|
475
|
+
}
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
function decodeRuby(args, cwd, repoRoot, segment) {
|
|
479
|
+
const parsed = parseRubyTestInvocation(args);
|
|
480
|
+
if (!parsed) {
|
|
481
|
+
return unsupportedProcess('ruby', segment, 'process.ruby_grammar_incomplete');
|
|
482
|
+
}
|
|
483
|
+
const scriptPath = resolvePathOperand(parsed.scriptPath, cwd);
|
|
484
|
+
if (!pathWithinRoot(canonicalPath(repoRoot), canonicalPath(scriptPath))) {
|
|
485
|
+
return unsupportedProcess('ruby', segment, 'process.ruby_outside_repo');
|
|
486
|
+
}
|
|
487
|
+
for (const includePath of parsed.includePaths) {
|
|
488
|
+
const resolvedInclude = resolvePathOperand(includePath, cwd);
|
|
489
|
+
if (!pathWithinRoot(canonicalPath(repoRoot), canonicalPath(resolvedInclude))) {
|
|
490
|
+
return unsupportedProcess('ruby', segment, 'process.ruby_outside_repo');
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
const lowered = [
|
|
494
|
+
processRequirement('ruby', 'spawn', segment, ['process.test_runner.minitest']),
|
|
495
|
+
requirement('fs.read', 'fs.read', { kind: 'path', path: scriptPath }, segment, [
|
|
496
|
+
'ruby.minitest_script_read',
|
|
497
|
+
]),
|
|
498
|
+
];
|
|
499
|
+
for (const includePath of parsed.includePaths) {
|
|
500
|
+
lowered.push(requirement('fs.read', 'fs.read', { kind: 'path', path: resolvePathOperand(includePath, cwd) }, segment, ['ruby.minitest_load_path_read']));
|
|
501
|
+
}
|
|
502
|
+
return lowered;
|
|
503
|
+
}
|
|
384
504
|
function decodeRuntimeMetadataProcess(head, args, segment) {
|
|
385
505
|
if (head === 'bundle') {
|
|
386
506
|
if (args.length === 1 && isMetadataOnlyArgv(args)) {
|
|
@@ -402,6 +522,10 @@ function decodeRuntimeMetadataProcess(head, args, segment) {
|
|
|
402
522
|
]),
|
|
403
523
|
];
|
|
404
524
|
}
|
|
525
|
+
const bundleExecInner = decodeBundleExecInner(innerHead, innerArgs, segment);
|
|
526
|
+
if (bundleExecInner) {
|
|
527
|
+
return bundleExecInner;
|
|
528
|
+
}
|
|
405
529
|
}
|
|
406
530
|
return null;
|
|
407
531
|
}
|
|
@@ -417,9 +541,75 @@ function decodeRuntimeMetadataProcess(head, args, segment) {
|
|
|
417
541
|
}
|
|
418
542
|
return null;
|
|
419
543
|
}
|
|
544
|
+
function decodeSetBuiltin(args) {
|
|
545
|
+
let index = 0;
|
|
546
|
+
while (index < args.length) {
|
|
547
|
+
const arg = args[index] ?? '';
|
|
548
|
+
if (arg === '--') {
|
|
549
|
+
index += 1;
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
if (arg === '-o' || arg === '+o') {
|
|
553
|
+
if (!args[index + 1]) {
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
index += 2;
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
if (/^[-+][A-Za-z0-9]+$/.test(arg)) {
|
|
560
|
+
index += 1;
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
return true;
|
|
566
|
+
}
|
|
567
|
+
function decodeShellControlBuiltin(head, args) {
|
|
568
|
+
if (head === 'set') {
|
|
569
|
+
return decodeSetBuiltin(args) ? [] : null;
|
|
570
|
+
}
|
|
571
|
+
if (head === 'wait') {
|
|
572
|
+
if (args.length === 0 || args.every((arg) => /^\d+$/.test(arg))) {
|
|
573
|
+
return [];
|
|
574
|
+
}
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
if (head === 'exit') {
|
|
578
|
+
if (args.length === 0 || (args.length === 1 && /^-?\d+$/.test(args[0] ?? ''))) {
|
|
579
|
+
return [];
|
|
580
|
+
}
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
function decodeDockerComposeRun(head, args, segment) {
|
|
586
|
+
let composeArgs = null;
|
|
587
|
+
let command = head;
|
|
588
|
+
if (head === 'docker-compose') {
|
|
589
|
+
composeArgs = args;
|
|
590
|
+
}
|
|
591
|
+
else if (head === 'docker' && args[0] === 'compose') {
|
|
592
|
+
composeArgs = args.slice(1);
|
|
593
|
+
command = 'docker';
|
|
594
|
+
}
|
|
595
|
+
if (!composeArgs) {
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
if (composeArgs.includes('run')) {
|
|
599
|
+
return [processRequirement(command, 'spawn', segment, ['process.docker_compose_run'])];
|
|
600
|
+
}
|
|
601
|
+
return unsupportedProcess(command, segment, 'process.docker_compose_grammar_incomplete');
|
|
602
|
+
}
|
|
420
603
|
function decodeProcessOrFilesystem(params) {
|
|
421
604
|
const { tokens, head, env, cwd, repoRoot, segment } = params;
|
|
422
605
|
const args = tokens.slice(1);
|
|
606
|
+
if (tokens.length > 0 && tokens.every((token) => ENV_PREFIX_PATTERN.test(token))) {
|
|
607
|
+
return [];
|
|
608
|
+
}
|
|
609
|
+
const shellControl = decodeShellControlBuiltin(head, args);
|
|
610
|
+
if (shellControl) {
|
|
611
|
+
return shellControl;
|
|
612
|
+
}
|
|
423
613
|
if (isCommandInspection(tokens)) {
|
|
424
614
|
return [processRequirement(head, 'inspect', segment, ['process.inspect.command_lookup'])];
|
|
425
615
|
}
|
|
@@ -500,6 +690,19 @@ function decodeProcessOrFilesystem(params) {
|
|
|
500
690
|
if (runtimeMetadata) {
|
|
501
691
|
return runtimeMetadata;
|
|
502
692
|
}
|
|
693
|
+
if (head === 'ruby') {
|
|
694
|
+
return decodeRuby(args, cwd, repoRoot, segment);
|
|
695
|
+
}
|
|
696
|
+
if (head === 'rubocop' || head === 'rspec') {
|
|
697
|
+
const decoded = decodeBundleExecInner(head, args, segment);
|
|
698
|
+
if (decoded) {
|
|
699
|
+
return decoded;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
const dockerCompose = decodeDockerComposeRun(head, args, segment);
|
|
703
|
+
if (dockerCompose) {
|
|
704
|
+
return dockerCompose;
|
|
705
|
+
}
|
|
503
706
|
if ((head === 'npm' || head === 'pnpm') && args.length === 1 && isMetadataOnlyArgv(args)) {
|
|
504
707
|
return [processRequirement(head, 'inspect', segment, ['process.inspect.package_manager'])];
|
|
505
708
|
}
|
|
@@ -518,6 +721,9 @@ function decodeProcessOrFilesystem(params) {
|
|
|
518
721
|
if (head === 'sed') {
|
|
519
722
|
return decodeSed(args, cwd, segment);
|
|
520
723
|
}
|
|
724
|
+
if (head === 'base64' && args.length === 1 && ['-d', '-D', '--decode'].includes(args[0] ?? '')) {
|
|
725
|
+
return [processRequirement(head, 'inspect', segment, ['process.inspect.base64_stdin'])];
|
|
726
|
+
}
|
|
521
727
|
if (head === 'node') {
|
|
522
728
|
return decodeNode(args, cwd, segment);
|
|
523
729
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { expandMakeExpression, normalizeMakeRecipeLine, parseMakefileVariables, parsePhonyTargets, } from './makefile-expand.js';
|
|
3
4
|
const MAX_RESOLVE_DEPTH = 8;
|
|
4
5
|
const PNPM_BUILTIN_COMMANDS = new Set([
|
|
5
6
|
'add',
|
|
@@ -145,10 +146,9 @@ function resolveNpmRecipe(cwd, repoRoot, scriptName, extraArgs) {
|
|
|
145
146
|
reason: 'npm_script_resolved',
|
|
146
147
|
};
|
|
147
148
|
}
|
|
148
|
-
function
|
|
149
|
+
function parseMakefileRecipeContent(content) {
|
|
149
150
|
const targets = new Map();
|
|
150
151
|
try {
|
|
151
|
-
const content = readFileSync(makefilePath, 'utf8');
|
|
152
152
|
const lines = content.split('\n');
|
|
153
153
|
let currentTarget = null;
|
|
154
154
|
let recipeLines = [];
|
|
@@ -206,7 +206,7 @@ function parseMakefileRecipes(makefilePath) {
|
|
|
206
206
|
}
|
|
207
207
|
return targets;
|
|
208
208
|
}
|
|
209
|
-
function resolveMakeRecipe(cwd, repoRoot, target) {
|
|
209
|
+
function resolveMakeRecipe(cwd, repoRoot, target, cliVars = {}) {
|
|
210
210
|
const candidates = ['Makefile', 'makefile', 'GNUmakefile'];
|
|
211
211
|
let makefilePath = null;
|
|
212
212
|
let searchDir = path.resolve(cwd);
|
|
@@ -227,7 +227,10 @@ function resolveMakeRecipe(cwd, repoRoot, target) {
|
|
|
227
227
|
if (!makefilePath) {
|
|
228
228
|
return { recipes: [], opaque: true, reason: 'unknown_local_effect' };
|
|
229
229
|
}
|
|
230
|
-
const
|
|
230
|
+
const makefileContent = readFileSync(makefilePath, 'utf8');
|
|
231
|
+
const makefileVars = parseMakefileVariables(makefileContent);
|
|
232
|
+
const phonyTargets = parsePhonyTargets(makefileContent);
|
|
233
|
+
const targets = parseMakefileRecipeContent(makefileContent);
|
|
231
234
|
if (!targets.has(target)) {
|
|
232
235
|
return { recipes: [], opaque: true, reason: 'make_target_undefined' };
|
|
233
236
|
}
|
|
@@ -253,7 +256,16 @@ function resolveMakeRecipe(cwd, repoRoot, target) {
|
|
|
253
256
|
return false;
|
|
254
257
|
}
|
|
255
258
|
}
|
|
256
|
-
|
|
259
|
+
// When the requested target has its own recipes, treat those as the
|
|
260
|
+
// authorization authority. Skip .PHONY / `_`-prefixed prerequisite
|
|
261
|
+
// recipes (e.g. `_start_test_deps` docker-compose up) so setup-only
|
|
262
|
+
// side effects do not obscure the target's direct command.
|
|
263
|
+
const skipPhonyPrerequisiteRecipes = name !== target &&
|
|
264
|
+
(phonyTargets.has(name) || name.startsWith('_')) &&
|
|
265
|
+
(targets.get(target)?.recipes.length ?? 0) > 0;
|
|
266
|
+
if (!skipPhonyPrerequisiteRecipes) {
|
|
267
|
+
recipeLines.push(...entry.recipes);
|
|
268
|
+
}
|
|
257
269
|
visiting.delete(name);
|
|
258
270
|
visited.add(name);
|
|
259
271
|
return true;
|
|
@@ -261,15 +273,24 @@ function resolveMakeRecipe(cwd, repoRoot, target) {
|
|
|
261
273
|
if (!collect(target)) {
|
|
262
274
|
return { recipes: recipeLines, opaque: true, reason: 'make_dependency_cycle' };
|
|
263
275
|
}
|
|
276
|
+
const expandedRecipes = [];
|
|
264
277
|
for (const line of recipeLines) {
|
|
265
|
-
|
|
278
|
+
const normalized = normalizeMakeRecipeLine(line);
|
|
279
|
+
const expanded = expandMakeExpression(normalized, cliVars, makefileVars);
|
|
280
|
+
if (expanded === null) {
|
|
266
281
|
return { recipes: recipeLines, opaque: true, reason: 'make_recipe_dynamic' };
|
|
267
282
|
}
|
|
283
|
+
expandedRecipes.push(expanded);
|
|
284
|
+
}
|
|
285
|
+
for (const line of expandedRecipes) {
|
|
286
|
+
if (/\$\(/.test(line) || /\$\{/.test(line)) {
|
|
287
|
+
return { recipes: expandedRecipes, opaque: true, reason: 'make_recipe_dynamic' };
|
|
288
|
+
}
|
|
268
289
|
}
|
|
269
290
|
if (opaquePrerequisites) {
|
|
270
|
-
return { recipes:
|
|
291
|
+
return { recipes: expandedRecipes, opaque: true, reason: 'make_prerequisite_dynamic' };
|
|
271
292
|
}
|
|
272
|
-
return { recipes:
|
|
293
|
+
return { recipes: expandedRecipes, opaque: false, reason: 'make_recipe_resolved' };
|
|
273
294
|
}
|
|
274
295
|
export function resolveLauncherRecipe(params) {
|
|
275
296
|
if (params.depth >= MAX_RESOLVE_DEPTH) {
|
|
@@ -291,8 +312,28 @@ export function resolveLauncherRecipe(params) {
|
|
|
291
312
|
}
|
|
292
313
|
return resolution;
|
|
293
314
|
}
|
|
294
|
-
if (tokens[0] === 'make'
|
|
295
|
-
|
|
315
|
+
if (tokens[0] === 'make') {
|
|
316
|
+
if (tokens.includes('-n') || tokens.includes('--dry-run')) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
let target = null;
|
|
320
|
+
const cliVars = {};
|
|
321
|
+
for (const token of tokens.slice(1)) {
|
|
322
|
+
if (token.startsWith('-')) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const assignment = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(token);
|
|
326
|
+
if (assignment) {
|
|
327
|
+
cliVars[assignment[1] ?? ''] = assignment[2] ?? '';
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (!target) {
|
|
331
|
+
target = token;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (target) {
|
|
335
|
+
return resolveMakeRecipe(params.cwd, params.repoRoot, target, cliVars);
|
|
336
|
+
}
|
|
296
337
|
}
|
|
297
338
|
if (tokens[0] === 'pnpm' && tokens[1] === 'exec' && tokens[2]) {
|
|
298
339
|
return {
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function parseMakefileVariables(content: string): Map<string, string>;
|
|
2
|
+
export declare function parsePhonyTargets(content: string): Set<string>;
|
|
3
|
+
export declare function normalizeMakeRecipeLine(line: string): string;
|
|
4
|
+
export declare function expandMakeExpression(expression: string, cliVars: Readonly<Record<string, string>>, makefileVars: ReadonlyMap<string, string>): string | null;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
const MAX_EXPAND_DEPTH = 16;
|
|
2
|
+
export function parseMakefileVariables(content) {
|
|
3
|
+
const variables = new Map();
|
|
4
|
+
for (const line of content.split('\n')) {
|
|
5
|
+
const trimmed = line.trim();
|
|
6
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
7
|
+
continue;
|
|
8
|
+
}
|
|
9
|
+
const match = /^([A-Za-z_][A-Za-z0-9_]*)\s*[:?]?=\s*(.+)$/.exec(trimmed);
|
|
10
|
+
if (!match) {
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
variables.set(match[1] ?? '', (match[2] ?? '').trim());
|
|
14
|
+
}
|
|
15
|
+
return variables;
|
|
16
|
+
}
|
|
17
|
+
export function parsePhonyTargets(content) {
|
|
18
|
+
const phony = new Set();
|
|
19
|
+
for (const line of content.split('\n')) {
|
|
20
|
+
const trimmed = line.trim();
|
|
21
|
+
const match = /^\.PHONY:\s*(.+)$/.exec(trimmed);
|
|
22
|
+
if (!match) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
for (const token of (match[1] ?? '').split(/\s+/)) {
|
|
26
|
+
if (token) {
|
|
27
|
+
phony.add(token);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return phony;
|
|
32
|
+
}
|
|
33
|
+
export function normalizeMakeRecipeLine(line) {
|
|
34
|
+
let normalized = line.trim();
|
|
35
|
+
while (normalized.startsWith('@') || normalized.startsWith('-') || normalized.startsWith('+')) {
|
|
36
|
+
normalized = normalized.slice(1).trimStart();
|
|
37
|
+
}
|
|
38
|
+
return normalized;
|
|
39
|
+
}
|
|
40
|
+
export function expandMakeExpression(expression, cliVars, makefileVars) {
|
|
41
|
+
if (/\$\(\s*shell\b/i.test(expression) || expression.includes('$$')) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const expanded = expandMakeValue(expression, cliVars, makefileVars, 0);
|
|
46
|
+
if (expanded === null || /\$\(/.test(expanded) || /\$\{/.test(expanded)) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return expanded;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function expandMakeValue(expression, cliVars, makefileVars, depth) {
|
|
56
|
+
if (depth > MAX_EXPAND_DEPTH) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
let value = expression.trim();
|
|
60
|
+
let changed = true;
|
|
61
|
+
let iterations = 0;
|
|
62
|
+
while (changed && iterations < MAX_EXPAND_DEPTH) {
|
|
63
|
+
changed = false;
|
|
64
|
+
iterations += 1;
|
|
65
|
+
const orMatch = value.match(/\$\(\s*or\s+([^()]*(?:\([^)]*\)[^()]*)*)\)/);
|
|
66
|
+
if (orMatch) {
|
|
67
|
+
const [fullMatch, inner] = orMatch;
|
|
68
|
+
const parts = splitMakeFunctionArgs(inner ?? '');
|
|
69
|
+
let selected = null;
|
|
70
|
+
for (const part of parts) {
|
|
71
|
+
const expanded = expandMakeValue(part.trim(), cliVars, makefileVars, depth + 1);
|
|
72
|
+
if (expanded !== null && expanded.trim() !== '') {
|
|
73
|
+
selected = expanded;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (selected === null) {
|
|
78
|
+
const fallback = parts.at(-1)?.trim();
|
|
79
|
+
selected =
|
|
80
|
+
fallback === undefined ? '' : expandMakeValue(fallback, cliVars, makefileVars, depth + 1);
|
|
81
|
+
}
|
|
82
|
+
if (selected === null) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
value = value.replace(fullMatch, selected);
|
|
86
|
+
changed = true;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const varMatch = value.match(/\$\(([A-Za-z_][A-Za-z0-9_]*)\)/);
|
|
90
|
+
if (varMatch) {
|
|
91
|
+
const [fullMatch, name] = varMatch;
|
|
92
|
+
const resolved = resolveMakeVariable(name ?? '', cliVars, makefileVars, depth + 1);
|
|
93
|
+
if (resolved === null) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
value = value.replace(fullMatch, resolved);
|
|
97
|
+
changed = true;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const bracedMatch = value.match(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/);
|
|
101
|
+
if (bracedMatch) {
|
|
102
|
+
const [fullMatch, name] = bracedMatch;
|
|
103
|
+
const resolved = name === 'PWD' ? '.' : resolveMakeVariable(name ?? '', cliVars, makefileVars, depth + 1);
|
|
104
|
+
if (resolved === null) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
value = value.replace(fullMatch, resolved);
|
|
108
|
+
changed = true;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
function resolveMakeVariable(name, cliVars, makefileVars, depth) {
|
|
116
|
+
if (Object.hasOwn(cliVars, name)) {
|
|
117
|
+
return cliVars[name] ?? '';
|
|
118
|
+
}
|
|
119
|
+
const definition = makefileVars.get(name);
|
|
120
|
+
if (definition === undefined) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
return expandMakeValue(definition, cliVars, makefileVars, depth);
|
|
124
|
+
}
|
|
125
|
+
function splitMakeFunctionArgs(input) {
|
|
126
|
+
const parts = [];
|
|
127
|
+
let current = '';
|
|
128
|
+
let depth = 0;
|
|
129
|
+
for (const char of input) {
|
|
130
|
+
if (char === '(') {
|
|
131
|
+
depth += 1;
|
|
132
|
+
current += char;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (char === ')') {
|
|
136
|
+
depth -= 1;
|
|
137
|
+
current += char;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (char === ',' && depth === 0) {
|
|
141
|
+
parts.push(current.trim());
|
|
142
|
+
current = '';
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
current += char;
|
|
146
|
+
}
|
|
147
|
+
if (current.trim()) {
|
|
148
|
+
parts.push(current.trim());
|
|
149
|
+
}
|
|
150
|
+
return parts;
|
|
151
|
+
}
|
|
@@ -16,6 +16,7 @@ export declare function peelTransparentWrappers(tokens: string[]): {
|
|
|
16
16
|
export declare function isVariableIndirectHead(head: string): boolean;
|
|
17
17
|
export declare function extractEvalBody(tokens: string[]): string | null;
|
|
18
18
|
export declare function extractRecursiveScript(tokens: string[]): string | null;
|
|
19
|
+
export declare function extractDockerComposeRunScript(tokens: string[]): string | null;
|
|
19
20
|
/**
|
|
20
21
|
* True when a recursive script is evaluated from a command argument rather
|
|
21
22
|
* than expanded from a static launcher recipe. Callers use this semantic fact
|
|
@@ -300,7 +300,6 @@ export function extractRecursiveScript(tokens) {
|
|
|
300
300
|
return null;
|
|
301
301
|
}
|
|
302
302
|
const head = normalizeHead(filtered[0] ?? '');
|
|
303
|
-
const second = filtered[1] ?? '';
|
|
304
303
|
if (head === 'eval') {
|
|
305
304
|
const body = filtered.slice(1).join(' ').trim();
|
|
306
305
|
return body || null;
|
|
@@ -308,20 +307,33 @@ export function extractRecursiveScript(tokens) {
|
|
|
308
307
|
if (SHELL_INTERPRETERS.has(head) || CODE_INTERPRETERS.has(head)) {
|
|
309
308
|
const flagIndex = filtered.findIndex((token) => SCRIPT_FLAGS.has(token));
|
|
310
309
|
if (flagIndex !== -1) {
|
|
311
|
-
const body = filtered
|
|
312
|
-
.slice(flagIndex + 1)
|
|
313
|
-
.join(' ')
|
|
314
|
-
.replace(/^['"]|['"]$/g, '')
|
|
315
|
-
.trim();
|
|
310
|
+
const body = filtered[flagIndex + 1] ?? '';
|
|
316
311
|
return body || null;
|
|
317
312
|
}
|
|
318
313
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
export function extractDockerComposeRunScript(tokens) {
|
|
317
|
+
const head = normalizeHead(tokens[0] ?? '');
|
|
318
|
+
const usesCompose = head === 'docker-compose' || (head === 'docker' && (tokens[1] ?? '') === 'compose');
|
|
319
|
+
if (!usesCompose) {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
if (!tokens.includes('run')) {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
const runIndex = tokens.indexOf('run');
|
|
326
|
+
const tail = tokens.slice(runIndex + 1);
|
|
327
|
+
for (let index = 0; index < tail.length; index += 1) {
|
|
328
|
+
const shellHead = normalizeHead(tail[index] ?? '');
|
|
329
|
+
if (!SHELL_INTERPRETERS.has(shellHead)) {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const flag = tail[index + 1] ?? '';
|
|
333
|
+
if (flag !== '-lc' && flag !== '-c') {
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
const body = tail[index + 2] ?? '';
|
|
325
337
|
return body || null;
|
|
326
338
|
}
|
|
327
339
|
return null;
|
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
* These commands guard classifier availability in tests and never grant runtime authority.
|
|
4
4
|
* @see src/__tests__/verdict/structural-suite.test.ts
|
|
5
5
|
*/
|
|
6
|
-
export declare const BENIGN_PROBE_CORES: readonly ["npm test", "npm run build", "pnpm test", "pnpm build", "pnpm vitest run src/example.test.ts", "bash -lc 'git status'", "belay approve belay_deadbeef1234", "bundle -v", "ruby -v", "yarn --version", "make -n test", "bin/rails routes", "bundle exec rubocop --version"];
|
|
6
|
+
export declare const BENIGN_PROBE_CORES: readonly ["npm test", "npm run build", "pnpm test", "pnpm build", "pnpm vitest run src/example.test.ts", "bash -lc 'git status'", "belay approve belay_deadbeef1234", "bundle -v", "ruby -v", "yarn --version", "make -n test", "bin/rails routes", "bundle exec rubocop --version", "bundle exec rubocop test/upgrade_script_contract_test.rb", "ruby -Itest test/upgrade_script_contract_test.rb"];
|
package/dist/defaults.js
CHANGED
|
@@ -40,7 +40,7 @@ export function getManagedHookEntries(platform = process.platform, hooksDir, rep
|
|
|
40
40
|
definition: {
|
|
41
41
|
command: toolGate,
|
|
42
42
|
placement: 'prepend',
|
|
43
|
-
matcher: '
|
|
43
|
+
matcher: 'Write',
|
|
44
44
|
},
|
|
45
45
|
},
|
|
46
46
|
{
|
|
@@ -48,7 +48,7 @@ export function getManagedHookEntries(platform = process.platform, hooksDir, rep
|
|
|
48
48
|
definition: {
|
|
49
49
|
command: toolGate,
|
|
50
50
|
placement: 'prepend',
|
|
51
|
-
matcher: '
|
|
51
|
+
matcher: 'StrReplace',
|
|
52
52
|
},
|
|
53
53
|
},
|
|
54
54
|
{
|
|
@@ -56,7 +56,7 @@ export function getManagedHookEntries(platform = process.platform, hooksDir, rep
|
|
|
56
56
|
definition: {
|
|
57
57
|
command: toolGate,
|
|
58
58
|
placement: 'prepend',
|
|
59
|
-
matcher: '
|
|
59
|
+
matcher: 'Delete',
|
|
60
60
|
},
|
|
61
61
|
},
|
|
62
62
|
{
|
|
@@ -64,7 +64,8 @@ export function getManagedHookEntries(platform = process.platform, hooksDir, rep
|
|
|
64
64
|
definition: {
|
|
65
65
|
command: toolGate,
|
|
66
66
|
placement: 'prepend',
|
|
67
|
-
|
|
67
|
+
// Agent Shell tool (preToolUse) — distinct from terminal beforeShellExecution.
|
|
68
|
+
matcher: 'Shell',
|
|
68
69
|
},
|
|
69
70
|
},
|
|
70
71
|
{
|