jules-orchestrator-kit 0.52.4 → 0.52.5
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/index.mjs +1 -0
- package/package.json +1 -1
- package/src/config.mjs +2 -2
- package/src/engine.mjs +9 -1
- package/src/security.mjs +147 -21
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 **828 unit tests across 91 suites passing in < 10.0s**.
|
|
147
147
|
|
|
148
148
|
<br/>
|
|
149
149
|
|
package/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jules-orchestrator-kit",
|
|
3
|
-
"version": "0.52.
|
|
3
|
+
"version": "0.52.5",
|
|
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/config.mjs
CHANGED
|
@@ -35,13 +35,13 @@ export const BUILTIN_DENY = [
|
|
|
35
35
|
"**/*.pem",
|
|
36
36
|
"**/*.key",
|
|
37
37
|
"**/id_rsa*",
|
|
38
|
-
".agent/config.yml",
|
|
39
|
-
".agent/jules.yml",
|
|
40
38
|
".agent/jules-queue/**",
|
|
41
39
|
".github/**",
|
|
42
40
|
];
|
|
43
41
|
|
|
44
42
|
export const BUILTIN_PROTECT = [
|
|
43
|
+
".agent/config.yml",
|
|
44
|
+
".agent/jules.yml",
|
|
45
45
|
".agent/rules/**",
|
|
46
46
|
".agent/SYSTEM_LEARNINGS.md",
|
|
47
47
|
".agent/knowledge/**",
|
package/src/engine.mjs
CHANGED
|
@@ -173,6 +173,7 @@ export async function gate(opts = {}) {
|
|
|
173
173
|
// Phase 1: Scope Guard (Fetch trusted config strictly from origin/base using single normalizeScope)
|
|
174
174
|
let trustedScope = config.scope;
|
|
175
175
|
let trustedVerify = config.verify;
|
|
176
|
+
let trustedDiffKb = 75;
|
|
176
177
|
|
|
177
178
|
const trustedConfigRaw = showFromOrigin(root, base, ".agent/config.yml") || showFromOrigin(root, base, ".agent/jules.yml");
|
|
178
179
|
if (trustedConfigRaw) {
|
|
@@ -180,6 +181,9 @@ export async function gate(opts = {}) {
|
|
|
180
181
|
const parsed = parseYaml(trustedConfigRaw);
|
|
181
182
|
// CRITICAL B1 FIX: normalizeScope ensures BUILTIN_DENY is ALWAYS merged with user deny rules
|
|
182
183
|
trustedScope = normalizeScope(parsed);
|
|
184
|
+
if (parsed.limits?.diff_kb || parsed.limits?.diffKb) {
|
|
185
|
+
trustedDiffKb = Number(parsed.limits.diff_kb || parsed.limits.diffKb) || 75;
|
|
186
|
+
}
|
|
183
187
|
if (parsed.verify || parsed.test_cmd || parsed.build_cmd) {
|
|
184
188
|
trustedVerify = {
|
|
185
189
|
setup: parsed.verify?.setup || config.verify.setup,
|
|
@@ -200,6 +204,9 @@ export async function gate(opts = {}) {
|
|
|
200
204
|
} else {
|
|
201
205
|
// CRITICAL H-c FIX: Fall back strictly to normalizeScope({}) (built-ins only), never HEAD config
|
|
202
206
|
trustedScope = normalizeScope({});
|
|
207
|
+
if (opts.config?.limits?.diffKb || opts.config?.limits?.diff_kb) {
|
|
208
|
+
trustedDiffKb = Number(opts.config.limits.diffKb || opts.config.limits.diff_kb) || 75;
|
|
209
|
+
}
|
|
203
210
|
}
|
|
204
211
|
|
|
205
212
|
const scopeResult = checkScope(files, trustedScope, {
|
|
@@ -218,7 +225,8 @@ export async function gate(opts = {}) {
|
|
|
218
225
|
}
|
|
219
226
|
|
|
220
227
|
// Phase 2: Diff Payload Governor
|
|
221
|
-
|
|
228
|
+
// Bound to trusted base commit config strictly; uncommitted disk config cannot raise limit in committed mode.
|
|
229
|
+
const limitBytes = trustedDiffKb * 1024;
|
|
222
230
|
const payloadOk = bytes <= limitBytes;
|
|
223
231
|
phases.push({ phase: "payload", ok: payloadOk, bytes, limitBytes });
|
|
224
232
|
appendTelemetry(root, "gate_phase", { phase: "payload", ok: payloadOk });
|
package/src/security.mjs
CHANGED
|
@@ -795,14 +795,85 @@ function splitDiffByFile(diffText) {
|
|
|
795
795
|
return [...byFile.values()].filter((s) => s.lines.length > 0);
|
|
796
796
|
}
|
|
797
797
|
|
|
798
|
+
const CANDIDATE_TOKEN_REGEX = /[A-Za-z0-9_-]{24,}/g;
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Checks for high-entropy continuous tokens (>= 24 chars, entropy > 4.5) on added lines.
|
|
802
|
+
* Filters out URLs, SRI hashes (sha512-, sha256-, sha384-), and lockfiles to eliminate false positives.
|
|
803
|
+
*
|
|
804
|
+
* @param {string} text - Text to scan
|
|
805
|
+
* @param {string|null} [file=null] - File path associated with the text
|
|
806
|
+
* @returns {boolean}
|
|
807
|
+
*/
|
|
808
|
+
export function hasHighEntropyToken(text = "", file = null) {
|
|
809
|
+
if (!text || typeof text !== "string") return false;
|
|
810
|
+
if (
|
|
811
|
+
file &&
|
|
812
|
+
(file.endsWith(".lock") ||
|
|
813
|
+
file.endsWith(".lockb") ||
|
|
814
|
+
file.includes("package-lock.json") ||
|
|
815
|
+
file.includes("pnpm-lock.yaml") ||
|
|
816
|
+
file.includes("yarn.lock") ||
|
|
817
|
+
file.includes("Cargo.lock") ||
|
|
818
|
+
file.includes("composer.lock"))
|
|
819
|
+
) {
|
|
820
|
+
return false;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
const lines = text.split("\n");
|
|
824
|
+
for (const rawLine of lines) {
|
|
825
|
+
if (
|
|
826
|
+
rawLine.includes("://") ||
|
|
827
|
+
rawLine.includes("data:image/") ||
|
|
828
|
+
rawLine.includes("sha512-") ||
|
|
829
|
+
rawLine.includes("sha256-") ||
|
|
830
|
+
rawLine.includes("sha384-")
|
|
831
|
+
) {
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
CANDIDATE_TOKEN_REGEX.lastIndex = 0;
|
|
836
|
+
let match;
|
|
837
|
+
while ((match = CANDIDATE_TOKEN_REGEX.exec(rawLine)) !== null) {
|
|
838
|
+
const token = match[0];
|
|
839
|
+
if (token.startsWith("sha512-") || token.startsWith("sha256-")) continue;
|
|
840
|
+
if (token.length >= 24) {
|
|
841
|
+
// If the token is a formatted base64 blob, distinguish binary assets and plain prose
|
|
842
|
+
if (token.length % 4 === 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(token)) {
|
|
843
|
+
try {
|
|
844
|
+
const plain = Buffer.from(token, "base64").toString("utf-8");
|
|
845
|
+
const pr = printableRatio(plain);
|
|
846
|
+
if (pr < 0.9) {
|
|
847
|
+
// Ordinary binary asset (e.g. icon/font/wasm) - do not trip on binary entropy
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
// If it decodes to text, check decoded plain text entropy
|
|
851
|
+
if (shannonEntropy(plain) > 4.5) {
|
|
852
|
+
return true;
|
|
853
|
+
}
|
|
854
|
+
continue;
|
|
855
|
+
} catch (_) {}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const ent = shannonEntropy(token);
|
|
859
|
+
if (ent > 4.5) {
|
|
860
|
+
return true;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return false;
|
|
866
|
+
}
|
|
867
|
+
|
|
798
868
|
/**
|
|
799
869
|
* Classify a block of added lines. Returns the single most severe finding, or
|
|
800
870
|
* null when the block is clean.
|
|
801
871
|
*
|
|
802
872
|
* @param {string} addedLines
|
|
873
|
+
* @param {string|null} [file=null]
|
|
803
874
|
* @returns {{ severity: string, type: string, description: string, encoded: boolean }|null}
|
|
804
875
|
*/
|
|
805
|
-
function classifyAddedLines(addedLines) {
|
|
876
|
+
function classifyAddedLines(addedLines, file = null) {
|
|
806
877
|
const { all: variants, normalized, base64Normalized } = secretScanVariants(addedLines);
|
|
807
878
|
if (variants.some((v) => hasHighConfidenceSecret(v))) {
|
|
808
879
|
return { severity: "CRITICAL", type: "HIGH_CONFIDENCE_SECRET", encoded: false, description: "High-confidence secret pattern detected in added diff lines" };
|
|
@@ -820,6 +891,9 @@ function classifyAddedLines(addedLines) {
|
|
|
820
891
|
if (variants.some((v) => hasLowConfidenceSecret(v))) {
|
|
821
892
|
return { severity: "HIGH", type: "LOW_CONFIDENCE_SECRET", encoded: false, description: "Low-confidence secret or authorization token detected in added diff lines" };
|
|
822
893
|
}
|
|
894
|
+
if (hasHighEntropyToken(addedLines, file)) {
|
|
895
|
+
return { severity: "HIGH", type: "HIGH_ENTROPY_TOKEN", encoded: false, description: "High-entropy token detected in added diff lines (potential unstructured secret or API key)" };
|
|
896
|
+
}
|
|
823
897
|
return null;
|
|
824
898
|
}
|
|
825
899
|
|
|
@@ -834,12 +908,13 @@ function classifyAddedLines(addedLines) {
|
|
|
834
908
|
*
|
|
835
909
|
* @param {Array<{ text: string, no: number|null }>} lines
|
|
836
910
|
* @param {string} type
|
|
911
|
+
* @param {string|null} [file=null]
|
|
837
912
|
* @returns {number|null}
|
|
838
913
|
*/
|
|
839
|
-
function locateFindingLine(lines, type) {
|
|
914
|
+
function locateFindingLine(lines, type, file = null) {
|
|
840
915
|
for (const line of lines) {
|
|
841
916
|
if (line.no === null) continue;
|
|
842
|
-
const hit = classifyAddedLines(line.text);
|
|
917
|
+
const hit = classifyAddedLines(line.text, file);
|
|
843
918
|
if (hit && hit.type === type) return line.no;
|
|
844
919
|
}
|
|
845
920
|
return null;
|
|
@@ -859,8 +934,10 @@ export function checkTestTampering(diffOrText = "", options = {}) {
|
|
|
859
934
|
|
|
860
935
|
const violations = [];
|
|
861
936
|
const lines = diffOrText.split("\n");
|
|
937
|
+
let lastOldFile = null;
|
|
862
938
|
let currentFile = null;
|
|
863
|
-
let
|
|
939
|
+
let currentOldLineNo = null;
|
|
940
|
+
let currentNewLineNo = null;
|
|
864
941
|
|
|
865
942
|
const isTestFile = (f) => {
|
|
866
943
|
if (!f) return false;
|
|
@@ -898,18 +975,32 @@ export function checkTestTampering(diffOrText = "", options = {}) {
|
|
|
898
975
|
{ pattern: /\bassert\.(?:isFalse|isNotOk)\s*\(\s*false\s*(?:,[^)]*)?\)/i, desc: "Vacuous falsity assertion (assert.isFalse(false))" },
|
|
899
976
|
];
|
|
900
977
|
|
|
978
|
+
const ASSERTION_PATTERN = /(?:\b(?:assert(?:\.[a-zA-Z0-9_$]+)?|expect|t\.(?:assert|expect|is|equal|true|false|Errorf|Fatalf)|require\.[a-zA-Z0-9_$]+)\b|assert!|assert_eq!|assert_ne!)/i;
|
|
979
|
+
const isCommentLine = (str) => /^\s*(?:\/\/|\/\*|\*|#|--|;)/.test(str);
|
|
980
|
+
|
|
981
|
+
const fileAssertions = new Map();
|
|
982
|
+
|
|
901
983
|
for (let i = 0; i < lines.length; i++) {
|
|
902
984
|
const line = lines[i];
|
|
985
|
+
|
|
986
|
+
if ((line.startsWith("--- ") || line.startsWith("--- a/")) && !line.startsWith("----")) {
|
|
987
|
+
const orig = line.slice(3).split("\t")[0].trim().replace(/^a\//, "");
|
|
988
|
+
lastOldFile = orig && orig !== "/dev/null" ? orig : null;
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
|
|
903
992
|
if ((line.startsWith("+++ ") || line.startsWith("+++ b/") || line.startsWith("+++ /dev/null")) && !line.startsWith("++++")) {
|
|
904
993
|
const target = line.slice(3).split("\t")[0].trim().replace(/^b\//, "");
|
|
905
|
-
currentFile = target && target !== "/dev/null" ? target :
|
|
906
|
-
|
|
994
|
+
currentFile = target && target !== "/dev/null" ? target : lastOldFile;
|
|
995
|
+
currentOldLineNo = null;
|
|
996
|
+
currentNewLineNo = null;
|
|
907
997
|
continue;
|
|
908
998
|
}
|
|
909
999
|
|
|
910
|
-
const hunkMatch = /^@@
|
|
1000
|
+
const hunkMatch = /^@@ -(\d+)(?:,\d+)? \+(\d+)/.exec(line);
|
|
911
1001
|
if (hunkMatch) {
|
|
912
|
-
|
|
1002
|
+
currentOldLineNo = Number(hunkMatch[1]);
|
|
1003
|
+
currentNewLineNo = Number(hunkMatch[2]);
|
|
913
1004
|
continue;
|
|
914
1005
|
}
|
|
915
1006
|
|
|
@@ -917,17 +1008,29 @@ export function checkTestTampering(diffOrText = "", options = {}) {
|
|
|
917
1008
|
continue;
|
|
918
1009
|
}
|
|
919
1010
|
|
|
920
|
-
if (
|
|
1011
|
+
if (!fileAssertions.has(currentFile)) {
|
|
1012
|
+
fileAssertions.set(currentFile, { removed: [], added: 0 });
|
|
1013
|
+
}
|
|
1014
|
+
const fileStats = fileAssertions.get(currentFile);
|
|
1015
|
+
|
|
1016
|
+
if (line.startsWith("-") && !line.startsWith("---")) {
|
|
1017
|
+
const deletedText = line.slice(1);
|
|
1018
|
+
if (!isCommentLine(deletedText) && ASSERTION_PATTERN.test(deletedText)) {
|
|
1019
|
+
fileStats.removed.push({ line: currentOldLineNo, text: deletedText });
|
|
1020
|
+
}
|
|
1021
|
+
if (currentOldLineNo !== null) currentOldLineNo++;
|
|
1022
|
+
} else if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
921
1023
|
const addedText = line.slice(1);
|
|
1024
|
+
let isVacuous = false;
|
|
922
1025
|
|
|
923
1026
|
// Check skip injections
|
|
924
1027
|
for (const skipRule of SKIP_INJECTIONS) {
|
|
925
1028
|
if (skipRule.pattern.test(addedText)) {
|
|
926
1029
|
violations.push({
|
|
927
1030
|
file: currentFile,
|
|
928
|
-
line:
|
|
1031
|
+
line: currentNewLineNo,
|
|
929
1032
|
type: "TEST_SKIP_INJECTION",
|
|
930
|
-
reason: `Test Tamper Guard: ${skipRule.desc} found in ${currentFile}${
|
|
1033
|
+
reason: `Test Tamper Guard: ${skipRule.desc} found in ${currentFile}${currentNewLineNo ? `:${currentNewLineNo}` : ""}`,
|
|
931
1034
|
});
|
|
932
1035
|
}
|
|
933
1036
|
}
|
|
@@ -935,28 +1038,51 @@ export function checkTestTampering(diffOrText = "", options = {}) {
|
|
|
935
1038
|
// Check vacuous assertions
|
|
936
1039
|
for (const vacuousRule of VACUOUS_ASSERTIONS) {
|
|
937
1040
|
if (vacuousRule.pattern.test(addedText)) {
|
|
1041
|
+
isVacuous = true;
|
|
938
1042
|
violations.push({
|
|
939
1043
|
file: currentFile,
|
|
940
|
-
line:
|
|
1044
|
+
line: currentNewLineNo,
|
|
941
1045
|
type: "VACUOUS_ASSERTION",
|
|
942
|
-
reason: `Test Tamper Guard: ${vacuousRule.desc} detected in ${currentFile}${
|
|
1046
|
+
reason: `Test Tamper Guard: ${vacuousRule.desc} detected in ${currentFile}${currentNewLineNo ? `:${currentNewLineNo}` : ""}`,
|
|
943
1047
|
});
|
|
944
1048
|
}
|
|
945
1049
|
}
|
|
946
1050
|
|
|
947
1051
|
// Check commented-out assertions
|
|
1052
|
+
let isCommented = false;
|
|
948
1053
|
if (COMMENTED_ASSERTION.test(line)) {
|
|
1054
|
+
isCommented = true;
|
|
949
1055
|
violations.push({
|
|
950
1056
|
file: currentFile,
|
|
951
|
-
line:
|
|
1057
|
+
line: currentNewLineNo,
|
|
952
1058
|
type: "COMMENTED_ASSERTION",
|
|
953
|
-
reason: `Test Tamper Guard: Commented-out test assertion detected in ${currentFile}${
|
|
1059
|
+
reason: `Test Tamper Guard: Commented-out test assertion detected in ${currentFile}${currentNewLineNo ? `:${currentNewLineNo}` : ""}`,
|
|
954
1060
|
});
|
|
955
1061
|
}
|
|
956
1062
|
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1063
|
+
// If valid non-vacuous, non-commented assertion is added, increment added count
|
|
1064
|
+
if (!isVacuous && !isCommented && !isCommentLine(addedText) && ASSERTION_PATTERN.test(addedText)) {
|
|
1065
|
+
fileStats.added++;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
if (currentNewLineNo !== null) currentNewLineNo++;
|
|
1069
|
+
} else if (!line.startsWith("\\")) {
|
|
1070
|
+
if (currentOldLineNo !== null) currentOldLineNo++;
|
|
1071
|
+
if (currentNewLineNo !== null) currentNewLineNo++;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
for (const [file, stats] of fileAssertions.entries()) {
|
|
1076
|
+
if (stats.removed.length > stats.added) {
|
|
1077
|
+
const unreplaced = stats.removed.slice(stats.added);
|
|
1078
|
+
for (const item of unreplaced) {
|
|
1079
|
+
violations.push({
|
|
1080
|
+
file,
|
|
1081
|
+
line: item.line,
|
|
1082
|
+
type: "ASSERTION_REMOVAL",
|
|
1083
|
+
reason: `Test Tamper Guard: Assertion removed without replacement in ${file}${item.line ? `:${item.line}` : ""}: "${item.text.trim()}"`,
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
960
1086
|
}
|
|
961
1087
|
}
|
|
962
1088
|
|
|
@@ -973,9 +1099,9 @@ export function scanDiff(diffTextStr = "", options = {}) {
|
|
|
973
1099
|
const findings = [];
|
|
974
1100
|
|
|
975
1101
|
for (const segment of segments) {
|
|
976
|
-
const hit = classifyAddedLines(segment.lines.map((l) => l.text).join("\n"));
|
|
1102
|
+
const hit = classifyAddedLines(segment.lines.map((l) => l.text).join("\n"), segment.file);
|
|
977
1103
|
if (!hit) continue;
|
|
978
|
-
const line = segment.file ? locateFindingLine(segment.lines, hit.type) : null;
|
|
1104
|
+
const line = segment.file ? locateFindingLine(segment.lines, hit.type, segment.file) : null;
|
|
979
1105
|
const at = segment.file ? ` (${segment.file}${line ? `:${line}` : ""})` : "";
|
|
980
1106
|
findings.push({
|
|
981
1107
|
severity: hit.severity,
|
|
@@ -991,7 +1117,7 @@ export function scanDiff(diffTextStr = "", options = {}) {
|
|
|
991
1117
|
// detection for attribution, fall back to the joined text when every file
|
|
992
1118
|
// came back clean — the cost lands only on diffs with nothing to report.
|
|
993
1119
|
if (findings.length === 0 && segments.length > 1) {
|
|
994
|
-
const hit = classifyAddedLines(segments.flatMap((s) => s.lines.map((l) => l.text)).join("\n"));
|
|
1120
|
+
const hit = classifyAddedLines(segments.flatMap((s) => s.lines.map((l) => l.text)).join("\n"), null);
|
|
995
1121
|
if (hit) {
|
|
996
1122
|
findings.push({
|
|
997
1123
|
severity: hit.severity,
|