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