jules-orchestrator-kit 0.6.0 → 0.6.1

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/.env.example CHANGED
@@ -20,6 +20,13 @@ JULES_REPO=owner/repo
20
20
  # Enable Dry-Run mode to simulate payload dispatch without making API calls
21
21
  # JULES_DRY_RUN=false
22
22
 
23
+ # --- Budget & Security Gatekeeper ---
24
+ # Daily max session limit for autonomous dispatches (Default: 300)
25
+ # JULES_DAILY_BUDGET=300
26
+
27
+ # Allow modifications to command-defining files (package.json, Cargo.toml, etc.) in PRs (Default: false)
28
+ # JULES_ALLOW_COMMAND_FILE_CHANGES=false
29
+
23
30
  # --- Execution Scope & Git ---
24
31
  # Base Branch for PR Audits & Merge-Base Calculations (Default: main)
25
32
  BASE_BRANCH=main
package/README.md CHANGED
@@ -162,6 +162,8 @@ All commands are registered in `package.json` and can be run via `npm run <comma
162
162
  | `JULES_REPO` | Target GitHub Repository (Format: `owner/repo`) |
163
163
  | `JULES_REPOLESS` | Set to `true` or `1` to run in repoless/serverless mode |
164
164
  | `JULES_DRY_RUN` | Set to `true` or `1` to simulate dispatching without making API calls |
165
+ | `JULES_DAILY_BUDGET` | Daily max session budget for autonomous dispatches (Default: `300`) |
166
+ | `JULES_ALLOW_COMMAND_FILE_CHANGES` | Set to `true` to allow PR changes to command-defining files like `package.json` (Default: `false`) |
165
167
  | `BASE_BRANCH` | Base branch for PR Audits & Merge-Base calculations (Default: `main`) |
166
168
  | `GITHUB_HEAD_REF` | PR Head Branch (Used dynamically by CI during OODA repair) |
167
169
  | `JULES_PROJECT_ROOT` | Root directory of the project (Auto-assigned during swarm executions) |
@@ -176,6 +178,10 @@ All commands are registered in `package.json` and can be run via `npm run <comma
176
178
  | `GITHUB_STEP_SUMMARY` | GitHub Actions Step Summary File Path |
177
179
  | `NO_COLOR` | Set to `true` to disable ANSI color output |
178
180
 
181
+ > [!NOTE]
182
+ > **Scope Enforcement via `allow_paths`**
183
+ > In `.agent/jules.yml`, defining `allow_paths` acts as a strict allowlist (deny-by-default). When non-empty, any file modified outside `allow_paths` will trigger a security violation (Exit Code 3). `forbidden_paths` always take absolute precedence and cannot be overridden.
184
+
179
185
  ### Exit Codes
180
186
  The Gatekeeper (`jules-self-audit.mjs` and related scripts) uses standard exit codes to signal status to CI systems.
181
187
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jules-orchestrator-kit",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Orchestration kit for running Google Jules autonomous agents.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -5,7 +5,7 @@ import crypto from "node:crypto";
5
5
  import os from "node:os";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { resolveWorkspaceExecutionBoundary } from "./command-resolver.mjs";
8
- import { log, logToHistory, redactSecrets } from "./utils.mjs";
8
+ import { log, logToHistory, redactSecrets, hasHighConfidenceSecret, hasLowConfidenceSecret } from "./utils.mjs";
9
9
 
10
10
  function runGitCommand(args, ignoreError = false) {
11
11
  try {
@@ -261,24 +261,37 @@ export function runSelfAudit() {
261
261
  }
262
262
  log.success("Restricted File Boundary Check: PASSED");
263
263
 
264
- log.info("Analyzing Diff Payload Size & Content...");
265
- const diffPayload = runGitCommand(["diff", `${mergeBase}...HEAD`], true);
266
- const diffBytes = Buffer.byteLength(diffPayload, "utf8");
264
+ log.info("Analyzing Code Diff Payload Size & Content...");
265
+ const codeDiffPayload = changedCodeFiles.length > 0
266
+ ? runGitCommand(["diff", `${mergeBase}...HEAD`, "--", ...changedCodeFiles], true)
267
+ : runGitCommand(["diff", `${mergeBase}...HEAD`], true);
268
+
269
+ const diffBytes = Buffer.byteLength(codeDiffPayload, "utf8");
267
270
  const maxDiffBytes = 75 * 1024; // 75 KB
268
271
 
269
272
  if (diffBytes > maxDiffBytes) {
270
- const err = new Error(`DIFF PAYLOAD TOO LARGE: ${diffBytes} bytes exceeds ${maxDiffBytes} bytes. Split the task.`);
273
+ const err = new Error(`DIFF PAYLOAD TOO LARGE: ${diffBytes} bytes exceeds ${maxDiffBytes} bytes limit for code files. Split the task.`);
271
274
  err.code = 5;
272
275
  throw err;
273
276
  }
274
277
 
275
- const redactedDiff = redactSecrets(diffPayload);
276
- if (redactedDiff !== diffPayload) {
277
- const err = new Error("SECRET LEAK PREVENTED: Secret-like content detected in diff. Aborting.");
278
+ const fullDiffPayload = runGitCommand(["diff", `${mergeBase}...HEAD`], true);
279
+ const addedLines = fullDiffPayload
280
+ .split("\n")
281
+ .filter((line) => line.startsWith("+") && !line.startsWith("+++"))
282
+ .map((line) => line.slice(1))
283
+ .join("\n");
284
+
285
+ if (hasHighConfidenceSecret(addedLines)) {
286
+ const err = new Error("SECRET LEAK PREVENTED: High-confidence secret token or private key detected in added diff lines. Aborting.");
278
287
  err.code = 6;
279
288
  throw err;
280
289
  }
281
- log.success(`Diff Inspection Check: PASSED (${diffBytes} bytes, No Secrets Detected)`);
290
+
291
+ if (hasLowConfidenceSecret(addedLines)) {
292
+ log.warn("SECURITY WARNING: Low-confidence secret or test key pattern detected in added diff lines.");
293
+ }
294
+ log.success(`Diff Inspection Check: PASSED (${diffBytes} bytes code diff, No Active High-Confidence Secrets)`);
282
295
 
283
296
  log.info("Resolving Dynamic Verification Suite from Trusted Base Branch...");
284
297
 
@@ -382,7 +395,7 @@ export function runSelfAudit() {
382
395
  }
383
396
  }
384
397
  } catch (e) {
385
- // Ignorera fel här
398
+ // Ignore errors here
386
399
  }
387
400
 
388
401
  if (oodaRetries < 3) {
@@ -415,6 +428,13 @@ export function runSelfAudit() {
415
428
  throw err;
416
429
  }
417
430
 
431
+ try {
432
+ const oodaStateFile = path.resolve(process.cwd(), ".agent/state/ooda.json");
433
+ if (fs.existsSync(oodaStateFile)) {
434
+ fs.rmSync(oodaStateFile, { force: true });
435
+ }
436
+ } catch (_) {}
437
+
418
438
  log.success("🎉 JULES PR SELF-AUDIT PASSED SUCCESSFULLY!");
419
439
  }
420
440
 
@@ -486,7 +506,7 @@ export function runPreflightSandbox() {
486
506
  fs.rmSync(tempArchive, { force: true });
487
507
  }
488
508
  } catch (e) {
489
- // Ignorera fel vid cleanup
509
+ // Ignore cleanup errors
490
510
  }
491
511
  }
492
512
  }
package/scripts/utils.mjs CHANGED
@@ -106,6 +106,42 @@ export function calculateShannonEntropy(str) {
106
106
  }, 0);
107
107
  }
108
108
 
109
+ export const HIGH_CONFIDENCE_PATTERNS = [
110
+ /\bgh[pousr]_[a-zA-Z0-9]{36,}\b/g,
111
+ /\bgithub_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}\b/g,
112
+ /\bAKIA[0-9A-Z]{16}\b/g,
113
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
114
+ /\bnpm_[a-zA-Z0-9]{36}\b/g,
115
+ /\bsk_live_[0-9a-zA-Z]{24,}\b/g,
116
+ ];
117
+
118
+ export const LOW_CONFIDENCE_PATTERNS = [
119
+ /\bAIza[0-9A-Za-z_-]{30,40}\b/g,
120
+ /\bya29\.[a-zA-Z0-9_\-]{20,}\b/g,
121
+ /\bBearer\s+[a-zA-Z0-9\-\._~+\/]+=*/g,
122
+ /\bsk-(?:ant-api03-|proj-|svcacct-)[a-zA-Z0-9\-\_]{32,}\b/g,
123
+ /\bxox[baprs]-[a-zA-Z0-9\-]{10,}\b/g,
124
+ /\beyJ[a-zA-Z0-9_\-]{10,}\.[a-zA-Z0-9_\-]{10,}\.[a-zA-Z0-9_\-]{10,}\b/g,
125
+ /\b(?:rk|pk)_(?:live|test)_[0-9a-zA-Z]{24,}\b/g,
126
+ /\bsk_test_[0-9a-zA-Z]{24,}\b/g,
127
+ ];
128
+
129
+ export function hasHighConfidenceSecret(text) {
130
+ if (!text) return false;
131
+ return HIGH_CONFIDENCE_PATTERNS.some((pat) => {
132
+ pat.lastIndex = 0;
133
+ return pat.test(text);
134
+ });
135
+ }
136
+
137
+ export function hasLowConfidenceSecret(text) {
138
+ if (!text) return false;
139
+ return LOW_CONFIDENCE_PATTERNS.some((pat) => {
140
+ pat.lastIndex = 0;
141
+ return pat.test(text);
142
+ });
143
+ }
144
+
109
145
  export function redactSecrets(text) {
110
146
  if (!text) return "";
111
147
  let sanitized = text;
@@ -122,21 +158,8 @@ export function redactSecrets(text) {
122
158
  }
123
159
  }
124
160
 
125
- const patterns = [
126
- /\bgh[pousr]_[a-zA-Z0-9]{36,}\b/g,
127
- /\bgithub_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}\b/g,
128
- /\bAKIA[0-9A-Z]{16}\b/g,
129
- /\bAIza[0-9A-Za-z_-]{30,40}\b/g,
130
- /\bya29\.[a-zA-Z0-9_\-]{20,}\b/g,
131
- /\bBearer\s+[a-zA-Z0-9\-\._~+\/]+=*/g,
132
- /\bsk-(?:ant-api03-|proj-|svcacct-)[a-zA-Z0-9\-\_]{32,}\b/g,
133
- /\bxox[baprs]-[a-zA-Z0-9\-]{10,}\b/g,
134
- /\beyJ[a-zA-Z0-9_\-]{10,}\.[a-zA-Z0-9_\-]{10,}\.[a-zA-Z0-9_\-]{10,}\b/g,
135
- /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
136
- /\bnpm_[a-zA-Z0-9]{36}\b/g,
137
- /\b(?:sk|rk|pk)_(?:live|test)_[0-9a-zA-Z]{24,}\b/g,
138
- ];
139
- for (const pat of patterns) {
161
+ const allPatterns = [...HIGH_CONFIDENCE_PATTERNS, ...LOW_CONFIDENCE_PATTERNS];
162
+ for (const pat of allPatterns) {
140
163
  sanitized = sanitized.replace(pat, "[REDACTED_BY_SECURITY_GATE]");
141
164
  }
142
165
  return sanitized;
@@ -173,7 +196,7 @@ export function checkDailyBudget(maxSessions = 300) {
173
196
  if (
174
197
  entry.timestamp &&
175
198
  entry.timestamp.startsWith(dateStr) &&
176
- (entry.event === "session_dispatched" || entry.event === "budget_reserved" || !entry.event)
199
+ (entry.event === "budget_reserved" || !entry.event)
177
200
  ) {
178
201
  usedToday++;
179
202
  }
@@ -185,24 +208,43 @@ export function checkDailyBudget(maxSessions = 300) {
185
208
  return { ok: usedToday < maxSessions, used: usedToday, budget: maxSessions };
186
209
  }
187
210
 
211
+ function sleepSync(ms) {
212
+ try {
213
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
214
+ } catch (_) {}
215
+ }
216
+
188
217
  export function reserveDailyBudget(maxSessions = 300, taskKey = "") {
189
218
  const stateDir = path.resolve(process.env.JULES_PROJECT_ROOT || process.cwd(), ".agent/state");
190
219
  ensureDir(stateDir);
191
220
 
192
221
  const lockFile = path.join(stateDir, "budget.lock");
193
222
  let fd;
223
+ const maxLockAttempts = 10;
194
224
 
195
- try {
196
- fd = fs.openSync(lockFile, "wx");
197
- } catch {
198
- // If lock fails, wait 50ms and retry once before returning locked state
225
+ for (let attempt = 0; attempt < maxLockAttempts; attempt++) {
199
226
  try {
200
- fd = fs.openSync(lockFile, "w");
201
- } catch {
202
- return { ok: false, reason: "locked", used: null, budget: maxSessions };
227
+ try {
228
+ const stat = fs.statSync(lockFile);
229
+ if (Date.now() - stat.mtimeMs > 30000) {
230
+ fs.rmSync(lockFile, { force: true });
231
+ }
232
+ } catch (_) {}
233
+
234
+ fd = fs.openSync(lockFile, "wx");
235
+ break;
236
+ } catch (err) {
237
+ if (err.code !== "EEXIST") throw err;
238
+ if (attempt < maxLockAttempts - 1) {
239
+ sleepSync(Math.floor(50 + Math.random() * 50));
240
+ }
203
241
  }
204
242
  }
205
243
 
244
+ if (fd === undefined) {
245
+ return { ok: false, reason: "locked", used: null, budget: maxSessions };
246
+ }
247
+
206
248
  try {
207
249
  const check = checkDailyBudget(maxSessions);
208
250
  if (!check.ok) {