jules-orchestrator-kit 0.74.0 → 0.75.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/CHANGELOG.md +27 -1
- package/JULES_RULES_TEMPLATE.md +3 -0
- package/README.md +4 -2
- package/bin/agentctl.mjs +395 -23
- package/package.json +1 -1
- package/scripts/ci-scope-guard.mjs +226 -23
- package/scripts/run-tests.mjs +5 -1
- package/src/bidi-guard.mjs +190 -15
- package/src/ci-templates.mjs +184 -21
- package/src/config.mjs +40 -1
- package/src/engine.mjs +342 -48
- package/src/evidence.mjs +22 -5
- package/src/git.mjs +15 -0
- package/src/migrate.mjs +493 -0
- package/src/ops/command-registry.mjs +93 -4
- package/src/ops/doctor-registry.mjs +184 -32
- package/src/ops/pr-harvest.mjs +22 -2
- package/src/ops/toolchain-diagnostics.mjs +253 -0
- package/src/ops/tui-menu.mjs +192 -0
- package/src/process-tree.mjs +146 -0
- package/src/prompt-guard.mjs +9 -0
- package/src/provider.mjs +14 -0
- package/src/scaffold.mjs +49 -1
- package/src/scope-guard.mjs +73 -5
- package/src/secret-scanner.mjs +14 -2
- package/src/security.mjs +41 -30
- package/src/self-audit.mjs +237 -3
- package/src/state.mjs +123 -13
- package/src/uninstall.mjs +227 -0
- package/src/wizard-init.mjs +11 -0
- package/src/wizard-task.mjs +17 -22
package/src/ci-templates.mjs
CHANGED
|
@@ -149,51 +149,214 @@ ${extraSteps ? extraSteps + "\n" : ""}
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
/**
|
|
152
|
-
*
|
|
152
|
+
* Generate a workflow_run auto-merge workflow for GitHub Actions that safely
|
|
153
|
+
* merges qualifying green PRs opened by authorized automation bots.
|
|
154
|
+
*
|
|
155
|
+
* Implements a strict trust boundary:
|
|
156
|
+
* - Executes exclusively on the base branch (default `main`)
|
|
157
|
+
* - Evaluates PR commits strictly as unexecuted data (fetch + git diff)
|
|
158
|
+
* - Verifies the bot actor against authorized identities (default `google-labs-jules[bot]`)
|
|
159
|
+
* - Merges cleanly via `gh pr merge --squash --delete-branch`
|
|
160
|
+
*
|
|
161
|
+
* @param {object} opts
|
|
162
|
+
* @param {object} [opts.config]
|
|
163
|
+
* @param {string} [opts.version]
|
|
164
|
+
* @param {string[]} [opts.allowedActors]
|
|
165
|
+
* @returns {{ file: string, content: string }}
|
|
166
|
+
*/
|
|
167
|
+
export function buildAutoMergeWorkflow(opts = {}) {
|
|
168
|
+
const base = opts.config?.baseBranch || opts.baseBranch || "main";
|
|
169
|
+
const version = opts.version || "0";
|
|
170
|
+
const defaultActors = ["google-labs-jules[bot]", "github-actions[bot]"];
|
|
171
|
+
const allowedActors = opts.allowedActors || defaultActors;
|
|
172
|
+
const actorCases = allowedActors.map((a) => `"${a}"`).join("|");
|
|
173
|
+
|
|
174
|
+
const content = `# Generated by jules-orchestrator-kit v${version}.
|
|
175
|
+
#
|
|
176
|
+
# Auto-merge verified pull requests opened by authorized automation bots.
|
|
177
|
+
#
|
|
178
|
+
# TRUST BOUNDARY ARCHITECTURE:
|
|
179
|
+
# For pull requests from repository branches, GitHub Actions runs
|
|
180
|
+
# 'on: pull_request' workflows from the PR branch head. A pull request can
|
|
181
|
+
# therefore supply its own modified workflow code.
|
|
182
|
+
#
|
|
183
|
+
# This workflow runs on 'workflow_run' after 'Agent Safety Gate' completes.
|
|
184
|
+
# It executes STRICTLY from the default branch (${base}), ensuring that:
|
|
185
|
+
# 1. It only runs code, scripts, and policies committed to ${base}.
|
|
186
|
+
# 2. The pull request commits are fetched strictly as unexecuted data.
|
|
187
|
+
# 3. Privileged write tokens are never exposed to the untrusted PR branch.
|
|
188
|
+
#
|
|
189
|
+
# Regenerate with: agentctl ci init --with-automerge --force
|
|
190
|
+
|
|
191
|
+
name: Agent Auto-Merge
|
|
192
|
+
|
|
193
|
+
on:
|
|
194
|
+
workflow_run:
|
|
195
|
+
workflows: ["Agent Safety Gate"]
|
|
196
|
+
types: [completed]
|
|
197
|
+
|
|
198
|
+
permissions:
|
|
199
|
+
contents: read
|
|
200
|
+
|
|
201
|
+
concurrency:
|
|
202
|
+
group: agent-automerge-\${{ github.event.workflow_run.head_sha }}
|
|
203
|
+
cancel-in-progress: false
|
|
204
|
+
|
|
205
|
+
jobs:
|
|
206
|
+
automerge:
|
|
207
|
+
if: >-
|
|
208
|
+
github.event.workflow_run.conclusion == 'success' &&
|
|
209
|
+
github.event.workflow_run.event == 'pull_request' &&
|
|
210
|
+
github.event.workflow_run.head_repository.full_name == github.repository
|
|
211
|
+
runs-on: ubuntu-latest
|
|
212
|
+
permissions:
|
|
213
|
+
contents: write
|
|
214
|
+
pull-requests: write
|
|
215
|
+
env:
|
|
216
|
+
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
217
|
+
REPO: \${{ github.repository }}
|
|
218
|
+
HEAD_SHA: \${{ github.event.workflow_run.head_sha }}
|
|
219
|
+
ACTOR: \${{ github.event.workflow_run.actor.login }}
|
|
220
|
+
steps:
|
|
221
|
+
- name: Checkout base revision (${base})
|
|
222
|
+
uses: actions/checkout@v4
|
|
223
|
+
with:
|
|
224
|
+
ref: ${base}
|
|
225
|
+
fetch-depth: 0
|
|
226
|
+
persist-credentials: false
|
|
227
|
+
|
|
228
|
+
- name: Verify authorized automation actor
|
|
229
|
+
run: |
|
|
230
|
+
set -euo pipefail
|
|
231
|
+
case "$ACTOR" in
|
|
232
|
+
${actorCases})
|
|
233
|
+
echo "Authorized automation actor: $ACTOR"
|
|
234
|
+
;;
|
|
235
|
+
*)
|
|
236
|
+
echo "Actor '$ACTOR' is not an authorized automation bot; leaving PR for human review."
|
|
237
|
+
exit 0
|
|
238
|
+
;;
|
|
239
|
+
esac
|
|
240
|
+
|
|
241
|
+
- id: pr
|
|
242
|
+
name: Resolve Pull Request
|
|
243
|
+
run: |
|
|
244
|
+
set -euo pipefail
|
|
245
|
+
num=$(gh pr list --repo "$REPO" --state open --base "${base}" \\
|
|
246
|
+
--json number,headRefOid \\
|
|
247
|
+
--jq "[.[] | select(.headRefOid == \\"$HEAD_SHA\\")] | .[0].number // empty")
|
|
248
|
+
if [ -z "$num" ]; then
|
|
249
|
+
echo "No open PR found for head SHA $HEAD_SHA; nothing to merge."
|
|
250
|
+
echo "found=false" >> "$GITHUB_OUTPUT"
|
|
251
|
+
exit 0
|
|
252
|
+
fi
|
|
253
|
+
echo "Resolved PR #$num for $HEAD_SHA"
|
|
254
|
+
echo "number=$num" >> "$GITHUB_OUTPUT"
|
|
255
|
+
echo "found=true" >> "$GITHUB_OUTPUT"
|
|
256
|
+
|
|
257
|
+
- name: Fetch PR commits as untrusted data
|
|
258
|
+
if: steps.pr.outputs.found == 'true'
|
|
259
|
+
run: |
|
|
260
|
+
set -euo pipefail
|
|
261
|
+
git fetch --no-tags origin "+$HEAD_SHA:refs/pr-head"
|
|
262
|
+
|
|
263
|
+
- name: Set up Node.js
|
|
264
|
+
if: steps.pr.outputs.found == 'true'
|
|
265
|
+
uses: actions/setup-node@v4
|
|
266
|
+
with:
|
|
267
|
+
node-version: "20"
|
|
268
|
+
|
|
269
|
+
- name: Verify safety gate from base
|
|
270
|
+
if: steps.pr.outputs.found == 'true'
|
|
271
|
+
run: |
|
|
272
|
+
npx --yes jules-orchestrator-kit check --mode committed --base "${base}"
|
|
273
|
+
|
|
274
|
+
- name: Squash and merge pull request
|
|
275
|
+
if: steps.pr.outputs.found == 'true'
|
|
276
|
+
run: |
|
|
277
|
+
gh pr merge --squash --delete-branch "\${{ steps.pr.outputs.number }}" --repo "$REPO"
|
|
278
|
+
`;
|
|
279
|
+
|
|
280
|
+
return { file: ".github/workflows/agent-automerge.yml", content };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Write the generated workflow(s), refusing to clobber existing files unless
|
|
153
285
|
* asked.
|
|
154
286
|
*
|
|
155
287
|
* @param {string} root
|
|
156
|
-
* @param {object} [opts] - see {@link buildCiWorkflow}, plus `force` and `
|
|
157
|
-
* @returns {{ ok: boolean, file?: string, written?: boolean, stack?: string, setupSummary?: string, content?: string, error?: string }}
|
|
288
|
+
* @param {object} [opts] - see {@link buildCiWorkflow}, plus `force`, `dryRun`, and `withAutomerge`
|
|
289
|
+
* @returns {{ ok: boolean, file?: string, files?: string[], written?: boolean, stack?: string, setupSummary?: string, content?: string, error?: string }}
|
|
158
290
|
*/
|
|
159
291
|
export function writeCiWorkflow(root, opts = {}) {
|
|
292
|
+
const target = CI_TARGETS.includes(opts.target) ? opts.target : (opts.target ? opts.target : "github");
|
|
160
293
|
if (opts.target && !CI_TARGETS.includes(opts.target)) {
|
|
161
294
|
return { ok: false, error: `Unknown CI target '${opts.target}'. Choose one of: ${CI_TARGETS.join(", ")}` };
|
|
162
295
|
}
|
|
163
296
|
|
|
297
|
+
const withAutomerge = Boolean(opts.withAutomerge || opts["with-automerge"]);
|
|
298
|
+
if (withAutomerge && target !== "github") {
|
|
299
|
+
return { ok: false, error: "Auto-merge workflow is currently only supported for GitHub Actions." };
|
|
300
|
+
}
|
|
301
|
+
|
|
164
302
|
const built = buildCiWorkflow(opts);
|
|
165
303
|
const stackId = opts.stack?.stack || "unknown";
|
|
166
|
-
const
|
|
304
|
+
const itemsToWrite = [built];
|
|
167
305
|
|
|
168
|
-
if (
|
|
169
|
-
|
|
306
|
+
if (withAutomerge) {
|
|
307
|
+
itemsToWrite.push(buildAutoMergeWorkflow(opts));
|
|
170
308
|
}
|
|
171
309
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
identical = readFileSync(abs, "utf-8") === built.content;
|
|
176
|
-
} catch (_) {
|
|
177
|
-
// Unreadable target: treat as different so the caller is told to --force
|
|
178
|
-
// rather than being told nothing changed.
|
|
179
|
-
}
|
|
310
|
+
const files = itemsToWrite.map((item) => item.file);
|
|
311
|
+
|
|
312
|
+
if (opts.dryRun) {
|
|
180
313
|
return {
|
|
181
314
|
ok: true,
|
|
182
315
|
file: built.file,
|
|
316
|
+
files,
|
|
183
317
|
written: false,
|
|
184
|
-
identical,
|
|
185
318
|
stack: stackId,
|
|
186
319
|
setupSummary: built.setupSummary,
|
|
187
320
|
content: built.content,
|
|
321
|
+
workflows: itemsToWrite,
|
|
188
322
|
};
|
|
189
323
|
}
|
|
190
324
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
325
|
+
let allWritten = true;
|
|
326
|
+
let allIdentical = true;
|
|
327
|
+
for (const item of itemsToWrite) {
|
|
328
|
+
const abs = join(root, item.file);
|
|
329
|
+
if (existsSync(abs) && !opts.force) {
|
|
330
|
+
let identical = false;
|
|
331
|
+
try {
|
|
332
|
+
identical = readFileSync(abs, "utf-8") === item.content;
|
|
333
|
+
} catch (_) {
|
|
334
|
+
// Unreadable target: treat as different so the caller is told to --force
|
|
335
|
+
// rather than being told nothing changed.
|
|
336
|
+
}
|
|
337
|
+
if (!identical) {
|
|
338
|
+
allIdentical = false;
|
|
339
|
+
}
|
|
340
|
+
allWritten = false;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
346
|
+
writeFileSync(abs, item.content, "utf-8");
|
|
347
|
+
} catch (err) {
|
|
348
|
+
return { ok: false, error: `Could not write ${item.file}: ${err.message}` };
|
|
349
|
+
}
|
|
196
350
|
}
|
|
197
351
|
|
|
198
|
-
return {
|
|
352
|
+
return {
|
|
353
|
+
ok: true,
|
|
354
|
+
file: built.file,
|
|
355
|
+
files,
|
|
356
|
+
written: allWritten,
|
|
357
|
+
identical: allWritten ? false : allIdentical,
|
|
358
|
+
stack: stackId,
|
|
359
|
+
setupSummary: built.setupSummary,
|
|
360
|
+
content: built.content,
|
|
361
|
+
};
|
|
199
362
|
}
|
package/src/config.mjs
CHANGED
|
@@ -34,7 +34,7 @@ const DEFAULTS = {
|
|
|
34
34
|
// use: an identical exfiltration job placed in `.gitlab-ci.yml` was approved
|
|
35
35
|
// where `.github/workflows/x.yml` was rejected. A safety gate that is only
|
|
36
36
|
// safe on GitHub is not a safety gate.
|
|
37
|
-
const CI_DEFINITIONS = [
|
|
37
|
+
export const CI_DEFINITIONS = [
|
|
38
38
|
".github/**",
|
|
39
39
|
".gitlab-ci.yml",
|
|
40
40
|
"**/.gitlab-ci.yml",
|
|
@@ -1019,9 +1019,11 @@ export function resolveTrustedPolicy(root = process.cwd(), baseRef = null, mode
|
|
|
1019
1019
|
|
|
1020
1020
|
let trustedConfigRaw = null;
|
|
1021
1021
|
let trustedJulesRaw = null;
|
|
1022
|
+
let trustedProtectedRaw = null;
|
|
1022
1023
|
try {
|
|
1023
1024
|
trustedConfigRaw = showFromOrigin(root, base, ".agent/config.yml");
|
|
1024
1025
|
trustedJulesRaw = showFromOrigin(root, base, ".agent/jules.yml");
|
|
1026
|
+
trustedProtectedRaw = showFromOrigin(root, base, ".agent/protected-paths.json");
|
|
1025
1027
|
} catch (_) {
|
|
1026
1028
|
// If base branch cannot be resolved or show fails, let changedFiles report code 1
|
|
1027
1029
|
}
|
|
@@ -1035,7 +1037,21 @@ export function resolveTrustedPolicy(root = process.cwd(), baseRef = null, mode
|
|
|
1035
1037
|
if (raw) parsed = parseYaml(raw) || {};
|
|
1036
1038
|
} catch (_) {}
|
|
1037
1039
|
|
|
1040
|
+
let trustedProtectedPaths = [];
|
|
1041
|
+
if (trustedProtectedRaw) {
|
|
1042
|
+
try {
|
|
1043
|
+
const parsedProtected = JSON.parse(trustedProtectedRaw);
|
|
1044
|
+
if (Array.isArray(parsedProtected.protected)) {
|
|
1045
|
+
trustedProtectedPaths = parsedProtected.protected.filter((p) => typeof p === "string" && p);
|
|
1046
|
+
}
|
|
1047
|
+
} catch (_) {}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1038
1050
|
const trustedScope = normalizeScope(parsed);
|
|
1051
|
+
if (trustedProtectedPaths.length > 0) {
|
|
1052
|
+
trustedScope.protect = dedupe([...trustedScope.protect, ...trustedProtectedPaths]);
|
|
1053
|
+
trustedScope.protectedPatterns = trustedProtectedPaths;
|
|
1054
|
+
}
|
|
1039
1055
|
const trustedDiffKb = Number(parsed.limits?.diff_kb || parsed.limits?.diffKb) || 75;
|
|
1040
1056
|
|
|
1041
1057
|
const rawSetup = parsed.setup_cmd ?? parsed.verify?.setup;
|
|
@@ -1100,6 +1116,7 @@ export function resolveTrustedPolicy(root = process.cwd(), baseRef = null, mode
|
|
|
1100
1116
|
// Inspect any proposed scaffold in this change (F06)
|
|
1101
1117
|
let proposedConfig = null;
|
|
1102
1118
|
let proposedJules = null;
|
|
1119
|
+
let proposedProtectedPaths = [];
|
|
1103
1120
|
|
|
1104
1121
|
try {
|
|
1105
1122
|
if (mode === "committed") {
|
|
@@ -1107,11 +1124,29 @@ export function resolveTrustedPolicy(root = process.cwd(), baseRef = null, mode
|
|
|
1107
1124
|
if (c) proposedConfig = parseYaml(c);
|
|
1108
1125
|
const j = showFromOrigin(root, "HEAD", ".agent/jules.yml");
|
|
1109
1126
|
if (j) proposedJules = parseYaml(j);
|
|
1127
|
+
const p = showFromOrigin(root, "HEAD", ".agent/protected-paths.json");
|
|
1128
|
+
if (p) {
|
|
1129
|
+
try {
|
|
1130
|
+
const parsedP = JSON.parse(p);
|
|
1131
|
+
if (Array.isArray(parsedP.protected)) {
|
|
1132
|
+
proposedProtectedPaths = parsedP.protected.filter((x) => typeof x === "string" && x);
|
|
1133
|
+
}
|
|
1134
|
+
} catch (_) {}
|
|
1135
|
+
}
|
|
1110
1136
|
} else {
|
|
1111
1137
|
const configPath = join(root, ".agent/config.yml");
|
|
1112
1138
|
if (existsSync(configPath)) proposedConfig = parseYaml(readFileSync(configPath, "utf-8"));
|
|
1113
1139
|
const julesPath = join(root, ".agent/jules.yml");
|
|
1114
1140
|
if (existsSync(julesPath)) proposedJules = parseYaml(readFileSync(julesPath, "utf-8"));
|
|
1141
|
+
const protectedPath = join(root, ".agent/protected-paths.json");
|
|
1142
|
+
if (existsSync(protectedPath)) {
|
|
1143
|
+
try {
|
|
1144
|
+
const parsedP = JSON.parse(readFileSync(protectedPath, "utf-8"));
|
|
1145
|
+
if (Array.isArray(parsedP.protected)) {
|
|
1146
|
+
proposedProtectedPaths = parsedP.protected.filter((x) => typeof x === "string" && x);
|
|
1147
|
+
}
|
|
1148
|
+
} catch (_) {}
|
|
1149
|
+
}
|
|
1115
1150
|
}
|
|
1116
1151
|
} catch (_) {}
|
|
1117
1152
|
|
|
@@ -1182,6 +1217,10 @@ export function resolveTrustedPolicy(root = process.cwd(), baseRef = null, mode
|
|
|
1182
1217
|
|
|
1183
1218
|
const stages = explicitStages ?? profilePlan?.stages ?? null;
|
|
1184
1219
|
const trustedScope = normalizeScope(pConfig);
|
|
1220
|
+
if (proposedProtectedPaths.length > 0) {
|
|
1221
|
+
trustedScope.protect = dedupe([...trustedScope.protect, ...proposedProtectedPaths]);
|
|
1222
|
+
trustedScope.protectedPatterns = proposedProtectedPaths;
|
|
1223
|
+
}
|
|
1185
1224
|
const trustedLimits = {
|
|
1186
1225
|
diffKb: Number(pConfig.limits?.diff_kb || pConfig.limits?.diffKb) || 75,
|
|
1187
1226
|
};
|