agent-merge-broker 0.5.0 → 0.6.0
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 +54 -18
- package/dist/broker.d.ts +41 -1
- package/dist/broker.d.ts.map +1 -1
- package/dist/broker.js +241 -6
- package/dist/broker.js.map +1 -1
- package/dist/cli.js +120 -13
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +33 -2
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/provenance.d.ts +12 -0
- package/dist/provenance.d.ts.map +1 -1
- package/dist/provenance.js +104 -0
- package/dist/provenance.js.map +1 -1
- package/dist/publisher.d.ts +3 -3
- package/dist/publisher.js +3 -3
- package/dist/serve-log.d.ts +67 -0
- package/dist/serve-log.d.ts.map +1 -0
- package/dist/serve-log.js +62 -0
- package/dist/serve-log.js.map +1 -0
- package/dist/service.d.ts +64 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +200 -0
- package/dist/service.js.map +1 -0
- package/dist/store.d.ts +16 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +73 -0
- package/dist/store.js.map +1 -1
- package/dist/types.d.ts +18 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +1 -1
- package/dist/validation.js.map +1 -1
- package/dist/verify.d.ts +8 -0
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +131 -42
- package/dist/verify.js.map +1 -1
- package/docs/ARCHITECTURE.md +26 -11
- package/docs/PROTOCOL.md +14 -4
- package/docs/RELEASING.md +17 -8
- package/docs/SECURITY.md +31 -6
- package/package.json +2 -2
- package/schemas/config.schema.json +18 -2
- package/schemas/provenance.schema.json +11 -1
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ It is deliberately **not** an agent framework and **not** a replacement for prot
|
|
|
45
45
|
|
|
46
46
|
`0.3.0` was the first public release: the local broker core, the GitHub CLI publishing adapter with auto-merge, and the remote provenance verifier.
|
|
47
47
|
|
|
48
|
-
`0.
|
|
48
|
+
`0.6.0` is current. Batch manifests are authenticated with a repository Ed25519 identity, post-assembly branch mutation is rejected, interrupted integration state recovers without rebuilding tasks, and the integration loop can run as a supervised per-user service.
|
|
49
49
|
|
|
50
50
|
The on-disk state, receipt, and provenance formats are versioned, but compatibility is not guaranteed until `1.0.0`. Expect format migrations before then.
|
|
51
51
|
|
|
@@ -80,7 +80,10 @@ npm link
|
|
|
80
80
|
- `.merge-broker/config.json` — repository policy and commands
|
|
81
81
|
- `.merge-broker/agent-instructions.md` — a reusable worker contract
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
It also creates an Ed25519 provenance private key, mode `0600`, under Git's common runtime directory.
|
|
84
|
+
Only its public key is written to the committed configuration. Runtime state, receipt records,
|
|
85
|
+
manifests, keys, locks, and integration worktrees therefore stay outside commits while every linked
|
|
86
|
+
worktree sees the same broker authority.
|
|
84
87
|
|
|
85
88
|
## Quick start
|
|
86
89
|
|
|
@@ -88,6 +91,7 @@ Initialize an existing Git repository and edit its generated configuration:
|
|
|
88
91
|
|
|
89
92
|
```bash
|
|
90
93
|
merge-broker init --base main --base-ref origin/main --remote origin
|
|
94
|
+
git add .merge-broker && git commit -m 'Configure authenticated merge brokerage'
|
|
91
95
|
merge-broker doctor
|
|
92
96
|
```
|
|
93
97
|
|
|
@@ -178,29 +182,49 @@ This sets `core.hooksPath`, so it refuses to run when the repository already has
|
|
|
178
182
|
stop working, and `--uninstall` puts everything back. `MERGE_BROKER_ALLOW_DIRECT_PUSH=1` is the
|
|
179
183
|
deliberate emergency bypass.
|
|
180
184
|
|
|
181
|
-
|
|
182
|
-
|
|
185
|
+
### Publishing without a terminal
|
|
186
|
+
|
|
187
|
+
`serve` polls for verified batches and publishes them, but only while somebody keeps it running in a
|
|
188
|
+
terminal. When nobody does, a submitted task sits in `submitted` indefinitely — and to the agent
|
|
189
|
+
that submitted it, waiting forever is indistinguishable from being rejected. Install the loop as a
|
|
190
|
+
per-user service instead:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
merge-broker install-service
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
This writes a launchd agent on macOS or a systemd user unit on Linux, one per repository, and starts
|
|
197
|
+
it. It is deliberately a *user* service on both platforms: a system daemon would need root and would
|
|
198
|
+
run as the wrong user for the repository's SSH and forge credentials. `--uninstall` removes it, and
|
|
199
|
+
the service writes to `$(git rev-parse --git-common-dir)/merge-broker/serve.log`, including when it
|
|
200
|
+
was installed from a linked worktree whose `.git` is a file.
|
|
201
|
+
|
|
202
|
+
A remote gate authenticates a pull request as an immutable broker batch. It reads the trusted public
|
|
203
|
+
key from the protected base branch, so it can reject unsigned, forged, or post-assembly work before
|
|
204
|
+
installing dependencies:
|
|
183
205
|
|
|
184
206
|
```yaml
|
|
185
207
|
- uses: actions/checkout@v4
|
|
186
208
|
with:
|
|
187
209
|
ref: ${{ github.event.pull_request.head.sha }}
|
|
188
210
|
fetch-depth: 0
|
|
189
|
-
- uses: WeSpitfire/agent-merge-broker/verify@
|
|
211
|
+
- uses: WeSpitfire/agent-merge-broker/verify@v0.6.0
|
|
190
212
|
```
|
|
191
213
|
|
|
192
|
-
The check
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
214
|
+
The check verifies the Ed25519 signature, branch and batch identity, real base history, one-file
|
|
215
|
+
manifest commit, integrated diff, submitted commit trail, and recorded validation results. The
|
|
216
|
+
provenance commit must remain the branch head. Even a normal base-update merge can carry arbitrary
|
|
217
|
+
conflict resolution, so any post-assembly merge is rejected; re-cut a stale batch with `batch
|
|
218
|
+
refresh` instead.
|
|
197
219
|
|
|
198
220
|
Verification policy is read from the configuration committed on the *base* branch, never from the
|
|
199
|
-
change under review.
|
|
221
|
+
change under review. Repositories initialized before `0.6.0` must run `merge-broker provenance
|
|
222
|
+
setup-signing`, commit the resulting public-key policy, and keep the private key outside the working
|
|
223
|
+
tree. Until then verification reports structural-only rather than authenticated provenance.
|
|
200
224
|
|
|
201
225
|
## Automatic merging
|
|
202
226
|
|
|
203
|
-
With `publish.mode` set to `pull-request` and `publish.autoMerge` enabled, the broker enables GitHub auto-merge on each published batch. GitHub lands the pull request once required status checks pass
|
|
227
|
+
With `publish.mode` set to `pull-request` and `publish.autoMerge` enabled, the broker enables GitHub auto-merge on each published batch. GitHub lands the pull request once required status checks pass. The broker never pushes to the base branch itself, so branch protection remains the authority on what may merge. If the base moves first, re-cut the batch; do not use GitHub's update-branch merge because immutable provenance deliberately rejects it.
|
|
204
228
|
|
|
205
229
|
Two configuration combinations cannot work and are rejected at load time rather than stalling silently:
|
|
206
230
|
|
|
@@ -250,7 +274,13 @@ forge that fails halfway leaves a published batch carrying a `publishWarning` ra
|
|
|
250
274
|
whose pull request exists but whose state does not admit it. Running `batch publish` again finds the
|
|
251
275
|
existing pull request and retries what is left.
|
|
252
276
|
|
|
253
|
-
A process that dies mid-integration
|
|
277
|
+
A process that dies mid-integration can leave both a lock and durable `running` state. A holder on
|
|
278
|
+
this machine is reclaimed automatically once its process is gone; a holder on another machine cannot
|
|
279
|
+
be probed and waits out the stale window. Once the integration lock is safely acquired, `serve` and
|
|
280
|
+
`integrate` automatically mark the abandoned batch failed, clean its broker-owned worktree and
|
|
281
|
+
branch, and return its tasks to `submitted` without spending their attempt budget. `merge-broker
|
|
282
|
+
recover` performs that reconciliation explicitly. `unlock --force` remains only for an owner that
|
|
283
|
+
cannot be proven gone and must follow confirming no integration is active.
|
|
254
284
|
|
|
255
285
|
## Housekeeping
|
|
256
286
|
|
|
@@ -298,7 +328,9 @@ The generated `.merge-broker/config.json` is intentionally explicit and reviewab
|
|
|
298
328
|
"maxAttempts": 3,
|
|
299
329
|
"provenance": {
|
|
300
330
|
"enabled": true,
|
|
301
|
-
"directory": ".merge-broker/attestations"
|
|
331
|
+
"directory": ".merge-broker/attestations",
|
|
332
|
+
"requireSignature": true,
|
|
333
|
+
"publicKey": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n"
|
|
302
334
|
}
|
|
303
335
|
},
|
|
304
336
|
"validation": {
|
|
@@ -330,8 +362,9 @@ The generated `.merge-broker/config.json` is intentionally explicit and reviewab
|
|
|
330
362
|
Validator commands run inside the isolated integration worktree, under `/bin/sh` unless
|
|
331
363
|
`validation.shell` names another interpreter. The shell is deliberately fixed and is never a login
|
|
332
364
|
shell: an integration decision must not depend on whose machine assembled the batch. The environment
|
|
333
|
-
is inherited from the process that invoked the broker, minus `MERGE_BROKER_TOKEN`,
|
|
334
|
-
|
|
365
|
+
is inherited from the process that invoked the broker, minus `MERGE_BROKER_TOKEN`,
|
|
366
|
+
`MERGE_BROKER_SIGNING_KEY`, and `MERGE_BROKER_SIGNING_KEY_FILE`, so PATH and toolchain managers work
|
|
367
|
+
while broker credentials stay out of repository-defined commands.
|
|
335
368
|
|
|
336
369
|
Validators receive these environment variables:
|
|
337
370
|
|
|
@@ -353,8 +386,8 @@ The JSON schemas in [`schemas/`](schemas/) can be used by editors, adapters, and
|
|
|
353
386
|
### One authoritative CI pass
|
|
354
387
|
|
|
355
388
|
Repositories that make GitHub the authoritative validator can leave broker
|
|
356
|
-
authoritative validators empty,
|
|
357
|
-
without
|
|
389
|
+
authoritative validators empty, require signed provenance, and reject any PR
|
|
390
|
+
without an authenticated broker manifest before installing dependencies. The full lint,
|
|
358
391
|
type, test, and build suite then runs exactly once on the assembled broker PR.
|
|
359
392
|
Task worktrees retain only fast changed-scope feedback, while deployment builds
|
|
360
393
|
the already-checked revision without repeating the whole suite.
|
|
@@ -364,7 +397,9 @@ the already-checked revision without repeating the whole suite.
|
|
|
364
397
|
```text
|
|
365
398
|
merge-broker init
|
|
366
399
|
merge-broker doctor
|
|
400
|
+
merge-broker provenance setup-signing [--private-key <path>] [--rotate]
|
|
367
401
|
merge-broker install-hooks [--force] [--uninstall]
|
|
402
|
+
merge-broker install-service [--uninstall] [--interval <seconds>] [--no-eager]
|
|
368
403
|
merge-broker verify-provenance --branch <ref> --head <sha> --base <sha>
|
|
369
404
|
merge-broker validate [--task <id>] [--scope focused|authoritative|all] [--base <ref>] [--cwd <path>]
|
|
370
405
|
merge-broker task register|claim|extend|heartbeat|submit|retry|release|cancel|show
|
|
@@ -377,6 +412,7 @@ merge-broker metrics
|
|
|
377
412
|
merge-broker events
|
|
378
413
|
merge-broker prune [--older-than <days>] [--dry-run]
|
|
379
414
|
merge-broker unlock [state|integration] [--force]
|
|
415
|
+
merge-broker recover
|
|
380
416
|
merge-broker serve [--publish] [--eager]
|
|
381
417
|
```
|
|
382
418
|
|
package/dist/broker.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { GitRepository } from "./git.js";
|
|
2
2
|
import { StateStore, type LockStatus } from "./store.js";
|
|
3
3
|
import { type HookInstallation } from "./hooks.js";
|
|
4
|
-
import
|
|
4
|
+
import { type ServiceInstallation } from "./service.js";
|
|
5
|
+
import type { BatchRecord, BrokerConfig, BrokerState, IntegrationOptions, IntegrationResult, LocalValidationResult, RefreshResult, RecoveryResult, PruneOptions, PruneResult, SchedulePlan, TaskRecord } from "./types.js";
|
|
5
6
|
export interface RegisterTaskInput {
|
|
6
7
|
id: string;
|
|
7
8
|
title?: string;
|
|
@@ -35,6 +36,19 @@ export declare class MergeBroker {
|
|
|
35
36
|
configPath: string;
|
|
36
37
|
created: boolean;
|
|
37
38
|
}>;
|
|
39
|
+
/**
|
|
40
|
+
* Enables authenticated provenance for an existing repository or rotates/imports its identity.
|
|
41
|
+
* The private key stays in Git's runtime state; configuration receives only the public key.
|
|
42
|
+
*/
|
|
43
|
+
setupProvenanceSigning(options?: {
|
|
44
|
+
privateKeyFile?: string;
|
|
45
|
+
rotate?: boolean;
|
|
46
|
+
}): Promise<{
|
|
47
|
+
publicKey: string;
|
|
48
|
+
keyId: string;
|
|
49
|
+
keyPath: string;
|
|
50
|
+
}>;
|
|
51
|
+
private provenanceSigningPrivateKey;
|
|
38
52
|
registerTask(input: RegisterTaskInput): Promise<TaskRecord>;
|
|
39
53
|
claimTask(input: ClaimTaskInput): Promise<{
|
|
40
54
|
task: TaskRecord;
|
|
@@ -81,6 +95,12 @@ export declare class MergeBroker {
|
|
|
81
95
|
cwd?: string;
|
|
82
96
|
}): Promise<LocalValidationResult>;
|
|
83
97
|
integrate(options?: IntegrationOptions): Promise<IntegrationResult>;
|
|
98
|
+
private recoverAbandonedIntegrationsLocked;
|
|
99
|
+
/**
|
|
100
|
+
* Recovers durable `running` state left by a killed integration process. Acquiring the integration
|
|
101
|
+
* lock is the proof that the former process is no longer allowed to make progress.
|
|
102
|
+
*/
|
|
103
|
+
recoverAbandonedIntegrations(): Promise<RecoveryResult>;
|
|
84
104
|
publishBatch(id: string): Promise<BatchRecord>;
|
|
85
105
|
syncBatch(id: string): Promise<BatchRecord>;
|
|
86
106
|
syncPublishedBatches(): Promise<{
|
|
@@ -136,6 +156,26 @@ export declare class MergeBroker {
|
|
|
136
156
|
force?: boolean;
|
|
137
157
|
uninstall?: boolean;
|
|
138
158
|
}): Promise<HookInstallation>;
|
|
159
|
+
/**
|
|
160
|
+
* Installs the integration loop as a per-user service.
|
|
161
|
+
*
|
|
162
|
+
* Without it `serve` only runs while a terminal is open, so a submitted task
|
|
163
|
+
* waits for a human to notice — indistinguishable, to the agent that
|
|
164
|
+
* submitted it, from the broker rejecting the work.
|
|
165
|
+
*/
|
|
166
|
+
installService(options?: {
|
|
167
|
+
uninstall?: boolean;
|
|
168
|
+
intervalSeconds?: number;
|
|
169
|
+
eager?: boolean;
|
|
170
|
+
nodePath?: string;
|
|
171
|
+
cliPath?: string;
|
|
172
|
+
pathEntries?: string[];
|
|
173
|
+
logFile?: string;
|
|
174
|
+
}): Promise<ServiceInstallation | {
|
|
175
|
+
name: string;
|
|
176
|
+
file: string;
|
|
177
|
+
removed: boolean;
|
|
178
|
+
}>;
|
|
139
179
|
inspectLocks(): Promise<LockStatus[]>;
|
|
140
180
|
releaseLock(name: string, options?: {
|
|
141
181
|
force?: boolean;
|
package/dist/broker.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"broker.d.ts","sourceRoot":"","sources":["../src/broker.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"broker.d.ts","sourceRoot":"","sources":["../src/broker.ts"],"names":[],"mappings":"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"}
|
package/dist/broker.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
3
4
|
import { BrokerError, CommandError, ValidationError } from "./errors.js";
|
|
4
5
|
import { GitRepository } from "./git.js";
|
|
5
|
-
import { initializeConfig, loadConfig } from "./config.js";
|
|
6
|
+
import { initializeConfig, loadConfig, writeConfig } from "./config.js";
|
|
6
7
|
import { patternSetsMayOverlap, unexpectedPaths } from "./patterns.js";
|
|
7
8
|
import { scheduleTasks } from "./scheduler.js";
|
|
8
9
|
import { StateStore } from "./store.js";
|
|
9
10
|
import { runValidators } from "./validation.js";
|
|
10
11
|
import { closePullRequest, enableAutoMerge, inspectPullRequest, publishBatch as publishPreparedBatch, } from "./publisher.js";
|
|
11
|
-
import { buildBatchProvenance, provenancePath } from "./provenance.js";
|
|
12
|
+
import { buildBatchProvenance, provenanceKeyId, provenancePath, publicKeyFromPrivate, signBatchProvenance, } from "./provenance.js";
|
|
12
13
|
import { installHooks, uninstallHooks } from "./hooks.js";
|
|
14
|
+
import { installService, uninstallService, } from "./service.js";
|
|
13
15
|
function now() {
|
|
14
16
|
return new Date().toISOString();
|
|
15
17
|
}
|
|
@@ -189,8 +191,62 @@ export class MergeBroker {
|
|
|
189
191
|
const initialized = await initializeConfig(repo.root, options);
|
|
190
192
|
const store = new StateStore(repo.commonGitDir, initialized.config.stateDirectory, initialized.config.leases.lockTimeoutSeconds);
|
|
191
193
|
await store.initialize();
|
|
194
|
+
const provenance = initialized.config.integration.provenance;
|
|
195
|
+
if (initialized.created && provenance?.enabled) {
|
|
196
|
+
const identity = await store.provisionProvenanceSigningKey();
|
|
197
|
+
provenance.requireSignature = true;
|
|
198
|
+
provenance.publicKey = identity.publicKey;
|
|
199
|
+
await writeConfig(repo.root, initialized.config);
|
|
200
|
+
}
|
|
192
201
|
return { repoRoot: repo.root, configPath: initialized.path, created: initialized.created };
|
|
193
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Enables authenticated provenance for an existing repository or rotates/imports its identity.
|
|
205
|
+
* The private key stays in Git's runtime state; configuration receives only the public key.
|
|
206
|
+
*/
|
|
207
|
+
async setupProvenanceSigning(options = {}) {
|
|
208
|
+
const privateKey = options.privateKeyFile
|
|
209
|
+
? await readFile(path.resolve(options.privateKeyFile), "utf8")
|
|
210
|
+
: undefined;
|
|
211
|
+
const identity = await this.store.provisionProvenanceSigningKey({
|
|
212
|
+
...(privateKey ? { privateKey } : {}),
|
|
213
|
+
rotate: options.rotate ?? false,
|
|
214
|
+
});
|
|
215
|
+
this.config.integration.provenance ??= {
|
|
216
|
+
enabled: true,
|
|
217
|
+
directory: ".merge-broker/attestations",
|
|
218
|
+
};
|
|
219
|
+
this.config.integration.provenance.enabled = true;
|
|
220
|
+
this.config.integration.provenance.requireSignature = true;
|
|
221
|
+
this.config.integration.provenance.publicKey = identity.publicKey;
|
|
222
|
+
await writeConfig(this.repo.root, this.config);
|
|
223
|
+
return identity;
|
|
224
|
+
}
|
|
225
|
+
async provenanceSigningPrivateKey() {
|
|
226
|
+
const provenance = this.config.integration.provenance;
|
|
227
|
+
if (!provenance?.enabled || !provenance.publicKey) {
|
|
228
|
+
if (provenance?.requireSignature) {
|
|
229
|
+
throw new BrokerError("SIGNING_KEY_REQUIRED", "Signed provenance is required, but configuration has no trusted public key. Run `merge-broker provenance setup-signing`.");
|
|
230
|
+
}
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
const fromEnvironment = process.env.MERGE_BROKER_SIGNING_KEY;
|
|
234
|
+
const fromFile = process.env.MERGE_BROKER_SIGNING_KEY_FILE
|
|
235
|
+
? await readFile(path.resolve(process.env.MERGE_BROKER_SIGNING_KEY_FILE), "utf8")
|
|
236
|
+
: undefined;
|
|
237
|
+
const privateKey = fromEnvironment ?? fromFile ?? (await this.store.readProvenanceSigningKey(provenance.publicKey));
|
|
238
|
+
if (!privateKey) {
|
|
239
|
+
if (!provenance.requireSignature)
|
|
240
|
+
return undefined;
|
|
241
|
+
throw new BrokerError("SIGNING_KEY_REQUIRED", `Signed provenance is required, but the private key is unavailable. Restore ${this.store.provenanceSigningKeyFile}, set MERGE_BROKER_SIGNING_KEY_FILE, or import it with \`merge-broker provenance setup-signing --private-key <path>\`.`);
|
|
242
|
+
}
|
|
243
|
+
const expectedKeyId = provenanceKeyId(provenance.publicKey);
|
|
244
|
+
const actualKeyId = provenanceKeyId(publicKeyFromPrivate(privateKey));
|
|
245
|
+
if (expectedKeyId !== actualKeyId) {
|
|
246
|
+
throw new BrokerError("SIGNING_KEY_MISMATCH", "The available provenance private key does not match the public key committed in configuration.", { expectedKeyId, actualKeyId });
|
|
247
|
+
}
|
|
248
|
+
return privateKey;
|
|
249
|
+
}
|
|
194
250
|
async registerTask(input) {
|
|
195
251
|
assertTaskId(input.id);
|
|
196
252
|
if (input.priority !== undefined && !Number.isInteger(input.priority)) {
|
|
@@ -589,6 +645,13 @@ export class MergeBroker {
|
|
|
589
645
|
}
|
|
590
646
|
async integrate(options = {}) {
|
|
591
647
|
return await this.store.withIntegrationLock(async () => {
|
|
648
|
+
// A killed process can leave durable state at `running`/`integrating` after its lock is gone.
|
|
649
|
+
// Owning the integration lock proves nobody else can still be advancing that transaction, so
|
|
650
|
+
// recover it before planning. This also lets the recovered tasks participate immediately.
|
|
651
|
+
await this.recoverAbandonedIntegrationsLocked();
|
|
652
|
+
// Key problems are operator/configuration failures, not task failures. Resolve them before
|
|
653
|
+
// tasks move to `integrating`, so a missing key cannot spend a worker's retry budget.
|
|
654
|
+
const signingPrivateKey = await this.provenanceSigningPrivateKey();
|
|
592
655
|
// One batch in flight at a time.
|
|
593
656
|
//
|
|
594
657
|
// A batch is cut from the base branch tip so it is born mergeable. Cutting a second one while
|
|
@@ -705,13 +768,19 @@ export class MergeBroker {
|
|
|
705
768
|
const integratedHeadSha = headSha;
|
|
706
769
|
const integratedPaths = await this.repo.changedFilesBetween(baseSha, integratedHeadSha);
|
|
707
770
|
const relativePath = provenancePath(provenance.directory, id);
|
|
708
|
-
|
|
771
|
+
let record = buildBatchProvenance({
|
|
709
772
|
batch,
|
|
710
773
|
tasks: plan.selected,
|
|
711
774
|
integratedHeadSha,
|
|
712
775
|
integratedPaths,
|
|
713
776
|
history: this.config.integration.history,
|
|
714
777
|
});
|
|
778
|
+
if (signingPrivateKey && provenance.publicKey) {
|
|
779
|
+
record = signBatchProvenance(record, signingPrivateKey, provenance.publicKey);
|
|
780
|
+
}
|
|
781
|
+
else if (provenance.requireSignature) {
|
|
782
|
+
throw new BrokerError("SIGNING_KEY_REQUIRED", "Signed provenance is required for this batch.");
|
|
783
|
+
}
|
|
715
784
|
headSha = await this.repo.commitGeneratedFile(worktree, relativePath, `${JSON.stringify(record, null, 2)}\n`, `Record Merge Broker batch ${id}`);
|
|
716
785
|
batch.integratedHeadSha = integratedHeadSha;
|
|
717
786
|
batch.provenancePath = relativePath;
|
|
@@ -837,6 +906,104 @@ export class MergeBroker {
|
|
|
837
906
|
};
|
|
838
907
|
});
|
|
839
908
|
}
|
|
909
|
+
async recoverAbandonedIntegrationsLocked() {
|
|
910
|
+
const snapshot = await this.store.read();
|
|
911
|
+
const running = Object.values(snapshot.batches).filter((batch) => batch.status === "running");
|
|
912
|
+
if (running.length === 0) {
|
|
913
|
+
return {
|
|
914
|
+
batches: [],
|
|
915
|
+
tasks: [],
|
|
916
|
+
worktreesRemoved: [],
|
|
917
|
+
branchesRemoved: [],
|
|
918
|
+
cleanupWarnings: [],
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
const recovered = await this.store.transaction((state, audit) => {
|
|
922
|
+
const batches = [];
|
|
923
|
+
const tasks = [];
|
|
924
|
+
for (const candidate of running) {
|
|
925
|
+
const batch = state.batches[candidate.id];
|
|
926
|
+
if (!batch || batch.status !== "running")
|
|
927
|
+
continue;
|
|
928
|
+
batch.status = "failed";
|
|
929
|
+
batch.error = "Integration process stopped before the transaction completed; tasks were recovered for retry.";
|
|
930
|
+
batch.finishedAt = now();
|
|
931
|
+
batches.push(batch.id);
|
|
932
|
+
const requeued = [];
|
|
933
|
+
for (const taskId of batch.taskIds) {
|
|
934
|
+
const task = state.tasks[taskId];
|
|
935
|
+
if (!task || task.status !== "integrating" || task.batchId !== batch.id)
|
|
936
|
+
continue;
|
|
937
|
+
task.status = "submitted";
|
|
938
|
+
task.updatedAt = now();
|
|
939
|
+
delete task.batchId;
|
|
940
|
+
delete task.lastError;
|
|
941
|
+
tasks.push(taskId);
|
|
942
|
+
requeued.push(taskId);
|
|
943
|
+
}
|
|
944
|
+
audit("batch.recovered", {
|
|
945
|
+
batchId: batch.id,
|
|
946
|
+
details: { requeued, reason: "abandoned integration transaction" },
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
return { batches, tasks };
|
|
950
|
+
});
|
|
951
|
+
const worktreesRemoved = [];
|
|
952
|
+
const branchesRemoved = [];
|
|
953
|
+
const cleanupWarnings = [];
|
|
954
|
+
const registeredWorktrees = new Set((await this.repo.listWorktrees()).map((worktree) => path.resolve(worktree.path)));
|
|
955
|
+
const worktreeRoot = `${path.resolve(this.store.worktreesDirectory)}${path.sep}`;
|
|
956
|
+
for (const batchId of recovered.batches) {
|
|
957
|
+
const candidate = running.find((batch) => batch.id === batchId);
|
|
958
|
+
const worktree = candidate?.worktree ? path.resolve(candidate.worktree) : undefined;
|
|
959
|
+
if (worktree && registeredWorktrees.has(worktree)) {
|
|
960
|
+
if (!worktree.startsWith(worktreeRoot)) {
|
|
961
|
+
cleanupWarnings.push(`Refused to remove recovery worktree outside broker state: ${worktree}`);
|
|
962
|
+
}
|
|
963
|
+
else {
|
|
964
|
+
try {
|
|
965
|
+
await this.repo.removeWorktree(worktree);
|
|
966
|
+
worktreesRemoved.push(worktree);
|
|
967
|
+
}
|
|
968
|
+
catch (error) {
|
|
969
|
+
cleanupWarnings.push(errorMessage(error));
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
const branchName = cleanBranchFragment(`${this.config.integration.branchPrefix}${batchId}`);
|
|
974
|
+
const branch = await this.repo.git(["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], this.repo.root, true);
|
|
975
|
+
if (branch.exitCode === 0) {
|
|
976
|
+
const deleted = await this.repo.git(["branch", "-D", "--", branchName], this.repo.root, true);
|
|
977
|
+
if (deleted.exitCode === 0)
|
|
978
|
+
branchesRemoved.push(branchName);
|
|
979
|
+
else
|
|
980
|
+
cleanupWarnings.push(`Could not remove recovery branch ${branchName}: ${deleted.stderr.trim()}`);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
await this.store.transaction((state, audit) => {
|
|
984
|
+
for (const batchId of recovered.batches) {
|
|
985
|
+
const batch = state.batches[batchId];
|
|
986
|
+
if (!batch)
|
|
987
|
+
continue;
|
|
988
|
+
if (worktreesRemoved.includes(path.resolve(batch.worktree ?? "")))
|
|
989
|
+
delete batch.worktree;
|
|
990
|
+
if (cleanupWarnings.length > 0) {
|
|
991
|
+
batch.error = `${batch.error ?? "Integration was recovered."} Cleanup: ${cleanupWarnings.join("; ")}`;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
audit("integration.recovery_completed", {
|
|
995
|
+
details: { ...recovered, worktreesRemoved, branchesRemoved, cleanupWarnings },
|
|
996
|
+
});
|
|
997
|
+
});
|
|
998
|
+
return { ...recovered, worktreesRemoved, branchesRemoved, cleanupWarnings };
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Recovers durable `running` state left by a killed integration process. Acquiring the integration
|
|
1002
|
+
* lock is the proof that the former process is no longer allowed to make progress.
|
|
1003
|
+
*/
|
|
1004
|
+
async recoverAbandonedIntegrations() {
|
|
1005
|
+
return await this.store.withIntegrationLock(async () => await this.recoverAbandonedIntegrationsLocked());
|
|
1006
|
+
}
|
|
840
1007
|
async publishBatch(id) {
|
|
841
1008
|
const state = await this.store.read();
|
|
842
1009
|
const batch = requireBatch(state, id);
|
|
@@ -1045,6 +1212,9 @@ export class MergeBroker {
|
|
|
1045
1212
|
let pullRequestClosed;
|
|
1046
1213
|
if (batch.pullRequestUrl) {
|
|
1047
1214
|
pullRequestClosed = await closePullRequest(this.repo.root, batch.pullRequestUrl, `Superseded: the base branch moved to ${currentBase.slice(0, 7)}, so this batch was re-cut from the current tip.`);
|
|
1215
|
+
if (!pullRequestClosed) {
|
|
1216
|
+
throw new BrokerError("PULL_REQUEST_CLOSE_FAILED", `Could not close superseded pull request ${batch.pullRequestUrl}. The existing batch and its tasks were left unchanged so two mergeable copies cannot exist. Retry when the forge responds.`, { batchId: id, pullRequestUrl: batch.pullRequestUrl });
|
|
1217
|
+
}
|
|
1048
1218
|
}
|
|
1049
1219
|
if (batch.branchName)
|
|
1050
1220
|
await this.repo.deleteBranch(batch.branchName);
|
|
@@ -1188,6 +1358,43 @@ export class MergeBroker {
|
|
|
1188
1358
|
force: options.force ?? false,
|
|
1189
1359
|
});
|
|
1190
1360
|
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Installs the integration loop as a per-user service.
|
|
1363
|
+
*
|
|
1364
|
+
* Without it `serve` only runs while a terminal is open, so a submitted task
|
|
1365
|
+
* waits for a human to notice — indistinguishable, to the agent that
|
|
1366
|
+
* submitted it, from the broker rejecting the work.
|
|
1367
|
+
*/
|
|
1368
|
+
async installService(options = {}) {
|
|
1369
|
+
if (options.uninstall)
|
|
1370
|
+
return await uninstallService(this.repo.root);
|
|
1371
|
+
const nodePath = options.nodePath ?? process.execPath;
|
|
1372
|
+
const cliPath = options.cliPath ?? process.argv[1] ?? "";
|
|
1373
|
+
if (!path.isAbsolute(cliPath)) {
|
|
1374
|
+
throw new BrokerError("SERVICE_CLI_PATH", "Could not determine an absolute path to the broker CLI. Pass --cli-path.");
|
|
1375
|
+
}
|
|
1376
|
+
return await installService({
|
|
1377
|
+
repositoryRoot: this.repo.root,
|
|
1378
|
+
nodePath,
|
|
1379
|
+
cliPath,
|
|
1380
|
+
intervalSeconds: options.intervalSeconds ?? 15,
|
|
1381
|
+
eager: options.eager ?? true,
|
|
1382
|
+
// node's own directory is included because a version-managed node is not
|
|
1383
|
+
// on the default PATH a login-less agent receives.
|
|
1384
|
+
pathEntries: options.pathEntries ?? [
|
|
1385
|
+
path.dirname(nodePath),
|
|
1386
|
+
"/opt/homebrew/bin",
|
|
1387
|
+
"/usr/local/bin",
|
|
1388
|
+
"/usr/bin",
|
|
1389
|
+
"/bin",
|
|
1390
|
+
"/usr/sbin",
|
|
1391
|
+
"/sbin",
|
|
1392
|
+
],
|
|
1393
|
+
// `.git` is a file in linked worktrees. Runtime state already resolves Git's common directory,
|
|
1394
|
+
// which is the one log location every checkout can safely create and inspect.
|
|
1395
|
+
logFile: options.logFile ?? path.join(this.store.directory, "serve.log"),
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1191
1398
|
async inspectLocks() {
|
|
1192
1399
|
return await Promise.all(["state", "integration"].map(async (name) => await this.store.inspectLock(name)));
|
|
1193
1400
|
}
|
|
@@ -1240,15 +1447,40 @@ export class MergeBroker {
|
|
|
1240
1447
|
};
|
|
1241
1448
|
}
|
|
1242
1449
|
async doctor() {
|
|
1243
|
-
const [baseSha, clean, worktrees, locks] = await Promise.all([
|
|
1450
|
+
const [baseSha, clean, worktrees, locks, state] = await Promise.all([
|
|
1244
1451
|
this.repo.resolveCommit(this.config.baseRef),
|
|
1245
1452
|
this.repo.isClean(),
|
|
1246
1453
|
this.repo.listWorktrees(),
|
|
1247
1454
|
this.inspectLocks(),
|
|
1455
|
+
this.store.read(),
|
|
1248
1456
|
]);
|
|
1249
1457
|
const warnings = [];
|
|
1458
|
+
let ok = true;
|
|
1459
|
+
const provenance = this.config.integration.provenance;
|
|
1460
|
+
let signingKeyId;
|
|
1461
|
+
if (provenance?.enabled && provenance.requireSignature) {
|
|
1462
|
+
try {
|
|
1463
|
+
await this.provenanceSigningPrivateKey();
|
|
1464
|
+
if (provenance.publicKey)
|
|
1465
|
+
signingKeyId = provenanceKeyId(provenance.publicKey);
|
|
1466
|
+
}
|
|
1467
|
+
catch (error) {
|
|
1468
|
+
ok = false;
|
|
1469
|
+
warnings.push(errorMessage(error));
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
else if (provenance?.enabled) {
|
|
1473
|
+
warnings.push("Provenance signatures are not required, so remote verification can confirm structure but cannot authenticate which broker created it. Run `merge-broker provenance setup-signing`.");
|
|
1474
|
+
}
|
|
1250
1475
|
if (this.config.validation.focused.length === 0 && this.config.validation.authoritative.length === 0) {
|
|
1251
|
-
warnings.push("No validators are configured, so batches are assembled without being checked. Configure validation.authoritative
|
|
1476
|
+
warnings.push("No validators are configured, so batches are assembled without being checked. Configure validation.authoritative or require an authoritative remote CI suite on every batch PR.");
|
|
1477
|
+
}
|
|
1478
|
+
const runningBatches = Object.values(state.batches)
|
|
1479
|
+
.filter((batch) => batch.status === "running")
|
|
1480
|
+
.map((batch) => batch.id);
|
|
1481
|
+
if (runningBatches.length > 0) {
|
|
1482
|
+
ok = false;
|
|
1483
|
+
warnings.push(`Integration state is incomplete for ${runningBatches.join(", ")}. Run \`merge-broker recover\` after confirming no broker process is active.`);
|
|
1252
1484
|
}
|
|
1253
1485
|
for (const lock of locks) {
|
|
1254
1486
|
if (!lock.held)
|
|
@@ -1256,7 +1488,7 @@ export class MergeBroker {
|
|
|
1256
1488
|
warnings.push(`The ${lock.name} lock is held${lock.owner?.host ? ` by ${lock.owner.host}` : ""} and is ${Math.round((lock.ageMs ?? 0) / 1_000)}s old${lock.abandoned ? "; its owning process is gone, so \"merge-broker unlock\" can clear it" : ""}.`);
|
|
1257
1489
|
}
|
|
1258
1490
|
return {
|
|
1259
|
-
ok
|
|
1491
|
+
ok,
|
|
1260
1492
|
warnings,
|
|
1261
1493
|
locks,
|
|
1262
1494
|
repository: this.repo.root,
|
|
@@ -1271,6 +1503,9 @@ export class MergeBroker {
|
|
|
1271
1503
|
publishMode: this.config.publish.mode,
|
|
1272
1504
|
focusedValidators: this.config.validation.focused.map((validator) => validator.name),
|
|
1273
1505
|
authoritativeValidators: this.config.validation.authoritative.map((validator) => validator.name),
|
|
1506
|
+
provenanceAuthenticated: Boolean(provenance?.enabled && provenance.requireSignature && signingKeyId),
|
|
1507
|
+
provenanceKeyId: signingKeyId,
|
|
1508
|
+
runningBatches,
|
|
1274
1509
|
};
|
|
1275
1510
|
}
|
|
1276
1511
|
}
|