jules-orchestrator-kit 0.52.2 → 0.52.4
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/bin/agentctl.mjs +4 -1
- package/package.json +1 -1
- package/src/coverage.mjs +10 -18
- package/src/engine.mjs +14 -2
- package/src/git.mjs +19 -14
- package/src/mutation.mjs +12 -18
- package/src/perf.mjs +10 -14
- package/src/provider.mjs +1 -1
- package/src/remediation.mjs +1 -1
- package/src/security.mjs +46 -4
- package/src/session-ops.mjs +18 -10
- package/src/webhook.mjs +9 -7
- package/src/wizard-init.mjs +36 -10
package/README.md
CHANGED
|
@@ -143,7 +143,7 @@ To maximize PR merge rates, dispatch tasks according to deterministic boundaries
|
|
|
143
143
|
* **Fail-Closed Security & Secret Redaction:** Evaluates explicit Deny rules before Allow rules against canonicalized, case-folded paths. Redacts high-entropy keys and base64-encoded credentials (such as Kubernetes `Secret` manifests).
|
|
144
144
|
* **Complexity & Cost Router:** Zero-dependency heuristic classifier (`src/router.mjs`) routing mechanical tasks to lightweight models while reserving primary models for complex refactors, with a `node --check` syntax-verification gate that transparently escalates a FAST-tier result to the primary provider if it left broken JS on disk.
|
|
145
145
|
* **Terminal UI & Diagnostic Matrix (`agentctl doctor`):** Interactive terminal dashboard, task sidecar manager, and automated transactional self-repair.
|
|
146
|
-
* **Verified Test Suite:** Tested with **
|
|
146
|
+
* **Verified Test Suite:** Tested with **815 unit tests across 91 suites passing in < 10.0s**.
|
|
147
147
|
|
|
148
148
|
<br/>
|
|
149
149
|
|
package/bin/agentctl.mjs
CHANGED
|
@@ -427,7 +427,10 @@ async function main() {
|
|
|
427
427
|
} else if (res.flakyVerdict?.verdict === "QUARANTINED") {
|
|
428
428
|
console.log(`💡 Remediation Hint (Exit 8 Flaky Test Quarantined):`);
|
|
429
429
|
console.log(` • This command has alternated between pass and fail across recent runs.`);
|
|
430
|
-
|
|
430
|
+
} else if (res.code === 188) {
|
|
431
|
+
console.log(`💡 Remediation Hint (Exit 188 Offline Network Violation / Infrastructure):`);
|
|
432
|
+
console.log(` • An unmocked network request was blocked by the preload network guard during verification.`);
|
|
433
|
+
console.log(` • Ensure dependencies are installed locally (run: npm install) and all network calls in tests are mocked.\n`);
|
|
431
434
|
} else if (failedPhase === "verify" || failedPhase === "evidence") {
|
|
432
435
|
console.log(`💡 Remediation Hint (Exit ${res.code} Verification Failed):`);
|
|
433
436
|
console.log(` • The stage above exited non-zero. Reproduce it locally, then re-run the gate.`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jules-orchestrator-kit",
|
|
3
|
-
"version": "0.52.
|
|
3
|
+
"version": "0.52.4",
|
|
4
4
|
"description": "Zero-dependency safety gatekeeper, test oracle generator, and multi-agent coordination protocol for Google Jules (jules) autonomous agents.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
package/src/coverage.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { readFileSync, readdirSync, mkdtempSync, rmSync, existsSync, realpathSyn
|
|
|
2
2
|
import { join, resolve, relative, isAbsolute } from "node:path";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import {
|
|
5
|
+
import { runCmd } from "./git.mjs";
|
|
6
6
|
import { resolveVerify } from "./config.mjs";
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -136,23 +136,15 @@ export function runV8Coverage(testCmd, options = {}) {
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
149
|
-
encoding: "utf-8",
|
|
150
|
-
});
|
|
151
|
-
} catch (err) {
|
|
152
|
-
exitCode = err.status || 1;
|
|
153
|
-
stdout = err.stdout ? String(err.stdout) : "";
|
|
154
|
-
stderr = err.stderr ? String(err.stderr) : err.message;
|
|
155
|
-
}
|
|
139
|
+
const res = runCmd(cmd, {
|
|
140
|
+
cwd: root,
|
|
141
|
+
env,
|
|
142
|
+
timeout: timeoutMs,
|
|
143
|
+
ignoreError: true,
|
|
144
|
+
});
|
|
145
|
+
const exitCode = res.status;
|
|
146
|
+
const stdout = res.stdout;
|
|
147
|
+
const stderr = res.stderr;
|
|
156
148
|
|
|
157
149
|
const coverageByFile = new Map();
|
|
158
150
|
const files = existsSync(tempCoverageDir) ? readdirSync(tempCoverageDir) : [];
|
package/src/engine.mjs
CHANGED
|
@@ -433,6 +433,14 @@ export async function gate(opts = {}) {
|
|
|
433
433
|
// the one gate failure the operator is expected to fix themselves was the
|
|
434
434
|
// only one that told them nothing about how. Both streams are already
|
|
435
435
|
// redacted at the point they were captured.
|
|
436
|
+
const isNetViolation = failingCmd?.status === 188;
|
|
437
|
+
const netViolationDiagnostics = isNetViolation
|
|
438
|
+
? [
|
|
439
|
+
"Offline Network Guard: Outbound network request was blocked (Exit 188).",
|
|
440
|
+
"Ensure dependencies are installed locally (run npm install) and tests do not perform unmocked network access.",
|
|
441
|
+
]
|
|
442
|
+
: [];
|
|
443
|
+
|
|
436
444
|
const verifyFailure = failingCmd
|
|
437
445
|
? {
|
|
438
446
|
stageId: failingCmd.stageId || failingCmd.phase || "verify",
|
|
@@ -440,7 +448,7 @@ export async function gate(opts = {}) {
|
|
|
440
448
|
exitCode: failingCmd.status ?? null,
|
|
441
449
|
stdout: failingCmd.stdout || "",
|
|
442
450
|
stderr: failingCmd.stderr || "",
|
|
443
|
-
diagnostics: failingCmd.diagnostics || [],
|
|
451
|
+
diagnostics: [...netViolationDiagnostics, ...(failingCmd.diagnostics || [])],
|
|
444
452
|
}
|
|
445
453
|
: testTampered
|
|
446
454
|
? {
|
|
@@ -510,6 +518,10 @@ export async function gate(opts = {}) {
|
|
|
510
518
|
}
|
|
511
519
|
|
|
512
520
|
if (!verifyOk && opts.fix && failingCmd) {
|
|
521
|
+
if (isNetViolation) {
|
|
522
|
+
appendTelemetry(root, "gate_finished", { ok: false, code: 188, networkViolation: true });
|
|
523
|
+
return { ok: false, code: 188, phases, evidence: evidenceManifest };
|
|
524
|
+
}
|
|
513
525
|
const repairs = await repair(failingCmd, { config, root, signal: opts.signal, progressBus, progressToken });
|
|
514
526
|
const finalOk = repairs.ok;
|
|
515
527
|
const finalCode = repairs.ok ? 0 : 4;
|
|
@@ -520,7 +532,7 @@ export async function gate(opts = {}) {
|
|
|
520
532
|
return { ok: false, code: 4, phases, repairs, evidence: evidenceManifest };
|
|
521
533
|
}
|
|
522
534
|
|
|
523
|
-
const code = verifyOk ? 0 : testTampered ? 3 : 4;
|
|
535
|
+
const code = verifyOk ? 0 : isNetViolation ? 188 : testTampered ? 3 : 4;
|
|
524
536
|
appendTelemetry(root, "gate_finished", { ok: verifyOk, code });
|
|
525
537
|
return { ok: verifyOk, code, phases, evidence: evidenceManifest };
|
|
526
538
|
}
|
package/src/git.mjs
CHANGED
|
@@ -306,7 +306,10 @@ export function ensureBaseFetched(root = process.cwd(), baseRef = "main") {
|
|
|
306
306
|
}
|
|
307
307
|
|
|
308
308
|
export function resolveBase(root = process.cwd(), baseRef = "main") {
|
|
309
|
-
const
|
|
309
|
+
const isHeadRef = baseRef === "HEAD" || baseRef.startsWith("HEAD~") || baseRef.startsWith("HEAD^") || baseRef.startsWith("HEAD@");
|
|
310
|
+
const candidates = isHeadRef
|
|
311
|
+
? [baseRef]
|
|
312
|
+
: [`origin/${baseRef}`, `refs/remotes/origin/${baseRef}`, baseRef];
|
|
310
313
|
for (const ref of candidates) {
|
|
311
314
|
try {
|
|
312
315
|
const res = execFileSync("git", ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], {
|
|
@@ -321,20 +324,22 @@ export function resolveBase(root = process.cwd(), baseRef = "main") {
|
|
|
321
324
|
} catch (_) {}
|
|
322
325
|
}
|
|
323
326
|
|
|
324
|
-
|
|
327
|
+
if (!isHeadRef) {
|
|
328
|
+
ensureBaseFetched(root, baseRef);
|
|
325
329
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
330
|
+
for (const ref of candidates) {
|
|
331
|
+
try {
|
|
332
|
+
const res = execFileSync("git", ["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], {
|
|
333
|
+
cwd: root,
|
|
334
|
+
encoding: "utf-8",
|
|
335
|
+
shell: false,
|
|
336
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
337
|
+
});
|
|
338
|
+
if (res && res.trim()) {
|
|
339
|
+
return res.trim();
|
|
340
|
+
}
|
|
341
|
+
} catch (_) {}
|
|
342
|
+
}
|
|
338
343
|
}
|
|
339
344
|
|
|
340
345
|
throw new GateError(
|
package/src/mutation.mjs
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
2
|
import { resolve, isAbsolute } from "node:path";
|
|
3
|
-
import { execSync } from "node:child_process";
|
|
4
3
|
import { createHash } from "node:crypto";
|
|
5
|
-
import { diffText } from "./git.mjs";
|
|
4
|
+
import { diffText, runCmd } from "./git.mjs";
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* @typedef {Object} MutationCandidate
|
|
@@ -574,22 +573,17 @@ export function executeMutant(mutant, options = {}) {
|
|
|
574
573
|
stdout = execResult.stdout || "";
|
|
575
574
|
stderr = execResult.stderr || "";
|
|
576
575
|
} else {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
stdout = execErr.stdout ? execErr.stdout.toString() : "";
|
|
589
|
-
stderr = execErr.stderr ? execErr.stderr.toString() : execErr.message;
|
|
590
|
-
if (execErr.code === "ETIMEDOUT" || execErr.killed) {
|
|
591
|
-
status = "TIMEOUT";
|
|
592
|
-
}
|
|
576
|
+
const execResult = runCmd(testCmd, {
|
|
577
|
+
cwd: root,
|
|
578
|
+
timeout: timeoutMs,
|
|
579
|
+
env: { ...process.env, CI: "true", JULES_MUTATION_RUN: "true" },
|
|
580
|
+
ignoreError: true,
|
|
581
|
+
});
|
|
582
|
+
exitCode = execResult.status;
|
|
583
|
+
stdout = execResult.stdout;
|
|
584
|
+
stderr = execResult.stderr;
|
|
585
|
+
if (execResult.status === 124 || stderr.includes("ETIMEDOUT")) {
|
|
586
|
+
status = "TIMEOUT";
|
|
593
587
|
}
|
|
594
588
|
}
|
|
595
589
|
|
package/src/perf.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { monitorEventLoopDelay } from "node:perf_hooks";
|
|
2
|
-
import {
|
|
2
|
+
import { runCmd } from "./git.mjs";
|
|
3
3
|
import { resolveVerify } from "./config.mjs";
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -41,19 +41,15 @@ export function measureEventLoopDelay(target, options = {}) {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
exitCode = err.status || 1;
|
|
54
|
-
stdout = err.stdout ? String(err.stdout) : "";
|
|
55
|
-
stderr = err.stderr ? String(err.stderr) : err.message;
|
|
56
|
-
}
|
|
44
|
+
const execResult = runCmd(cmd, {
|
|
45
|
+
cwd: root,
|
|
46
|
+
env,
|
|
47
|
+
timeout: timeoutMs,
|
|
48
|
+
ignoreError: true,
|
|
49
|
+
});
|
|
50
|
+
exitCode = execResult.status;
|
|
51
|
+
stdout = execResult.stdout;
|
|
52
|
+
stderr = execResult.stderr;
|
|
57
53
|
}
|
|
58
54
|
} finally {
|
|
59
55
|
histogram.disable();
|
package/src/provider.mjs
CHANGED
|
@@ -953,7 +953,7 @@ export function createProvider(spec = "jules", config = {}) {
|
|
|
953
953
|
if (ctx.pageSize) params.set("pageSize", String(ctx.pageSize));
|
|
954
954
|
if (ctx.pageToken) params.set("pageToken", String(ctx.pageToken));
|
|
955
955
|
const url = params.toString() ? `${baseUrl}?${params.toString()}` : baseUrl;
|
|
956
|
-
const res = await this.getSession(
|
|
956
|
+
const res = await this.getSession(sessionId, { ...ctx, customUrl: url });
|
|
957
957
|
return {
|
|
958
958
|
activities: res?.raw?.activities || [],
|
|
959
959
|
nextPageToken: res?.raw?.nextPageToken,
|
package/src/remediation.mjs
CHANGED
|
@@ -285,7 +285,7 @@ export function createWhackAMoleDetector(opts = {}) {
|
|
|
285
285
|
const oscillatingTests = [...cycleTests];
|
|
286
286
|
|
|
287
287
|
const testSummary = oscillatingTests.length > 0 ? oscillatingTests.join(" <-> ") : "tests";
|
|
288
|
-
const promptDirective = `[WHACK_A_MOLE_WARNING] You are trapped in a local optimization cycle where fixing one test breaks another (
|
|
288
|
+
const promptDirective = `[WHACK_A_MOLE_WARNING] You are trapped in a local optimization cycle where fixing one test breaks another (<UNTRUSTED>${testSummary}</UNTRUSTED>). Do not add more conditional edge-case band-aids. Revert recent patches and refactor the core logic cleanly.`;
|
|
289
289
|
|
|
290
290
|
return {
|
|
291
291
|
whackAMole: true,
|
package/src/security.mjs
CHANGED
|
@@ -588,6 +588,11 @@ function secretScanVariants(addedLines) {
|
|
|
588
588
|
// Collapse method concatenations like .concat("...") or .join("")
|
|
589
589
|
dejoined = dejoined.replace(/\.concat\(\s*["'`]/g, "").replace(/\.join\(\s*["'`]{2}\s*\)/g, "");
|
|
590
590
|
|
|
591
|
+
// Collapse whitespace/newlines between adjacent base64 characters (including line-wrapped PEM/base64, template literals, and quoted string chunks)
|
|
592
|
+
const base64Dejoined = stripped
|
|
593
|
+
.replace(/([A-Za-z0-9+/=_-])\s*[\r\n]+\s*(?=[A-Za-z0-9+/=_-])/g, "$1")
|
|
594
|
+
.replace(/([A-Za-z0-9+/=_-])["'`]\s*(?:\+\s*)?[\r\n]+\s*["'`]?([A-Za-z0-9+/=_-])/g, "$1$2");
|
|
595
|
+
|
|
591
596
|
const hexDecoded = tryHexDecodeTokens(dejoined);
|
|
592
597
|
const pctDecoded = tryPercentDecode(dejoined);
|
|
593
598
|
// Confusable / NFKD normalisation runs over both the raw text and the
|
|
@@ -597,8 +602,9 @@ function secretScanVariants(addedLines) {
|
|
|
597
602
|
const confusableDejoined = normalizeSecretText(dejoined);
|
|
598
603
|
|
|
599
604
|
return {
|
|
600
|
-
all: [...new Set([addedLines, stripped, dejoined, hexDecoded, pctDecoded, confusable, confusableDejoined])],
|
|
605
|
+
all: [...new Set([addedLines, stripped, dejoined, base64Dejoined, hexDecoded, pctDecoded, confusable, confusableDejoined])],
|
|
601
606
|
normalized: dejoined,
|
|
607
|
+
base64Normalized: base64Dejoined,
|
|
602
608
|
};
|
|
603
609
|
}
|
|
604
610
|
|
|
@@ -725,9 +731,22 @@ function decodeBase64Blobs(text, onDecoded) {
|
|
|
725
731
|
* @returns {boolean}
|
|
726
732
|
*/
|
|
727
733
|
export function hasEncodedSecret(text) {
|
|
734
|
+
if (!text) return false;
|
|
728
735
|
const result = decodeBase64Blobs(text);
|
|
729
736
|
if (result.capped) return true; // Fail closed if cap exceeded
|
|
730
|
-
|
|
737
|
+
if (result.decoded.some((plain) => hasHighConfidenceSecret(plain))) return true;
|
|
738
|
+
|
|
739
|
+
if (text.includes("\n") || text.includes("\r")) {
|
|
740
|
+
const collapsed = text
|
|
741
|
+
.replace(/([A-Za-z0-9+/=_-])\s*[\r\n]+\s*(?=[A-Za-z0-9+/=_-])/g, "$1")
|
|
742
|
+
.replace(/([A-Za-z0-9+/=_-])["'`]\s*(?:\+\s*)?[\r\n]+\s*["'`]?([A-Za-z0-9+/=_-])/g, "$1$2");
|
|
743
|
+
if (collapsed !== text) {
|
|
744
|
+
const collapsedResult = decodeBase64Blobs(collapsed);
|
|
745
|
+
if (collapsedResult.capped) return true;
|
|
746
|
+
if (collapsedResult.decoded.some((plain) => hasHighConfidenceSecret(plain))) return true;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return false;
|
|
731
750
|
}
|
|
732
751
|
|
|
733
752
|
/**
|
|
@@ -784,14 +803,14 @@ function splitDiffByFile(diffText) {
|
|
|
784
803
|
* @returns {{ severity: string, type: string, description: string, encoded: boolean }|null}
|
|
785
804
|
*/
|
|
786
805
|
function classifyAddedLines(addedLines) {
|
|
787
|
-
const { all: variants, normalized } = secretScanVariants(addedLines);
|
|
806
|
+
const { all: variants, normalized, base64Normalized } = secretScanVariants(addedLines);
|
|
788
807
|
if (variants.some((v) => hasHighConfidenceSecret(v))) {
|
|
789
808
|
return { severity: "CRITICAL", type: "HIGH_CONFIDENCE_SECRET", encoded: false, description: "High-confidence secret pattern detected in added diff lines" };
|
|
790
809
|
}
|
|
791
810
|
// Only worth decoding when nothing was found in the clear, and only against
|
|
792
811
|
// the fully-normalised text: decoding is the expensive step, and the
|
|
793
812
|
// intermediate variants differ from it in ways base64 blobs do not care about.
|
|
794
|
-
if (hasEncodedSecret(normalized)) {
|
|
813
|
+
if (hasEncodedSecret(normalized) || (base64Normalized && hasEncodedSecret(base64Normalized))) {
|
|
795
814
|
// Same type as the cleartext case: every gate that blocks on
|
|
796
815
|
// HIGH_CONFIDENCE_SECRET should block on this too, and a new type would
|
|
797
816
|
// have silently passed through the ones not updated. The description
|
|
@@ -868,6 +887,17 @@ export function checkTestTampering(diffOrText = "", options = {}) {
|
|
|
868
887
|
|
|
869
888
|
const COMMENTED_ASSERTION = /^\+\s*(?:\/\/|\/\*)\s*(?:expect\(|assert\.|assert\(|t\.expect|t\.assert)/i;
|
|
870
889
|
|
|
890
|
+
const VACUOUS_ASSERTIONS = [
|
|
891
|
+
{ pattern: /\bassert(?:\.ok)?\s*\(\s*true\s*(?:,[^)]*)?\)/i, desc: "Vacuous truth assertion (assert.ok(true))" },
|
|
892
|
+
{ pattern: /\bassert\.(?:strictEqual|deepStrictEqual|equal|deepEqual)\s*\(\s*([^,]+?)\s*,\s*\1\s*(?:,[^)]*)?\)/i, desc: "Vacuous identity assertion (assert.equal(X, X))" },
|
|
893
|
+
{ pattern: /\bexpect\s*\(\s*true\s*\)\s*\.(?:toBe|toEqual)\s*\(\s*true\s*\)/i, desc: "Vacuous truth expectation (expect(true).toBe(true))" },
|
|
894
|
+
{ pattern: /\bexpect\s*\(\s*([^)]+?)\s*\)\s*\.(?:toBe|toEqual)\s*\(\s*\1\s*\)/i, desc: "Vacuous identity expectation (expect(X).toBe(X))" },
|
|
895
|
+
{ pattern: /\bexpect\s*\(\s*true\s*\)\s*\.toBeTruthy\s*\(/i, desc: "Vacuous truth expectation (expect(true).toBeTruthy())" },
|
|
896
|
+
{ pattern: /\bexpect\s*\(\s*false\s*\)\s*\.toBeFalsy\s*\(/i, desc: "Vacuous falsity expectation (expect(false).toBeFalsy())" },
|
|
897
|
+
{ pattern: /\bassert\.(?:isTrue|isOk)\s*\(\s*true\s*(?:,[^)]*)?\)/i, desc: "Vacuous truth assertion (assert.isTrue(true))" },
|
|
898
|
+
{ pattern: /\bassert\.(?:isFalse|isNotOk)\s*\(\s*false\s*(?:,[^)]*)?\)/i, desc: "Vacuous falsity assertion (assert.isFalse(false))" },
|
|
899
|
+
];
|
|
900
|
+
|
|
871
901
|
for (let i = 0; i < lines.length; i++) {
|
|
872
902
|
const line = lines[i];
|
|
873
903
|
if ((line.startsWith("+++ ") || line.startsWith("+++ b/") || line.startsWith("+++ /dev/null")) && !line.startsWith("++++")) {
|
|
@@ -902,6 +932,18 @@ export function checkTestTampering(diffOrText = "", options = {}) {
|
|
|
902
932
|
}
|
|
903
933
|
}
|
|
904
934
|
|
|
935
|
+
// Check vacuous assertions
|
|
936
|
+
for (const vacuousRule of VACUOUS_ASSERTIONS) {
|
|
937
|
+
if (vacuousRule.pattern.test(addedText)) {
|
|
938
|
+
violations.push({
|
|
939
|
+
file: currentFile,
|
|
940
|
+
line: currentLineNo,
|
|
941
|
+
type: "VACUOUS_ASSERTION",
|
|
942
|
+
reason: `Test Tamper Guard: ${vacuousRule.desc} detected in ${currentFile}${currentLineNo ? `:${currentLineNo}` : ""}`,
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
905
947
|
// Check commented-out assertions
|
|
906
948
|
if (COMMENTED_ASSERTION.test(line)) {
|
|
907
949
|
violations.push({
|
package/src/session-ops.mjs
CHANGED
|
@@ -68,17 +68,25 @@ export async function extractSessionPatch(sessionId, opts = {}) {
|
|
|
68
68
|
// 2. Inspect activity artifacts and logs if patch wasn't in top-level output
|
|
69
69
|
for (const act of activities) {
|
|
70
70
|
if (act.pullRequest) pr = act.pullRequest;
|
|
71
|
-
if (act.gitPatch || act.patch || act.diff) {
|
|
72
|
-
patch = patch || act.gitPatch || act.patch || act.diff;
|
|
71
|
+
if (act.gitPatch || act.patch || act.diff || act.changeSet?.gitPatch?.unidiffPatch) {
|
|
72
|
+
patch = patch || act.gitPatch || act.patch || act.diff || act.changeSet?.gitPatch?.unidiffPatch;
|
|
73
73
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
74
|
+
const artifactList = [
|
|
75
|
+
...(Array.isArray(act.artifacts) ? act.artifacts : []),
|
|
76
|
+
...(Array.isArray(act.artifactsCreated?.artifacts) ? act.artifactsCreated.artifacts : []),
|
|
77
|
+
];
|
|
78
|
+
for (const art of artifactList) {
|
|
79
|
+
const artPatch =
|
|
80
|
+
art.patch ||
|
|
81
|
+
art.diff ||
|
|
82
|
+
art.changeSet?.gitPatch?.unidiffPatch ||
|
|
83
|
+
art.gitPatch?.unidiffPatch ||
|
|
84
|
+
(typeof art.content === "string" && art.content.startsWith("diff --git") ? art.content : "");
|
|
85
|
+
if (artPatch) {
|
|
86
|
+
patch = patch || artPatch;
|
|
87
|
+
}
|
|
88
|
+
if (art.path || art.filename) {
|
|
89
|
+
files.add(art.path || art.filename);
|
|
82
90
|
}
|
|
83
91
|
}
|
|
84
92
|
if (act.changedFiles && Array.isArray(act.changedFiles)) {
|
package/src/webhook.mjs
CHANGED
|
@@ -480,25 +480,27 @@ export function verifySignature(payload, signatureHeader, secret) {
|
|
|
480
480
|
if (!/^[0-9a-fA-F]{64}$/.test(hexSignature)) return false;
|
|
481
481
|
|
|
482
482
|
const hmac = createHmac("sha256", secret);
|
|
483
|
-
const
|
|
483
|
+
const encoder = new TextEncoder();
|
|
484
|
+
const bodyBuf = payload instanceof Uint8Array ? payload : encoder.encode(payload);
|
|
484
485
|
const digest = `sha256=${hmac.update(bodyBuf).digest("hex")}`;
|
|
485
486
|
|
|
486
|
-
const sigBuf =
|
|
487
|
-
const digestBuf =
|
|
487
|
+
const sigBuf = encoder.encode(signatureHeader);
|
|
488
|
+
const digestBuf = encoder.encode(digest);
|
|
488
489
|
|
|
489
|
-
if (sigBuf.
|
|
490
|
+
if (sigBuf.byteLength !== digestBuf.byteLength) return false;
|
|
490
491
|
return timingSafeEqual(sigBuf, digestBuf);
|
|
491
492
|
}
|
|
492
493
|
|
|
493
494
|
/**
|
|
494
495
|
* Parse incoming webhook body buffer into a JS object.
|
|
495
|
-
* @param {Buffer} bodyBuf
|
|
496
|
+
* @param {Buffer|Uint8Array|string} bodyBuf
|
|
496
497
|
* @param {string} contentType
|
|
497
498
|
* @returns {object} Parsed JSON payload
|
|
498
499
|
*/
|
|
499
500
|
export function parseWebhookPayload(bodyBuf, contentType = "application/json") {
|
|
500
|
-
if (!bodyBuf || bodyBuf.length === 0) return {};
|
|
501
|
-
const
|
|
501
|
+
if (!bodyBuf || (bodyBuf.length === 0 && bodyBuf.byteLength === 0)) return {};
|
|
502
|
+
const decoder = new TextDecoder();
|
|
503
|
+
const str = (bodyBuf instanceof Uint8Array ? decoder.decode(bodyBuf) : String(bodyBuf)).trim();
|
|
502
504
|
if (!str) return {};
|
|
503
505
|
|
|
504
506
|
if (contentType.includes("application/x-www-form-urlencoded")) {
|
package/src/wizard-init.mjs
CHANGED
|
@@ -123,7 +123,7 @@ export function planInit(root = process.cwd(), options = {}) {
|
|
|
123
123
|
const tierName = options.tier || FALLBACK_TIER;
|
|
124
124
|
// An unrecognised name resolves the same way loadConfig() resolves it, so the
|
|
125
125
|
// scaffolded limits always match what the runtime will later enforce.
|
|
126
|
-
const
|
|
126
|
+
const tierPresetLimits = TIER_PROFILES[tierName] || TIER_PROFILES[FALLBACK_TIER];
|
|
127
127
|
|
|
128
128
|
// Preserve existing config if present
|
|
129
129
|
let existingConfig = {};
|
|
@@ -134,6 +134,21 @@ export function planInit(root = process.cwd(), options = {}) {
|
|
|
134
134
|
} catch (_) {}
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
// Preserve existing jules.yml if present
|
|
138
|
+
let existingJules = {};
|
|
139
|
+
const existingJulesPath = join(root, ".agent", "jules.yml");
|
|
140
|
+
if (existsSync(existingJulesPath)) {
|
|
141
|
+
try {
|
|
142
|
+
existingJules = parseYaml(readFileSync(existingJulesPath, "utf-8")) || {};
|
|
143
|
+
} catch (_) {}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const customLimits = options.limits || existingConfig.limits;
|
|
147
|
+
const isCustomLimits = customLimits && Object.entries(customLimits).some(
|
|
148
|
+
([k, v]) => v !== undefined && tierPresetLimits[k] !== undefined && Number(v) !== Number(tierPresetLimits[k])
|
|
149
|
+
);
|
|
150
|
+
const limits = isCustomLimits ? { ...tierPresetLimits, ...customLimits } : tierPresetLimits;
|
|
151
|
+
|
|
137
152
|
const verify = {
|
|
138
153
|
test: options.testCmd || existingConfig.verify?.test || oracle.candidates.testCmd || "",
|
|
139
154
|
build: options.buildCmd || existingConfig.verify?.build || oracle.candidates.buildCmd || "",
|
|
@@ -143,19 +158,17 @@ export function planInit(root = process.cwd(), options = {}) {
|
|
|
143
158
|
|
|
144
159
|
const selectedPresets = options.presets || existingConfig.presets || ["nightly-security-audit", "flaky-test-quarantine"];
|
|
145
160
|
|
|
161
|
+
const limitsBlock = isCustomLimits
|
|
162
|
+
? `\nlimits:\n concurrency: ${limits.concurrency}\n daily_tasks: ${limits.daily_tasks}\n stagger_ms: ${limits.stagger_ms}\n diff_kb: ${limits.diff_kb}\n`
|
|
163
|
+
: "";
|
|
164
|
+
|
|
146
165
|
const configYaml = `# Google Jules Orchestrator Kit Config (v${KIT_VERSION})
|
|
147
166
|
version: 1
|
|
148
167
|
provider: ${existingConfig.provider || "jules"}
|
|
149
168
|
tier: ${tierName}
|
|
150
169
|
base_branch: ${options.baseBranch || existingConfig.base_branch || "main"}
|
|
151
170
|
branch_prefix: ${existingConfig.branch_prefix || "agent/"}
|
|
152
|
-
|
|
153
|
-
limits:
|
|
154
|
-
concurrency: ${limits.concurrency}
|
|
155
|
-
daily_tasks: ${limits.daily_tasks}
|
|
156
|
-
stagger_ms: ${limits.stagger_ms}
|
|
157
|
-
diff_kb: ${limits.diff_kb}
|
|
158
|
-
|
|
171
|
+
${limitsBlock}
|
|
159
172
|
verify:
|
|
160
173
|
test: "${verify.test}"
|
|
161
174
|
build: "${verify.build}"
|
|
@@ -166,10 +179,23 @@ presets:
|
|
|
166
179
|
${selectedPresets.map((p) => ` - ${p}`).join("\n")}
|
|
167
180
|
`;
|
|
168
181
|
|
|
169
|
-
const
|
|
170
|
-
|
|
182
|
+
const forbiddenPaths = options.forbiddenPaths || existingJules.forbidden_paths || [
|
|
183
|
+
".github/**",
|
|
184
|
+
"**/secrets/**",
|
|
185
|
+
"**/*.pem",
|
|
186
|
+
"**/lock-manager/**",
|
|
187
|
+
"scripts/jules-self-audit.mjs",
|
|
188
|
+
".agent/jules.yml",
|
|
189
|
+
];
|
|
190
|
+
const allowPaths = options.allowPaths || existingJules.allow_paths || [];
|
|
191
|
+
|
|
192
|
+
const julesYaml = `# Google Jules Repository Configuration (Version 2)
|
|
193
|
+
version: 2
|
|
171
194
|
test_cmd: "${verify.test}"
|
|
172
195
|
build_cmd: "${verify.build}"
|
|
196
|
+
forbidden_paths:
|
|
197
|
+
${forbiddenPaths.map((p) => ` - "${p}"`).join("\n")}
|
|
198
|
+
allow_paths: ${allowPaths.length > 0 ? "\n" + allowPaths.map((p) => ` - "${p}"`).join("\n") : "[]"}
|
|
173
199
|
`;
|
|
174
200
|
|
|
175
201
|
return {
|