@guilz-dev/belay 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -10
- package/dist/adapters/shared/gate-runtime.d.ts +5 -0
- package/dist/adapters/shared/gate-runtime.js +237 -33
- package/dist/bundle/claude-runtime.mjs +4082 -1230
- package/dist/bundle/codex-runtime.mjs +4057 -1205
- package/dist/bundle/cursor-runtime.mjs +4057 -1205
- package/dist/commands/classify-for-report.js +5 -0
- package/dist/commands/doctor.js +73 -6
- 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-io.js +8 -0
- package/dist/core/audit-metrics.d.ts +11 -0
- package/dist/core/audit-metrics.js +14 -0
- package/dist/core/audit-recovery-metrics.d.ts +33 -0
- package/dist/core/audit-recovery-metrics.js +146 -0
- package/dist/core/audit-types.d.ts +7 -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/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/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 +100 -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/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/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -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
|
@@ -6,6 +6,22 @@ export function normalizeEffectTags(tags) {
|
|
|
6
6
|
}
|
|
7
7
|
return [...set].sort();
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Joins opacity evidence without allowing a later analysis stage to weaken an
|
|
11
|
+
* earlier uncertainty classification.
|
|
12
|
+
*/
|
|
13
|
+
export function joinEffectOpacity(...opacities) {
|
|
14
|
+
if (opacities.includes('unparseable')) {
|
|
15
|
+
return 'unparseable';
|
|
16
|
+
}
|
|
17
|
+
if (opacities.includes('opaque')) {
|
|
18
|
+
return 'opaque';
|
|
19
|
+
}
|
|
20
|
+
if (opacities.includes('recursive')) {
|
|
21
|
+
return 'recursive';
|
|
22
|
+
}
|
|
23
|
+
return 'transparent';
|
|
24
|
+
}
|
|
9
25
|
export function mergeRequirements(...groups) {
|
|
10
26
|
const seen = new Map();
|
|
11
27
|
for (const group of groups) {
|
|
@@ -45,13 +61,7 @@ export function mergeEffectPlans(...plans) {
|
|
|
45
61
|
return present[0];
|
|
46
62
|
}
|
|
47
63
|
const children = present.flatMap((plan) => plan.root.kind === 'merge' ? [...plan.root.children] : [plan.root]);
|
|
48
|
-
const opacity = present.
|
|
49
|
-
? 'unparseable'
|
|
50
|
-
: present.some((plan) => plan.opacity === 'opaque')
|
|
51
|
-
? 'opaque'
|
|
52
|
-
: present.some((plan) => plan.opacity === 'recursive')
|
|
53
|
-
? 'recursive'
|
|
54
|
-
: 'transparent';
|
|
64
|
+
const opacity = joinEffectOpacity(...present.map((plan) => plan.opacity));
|
|
55
65
|
const fingerprints = present.map((plan) => plan.inputFingerprint).sort();
|
|
56
66
|
return {
|
|
57
67
|
version: 1,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { collectRequirements } from './build.js';
|
|
2
|
-
import { mergeRequirements } from './normalize.js';
|
|
2
|
+
import { joinEffectOpacity, mergeRequirements } from './normalize.js';
|
|
3
3
|
/**
|
|
4
4
|
* Build a canonical EffectPlan from shell segments that have already been
|
|
5
5
|
* semantically analyzed. This function only assembles and normalizes effects;
|
|
@@ -16,7 +16,7 @@ export function buildShellEffectPlan(params) {
|
|
|
16
16
|
version: 1,
|
|
17
17
|
root,
|
|
18
18
|
inputFingerprint: params.inputFingerprint,
|
|
19
|
-
opacity:
|
|
19
|
+
opacity: joinEffectOpacity(...params.segments.map((segment) => segment.opacity)),
|
|
20
20
|
disposition: requirements.length === 0 ? 'effect_free' : 'effects',
|
|
21
21
|
completeness,
|
|
22
22
|
signals: [
|
|
@@ -63,15 +63,3 @@ function mergeNodes(nodes) {
|
|
|
63
63
|
}
|
|
64
64
|
return { kind: 'merge', children: nodes };
|
|
65
65
|
}
|
|
66
|
-
function mergeOpacity(opacities) {
|
|
67
|
-
if (opacities.includes('unparseable')) {
|
|
68
|
-
return 'unparseable';
|
|
69
|
-
}
|
|
70
|
-
if (opacities.includes('opaque')) {
|
|
71
|
-
return 'opaque';
|
|
72
|
-
}
|
|
73
|
-
if (opacities.includes('recursive')) {
|
|
74
|
-
return 'recursive';
|
|
75
|
-
}
|
|
76
|
-
return 'transparent';
|
|
77
|
-
}
|
|
@@ -5,7 +5,8 @@ import { isFdDuplication, isRedirectOperator, tokenizeShell } from '../shell-tok
|
|
|
5
5
|
import { decodeEgressEffects } from '../verdict/egress-classify.js';
|
|
6
6
|
import { decodeGitEffects } from '../verdict/git-classifier.js';
|
|
7
7
|
import { resolveLauncherRecipe } from '../verdict/launcher-resolve.js';
|
|
8
|
-
import { extractRecursiveScript, parseSegment, redactCommand, segmentOpacity, splitStructuralShellSegments, structuralSubstitutionInners, } from '../verdict/parser.js';
|
|
8
|
+
import { extractRecursiveScript, isCommandInspection, isDynamicRecursiveEvaluation, parseSegment, redactCommand, segmentOpacity, splitStructuralShellSegments, structuralSubstitutionInners, } from '../verdict/parser.js';
|
|
9
|
+
import { joinEffectOpacity } from './normalize.js';
|
|
9
10
|
import { classifyPackageAcquisitionSpec, innerRecipeFromPeel, peelPackageExecArgv, resolveLocalBin, } from './package-exec.js';
|
|
10
11
|
import { buildShellEffectPlan, } from './shell-build.js';
|
|
11
12
|
const MAX_LOWER_DEPTH = 8;
|
|
@@ -61,7 +62,7 @@ function lowerTopLevelSegments(command, context) {
|
|
|
61
62
|
...(requiresCwd
|
|
62
63
|
? {
|
|
63
64
|
completeness: 'partial',
|
|
64
|
-
opacity: result.opacity
|
|
65
|
+
opacity: joinEffectOpacity(result.opacity, 'opaque'),
|
|
65
66
|
}
|
|
66
67
|
: {}),
|
|
67
68
|
signals: [...new Set([...result.signals, 'shell.cwd_unknown'])],
|
|
@@ -117,6 +118,12 @@ function requiresKnownCwd(requirementValue) {
|
|
|
117
118
|
requirementValue.resource.kind === 'executable' &&
|
|
118
119
|
requirementValue.resource.operation === 'spawn');
|
|
119
120
|
}
|
|
121
|
+
function joinNestedOpacity(outer, nested) {
|
|
122
|
+
const nestedOpacity = nested.completeness === 'partial' && nested.opacity === 'transparent'
|
|
123
|
+
? 'recursive'
|
|
124
|
+
: nested.opacity;
|
|
125
|
+
return joinEffectOpacity(outer, nestedOpacity);
|
|
126
|
+
}
|
|
120
127
|
function lowerSegment(command, context) {
|
|
121
128
|
const commandRedacted = redactCommand(command);
|
|
122
129
|
const rawTokens = tokenizeShell(command);
|
|
@@ -134,20 +141,20 @@ function lowerSegment(command, context) {
|
|
|
134
141
|
requirements.push(requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, commandRedacted, [
|
|
135
142
|
'shell.env_wrapper_incomplete',
|
|
136
143
|
]));
|
|
137
|
-
opacity = 'opaque';
|
|
144
|
+
opacity = joinEffectOpacity(opacity, 'opaque');
|
|
138
145
|
}
|
|
139
|
-
if (
|
|
146
|
+
if (parsed.encounteredXargs) {
|
|
140
147
|
requirements.push(requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, commandRedacted, [
|
|
141
148
|
'shell.xargs_stdin_dynamic',
|
|
142
149
|
]));
|
|
143
150
|
signals.add('shell.xargs_stdin_dynamic');
|
|
144
|
-
opacity = 'opaque';
|
|
151
|
+
opacity = joinEffectOpacity(opacity, 'opaque');
|
|
145
152
|
}
|
|
146
153
|
if (context.depth >= MAX_LOWER_DEPTH) {
|
|
147
154
|
requirements.push(requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, commandRedacted, [
|
|
148
155
|
'shell.lower_depth_exceeded',
|
|
149
156
|
]));
|
|
150
|
-
return shellSegment(commandRedacted, head, requirements, 'opaque', signals);
|
|
157
|
+
return shellSegment(commandRedacted, head, requirements, joinEffectOpacity(opacity, 'opaque'), signals);
|
|
151
158
|
}
|
|
152
159
|
const packageExec = peelPackageExecArgv(tokens);
|
|
153
160
|
if (packageExec) {
|
|
@@ -156,7 +163,7 @@ function lowerSegment(command, context) {
|
|
|
156
163
|
signals.add(signal);
|
|
157
164
|
}
|
|
158
165
|
if (packageExec.opaque) {
|
|
159
|
-
opacity = 'opaque';
|
|
166
|
+
opacity = joinEffectOpacity(opacity, 'opaque');
|
|
160
167
|
}
|
|
161
168
|
const innerRecipe = packageExecInnerIsMetadata(packageExec)
|
|
162
169
|
? null
|
|
@@ -173,16 +180,18 @@ function lowerSegment(command, context) {
|
|
|
173
180
|
for (const signal of nestedSegment.signals) {
|
|
174
181
|
signals.add(signal);
|
|
175
182
|
}
|
|
176
|
-
|
|
177
|
-
opacity = nestedSegment.opacity === 'unparseable' ? 'unparseable' : 'recursive';
|
|
178
|
-
}
|
|
183
|
+
opacity = joinNestedOpacity(opacity, nestedSegment);
|
|
179
184
|
}
|
|
180
185
|
}
|
|
181
186
|
return shellSegment(commandRedacted, head, requirements, opacity, signals);
|
|
182
187
|
}
|
|
183
188
|
const recursiveScript = extractRecursiveScript(tokens);
|
|
184
|
-
if (recursiveScript) {
|
|
185
|
-
|
|
189
|
+
if (recursiveScript && opacity !== 'opaque' && opacity !== 'unparseable') {
|
|
190
|
+
const dynamicEvaluation = isDynamicRecursiveEvaluation(tokens);
|
|
191
|
+
requirements.push(processRequirement(head || 'sh', 'spawn', commandRedacted, [
|
|
192
|
+
'shell.recursive_wrapper',
|
|
193
|
+
...(dynamicEvaluation ? ['dynamic_shell_evaluation'] : []),
|
|
194
|
+
]));
|
|
186
195
|
const nested = lowerTopLevelSegments(recursiveScript, {
|
|
187
196
|
...context,
|
|
188
197
|
command: recursiveScript,
|
|
@@ -196,6 +205,9 @@ function lowerSegment(command, context) {
|
|
|
196
205
|
}
|
|
197
206
|
}
|
|
198
207
|
signals.add('shell.recursive_wrapper');
|
|
208
|
+
if (dynamicEvaluation) {
|
|
209
|
+
signals.add('dynamic_shell_evaluation');
|
|
210
|
+
}
|
|
199
211
|
return shellSegment(commandRedacted, head, requirements, 'recursive', signals);
|
|
200
212
|
}
|
|
201
213
|
const launcher = resolveLauncherRecipe({
|
|
@@ -219,16 +231,14 @@ function lowerSegment(command, context) {
|
|
|
219
231
|
for (const signal of nestedSegment.signals) {
|
|
220
232
|
signals.add(signal);
|
|
221
233
|
}
|
|
222
|
-
|
|
223
|
-
opacity = nestedSegment.opacity === 'unparseable' ? 'unparseable' : 'recursive';
|
|
224
|
-
}
|
|
234
|
+
opacity = joinNestedOpacity(opacity, nestedSegment);
|
|
225
235
|
}
|
|
226
236
|
}
|
|
227
237
|
if (launcher.opaque || launcher.recipes.length === 0) {
|
|
228
238
|
requirements.push(requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, commandRedacted, [
|
|
229
239
|
`launcher.${launcher.reason}`,
|
|
230
240
|
]));
|
|
231
|
-
opacity = 'opaque';
|
|
241
|
+
opacity = joinEffectOpacity(opacity, 'opaque');
|
|
232
242
|
}
|
|
233
243
|
return shellSegment(commandRedacted, head, requirements, opacity, signals);
|
|
234
244
|
}
|
|
@@ -270,7 +280,7 @@ function lowerSegment(command, context) {
|
|
|
270
280
|
const environmentSignals = effectChangingEnvironmentSignals(head, env, environment.changedNames);
|
|
271
281
|
if (environmentSignals.length > 0) {
|
|
272
282
|
requirements.push(requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, commandRedacted, environmentSignals));
|
|
273
|
-
opacity = 'opaque';
|
|
283
|
+
opacity = joinEffectOpacity(opacity, 'opaque');
|
|
274
284
|
for (const signal of environmentSignals) {
|
|
275
285
|
signals.add(signal);
|
|
276
286
|
}
|
|
@@ -281,7 +291,7 @@ function lowerSegment(command, context) {
|
|
|
281
291
|
ensureRequirement(requirements, requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, commandRedacted, [
|
|
282
292
|
'pipe_to_shell',
|
|
283
293
|
]));
|
|
284
|
-
opacity = 'opaque';
|
|
294
|
+
opacity = joinEffectOpacity(opacity, 'opaque');
|
|
285
295
|
}
|
|
286
296
|
if (opacity === 'unparseable' || opacity === 'opaque') {
|
|
287
297
|
ensureRequirement(requirements, requirement('indeterminate', 'indeterminate', { kind: 'unknown' }, commandRedacted, [
|
|
@@ -356,9 +366,63 @@ function packageExecInnerIsMetadata(peel) {
|
|
|
356
366
|
const args = peel.innerTokens.slice(1);
|
|
357
367
|
return (args.length > 0 && args.every((arg) => ['--help', '--version', '-V', '-h', '-v'].includes(arg)));
|
|
358
368
|
}
|
|
369
|
+
const METADATA_ONLY_FLAGS = new Set(['--version', '-v', '-V', '--help', '-h']);
|
|
370
|
+
function isMetadataOnlyArgv(argv) {
|
|
371
|
+
return argv.length > 0 && argv.every((token) => METADATA_ONLY_FLAGS.has(token));
|
|
372
|
+
}
|
|
373
|
+
function executableBaseName(head) {
|
|
374
|
+
return path.basename(head);
|
|
375
|
+
}
|
|
376
|
+
const RAILS_READ_ONLY_SUBCOMMANDS = new Set(['routes', 'middleware', 'stats', 'about', 'version']);
|
|
377
|
+
function railsReadOnlySubcommand(args) {
|
|
378
|
+
const subcommand = args[0];
|
|
379
|
+
if (!subcommand || subcommand.startsWith('-')) {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
return RAILS_READ_ONLY_SUBCOMMANDS.has(subcommand);
|
|
383
|
+
}
|
|
384
|
+
function decodeRuntimeMetadataProcess(head, args, segment) {
|
|
385
|
+
if (head === 'bundle') {
|
|
386
|
+
if (args.length === 1 && isMetadataOnlyArgv(args)) {
|
|
387
|
+
return [processRequirement(head, 'inspect', segment, ['process.inspect.runtime_metadata'])];
|
|
388
|
+
}
|
|
389
|
+
if (args[0] === 'exec' && args.length >= 2) {
|
|
390
|
+
const innerHead = args[1] ?? '';
|
|
391
|
+
const innerArgs = args.slice(2);
|
|
392
|
+
const innerBase = executableBaseName(innerHead);
|
|
393
|
+
if ((innerBase === 'rails' || innerBase === 'rake') && railsReadOnlySubcommand(innerArgs)) {
|
|
394
|
+
return [
|
|
395
|
+
processRequirement(innerHead, 'inspect', segment, ['process.inspect.rails_read_only']),
|
|
396
|
+
];
|
|
397
|
+
}
|
|
398
|
+
if (isMetadataOnlyArgv(innerArgs)) {
|
|
399
|
+
return [
|
|
400
|
+
processRequirement(innerHead, 'inspect', segment, [
|
|
401
|
+
'process.inspect.bundle_exec_metadata',
|
|
402
|
+
]),
|
|
403
|
+
];
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
if ((head === 'ruby' || head === 'yarn') && args.length >= 1 && isMetadataOnlyArgv(args)) {
|
|
409
|
+
return [processRequirement(head, 'inspect', segment, ['process.inspect.runtime_metadata'])];
|
|
410
|
+
}
|
|
411
|
+
if (head === 'make' && args.includes('-n')) {
|
|
412
|
+
return [processRequirement(head, 'inspect', segment, ['process.inspect.make_dry_run'])];
|
|
413
|
+
}
|
|
414
|
+
const base = executableBaseName(head);
|
|
415
|
+
if ((base === 'rails' || base === 'rake') && railsReadOnlySubcommand(args)) {
|
|
416
|
+
return [processRequirement(head, 'inspect', segment, ['process.inspect.rails_read_only'])];
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
359
420
|
function decodeProcessOrFilesystem(params) {
|
|
360
421
|
const { tokens, head, env, cwd, repoRoot, segment } = params;
|
|
361
422
|
const args = tokens.slice(1);
|
|
423
|
+
if (isCommandInspection(tokens)) {
|
|
424
|
+
return [processRequirement(head, 'inspect', segment, ['process.inspect.command_lookup'])];
|
|
425
|
+
}
|
|
362
426
|
if (head === 'lsof') {
|
|
363
427
|
return validLsof(args)
|
|
364
428
|
? [processRequirement(head, 'inspect', segment, ['process.inspect.lsof'])]
|
|
@@ -432,7 +496,11 @@ function decodeProcessOrFilesystem(params) {
|
|
|
432
496
|
if (head === 'prisma') {
|
|
433
497
|
return decodePrisma(args, env, repoRoot, segment);
|
|
434
498
|
}
|
|
435
|
-
|
|
499
|
+
const runtimeMetadata = decodeRuntimeMetadataProcess(head, args, segment);
|
|
500
|
+
if (runtimeMetadata) {
|
|
501
|
+
return runtimeMetadata;
|
|
502
|
+
}
|
|
503
|
+
if ((head === 'npm' || head === 'pnpm') && args.length === 1 && isMetadataOnlyArgv(args)) {
|
|
436
504
|
return [processRequirement(head, 'inspect', segment, ['process.inspect.package_manager'])];
|
|
437
505
|
}
|
|
438
506
|
if (head === 'belay') {
|
|
@@ -544,12 +612,9 @@ function decodeBelay(args, repoRoot, segment) {
|
|
|
544
612
|
return [processRequirement('belay', 'spawn', segment, ['process.known_local_spawn'])];
|
|
545
613
|
}
|
|
546
614
|
function decodeTsc(args, cwd, segment) {
|
|
547
|
-
const
|
|
548
|
-
const requirements = [
|
|
549
|
-
|
|
550
|
-
inspect ? 'process.inspect.tsc_metadata' : 'process.known_local_spawn',
|
|
551
|
-
]),
|
|
552
|
-
];
|
|
615
|
+
const metadataOnly = args.length > 0 && args.every((arg) => ['--help', '--version', '-h', '-v'].includes(arg));
|
|
616
|
+
const requirements = [];
|
|
617
|
+
let hasOutput = false;
|
|
553
618
|
for (let index = 0; index < args.length; index += 1) {
|
|
554
619
|
const arg = args[index] ?? '';
|
|
555
620
|
const inline = arg.match(/^--(?:outDir|outFile)=(.+)$/)?.[1];
|
|
@@ -558,11 +623,21 @@ function decodeTsc(args, cwd, segment) {
|
|
|
558
623
|
if (!output) {
|
|
559
624
|
continue;
|
|
560
625
|
}
|
|
626
|
+
hasOutput = true;
|
|
561
627
|
addWriteEffects(requirements, resolvePathOperand(output, cwd), segment, ['tsc.output']);
|
|
562
628
|
if (separate) {
|
|
563
629
|
index += 1;
|
|
564
630
|
}
|
|
565
631
|
}
|
|
632
|
+
const typecheckOnly = args.includes('--noEmit') && !hasOutput;
|
|
633
|
+
const inspect = metadataOnly || typecheckOnly;
|
|
634
|
+
requirements.unshift(processRequirement('tsc', inspect ? 'inspect' : 'spawn', segment, [
|
|
635
|
+
metadataOnly
|
|
636
|
+
? 'process.inspect.tsc_metadata'
|
|
637
|
+
: typecheckOnly
|
|
638
|
+
? 'process.inspect.tsc_typecheck'
|
|
639
|
+
: 'process.known_local_spawn',
|
|
640
|
+
]));
|
|
566
641
|
return requirements;
|
|
567
642
|
}
|
|
568
643
|
function decodeCopyMove(head, args, cwd, segment) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { Assessment, ClassifyResult, HookVerdict } from './types.js';
|
|
1
|
+
import type { Assessment, ClassifyResult, HookVerdict, MediatedExecutionResult } from './types.js';
|
|
2
|
+
export type { MediatedExecutionResult } from './types.js';
|
|
2
3
|
export declare const GATE_CONTRACT_VERSION: 1;
|
|
3
4
|
export type GatedActionKind = 'shell' | 'subagent' | 'tool';
|
|
4
5
|
export interface GatedAction {
|
|
@@ -34,6 +35,8 @@ export interface GateVerdict extends GatePermissionResponse {
|
|
|
34
35
|
effectPlan?: ClassifyResult['effectPlan'];
|
|
35
36
|
effectPlanPolicyDecisions?: ClassifyResult['effectPlanPolicyDecisions'];
|
|
36
37
|
boundaryProfile?: string;
|
|
38
|
+
wouldMediate?: boolean;
|
|
39
|
+
mediatedExecution?: MediatedExecutionResult;
|
|
37
40
|
}
|
|
38
41
|
export declare function isGatedAction(value: unknown): value is GatedAction;
|
|
39
42
|
export declare function classifyResultToGateVerdict(params: {
|
|
@@ -31,6 +31,8 @@ export function classifyResultToGateVerdict(params) {
|
|
|
31
31
|
effectPlan: result.effectPlan,
|
|
32
32
|
effectPlanPolicyDecisions: result.effectPlanPolicyDecisions,
|
|
33
33
|
boundaryProfile: result.boundaryProfile,
|
|
34
|
+
wouldMediate: result.wouldMediate,
|
|
35
|
+
mediatedExecution: result.mediatedExecution,
|
|
34
36
|
};
|
|
35
37
|
}
|
|
36
38
|
export function unnormalizedGateVerdict(params) {
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { type SpawnOptionsWithoutStdio } from 'node:child_process';
|
|
2
|
+
import type { ScrubOptions } from './types.js';
|
|
2
3
|
export interface ShellRunResult {
|
|
3
4
|
exitCode: number | null;
|
|
4
5
|
signal: string | null;
|
|
5
6
|
timedOut: boolean;
|
|
6
|
-
stdout
|
|
7
|
-
stderr
|
|
7
|
+
stdout: string;
|
|
8
|
+
stderr: string;
|
|
9
|
+
stdoutTruncated: boolean;
|
|
10
|
+
stderrTruncated: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface ProcessOutputPolicy {
|
|
13
|
+
/** Scrub credential context before the irreversible output-tail cap is applied. */
|
|
14
|
+
scrubOptions: ScrubOptions;
|
|
8
15
|
}
|
|
9
16
|
export declare function windowsProcessTreeKillArgs(pid: number): string[];
|
|
10
|
-
export declare function runProcessWithBoundedOutput(file: string, args: string[], options: SpawnOptionsWithoutStdio, timeoutMs: number): Promise<ShellRunResult>;
|
|
17
|
+
export declare function runProcessWithBoundedOutput(file: string, args: string[], options: SpawnOptionsWithoutStdio, timeoutMs: number, outputPolicy?: ProcessOutputPolicy): Promise<ShellRunResult>;
|
|
@@ -1,11 +1,36 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
|
|
2
|
+
import { appendBoundedOutputTail, decodeBoundedOutputTail, OUTPUT_TAIL_LIMIT_BYTES, } from './bounded-output.js';
|
|
3
|
+
import { createStreamingScrubber } from './scrub.js';
|
|
3
4
|
const EXIT_OUTPUT_GRACE_MS = 25;
|
|
4
5
|
const TIMEOUT_KILL_GRACE_MS = 250;
|
|
5
6
|
const WINDOWS_TASKKILL_TIMEOUT_MS = 5_000;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
class ScrubbedOutputTail {
|
|
8
|
+
#scrubber;
|
|
9
|
+
#tail = Buffer.alloc(0);
|
|
10
|
+
#tailTruncated = false;
|
|
11
|
+
#rawBytes = 0;
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.#scrubber = createStreamingScrubber(options);
|
|
14
|
+
}
|
|
15
|
+
append(chunk) {
|
|
16
|
+
const bytes = Buffer.from(chunk);
|
|
17
|
+
this.#rawBytes += bytes.length;
|
|
18
|
+
this.#appendSafe(this.#scrubber.write(bytes));
|
|
19
|
+
}
|
|
20
|
+
finish() {
|
|
21
|
+
this.#appendSafe(this.#scrubber.end());
|
|
22
|
+
return {
|
|
23
|
+
value: decodeBoundedOutputTail(this.#tail),
|
|
24
|
+
truncated: this.#rawBytes > OUTPUT_TAIL_LIMIT_BYTES || this.#tailTruncated,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
#appendSafe(value) {
|
|
28
|
+
if (!value)
|
|
29
|
+
return;
|
|
30
|
+
const output = appendBoundedOutputTail(this.#tail, value);
|
|
31
|
+
this.#tail = output.tail;
|
|
32
|
+
this.#tailTruncated ||= output.truncated;
|
|
33
|
+
}
|
|
9
34
|
}
|
|
10
35
|
export function windowsProcessTreeKillArgs(pid) {
|
|
11
36
|
return ['/pid', String(pid), '/t', '/f'];
|
|
@@ -36,7 +61,7 @@ function forceKillWindowsProcessTree(pid, fallback) {
|
|
|
36
61
|
taskkill.on('close', (code) => finish(code === 0));
|
|
37
62
|
});
|
|
38
63
|
}
|
|
39
|
-
export function runProcessWithBoundedOutput(file, args, options, timeoutMs) {
|
|
64
|
+
export function runProcessWithBoundedOutput(file, args, options, timeoutMs, outputPolicy) {
|
|
40
65
|
return new Promise((resolve) => {
|
|
41
66
|
const detached = process.platform !== 'win32';
|
|
42
67
|
const child = spawn(file, args, {
|
|
@@ -46,15 +71,35 @@ export function runProcessWithBoundedOutput(file, args, options, timeoutMs) {
|
|
|
46
71
|
});
|
|
47
72
|
let stdout = Buffer.alloc(0);
|
|
48
73
|
let stderr = Buffer.alloc(0);
|
|
74
|
+
let stdoutTruncated = false;
|
|
75
|
+
let stderrTruncated = false;
|
|
76
|
+
const scrubbedStdout = outputPolicy
|
|
77
|
+
? new ScrubbedOutputTail(outputPolicy.scrubOptions)
|
|
78
|
+
: undefined;
|
|
79
|
+
const scrubbedStderr = outputPolicy
|
|
80
|
+
? new ScrubbedOutputTail(outputPolicy.scrubOptions)
|
|
81
|
+
: undefined;
|
|
49
82
|
let timedOut = false;
|
|
50
83
|
let settled = false;
|
|
51
84
|
let windowsCleanupPending = false;
|
|
52
85
|
let exitTimer;
|
|
53
86
|
child.stdout.on('data', (chunk) => {
|
|
54
|
-
|
|
87
|
+
if (scrubbedStdout) {
|
|
88
|
+
scrubbedStdout.append(chunk);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const output = appendBoundedOutputTail(stdout, chunk);
|
|
92
|
+
stdout = output.tail;
|
|
93
|
+
stdoutTruncated ||= output.truncated;
|
|
55
94
|
});
|
|
56
95
|
child.stderr.on('data', (chunk) => {
|
|
57
|
-
|
|
96
|
+
if (scrubbedStderr) {
|
|
97
|
+
scrubbedStderr.append(chunk);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const output = appendBoundedOutputTail(stderr, chunk);
|
|
101
|
+
stderr = output.tail;
|
|
102
|
+
stderrTruncated ||= output.truncated;
|
|
58
103
|
});
|
|
59
104
|
const finish = (exitCode, signal) => {
|
|
60
105
|
if (settled) {
|
|
@@ -67,12 +112,16 @@ export function runProcessWithBoundedOutput(file, args, options, timeoutMs) {
|
|
|
67
112
|
}
|
|
68
113
|
child.stdout.destroy();
|
|
69
114
|
child.stderr.destroy();
|
|
115
|
+
const safeStdout = scrubbedStdout?.finish();
|
|
116
|
+
const safeStderr = scrubbedStderr?.finish();
|
|
70
117
|
resolve({
|
|
71
118
|
exitCode,
|
|
72
119
|
signal: signal ? String(signal) : null,
|
|
73
120
|
timedOut,
|
|
74
|
-
stdout: stdout
|
|
75
|
-
stderr: stderr
|
|
121
|
+
stdout: safeStdout?.value ?? decodeBoundedOutputTail(stdout),
|
|
122
|
+
stderr: safeStderr?.value ?? decodeBoundedOutputTail(stderr),
|
|
123
|
+
stdoutTruncated: safeStdout?.truncated ?? stdoutTruncated,
|
|
124
|
+
stderrTruncated: safeStderr?.truncated ?? stderrTruncated,
|
|
76
125
|
});
|
|
77
126
|
};
|
|
78
127
|
const kill = (signal) => {
|
|
@@ -104,7 +153,14 @@ export function runProcessWithBoundedOutput(file, args, options, timeoutMs) {
|
|
|
104
153
|
}, TIMEOUT_KILL_GRACE_MS);
|
|
105
154
|
}, timeoutMs);
|
|
106
155
|
child.on('error', (error) => {
|
|
107
|
-
|
|
156
|
+
if (scrubbedStderr) {
|
|
157
|
+
scrubbedStderr.append(error.message);
|
|
158
|
+
finish(1, null);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const output = appendBoundedOutputTail(stderr, error.message);
|
|
162
|
+
stderr = output.tail;
|
|
163
|
+
stderrTruncated ||= output.truncated;
|
|
108
164
|
finish(1, null);
|
|
109
165
|
});
|
|
110
166
|
child.on('exit', (exitCode, signal) => {
|
|
@@ -17,8 +17,11 @@ export declare function prepareRecoveryCheckpoint(params: {
|
|
|
17
17
|
protectedRoots?: string[];
|
|
18
18
|
config: CheckpointConfig;
|
|
19
19
|
backend?: RecoveryBackend;
|
|
20
|
+
/** Snapshot-time resource identity; re-validated before manifest commit. */
|
|
21
|
+
expectedResourceIdentity?: string;
|
|
20
22
|
}): Promise<PreparedRecoveryCheckpoint>;
|
|
21
23
|
export declare function discardPreparedRecoveryCheckpoint(stateDir: string, checkpointId: string): Promise<boolean>;
|
|
24
|
+
export declare function markRecoveryCheckpointNeedsManualRepair(stateDir: string, checkpointId: string, detail: string): Promise<void>;
|
|
22
25
|
export declare function listRecoveryCheckpoints(stateDir: string, repoRoot?: string): Promise<RecoveryCheckpointSummary[]>;
|
|
23
26
|
export declare function recoveryCheckpointStorageBytes(stateDir: string, repoRoot?: string): Promise<number>;
|
|
24
27
|
export declare function showRecoveryCheckpoint(stateDir: string, checkpointId: string, expectedRepoRoot?: string): Promise<import("./artifact-store.js").RecoveryArtifact>;
|
|
@@ -4,13 +4,31 @@ import { mkdir, readdir, readFile, rename, rm } from 'node:fs/promises';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { canonicalStringify, hashValue } from '../fingerprint.js';
|
|
6
6
|
import { canonicalPath } from '../path-utils.js';
|
|
7
|
+
import { isGitWorktreeAvailable } from '../transactional/git-worktree.js';
|
|
7
8
|
import { artifactRepoRoot, atomicWriteJson, checkpointDir, checkpointIds, checkpointIdsForRepo, checkpointsRoot, cleanupOrphanedStaging, directorySize, fsyncPath, markRecoveryCheckpointApplied, markRecoveryCheckpointApplying, RECOVERY_CHECKPOINT_CORRUPT, readRecoveryArtifact, writeRecoveryState, } from './artifact-store.js';
|
|
8
9
|
import { matchRecoverySide, reconcileRecoveryCheckpoint } from './reconcile.js';
|
|
9
10
|
import { currentRecoveryResourceIdentity } from './resource-identity.js';
|
|
10
11
|
import { RECOVERY_RESTORE_CONFLICT, RECOVERY_RESTORE_REASON, recoveryRestoreBinding, restoreRecoveryCheckpoint, } from './restore.js';
|
|
11
12
|
import { assertRecoverySafeTarget, captureRecoverySnapshot, withoutRecoveryBlob, } from './snapshot-node.js';
|
|
12
|
-
function
|
|
13
|
+
async function resolveRecoveryResourceKind(backend, repoRoot) {
|
|
14
|
+
if (backend === 'git_worktree') {
|
|
15
|
+
return 'git_repository';
|
|
16
|
+
}
|
|
17
|
+
if (await isGitWorktreeAvailable(repoRoot)) {
|
|
18
|
+
return 'git_repository';
|
|
19
|
+
}
|
|
20
|
+
return 'directory';
|
|
21
|
+
}
|
|
22
|
+
function recoveryProofProbeSignals(backend, resourceKind) {
|
|
13
23
|
if (backend === 'file_checkpoint') {
|
|
24
|
+
if (resourceKind === 'directory') {
|
|
25
|
+
return [
|
|
26
|
+
'non_git_workspace',
|
|
27
|
+
'non_git_file_checkpoint',
|
|
28
|
+
'file_checkpoint',
|
|
29
|
+
'observed_repo_local_diff',
|
|
30
|
+
];
|
|
31
|
+
}
|
|
14
32
|
return ['dirty_git_worktree', 'file_checkpoint', 'observed_repo_local_diff'];
|
|
15
33
|
}
|
|
16
34
|
return ['clean_git_worktree', 'observed_repo_local_diff'];
|
|
@@ -45,6 +63,7 @@ async function garbageCollect(stateDir, config, repoRoot) {
|
|
|
45
63
|
}
|
|
46
64
|
export async function prepareRecoveryCheckpoint(params) {
|
|
47
65
|
const backend = params.backend ?? 'git_worktree';
|
|
66
|
+
const resourceKind = await resolveRecoveryResourceKind(backend, params.repoRoot);
|
|
48
67
|
await mkdir(checkpointsRoot(params.stateDir), { recursive: true, mode: 0o700 });
|
|
49
68
|
await cleanupOrphanedStaging(params.stateDir);
|
|
50
69
|
await garbageCollect(params.stateDir, params.config, params.repoRoot);
|
|
@@ -88,6 +107,10 @@ export async function prepareRecoveryCheckpoint(params) {
|
|
|
88
107
|
});
|
|
89
108
|
}
|
|
90
109
|
const createdAt = new Date().toISOString();
|
|
110
|
+
const repoIdentity = await currentRecoveryResourceIdentity(params.repoRoot, resourceKind);
|
|
111
|
+
if (params.expectedResourceIdentity && repoIdentity !== params.expectedResourceIdentity) {
|
|
112
|
+
throw new Error('recovery_checkpoint_repo_mismatch');
|
|
113
|
+
}
|
|
91
114
|
const proof = {
|
|
92
115
|
version: 1,
|
|
93
116
|
backend,
|
|
@@ -97,15 +120,15 @@ export async function prepareRecoveryCheckpoint(params) {
|
|
|
97
120
|
effectClosure: 'repo_local_fs_observed',
|
|
98
121
|
issuedAt: createdAt,
|
|
99
122
|
expiresAt: new Date(Date.now() + params.config.appliedRetentionHours * 60 * 60 * 1000).toISOString(),
|
|
100
|
-
probeSignals: recoveryProofProbeSignals(backend),
|
|
123
|
+
probeSignals: recoveryProofProbeSignals(backend, resourceKind),
|
|
101
124
|
};
|
|
102
125
|
const manifest = {
|
|
103
126
|
version: 2,
|
|
104
127
|
checkpointId,
|
|
105
128
|
backend,
|
|
106
129
|
repoRoot: canonicalPath(params.repoRoot),
|
|
107
|
-
resourceKind
|
|
108
|
-
repoIdentity
|
|
130
|
+
resourceKind,
|
|
131
|
+
repoIdentity,
|
|
109
132
|
commandFingerprint: params.commandFingerprint,
|
|
110
133
|
createdAt,
|
|
111
134
|
expiresAt: proof.expiresAt,
|
|
@@ -144,6 +167,10 @@ export async function discardPreparedRecoveryCheckpoint(stateDir, checkpointId)
|
|
|
144
167
|
await rm(loaded.artifactDir, { recursive: true, force: true });
|
|
145
168
|
return true;
|
|
146
169
|
}
|
|
170
|
+
export async function markRecoveryCheckpointNeedsManualRepair(stateDir, checkpointId, detail) {
|
|
171
|
+
const loaded = await readRecoveryArtifact(stateDir, checkpointId);
|
|
172
|
+
await writeRecoveryState(loaded.artifactDir, 'needs_manual_repair', loaded.manifestHash, detail);
|
|
173
|
+
}
|
|
147
174
|
export async function listRecoveryCheckpoints(stateDir, repoRoot) {
|
|
148
175
|
await cleanupOrphanedStaging(stateDir);
|
|
149
176
|
const summaries = [];
|
|
@@ -176,6 +203,7 @@ export async function listRecoveryCheckpoints(stateDir, repoRoot) {
|
|
|
176
203
|
state,
|
|
177
204
|
...(loaded.state.detail ? { stateDetail: loaded.state.detail } : {}),
|
|
178
205
|
backend: loaded.manifest.backend,
|
|
206
|
+
resourceKind: loaded.manifest.version === 2 ? loaded.manifest.resourceKind : 'git_repository',
|
|
179
207
|
repoRoot: loaded.manifest.repoRoot,
|
|
180
208
|
commandFingerprint: loaded.manifest.commandFingerprint,
|
|
181
209
|
createdAt: loaded.manifest.createdAt,
|
|
@@ -202,6 +230,7 @@ export async function listRecoveryCheckpoints(stateDir, repoRoot) {
|
|
|
202
230
|
checkpointId: id,
|
|
203
231
|
state: 'corrupt',
|
|
204
232
|
backend: manifest.backend,
|
|
233
|
+
resourceKind: manifest.version === 2 ? manifest.resourceKind : 'git_repository',
|
|
205
234
|
repoRoot: manifest.repoRoot,
|
|
206
235
|
commandFingerprint: manifest.commandFingerprint,
|
|
207
236
|
createdAt: manifest.createdAt,
|
|
@@ -86,6 +86,7 @@ export interface RecoveryCheckpointSummary {
|
|
|
86
86
|
/** Operator-facing detail from durable checkpoint state (e.g. mixed-state reason). */
|
|
87
87
|
stateDetail?: string;
|
|
88
88
|
backend: RecoveryBackend;
|
|
89
|
+
resourceKind: 'git_repository' | 'directory';
|
|
89
90
|
repoRoot: string;
|
|
90
91
|
commandFingerprint: string;
|
|
91
92
|
createdAt: string;
|
package/dist/core/scrub.d.ts
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
1
|
import type { ScrubOptions } from './types.js';
|
|
2
2
|
export declare function scrubString(value: string, options?: ScrubOptions): string;
|
|
3
|
+
export interface StreamingScrubber {
|
|
4
|
+
/** Raw, undecided UTF-8 bytes retained between writes. */
|
|
5
|
+
readonly bufferedBytes: number;
|
|
6
|
+
write(chunk: Buffer | string): string;
|
|
7
|
+
end(): string;
|
|
8
|
+
}
|
|
9
|
+
export declare const STREAMING_SCRUBBER_MAX_BUFFER_BYTES = 256;
|
|
10
|
+
/**
|
|
11
|
+
* Creates a deterministic scrubber for unbounded process output.
|
|
12
|
+
*
|
|
13
|
+
* Only bounded, undecided grammar prefixes are retained. Once a bounded sensitive marker is
|
|
14
|
+
* recognized, the scrubber enters a suppression state before consuming any unbounded whitespace,
|
|
15
|
+
* credential value, or URL authority. URL authorities are conservatively hidden in full because a
|
|
16
|
+
* streaming parser cannot know whether an arbitrarily long username will later be followed by a
|
|
17
|
+
* password separator without retaining it.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createStreamingScrubber(options?: ScrubOptions): StreamingScrubber;
|
|
3
20
|
export declare function scrubValue(value: unknown, options?: ScrubOptions): unknown;
|