toolroll 0.9.77 → 0.9.79
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 +96 -0
- package/dist/agent-fence.js +18 -2
- package/dist/backup-ui.d.ts +9 -1
- package/dist/backup-ui.js +28 -3
- package/dist/backup.js +4 -3
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +18 -18
- package/dist/browser-workspace.d.ts +2 -0
- package/dist/builder.d.ts +4 -3
- package/dist/builder.js +14 -8
- package/dist/check-failure.js +31 -3
- package/dist/check-report.d.ts +18 -3
- package/dist/check-report.js +67 -7
- package/dist/claim.d.ts +9 -0
- package/dist/claim.js +9 -1
- package/dist/contracts/console-api.d.ts +1 -1
- package/dist/contracts/console-api.js +1 -1
- package/dist/contracts/release-terms.d.ts +41 -0
- package/dist/contracts/release-terms.js +53 -0
- package/dist/controller-service.js +4 -1
- package/dist/deploy-rescue.d.ts +41 -0
- package/dist/deploy-rescue.js +94 -0
- package/dist/dispose.js +4 -3
- package/dist/evidence.d.ts +9 -0
- package/dist/evidence.js +15 -3
- package/dist/exec.d.ts +2 -0
- package/dist/exec.js +15 -1
- package/dist/git-views.d.ts +1 -1
- package/dist/git-views.js +2 -2
- package/dist/guides.js +18 -8
- package/dist/latency-timeline.d.ts +46 -0
- package/dist/latency-timeline.js +120 -0
- package/dist/live-backup.d.ts +39 -0
- package/dist/live-backup.js +271 -0
- package/dist/operate.js +203 -16
- package/dist/planner.js +2 -2
- package/dist/project-knowledge.d.ts +7 -7
- package/dist/release-adapters.d.ts +41 -3
- package/dist/release-adapters.js +275 -73
- package/dist/release-carry.d.ts +21 -1
- package/dist/release-carry.js +22 -0
- package/dist/release-cli.d.ts +3 -0
- package/dist/release-cli.js +74 -5
- package/dist/release-gate.d.ts +5 -0
- package/dist/release-gate.js +1 -1
- package/dist/release-net.d.ts +90 -0
- package/dist/release-net.js +253 -0
- package/dist/release-recheck.js +3 -2
- package/dist/release.d.ts +40 -5
- package/dist/release.js +142 -52
- package/dist/runner-process.d.ts +67 -0
- package/dist/runner-process.js +219 -0
- package/dist/runner.d.ts +48 -6
- package/dist/runner.js +71 -8
- package/dist/scope.d.ts +19 -1
- package/dist/scope.js +19 -2
- package/dist/scout.js +2 -2
- package/dist/serve.js +11 -0
- package/dist/server/http.d.ts +2 -0
- package/dist/server/pages.js +7 -3
- package/dist/server/remote-hooks.d.ts +1 -1
- package/dist/server/remote-hooks.js +5 -3
- package/dist/server/render-results.d.ts +2 -0
- package/dist/server/render-results.js +1 -0
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/settings.js +22 -2
- package/dist/server/tasks.js +4 -1
- package/dist/storage-sweep.js +1 -1
- package/dist/store.d.ts +125 -2
- package/dist/store.js +250 -12
- package/dist/surface.js +3 -1
- package/dist/teardown-git.d.ts +1 -1
- package/dist/teardown-git.js +12 -12
- package/dist/toolroll-update.js +2 -2
- package/dist/up-role-child.js +6 -2
- package/dist/workspace-revision.js +3 -1
- package/package.json +1 -1
|
@@ -804,6 +804,8 @@ export type BrowserResultView = {
|
|
|
804
804
|
efficiency?: import("./run-efficiency.js").RunEfficiency | null;
|
|
805
805
|
/** What its network was and what its proxy saw (egress.ts): display only. */
|
|
806
806
|
network?: import("./egress.js").RunNetworkRecord | null;
|
|
807
|
+
/** Where the time went: its release check's parts and the task's deploys (latency-timeline.ts), display only. */
|
|
808
|
+
timing?: import("./latency-timeline.js").TimingView | null;
|
|
807
809
|
/** Words for the result's Accept: Accept and finish when offered, else the Needs you acceptance. */
|
|
808
810
|
decision: BrowserResultDecision | null;
|
|
809
811
|
checks: {
|
package/dist/builder.d.ts
CHANGED
|
@@ -174,10 +174,11 @@ export declare const REPAIR_MAX_TURNS = 4;
|
|
|
174
174
|
export declare const REPAIR_TIMEOUT_MS: number;
|
|
175
175
|
/**
|
|
176
176
|
* How often a running build says "still here" — extending its lease and its
|
|
177
|
-
* runner's liveness in one beat.
|
|
178
|
-
* window means
|
|
177
|
+
* runner's liveness in one beat. Every 15 s against a one-minute liveness
|
|
178
|
+
* window (runner.ts) means three beats can be lost to load before anything
|
|
179
|
+
* looks dead.
|
|
179
180
|
*/
|
|
180
|
-
export declare const DEFAULT_PULSE_MS =
|
|
181
|
+
export declare const DEFAULT_PULSE_MS = 15000;
|
|
181
182
|
/** Branches an unattended agent may never commit to, whatever it was asked. */
|
|
182
183
|
export declare const PROTECTED: Set<string>;
|
|
183
184
|
/**
|
package/dist/builder.js
CHANGED
|
@@ -56,12 +56,13 @@ import { witnessedRunner } from "./process-custody.js";
|
|
|
56
56
|
import { runWithIsolatedDatabase } from "./child-database.js";
|
|
57
57
|
import { recordWorktreeProcess, saveWorkPatch } from "./worktree.js";
|
|
58
58
|
import { launchSandboxOf, protectedChanges, protectedLinks } from "./agent-fence.js";
|
|
59
|
-
import { approvalOf, digestOf, idleClock, isAcpProfile, profileDigestOf, routeParityProblem, watchdogsOf } from "./scope.js";
|
|
59
|
+
import { approvalOf, digestOf, idleClock, isAcpProfile, profileDigestOf, releaseCheckScope, routeParityProblem, watchdogsOf } from "./scope.js";
|
|
60
60
|
import { legOf, routeDigestOf, routeFromJson } from "./phase-routing.js";
|
|
61
61
|
import { execFileSync } from "node:child_process";
|
|
62
62
|
import { currentClaim, heartbeat, SYNC_MAX_AGE_MS, finalize } from "./claim.js";
|
|
63
63
|
import { missingCapability } from "./dispatch.js";
|
|
64
|
-
import { heartbeat as runnerHeartbeat } from "./runner.js";
|
|
64
|
+
import { heartbeat as runnerHeartbeat, RUNNER_HEARTBEAT_MS } from "./runner.js";
|
|
65
|
+
import { keepCheckTimeline } from "./latency-timeline.js";
|
|
65
66
|
import { MARKER as LEASE_MARKER } from "./worktree.js";
|
|
66
67
|
import { parseDecision, parseHandoff, repairPrompt, HEADLESS_RULE, HANDOFF_CONCLUSION_CAP, HANDOFF_ITEM_CAP, HANDOFF_LIST_CAP, HANDOFF_PAYLOAD_CAP } from "./decision.js";
|
|
67
68
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
@@ -191,10 +192,11 @@ export const REPAIR_MAX_TURNS = 4;
|
|
|
191
192
|
export const REPAIR_TIMEOUT_MS = 5 * 60_000;
|
|
192
193
|
/**
|
|
193
194
|
* How often a running build says "still here" — extending its lease and its
|
|
194
|
-
* runner's liveness in one beat.
|
|
195
|
-
* window means
|
|
195
|
+
* runner's liveness in one beat. Every 15 s against a one-minute liveness
|
|
196
|
+
* window (runner.ts) means three beats can be lost to load before anything
|
|
197
|
+
* looks dead.
|
|
196
198
|
*/
|
|
197
|
-
export const DEFAULT_PULSE_MS =
|
|
199
|
+
export const DEFAULT_PULSE_MS = RUNNER_HEARTBEAT_MS;
|
|
198
200
|
/** Branches an unattended agent may never commit to, whatever it was asked. */
|
|
199
201
|
export const PROTECTED = new Set(["main", "master", "trunk", "develop", "release"]);
|
|
200
202
|
const GIT = "git";
|
|
@@ -378,12 +380,13 @@ export function proveApprovedProfile(scope, given) {
|
|
|
378
380
|
acceptance: scope.acceptance, candidate: scope.candidate ?? null,
|
|
379
381
|
qualityMode: scope.qualityMode ?? "default",
|
|
380
382
|
releaseTerms: scope.releaseTerms ?? null,
|
|
383
|
+
releaseCheck: scope.releaseCheck ?? null,
|
|
381
384
|
}, snapshot,
|
|
382
385
|
// v47: the SEALED route is part of the signed bytes whenever it
|
|
383
386
|
// says more than the legacy resolution — re-derived here from
|
|
384
387
|
// the seal's own snapshot, never from mutable configuration.
|
|
385
388
|
routeFromJson(scope.approvedRouteJson ?? null))
|
|
386
|
-
: digestOf({ goal: scope.goal, outOfScope: scope.outOfScope, touches: scope.touches, budgetMicrousd: scope.budgetMicrousd, acceptance: scope.acceptance, candidate: scope.candidate ?? null, releaseTerms: scope.releaseTerms ?? null });
|
|
389
|
+
: digestOf({ goal: scope.goal, outOfScope: scope.outOfScope, touches: scope.touches, budgetMicrousd: scope.budgetMicrousd, acceptance: scope.acceptance, candidate: scope.candidate ?? null, releaseTerms: scope.releaseTerms ?? null, releaseCheck: scope.releaseCheck ?? null });
|
|
387
390
|
if (rederived !== scope.approvedDigest) {
|
|
388
391
|
return { ok: false, message: "the approval record does not verify against the stored terms — re-approve (stale-approval)" };
|
|
389
392
|
}
|
|
@@ -2372,7 +2375,8 @@ async function settleProof(captured, statArtifactId, sealedHead) {
|
|
|
2372
2375
|
const access = checkLaunchOf(store, runId, captured.effective.profile).access;
|
|
2373
2376
|
const decision = planTargetedRecheck(store, root, { runId, taskId: request.taskId, repo, head: sealedHead, command: configured, access, now: now() });
|
|
2374
2377
|
// A release's check (signed terms or an open authority): CI never stands in for its unit suite.
|
|
2375
|
-
|
|
2378
|
+
// A release's check: signed terms, a release check purpose (pipeline-speed.md, P0) or an open authority.
|
|
2379
|
+
const release = decision.authority !== null || releaseCheckScope(store.getScope(request.taskId));
|
|
2376
2380
|
try {
|
|
2377
2381
|
checkAttempt = prepareCheckAttempt(store, root, { runId, repo, head: sealedHead, command: configured, access, authority: decision.authority,
|
|
2378
2382
|
...(decision.mode === "targeted" ? { plan: decision.plan, snapshot: decision.snapshot } : {}), watch: decision.watch, release }, now());
|
|
@@ -2618,6 +2622,8 @@ async function settleProof(captured, statArtifactId, sealedHead) {
|
|
|
2618
2622
|
let sealedReport = null;
|
|
2619
2623
|
if (checkAttempt !== null && last !== null && !last.notFound) {
|
|
2620
2624
|
sealedReport = sealCheckOutcome(store, root, checkAttempt, { stdout: last.stdout, exitCode: last.timedOut ? null : last.code }, now());
|
|
2625
|
+
// Where the check's time went (latency-timeline.ts): each part's start and end, display only.
|
|
2626
|
+
keepCheckTimeline(store, { runId, taskId: request.taskId, report: sealedReport, stdout: last.stdout, now: now() });
|
|
2621
2627
|
}
|
|
2622
2628
|
// What the check covered (receipt version 3): full, composed from the release's last full check, partial, or
|
|
2623
2629
|
// identity-removed (which stops the release carrying).
|
|
@@ -2630,7 +2636,7 @@ async function settleProof(captured, statArtifactId, sealedHead) {
|
|
|
2630
2636
|
verifyCommand = effective.result;
|
|
2631
2637
|
releaseFacts = effective.facts;
|
|
2632
2638
|
}
|
|
2633
|
-
else if (verifyCommand.configured && verifyCommand.ran && chosenCheck?.level === "full" && (store.getScope(request.taskId)
|
|
2639
|
+
else if (verifyCommand.configured && verifyCommand.ran && chosenCheck?.level === "full" && releaseCheckScope(store.getScope(request.taskId))) {
|
|
2634
2640
|
// A release's Full check that couldn't be sealed before it ran (no nonce, no inventory) has no result it can count.
|
|
2635
2641
|
verifyCommand = { configured: true, ran: false, attemptFailed: true, failure: "no-result" };
|
|
2636
2642
|
recordCheckNote("The release check ran without a sealed attempt (its commit couldn't be read before it ran), so it has no result that can count.");
|
package/dist/check-failure.js
CHANGED
|
@@ -22,6 +22,7 @@ import { CHECK_REPORT_CAPTURE, readSealedJson } from "./check-report.js";
|
|
|
22
22
|
import { readCheckReport } from "./contracts/release-check.js";
|
|
23
23
|
import { regateTask } from "./dispose.js";
|
|
24
24
|
import { INFRA_RERUNS_PER_CANDIDATE, INFRA_RERUNS_PER_INCIDENT } from "./release.js";
|
|
25
|
+
import { releaseCheckScope } from "./scope.js";
|
|
25
26
|
import { verificationEvidence } from "./verification-evidence.js";
|
|
26
27
|
/** The plane's limits for confirmed infrastructure failures (one rerun per incident, two per candidate). */
|
|
27
28
|
export { INFRA_RERUNS_PER_CANDIDATE, INFRA_RERUNS_PER_INCIDENT };
|
|
@@ -235,10 +236,37 @@ export const classWords = (kind) => ({
|
|
|
235
236
|
export function recheckReleaseTask(store, root, taskId, now) {
|
|
236
237
|
const ref = store.lookupRef(taskId);
|
|
237
238
|
const scope = store.getScope(taskId);
|
|
238
|
-
if (ref === null || scope === null || (scope
|
|
239
|
+
if (ref === null || scope === null || !releaseCheckScope(scope) || (scope.candidate ?? null) === null)
|
|
239
240
|
return { action: "stop", class: null, why: `${taskId} isn't a release task`, source: null };
|
|
240
|
-
// One transaction: the rerun and its ledger row land together, or
|
|
241
|
-
return store.transact(() =>
|
|
241
|
+
// One transaction: the binding, the rerun and its ledger row are read and land together, or nothing does.
|
|
242
|
+
return store.transact(() => {
|
|
243
|
+
const bound = releasePurposeBinding(store, store.getScope(taskId) ?? scope, ref.repo);
|
|
244
|
+
if (bound !== null)
|
|
245
|
+
return { action: "stop", class: null, why: bound, source: null };
|
|
246
|
+
return recheckInside(store, root, taskId, ref.repo, String(scope.candidate), now);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Why the release's sealed purpose no longer names this exact check, or null when it does (pipeline-speed.md, P0). A
|
|
251
|
+
* release check purpose (a release that can't carry) authorizes a rerun only of the identical approved candidate: its
|
|
252
|
+
* sealed commit is the scope's candidate, its project is the task's, the project's live release check grant is the one
|
|
253
|
+
* sealed and its check access is the one sealed. Signed terms keep their own rules (a carried commit's candidate is
|
|
254
|
+
* not the terms' first commit) and are held to the same project and grant.
|
|
255
|
+
*/
|
|
256
|
+
function releasePurposeBinding(store, scope, repo) {
|
|
257
|
+
const sealed = scope.releaseTerms ?? scope.releaseCheck ?? null;
|
|
258
|
+
if (sealed === null)
|
|
259
|
+
return "it carries no release terms or release check purpose";
|
|
260
|
+
if (repo === null || sealed.repo !== repo)
|
|
261
|
+
return "its sealed release names another project";
|
|
262
|
+
if (scope.releaseTerms == null && scope.releaseCheck.approvedSha !== scope.candidate)
|
|
263
|
+
return "its sealed release check names another commit";
|
|
264
|
+
const grant = store.liveVerifyCommand(repo);
|
|
265
|
+
if (grant === null || grant.id !== sealed.grant.id || grant.digest !== sealed.grant.digest)
|
|
266
|
+
return "the project's release check changed since the approval";
|
|
267
|
+
if (scope.releaseTerms == null && store.projectCheckAccess(repo) !== scope.releaseCheck.access)
|
|
268
|
+
return "the project's check access changed since the approval";
|
|
269
|
+
return null;
|
|
242
270
|
}
|
|
243
271
|
function recheckInside(store, root, taskId, repo, candidate, now) {
|
|
244
272
|
const ref = store.lookupRef(taskId);
|
package/dist/check-report.d.ts
CHANGED
|
@@ -52,8 +52,8 @@ export declare const FULL_PARTS: readonly CheckPart[];
|
|
|
52
52
|
export declare const NOTHING_PATHS: readonly RegExp[];
|
|
53
53
|
/** Model-facing code, whose change runs the real-model journeys: the plane's copy of scripts/release-check.mjs MODEL. */
|
|
54
54
|
export declare const MODEL_PATHS: readonly RegExp[];
|
|
55
|
-
/** Whether a changed path can affect an upgrade: the plane's copy of scripts/release-check.mjs needsUpgrade
|
|
56
|
-
* version-only exception
|
|
55
|
+
/** Whether a changed path can affect an upgrade: the plane's copy of scripts/release-check.mjs needsUpgrade. Its
|
|
56
|
+
* version-only exception is decided apart, from verified blobs (versionOnlyChange), never from the path alone. */
|
|
57
57
|
export declare function upgradeRelevant(file: string): boolean;
|
|
58
58
|
/** The paths that differ between two verified trees: added, removed, or with another mode or blob. */
|
|
59
59
|
export declare function changedPaths(facts: TreeFacts, base: TreeFacts): string[];
|
|
@@ -65,7 +65,10 @@ export declare function changedPaths(facts: TreeFacts, base: TreeFacts): string[
|
|
|
65
65
|
* changed; the upgrade path when a change can affect an upgrade and the tree has it. With no base every one the tree
|
|
66
66
|
* can run is owed. The check is told these on its handshake and runs at least them.
|
|
67
67
|
*/
|
|
68
|
-
export declare function fullObligationsOf(facts: TreeFacts, base: TreeFacts | null
|
|
68
|
+
export declare function fullObligationsOf(facts: TreeFacts, base: TreeFacts | null,
|
|
69
|
+
/** package.json / package-lock.json paths whose change since `base` is only the project's own version, proved by the
|
|
70
|
+
* plane from both verified blobs (versionBumpsOf): such a change alone doesn't owe the upgrade path. */
|
|
71
|
+
versionBumps?: readonly string[]): CheckPart[];
|
|
69
72
|
/** What a check must account for, sealed before it runs: every unit file of the tree, whatever runs (a targeted
|
|
70
73
|
* check accounts for the rest through its base), and the parts it owes (a full check's from fullObligationsOf), and
|
|
71
74
|
* whether CI may stand in for its unit suite. */
|
|
@@ -153,3 +156,15 @@ export declare function fullCheckProblem(input: {
|
|
|
153
156
|
inventory: SealedInventory | null;
|
|
154
157
|
exitCode: number | null;
|
|
155
158
|
}): string | null;
|
|
159
|
+
/** Where a release bumps the project's own version: the plane's copy of scripts/release-check.mjs VERSIONED. */
|
|
160
|
+
export declare const VERSIONED_PATHS: readonly string[];
|
|
161
|
+
/**
|
|
162
|
+
* Whether `path` changed from `before` to `after` only in the project's own version (pipeline-speed.md, item 4): the
|
|
163
|
+
* plane's copy of scripts/release-check.mjs versionOnly — the top-level "version", and in the lockfile the root
|
|
164
|
+
* package's. Anything else (a dependency, a script, a reordering, a duplicate key, a file added or removed, bytes that
|
|
165
|
+
* aren't one JSON object) is a real change.
|
|
166
|
+
*/
|
|
167
|
+
export declare function versionOnlyChange(path: string, before: Buffer | null, after: Buffer | null): boolean;
|
|
168
|
+
/** The versioned files whose change from `base` to `facts` is only the version, read from verified blobs in the
|
|
169
|
+
* project's own Git directory. Anything unreadable is a real change (owes more, never less). */
|
|
170
|
+
export declare function versionBumpsOf(gitDir: string, base: TreeFacts, facts: TreeFacts): string[];
|
package/dist/check-report.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import { createHash, randomBytes } from "node:crypto";
|
|
21
21
|
import { lstatSync, realpathSync } from "node:fs";
|
|
22
22
|
import { join } from "node:path";
|
|
23
|
-
import { readVerifiedCommit, verifiedTreeEntries } from "./commit-extract.js";
|
|
23
|
+
import { readVerifiedCommit, verifiedBlobs, verifiedTreeEntries } from "./commit-extract.js";
|
|
24
24
|
import { CHECK_PARTS, CHECK_REPORT_MAX_BYTES, CHECK_REPORT_PREFIX, readCheckReport, } from "./contracts/release-check.js";
|
|
25
25
|
import { readVerifiedArtifact, storeEvidence } from "./evidence.js";
|
|
26
26
|
import { releaseBaselineRecord } from "./release-card.js";
|
|
@@ -107,8 +107,8 @@ export const MODEL_PATHS = Object.freeze([
|
|
|
107
107
|
/^src\/(subagents|subagent-work|task-sizing|flow-sort|flow-draft)\.ts$/,
|
|
108
108
|
/^src\/(provider|provider-[a-z-]+|invoke|exec|attest|coding-provider|assignment-adapters|subscription-[a-z-]+)\.ts$/,
|
|
109
109
|
]);
|
|
110
|
-
/** Whether a changed path can affect an upgrade: the plane's copy of scripts/release-check.mjs needsUpgrade
|
|
111
|
-
* version-only exception
|
|
110
|
+
/** Whether a changed path can affect an upgrade: the plane's copy of scripts/release-check.mjs needsUpgrade. Its
|
|
111
|
+
* version-only exception is decided apart, from verified blobs (versionOnlyChange), never from the path alone. */
|
|
112
112
|
export function upgradeRelevant(file) {
|
|
113
113
|
if (/^(docs|evidence|design|output)\//.test(file) || /\.md$/.test(file) || file === "LICENSE")
|
|
114
114
|
return false;
|
|
@@ -134,7 +134,10 @@ export function changedPaths(facts, base) {
|
|
|
134
134
|
* changed; the upgrade path when a change can affect an upgrade and the tree has it. With no base every one the tree
|
|
135
135
|
* can run is owed. The check is told these on its handshake and runs at least them.
|
|
136
136
|
*/
|
|
137
|
-
export function fullObligationsOf(facts, base
|
|
137
|
+
export function fullObligationsOf(facts, base,
|
|
138
|
+
/** package.json / package-lock.json paths whose change since `base` is only the project's own version, proved by the
|
|
139
|
+
* plane from both verified blobs (versionBumpsOf): such a change alone doesn't owe the upgrade path. */
|
|
140
|
+
versionBumps = []) {
|
|
138
141
|
// What the tree can run is read from the candidate AND the last release (security scan): a candidate that deletes a
|
|
139
142
|
// journey script, the upgrade path or its unit files doesn't take away what was owed at the base — the part stays
|
|
140
143
|
// owed, and a check that can't run it has no result. Any entry counts, a symlink included, never only a regular file.
|
|
@@ -146,7 +149,8 @@ export function fullObligationsOf(facts, base) {
|
|
|
146
149
|
const code = changed?.filter(path => !NOTHING_PATHS.some(re => re.test(path))) ?? null;
|
|
147
150
|
const checks = code === null || code.length > 0;
|
|
148
151
|
const real = code === null || code.some(path => !/\.test\.[cm]?[jt]s$/.test(path) && MODEL_PATHS.some(re => re.test(path)));
|
|
149
|
-
const
|
|
152
|
+
const bumped = (path) => VERSIONED_PATHS.includes(path) && versionBumps.includes(path);
|
|
153
|
+
const upgrade = has("scripts/upgrade-path.mjs") && (changed === null || changed.some(path => !bumped(path) && upgradeRelevant(path)));
|
|
150
154
|
return [
|
|
151
155
|
"typecheck", "build",
|
|
152
156
|
...(checks && units ? ["unit"] : []),
|
|
@@ -195,7 +199,7 @@ export function prepareCheckAttempt(store, root, input, now) {
|
|
|
195
199
|
}
|
|
196
200
|
baseFacts = baseRead.facts;
|
|
197
201
|
}
|
|
198
|
-
const owed = fullObligationsOf(read.facts, baseFacts);
|
|
202
|
+
const owed = fullObligationsOf(read.facts, baseFacts, baseFacts === null ? [] : versionBumpsOf(gitDir, baseFacts, read.facts));
|
|
199
203
|
const release = input.release !== false;
|
|
200
204
|
const ciSubstitute = !release;
|
|
201
205
|
const inventory = inventoryOf(read.facts, plan, { owed, release });
|
|
@@ -213,7 +217,9 @@ export function prepareCheckAttempt(store, root, input, now) {
|
|
|
213
217
|
inventoryDigest: payloadDigest(inventory), contextDigest: payloadDigest(context), nonceHash: sha256(nonce),
|
|
214
218
|
}, now);
|
|
215
219
|
// The command's own limit rides along, so the check ends itself (logs and report printed) before the plane stops it.
|
|
216
|
-
|
|
220
|
+
// `reportBytes`: the largest report this plane reads back once sealed. A plane that doesn't say (0.9.77 and older) reads
|
|
221
|
+
// only 64 KiB, so the check gives it no larger report rather than one that poisons the receipt (scripts/release-check.mjs).
|
|
222
|
+
const stdin = JSON.stringify({ nonce, mode, plan, watch: [...(input.watch ?? [])], limitMs: input.command.timeoutMs, reportBytes: CHECK_ARTIFACT_CAP, ...(plan === null ? { owed, ciSubstitute } : {}) });
|
|
217
223
|
return { runId: input.runId, nonce, stdin, mode, inventory, context, plan, snapshot, watch: [...(input.watch ?? [])] };
|
|
218
224
|
}
|
|
219
225
|
/** Seal one JSON payload as a structured-output artifact under its capture label. */
|
|
@@ -388,3 +394,57 @@ export function fullCheckProblem(input) {
|
|
|
388
394
|
}
|
|
389
395
|
return null;
|
|
390
396
|
}
|
|
397
|
+
/** Where a release bumps the project's own version: the plane's copy of scripts/release-check.mjs VERSIONED. */
|
|
398
|
+
export const VERSIONED_PATHS = Object.freeze(["package.json", "package-lock.json"]);
|
|
399
|
+
/**
|
|
400
|
+
* Whether `path` changed from `before` to `after` only in the project's own version (pipeline-speed.md, item 4): the
|
|
401
|
+
* plane's copy of scripts/release-check.mjs versionOnly — the top-level "version", and in the lockfile the root
|
|
402
|
+
* package's. Anything else (a dependency, a script, a reordering, a duplicate key, a file added or removed, bytes that
|
|
403
|
+
* aren't one JSON object) is a real change.
|
|
404
|
+
*/
|
|
405
|
+
export function versionOnlyChange(path, before, after) {
|
|
406
|
+
if (!VERSIONED_PATHS.includes(path) || before === null || after === null)
|
|
407
|
+
return false;
|
|
408
|
+
const rest = (bytes) => {
|
|
409
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
410
|
+
const json = JSON.parse(text);
|
|
411
|
+
// Parsing is lossy for duplicate keys and rounded numbers. Such bytes never earn an exemption.
|
|
412
|
+
const tokens = (value) => JSON.stringify(value.match(/"(?:\\.|[^"\\])*"|[^\s"{}\[\]:,]+|[{}\[\]:,]/g));
|
|
413
|
+
if (tokens(text) !== tokens(JSON.stringify(json)))
|
|
414
|
+
throw new Error("ambiguous JSON");
|
|
415
|
+
if (json === null || typeof json !== "object" || Array.isArray(json))
|
|
416
|
+
throw new Error("not an object");
|
|
417
|
+
const object = json;
|
|
418
|
+
if (typeof object["version"] !== "string")
|
|
419
|
+
throw new Error("not a version string");
|
|
420
|
+
delete object["version"];
|
|
421
|
+
const packages = object["packages"];
|
|
422
|
+
if (path === "package-lock.json" && packages !== null && typeof packages === "object" && !Array.isArray(packages)) {
|
|
423
|
+
const root = packages[""];
|
|
424
|
+
if (root !== null && typeof root === "object" && !Array.isArray(root)) {
|
|
425
|
+
if (typeof root["version"] !== "string")
|
|
426
|
+
throw new Error("not a root version string");
|
|
427
|
+
delete root["version"];
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return JSON.stringify(object);
|
|
431
|
+
};
|
|
432
|
+
try {
|
|
433
|
+
return rest(before) === rest(after);
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
/** The versioned files whose change from `base` to `facts` is only the version, read from verified blobs in the
|
|
440
|
+
* project's own Git directory. Anything unreadable is a real change (owes more, never less). */
|
|
441
|
+
export function versionBumpsOf(gitDir, base, facts) {
|
|
442
|
+
const before = verifiedBlobs(gitDir, base.tree, VERSIONED_PATHS);
|
|
443
|
+
const after = verifiedBlobs(gitDir, facts.tree, VERSIONED_PATHS);
|
|
444
|
+
if (!before.ok || !after.ok)
|
|
445
|
+
return [];
|
|
446
|
+
return VERSIONED_PATHS.filter(path => {
|
|
447
|
+
const a = before.value.get(path), b = after.value.get(path);
|
|
448
|
+
return a !== undefined && b !== undefined && a.id !== b.id && versionOnlyChange(path, a.bytes, b.bytes);
|
|
449
|
+
});
|
|
450
|
+
}
|
package/dist/claim.d.ts
CHANGED
|
@@ -103,6 +103,15 @@ export type AcquireResult = {
|
|
|
103
103
|
| {
|
|
104
104
|
ok: false;
|
|
105
105
|
reason: "coordinator-filed";
|
|
106
|
+
} | {
|
|
107
|
+
ok: false;
|
|
108
|
+
reason: "watch-custody";
|
|
109
|
+
}
|
|
110
|
+
/** The runner reported draining (v137 lifecycle): its stop has begun, so it takes no new work. */
|
|
111
|
+
| {
|
|
112
|
+
ok: false;
|
|
113
|
+
reason: "draining";
|
|
114
|
+
until: string;
|
|
106
115
|
};
|
|
107
116
|
/** `fenced` means the lease was superseded; `unknown` means it never existed. */
|
|
108
117
|
export type FenceResult = {
|
package/dist/claim.js
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* rather than probable.
|
|
29
29
|
*/
|
|
30
30
|
import { randomUUID } from "node:crypto";
|
|
31
|
-
import { authenticate } from "./runner.js";
|
|
31
|
+
import { authenticate, drainOf } from "./runner.js";
|
|
32
32
|
import { pauseForAuth } from "./provider-auth.js";
|
|
33
33
|
import { DEFAULT_MAX_OPEN_DECISIONS, missingCapability, scopeApprovedForDispatch, taskReadinessBlocker, } from "./dispatch.js";
|
|
34
34
|
import { BUILT_IN, } from "./store.js";
|
|
@@ -85,6 +85,10 @@ function acquireLocked(store, taskRef, runner, options) {
|
|
|
85
85
|
const gate = authenticate(store, runner, options.token);
|
|
86
86
|
if (!gate.ok)
|
|
87
87
|
return { ok: false, reason: "unauthenticated", detail: gate.reason };
|
|
88
|
+
// DRAINING (v137): a runner whose stop has begun admits nothing new, on every claim road.
|
|
89
|
+
const drain = drainOf(store, gate.runner, now);
|
|
90
|
+
if (drain !== null)
|
|
91
|
+
return { ok: false, reason: "draining", until: drain.until };
|
|
88
92
|
const placedRef = store.refForId(taskRef);
|
|
89
93
|
const placedRepo = placedRef?.repo ?? null;
|
|
90
94
|
if (placedRepo === null)
|
|
@@ -92,6 +96,10 @@ function acquireLocked(store, taskRef, runner, options) {
|
|
|
92
96
|
if (!gate.runner.repos.includes(placedRepo)) {
|
|
93
97
|
return { ok: false, reason: "unauthorized-repo", repo: placedRepo };
|
|
94
98
|
}
|
|
99
|
+
// The watch may have lapsed while this pass awaited I/O, before its heartbeat callback ran. Runner tokens
|
|
100
|
+
// survive a role restart, so prove this exact watch in the claim transaction as well.
|
|
101
|
+
if (options.incarnation !== undefined && db.prepare("SELECT 1 FROM watch_lease WHERE runner = ? AND repo = ? AND owner = ? AND expires_at > ?").get(runner, placedRepo, options.incarnation, stamp) === undefined)
|
|
102
|
+
return { ok: false, reason: "watch-custody" };
|
|
95
103
|
// THE COORDINATOR QUARANTINE in the one primitive every claim road
|
|
96
104
|
// shares (MCP spec v6, round-3 f1): a coordinator-filed task whose
|
|
97
105
|
// scope has not been sealed by the password ceremony claims for
|
|
@@ -98,7 +98,7 @@ export declare const CONSOLE_FORMS: {
|
|
|
98
98
|
readonly storage: FormContract<"cleanup" | "password" | "path" | "preview">;
|
|
99
99
|
readonly pullRequests: FormContract<"act" | "base" | "github" | "method" | "password" | "repo" | "when-green">;
|
|
100
100
|
readonly checks: FormContract<"act" | "command" | "level" | "on" | "password" | "repo" | "timeout">;
|
|
101
|
-
readonly backups: FormContract<"every" | "folder" | "keep" | "password">;
|
|
101
|
+
readonly backups: FormContract<"every" | "folder" | "keep" | "live_every" | "live_folder" | "password">;
|
|
102
102
|
readonly data: FormContract<"password">;
|
|
103
103
|
readonly flowsOn: FormContract<"repo" | "starter">;
|
|
104
104
|
readonly integrationsTest: FormContract<"key">;
|
|
@@ -107,7 +107,7 @@ export const CONSOLE_FORMS = {
|
|
|
107
107
|
storage: formContract("POST /settings/storage, /settings/storage/clean, /settings/storage/discard", ["password", "cleanup", "preview", "path"]),
|
|
108
108
|
pullRequests: formContract("POST /settings/pull-requests", ["repo", "act", "password", "github", "base", "method", "when-green"]),
|
|
109
109
|
checks: formContract("POST /settings/checks", ["repo", "password", "act", "on", "level", "command", "timeout"]),
|
|
110
|
-
backups: formContract("POST /settings/backups", ["password", "every", "keep", "folder"]),
|
|
110
|
+
backups: formContract("POST /settings/backups", ["password", "every", "keep", "folder", "live_every", "live_folder"]),
|
|
111
111
|
data: formContract("POST /settings/data", ["password"]),
|
|
112
112
|
flowsOn: formContract("POST /settings/flows/on", ["repo", "starter"]),
|
|
113
113
|
integrationsTest: formContract("POST /settings/integrations/test", ["key"]),
|
|
@@ -64,3 +64,44 @@ export declare function storedReleaseTerms(raw: unknown): {
|
|
|
64
64
|
ok: false;
|
|
65
65
|
problem: string;
|
|
66
66
|
};
|
|
67
|
+
/**
|
|
68
|
+
* A release check's purpose (docs/plans/pipeline-speed.md, P0): what a person agrees to when they approve a release
|
|
69
|
+
* whose later commits can't carry (its checks run outside the sandbox, or carrying is off) — this exact candidate is a
|
|
70
|
+
* release's check. It never carries a later commit, never lets CI stand in for the check's units and never approves
|
|
71
|
+
* itself; it lets the plane hold the check to the release's strict report rules and rerun it, on this identical
|
|
72
|
+
* candidate under this same approval, after a failure the plane itself confirmed as infrastructure. Part of the scope
|
|
73
|
+
* digest when present; read strictly. A scope carries terms or this purpose, never both.
|
|
74
|
+
*/
|
|
75
|
+
export declare const releaseCheckPurposeSchema: z.ZodObject<{
|
|
76
|
+
version: z.ZodLiteral<1>;
|
|
77
|
+
purpose: z.ZodLiteral<"release-check">;
|
|
78
|
+
repo: z.ZodString;
|
|
79
|
+
packageVersion: z.ZodString;
|
|
80
|
+
branch: z.ZodString;
|
|
81
|
+
pr: z.ZodInt;
|
|
82
|
+
base: z.ZodLiteral<"main">;
|
|
83
|
+
approvedSha: z.ZodString;
|
|
84
|
+
approvedTree: z.ZodString;
|
|
85
|
+
grant: z.ZodObject<{
|
|
86
|
+
id: z.ZodInt;
|
|
87
|
+
digest: z.ZodString;
|
|
88
|
+
}, z.core.$strict>;
|
|
89
|
+
access: z.ZodEnum<{
|
|
90
|
+
sandboxed: "sandboxed";
|
|
91
|
+
"sign-ins": "sign-ins";
|
|
92
|
+
unsandboxed: "unsandboxed";
|
|
93
|
+
}>;
|
|
94
|
+
}, z.core.$strict>;
|
|
95
|
+
export type ReleaseCheckPurpose = z.infer<typeof releaseCheckPurposeSchema>;
|
|
96
|
+
export declare function readReleaseCheckPurpose(input: unknown): ContractResult<ReleaseCheckPurpose>;
|
|
97
|
+
/** The digest a scope binds for its release check purpose, domain-separated from the terms' and every other input. */
|
|
98
|
+
export declare function releaseCheckPurposeDigest(purpose: ReleaseCheckPurpose): string;
|
|
99
|
+
/** Strict stored purpose: null column → null; anything that isn't exactly a purpose, as written → a problem. */
|
|
100
|
+
export declare function storedReleaseCheckPurpose(raw: unknown): {
|
|
101
|
+
ok: true;
|
|
102
|
+
purpose: ReleaseCheckPurpose | null;
|
|
103
|
+
} | {
|
|
104
|
+
ok: false;
|
|
105
|
+
problem: string;
|
|
106
|
+
};
|
|
107
|
+
export declare function serializeReleaseCheckPurpose(purpose: ReleaseCheckPurpose): string;
|
|
@@ -72,3 +72,56 @@ export function storedReleaseTerms(raw) {
|
|
|
72
72
|
return { ok: false, problem: "the release terms are not stored exactly as written" };
|
|
73
73
|
return { ok: true, terms: read.value };
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* A release check's purpose (docs/plans/pipeline-speed.md, P0): what a person agrees to when they approve a release
|
|
77
|
+
* whose later commits can't carry (its checks run outside the sandbox, or carrying is off) — this exact candidate is a
|
|
78
|
+
* release's check. It never carries a later commit, never lets CI stand in for the check's units and never approves
|
|
79
|
+
* itself; it lets the plane hold the check to the release's strict report rules and rerun it, on this identical
|
|
80
|
+
* candidate under this same approval, after a failure the plane itself confirmed as infrastructure. Part of the scope
|
|
81
|
+
* digest when present; read strictly. A scope carries terms or this purpose, never both.
|
|
82
|
+
*/
|
|
83
|
+
export const releaseCheckPurposeSchema = versioned(1, {
|
|
84
|
+
purpose: z.literal("release-check"),
|
|
85
|
+
repo: z.string().min(1).max(4096),
|
|
86
|
+
packageVersion: z.string().regex(/^\d+\.\d+\.\d+$/),
|
|
87
|
+
branch: z.string().min(1).max(255).regex(/^[A-Za-z0-9._/-]+$/),
|
|
88
|
+
pr: z.int().min(1),
|
|
89
|
+
base: z.literal("main"),
|
|
90
|
+
approvedSha: sha,
|
|
91
|
+
approvedTree: sha,
|
|
92
|
+
grant: z.strictObject({ id: z.int(), digest: z.string().min(1).max(128) }),
|
|
93
|
+
access: z.enum(["sandboxed", "sign-ins", "unsandboxed"]),
|
|
94
|
+
});
|
|
95
|
+
export function readReleaseCheckPurpose(input) {
|
|
96
|
+
return readVersioned(releaseCheckPurposeSchema, input);
|
|
97
|
+
}
|
|
98
|
+
/** The digest a scope binds for its release check purpose, domain-separated from the terms' and every other input. */
|
|
99
|
+
export function releaseCheckPurposeDigest(purpose) {
|
|
100
|
+
return createHash("sha256").update(`release-check-purpose:${JSON.stringify(purpose)}`).digest("hex");
|
|
101
|
+
}
|
|
102
|
+
/** Strict stored purpose: null column → null; anything that isn't exactly a purpose, as written → a problem. */
|
|
103
|
+
export function storedReleaseCheckPurpose(raw) {
|
|
104
|
+
if (raw === null || raw === undefined)
|
|
105
|
+
return { ok: true, purpose: null };
|
|
106
|
+
if (typeof raw !== "string")
|
|
107
|
+
return { ok: false, problem: "the release check purpose is not stored as text" };
|
|
108
|
+
let parsed;
|
|
109
|
+
try {
|
|
110
|
+
parsed = JSON.parse(raw);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return { ok: false, problem: "the release check purpose can't be read" };
|
|
114
|
+
}
|
|
115
|
+
const read = readReleaseCheckPurpose(parsed);
|
|
116
|
+
if (!read.ok)
|
|
117
|
+
return { ok: false, problem: `the release check purpose doesn't match its contract (${read.issues[0]?.line ?? "invalid"})` };
|
|
118
|
+
if (JSON.stringify(read.value) !== raw)
|
|
119
|
+
return { ok: false, problem: "the release check purpose is not stored exactly as written" };
|
|
120
|
+
return { ok: true, purpose: read.value };
|
|
121
|
+
}
|
|
122
|
+
export function serializeReleaseCheckPurpose(purpose) {
|
|
123
|
+
const read = readReleaseCheckPurpose(purpose);
|
|
124
|
+
if (!read.ok)
|
|
125
|
+
throw new Error(`The release check purpose doesn't match its contract: ${read.issues[0]?.line ?? "invalid"}`);
|
|
126
|
+
return JSON.stringify(read.value);
|
|
127
|
+
}
|
|
@@ -8,7 +8,10 @@ const stop = () => stopping.abort();
|
|
|
8
8
|
process.on("SIGTERM", stop);
|
|
9
9
|
process.on("SIGINT", stop);
|
|
10
10
|
try {
|
|
11
|
-
|
|
11
|
+
// A stop is forwarded to the controller (SIGTERM) and waited for. Past 55 s its whole tree is stopped outright: before
|
|
12
|
+
// launchd's own ExitTimeOut (60 s) kills this process and could leave the controller running without it. `up` signs
|
|
13
|
+
// off within 50 s at its default grace and stops itself if this process dies first (operate.ts upCommand).
|
|
14
|
+
await superviseController({ file, argv, signal: stopping.signal, shutdownMs: 55_000, onState: state => {
|
|
12
15
|
if (state.phase === "backoff")
|
|
13
16
|
process.stderr.write(`Controller exited (${state.exit?.signal ?? state.exit?.code ?? "unknown"}); restart in ${state.retryMs} ms.\n`);
|
|
14
17
|
} });
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Phases where the new service may be running: recovery leaves them to the release, which resumes forward. */
|
|
2
|
+
export declare const FORWARD_ONLY: readonly string[];
|
|
3
|
+
export type RescueWorld = {
|
|
4
|
+
/** The live database. */
|
|
5
|
+
database: string;
|
|
6
|
+
/** The installation's state folder: deployments stage under staged-upgrades/ there. */
|
|
7
|
+
stateDir: string;
|
|
8
|
+
/** Every deployment journal under staged-upgrades/: its stage and contents, null when unreadable. */
|
|
9
|
+
journals(): {
|
|
10
|
+
stage: string;
|
|
11
|
+
journal: Record<string, unknown> | null;
|
|
12
|
+
}[];
|
|
13
|
+
/** The builder run's worktree, from the plane's records (the deploy script's --source), or null. */
|
|
14
|
+
runWorktree(run: number): string | null;
|
|
15
|
+
/** The deployment's own recovery for that stage (release-adapters.ts recoverDeploy). */
|
|
16
|
+
recover(input: {
|
|
17
|
+
run: number;
|
|
18
|
+
worktree: string;
|
|
19
|
+
stage: string;
|
|
20
|
+
}): Promise<{
|
|
21
|
+
ok: true;
|
|
22
|
+
} | {
|
|
23
|
+
ok: false;
|
|
24
|
+
message: string;
|
|
25
|
+
}>;
|
|
26
|
+
};
|
|
27
|
+
export type RescueOutcome = {
|
|
28
|
+
ok: true;
|
|
29
|
+
acted: boolean;
|
|
30
|
+
stage: string | null;
|
|
31
|
+
phase: string | null;
|
|
32
|
+
words: string[];
|
|
33
|
+
} | {
|
|
34
|
+
ok: false;
|
|
35
|
+
reason: "usage" | "conflict" | "damaged" | "forward" | "failed";
|
|
36
|
+
message: string;
|
|
37
|
+
};
|
|
38
|
+
/** What recovery will do from a phase, in a few words. */
|
|
39
|
+
export declare function rescuePlan(phase: string, journal: Record<string, unknown>): string[];
|
|
40
|
+
/** Find the one unfinished deployment (or the named stage), verify its journal, and recover it (with `yes`). */
|
|
41
|
+
export declare function rescueDeployment(stageArg: string | undefined, yes: boolean, world: RescueWorld): Promise<RescueOutcome>;
|