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 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 **815 unit tests across 91 suites passing in < 10.0s**.
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
@@ -14,6 +14,7 @@ export {
14
14
  checkScope,
15
15
  scanDiff,
16
16
  hasEncodedSecret,
17
+ hasHighEntropyToken,
17
18
  checkEdgeRuntimeImports,
18
19
  checkCrossPackageImports,
19
20
  checkTestTampering,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jules-orchestrator-kit",
3
- "version": "0.52.4",
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
- const limitBytes = (config.limits.diffKb || 75) * 1024;
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 currentLineNo = null;
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 : null;
906
- currentLineNo = null;
994
+ currentFile = target && target !== "/dev/null" ? target : lastOldFile;
995
+ currentOldLineNo = null;
996
+ currentNewLineNo = null;
907
997
  continue;
908
998
  }
909
999
 
910
- const hunkMatch = /^@@ -\d+(?:,\d+)? \+(\d+)/.exec(line);
1000
+ const hunkMatch = /^@@ -(\d+)(?:,\d+)? \+(\d+)/.exec(line);
911
1001
  if (hunkMatch) {
912
- currentLineNo = Number(hunkMatch[1]);
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 (line.startsWith("+") && !line.startsWith("+++")) {
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: currentLineNo,
1031
+ line: currentNewLineNo,
929
1032
  type: "TEST_SKIP_INJECTION",
930
- reason: `Test Tamper Guard: ${skipRule.desc} found in ${currentFile}${currentLineNo ? `:${currentLineNo}` : ""}`,
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: currentLineNo,
1044
+ line: currentNewLineNo,
941
1045
  type: "VACUOUS_ASSERTION",
942
- reason: `Test Tamper Guard: ${vacuousRule.desc} detected in ${currentFile}${currentLineNo ? `:${currentLineNo}` : ""}`,
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: currentLineNo,
1057
+ line: currentNewLineNo,
952
1058
  type: "COMMENTED_ASSERTION",
953
- reason: `Test Tamper Guard: Commented-out test assertion detected in ${currentFile}${currentLineNo ? `:${currentLineNo}` : ""}`,
1059
+ reason: `Test Tamper Guard: Commented-out test assertion detected in ${currentFile}${currentNewLineNo ? `:${currentNewLineNo}` : ""}`,
954
1060
  });
955
1061
  }
956
1062
 
957
- if (currentLineNo !== null) currentLineNo++;
958
- } else if (currentLineNo !== null && !line.startsWith("-") && !line.startsWith("\\")) {
959
- currentLineNo++;
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,