toolroll 0.9.70 → 0.9.72
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 +99 -0
- package/dist/agent-fence.d.ts +7 -0
- package/dist/agent-fence.js +10 -2
- package/dist/approval-authority.d.ts +20 -0
- package/dist/approval-authority.js +67 -0
- package/dist/assignment.js +26 -4
- package/dist/browser/workspace.css +1 -1
- package/dist/builder.js +48 -6
- package/dist/chat-decide.js +17 -2
- package/dist/check-report.d.ts +109 -0
- package/dist/check-report.js +204 -0
- package/dist/claim.js +1 -1
- package/dist/commit-extract.d.ts +94 -0
- package/dist/commit-extract.js +436 -0
- package/dist/contracts/console-api.d.ts +1 -1
- package/dist/contracts/console-api.js +1 -0
- package/dist/contracts/flow.d.ts +16 -0
- package/dist/contracts/flow.js +3 -0
- package/dist/contracts/release-check.d.ts +335 -0
- package/dist/contracts/release-check.js +159 -0
- package/dist/contracts/release-terms.d.ts +60 -0
- package/dist/contracts/release-terms.js +70 -0
- package/dist/contracts/verification-receipt.d.ts +42 -8
- package/dist/contracts/verification-receipt.js +24 -7
- package/dist/contracts/verification-view.d.ts +1 -0
- package/dist/deploy-gate.d.ts +17 -0
- package/dist/deploy-gate.js +68 -0
- package/dist/dispatch.js +3 -10
- package/dist/dispose.js +4 -1
- package/dist/evidence-pack.d.ts +1 -1
- package/dist/evidence-pack.js +20 -2
- package/dist/evidence.d.ts +1 -0
- package/dist/evidence.js +2 -1
- package/dist/flow-engine.js +5 -1
- package/dist/flow-pull-request.d.ts +5 -2
- package/dist/flow-pull-request.js +14 -5
- package/dist/flow-schedule.js +25 -6
- package/dist/flows-ui.js +7 -1
- package/dist/invoke.d.ts +19 -1
- package/dist/invoke.js +49 -2
- package/dist/lead-status.js +5 -2
- package/dist/live-terminal.d.ts +10 -6
- package/dist/live-terminal.js +55 -14
- package/dist/nightly-check.d.ts +104 -0
- package/dist/nightly-check.js +362 -0
- package/dist/operate.js +39 -2
- package/dist/person-proof.d.ts +2 -0
- package/dist/person-proof.js +14 -0
- package/dist/plan-auto.js +4 -4
- package/dist/planner-source.d.ts +1 -1
- package/dist/proof.js +2 -0
- package/dist/publish.js +156 -144
- package/dist/pull-request-flow.d.ts +2 -0
- package/dist/pull-request-flow.js +24 -2
- package/dist/release-adapters.d.ts +8 -0
- package/dist/release-adapters.js +212 -15
- package/dist/release-authority.d.ts +112 -0
- package/dist/release-authority.js +126 -0
- package/dist/release-carry.d.ts +233 -0
- package/dist/release-carry.js +645 -0
- package/dist/release-cli.d.ts +3 -0
- package/dist/release-cli.js +124 -5
- package/dist/release-coverage.js +3 -0
- package/dist/release-recheck.d.ts +146 -0
- package/dist/release-recheck.js +657 -0
- package/dist/release-view.d.ts +34 -0
- package/dist/release-view.js +27 -0
- package/dist/release.d.ts +131 -1
- package/dist/release.js +562 -244
- package/dist/remote-main.d.ts +2 -0
- package/dist/remote-main.js +1 -1
- package/dist/result-completion.d.ts +6 -0
- package/dist/result-completion.js +15 -1
- package/dist/result-follow-ups.js +29 -1
- package/dist/sandbox-runtime.d.ts +59 -1
- package/dist/sandbox-runtime.js +159 -5
- package/dist/scope.d.ts +115 -5
- package/dist/scope.js +219 -7
- package/dist/server/render-tasks.d.ts +8 -0
- package/dist/server/render-tasks.js +50 -9
- package/dist/server/route-table.js +1 -0
- package/dist/server/task-views.js +4 -0
- package/dist/server/tasks.js +12 -0
- package/dist/store.d.ts +227 -5
- package/dist/store.js +727 -37
- package/dist/task-outcome-cli.js +7 -3
- package/dist/toolroll-update.js +2 -2
- package/dist/verification-evidence.d.ts +25 -1
- package/dist/verification-evidence.js +57 -2
- package/dist/work-index.js +3 -6
- package/package.json +1 -1
|
@@ -5,12 +5,15 @@
|
|
|
5
5
|
* command, the retained log and a reused gate's source — are checked in plain code after parsing.
|
|
6
6
|
*
|
|
7
7
|
* Version 1 is a gate executed in its own run. Version 2 added `reusedFrom` and `executedHere: false` for a gate
|
|
8
|
-
* reused by an unchanged observation follow-up.
|
|
9
|
-
*
|
|
8
|
+
* reused by an unchanged observation follow-up. Version 3 (docs/plans/release-once.md) is a release check: it keeps
|
|
9
|
+
* the raw exit (`result`) apart from `coverage` — `full`, `composed` (a targeted run composed with the full check A),
|
|
10
|
+
* `partial` (never passes) or `identity-removed` (a required test went away; asks again) — names the sealed check
|
|
11
|
+
* artifacts, and for a composed check every link its reader proves again. A direct receipt is still sealed as version 1,
|
|
12
|
+
* so its bytes (and the digests bound to them) read the same in every Toolroll that reads receipts.
|
|
10
13
|
*/
|
|
11
14
|
import { z } from "zod";
|
|
12
15
|
import { type ContractResult } from "./contract.js";
|
|
13
|
-
export declare const VERIFICATION_FAILURES: readonly ["spawn-failed", "timed-out", "dependency-missing", "setup-stale", "setup-failed", "own-install-failed", "tracked-files-changed", "setup-changed-files", "checkout-moved", "cleanliness-unavailable", "dependency-still-missing", "retry-spawn-failed", "retry-timed-out", "custody-lost"];
|
|
16
|
+
export declare const VERIFICATION_FAILURES: readonly ["spawn-failed", "timed-out", "dependency-missing", "setup-stale", "setup-failed", "own-install-failed", "tracked-files-changed", "setup-changed-files", "checkout-moved", "cleanliness-unavailable", "dependency-still-missing", "retry-spawn-failed", "retry-timed-out", "custody-lost", "coverage-incomplete"];
|
|
14
17
|
/**
|
|
15
18
|
* What the repository's approved verification command did: not configured; configured but the run could not attempt
|
|
16
19
|
* it (`attemptFailed`, which downgrades to `short` rather than `refuted`: "we could not check" is not "the claim is
|
|
@@ -25,6 +28,7 @@ export declare const verifyCommandFactsSchema: z.ZodUnion<readonly [z.ZodObject<
|
|
|
25
28
|
failure: z.ZodOptional<z.ZodEnum<{
|
|
26
29
|
"checkout-moved": "checkout-moved";
|
|
27
30
|
"cleanliness-unavailable": "cleanliness-unavailable";
|
|
31
|
+
"coverage-incomplete": "coverage-incomplete";
|
|
28
32
|
"custody-lost": "custody-lost";
|
|
29
33
|
"dependency-missing": "dependency-missing";
|
|
30
34
|
"dependency-still-missing": "dependency-still-missing";
|
|
@@ -71,10 +75,12 @@ export declare const receiptLogSchema: z.ZodObject<{
|
|
|
71
75
|
ok: "ok";
|
|
72
76
|
}>>;
|
|
73
77
|
}, z.core.$strict>;
|
|
74
|
-
export declare const RECEIPT_VERSION =
|
|
78
|
+
export declare const RECEIPT_VERSION = 3;
|
|
79
|
+
export declare const RECEIPT_COVERAGES: readonly ["full", "composed", "partial", "identity-removed"];
|
|
80
|
+
export type ReceiptCoverage = typeof RECEIPT_COVERAGES[number];
|
|
75
81
|
/** The receipt as read: a reused gate names its source run and digest; a direct one names neither. */
|
|
76
82
|
export declare const verificationReceiptSchema: z.ZodObject<{
|
|
77
|
-
version: z.ZodLiteral<
|
|
83
|
+
version: z.ZodLiteral<3>;
|
|
78
84
|
run: z.ZodInt;
|
|
79
85
|
head: z.ZodString;
|
|
80
86
|
base: z.ZodNullable<z.ZodString>;
|
|
@@ -105,6 +111,7 @@ export declare const verificationReceiptSchema: z.ZodObject<{
|
|
|
105
111
|
failure: z.ZodOptional<z.ZodEnum<{
|
|
106
112
|
"checkout-moved": "checkout-moved";
|
|
107
113
|
"cleanliness-unavailable": "cleanliness-unavailable";
|
|
114
|
+
"coverage-incomplete": "coverage-incomplete";
|
|
108
115
|
"custody-lost": "custody-lost";
|
|
109
116
|
"dependency-missing": "dependency-missing";
|
|
110
117
|
"dependency-still-missing": "dependency-still-missing";
|
|
@@ -136,13 +143,40 @@ export declare const verificationReceiptSchema: z.ZodObject<{
|
|
|
136
143
|
ok: "ok";
|
|
137
144
|
}>>;
|
|
138
145
|
}, z.core.$strict>;
|
|
146
|
+
coverage: z.ZodOptional<z.ZodEnum<{
|
|
147
|
+
composed: "composed";
|
|
148
|
+
full: "full";
|
|
149
|
+
"identity-removed": "identity-removed";
|
|
150
|
+
partial: "partial";
|
|
151
|
+
}>>;
|
|
152
|
+
check: z.ZodOptional<z.ZodObject<{
|
|
153
|
+
attempt: z.ZodEnum<{
|
|
154
|
+
full: "full";
|
|
155
|
+
targeted: "targeted";
|
|
156
|
+
}>;
|
|
157
|
+
report: z.ZodNullable<z.ZodString>;
|
|
158
|
+
inventory: z.ZodNullable<z.ZodString>;
|
|
159
|
+
exitCode: z.ZodOptional<z.ZodNullable<z.ZodInt>>;
|
|
160
|
+
}, z.core.$strict>>;
|
|
161
|
+
composed: z.ZodOptional<z.ZodObject<{
|
|
162
|
+
base: z.ZodInt;
|
|
163
|
+
baseReceipt: z.ZodString;
|
|
164
|
+
baseReport: z.ZodString;
|
|
165
|
+
authority: z.ZodInt;
|
|
166
|
+
carries: z.ZodArray<z.ZodInt>;
|
|
167
|
+
artifacts: z.ZodString;
|
|
168
|
+
plan: z.ZodString;
|
|
169
|
+
snapshot: z.ZodString;
|
|
170
|
+
report: z.ZodString;
|
|
171
|
+
}, z.core.$strict>>;
|
|
139
172
|
}, z.core.$strict>;
|
|
140
173
|
export type VerificationReceipt = z.infer<typeof verificationReceiptSchema>;
|
|
141
|
-
/** A version 1 receipt read as version
|
|
142
|
-
* is refused by its reader, which still knows the version
|
|
174
|
+
/** A version 1 or 2 receipt read as version 3: the same fields. Reuse is a version 2 field and coverage a version 3
|
|
175
|
+
* one; a receipt naming a field newer than it was sealed as is refused by its reader, which still knows the version. */
|
|
143
176
|
export declare function upgradeDirectReceipt(body: Record<string, unknown>): Record<string, unknown>;
|
|
144
177
|
export declare const RECEIPT_UPGRADES: {
|
|
145
178
|
readonly 1: typeof upgradeDirectReceipt;
|
|
179
|
+
readonly 2: typeof upgradeDirectReceipt;
|
|
146
180
|
};
|
|
147
|
-
/** Read a receipt: version
|
|
181
|
+
/** Read a receipt: version 3 as itself, 1 and 2 upgraded, a newer one refused plainly. */
|
|
148
182
|
export declare function readVerificationReceipt(input: unknown): ContractResult<VerificationReceipt>;
|
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
* command, the retained log and a reused gate's source — are checked in plain code after parsing.
|
|
6
6
|
*
|
|
7
7
|
* Version 1 is a gate executed in its own run. Version 2 added `reusedFrom` and `executedHere: false` for a gate
|
|
8
|
-
* reused by an unchanged observation follow-up.
|
|
9
|
-
*
|
|
8
|
+
* reused by an unchanged observation follow-up. Version 3 (docs/plans/release-once.md) is a release check: it keeps
|
|
9
|
+
* the raw exit (`result`) apart from `coverage` — `full`, `composed` (a targeted run composed with the full check A),
|
|
10
|
+
* `partial` (never passes) or `identity-removed` (a required test went away; asks again) — names the sealed check
|
|
11
|
+
* artifacts, and for a composed check every link its reader proves again. A direct receipt is still sealed as version 1,
|
|
12
|
+
* so its bytes (and the digests bound to them) read the same in every Toolroll that reads receipts.
|
|
10
13
|
*/
|
|
11
14
|
import { z } from "zod";
|
|
12
15
|
import { readVersioned, versioned } from "./contract.js";
|
|
@@ -25,6 +28,8 @@ export const VERIFICATION_FAILURES = [
|
|
|
25
28
|
"retry-spawn-failed",
|
|
26
29
|
"retry-timed-out",
|
|
27
30
|
"custody-lost",
|
|
31
|
+
// A release check that didn't cover what it owed, or lost a required test (release-once.md): never a pass.
|
|
32
|
+
"coverage-incomplete",
|
|
28
33
|
];
|
|
29
34
|
/**
|
|
30
35
|
* What the repository's approved verification command did: not configured; configured but the run could not attempt
|
|
@@ -59,7 +64,8 @@ export const receiptLogSchema = z.strictObject({
|
|
|
59
64
|
redacted: z.boolean(),
|
|
60
65
|
captureStatus: z.enum(["ok", "failed"]).nullable(),
|
|
61
66
|
});
|
|
62
|
-
export const RECEIPT_VERSION =
|
|
67
|
+
export const RECEIPT_VERSION = 3;
|
|
68
|
+
export const RECEIPT_COVERAGES = ["full", "composed", "partial", "identity-removed"];
|
|
63
69
|
const receiptFields = {
|
|
64
70
|
run: z.int(),
|
|
65
71
|
head: z.string(),
|
|
@@ -74,14 +80,25 @@ export const verificationReceiptSchema = versioned(RECEIPT_VERSION, {
|
|
|
74
80
|
command: receiptCommandSchema,
|
|
75
81
|
result: verifyCommandFactsSchema,
|
|
76
82
|
log: receiptLogSchema,
|
|
83
|
+
/** Version 3: what the check covered, apart from its exit. */
|
|
84
|
+
coverage: z.enum(RECEIPT_COVERAGES).optional(),
|
|
85
|
+
/** Version 3: the attempt sealed before the check ran, and its sealed report (null: it gave none). */
|
|
86
|
+
check: z.strictObject({ attempt: z.enum(["full", "targeted"]), report: z.string().nullable(), inventory: z.string().nullable(),
|
|
87
|
+
/** The command's own exit, kept here when `result` reads a failed attempt because the coverage fell short. */
|
|
88
|
+
exitCode: z.int().min(0).max(255).nullable().optional() }).optional(),
|
|
89
|
+
/** Version 3, composed: the full check A and every link the reader proves again. */
|
|
90
|
+
composed: z.strictObject({
|
|
91
|
+
base: z.int().min(1), baseReceipt: z.string(), baseReport: z.string(), authority: z.int().min(1), carries: z.array(z.int()),
|
|
92
|
+
artifacts: z.string(), plan: z.string(), snapshot: z.string(), report: z.string(),
|
|
93
|
+
}).optional(),
|
|
77
94
|
});
|
|
78
|
-
/** A version 1 receipt read as version
|
|
79
|
-
* is refused by its reader, which still knows the version
|
|
95
|
+
/** A version 1 or 2 receipt read as version 3: the same fields. Reuse is a version 2 field and coverage a version 3
|
|
96
|
+
* one; a receipt naming a field newer than it was sealed as is refused by its reader, which still knows the version. */
|
|
80
97
|
export function upgradeDirectReceipt(body) {
|
|
81
98
|
return { ...body, version: RECEIPT_VERSION };
|
|
82
99
|
}
|
|
83
|
-
export const RECEIPT_UPGRADES = { 1: upgradeDirectReceipt };
|
|
84
|
-
/** Read a receipt: version
|
|
100
|
+
export const RECEIPT_UPGRADES = { 1: upgradeDirectReceipt, 2: upgradeDirectReceipt };
|
|
101
|
+
/** Read a receipt: version 3 as itself, 1 and 2 upgraded, a newer one refused plainly. */
|
|
85
102
|
export function readVerificationReceipt(input) {
|
|
86
103
|
return readVersioned(verificationReceiptSchema, input, RECEIPT_UPGRADES);
|
|
87
104
|
}
|
|
@@ -39,6 +39,7 @@ export declare const legacyGateViewSchema: z.ZodObject<{
|
|
|
39
39
|
failure: z.ZodOptional<z.ZodEnum<{
|
|
40
40
|
"checkout-moved": "checkout-moved";
|
|
41
41
|
"cleanliness-unavailable": "cleanliness-unavailable";
|
|
42
|
+
"coverage-incomplete": "coverage-incomplete";
|
|
42
43
|
"custody-lost": "custody-lost";
|
|
43
44
|
"dependency-missing": "dependency-missing";
|
|
44
45
|
"dependency-still-missing": "dependency-still-missing";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Store } from "./store.js";
|
|
2
|
+
export type DeploymentGate = {
|
|
3
|
+
ok: true;
|
|
4
|
+
taskId: string;
|
|
5
|
+
head: string;
|
|
6
|
+
repo: string;
|
|
7
|
+
approvedBy: string;
|
|
8
|
+
} | {
|
|
9
|
+
ok: false;
|
|
10
|
+
message: string;
|
|
11
|
+
};
|
|
12
|
+
/** Whether run `runId` is a deployable candidate now, read only from the plane's records and this runtime's code. */
|
|
13
|
+
export declare function deploymentGate(store: Store, input: {
|
|
14
|
+
runId: number;
|
|
15
|
+
evidenceRoot: string;
|
|
16
|
+
now: Date;
|
|
17
|
+
}): DeploymentGate;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The deployment gate, in the runtime that is releasing (docs/plans/approval-policy.md §1.7): whether a candidate run
|
|
3
|
+
* may be deployed at all — a person approved its scope, its exact result passed the approved check and was marked
|
|
4
|
+
* complete, and nothing still blocks it. `toolroll release` asks this BEFORE it starts anything from the candidate's
|
|
5
|
+
* checkout, so the candidate's own scripts never decide their own deployment. (scripts/deploy-candidate.mjs keeps the
|
|
6
|
+
* same checks for the deployment script, proven again there with the installed runtime's modules.)
|
|
7
|
+
*/
|
|
8
|
+
import { assignmentOf } from "./assignment.js";
|
|
9
|
+
import { liveReleaseOf, personApprovalOf } from "./scope.js";
|
|
10
|
+
import { verificationEvidence } from "./verification-evidence.js";
|
|
11
|
+
/** Whether run `runId` is a deployable candidate now, read only from the plane's records and this runtime's code. */
|
|
12
|
+
export function deploymentGate(store, input) {
|
|
13
|
+
const refuse = (message) => ({ ok: false, message });
|
|
14
|
+
const run = store.getRun(input.runId);
|
|
15
|
+
const ref = run === null ? null : store.refById(run.taskRef);
|
|
16
|
+
const scope = ref === null ? null : store.getScope(ref.externalId);
|
|
17
|
+
const head = run?.headRevision ?? null;
|
|
18
|
+
if (run === null || ref === null || head === null)
|
|
19
|
+
return refuse(`Run ${input.runId} is not a built candidate.`);
|
|
20
|
+
// `task regate` reruns the approved check on the last built commit and records no-change: a candidate only for that commit.
|
|
21
|
+
const regated = run.outcome === "no-change" && scope?.candidate === head &&
|
|
22
|
+
store.runsFor(run.taskRef).find(one => one.id < input.runId && one.role === "builder" && one.outcome === "built")?.headRevision === head;
|
|
23
|
+
if (run.role !== "builder" || !(run.outcome === "built" || regated))
|
|
24
|
+
return refuse(`Run ${input.runId} is not a built candidate at ${head.slice(0, 7)}.`);
|
|
25
|
+
if (scope === null || scope.approvedDigest !== scope.digest || scope.digest !== run.scopeDigest)
|
|
26
|
+
return refuse("The run's signed scope is no longer the approved one.");
|
|
27
|
+
// One approval per release (release-once.md): a release's task deploys only while its authority stands for it — the
|
|
28
|
+
// live predicate proves the anchor's person approval (a carried commit's own seal is never a person's). Any other task
|
|
29
|
+
// needs a person's approval of its own.
|
|
30
|
+
const release = liveReleaseOf(store, ref.externalId, "deploy", input.now);
|
|
31
|
+
if (release.applies && !release.ok)
|
|
32
|
+
return refuse(`This release can't deploy: ${release.why}.`);
|
|
33
|
+
const anchorPerson = release.applies && release.ok ? personApprovalOf(store, release.authority.anchorTask) : null;
|
|
34
|
+
const person = anchorPerson ?? personApprovalOf(store, ref.externalId);
|
|
35
|
+
if (!person.ok)
|
|
36
|
+
return refuse(`A person has to approve this candidate's scope: ${person.why}.`);
|
|
37
|
+
const evidence = verificationEvidence(store, input.evidenceRoot, input.runId);
|
|
38
|
+
if (!evidence.ok || evidence.bytes === null)
|
|
39
|
+
return refuse(`Native check unavailable: ${evidence.ok ? "no receipt" : evidence.problem}.`);
|
|
40
|
+
let gate;
|
|
41
|
+
try {
|
|
42
|
+
gate = JSON.parse(evidence.bytes.toString());
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return refuse("The native check's receipt can't be read.");
|
|
46
|
+
}
|
|
47
|
+
const live = ref.repo === null ? null : store.liveVerifyCommand(ref.repo);
|
|
48
|
+
const result = gate["result"];
|
|
49
|
+
// A release check's coverage (receipt version 3): partial or identity-removed never deploys, whatever the exit.
|
|
50
|
+
if (gate["coverage"] !== undefined && gate["coverage"] !== "full" && gate["coverage"] !== "composed")
|
|
51
|
+
return refuse(`The release check didn't cover this candidate (${String(gate["coverage"])}).`);
|
|
52
|
+
const command = gate["command"];
|
|
53
|
+
if (gate["run"] !== input.runId || gate["head"] !== head || gate["scopeDigest"] !== scope.digest || result?.ran !== true || result.exitCode !== 0
|
|
54
|
+
|| live === null || command?.command !== live.command)
|
|
55
|
+
return refuse("The native check did not pass this candidate under the currently approved command.");
|
|
56
|
+
const assignment = assignmentOf(store, ref.externalId, input.now, { principal: "operator", repos: null, includeUnplaced: true }, input.evidenceRoot);
|
|
57
|
+
if (assignment?.state !== "complete" || assignment.receipt?.runId !== input.runId || assignment.receipt.head !== head
|
|
58
|
+
|| assignment.receipt.scopeDigest !== scope.digest || assignment.completion?.digest !== assignment.receipt.digest)
|
|
59
|
+
return refuse("The lead or user has not marked this exact result complete.");
|
|
60
|
+
const blockers = assignment.readiness?.blockers;
|
|
61
|
+
if (!Array.isArray(blockers))
|
|
62
|
+
return refuse("The result's readiness could not be read.");
|
|
63
|
+
if (blockers.length > 0)
|
|
64
|
+
return refuse(`This result isn't ready to deploy: ${blockers.map(one => one.message).join(" ")}`);
|
|
65
|
+
if (ref.repo === null)
|
|
66
|
+
return refuse("The candidate's task is in no project.");
|
|
67
|
+
return { ok: true, taskId: ref.externalId, head, repo: ref.repo, approvedBy: person.by };
|
|
68
|
+
}
|
package/dist/dispatch.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* fleet observations on top for people and agents reading the queue. A
|
|
7
7
|
* diagnosis can go stale; it can never grant a lease.
|
|
8
8
|
*/
|
|
9
|
+
import { authorityStandsSql } from "./approval-authority.js";
|
|
9
10
|
import { isAlive } from "./runner.js";
|
|
10
11
|
import { updateAdmissionPaused, UPDATE_PAUSED } from "./desktop-update-gate.js";
|
|
11
12
|
import { approvalOf } from "./scope.js";
|
|
@@ -15,8 +16,6 @@ import { REVIEW_TOKENS, resultStatusOf, reviewFactsOf } from "./workspace-ui.js"
|
|
|
15
16
|
import { replacedWords } from "./task-status.js";
|
|
16
17
|
import { failedAttemptSentence, latestFinishedAttempt } from "./needs-you.js";
|
|
17
18
|
import { buildReviewOf } from "./review-switch.js";
|
|
18
|
-
import { grantStandsSql } from "./flow-unattended.js";
|
|
19
|
-
import { accountReachesSql } from "./project-access.js";
|
|
20
19
|
import { BUILT_IN, parseCapabilityKey } from "./store.js";
|
|
21
20
|
export const DEFAULT_MAX_OPEN_DECISIONS = 5;
|
|
22
21
|
/**
|
|
@@ -77,14 +76,8 @@ export function scopeApprovedForDispatch(store, taskRef, now) {
|
|
|
77
76
|
.prepare(`SELECT 1 AS hit FROM task_scope
|
|
78
77
|
JOIN task_ref ON task_ref.id = ? AND task_scope.task_id = task_ref.external_id
|
|
79
78
|
WHERE task_scope.approved_digest = task_scope.digest AND task_scope.approved_at IS NOT NULL
|
|
80
|
-
AND (
|
|
81
|
-
|
|
82
|
-
JOIN approver signer ON signer.name = om.signed_by
|
|
83
|
-
WHERE om.repo = task_ref.repo AND om.revoked_at IS NULL
|
|
84
|
-
AND om.absolute_expiry > ? AND om.digest = task_scope.mode_digest
|
|
85
|
-
AND signer.revoked_at IS NULL AND signer.role = 'approver' AND ${accountReachesSql("signer", "task_ref.repo")}))
|
|
86
|
-
OR (task_scope.approval_basis = 'grant' AND ${grantStandsSql("task_scope.mode_digest", "?", "task_ref.repo")}))`)
|
|
87
|
-
.get(taskRef, now.toISOString(), now.toISOString());
|
|
79
|
+
AND ${authorityStandsSql("task_scope", "task_ref.repo", "?")}`)
|
|
80
|
+
.get(taskRef, now.toISOString(), now.toISOString(), now.toISOString());
|
|
88
81
|
return row !== undefined;
|
|
89
82
|
}
|
|
90
83
|
/** The phase the ordinary dispatch loop would choose before it resolves an
|
package/dist/dispose.js
CHANGED
|
@@ -19,6 +19,7 @@ import { modeTermsFromJson } from "./modes.js";
|
|
|
19
19
|
import { evidenceRoot, readVerifiedArtifact, writeEvidenceFile } from "./evidence.js";
|
|
20
20
|
import { queueBuildReview } from "./build-review.js";
|
|
21
21
|
import { homedir } from "node:os";
|
|
22
|
+
import { nightlyTaskRef, releaseTaskRef } from "./store.js";
|
|
22
23
|
import {} from "./proof.js";
|
|
23
24
|
import { verificationEvidence, failedVerificationEvidence } from "./verification-evidence.js";
|
|
24
25
|
import { classifyGateFailure, describeGateFailure, gateFailureSummary } from "./gate-failure.js";
|
|
@@ -162,7 +163,9 @@ function disposeBuildOutcomeLocked(context, result) {
|
|
|
162
163
|
grant.publishOn !== "complete" &&
|
|
163
164
|
headSha !== null &&
|
|
164
165
|
headWithin(branch, grant.headPrefix) &&
|
|
165
|
-
(grant.selector === "all" || origin === "ours")
|
|
166
|
+
(grant.selector === "all" || origin === "ours") &&
|
|
167
|
+
// A nightly check or a release's task is never published (store.createPublicationIntent refuses both).
|
|
168
|
+
!nightlyTaskRef(store, taskRef) && !releaseTaskRef(store, taskRef)) {
|
|
166
169
|
const intentId = store.createPublicationIntent({
|
|
167
170
|
run: runId,
|
|
168
171
|
taskRef,
|
package/dist/evidence-pack.d.ts
CHANGED
package/dist/evidence-pack.js
CHANGED
|
@@ -15,6 +15,7 @@ import { rulesWords } from "./approval-policy.js";
|
|
|
15
15
|
import { assignmentOf } from "./assignment.js";
|
|
16
16
|
import { projectName } from "./project.js";
|
|
17
17
|
import { changedFilesOf } from "./result-completion.js";
|
|
18
|
+
import { approvalProofOf, personProof } from "./scope.js";
|
|
18
19
|
import { html, htmlString, joinHtml, styleElement } from "./html.js";
|
|
19
20
|
export const EVIDENCE_PACK_FORMAT = "standing-orders/evidence-pack/v1";
|
|
20
21
|
/** How to recheck one entry's seal, for whoever reads the JSON. */
|
|
@@ -55,7 +56,7 @@ export function evidencePack(store, taskId, access, who, now, evidenceRoot, chec
|
|
|
55
56
|
goal: scope.goal, outOfScope: scope.outOfScope, touches: scope.touches, acceptance: scope.acceptance.map(one => ({ id: one.id, statement: one.statement })),
|
|
56
57
|
budgetUsd: scope.budgetMicrousd === null ? null : scope.budgetMicrousd / 1_000_000, digest: scope.digest,
|
|
57
58
|
approved: scope.approvedBy === null || scope.approvedAt === null || scope.approvedDigest === null ? null
|
|
58
|
-
: { by: scope.approvedBy, at: scope.approvedAt, digest: scope.approvedDigest, basis: scope.approvalBasis
|
|
59
|
+
: { by: scope.approvedBy, at: scope.approvedAt, digest: scope.approvedDigest, basis: approvalBasisWord(store, version.id, scope.approvalBasis ?? null, scope.approvedBy, scope.approvedDigest), current: scope.approvedDigest === scope.digest },
|
|
59
60
|
writtenBy: history.authors,
|
|
60
61
|
approvals: history.votes,
|
|
61
62
|
},
|
|
@@ -206,7 +207,7 @@ export function evidencePackHtml(pack) {
|
|
|
206
207
|
...(scope.touches.length ? [["Touches", joinHtml(scope.touches, ", ")]] : []),
|
|
207
208
|
...(scope.acceptance.length ? [["Acceptance", html `<ol class="criteria">${scope.acceptance.map(one => html `<li>${one.statement}</li>`)}</ol>`]] : []),
|
|
208
209
|
...(scope.writtenBy.length ? [["Scope written by", joinHtml([...new Set(scope.writtenBy.map(one => one.author))], ", ")]] : []),
|
|
209
|
-
["Approved", approved === null ? html `Not approved` : html `${approved.by}${approved.basis === "mode" ? " (operating mode)" : approved.basis === "flow" ? " (flow runs on its own)" : ""} · ${when(approved.at)}${approved.current ? "" : " · the scope changed after this"}`],
|
|
210
|
+
["Approved", approved === null ? html `Not approved` : html `${approved.by}${approved.basis === "mode" ? " (operating mode)" : approved.basis === "flow" ? " (flow runs on its own)" : approved.basis === "lead" ? " (the lead, not a person)" : approved.basis === "schedule" ? " (standing order)" : approved.basis === "carried" ? " (carried from their release approval)" : approved.basis === "unknown" ? " (no person's proof on record)" : ""} · ${when(approved.at)}${approved.current ? "" : " · the scope changed after this"}`],
|
|
210
211
|
...(approvals.length > 1 ? [["Approvals", joinHtml(approvals.map(one => html `${one.approver} · ${when(one.at)}`), html `<br>`)]] : []),
|
|
211
212
|
["Scope digest", short(scope.digest)],
|
|
212
213
|
]),
|
|
@@ -252,3 +253,20 @@ export function standaloneEvidenceHtml(pack) {
|
|
|
252
253
|
return htmlString(html `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Evidence pack · ${pack.task.id}</title>${styleElement(css)}</head><body>${evidencePackHtml(pack)}</body></html>
|
|
253
254
|
`);
|
|
254
255
|
}
|
|
256
|
+
/** Who approved, in the pack's words (approval policy §1.5): `person` only when a person's proof stands behind it. */
|
|
257
|
+
function approvalBasisWord(store, taskId, basis, by, digest) {
|
|
258
|
+
if (basis === "mode")
|
|
259
|
+
return "mode";
|
|
260
|
+
if (basis === "grant")
|
|
261
|
+
return "flow";
|
|
262
|
+
if (basis === "schedule")
|
|
263
|
+
return "schedule";
|
|
264
|
+
// A later release commit carried under a person's release approval (release-once.md): never a person's own yes.
|
|
265
|
+
if (basis === "carried")
|
|
266
|
+
return "carried";
|
|
267
|
+
if (basis === "lead")
|
|
268
|
+
return "lead";
|
|
269
|
+
if (basis === "password" || basis === "chat")
|
|
270
|
+
return personProof(approvalProofOf(store, taskId, by, digest)?.method) ? "person" : "unknown";
|
|
271
|
+
return "unknown";
|
|
272
|
+
}
|
package/dist/evidence.d.ts
CHANGED
|
@@ -201,6 +201,7 @@ export declare function storeEvidence(store: Store, root: string, runId: number,
|
|
|
201
201
|
redacted?: boolean;
|
|
202
202
|
captureStatus?: "ok" | "failed";
|
|
203
203
|
sourceBytesOriginal?: number;
|
|
204
|
+
cap?: number;
|
|
204
205
|
}): number;
|
|
205
206
|
/**
|
|
206
207
|
* Read an artifact's bytes back, believing nothing until it is proved on the
|
package/dist/evidence.js
CHANGED
|
@@ -397,7 +397,8 @@ export function imageDimensions(bytes, kind) {
|
|
|
397
397
|
* store the caller passed — inside whatever transaction the caller holds.
|
|
398
398
|
*/
|
|
399
399
|
export function storeEvidence(store, root, runId, kind, name, content, capture, now, options = {}) {
|
|
400
|
-
|
|
400
|
+
// `cap`: a sealed structured payload with its own documented bound (a release check's report: 256 KiB), never above 1 MiB.
|
|
401
|
+
const cap = options.cap === undefined ? EVIDENCE_CAPS[kind] : Math.min(options.cap, 1024 * 1024);
|
|
401
402
|
const stored = content.subarray(0, cap);
|
|
402
403
|
// Some callers that deliberately summarize/bound a source before storage
|
|
403
404
|
// may still report its true original size. This keeps `truncated` honest
|
package/dist/flow-engine.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* waits for people. See flows.ts for what each zone does.
|
|
7
7
|
*/
|
|
8
8
|
import { assignmentOf } from "./assignment.js";
|
|
9
|
+
import { currentActor } from "./actor.js";
|
|
10
|
+
import { activeRemote } from "./remote-run.js";
|
|
9
11
|
import { diagnoseTaskDispatch } from "./dispatch.js";
|
|
10
12
|
import { passOn, TEXT_LIMITS } from "./text-limits.js";
|
|
11
13
|
import { deciderOf, durationWords, fillFlowText, flowGoal, flowValueLabel, flowWorkTemplate, validateFlowDefinition, withinHours } from "./flows.js";
|
|
@@ -528,7 +530,9 @@ export function decideFlowCard(store, input, now) {
|
|
|
528
530
|
ledger("approved");
|
|
529
531
|
return { ok: true, said: "Approved. The card is done." };
|
|
530
532
|
}
|
|
531
|
-
|
|
533
|
+
// Who decided, from trusted context (approval policy §1.7): only a person's approval lets a Pull request zone merge.
|
|
534
|
+
const authority = input.subagent !== undefined ? "subagent" : currentActor()?.lead === true ? "lead" : activeRemote() !== null ? "remote" : "person";
|
|
535
|
+
if (!store.moveFlowCard(card.id, { to: stage.next, outcome: "approved", actor: input.actor, note: input.note, expectEntry: card.entry, authority }, now))
|
|
532
536
|
return { ok: false, message: "That card has moved on since; nothing was changed." };
|
|
533
537
|
ledger("approved");
|
|
534
538
|
const title = definition.stages.find(one => one.id === stage.next)?.title ?? stage.next;
|
|
@@ -5,7 +5,10 @@ export type PullRequestIo = {
|
|
|
5
5
|
gh: Runner;
|
|
6
6
|
evidenceRoot?: string;
|
|
7
7
|
};
|
|
8
|
-
/** Whether a person approved this card at a "Person decides" zone since it was last built; their name when so.
|
|
9
|
-
|
|
8
|
+
/** Whether a person approved this card at a "Person decides" zone since it was last built; their name when so.
|
|
9
|
+
* The MOST RECENT decision governs (approval policy §1.7): it must be a person's recorded decision, after the card's
|
|
10
|
+
* last build by the current definition AND after the exact build being merged finished (`builtAt`), so editing the
|
|
11
|
+
* flow can't let an older person decision stand behind a newer machine one. */
|
|
12
|
+
export declare function approvedSinceBuild(store: Store, definition: FlowDefinition, card: FlowCardRow, builtAt?: string | null): string | null;
|
|
10
13
|
/** One pass of a Pull request zone for one card. Returns whether it changed anything. */
|
|
11
14
|
export declare function pullRequestStep(store: Store, flow: FlowRow, definition: FlowDefinition, stage: FlowStage, card: FlowCardRow, now: Date, io: PullRequestIo): Promise<boolean>;
|
|
@@ -19,15 +19,24 @@ import { adoptPublishedPullRequest } from "./publish.js";
|
|
|
19
19
|
import { failingCheckOf, mergePullRequest, owePullRequest, pullRequestBlocker, pullRequestFollowOf } from "./pull-request-flow.js";
|
|
20
20
|
import { requestResultChanges } from "./result-actions.js";
|
|
21
21
|
import { revisionSourceOf } from "./result-review.js";
|
|
22
|
-
/** Whether a person approved this card at a "Person decides" zone since it was last built; their name when so.
|
|
23
|
-
|
|
22
|
+
/** Whether a person approved this card at a "Person decides" zone since it was last built; their name when so.
|
|
23
|
+
* The MOST RECENT decision governs (approval policy §1.7): it must be a person's recorded decision, after the card's
|
|
24
|
+
* last build by the current definition AND after the exact build being merged finished (`builtAt`), so editing the
|
|
25
|
+
* flow can't let an older person decision stand behind a newer machine one. */
|
|
26
|
+
export function approvedSinceBuild(store, definition, card, builtAt = null) {
|
|
24
27
|
const kindOf = (id) => definition.stages.find(one => one.id === id)?.kind ?? null;
|
|
25
28
|
const events = store.flowEvents(card.id);
|
|
26
29
|
let built = -1;
|
|
27
30
|
events.forEach((event, index) => { if (kindOf(event.fromStage) === "task" || kindOf(event.toStage) === "task")
|
|
28
31
|
built = index; });
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
// The latest DECISION of any kind governs: only decideFlowCard writes `approved` or `sent-back` (from a decision
|
|
33
|
+
// zone), whatever the zones are called now, so a send-back after a yes takes that yes back.
|
|
34
|
+
const latest = events.slice(built + 1).reverse().find(event => event.outcome === "approved" || event.outcome === "sent-back");
|
|
35
|
+
if (latest === undefined || latest.authority !== "person")
|
|
36
|
+
return null;
|
|
37
|
+
if (builtAt !== null && latest.at < builtAt)
|
|
38
|
+
return null;
|
|
39
|
+
return latest.actor;
|
|
31
40
|
}
|
|
32
41
|
/** One pass of a Pull request zone for one card. Returns whether it changed anything. */
|
|
33
42
|
export async function pullRequestStep(store, flow, definition, stage, card, now, io) {
|
|
@@ -134,7 +143,7 @@ export async function pullRequestStep(store, flow, definition, stage, card, now,
|
|
|
134
143
|
// Checks pass.
|
|
135
144
|
if (stage.merge === undefined)
|
|
136
145
|
return passed(`${pr} passed its checks.`, `Pull request: ${link} (checks passed)`);
|
|
137
|
-
const approver = approvedSinceBuild(store, definition, card);
|
|
146
|
+
const approver = approvedSinceBuild(store, definition, card, store.getRun(runId)?.finishedAt ?? null);
|
|
138
147
|
if (approver === null)
|
|
139
148
|
return waiting(`Checks passed on ${pr}, but no person approved this card since it was built, so it wasn't merged. Merge it from its task, or send it through a decision.`);
|
|
140
149
|
const merged = await mergePullRequest(store, { runId, by: `${approver} (approved in ${flow.name})`, exec: io.gh, method: stage.merge, clock: () => now });
|
package/dist/flow-schedule.js
CHANGED
|
@@ -14,6 +14,7 @@ import { legOf, routeDigestOf, routeFromJson, routeProblems } from "./phase-rout
|
|
|
14
14
|
import { canonicalAcceptance, digestOf, parseAcceptanceCriteria, profileDigestOf, profileFromJson } from "./scope.js";
|
|
15
15
|
import { hasDisguisedText, hasForbiddenControls } from "./decision.js";
|
|
16
16
|
import { BUILT_IN, parseCapabilityKey } from "./store.js";
|
|
17
|
+
import { fileNightlyCheck } from "./nightly-check.js";
|
|
17
18
|
export const WEEKDAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
18
19
|
export function validTimezone(value) {
|
|
19
20
|
if (value.trim() !== value)
|
|
@@ -195,10 +196,22 @@ export class StandingOrderRefused extends Error {
|
|
|
195
196
|
*/
|
|
196
197
|
export function fileStandingOrder(store, input, now) {
|
|
197
198
|
const { order, repo } = input;
|
|
199
|
+
// The nightly full check on main files a check-only task of its own (nightly-check.ts).
|
|
200
|
+
if (order.check !== undefined)
|
|
201
|
+
return fileNightlyCheck(store, input, now);
|
|
198
202
|
const blocker = store.standingOrderBlocker(input.trigger, order.routine, now);
|
|
199
203
|
if (blocker !== null)
|
|
200
204
|
return { made: "skipped", note: `Skipped: the last one (${blocker.taskId}, ${blocker.state}) hasn't finished.` };
|
|
201
|
-
const
|
|
205
|
+
const standing = standingApproval(order, repo, input.schedule);
|
|
206
|
+
// The carried approval stands only while its approver still can approve here (approval policy §1.6): a demoted
|
|
207
|
+
// person, one off the project, or an approval that names nobody files an ordinary proposal instead.
|
|
208
|
+
const approverStands = standing.live && standing.by !== null && (() => {
|
|
209
|
+
const account = store.accountOf(standing.by);
|
|
210
|
+
return account !== null && account.revokedAt === null && account.role === "approver" && store.accountCanAccess(standing.by, repo);
|
|
211
|
+
})();
|
|
212
|
+
const approval = standing.live && !approverStands
|
|
213
|
+
? { live: false, problem: standing.by === null ? "it names no approver" : "its approver can no longer approve in this project", acceptance: standing.acceptance }
|
|
214
|
+
: standing;
|
|
202
215
|
if (approval.acceptance === null)
|
|
203
216
|
return { made: "skipped", note: "Skipped: its success checks can't be read. Set the flow up again." };
|
|
204
217
|
if (order.costCeilingUsd !== null) {
|
|
@@ -238,11 +251,17 @@ export function fileStandingOrder(store, input, now) {
|
|
|
238
251
|
store.setRequirements(ref.id, order.requirements);
|
|
239
252
|
const draft = { goal: order.goal, outOfScope: order.outOfScope, touches: [...order.touches], acceptance: approval.acceptance, ...(order.budgetPerRunMicrousd == null ? {} : { budgetMicrousd: order.budgetPerRunMicrousd }) };
|
|
240
253
|
const digest = digestOf(draft, approval.profile, approval.route);
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
254
|
+
// Filed unapproved; only the seal below approves it (no approval fields are written beside it).
|
|
255
|
+
store.saveScope({ taskId, ...draft, proposedAt: now.toISOString(), digest, budgetMicrousd: order.budgetPerRunMicrousd ?? null, approvedAt: null, approvedBy: null, approvedDigest: null }, {}, { profile: approval.profile, route: approval.route });
|
|
256
|
+
// Its own authority (approval policy §1.6): the standing order's carried approval, never a person's ceremony. A seal
|
|
257
|
+
// the project's rules refuse to an automatic approval (checks beyond the sandbox, protected work, policy) leaves the
|
|
258
|
+
// task filed and waiting for a person, as any unapproved firing does — never a dropped firing.
|
|
259
|
+
const sealed = store.sealScopeApproval(taskId, approval.by, now, {}, { kind: "schedule", modeDigest: order.approval.digest }, "automation");
|
|
260
|
+
if (!sealed)
|
|
261
|
+
return { made: "filed", taskId, approved: false, note: "Its approval can't stand on its own here (its checks reach beyond the sandbox, or the project's rules need a person), so this one waits for approval." };
|
|
262
|
+
const sealedRoute = store.sealedRouteOf(taskId);
|
|
263
|
+
if (sealedRoute === null || !sealedRoute.ok || routeDigestOf(sealedRoute.route) !== routeDigestOf(approval.route)) {
|
|
264
|
+
throw new StandingOrderRefused(sealedRoute !== null && !sealedRoute.ok ? sealedRoute.detail : "the sealed agents aren't the approved ones");
|
|
246
265
|
}
|
|
247
266
|
return { made: "filed", taskId, approved: true, note: null };
|
|
248
267
|
}
|
package/dist/flows-ui.js
CHANGED
|
@@ -8,6 +8,7 @@ import { SENDER_WORDS } from "./webhook-signing.js";
|
|
|
8
8
|
import { READER_CAPTURED } from "./webhook-readers.js";
|
|
9
9
|
import { hostOf, linkWords, provedWords, snapshotOf } from "./task-source.js";
|
|
10
10
|
import { deciderOf, FLOW_COLORS, FLOW_KIND_WORDS, FLOW_STAGE_KINDS } from "./flows.js";
|
|
11
|
+
import { nightlyHealthWords } from "./nightly-check.js";
|
|
11
12
|
import { flowInsights, troubleWords } from "./flow-insights.js";
|
|
12
13
|
import { parseSortDecision, sortChip } from "./flow-sort.js";
|
|
13
14
|
import { flowSecretNames, readEmailSettings, sendingReady } from "./flow-actions.js";
|
|
@@ -68,7 +69,7 @@ export function flowsListHtml(store, flows, projects, canCreate, problem) {
|
|
|
68
69
|
const definition = flowDefinitionOf(flow);
|
|
69
70
|
const waiting = cards.filter(card => ["approval", "choose"].includes(definition?.stages.find(one => one.id === card.stage)?.kind ?? "")).length;
|
|
70
71
|
const buttons = store.flowTriggers(flow.id).filter(one => one.state === "active").flatMap(one => { const config = triggerConfigOf(one); return config?.kind === "button" ? [{ id: one.id, label: config.label }] : []; });
|
|
71
|
-
const trouble = troubleWords(flowInsights(store, flow, new Date(), 7));
|
|
72
|
+
const trouble = troubleWords(flowInsights(store, flow, new Date(), 7)) ?? nightlyHealthOf(store, flow);
|
|
72
73
|
return html `<article class="card"><div class="flow-row"><h2><a href="/flows/${flow.id}">${flow.name}</a></h2><span class="flow-counts">${projectName(flow.repo)} · ${cards.length} card${cards.length === 1 ? "" : "s"} in progress${waiting > 0 ? ` · ${waiting} waiting for a decision` : ""}${trouble === null ? "" : ` · ${trouble}`}</span></div>${buttons.length > 0 && html `<p class="flow-buttons">${joinHtml(buttons.map(one => html `<a class="button-link" href="/flows/${flow.id}?start=${one.id}">${one.label}</a>`), " ")}</p>`}</article>`;
|
|
73
74
|
});
|
|
74
75
|
const projectField = projects.length === 1 ? html `<input type="hidden" name="repo" value="${projects[0]}">` : html `<label>Project<select name="repo">${projects.map(repo => html `<option value="${repo}">${projectName(repo)}</option>`)}</select></label>`;
|
|
@@ -275,3 +276,8 @@ export function flowFallbackHtml(view) {
|
|
|
275
276
|
return html `<li><strong>${stage.title}</strong> · ${FLOW_KIND_WORDS[stage.kind].label}${cards.length > 0 && html `<ul>${cards.map(card => html `<li>${card.title}${card.waiting !== null && ` — ${card.waiting}`}</li>`)}</ul>`}</li>`;
|
|
276
277
|
})}</ol></section>`;
|
|
277
278
|
}
|
|
279
|
+
/** The nightly check's own health on its flow (release-once.md, part 3): nothing since the last firing for 26 hours. */
|
|
280
|
+
function nightlyHealthOf(store, flow) {
|
|
281
|
+
const nightly = store.flowTriggers(flow.id).find(one => one.state === "active" && triggerConfigOf(one)?.kind === "schedule" && triggerConfigOf(one).order?.check !== undefined);
|
|
282
|
+
return nightly === undefined ? null : nightlyHealthWords(store, flow.repo, nightly.updatedAt, new Date());
|
|
283
|
+
}
|
package/dist/invoke.d.ts
CHANGED
|
@@ -135,7 +135,25 @@ export declare function fencedCheckoutCommands(store: Store, repo: string, check
|
|
|
135
135
|
* under: open, or its allowlist through a proxy of its own (on Linux, relayed in by the sandbox runtime). Nothing about it is
|
|
136
136
|
* started unless all of it is.
|
|
137
137
|
*/
|
|
138
|
-
|
|
138
|
+
/**
|
|
139
|
+
* macOS: the sign-ins a check with sign-ins needs that live in the keychain, not in files: Claude Code's
|
|
140
|
+
* ("Claude Code-credentials", its current access token as CLAUDE_CODE_OAUTH_TOKEN) and gh's ("gh:github.com", as
|
|
141
|
+
* GH_TOKEN). Read here, by Toolroll, outside the sandbox, and handed to the check in its environment, so the check never
|
|
142
|
+
* reaches securityd (sandbox-runtime.ts SEATBELT_NO_KEYCHAIN) and nothing else in the keychain. The check can't renew
|
|
143
|
+
* Claude's token: one that has expired fails its sign-in. A value that isn't a plain token is left out.
|
|
144
|
+
*/
|
|
145
|
+
export declare const KEYCHAIN_SIGN_INS: readonly {
|
|
146
|
+
service: string;
|
|
147
|
+
env: string;
|
|
148
|
+
token(secret: string): string | null;
|
|
149
|
+
}[];
|
|
150
|
+
/** One keychain item's secret by its service, read with the system's `security` (null: none, or it can't be read). A
|
|
151
|
+
* keychain file may be named (tests: a throwaway one); never the item's value in an argument. */
|
|
152
|
+
export declare function readKeychainSecret(service: string, keychain?: string): string | null;
|
|
153
|
+
/** The keychain's sign-ins as a check's environment (macOS only; elsewhere, for anything missing, and where Toolroll
|
|
154
|
+
* keeps nothing in the keychain (TOOLROLL_SECRET_STORE=files: the test suite), nothing). */
|
|
155
|
+
export declare function keychainSignIns(platform?: NodeJS.Platform, read?: (service: string) => string | null): Record<string, string>;
|
|
156
|
+
export declare function signInsLaunch(store: Store, own: string, temp: string, checkout: string, policy: NetworkProfile, gitPaths: readonly string[], platform?: NodeJS.Platform, keychain?: () => Record<string, string>): Promise<{
|
|
139
157
|
grants: SignInGrants;
|
|
140
158
|
network: NetworkFence;
|
|
141
159
|
env: Record<string, string>;
|