@guilz-dev/belay 0.9.1 → 0.9.3
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 +1 -1
- package/dist/adapters/codex/runtime-entry.d.ts +3 -0
- package/dist/adapters/codex/runtime-entry.js +27 -4
- package/dist/adapters/cursor/cwd-resolution.d.ts +10 -0
- package/dist/adapters/cursor/cwd-resolution.js +58 -0
- package/dist/adapters/cursor/hooks.d.ts +5 -3
- package/dist/adapters/cursor/hooks.js +29 -20
- package/dist/adapters/cursor/runtime-entry.d.ts +1 -0
- package/dist/adapters/cursor/runtime-entry.js +107 -6
- package/dist/adapters/shared/gate-runtime.js +26 -3
- package/dist/adapters/shared/repo-root.js +20 -1
- package/dist/bundle/claude-runtime.mjs +1225 -287
- package/dist/bundle/codex-runtime.mjs +1247 -291
- package/dist/bundle/cursor-runtime.mjs +4320 -3154
- package/dist/cli.js +33 -3
- package/dist/commands/doctor.js +38 -9
- package/dist/commands/health-snapshot.d.ts +3 -0
- package/dist/commands/health-snapshot.js +56 -0
- package/dist/commands/report.js +14 -0
- package/dist/commands/status.js +15 -0
- package/dist/commands/where.d.ts +4 -0
- package/dist/commands/where.js +52 -0
- package/dist/core/approval-repo-lookup.d.ts +16 -0
- package/dist/core/approval-repo-lookup.js +48 -0
- package/dist/core/audit-io.d.ts +1 -1
- package/dist/core/audit-io.js +1 -1
- package/dist/core/audit-legacy-archive.d.ts +1 -0
- package/dist/core/audit-legacy-archive.js +5 -0
- package/dist/core/audit-query.d.ts +1 -0
- package/dist/core/audit-query.js +7 -0
- package/dist/core/audit-serialize.d.ts +3 -0
- package/dist/core/audit-serialize.js +39 -3
- package/dist/core/audit-summary.d.ts +9 -0
- package/dist/core/audit-summary.js +58 -1
- package/dist/core/audit-types.d.ts +4 -0
- package/dist/core/effect-ir/shell-lower.js +283 -27
- package/dist/core/replay-scrub.d.ts +1 -0
- package/dist/core/replay-scrub.js +22 -3
- package/dist/core/shell-tokenizer.d.ts +28 -0
- package/dist/core/shell-tokenizer.js +111 -29
- package/dist/core/verdict/docker-compose-run.d.ts +18 -0
- package/dist/core/verdict/docker-compose-run.js +136 -0
- package/dist/core/verdict/launcher-resolve.js +66 -25
- 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 +4 -0
- package/dist/core/verdict/parser.js +25 -30
- package/dist/core/verdict/recursive-invocation.d.ts +20 -0
- package/dist/core/verdict/recursive-invocation.js +224 -0
- package/dist/corpus/benign-probe-cores.d.ts +1 -1
- package/dist/corpus/benign-probe-cores.js +2 -0
- package/dist/defaults.js +16 -0
- package/dist/installer/scope-config.d.ts +2 -2
- package/dist/installer.d.ts +10 -1
- package/dist/installer.js +43 -2
- package/dist/types.d.ts +34 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -2
- package/skills/belay/SKILL.md +5 -0
- package/skills/belay/belay-report.md +4 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { filterAuditRecords, inferWouldBlock, isApprovalRecorded, isGateRecord, parseTimestamp, recordStringField, } from './audit-query.js';
|
|
1
|
+
import { auditToolInvocationCorrelationId, filterAuditRecords, inferWouldBlock, isApprovalRecorded, isGateRecord, parseTimestamp, recordStringField, } from './audit-query.js';
|
|
2
2
|
export const DEFAULT_SILENT_PASS_THRESHOLD = 0.5;
|
|
3
3
|
export const MIN_GATE_EVENTS_FOR_FENCE_DRIFT = 20;
|
|
4
4
|
function isTier0Reason(reason) {
|
|
@@ -38,6 +38,7 @@ function isGateEventRecord(record) {
|
|
|
38
38
|
export function summarizeAuditVisibility(records, filter = {}, options = {}) {
|
|
39
39
|
const filtered = filterAuditRecords(records, filter);
|
|
40
40
|
const gateRecords = filtered.filter(isGateEventRecord);
|
|
41
|
+
const allGateRecords = records.filter(isGateEventRecord);
|
|
41
42
|
const recentAskLimit = options.recentAskLimit ?? 10;
|
|
42
43
|
let askCount = 0;
|
|
43
44
|
let enforceAskCount = 0;
|
|
@@ -46,6 +47,23 @@ export function summarizeAuditVisibility(records, filter = {}, options = {}) {
|
|
|
46
47
|
let flagCount = 0;
|
|
47
48
|
let allowCount = 0;
|
|
48
49
|
const recentAsks = [];
|
|
50
|
+
const allowedByInvocation = new Map();
|
|
51
|
+
const recentHostDenials = [];
|
|
52
|
+
let unrecognizedHostFailureCount = 0;
|
|
53
|
+
const matchedHostDenials = new Set();
|
|
54
|
+
for (const record of allGateRecords) {
|
|
55
|
+
const invocationId = auditToolInvocationCorrelationId(record);
|
|
56
|
+
if (invocationId && !inferWouldBlock(record) && record.permission === 'allow') {
|
|
57
|
+
const existing = allowedByInvocation.get(invocationId);
|
|
58
|
+
const recordMs = parseTimestamp(record.timestamp) ?? Number.NEGATIVE_INFINITY;
|
|
59
|
+
const existingMs = existing
|
|
60
|
+
? (parseTimestamp(existing.timestamp) ?? Number.NEGATIVE_INFINITY)
|
|
61
|
+
: Number.NEGATIVE_INFINITY;
|
|
62
|
+
if (!existing || recordMs >= existingMs) {
|
|
63
|
+
allowedByInvocation.set(invocationId, record);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
49
67
|
for (const record of gateRecords) {
|
|
50
68
|
if (inferWouldBlock(record)) {
|
|
51
69
|
askCount += 1;
|
|
@@ -73,11 +91,47 @@ export function summarizeAuditVisibility(records, filter = {}, options = {}) {
|
|
|
73
91
|
allowCount += 1;
|
|
74
92
|
}
|
|
75
93
|
}
|
|
94
|
+
for (const record of filtered) {
|
|
95
|
+
if (record.event !== 'postToolUseFailure') {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (record.failureType !== 'permission_denied') {
|
|
99
|
+
unrecognizedHostFailureCount += 1;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const invocationId = auditToolInvocationCorrelationId(record);
|
|
103
|
+
const gate = invocationId ? allowedByInvocation.get(invocationId) : undefined;
|
|
104
|
+
if (!gate) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const gateMs = parseTimestamp(gate.timestamp);
|
|
108
|
+
const failureMs = parseTimestamp(record.timestamp);
|
|
109
|
+
if (gateMs !== null && failureMs !== null && failureMs < gateMs) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (invocationId && matchedHostDenials.has(invocationId)) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (invocationId) {
|
|
116
|
+
matchedHostDenials.add(invocationId);
|
|
117
|
+
}
|
|
118
|
+
recentHostDenials.push({
|
|
119
|
+
gateTimestamp: gate.timestamp,
|
|
120
|
+
failureTimestamp: record.timestamp,
|
|
121
|
+
summary: typeof gate.summary === 'string' ? gate.summary : '',
|
|
122
|
+
errorMessage: typeof record.errorMessage === 'string' ? record.errorMessage : '',
|
|
123
|
+
});
|
|
124
|
+
}
|
|
76
125
|
recentAsks.sort((left, right) => {
|
|
77
126
|
const leftMs = parseTimestamp(left.timestamp) ?? 0;
|
|
78
127
|
const rightMs = parseTimestamp(right.timestamp) ?? 0;
|
|
79
128
|
return rightMs - leftMs;
|
|
80
129
|
});
|
|
130
|
+
recentHostDenials.sort((left, right) => {
|
|
131
|
+
const leftMs = parseTimestamp(left.failureTimestamp) ?? 0;
|
|
132
|
+
const rightMs = parseTimestamp(right.failureTimestamp) ?? 0;
|
|
133
|
+
return rightMs - leftMs;
|
|
134
|
+
});
|
|
81
135
|
const gateEvents = gateRecords.length;
|
|
82
136
|
const silentPassRate = gateEvents > 0 ? (allowCount + flagCount) / gateEvents : 0;
|
|
83
137
|
return {
|
|
@@ -90,6 +144,9 @@ export function summarizeAuditVisibility(records, filter = {}, options = {}) {
|
|
|
90
144
|
allowCount,
|
|
91
145
|
silentPassRate,
|
|
92
146
|
recentAsks: recentAsks.slice(0, recentAskLimit),
|
|
147
|
+
hostDeniedAfterAllowCount: recentHostDenials.length,
|
|
148
|
+
recentHostDenials: recentHostDenials.slice(0, recentAskLimit),
|
|
149
|
+
unrecognizedHostFailureCount,
|
|
93
150
|
};
|
|
94
151
|
}
|
|
95
152
|
export function detectFenceDrift(summary, options = {}) {
|
|
@@ -12,6 +12,10 @@ export interface AuditRecord {
|
|
|
12
12
|
fingerprint?: string;
|
|
13
13
|
summary?: string;
|
|
14
14
|
approvalId?: string;
|
|
15
|
+
toolInvocationCorrelationId?: string;
|
|
16
|
+
toolName?: string;
|
|
17
|
+
failureType?: string;
|
|
18
|
+
errorMessage?: string;
|
|
15
19
|
wouldBlock?: boolean;
|
|
16
20
|
judgeFallbackReason?: string;
|
|
17
21
|
permission?: string;
|
|
@@ -1,11 +1,13 @@
|
|
|
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 {
|
|
4
|
+
import { canonicalPath, pathWithinRoot } from '../path-utils.js';
|
|
5
|
+
import { isFdDuplication, isRedirectOperator, lexShell, tokenizeShell, } from '../shell-tokenizer.js';
|
|
6
|
+
import { decodeDockerComposeRun as decodeStructuredDockerComposeRun } from '../verdict/docker-compose-run.js';
|
|
5
7
|
import { decodeEgressEffects } from '../verdict/egress-classify.js';
|
|
6
8
|
import { decodeGitEffects } from '../verdict/git-classifier.js';
|
|
7
9
|
import { resolveLauncherRecipe } from '../verdict/launcher-resolve.js';
|
|
8
|
-
import {
|
|
10
|
+
import { decodeRecursiveInvocationTokens, isCommandInspection, parseSegment, redactCommand, segmentOpacity, splitStructuralShellSegments, structuralSubstitutionInners, } from '../verdict/parser.js';
|
|
9
11
|
import { joinEffectOpacity } from './normalize.js';
|
|
10
12
|
import { classifyPackageAcquisitionSpec, innerRecipeFromPeel, peelPackageExecArgv, resolveLocalBin, } from './package-exec.js';
|
|
11
13
|
import { buildShellEffectPlan, } from './shell-build.js';
|
|
@@ -126,15 +128,29 @@ function joinNestedOpacity(outer, nested) {
|
|
|
126
128
|
}
|
|
127
129
|
function lowerSegment(command, context) {
|
|
128
130
|
const commandRedacted = redactCommand(command);
|
|
129
|
-
const
|
|
131
|
+
const lexed = lexShell(command);
|
|
132
|
+
const rawTokens = lexed.tokens.map((token) => token.value);
|
|
130
133
|
const environment = extractEnvironment(rawTokens, context.env);
|
|
131
134
|
const env = environment.env;
|
|
132
135
|
const parsed = parseSegment(command);
|
|
133
|
-
const
|
|
136
|
+
const parsedTokens = environment.commandTokens ?? parsed.tokens;
|
|
137
|
+
const tokens = stripRedirects(parsedTokens.length === 0 &&
|
|
138
|
+
rawTokens.length > 0 &&
|
|
139
|
+
rawTokens.every((token) => ENV_PREFIX_PATTERN.test(token))
|
|
140
|
+
? rawTokens
|
|
141
|
+
: parsedTokens).map((token) => expandKnownVariables(token, env));
|
|
142
|
+
const decoderTokens = alignStructuredTokens(stripStructuredRedirects(lexed.tokens), stripRedirects(parsedTokens));
|
|
134
143
|
const head = path.basename(tokens[0] ?? parsed.head);
|
|
135
144
|
let opacity = segmentOpacity(command);
|
|
136
145
|
const signals = new Set();
|
|
137
146
|
const requirements = [];
|
|
147
|
+
if (!lexed.complete) {
|
|
148
|
+
requirements.push(requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, commandRedacted, [
|
|
149
|
+
'shell.grammar_incomplete',
|
|
150
|
+
]));
|
|
151
|
+
signals.add('shell.grammar_incomplete');
|
|
152
|
+
opacity = joinEffectOpacity(opacity, 'unparseable');
|
|
153
|
+
}
|
|
138
154
|
addRedirectEffects(requirements, rawTokens, env, context, commandRedacted);
|
|
139
155
|
addSubstitutionEffects(requirements, command, context, commandRedacted, signals);
|
|
140
156
|
if (environment.malformed) {
|
|
@@ -150,7 +166,7 @@ function lowerSegment(command, context) {
|
|
|
150
166
|
signals.add('shell.xargs_stdin_dynamic');
|
|
151
167
|
opacity = joinEffectOpacity(opacity, 'opaque');
|
|
152
168
|
}
|
|
153
|
-
if (context.depth
|
|
169
|
+
if (context.depth > MAX_LOWER_DEPTH) {
|
|
154
170
|
requirements.push(requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, commandRedacted, [
|
|
155
171
|
'shell.lower_depth_exceeded',
|
|
156
172
|
]));
|
|
@@ -185,31 +201,69 @@ function lowerSegment(command, context) {
|
|
|
185
201
|
}
|
|
186
202
|
return shellSegment(commandRedacted, head, requirements, opacity, signals);
|
|
187
203
|
}
|
|
188
|
-
const
|
|
189
|
-
if (
|
|
190
|
-
|
|
191
|
-
requirements.push(processRequirement(head || 'sh', 'spawn', commandRedacted, [
|
|
204
|
+
const recursive = decodeRecursiveInvocationTokens(decoderTokens);
|
|
205
|
+
if (recursive.kind === 'static' && opacity !== 'opaque' && opacity !== 'unparseable') {
|
|
206
|
+
requirements.push(processRequirement(recursive.interpreter, 'spawn', commandRedacted, [
|
|
192
207
|
'shell.recursive_wrapper',
|
|
193
|
-
|
|
208
|
+
'dynamic_shell_evaluation',
|
|
194
209
|
]));
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
210
|
+
if (recursive.script !== '') {
|
|
211
|
+
const nested = lowerTopLevelSegments(recursive.script, {
|
|
212
|
+
...context,
|
|
213
|
+
command: recursive.script,
|
|
214
|
+
env,
|
|
215
|
+
depth: context.depth + 1,
|
|
216
|
+
});
|
|
217
|
+
for (const nestedSegment of nested) {
|
|
218
|
+
requirements.push(...nestedSegment.requirements.map((entry) => withInnerProvenance(entry, recursive.script, head, commandRedacted)));
|
|
219
|
+
for (const signal of nestedSegment.signals)
|
|
220
|
+
signals.add(signal);
|
|
205
221
|
}
|
|
206
222
|
}
|
|
207
223
|
signals.add('shell.recursive_wrapper');
|
|
208
|
-
|
|
209
|
-
|
|
224
|
+
signals.add('dynamic_shell_evaluation');
|
|
225
|
+
return shellSegment(commandRedacted, head, requirements, 'recursive', signals);
|
|
226
|
+
}
|
|
227
|
+
if (recursive.kind === 'dynamic' || recursive.kind === 'indeterminate') {
|
|
228
|
+
const recursiveSignals = [
|
|
229
|
+
recursive.signal,
|
|
230
|
+
...(recursive.kind === 'dynamic' ? ['dynamic_shell_evaluation'] : []),
|
|
231
|
+
];
|
|
232
|
+
requirements.push(processRequirement(recursive.interpreter, 'spawn', commandRedacted, recursiveSignals), requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, commandRedacted, [
|
|
233
|
+
...recursiveSignals,
|
|
234
|
+
]));
|
|
235
|
+
for (const signal of recursiveSignals)
|
|
236
|
+
signals.add(signal);
|
|
237
|
+
return shellSegment(commandRedacted, head, requirements, joinEffectOpacity(opacity, 'opaque'), signals);
|
|
238
|
+
}
|
|
239
|
+
const compose = decodeStructuredDockerComposeRun(decoderTokens);
|
|
240
|
+
if (compose.kind === 'recursive' && opacity !== 'opaque' && opacity !== 'unparseable') {
|
|
241
|
+
requirements.push(processRequirement(head, 'spawn', commandRedacted, ['process.docker_compose_run']));
|
|
242
|
+
if (compose.script !== '') {
|
|
243
|
+
const nested = lowerTopLevelSegments(compose.script, {
|
|
244
|
+
...context,
|
|
245
|
+
command: compose.script,
|
|
246
|
+
env,
|
|
247
|
+
depth: context.depth + 1,
|
|
248
|
+
});
|
|
249
|
+
for (const nestedSegment of nested) {
|
|
250
|
+
requirements.push(...nestedSegment.requirements.map((entry) => withInnerProvenance(entry, compose.script, head, commandRedacted)));
|
|
251
|
+
for (const signal of nestedSegment.signals)
|
|
252
|
+
signals.add(signal);
|
|
253
|
+
opacity = joinNestedOpacity(opacity, nestedSegment);
|
|
254
|
+
}
|
|
210
255
|
}
|
|
256
|
+
signals.add('process.docker_compose_run');
|
|
211
257
|
return shellSegment(commandRedacted, head, requirements, 'recursive', signals);
|
|
212
258
|
}
|
|
259
|
+
if (compose.kind === 'dynamic' || compose.kind === 'indeterminate') {
|
|
260
|
+
requirements.push(processRequirement(head, 'spawn', commandRedacted, ['process.docker_compose_run']), requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, commandRedacted, [
|
|
261
|
+
compose.signal,
|
|
262
|
+
]));
|
|
263
|
+
signals.add('process.docker_compose_run');
|
|
264
|
+
signals.add(compose.signal);
|
|
265
|
+
return shellSegment(commandRedacted, head, requirements, joinEffectOpacity(opacity, 'opaque'), signals);
|
|
266
|
+
}
|
|
213
267
|
const launcher = resolveLauncherRecipe({
|
|
214
268
|
tokens,
|
|
215
269
|
cwd: context.cwd,
|
|
@@ -381,6 +435,101 @@ function railsReadOnlySubcommand(args) {
|
|
|
381
435
|
}
|
|
382
436
|
return RAILS_READ_ONLY_SUBCOMMANDS.has(subcommand);
|
|
383
437
|
}
|
|
438
|
+
function isRubyTestScript(scriptPath) {
|
|
439
|
+
const base = path.basename(scriptPath);
|
|
440
|
+
return base.endsWith('_test.rb') || base.endsWith('_spec.rb');
|
|
441
|
+
}
|
|
442
|
+
function parseRubyTestInvocation(args) {
|
|
443
|
+
const includePaths = [];
|
|
444
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
445
|
+
const arg = args[index] ?? '';
|
|
446
|
+
if (arg === '-e' || arg === '-r') {
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
if (arg === '-I') {
|
|
450
|
+
const includePath = args[index + 1];
|
|
451
|
+
if (!includePath) {
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
includePaths.push(includePath);
|
|
455
|
+
index += 1;
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
if (arg.startsWith('-I') && arg.length > 2) {
|
|
459
|
+
includePaths.push(arg.slice(2));
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
if (arg.startsWith('-')) {
|
|
463
|
+
if (arg === '-n') {
|
|
464
|
+
if (!args[index + 1]) {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
index += 1;
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
if (arg.startsWith('-n')) {
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
if (isRubyTestScript(arg)) {
|
|
476
|
+
return { includePaths, scriptPath: arg };
|
|
477
|
+
}
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
function isRubocopMutating(args) {
|
|
483
|
+
return args.some((arg) => arg === '-A' ||
|
|
484
|
+
arg === '-a' ||
|
|
485
|
+
arg === '--auto-correct' ||
|
|
486
|
+
arg === '--autocorrect' ||
|
|
487
|
+
arg.startsWith('--auto-correct-all') ||
|
|
488
|
+
arg.startsWith('--autocorrect-all'));
|
|
489
|
+
}
|
|
490
|
+
function decodeBundleExecInner(innerHead, innerArgs, segment) {
|
|
491
|
+
const innerBase = executableBaseName(innerHead);
|
|
492
|
+
if (innerBase === 'rubocop') {
|
|
493
|
+
const mutating = isRubocopMutating(innerArgs);
|
|
494
|
+
return [
|
|
495
|
+
processRequirement(innerHead, mutating ? 'spawn' : 'inspect', segment, mutating ? ['process.linter.mutating'] : ['process.inspect.linter']),
|
|
496
|
+
];
|
|
497
|
+
}
|
|
498
|
+
if (innerBase === 'rspec') {
|
|
499
|
+
const targetArgs = innerArgs.filter((arg) => !arg.startsWith('-'));
|
|
500
|
+
if (targetArgs.length === 0) {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
return [processRequirement(innerHead, 'spawn', segment, ['process.test_runner.rspec'])];
|
|
504
|
+
}
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
function decodeRuby(args, cwd, repoRoot, segment) {
|
|
508
|
+
const parsed = parseRubyTestInvocation(args);
|
|
509
|
+
if (!parsed) {
|
|
510
|
+
return unsupportedProcess('ruby', segment, 'process.ruby_grammar_incomplete');
|
|
511
|
+
}
|
|
512
|
+
const scriptPath = resolvePathOperand(parsed.scriptPath, cwd);
|
|
513
|
+
if (!pathWithinRoot(canonicalPath(repoRoot), canonicalPath(scriptPath))) {
|
|
514
|
+
return unsupportedProcess('ruby', segment, 'process.ruby_outside_repo');
|
|
515
|
+
}
|
|
516
|
+
for (const includePath of parsed.includePaths) {
|
|
517
|
+
const resolvedInclude = resolvePathOperand(includePath, cwd);
|
|
518
|
+
if (!pathWithinRoot(canonicalPath(repoRoot), canonicalPath(resolvedInclude))) {
|
|
519
|
+
return unsupportedProcess('ruby', segment, 'process.ruby_outside_repo');
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
const lowered = [
|
|
523
|
+
processRequirement('ruby', 'spawn', segment, ['process.test_runner.minitest']),
|
|
524
|
+
requirement('fs.read', 'fs.read', { kind: 'path', path: scriptPath }, segment, [
|
|
525
|
+
'ruby.minitest_script_read',
|
|
526
|
+
]),
|
|
527
|
+
];
|
|
528
|
+
for (const includePath of parsed.includePaths) {
|
|
529
|
+
lowered.push(requirement('fs.read', 'fs.read', { kind: 'path', path: resolvePathOperand(includePath, cwd) }, segment, ['ruby.minitest_load_path_read']));
|
|
530
|
+
}
|
|
531
|
+
return lowered;
|
|
532
|
+
}
|
|
384
533
|
function decodeRuntimeMetadataProcess(head, args, segment) {
|
|
385
534
|
if (head === 'bundle') {
|
|
386
535
|
if (args.length === 1 && isMetadataOnlyArgv(args)) {
|
|
@@ -402,6 +551,10 @@ function decodeRuntimeMetadataProcess(head, args, segment) {
|
|
|
402
551
|
]),
|
|
403
552
|
];
|
|
404
553
|
}
|
|
554
|
+
const bundleExecInner = decodeBundleExecInner(innerHead, innerArgs, segment);
|
|
555
|
+
if (bundleExecInner) {
|
|
556
|
+
return bundleExecInner;
|
|
557
|
+
}
|
|
405
558
|
}
|
|
406
559
|
return null;
|
|
407
560
|
}
|
|
@@ -417,9 +570,75 @@ function decodeRuntimeMetadataProcess(head, args, segment) {
|
|
|
417
570
|
}
|
|
418
571
|
return null;
|
|
419
572
|
}
|
|
573
|
+
function decodeSetBuiltin(args) {
|
|
574
|
+
let index = 0;
|
|
575
|
+
while (index < args.length) {
|
|
576
|
+
const arg = args[index] ?? '';
|
|
577
|
+
if (arg === '--') {
|
|
578
|
+
index += 1;
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
if (arg === '-o' || arg === '+o') {
|
|
582
|
+
if (!args[index + 1]) {
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
index += 2;
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
if (/^[-+][A-Za-z0-9]+$/.test(arg)) {
|
|
589
|
+
index += 1;
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
return true;
|
|
595
|
+
}
|
|
596
|
+
function decodeShellControlBuiltin(head, args) {
|
|
597
|
+
if (head === 'set') {
|
|
598
|
+
return decodeSetBuiltin(args) ? [] : null;
|
|
599
|
+
}
|
|
600
|
+
if (head === 'wait') {
|
|
601
|
+
if (args.length === 0 || args.every((arg) => /^\d+$/.test(arg))) {
|
|
602
|
+
return [];
|
|
603
|
+
}
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
if (head === 'exit') {
|
|
607
|
+
if (args.length === 0 || (args.length === 1 && /^-?\d+$/.test(args[0] ?? ''))) {
|
|
608
|
+
return [];
|
|
609
|
+
}
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
function decodeDockerComposeRun(head, args, segment) {
|
|
615
|
+
let composeArgs = null;
|
|
616
|
+
let command = head;
|
|
617
|
+
if (head === 'docker-compose') {
|
|
618
|
+
composeArgs = args;
|
|
619
|
+
}
|
|
620
|
+
else if (head === 'docker' && args[0] === 'compose') {
|
|
621
|
+
composeArgs = args.slice(1);
|
|
622
|
+
command = 'docker';
|
|
623
|
+
}
|
|
624
|
+
if (!composeArgs) {
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
if (composeArgs.includes('run')) {
|
|
628
|
+
return [processRequirement(command, 'spawn', segment, ['process.docker_compose_run'])];
|
|
629
|
+
}
|
|
630
|
+
return unsupportedProcess(command, segment, 'process.docker_compose_grammar_incomplete');
|
|
631
|
+
}
|
|
420
632
|
function decodeProcessOrFilesystem(params) {
|
|
421
633
|
const { tokens, head, env, cwd, repoRoot, segment } = params;
|
|
422
634
|
const args = tokens.slice(1);
|
|
635
|
+
if (tokens.length > 0 && tokens.every((token) => ENV_PREFIX_PATTERN.test(token))) {
|
|
636
|
+
return [];
|
|
637
|
+
}
|
|
638
|
+
const shellControl = decodeShellControlBuiltin(head, args);
|
|
639
|
+
if (shellControl) {
|
|
640
|
+
return shellControl;
|
|
641
|
+
}
|
|
423
642
|
if (isCommandInspection(tokens)) {
|
|
424
643
|
return [processRequirement(head, 'inspect', segment, ['process.inspect.command_lookup'])];
|
|
425
644
|
}
|
|
@@ -500,6 +719,19 @@ function decodeProcessOrFilesystem(params) {
|
|
|
500
719
|
if (runtimeMetadata) {
|
|
501
720
|
return runtimeMetadata;
|
|
502
721
|
}
|
|
722
|
+
if (head === 'ruby') {
|
|
723
|
+
return decodeRuby(args, cwd, repoRoot, segment);
|
|
724
|
+
}
|
|
725
|
+
if (head === 'rubocop' || head === 'rspec') {
|
|
726
|
+
const decoded = decodeBundleExecInner(head, args, segment);
|
|
727
|
+
if (decoded) {
|
|
728
|
+
return decoded;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
const dockerCompose = decodeDockerComposeRun(head, args, segment);
|
|
732
|
+
if (dockerCompose) {
|
|
733
|
+
return dockerCompose;
|
|
734
|
+
}
|
|
503
735
|
if ((head === 'npm' || head === 'pnpm') && args.length === 1 && isMetadataOnlyArgv(args)) {
|
|
504
736
|
return [processRequirement(head, 'inspect', segment, ['process.inspect.package_manager'])];
|
|
505
737
|
}
|
|
@@ -1134,15 +1366,39 @@ function stripRedirects(tokens) {
|
|
|
1134
1366
|
stripped.push(token);
|
|
1135
1367
|
continue;
|
|
1136
1368
|
}
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1369
|
+
index += 1;
|
|
1370
|
+
}
|
|
1371
|
+
return stripped;
|
|
1372
|
+
}
|
|
1373
|
+
function stripStructuredRedirects(tokens) {
|
|
1374
|
+
const stripped = [];
|
|
1375
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
1376
|
+
const token = tokens[index];
|
|
1377
|
+
if (!token)
|
|
1378
|
+
continue;
|
|
1379
|
+
if (isFdDuplication(token.value)) {
|
|
1380
|
+
continue;
|
|
1142
1381
|
}
|
|
1382
|
+
if (!isRedirectOperator(token.value)) {
|
|
1383
|
+
stripped.push(token);
|
|
1384
|
+
continue;
|
|
1385
|
+
}
|
|
1386
|
+
index += 1;
|
|
1143
1387
|
}
|
|
1144
1388
|
return stripped;
|
|
1145
1389
|
}
|
|
1390
|
+
function alignStructuredTokens(tokens, values) {
|
|
1391
|
+
if (values.length === 0)
|
|
1392
|
+
return [];
|
|
1393
|
+
for (let start = tokens.length - values.length; start >= 0; start -= 1) {
|
|
1394
|
+
const candidate = tokens.slice(start);
|
|
1395
|
+
if (candidate.length === values.length &&
|
|
1396
|
+
candidate.every((token, index) => token.value === values[index])) {
|
|
1397
|
+
return candidate;
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
return [];
|
|
1401
|
+
}
|
|
1146
1402
|
function shellSegment(commandRedacted, segmentHead, requirements, opacity, signals) {
|
|
1147
1403
|
const normalizedRequirements = requirements.flatMap((entry) => {
|
|
1148
1404
|
const dynamicSignal = dynamicResourceSignal(entry.resource);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { GatedActionKind } from './gate-contract.js';
|
|
2
2
|
import type { ScrubOptions } from './types.js';
|
|
3
|
+
export declare function redactToolInvocationId(value: unknown, rawToolUseId?: string): unknown;
|
|
3
4
|
/** Subagent fingerprint input — must match classify-subagent `fingerprintSource`. */
|
|
4
5
|
export declare function subagentFingerprintSource(payload: Record<string, unknown>, scrubOptions: ScrubOptions): unknown;
|
|
5
6
|
/**
|
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
import { scrubValue } from './scrub.js';
|
|
2
|
+
export function redactToolInvocationId(value, rawToolUseId) {
|
|
3
|
+
if (typeof value === 'string') {
|
|
4
|
+
return rawToolUseId ? value.replaceAll(rawToolUseId, '<tool-use-id>') : value;
|
|
5
|
+
}
|
|
6
|
+
if (Array.isArray(value)) {
|
|
7
|
+
return value.map((item) => redactToolInvocationId(item, rawToolUseId));
|
|
8
|
+
}
|
|
9
|
+
if (value && typeof value === 'object') {
|
|
10
|
+
const result = {};
|
|
11
|
+
for (const [key, child] of Object.entries(value)) {
|
|
12
|
+
if (key !== 'tool_use_id') {
|
|
13
|
+
result[key] = redactToolInvocationId(child, rawToolUseId);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
2
20
|
/** Subagent fingerprint input — must match classify-subagent `fingerprintSource`. */
|
|
3
21
|
export function subagentFingerprintSource(payload, scrubOptions) {
|
|
4
22
|
const toolInput = payload.tool_input;
|
|
@@ -30,14 +48,15 @@ export function fingerprintReplayPayload(kind, payload, scrubOptions) {
|
|
|
30
48
|
if (!payload) {
|
|
31
49
|
return undefined;
|
|
32
50
|
}
|
|
51
|
+
const replayPayload = redactToolInvocationId(payload, typeof payload.tool_use_id === 'string' ? payload.tool_use_id : undefined);
|
|
33
52
|
if (kind === 'tool') {
|
|
34
|
-
const toolInput =
|
|
53
|
+
const toolInput = replayPayload.tool_input;
|
|
35
54
|
if (toolInput && typeof toolInput === 'object') {
|
|
36
55
|
return scrubValue(toolInput, scrubOptions);
|
|
37
56
|
}
|
|
38
57
|
}
|
|
39
58
|
if (kind === 'subagent') {
|
|
40
|
-
return subagentFingerprintSource(
|
|
59
|
+
return subagentFingerprintSource(replayPayload, scrubOptions);
|
|
41
60
|
}
|
|
42
|
-
return scrubValue(
|
|
61
|
+
return scrubValue(replayPayload, scrubOptions);
|
|
43
62
|
}
|
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
export declare function isRedirectOperator(token: string): boolean;
|
|
2
2
|
export declare function isFdDuplication(token: string): boolean;
|
|
3
|
+
export type ShellQuoteMode = 'unquoted' | 'single' | 'double';
|
|
4
|
+
export interface ShellWordPart {
|
|
5
|
+
value: string;
|
|
6
|
+
raw: string;
|
|
7
|
+
start: number;
|
|
8
|
+
end: number;
|
|
9
|
+
quote: ShellQuoteMode;
|
|
10
|
+
hasExpansion: boolean;
|
|
11
|
+
}
|
|
12
|
+
export type ShellToken = {
|
|
13
|
+
kind: 'word';
|
|
14
|
+
value: string;
|
|
15
|
+
raw: string;
|
|
16
|
+
start: number;
|
|
17
|
+
end: number;
|
|
18
|
+
parts: ShellWordPart[];
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'operator';
|
|
21
|
+
value: string;
|
|
22
|
+
raw: string;
|
|
23
|
+
start: number;
|
|
24
|
+
end: number;
|
|
25
|
+
};
|
|
26
|
+
export interface ShellLexResult {
|
|
27
|
+
tokens: ShellToken[];
|
|
28
|
+
complete: boolean;
|
|
29
|
+
}
|
|
30
|
+
export declare function lexShell(input: string): ShellLexResult;
|
|
3
31
|
export declare function tokenizeShell(input: string): string[];
|
|
4
32
|
export declare function normalizeShellCommand(command: string, repoRoot: string, normalizeToken: (t: string, r: string) => string): string;
|
|
5
33
|
export declare function splitTopLevelSegments(tokens: string[]): string[][];
|