jules-orchestrator-kit 0.70.0 → 0.72.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/.agent/rules/jules-protocol.md +0 -1
- package/CHANGELOG.md +66 -0
- package/JULES_RULES_TEMPLATE.md +0 -1
- package/README.md +56 -1
- package/ROADMAP_V1.md +31 -6
- package/bin/agentctl.mjs +101 -8
- package/package.json +1 -1
- package/scripts/guard-reach-check.mjs +74 -8
- package/src/config.mjs +434 -5
- package/src/coverage.mjs +85 -13
- package/src/engine.mjs +106 -130
- package/src/git.mjs +104 -1
- package/src/guard-policy.mjs +581 -0
- package/src/ops/command-registry.mjs +109 -0
- package/src/ops/test-collection.mjs +103 -3
- package/src/security.mjs +554 -4
- package/src/stack-detector.mjs +73 -14
- package/src/test-paths.mjs +18 -1
- package/src/wizard-init.mjs +54 -27
- package/src/wizard-oracle.mjs +1 -1
- package/src/wizard-task.mjs +15 -1
package/src/engine.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { loadConfig,
|
|
1
|
+
import { loadConfig, resolveTrustedPolicy } from "./config.mjs";
|
|
2
2
|
import { isTestPath } from "./test-paths.mjs";
|
|
3
|
-
import { isPlaceholderTestScript } from "./stack-detector.mjs";
|
|
3
|
+
import { isPlaceholderTestScript, isSrcLayout } from "./stack-detector.mjs";
|
|
4
4
|
import { checkCollectionFloor } from "./ops/test-collection.mjs";
|
|
5
5
|
import { checkScope, scanDiff, scanBinaryPayloads, redactSecrets } from "./security.mjs";
|
|
6
|
-
import { changedFiles, diffBytes, diffText, binaryDiffEntries, symlinkChanges, showFromOrigin, runCmd } from "./git.mjs";
|
|
6
|
+
import { changedFiles, diffBytes, diffText, binaryDiffEntries, symlinkChanges, showFromOrigin, runCmd, materializeSnapshot } from "./git.mjs";
|
|
7
7
|
import { createProvider, ProviderRateLimitError, ProviderUnavailableError } from "./provider.mjs";
|
|
8
8
|
import { resolveRoutedProvider } from "./router.mjs";
|
|
9
9
|
import { withBudget, appendLedger, getQueueDir, ensureDir, rollbackBudgetReservation, isConcurrencyGroupLocked, checkDailyBudget } from "./state.mjs";
|
|
@@ -152,13 +152,30 @@ export function fingerprintFailureState(failure = {}, root = process.cwd()) {
|
|
|
152
152
|
*/
|
|
153
153
|
export async function gate(opts = {}) {
|
|
154
154
|
const root = opts.root || process.cwd();
|
|
155
|
-
const config = opts.config || loadConfig(root);
|
|
156
|
-
const base = opts.base || config.baseBranch || "main";
|
|
157
155
|
const mode = opts.mode || (opts.workingTree ? "working-tree" : (process.env.JULES_GATE_MODE || "working-tree"));
|
|
156
|
+
const config = opts.config || (mode === "working-tree" ? loadConfig(root) : null);
|
|
158
157
|
const progressBus = opts.progressBus;
|
|
159
158
|
const progressToken = opts.progressToken;
|
|
160
159
|
const phases = [];
|
|
161
160
|
|
|
161
|
+
const policy = resolveTrustedPolicy(root, opts.base, mode, { ...opts, config });
|
|
162
|
+
if (!policy.ok) {
|
|
163
|
+
phases.push({
|
|
164
|
+
phase: "scope",
|
|
165
|
+
ok: false,
|
|
166
|
+
error: policy.error,
|
|
167
|
+
violations: [{ file: ".agent/config.yml", rule: "BUILTIN_PROTECT", reason: policy.error }],
|
|
168
|
+
});
|
|
169
|
+
appendTelemetry(root, "gate_finished", { ok: false, code: policy.code || 3, error: policy.error });
|
|
170
|
+
return { ok: false, code: policy.code || 3, phases, error: policy.error, base: policy.base };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const base = policy.base;
|
|
174
|
+
const trustedScope = policy.scope;
|
|
175
|
+
let trustedVerify = policy.verify;
|
|
176
|
+
const trustedDiffKb = policy.limits?.diffKb || 75;
|
|
177
|
+
const trustedEvidence = policy.evidence || { strictTestLock: true };
|
|
178
|
+
|
|
162
179
|
appendTelemetry(root, "gate_started", { base, mode });
|
|
163
180
|
|
|
164
181
|
let files = [];
|
|
@@ -172,73 +189,15 @@ export async function gate(opts = {}) {
|
|
|
172
189
|
} catch (err) {
|
|
173
190
|
phases.push({ phase: "git_resolution", ok: false, error: err.message });
|
|
174
191
|
appendTelemetry(root, "gate_finished", { ok: false, code: 1, error: err.message });
|
|
175
|
-
return { ok: false, code: 1, phases, error: err.message };
|
|
192
|
+
return { ok: false, code: 1, phases, error: err.message, base };
|
|
176
193
|
}
|
|
177
194
|
|
|
178
|
-
// Phase 1: Scope Guard
|
|
179
|
-
let trustedScope = config.scope;
|
|
180
|
-
let trustedVerify = config.verify;
|
|
181
|
-
let trustedDiffKb = 75;
|
|
182
|
-
|
|
183
|
-
const trustedConfigRaw = showFromOrigin(root, base, ".agent/config.yml") || showFromOrigin(root, base, ".agent/jules.yml");
|
|
184
|
-
if (trustedConfigRaw) {
|
|
185
|
-
try {
|
|
186
|
-
const parsed = parseYaml(trustedConfigRaw);
|
|
187
|
-
// CRITICAL B1 FIX: normalizeScope ensures BUILTIN_DENY is ALWAYS merged with user deny rules
|
|
188
|
-
trustedScope = normalizeScope(parsed);
|
|
189
|
-
if (parsed.limits?.diff_kb || parsed.limits?.diffKb) {
|
|
190
|
-
trustedDiffKb = Number(parsed.limits.diff_kb || parsed.limits.diffKb) || 75;
|
|
191
|
-
}
|
|
192
|
-
if (parsed.verify || parsed.test_cmd || parsed.build_cmd) {
|
|
193
|
-
trustedVerify = {
|
|
194
|
-
setup: parsed.verify?.setup || config.verify.setup,
|
|
195
|
-
lint: parsed.verify?.lint || parsed.lint_cmd || config.verify.lint,
|
|
196
|
-
test: parsed.verify?.test || parsed.test_cmd || config.verify.test,
|
|
197
|
-
unit: parsed.verify?.unit || config.verify.unit || parsed.verify?.test || parsed.test_cmd || config.verify.test,
|
|
198
|
-
fuzz: parsed.verify?.fuzz || parsed.fuzz_cmd || config.verify.fuzz,
|
|
199
|
-
invariant: parsed.verify?.invariant || parsed.invariant_cmd || config.verify.invariant,
|
|
200
|
-
e2e: parsed.verify?.e2e || parsed.e2e_cmd || config.verify.e2e,
|
|
201
|
-
teardown: parsed.verify?.teardown || config.verify.teardown,
|
|
202
|
-
build: parsed.verify?.build || parsed.build_cmd || config.verify.build,
|
|
203
|
-
stages: parsed.verify?.stages || config.verify.stages,
|
|
204
|
-
policy: parsed.verify?.policy || config.verify.policy,
|
|
205
|
-
// Read from the base commit like every other trusted field: an
|
|
206
|
-
// uncommitted `required: false` must not be able to switch the gate off.
|
|
207
|
-
required: parsed.verify?.required !== undefined ? parsed.verify.required !== false : config.verify.required !== false,
|
|
208
|
-
// The floor the collection check applies. Omitting it here meant
|
|
209
|
-
// `verify.minTests` was silently dropped and always defaulted to 1
|
|
210
|
-
// — while the failure message told the operator to set exactly
|
|
211
|
-
// that. A remediation hint that does nothing is worse than none.
|
|
212
|
-
minTests:
|
|
213
|
-
parsed.verify?.minTests !== undefined
|
|
214
|
-
? parsed.verify.minTests
|
|
215
|
-
: parsed.verify?.min_tests !== undefined
|
|
216
|
-
? parsed.verify.min_tests
|
|
217
|
-
: config.verify.minTests,
|
|
218
|
-
tamperGuard: parsed.verify?.tamperGuard || parsed.verify?.tamper_guard || config.verify.tamperGuard,
|
|
219
|
-
scope: parsed.verify?.scope || config.verify.scope || "global",
|
|
220
|
-
timeoutMs: parsed.verify?.timeoutMs || parsed.verify?.timeout_ms || config.verify.timeoutMs,
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
} catch (_) {}
|
|
224
|
-
} else {
|
|
225
|
-
// CRITICAL H-c FIX: Fall back strictly to normalizeScope({}) (built-ins only), never HEAD config
|
|
226
|
-
trustedScope = normalizeScope({});
|
|
227
|
-
if (opts.config?.limits?.diffKb || opts.config?.limits?.diff_kb) {
|
|
228
|
-
trustedDiffKb = Number(opts.config.limits.diffKb || opts.config.limits.diff_kb) || 75;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// A symlink is judged by its own name, so `notes.md -> .agent/config.yml`
|
|
233
|
-
// walked straight past a deny list that names the target. Judge both: the
|
|
234
|
-
// link because it is what the diff adds, and the path it resolves to because
|
|
235
|
-
// that is what it grants reach to.
|
|
195
|
+
// Phase 1: Scope Guard
|
|
236
196
|
let symlinks = [];
|
|
237
197
|
try {
|
|
238
198
|
symlinks = symlinkChanges(root, base, mode);
|
|
239
199
|
} catch (_) {
|
|
240
|
-
// Scope must still be enforced on the ordinary file list if git cannot
|
|
241
|
-
// describe the links.
|
|
200
|
+
// Scope must still be enforced on the ordinary file list if git cannot describe the links.
|
|
242
201
|
}
|
|
243
202
|
const scopeCandidates = [...files];
|
|
244
203
|
const symlinkTargetOf = new Map();
|
|
@@ -249,7 +208,12 @@ export async function gate(opts = {}) {
|
|
|
249
208
|
}
|
|
250
209
|
|
|
251
210
|
const scopeResult = checkScope(scopeCandidates, trustedScope, {
|
|
252
|
-
allowProtected:
|
|
211
|
+
allowProtected:
|
|
212
|
+
opts.allowProtected ||
|
|
213
|
+
process.env.JULES_ALLOW_COMMAND_FILE_CHANGES === "true" ||
|
|
214
|
+
process.env.JULES_ALLOW_COMMAND_FILE_CHANGES === "1" ||
|
|
215
|
+
process.env.AGENT_ALLOW_COMMAND_FILE_CHANGES === "true" ||
|
|
216
|
+
process.env.AGENT_ALLOW_COMMAND_FILE_CHANGES === "1",
|
|
253
217
|
});
|
|
254
218
|
// Report the violation against the link the change actually introduced, not
|
|
255
219
|
// against a path the diff never names — the operator has to be able to find it.
|
|
@@ -263,23 +227,8 @@ export async function gate(opts = {}) {
|
|
|
263
227
|
|
|
264
228
|
// Bootstrap: the files that bring a repository under the gate are not agent
|
|
265
229
|
// edits to the gate.
|
|
266
|
-
//
|
|
267
|
-
// `init` writes `.agent/**` and then tells the user to commit it. Doing
|
|
268
|
-
// exactly that produced Exit 3 on the very first run, because the base
|
|
269
|
-
// branch does not have the commit yet and every scaffolded path matches
|
|
270
|
-
// BUILTIN_PROTECT or BUILTIN_DENY. The advice printed alongside it was
|
|
271
|
-
// `--allow-protected` — so a newcomer's first lesson was how to switch the
|
|
272
|
-
// scope guard off. A gate that refuses its own installation is not strict,
|
|
273
|
-
// it is broken.
|
|
274
|
-
//
|
|
275
|
-
// Narrow on purpose, and only where it cannot weaken anything: the base
|
|
276
|
-
// commit must have no gate config at all — in which case `trustedScope` is
|
|
277
|
-
// already built-ins only and nothing in the added files is trusted — and
|
|
278
|
-
// every violating path must be scaffold that the base does not have. A
|
|
279
|
-
// repository already under the gate keeps the full rule, so an agent still
|
|
280
|
-
// cannot touch the policy it is governed by.
|
|
281
230
|
let acceptedScaffold = [];
|
|
282
|
-
if (!scopeResult.ok &&
|
|
231
|
+
if (!scopeResult.ok && policy.isBootstrap) {
|
|
283
232
|
const violations = scopeResult.violations || [];
|
|
284
233
|
const isScaffold = (f) => typeof f === "string" && f.replace(/\\/g, "/").startsWith(".agent/");
|
|
285
234
|
if (
|
|
@@ -307,7 +256,7 @@ export async function gate(opts = {}) {
|
|
|
307
256
|
|
|
308
257
|
if (!scopeResult.ok) {
|
|
309
258
|
appendTelemetry(root, "gate_finished", { ok: false, code: 3 });
|
|
310
|
-
return { ok: false, code: 3, phases };
|
|
259
|
+
return { ok: false, code: 3, phases, base };
|
|
311
260
|
}
|
|
312
261
|
|
|
313
262
|
// Phase 2: Diff Payload Governor
|
|
@@ -359,12 +308,18 @@ export async function gate(opts = {}) {
|
|
|
359
308
|
return { ok: false, code: 6, phases };
|
|
360
309
|
}
|
|
361
310
|
|
|
362
|
-
// Pre-verification: compute test directory integrity hash
|
|
363
|
-
const preTestHashResult = computeDirectoryHash(root, { testOnly: true });
|
|
364
|
-
const preTestHash = preTestHashResult.treeHash;
|
|
365
|
-
const executionRecords = [];
|
|
366
|
-
|
|
367
311
|
// Phase 4: Automated Test & Build Verification (Uses trusted verify commands only)
|
|
312
|
+
let snapshot = { cwd: root, cleanup: () => {}, mode };
|
|
313
|
+
try {
|
|
314
|
+
if (mode === "staged" || mode === "committed") {
|
|
315
|
+
snapshot = materializeSnapshot(root, mode, base);
|
|
316
|
+
}
|
|
317
|
+
} catch (snapErr) {
|
|
318
|
+
phases.push({ phase: "verify", ok: false, error: snapErr.message });
|
|
319
|
+
appendTelemetry(root, "gate_finished", { ok: false, code: snapErr.code || 1, error: snapErr.message });
|
|
320
|
+
return { ok: false, code: snapErr.code || 1, phases, error: snapErr.message, base };
|
|
321
|
+
}
|
|
322
|
+
|
|
368
323
|
let testResult = { ok: true, status: 0 };
|
|
369
324
|
let buildResult = { ok: true, status: 0 };
|
|
370
325
|
let serverResult = { ok: true, status: 0 };
|
|
@@ -400,21 +355,10 @@ export async function gate(opts = {}) {
|
|
|
400
355
|
const verifyTimeout = trustedVerify.timeoutMs || 300_000;
|
|
401
356
|
|
|
402
357
|
// Monorepo scoping: run the suites the change can actually break.
|
|
403
|
-
//
|
|
404
|
-
// `resolveWorkspaceBoundary()` shipped, was drawn in the architecture
|
|
405
|
-
// diagrams and documented as a headline feature — and was called by nothing.
|
|
406
|
-
// Every change in a monorepo ran the root suite, so a one-package edit was
|
|
407
|
-
// gated on every other package's tests. It resolves the changed files to
|
|
408
|
-
// their sub-projects and composes the per-project commands.
|
|
409
|
-
//
|
|
410
|
-
// It stays opt-in (`verify.scope: affected`) because narrowing what runs is
|
|
411
|
-
// only safe when someone asked for it, and it yields to the global command
|
|
412
|
-
// whenever a shared file is touched or no sub-project command is found — a
|
|
413
|
-
// narrower run that misses the breakage is worse than a slow one.
|
|
414
358
|
let boundary = null;
|
|
415
359
|
if (trustedVerify.scope === "affected" && !(Array.isArray(trustedVerify.stages) && trustedVerify.stages.length > 0)) {
|
|
416
360
|
try {
|
|
417
|
-
boundary = resolveWorkspaceBoundary(files,
|
|
361
|
+
boundary = resolveWorkspaceBoundary(files, snapshot.cwd);
|
|
418
362
|
} catch (_) {
|
|
419
363
|
boundary = null;
|
|
420
364
|
}
|
|
@@ -440,6 +384,13 @@ export async function gate(opts = {}) {
|
|
|
440
384
|
stagesToRun.push(...buildDefaultStages(trustedVerify));
|
|
441
385
|
}
|
|
442
386
|
|
|
387
|
+
// Pre-verification: compute test directory integrity hash on snapshot under test
|
|
388
|
+
const preTestHashResult = computeDirectoryHash(snapshot.cwd, { testOnly: true });
|
|
389
|
+
let preTestHash = preTestHashResult.treeHash;
|
|
390
|
+
let postTestHash = preTestHash;
|
|
391
|
+
const executionRecords = [];
|
|
392
|
+
let testTampered = false;
|
|
393
|
+
|
|
443
394
|
try {
|
|
444
395
|
for (const stage of stagesToRun) {
|
|
445
396
|
const isAssertion = Boolean(
|
|
@@ -459,7 +410,16 @@ export async function gate(opts = {}) {
|
|
|
459
410
|
let assertMetrics = {};
|
|
460
411
|
|
|
461
412
|
if (isAssertion) {
|
|
462
|
-
const assertRes = runAssertion(
|
|
413
|
+
const assertRes = runAssertion(
|
|
414
|
+
{
|
|
415
|
+
...stage,
|
|
416
|
+
allowTestModifications: opts.allowTestModifications === true,
|
|
417
|
+
allowTestChanges: opts.allowTestChanges,
|
|
418
|
+
tamperGuard: trustedVerify.tamperGuard,
|
|
419
|
+
allowUnreadableTests: opts.allowUnreadableTests === true,
|
|
420
|
+
},
|
|
421
|
+
snapshot.cwd
|
|
422
|
+
);
|
|
463
423
|
durationMs = assertRes.metrics?.durationMs ?? (Date.now() - startTime);
|
|
464
424
|
stdoutRedacted = assertRes.stdout || "";
|
|
465
425
|
stderrRedacted = assertRes.stderr || "";
|
|
@@ -470,9 +430,16 @@ export async function gate(opts = {}) {
|
|
|
470
430
|
} else {
|
|
471
431
|
const stageEnv = stage.networkAccess === "forbidden" || trustedVerify.policy?.offline
|
|
472
432
|
? { ...testEnv, JULES_SANDBOX_OFFLINE: "1" }
|
|
473
|
-
: testEnv;
|
|
433
|
+
: { ...testEnv };
|
|
434
|
+
|
|
435
|
+
// F11: Prepend src/ to PYTHONPATH for Python src-layout repositories
|
|
436
|
+
if (isSrcLayout(snapshot.cwd)) {
|
|
437
|
+
const currentPypath = stageEnv.PYTHONPATH || "";
|
|
438
|
+
const srcDir = join(snapshot.cwd, "src");
|
|
439
|
+
stageEnv.PYTHONPATH = currentPypath ? `${srcDir}:${currentPypath}` : srcDir;
|
|
440
|
+
}
|
|
474
441
|
|
|
475
|
-
res = runCmd(stage.cmd, { cwd:
|
|
442
|
+
res = runCmd(stage.cmd, { cwd: snapshot.cwd, ignoreError: true, env: stageEnv, timeout: stage.timeoutMs || verifyTimeout });
|
|
476
443
|
durationMs = Date.now() - startTime;
|
|
477
444
|
stdoutRedacted = redactSecrets(res.stdout || "");
|
|
478
445
|
stderrRedacted = redactSecrets(res.stderr || "");
|
|
@@ -493,7 +460,7 @@ export async function gate(opts = {}) {
|
|
|
493
460
|
|
|
494
461
|
if (stage.kind === "test" || stage.kind === "unit") {
|
|
495
462
|
testResult = { ok: stageOk, status: res.status, stdout: stdoutRedacted, stderr: stderrRedacted, command: stage.cmd };
|
|
496
|
-
const fingerprint = !stageOk ? fingerprintFailureState(testResult,
|
|
463
|
+
const fingerprint = !stageOk ? fingerprintFailureState(testResult, snapshot.cwd) : null;
|
|
497
464
|
if (stage.cmd) recordVerifyRun(root, stage.cmd, stageOk, fingerprint, durationMs);
|
|
498
465
|
if (!stageOk && stage.cmd) {
|
|
499
466
|
const runs = readVerifyRuns(root, stage.cmd);
|
|
@@ -517,7 +484,7 @@ export async function gate(opts = {}) {
|
|
|
517
484
|
});
|
|
518
485
|
appendTelemetry(root, "gate_phase", { phase: "verify", ok: false, quarantined: true });
|
|
519
486
|
appendTelemetry(root, "gate_finished", { ok: false, code: 8 });
|
|
520
|
-
return { ok: false, code: 8, phases, flakyVerdict: flakyVerdictResult };
|
|
487
|
+
return { ok: false, code: 8, phases, flakyVerdict: flakyVerdictResult, base };
|
|
521
488
|
}
|
|
522
489
|
}
|
|
523
490
|
} else if (stage.kind === "build") {
|
|
@@ -539,30 +506,30 @@ export async function gate(opts = {}) {
|
|
|
539
506
|
break;
|
|
540
507
|
}
|
|
541
508
|
}
|
|
509
|
+
|
|
510
|
+
if (!failingCmd && trustedVerify.server && trustedVerify.server.command) {
|
|
511
|
+
serverResult = await probeDevServer(trustedVerify.server, snapshot.cwd);
|
|
512
|
+
if (!serverResult.ok) {
|
|
513
|
+
failingCmd = serverResult;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Post-verification test integrity check (No Test Weakening Invariant)
|
|
518
|
+
const postTestHashResult = computeDirectoryHash(snapshot.cwd, { testOnly: true });
|
|
519
|
+
postTestHash = postTestHashResult.treeHash;
|
|
520
|
+
if (trustedEvidence.strictTestLock && !opts.allowTestModifications && preTestHashResult.fileCount > 0) {
|
|
521
|
+
const changedTestFile = files.find((f) => isTestPath(f));
|
|
522
|
+
if (changedTestFile && preTestHash !== postTestHash) {
|
|
523
|
+
testTampered = true;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
542
526
|
} finally {
|
|
543
527
|
if (trustedVerify.teardown) {
|
|
544
528
|
try {
|
|
545
|
-
runCmd(trustedVerify.teardown, { cwd:
|
|
529
|
+
runCmd(trustedVerify.teardown, { cwd: snapshot.cwd, ignoreError: true, env: testEnv });
|
|
546
530
|
} catch (_) {}
|
|
547
531
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
if (!failingCmd && trustedVerify.server && trustedVerify.server.command) {
|
|
551
|
-
serverResult = await probeDevServer(trustedVerify.server, root);
|
|
552
|
-
if (!serverResult.ok) {
|
|
553
|
-
failingCmd = serverResult;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
// Post-verification test integrity check (No Test Weakening Invariant)
|
|
558
|
-
const postTestHashResult = computeDirectoryHash(root, { testOnly: true });
|
|
559
|
-
const postTestHash = postTestHashResult.treeHash;
|
|
560
|
-
let testTampered = false;
|
|
561
|
-
if (config.evidence?.strictTestLock && !opts.allowTestModifications && preTestHashResult.fileCount > 0) {
|
|
562
|
-
const changedTestFile = files.find((f) => isTestPath(f));
|
|
563
|
-
if (changedTestFile && preTestHash !== postTestHash) {
|
|
564
|
-
testTampered = true;
|
|
565
|
-
}
|
|
532
|
+
snapshot.cleanup();
|
|
566
533
|
}
|
|
567
534
|
|
|
568
535
|
// A gate that ran no verification at all must not report APPROVED.
|
|
@@ -696,9 +663,9 @@ export async function gate(opts = {}) {
|
|
|
696
663
|
executionRecords,
|
|
697
664
|
secretScanOk: secretResult.ok,
|
|
698
665
|
diffKb: Math.round(bytes / 1024),
|
|
699
|
-
maxDiffKb:
|
|
666
|
+
maxDiffKb: trustedDiffKb,
|
|
700
667
|
protectedScopeOk: scopeResult.ok,
|
|
701
|
-
repository:
|
|
668
|
+
repository: policy.provider || config?.provider || "jules",
|
|
702
669
|
failedStage: failingCmd?.stageId || failingCmd?.phase || null,
|
|
703
670
|
diagnostics: failingCmd?.diagnostics?.length ? failingCmd.diagnostics : (failingCmd?.stderr ? [failingCmd.stderr] : []),
|
|
704
671
|
metrics: failingCmd?.metrics || {},
|
|
@@ -709,9 +676,18 @@ export async function gate(opts = {}) {
|
|
|
709
676
|
evidenceManifest.testIntegrity.tamperDetected = true;
|
|
710
677
|
}
|
|
711
678
|
let evidencePath = null;
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
679
|
+
const shouldPersistEvidence =
|
|
680
|
+
!opts.dryRun &&
|
|
681
|
+
!opts["dry-run"] &&
|
|
682
|
+
process.env.JULES_DRY_RUN !== "1" &&
|
|
683
|
+
process.env.AGENT_DRY_RUN !== "1" &&
|
|
684
|
+
trustedEvidence.enabled !== false &&
|
|
685
|
+
config?.evidence?.enabled !== false;
|
|
686
|
+
if (shouldPersistEvidence) {
|
|
687
|
+
try {
|
|
688
|
+
evidencePath = writeEvidenceManifest(root, evidenceManifest);
|
|
689
|
+
} catch (_) {}
|
|
690
|
+
}
|
|
715
691
|
|
|
716
692
|
// Export structured JSON report if requested
|
|
717
693
|
const jsonReportPath = opts.jsonReport || opts.jsonReportPath || process.env.JULES_JSON_REPORT || null;
|
package/src/git.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execFileSync, execSync } from "node:child_process";
|
|
2
|
-
import { readFileSync, existsSync, statSync, lstatSync, readlinkSync } from "node:fs";
|
|
2
|
+
import { readFileSync, existsSync, statSync, lstatSync, readlinkSync, mkdtempSync, rmSync, symlinkSync } from "node:fs";
|
|
3
3
|
import { join, delimiter } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
4
5
|
import { normalizePath, canonicalizePath } from "./config.mjs";
|
|
5
6
|
|
|
6
7
|
|
|
@@ -676,6 +677,108 @@ export function worktreePrune(root = process.cwd()) {
|
|
|
676
677
|
return git(["worktree", "prune"], { cwd: root });
|
|
677
678
|
}
|
|
678
679
|
|
|
680
|
+
/**
|
|
681
|
+
* Materialize the revision under review into an isolated execution directory (F10).
|
|
682
|
+
*
|
|
683
|
+
* In working-tree mode, verification executes against the live tree (`root`).
|
|
684
|
+
* In staged or committed mode, the verification command must execute against the
|
|
685
|
+
* snapshot being judged, rather than the working copy — attesting a revision
|
|
686
|
+
* you did not execute is the failure this tool exists to refuse.
|
|
687
|
+
*
|
|
688
|
+
* @param {string} root - repository root
|
|
689
|
+
* @param {string} mode - "working-tree" | "staged" | "committed"
|
|
690
|
+
* @param {string} [targetRef="HEAD"] - commit reference for committed mode
|
|
691
|
+
* @returns {{ cwd: string, cleanup: () => void, mode: string }}
|
|
692
|
+
*/
|
|
693
|
+
export function materializeSnapshot(root = process.cwd(), mode = "working-tree", targetRef = "HEAD") {
|
|
694
|
+
if (mode === "working-tree" || mode === "working") {
|
|
695
|
+
return {
|
|
696
|
+
cwd: root,
|
|
697
|
+
cleanup: () => {},
|
|
698
|
+
mode,
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const prefix = `jules-${mode}-snapshot-`;
|
|
703
|
+
const tmpDir = mkdtempSync(join(tmpdir(), prefix));
|
|
704
|
+
let worktreeAdded = false;
|
|
705
|
+
|
|
706
|
+
try {
|
|
707
|
+
if (mode === "staged" || mode === "index") {
|
|
708
|
+
execFileSync("git", ["-c", "core.quotePath=false", "checkout-index", "--all", `--prefix=${tmpDir}/`], {
|
|
709
|
+
cwd: root,
|
|
710
|
+
encoding: "utf-8",
|
|
711
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
712
|
+
});
|
|
713
|
+
} else {
|
|
714
|
+
// committed mode
|
|
715
|
+
try {
|
|
716
|
+
execFileSync("git", ["worktree", "add", "--detach", tmpDir, targetRef || "HEAD"], {
|
|
717
|
+
cwd: root,
|
|
718
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
719
|
+
});
|
|
720
|
+
worktreeAdded = true;
|
|
721
|
+
} catch (_) {
|
|
722
|
+
// Fallback: git archive piped to tar when worktree cannot be created (e.g. read-only .git)
|
|
723
|
+
try {
|
|
724
|
+
const tar = execFileSync("git", ["archive", targetRef || "HEAD"], {
|
|
725
|
+
cwd: root,
|
|
726
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
727
|
+
});
|
|
728
|
+
execFileSync("tar", ["-x", "-C", tmpDir], { input: tar, stdio: ["pipe", "ignore", "pipe"] });
|
|
729
|
+
} catch (tarErr) {
|
|
730
|
+
throw new Error(`Failed to extract snapshot for ${targetRef || "HEAD"}: ${tarErr.message}`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Preserve local runtime dependencies in snapshot so offline/sandboxed verification can run:
|
|
736
|
+
const ignoredDeps = ["node_modules", ".venv", "venv", "target", ".pytest_cache"];
|
|
737
|
+
for (const dep of ignoredDeps) {
|
|
738
|
+
const srcDep = join(root, dep);
|
|
739
|
+
const dstDep = join(tmpDir, dep);
|
|
740
|
+
if (existsSync(srcDep) && !existsSync(dstDep)) {
|
|
741
|
+
try {
|
|
742
|
+
symlinkSync(srcDep, dstDep, "junction");
|
|
743
|
+
} catch (_) {}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
const cleanup = () => {
|
|
748
|
+
if (worktreeAdded) {
|
|
749
|
+
try {
|
|
750
|
+
execFileSync("git", ["worktree", "remove", "--force", tmpDir], {
|
|
751
|
+
cwd: root,
|
|
752
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
753
|
+
});
|
|
754
|
+
} catch (_) {}
|
|
755
|
+
}
|
|
756
|
+
try {
|
|
757
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
758
|
+
} catch (_) {}
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
return {
|
|
762
|
+
cwd: tmpDir,
|
|
763
|
+
cleanup,
|
|
764
|
+
mode,
|
|
765
|
+
};
|
|
766
|
+
} catch (err) {
|
|
767
|
+
if (worktreeAdded) {
|
|
768
|
+
try {
|
|
769
|
+
execFileSync("git", ["worktree", "remove", "--force", tmpDir], {
|
|
770
|
+
cwd: root,
|
|
771
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
772
|
+
});
|
|
773
|
+
} catch (_) {}
|
|
774
|
+
}
|
|
775
|
+
try {
|
|
776
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
777
|
+
} catch (_) {}
|
|
778
|
+
throw new GateError(`Failed to materialize ${mode} snapshot for verification: ${err.message}`, { code: 1 });
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
679
782
|
/**
|
|
680
783
|
* Extract 'owner/repo' from a git remote URL if it points to GitHub.
|
|
681
784
|
* Handles SSH (git@github.com:owner/repo.git), HTTPS (https://github.com/owner/repo.git),
|