jules-orchestrator-kit 0.8.5 → 0.9.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/.agent/jules-queue/README.md +4 -4
- package/.agent/prompts/Bolt.md +18 -0
- package/.agent/prompts/Overseer.md +18 -0
- package/.agent/prompts/Sentinel.md +18 -0
- package/.agent/prompts/Task_Template.md +26 -0
- package/JULES_RULES_TEMPLATE.md +6 -1
- package/README.md +54 -31
- package/bin/agentctl.mjs +281 -0
- package/bin/mcp-server.mjs +5 -0
- package/index.mjs +32 -5
- package/package.json +13 -2
- package/scripts/asset-integrity-check.mjs +35 -0
- package/scripts/command-resolver.mjs +37 -214
- package/scripts/jules-cleanup.mjs +23 -164
- package/scripts/jules-create.mjs +12 -56
- package/scripts/jules-dispatch.mjs +103 -449
- package/scripts/jules-merge-swarm.mjs +19 -173
- package/scripts/jules-nightly.mjs +7 -128
- package/scripts/jules-patch.mjs +15 -103
- package/scripts/jules-queue-runner.mjs +28 -221
- package/scripts/jules-scan-todos.mjs +31 -125
- package/scripts/jules-self-audit.mjs +69 -656
- package/scripts/jules-status.mjs +42 -70
- package/scripts/jules-swarm.mjs +23 -327
- package/scripts/jules-webhook-receiver.mjs +70 -0
- package/scripts/lock-manager.mjs +11 -177
- package/scripts/risk-tier.mjs +29 -0
- package/scripts/rules-lint.mjs +20 -0
- package/scripts/stale-base-check.mjs +26 -0
- package/scripts/utils.mjs +107 -369
- package/scripts/validate-envelope.mjs +44 -0
- package/src/asset_integrity.mjs +76 -0
- package/src/config.mjs +325 -0
- package/src/engine.mjs +217 -0
- package/src/envelope.mjs +105 -0
- package/src/git.mjs +127 -0
- package/src/mcp.mjs +211 -0
- package/src/provider.mjs +179 -0
- package/src/risk.mjs +120 -0
- package/src/rules_budget.mjs +77 -0
- package/src/security.mjs +212 -0
- package/src/state.mjs +217 -0
- package/src/webhook.mjs +138 -0
|
@@ -1,692 +1,105 @@
|
|
|
1
|
-
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import crypto from "node:crypto";
|
|
5
|
-
import os from "node:os";
|
|
6
|
-
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { resolveWorkspaceExecutionBoundary } from "./command-resolver.mjs";
|
|
8
|
-
import { log, hasHighConfidenceSecret, hasLowConfidenceSecret, redactSecrets, anonymizePii } from "./utils.mjs";
|
|
1
|
+
#!/usr/bin/env node
|
|
9
2
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (ignoreError) return "";
|
|
19
|
-
const err = new Error(`Git command failed: git ${args.join(" ")}\n${error.message}`);
|
|
20
|
-
err.code = 1;
|
|
21
|
-
throw err;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
3
|
+
/**
|
|
4
|
+
* Backward compatibility shim for jules-self-audit.mjs in v0.9.0.
|
|
5
|
+
* Delegates execution to src/engine.mjs gate().
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { gate } from "../src/engine.mjs";
|
|
9
|
+
import { loadConfig, parseYaml } from "../src/config.mjs";
|
|
10
|
+
import { matchesGlob } from "../src/security.mjs";
|
|
24
11
|
|
|
25
12
|
export function matchGlob(filepath, globPattern) {
|
|
26
|
-
|
|
27
|
-
// Normalize consecutive glob wildcards (e.g., "**/**" -> "**")
|
|
28
|
-
const normalizedGlob = globPattern.replace(/\\/g, "/").replace(/(?:\*\*\/)+/g, "**/");
|
|
29
|
-
const segments = normalizedGlob.split("/");
|
|
30
|
-
const regexParts = [];
|
|
31
|
-
for (let i = 0; i < segments.length; i++) {
|
|
32
|
-
const seg = segments[i];
|
|
33
|
-
if (seg === "**") {
|
|
34
|
-
regexParts.push("**");
|
|
35
|
-
} else {
|
|
36
|
-
regexParts.push(
|
|
37
|
-
seg
|
|
38
|
-
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
39
|
-
.replace(/\*/g, "[^/]*")
|
|
40
|
-
.replace(/\?/g, "[^/]")
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
let patternStr = regexParts.join("/");
|
|
45
|
-
patternStr = patternStr.replace(/(?:^|\/)\*\*(?:\/|$)/g, (m) => {
|
|
46
|
-
if (m === "**") return ".*";
|
|
47
|
-
if (m === "/**/") return "(?:/|/.*/)";
|
|
48
|
-
if (m === "/**") return "(?:/.*)?";
|
|
49
|
-
if (m === "**/") return "(?:.*/)?";
|
|
50
|
-
return ".*";
|
|
51
|
-
});
|
|
52
|
-
patternStr = patternStr.replace(/\*\*/g, ".*");
|
|
53
|
-
return new RegExp(`^${patternStr}$`, "i").test(cleanPath);
|
|
13
|
+
return matchesGlob(filepath, globPattern);
|
|
54
14
|
}
|
|
55
15
|
|
|
16
|
+
export const EXECUTION_CONFIG_FILES = ["package.json", "tsconfig.json", "Cargo.toml", "go.mod", "pyproject.toml", "vite.config.ts", "jest.config.js", ".npmrc"];
|
|
17
|
+
export const RESTRICTED_AGENT_FILES = ["AGENTS.md", "JULES_RULES_TEMPLATE.md"];
|
|
18
|
+
export const COMMAND_DEFINING_FILES = ["package.json", "Cargo.toml", "go.mod", "pyproject.toml", ".agent/jules.yml", ".agent/config.yml"];
|
|
56
19
|
|
|
57
|
-
export const EXECUTION_CONFIG_FILES = [
|
|
58
|
-
".npmrc",
|
|
59
|
-
".yarnrc",
|
|
60
|
-
".yarnrc.yml",
|
|
61
|
-
"pnpmfile.js",
|
|
62
|
-
"pnpmfile.cjs",
|
|
63
|
-
".pnpmfile.cjs",
|
|
64
|
-
"jest.config.js",
|
|
65
|
-
"jest.config.cjs",
|
|
66
|
-
"jest.config.mjs",
|
|
67
|
-
"jest.config.ts",
|
|
68
|
-
"vitest.config.js",
|
|
69
|
-
"vitest.config.cjs",
|
|
70
|
-
"vitest.config.mjs",
|
|
71
|
-
"vitest.config.ts",
|
|
72
|
-
"playwright.config.js",
|
|
73
|
-
"playwright.config.ts",
|
|
74
|
-
"vite.config.js",
|
|
75
|
-
"vite.config.ts",
|
|
76
|
-
"webpack.config.js",
|
|
77
|
-
"webpack.config.cjs",
|
|
78
|
-
"webpack.config.mjs",
|
|
79
|
-
"rollup.config.js",
|
|
80
|
-
"rollup.config.mjs",
|
|
81
|
-
"esbuild.config.js",
|
|
82
|
-
"next.config.js",
|
|
83
|
-
"next.config.mjs",
|
|
84
|
-
"next.config.ts",
|
|
85
|
-
"babel.config.js",
|
|
86
|
-
"babel.config.cjs",
|
|
87
|
-
"babel.config.json",
|
|
88
|
-
".babelrc",
|
|
89
|
-
"tsconfig.json",
|
|
90
|
-
"tsconfig.*.json",
|
|
91
|
-
"deno.json",
|
|
92
|
-
"deno.jsonc",
|
|
93
|
-
"bunfig.toml"
|
|
94
|
-
];
|
|
95
|
-
|
|
96
|
-
export const RESTRICTED_AGENT_FILES = [
|
|
97
|
-
"AGENTS.md",
|
|
98
|
-
"JULES_RULES_TEMPLATE.md",
|
|
99
|
-
".agent/rules/**",
|
|
100
|
-
".agent/workflows/**",
|
|
101
|
-
".agent/jules.yml",
|
|
102
|
-
"jules.config.json"
|
|
103
|
-
];
|
|
104
|
-
|
|
105
|
-
export const COMMAND_DEFINING_FILES = [
|
|
106
|
-
"package.json",
|
|
107
|
-
"package-lock.json",
|
|
108
|
-
"pnpm-lock.yaml",
|
|
109
|
-
"pnpm-workspace.yaml",
|
|
110
|
-
"yarn.lock",
|
|
111
|
-
"bun.lockb",
|
|
112
|
-
"turbo.json",
|
|
113
|
-
"nx.json",
|
|
114
|
-
"lerna.json",
|
|
115
|
-
"rush.json",
|
|
116
|
-
"Cargo.toml",
|
|
117
|
-
"Cargo.lock",
|
|
118
|
-
"go.mod",
|
|
119
|
-
"go.sum",
|
|
120
|
-
"pyproject.toml",
|
|
121
|
-
"setup.py",
|
|
122
|
-
"requirements.txt",
|
|
123
|
-
"Makefile",
|
|
124
|
-
"build.gradle",
|
|
125
|
-
"build.gradle.kts",
|
|
126
|
-
"pom.xml",
|
|
127
|
-
".agent/jules.yml",
|
|
128
|
-
"jules.config.json"
|
|
129
|
-
];
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Lightweight Zero-Dependency YAML Pattern Extractor
|
|
133
|
-
* Extracts forbidden_paths and allow_paths arrays from jules.yml.
|
|
134
|
-
* Supported subset:
|
|
135
|
-
* - Flow style arrays: forbidden_paths: ["path1", "path2"]
|
|
136
|
-
* - Block style arrays: forbidden_paths:\n - path1\n - path2
|
|
137
|
-
* Limitations: Does not support multiline strings (|/>), nested dicts, or inline comments.
|
|
138
|
-
*/
|
|
139
20
|
export function loadForbiddenPatterns(configContent = "") {
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
"**/lock-manager*",
|
|
146
|
-
"scripts/jules-*",
|
|
147
|
-
".agent/jules.yml"
|
|
148
|
-
];
|
|
149
|
-
if (!configContent) return defaultForbidden;
|
|
150
|
-
|
|
151
|
-
// Flow style: forbidden_paths: [...]
|
|
152
|
-
const flowMatch = configContent.match(/forbidden_paths:\s*\[([^\]]+)\]/);
|
|
153
|
-
if (flowMatch) {
|
|
154
|
-
const parsed = flowMatch[1].split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
|
|
155
|
-
return Array.from(new Set([...defaultForbidden, ...parsed]));
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Block style: forbidden_paths:\n - "path1"\n - "path2"
|
|
159
|
-
const lines = configContent.split("\n");
|
|
160
|
-
let inForbidden = false;
|
|
161
|
-
const blockParsed = [];
|
|
162
|
-
for (const line of lines) {
|
|
163
|
-
if (line.trim().startsWith("forbidden_paths:")) {
|
|
164
|
-
inForbidden = true;
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
if (inForbidden) {
|
|
168
|
-
const lineWithoutComment = line.split("#")[0];
|
|
169
|
-
const trimmed = lineWithoutComment.trim();
|
|
170
|
-
if (trimmed.startsWith("-")) {
|
|
171
|
-
const val = trimmed.slice(1).trim().replace(/^["']|["']$/g, "");
|
|
172
|
-
if (val) blockParsed.push(val);
|
|
173
|
-
} else if (trimmed) {
|
|
174
|
-
break;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
if (blockParsed.length > 0) {
|
|
179
|
-
return Array.from(new Set([...defaultForbidden, ...blockParsed]));
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return defaultForbidden;
|
|
21
|
+
const defaults = ["scripts/jules-*", ".agent/jules.yml", ".github/**", "**/*.pem"];
|
|
22
|
+
if (!configContent) return defaults;
|
|
23
|
+
const parsed = parseYaml(configContent);
|
|
24
|
+
const userPaths = Array.isArray(parsed.forbidden_paths) ? parsed.forbidden_paths : [];
|
|
25
|
+
return Array.from(new Set([...defaults, ...userPaths]));
|
|
183
26
|
}
|
|
184
27
|
|
|
185
28
|
export function loadAllowedPatterns(configContent = "") {
|
|
186
29
|
if (!configContent) return [];
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
return flowMatch[1].split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
|
|
190
|
-
}
|
|
191
|
-
const lines = configContent.split("\n");
|
|
192
|
-
let inAllow = false;
|
|
193
|
-
const blockParsed = [];
|
|
194
|
-
for (const line of lines) {
|
|
195
|
-
if (line.trim().startsWith("allow_paths:")) {
|
|
196
|
-
inAllow = true;
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
if (inAllow) {
|
|
200
|
-
const trimmed = line.trim();
|
|
201
|
-
if (trimmed.startsWith("-")) {
|
|
202
|
-
blockParsed.push(trimmed.slice(1).trim().replace(/^["']|["']$/g, ""));
|
|
203
|
-
} else if (trimmed && !trimmed.startsWith("#")) {
|
|
204
|
-
break;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
return blockParsed;
|
|
30
|
+
const parsed = parseYaml(configContent);
|
|
31
|
+
return Array.isArray(parsed.allow_paths) ? parsed.allow_paths : [];
|
|
209
32
|
}
|
|
210
33
|
|
|
211
|
-
export function validateJulesConfig(
|
|
34
|
+
export function validateJulesConfig(_configContent = "", jsonGuardrailsContent = "") {
|
|
212
35
|
const errors = [];
|
|
213
|
-
|
|
214
|
-
if (configContent) {
|
|
215
|
-
const forbidden = loadForbiddenPatterns(configContent);
|
|
216
|
-
const allowed = loadAllowedPatterns(configContent);
|
|
217
|
-
const allPatterns = [...forbidden, ...allowed];
|
|
218
|
-
|
|
219
|
-
for (const pat of allPatterns) {
|
|
220
|
-
if (typeof pat !== "string" || !pat.trim()) {
|
|
221
|
-
errors.push("Empty or non-string glob pattern found in configuration.");
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
36
|
if (jsonGuardrailsContent) {
|
|
227
37
|
try {
|
|
228
|
-
const
|
|
229
|
-
if (Array.isArray(
|
|
230
|
-
for (
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
} catch (reErr) {
|
|
236
|
-
errors.push(`Invalid RegExp trigger at rule index ${i} ('${rule.trigger}'): ${reErr.message}`);
|
|
237
|
-
}
|
|
38
|
+
const parsed = JSON.parse(jsonGuardrailsContent);
|
|
39
|
+
if (Array.isArray(parsed.rules)) {
|
|
40
|
+
for (const rule of parsed.rules) {
|
|
41
|
+
try {
|
|
42
|
+
new RegExp(rule.trigger, "i");
|
|
43
|
+
} catch (err) {
|
|
44
|
+
errors.push(`Invalid RegExp trigger: ${err.message}`);
|
|
238
45
|
}
|
|
239
46
|
}
|
|
240
47
|
}
|
|
241
|
-
} catch (
|
|
242
|
-
errors.push(`
|
|
48
|
+
} catch (err) {
|
|
49
|
+
errors.push(`JSON Parse Error: ${err.message}`);
|
|
243
50
|
}
|
|
244
51
|
}
|
|
245
|
-
|
|
246
|
-
if (errors.length > 0) {
|
|
247
|
-
const err = new Error(`CONFIG VALIDATION ERROR:\n` + errors.map((e) => ` - ${e}`).join("\n"));
|
|
248
|
-
err.code = 3;
|
|
249
|
-
return { ok: false, errors, err };
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
return { ok: true, errors: [] };
|
|
52
|
+
return { ok: errors.length === 0, errors };
|
|
253
53
|
}
|
|
254
54
|
|
|
255
|
-
export function parseAndCleanStderr(
|
|
256
|
-
if (
|
|
257
|
-
|
|
258
|
-
const lines = clean.split("\n").filter(Boolean);
|
|
259
|
-
return lines.slice(-100).join("\n");
|
|
55
|
+
export function parseAndCleanStderr(str = "") {
|
|
56
|
+
if (typeof str !== "string") return "";
|
|
57
|
+
return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "").replace(/\u001b\[[0-9;]*m/g, "");
|
|
260
58
|
}
|
|
261
59
|
|
|
262
60
|
export function getOodaStateFile(mergeBase = "main") {
|
|
263
|
-
|
|
264
|
-
if (!fs.existsSync(oodaDir)) fs.mkdirSync(oodaDir, { recursive: true });
|
|
265
|
-
|
|
266
|
-
const keyInput = [
|
|
267
|
-
process.env.GITHUB_REPOSITORY || "",
|
|
268
|
-
process.env.GITHUB_REF || "",
|
|
269
|
-
process.env.GITHUB_HEAD_REF || "",
|
|
270
|
-
mergeBase,
|
|
271
|
-
process.cwd()
|
|
272
|
-
].join("\u0000");
|
|
273
|
-
|
|
274
|
-
const key = crypto.createHash("sha256").update(keyInput).digest("hex").slice(0, 16);
|
|
275
|
-
return path.join(oodaDir, `${key}.json`);
|
|
61
|
+
return `.agent/state/ooda-${mergeBase}.json`;
|
|
276
62
|
}
|
|
277
63
|
|
|
278
|
-
export function logAuditMetrics(
|
|
279
|
-
const historyDir = path.resolve(process.cwd(), ".agent/history");
|
|
280
|
-
if (!fs.existsSync(historyDir)) {
|
|
281
|
-
fs.mkdirSync(historyDir, { recursive: true });
|
|
282
|
-
}
|
|
283
|
-
const metricsFile = path.join(historyDir, "metrics.jsonl");
|
|
284
|
-
const logLine = JSON.stringify({
|
|
285
|
-
timestamp: new Date().toISOString(),
|
|
286
|
-
...metrics,
|
|
287
|
-
}) + "\n";
|
|
288
|
-
fs.appendFileSync(metricsFile, logLine, "utf-8");
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
export function runSelfAudit() {
|
|
292
|
-
log.header("Running Jules PR Self-Audit Gatekeeper...");
|
|
293
|
-
|
|
294
|
-
const targetBranch = process.env.BASE_BRANCH || "main";
|
|
295
|
-
const SAFE_BRANCH = /^[a-zA-Z0-9._\/-]+$/;
|
|
296
|
-
if (!SAFE_BRANCH.test(targetBranch)) {
|
|
297
|
-
const err = new Error(`FATAL: Invalid BASE_BRANCH "${targetBranch}". Must match ^[a-zA-Z0-9._\\/-]+$`);
|
|
298
|
-
err.code = 2;
|
|
299
|
-
throw err;
|
|
300
|
-
}
|
|
301
|
-
log.info(`Target Branch: ${targetBranch}`);
|
|
302
|
-
|
|
303
|
-
try {
|
|
304
|
-
execFileSync("git", ["--version"], { stdio: "ignore" });
|
|
305
|
-
} catch {
|
|
306
|
-
const err = new Error("FATAL: git is not installed or not in PATH.");
|
|
307
|
-
err.code = 2;
|
|
308
|
-
throw err;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (process.env.CI) {
|
|
312
|
-
log.info("☁️ CI environment detected. Fetching merge-base history...");
|
|
313
|
-
runGitCommand(["fetch", "origin", targetBranch, "--depth=100"], true);
|
|
314
|
-
const tmpRef = runGitCommand(["rev-parse", "--verify", `origin/${targetBranch}`], true) ? `origin/${targetBranch}` : targetBranch;
|
|
315
|
-
const tmpMerge = runGitCommand(["merge-base", "HEAD", tmpRef], true);
|
|
316
|
-
if (!tmpMerge) {
|
|
317
|
-
log.warn("Shallow fetch failed to find merge-base, executing full unshallow...");
|
|
318
|
-
runGitCommand(["fetch", "origin", targetBranch, "--unshallow"], true);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const mainRef = runGitCommand(["rev-parse", "--verify", `origin/${targetBranch}`], true) ? `origin/${targetBranch}` : targetBranch;
|
|
323
|
-
const mergeBase = runGitCommand(["merge-base", "HEAD", mainRef], true);
|
|
324
|
-
|
|
325
|
-
if (!mergeBase) {
|
|
326
|
-
const err = new Error(`FATAL: Could not compute merge-base with ${mainRef}. Make sure git history is unshallowed.`);
|
|
327
|
-
err.code = 2;
|
|
328
|
-
throw err;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
log.info(`Merge-Base Hash: ${mergeBase}`);
|
|
332
|
-
|
|
333
|
-
const rawDiffFiles = runGitCommand(["diff", "-z", "--name-only", `${mergeBase}...HEAD`])
|
|
334
|
-
.split("\0")
|
|
335
|
-
.map((f) => f.trim().replace(/^["']|["']$/g, ""))
|
|
336
|
-
.filter(Boolean);
|
|
337
|
-
|
|
338
|
-
const ALL_COMMAND_FILES = [...COMMAND_DEFINING_FILES, ...EXECUTION_CONFIG_FILES];
|
|
339
|
-
const commandFileChanges = rawDiffFiles.filter((file) =>
|
|
340
|
-
ALL_COMMAND_FILES.some((cmdFile) => file === cmdFile || file.endsWith(`/${cmdFile}`) || matchGlob(file, cmdFile))
|
|
341
|
-
);
|
|
342
|
-
|
|
343
|
-
if (commandFileChanges.length > 0 && process.env.JULES_ALLOW_COMMAND_FILE_CHANGES !== "true" && process.env.JULES_ALLOW_COMMAND_FILE_CHANGES !== "1") {
|
|
344
|
-
const err = new Error(
|
|
345
|
-
"COMMAND / BUILD CONFIG FILE CHANGE DETECTED: [" + commandFileChanges.join(", ") + "]. " +
|
|
346
|
-
"Refusing to execute verification scripts from untrusted branch. " +
|
|
347
|
-
"To override for legitimate dependency updates, set JULES_ALLOW_COMMAND_FILE_CHANGES=true."
|
|
348
|
-
);
|
|
349
|
-
err.code = 3;
|
|
350
|
-
throw err;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
const agentRuleChanges = rawDiffFiles.filter((file) =>
|
|
354
|
-
RESTRICTED_AGENT_FILES.some((pattern) => matchGlob(file, pattern))
|
|
355
|
-
);
|
|
356
|
-
|
|
357
|
-
if (agentRuleChanges.length > 0 && process.env.JULES_ALLOW_AGENT_RULE_CHANGES !== "true" && process.env.JULES_ALLOW_AGENT_RULE_CHANGES !== "1") {
|
|
358
|
-
const err = new Error(
|
|
359
|
-
"AGENT RULE FILE CHANGE DETECTED: [" + agentRuleChanges.join(", ") + "]. " +
|
|
360
|
-
"Refusing to load agent rules from untrusted branch. " +
|
|
361
|
-
"To override for legitimate rule updates, set JULES_ALLOW_AGENT_RULE_CHANGES=true."
|
|
362
|
-
);
|
|
363
|
-
err.code = 3;
|
|
364
|
-
throw err;
|
|
365
|
-
}
|
|
64
|
+
export function logAuditMetrics() {}
|
|
366
65
|
|
|
367
|
-
|
|
368
|
-
const
|
|
66
|
+
export async function runSelfAudit(opts = {}) {
|
|
67
|
+
const root = opts.root || process.env.JULES_PROJECT_ROOT || process.cwd();
|
|
68
|
+
const config = loadConfig(root);
|
|
69
|
+
const base = opts.base || process.env.BASE_BRANCH || config.baseBranch || "main";
|
|
369
70
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const trustedJson = runGitCommand(["show", `${mainRef}:.agent/rules/dynamic-guardrails.json`], true);
|
|
377
|
-
|
|
378
|
-
const validationRes = validateJulesConfig(trustedConfig, trustedJson);
|
|
379
|
-
if (!validationRes.ok) {
|
|
380
|
-
log.error(`Config validation failed: ${validationRes.err.message}`);
|
|
381
|
-
throw validationRes.err;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
const forbiddenPatterns = loadForbiddenPatterns(trustedConfig);
|
|
385
|
-
const allowedPatterns = loadAllowedPatterns(trustedConfig);
|
|
386
|
-
|
|
387
|
-
const violationsWithDetails = [];
|
|
388
|
-
rawDiffFiles.forEach((file) => {
|
|
389
|
-
const matchedForbidden = forbiddenPatterns.find((pattern) => matchGlob(file, pattern));
|
|
390
|
-
if (matchedForbidden) {
|
|
391
|
-
violationsWithDetails.push(`${file} (matched forbidden pattern '${matchedForbidden}')`);
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
if (allowedPatterns.length > 0) {
|
|
395
|
-
const isAllowed = allowedPatterns.some((pattern) => matchGlob(file, pattern));
|
|
396
|
-
if (!isAllowed) {
|
|
397
|
-
violationsWithDetails.push(`${file} (not permitted in allow_paths [${allowedPatterns.join(", ")}])`);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
71
|
+
const res = await gate({
|
|
72
|
+
root,
|
|
73
|
+
config,
|
|
74
|
+
base,
|
|
75
|
+
fix: process.env.ALLOW_AUTO_REPAIR === "true",
|
|
76
|
+
allowProtected: process.env.JULES_ALLOW_COMMAND_FILE_CHANGES === "true",
|
|
400
77
|
});
|
|
401
|
-
|
|
402
|
-
if (violationsWithDetails.length > 0) {
|
|
403
|
-
if (process.env.JULES_ALLOW_RESTRICTED_FILES === "true") {
|
|
404
|
-
log.warn("JULES_ALLOW_RESTRICTED_FILES=true is set. Bypassing restricted file boundary check.");
|
|
405
|
-
} else {
|
|
406
|
-
const err = new Error(
|
|
407
|
-
`RESTRICTED FILE VIOLATION DETECTED (config read from ${mainRef}:.agent/jules.yml):\n` +
|
|
408
|
-
violationsWithDetails.map((v) => ` - ${v}`).join("\n") +
|
|
409
|
-
"\nTo override restricted file checks, set JULES_ALLOW_RESTRICTED_FILES=true."
|
|
410
|
-
);
|
|
411
|
-
err.code = 3;
|
|
412
|
-
throw err;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
log.success("Restricted File Boundary Check: PASSED");
|
|
416
|
-
|
|
417
|
-
log.info("Analyzing Code Diff Payload Size & Content...");
|
|
418
|
-
const codeDiffPayload = changedCodeFiles.length > 0
|
|
419
|
-
? runGitCommand(["diff", `${mergeBase}...HEAD`, "--", ...changedCodeFiles], true)
|
|
420
|
-
: "";
|
|
421
|
-
|
|
422
|
-
const diffBytes = Buffer.byteLength(codeDiffPayload, "utf8");
|
|
423
|
-
const maxDiffKb = parseInt(process.env.JULES_MAX_DIFF_KB || "50", 10);
|
|
424
|
-
const maxDiffBytes = maxDiffKb * 1024;
|
|
425
|
-
|
|
426
|
-
if (diffBytes > maxDiffBytes) {
|
|
427
|
-
const err = new Error(`DIFF PAYLOAD TOO LARGE: ${diffBytes} bytes exceeds ${maxDiffKb} KB limit for code files. Split the task.`);
|
|
428
|
-
err.code = 5;
|
|
429
|
-
throw err;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
let fullDiffPayload = "";
|
|
433
|
-
try {
|
|
434
|
-
fullDiffPayload = runGitCommand(["diff", `${mergeBase}...HEAD`], false);
|
|
435
|
-
} catch (err) {
|
|
436
|
-
const error = new Error("DIFF PAYLOAD TOO LARGE: The PR diff exceeds the maximum buffer size (25MB) and cannot be safely scanned for secrets. Please split the task into smaller PRs.");
|
|
437
|
-
error.code = 5;
|
|
438
|
-
throw error;
|
|
439
|
-
}
|
|
440
|
-
const addedLines = fullDiffPayload
|
|
441
|
-
.split("\n")
|
|
442
|
-
.filter((line) => line.startsWith("+") && !line.startsWith("+++"))
|
|
443
|
-
.map((line) => line.slice(1))
|
|
444
|
-
.join("\n");
|
|
445
|
-
|
|
446
|
-
if (hasHighConfidenceSecret(addedLines)) {
|
|
447
|
-
const err = new Error("SECRET LEAK PREVENTED: High-confidence secret token or private key detected in added diff lines. Aborting.");
|
|
448
|
-
err.code = 6;
|
|
449
|
-
throw err;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
if (hasLowConfidenceSecret(addedLines)) {
|
|
453
|
-
log.warn("SECURITY WARNING: Low-confidence secret or test key pattern detected in added diff lines.");
|
|
454
|
-
}
|
|
455
|
-
log.success(`Diff Inspection Check: PASSED (${diffBytes} bytes code diff, No Active High-Confidence Secrets)`);
|
|
456
|
-
|
|
457
|
-
log.info("Resolving Dynamic Verification Suite from Trusted Base Branch...");
|
|
458
|
-
|
|
459
|
-
const runId = crypto.randomBytes(4).toString("hex");
|
|
460
|
-
const trustedDir = path.join(os.tmpdir(), `jules-trusted-tree-${runId}`);
|
|
461
|
-
const tempArchive = path.join(os.tmpdir(), `jules-trusted-archive-${runId}.tar`);
|
|
462
|
-
|
|
463
|
-
let resolutionRoot;
|
|
464
|
-
try {
|
|
465
|
-
fs.mkdirSync(trustedDir, { recursive: true });
|
|
466
|
-
execFileSync("git", ["archive", mainRef, "-o", tempArchive]);
|
|
467
|
-
execFileSync("tar", ["-xf", tempArchive, "-C", trustedDir]);
|
|
468
|
-
resolutionRoot = trustedDir;
|
|
469
|
-
} catch (err) {
|
|
470
|
-
const error = new Error("FATAL: Failed to extract trusted base branch for command resolution. Refusing to fall back to working tree.\n" + err.message);
|
|
471
|
-
error.code = 2;
|
|
472
|
-
throw error;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
let resolvedCmds;
|
|
476
|
-
try {
|
|
477
|
-
resolvedCmds = resolveWorkspaceExecutionBoundary(changedCodeFiles, resolutionRoot);
|
|
478
|
-
} finally {
|
|
479
|
-
try {
|
|
480
|
-
if (trustedDir && fs.existsSync(trustedDir)) {
|
|
481
|
-
fs.rmSync(trustedDir, { recursive: true, force: true });
|
|
482
|
-
}
|
|
483
|
-
if (tempArchive && fs.existsSync(tempArchive)) {
|
|
484
|
-
fs.rmSync(tempArchive, { force: true });
|
|
485
|
-
}
|
|
486
|
-
} catch (e) {}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
log.info(`Discovered Execution Scope: ${resolvedCmds.source}`);
|
|
490
|
-
|
|
491
|
-
const startTime = Date.now();
|
|
492
|
-
let testPassed = true;
|
|
493
|
-
let failureLog = "";
|
|
494
|
-
|
|
495
|
-
const runVerification = (cmd) => {
|
|
496
|
-
if (typeof cmd !== "string" || !cmd.trim()) {
|
|
497
|
-
return false;
|
|
498
|
-
}
|
|
499
|
-
if (/[\r\n\0]/.test(cmd)) {
|
|
500
|
-
log.error(`Security violation: Invalid control characters in command: ${cmd}`);
|
|
501
|
-
failureLog = `Security violation: Invalid control characters in verification command`;
|
|
502
|
-
return false;
|
|
503
|
-
}
|
|
504
|
-
try {
|
|
505
|
-
const timeoutMs = parseInt(process.env.JULES_VERIFY_TIMEOUT_MS || "600000", 10);
|
|
506
|
-
execSync(cmd, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: timeoutMs });
|
|
507
|
-
return true;
|
|
508
|
-
} catch (err) {
|
|
509
|
-
failureLog = parseAndCleanStderr((err.stderr || "") + "\n" + (err.stdout || "") + "\n" + (err.message || ""));
|
|
510
|
-
return false;
|
|
511
|
-
}
|
|
512
|
-
};
|
|
513
|
-
|
|
514
|
-
if (resolvedCmds.testCmd) {
|
|
515
|
-
log.step("▶", `Running Test Verification: ${resolvedCmds.testCmd}`);
|
|
516
|
-
if (!runVerification(resolvedCmds.testCmd)) {
|
|
517
|
-
testPassed = false;
|
|
518
|
-
log.error(`Test verification failed: ${resolvedCmds.testCmd}`);
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
if (testPassed && resolvedCmds.buildCmd) {
|
|
523
|
-
log.step("▶", `Running Build Verification: ${resolvedCmds.buildCmd}`);
|
|
524
|
-
if (!runVerification(resolvedCmds.buildCmd)) {
|
|
525
|
-
testPassed = false;
|
|
526
|
-
log.error(`Build verification failed: ${resolvedCmds.buildCmd}`);
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
if (!resolvedCmds.testCmd && !resolvedCmds.buildCmd) {
|
|
531
|
-
log.info("No build or test scripts found. Running git status check.");
|
|
532
|
-
execSync("git status", { stdio: "inherit" });
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
const durationMs = Date.now() - startTime;
|
|
536
|
-
logAuditMetrics({
|
|
537
|
-
targetBranch,
|
|
538
|
-
changedFilesCount: changedCodeFiles.length,
|
|
539
|
-
testPassed,
|
|
540
|
-
durationMs,
|
|
541
|
-
scope: resolvedCmds.source,
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
if (!testPassed) {
|
|
545
|
-
log.error("OODA SELF-HEALING FEEDBACK LOG:");
|
|
546
|
-
log.dim(failureLog.slice(-1500));
|
|
547
|
-
|
|
548
|
-
if (process.env.CI && !process.env.ALLOW_AUTO_REPAIR) {
|
|
549
|
-
const err = new Error("CI environment detected and ALLOW_AUTO_REPAIR is not set. Failing fast to save CI minutes.");
|
|
550
|
-
err.code = 4;
|
|
551
|
-
throw err;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
let oodaRetries = 0;
|
|
555
|
-
const oodaStateFile = getOodaStateFile(mergeBase);
|
|
556
|
-
try {
|
|
557
|
-
if (fs.existsSync(oodaStateFile)) {
|
|
558
|
-
const oodaData = JSON.parse(fs.readFileSync(oodaStateFile, "utf-8"));
|
|
559
|
-
oodaRetries = oodaData.attempts || 0;
|
|
560
|
-
}
|
|
561
|
-
} catch (e) {
|
|
562
|
-
// Ignore reading errors
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
if (oodaRetries < 3) {
|
|
566
|
-
log.info(`🛠️ Initiating OODA Auto-Repair (Attempt ${oodaRetries + 1}/3)...`);
|
|
567
|
-
try {
|
|
568
|
-
fs.writeFileSync(oodaStateFile, JSON.stringify({ attempts: oodaRetries + 1, lastAttempt: new Date().toISOString() }), "utf-8");
|
|
569
|
-
} catch (e) {}
|
|
570
|
-
const sanitizedFailureLog = redactSecrets(anonymizePii(failureLog.slice(-1500)));
|
|
571
|
-
const prompt = `OODA Auto-Repair Attempt ${oodaRetries + 1}\n\nThe verification suite failed with the following errors after the previous patch:\n\n\`\`\`\n${sanitizedFailureLog}\n\`\`\`\n\nPlease fix the errors so the verification passes.`;
|
|
572
|
-
|
|
573
|
-
const dispatchScript = path.resolve(process.cwd(), "scripts/jules-dispatch.mjs");
|
|
574
|
-
try {
|
|
575
|
-
const env = {
|
|
576
|
-
...process.env,
|
|
577
|
-
START_BRANCH: process.env.GITHUB_HEAD_REF || targetBranch,
|
|
578
|
-
BASE_BRANCH: targetBranch
|
|
579
|
-
};
|
|
580
|
-
execFileSync("node", [dispatchScript, "OODA Auto-Repair", prompt], {
|
|
581
|
-
stdio: "inherit",
|
|
582
|
-
env
|
|
583
|
-
});
|
|
584
|
-
} catch (dispatchErr) {
|
|
585
|
-
log.error(`Failed to trigger OODA repair dispatch: ${dispatchErr.message}`);
|
|
586
|
-
}
|
|
587
|
-
} else {
|
|
588
|
-
log.error("❌ OODA Auto-Repair exhausted maximum retries (3). Giving up.");
|
|
589
|
-
}
|
|
590
|
-
const err = new Error("OODA Auto-Repair failed or exhausted.");
|
|
591
|
-
err.code = 4;
|
|
592
|
-
throw err;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
try {
|
|
596
|
-
const oodaStateFile = getOodaStateFile(mergeBase);
|
|
597
|
-
if (fs.existsSync(oodaStateFile)) {
|
|
598
|
-
fs.rmSync(oodaStateFile, { force: true });
|
|
599
|
-
}
|
|
600
|
-
} catch (_) {}
|
|
601
|
-
|
|
602
|
-
log.success("🎉 JULES PR SELF-AUDIT PASSED SUCCESSFULLY!");
|
|
78
|
+
return res;
|
|
603
79
|
}
|
|
604
80
|
|
|
605
|
-
export function runPreflightSandbox() {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
const runId = crypto.randomBytes(4).toString("hex");
|
|
609
|
-
const sandboxDir = path.join(os.tmpdir(), `jules-preflight-${runId}`);
|
|
610
|
-
const tempArchive = path.join(os.tmpdir(), `jules-archive-${runId}.tar`);
|
|
611
|
-
|
|
612
|
-
try {
|
|
613
|
-
log.step("[1/5]", `Creating isolated environment in ${sandboxDir}...`);
|
|
614
|
-
fs.mkdirSync(sandboxDir, { recursive: true });
|
|
615
|
-
|
|
616
|
-
execFileSync("git", ["archive", "HEAD", "-o", tempArchive]);
|
|
617
|
-
execFileSync("tar", ["-xf", tempArchive, "-C", sandboxDir]);
|
|
618
|
-
|
|
619
|
-
log.step("[2/5]", "Sterilizing environment variables...");
|
|
620
|
-
const sterileEnv = { ...process.env };
|
|
621
|
-
const stripKeys = ["DATABASE_URL", "NPM_TOKEN", "GITHUB_TOKEN", "GH_TOKEN", "AWS_ACCESS_KEY_ID", "STRIPE_TEST_KEY"];
|
|
622
|
-
for (const key of stripKeys) {
|
|
623
|
-
if (sterileEnv[key]) {
|
|
624
|
-
sterileEnv[key] = `mock-${key.toLowerCase()}`;
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
log.step("[3/5]", "Resolving package manager and installing dependencies...");
|
|
629
|
-
let installCmd = "";
|
|
630
|
-
if (fs.existsSync(path.join(sandboxDir, "pnpm-lock.yaml"))) installCmd = "pnpm install --ignore-scripts";
|
|
631
|
-
else if (fs.existsSync(path.join(sandboxDir, "bun.lockb"))) installCmd = "bun install --ignore-scripts";
|
|
632
|
-
else if (fs.existsSync(path.join(sandboxDir, "yarn.lock"))) installCmd = "yarn install --ignore-scripts";
|
|
633
|
-
else if (fs.existsSync(path.join(sandboxDir, "package-lock.json"))) installCmd = "npm ci --ignore-scripts";
|
|
634
|
-
|
|
635
|
-
if (installCmd) {
|
|
636
|
-
try {
|
|
637
|
-
execSync(installCmd, { cwd: sandboxDir, stdio: "pipe", encoding: "utf-8" });
|
|
638
|
-
} catch (e) {
|
|
639
|
-
log.warn(`${installCmd} failed or wasn't needed. Output: ${e.stdout}\nError: ${e.stderr}\nProceeding...`);
|
|
640
|
-
}
|
|
641
|
-
} else {
|
|
642
|
-
log.info("Skipping dependency installation (no lockfile detected).");
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
log.step("[4/5]", "Resolving execution boundary in sandbox...");
|
|
646
|
-
const resolvedCmds = resolveWorkspaceExecutionBoundary([], sandboxDir);
|
|
647
|
-
|
|
648
|
-
if (resolvedCmds.testCmd || resolvedCmds.buildCmd) {
|
|
649
|
-
log.step("[5/5]", `Running verification: ${resolvedCmds.testCmd || resolvedCmds.buildCmd}`);
|
|
650
|
-
execSync(resolvedCmds.testCmd || resolvedCmds.buildCmd, {
|
|
651
|
-
cwd: sandboxDir,
|
|
652
|
-
env: sterileEnv,
|
|
653
|
-
stdio: "inherit"
|
|
654
|
-
});
|
|
655
|
-
log.success("Pre-Flight Passed. Zero epistemic drift detected.");
|
|
656
|
-
} else {
|
|
657
|
-
log.info("No test/build commands found for verification.");
|
|
658
|
-
log.success("Pre-Flight Passed (No-Op).");
|
|
659
|
-
}
|
|
660
|
-
} catch (err) {
|
|
661
|
-
log.error("\nPRE-FLIGHT FAILED!");
|
|
662
|
-
log.error("The agent would likely crash (The 5-Minute Drop-off) in the Web UI due to missing dependencies.");
|
|
663
|
-
const wrappedErr = new Error(err.message);
|
|
664
|
-
wrappedErr.code = 1;
|
|
665
|
-
throw wrappedErr;
|
|
666
|
-
} finally {
|
|
667
|
-
try {
|
|
668
|
-
fs.rmSync(sandboxDir, { recursive: true, force: true });
|
|
669
|
-
if (fs.existsSync(tempArchive)) {
|
|
670
|
-
fs.rmSync(tempArchive, { force: true });
|
|
671
|
-
}
|
|
672
|
-
} catch (e) {
|
|
673
|
-
// Ignore cleanup errors
|
|
674
|
-
}
|
|
675
|
-
}
|
|
81
|
+
export async function runPreflightSandbox() {
|
|
82
|
+
return { ok: true };
|
|
676
83
|
}
|
|
677
84
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
85
|
+
if (process.argv[1] && process.argv[1].endsWith("jules-self-audit.mjs")) {
|
|
86
|
+
runSelfAudit()
|
|
87
|
+
.then((res) => {
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
if (res.code === 3) {
|
|
90
|
+
console.error("❌ RESTRICTED FILE VIOLATION");
|
|
91
|
+
} else if (res.code === 6) {
|
|
92
|
+
console.error("❌ SECRET LEAK PREVENTED");
|
|
93
|
+
} else {
|
|
94
|
+
console.error(`❌ Self Audit Failed (Exit ${res.code})`);
|
|
95
|
+
}
|
|
96
|
+
process.exit(res.code);
|
|
97
|
+
}
|
|
98
|
+
console.log("Audit Complete: PASSED");
|
|
99
|
+
process.exit(0);
|
|
100
|
+
})
|
|
101
|
+
.catch((err) => {
|
|
102
|
+
console.error(`Audit Failure: ${err.message}`);
|
|
103
|
+
process.exit(err.code || 1);
|
|
104
|
+
});
|
|
690
105
|
}
|
|
691
|
-
|
|
692
|
-
|