toolroll 0.9.75 → 0.9.76
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 +49 -0
- package/dist/agent-fence.js +8 -0
- package/dist/approval-authority.js +6 -2
- package/dist/approval-rules-ui.d.ts +11 -0
- package/dist/approval-rules-ui.js +13 -1
- package/dist/batch-checks.js +3 -1
- package/dist/builder.js +1 -1
- package/dist/coding-workspace.js +7 -5
- package/dist/contracts/console-api.d.ts +1 -0
- package/dist/contracts/console-api.js +1 -0
- package/dist/deploy-gate.d.ts +3 -0
- package/dist/deploy-gate.js +3 -2
- package/dist/desktop-update.d.ts +1 -1
- package/dist/discover.js +6 -1
- package/dist/first-run.js +3 -1
- package/dist/git-incident.d.ts +49 -0
- package/dist/git-incident.js +69 -0
- package/dist/git-views.d.ts +20 -0
- package/dist/git-views.js +120 -0
- package/dist/grant.js +3 -1
- package/dist/operate.js +7 -1
- package/dist/planner.js +5 -1
- package/dist/project-knowledge.js +1 -1
- package/dist/project-learning.js +22 -2
- package/dist/pull-request-flow.js +4 -0
- package/dist/release-adapters.d.ts +24 -2
- package/dist/release-adapters.js +136 -10
- package/dist/release-authority.d.ts +46 -0
- package/dist/release-authority.js +64 -0
- package/dist/release-auto.d.ts +153 -0
- package/dist/release-auto.js +496 -0
- package/dist/release-cli.js +56 -2
- package/dist/release-coverage.js +1 -1
- package/dist/release.d.ts +105 -1
- package/dist/release.js +229 -9
- package/dist/repository-context.js +4 -6
- package/dist/result-follow-ups.js +3 -1
- package/dist/scope.d.ts +31 -2
- package/dist/scope.js +44 -3
- package/dist/scout.js +5 -1
- package/dist/server/render-tasks.d.ts +10 -0
- package/dist/server/render-tasks.js +23 -13
- package/dist/server/route-table.js +3 -0
- package/dist/server/settings.js +29 -2
- package/dist/server/task-views.js +8 -0
- package/dist/store.d.ts +1 -1
- package/dist/store.js +19 -4
- package/dist/task-base.js +1 -1
- package/dist/teardown-git.d.ts +38 -10
- package/dist/teardown-git.js +894 -108
- package/dist/test-infra.d.ts +26 -0
- package/dist/test-infra.js +321 -0
- package/dist/worktree.d.ts +4 -0
- package/dist/worktree.js +13 -7
- package/package.json +1 -1
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
/** Optional repository context. Callers admit project access; this module grants no authority.
|
|
2
2
|
* The index is disposable. Every read hashes the current bounded tracked source set;
|
|
3
3
|
* a missing/changed/unreadable index falls back to ordinary text search, never a task gate. */
|
|
4
|
-
import { inReadOnlyRequest } from './read-only-request.js';
|
|
5
4
|
import { checkoutGitSync } from './teardown-git.js';
|
|
6
5
|
import { createHash, randomUUID } from 'node:crypto';
|
|
7
|
-
import { execFileSync } from 'node:child_process';
|
|
8
6
|
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
9
7
|
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
10
8
|
import { scanForSecrets } from './evidence.js';
|
|
@@ -14,10 +12,10 @@ const CODE = /\.(?:[cm]?[jt]sx?)$/i;
|
|
|
14
12
|
const TEXT = /\.(?:[cm]?[jt]sx?|md|txt|css|html|json)$/i;
|
|
15
13
|
const MAX_NODES = 16_000, MAX_EDGES = 12_000;
|
|
16
14
|
const sha = (value) => createHash('sha256').update(value).digest('hex');
|
|
17
|
-
// Read-only and bounded
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
// Read-only and bounded, and always the hardened checkout git (a crew worktree is agent-written, and a page read or a
|
|
16
|
+
// lead's refresh reads it alike): no filter, hook, program or network, its config and info frozen in a view of the
|
|
17
|
+
// plane's own before they're inspected (teardown-git.ts checkoutGitSync).
|
|
18
|
+
const git = (repo, args) => checkoutGitSync(repo, args, { timeoutMs: 10_000, maxBuffer: 2_000_000 }).trimEnd();
|
|
21
19
|
const inside = (root, path) => { const r = relative(root, path); return r !== '..' && !r.startsWith('..' + sep) && !isAbsolute(r); };
|
|
22
20
|
function capture(request) {
|
|
23
21
|
const repo = realpathSync(request.repo);
|
|
@@ -4,6 +4,7 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { belongsToRelease } from "./release-authority.js";
|
|
6
6
|
import { run as execRun } from "./exec.js";
|
|
7
|
+
import { hardenedGit } from "./teardown-git.js";
|
|
7
8
|
import { approvedCommandShell, SETUP_ENV_ALLOWLIST, SETUP_ENV_DENYLIST } from "./builder.js";
|
|
8
9
|
import { runWithIsolatedDatabase } from "./child-database.js";
|
|
9
10
|
import { boundStreamHeadTail, readVerifiedArtifact, redactSecretLines, scanForSecrets, storeEvidence } from "./evidence.js";
|
|
@@ -161,7 +162,8 @@ export function waitingRequestsOfAnyKind(store, now) {
|
|
|
161
162
|
export async function runFollowUpCheck(store, root, request, deps = {}) {
|
|
162
163
|
const now = deps.now ?? (() => new Date());
|
|
163
164
|
const runner = deps.runner ?? execRun;
|
|
164
|
-
|
|
165
|
+
// The project's repository and a checkout of a build's commit, both agent-writable: the hardened git (teardown-git.ts).
|
|
166
|
+
const git = deps.git ?? hardenedGit(execRun);
|
|
165
167
|
const asked = store.handle.prepare("SELECT run_id FROM action_ledger WHERE id = ? AND action = ?").get(request, CHECKS_REQUESTED);
|
|
166
168
|
if (asked === undefined)
|
|
167
169
|
return null;
|
package/dist/scope.d.ts
CHANGED
|
@@ -418,7 +418,10 @@ export type ScopeAuthor = "mate" | "coordinator" | "scout" | "lead";
|
|
|
418
418
|
/** The authorities a seal can stand on (approval-policy plan §1.5). Anything else stands on nothing. */
|
|
419
419
|
/** `carried`: a later commit of a release carried under a person's release approval (release-once.md); only the carry
|
|
420
420
|
* transaction ever writes it. */
|
|
421
|
-
|
|
421
|
+
/** `auto-check` / `auto-green`: a release task the plane approved itself under the project's "Approve automatically when
|
|
422
|
+
* checks pass" (docs/plans/auto-approve-green.md) — `auto-check` lets its check run, `auto-green` is the release
|
|
423
|
+
* approval once CI and that check passed. Only release-auto.ts writes them; neither is ever a person's. */
|
|
424
|
+
export type ApprovalBasis = "password" | "mode" | "chat" | "grant" | "schedule" | "lead" | "carried" | "auto-check" | "auto-green";
|
|
422
425
|
export declare const APPROVAL_BASES: readonly ApprovalBasis[];
|
|
423
426
|
export type Approval = {
|
|
424
427
|
approved: true;
|
|
@@ -865,7 +868,7 @@ export declare function releaseTermsSentence(terms: ReleaseTerms): string;
|
|
|
865
868
|
export declare function releaseAsksAgainWords(terms: ReleaseTerms): string[];
|
|
866
869
|
export { personProof } from "./person-proof.js";
|
|
867
870
|
import { type ReleaseTerms } from "./contracts/release-terms.js";
|
|
868
|
-
import { type ReleaseAuthority, type ReleaseCarry } from "./release-authority.js";
|
|
871
|
+
import { type AutoEvidence, type ReleaseAuthority, type ReleaseCarry } from "./release-authority.js";
|
|
869
872
|
/** Whether the command running now is a machine's: the lead (its token) or a remote API run. Trusted context only. */
|
|
870
873
|
export declare const machineActing: () => boolean;
|
|
871
874
|
/** A proof row's detail, either form: the legacy text (`<at> · generation <n> · digest <d>`) or the versioned record
|
|
@@ -952,3 +955,29 @@ export declare function mainDebtProblem(store: Store, repo: string, taskId: stri
|
|
|
952
955
|
* chat it is approved only through the release card.
|
|
953
956
|
*/
|
|
954
957
|
export declare function deployGateScope(scope: Pick<Scope, "candidate" | "releaseTerms">): boolean;
|
|
958
|
+
/**
|
|
959
|
+
* The plane's own automatic approval of a release task (docs/plans/auto-approve-green.md), read the way the dispatch
|
|
960
|
+
* belts read it (release-authority.ts autoSealSql): sealed `kind` on the current digest, its evidence row naming exactly
|
|
961
|
+
* this task, kind, digest and candidate, a release task (candidate and terms signed in), the project's setting on now,
|
|
962
|
+
* and the person who turned it on still an approver there. Never a person's approval.
|
|
963
|
+
*/
|
|
964
|
+
export declare function autoReleaseApprovalOf(store: Store, taskId: string, kind: "auto-check" | "auto-green"): {
|
|
965
|
+
ok: true;
|
|
966
|
+
by: string;
|
|
967
|
+
at: string;
|
|
968
|
+
evidence: AutoEvidence;
|
|
969
|
+
} | {
|
|
970
|
+
ok: false;
|
|
971
|
+
why: string;
|
|
972
|
+
};
|
|
973
|
+
/** A release's approval: a person's (personApprovalOf, unchanged), or the plane's own `auto-green`. Only release
|
|
974
|
+
* readers ask this (liveReleaseOf, and the deploy gates inside `toolroll release`); everything else asks for a person. */
|
|
975
|
+
export declare function releaseApprovalOf(store: Store, taskId: string, _now?: Date): {
|
|
976
|
+
ok: true;
|
|
977
|
+
by: string;
|
|
978
|
+
at: string;
|
|
979
|
+
method: string;
|
|
980
|
+
} | {
|
|
981
|
+
ok: false;
|
|
982
|
+
why: string;
|
|
983
|
+
};
|
package/dist/scope.js
CHANGED
|
@@ -571,7 +571,7 @@ export function canonicalAcceptance(criteria) {
|
|
|
571
571
|
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
|
|
572
572
|
.map(c => ({ id: c.id, statement: c.statement.trim(), evidence: [...c.evidence].sort() }));
|
|
573
573
|
}
|
|
574
|
-
export const APPROVAL_BASES = ["password", "mode", "chat", "grant", "schedule", "lead", "carried"];
|
|
574
|
+
export const APPROVAL_BASES = ["password", "mode", "chat", "grant", "schedule", "lead", "carried", "auto-check", "auto-green"];
|
|
575
575
|
/**
|
|
576
576
|
* The exact bytes an operator agreed to.
|
|
577
577
|
*
|
|
@@ -1359,7 +1359,7 @@ function describeApproval(approval) {
|
|
|
1359
1359
|
export { personProof } from "./person-proof.js";
|
|
1360
1360
|
import { personProof } from "./person-proof.js";
|
|
1361
1361
|
import { releaseTermsDigest } from "./contracts/release-terms.js";
|
|
1362
|
-
import { CHECK_ACCESS_WIDTH, carriesUnsandboxed, newestReleaseGeneration, releaseAuthorityOfTask, releaseSealProblem } from "./release-authority.js";
|
|
1362
|
+
import { CHECK_ACCESS_WIDTH, autoEvidenceOf, carriesUnsandboxed, newestReleaseGeneration, releaseAuthorityOfTask, releaseAutoApproves, releaseSealProblem } from "./release-authority.js";
|
|
1363
1363
|
/** Whether the command running now is a machine's: the lead (its token) or a remote API run. Trusted context only. */
|
|
1364
1364
|
export const machineActing = () => currentActor()?.lead === true || activeRemote() !== null;
|
|
1365
1365
|
/** A proof row's detail, either form: the legacy text (`<at> · generation <n> · digest <d>`) or the versioned record
|
|
@@ -1400,6 +1400,8 @@ export function personApprovalOf(store, taskId) {
|
|
|
1400
1400
|
const basis = scope.approvalBasis ?? null;
|
|
1401
1401
|
if (basis === "lead")
|
|
1402
1402
|
return { ok: false, why: "approved by the lead, not by a person" };
|
|
1403
|
+
if (basis === "auto-check" || basis === "auto-green")
|
|
1404
|
+
return { ok: false, why: "approved automatically, not by a person" };
|
|
1403
1405
|
if (basis !== "password" && basis !== "chat")
|
|
1404
1406
|
return { ok: false, why: `approved by ${basis === null ? "an unknown road" : basis === "grant" ? "a flow" : basis}, not by a person` };
|
|
1405
1407
|
const origin = sealOriginOf(store, taskId, scope.approvedBy, scope.approvedDigest, scope.approvedAt);
|
|
@@ -1474,7 +1476,8 @@ export function liveReleaseOf(store, taskId, action, now = new Date()) {
|
|
|
1474
1476
|
if (a.tipTask !== taskId)
|
|
1475
1477
|
return refuse("this is not the release's current commit");
|
|
1476
1478
|
const anchor = store.getScope(a.anchorTask);
|
|
1477
|
-
|
|
1479
|
+
// A person's yes, or the plane's own automatic one while the project still approves releases automatically.
|
|
1480
|
+
const person = releaseApprovalOf(store, a.anchorTask, now);
|
|
1478
1481
|
if (!person.ok)
|
|
1479
1482
|
return refuse(`the release approval no longer stands: ${person.why}`);
|
|
1480
1483
|
if (anchor === null || anchor.approvedDigest !== a.anchorDigest || anchor.digest !== a.anchorDigest)
|
|
@@ -1539,3 +1542,41 @@ export function mainDebtProblem(store, repo, taskId) {
|
|
|
1539
1542
|
export function deployGateScope(scope) {
|
|
1540
1543
|
return (scope.candidate ?? null) !== null || (scope.releaseTerms ?? null) !== null;
|
|
1541
1544
|
}
|
|
1545
|
+
/**
|
|
1546
|
+
* The plane's own automatic approval of a release task (docs/plans/auto-approve-green.md), read the way the dispatch
|
|
1547
|
+
* belts read it (release-authority.ts autoSealSql): sealed `kind` on the current digest, its evidence row naming exactly
|
|
1548
|
+
* this task, kind, digest and candidate, a release task (candidate and terms signed in), the project's setting on now,
|
|
1549
|
+
* and the person who turned it on still an approver there. Never a person's approval.
|
|
1550
|
+
*/
|
|
1551
|
+
export function autoReleaseApprovalOf(store, taskId, kind) {
|
|
1552
|
+
const scope = store.getScope(taskId);
|
|
1553
|
+
const ref = store.lookupRef(taskId);
|
|
1554
|
+
if (scope === null || scope.approvedAt === null || scope.approvedBy === null || scope.approvedDigest === null)
|
|
1555
|
+
return { ok: false, why: "not approved yet" };
|
|
1556
|
+
if (scope.approvedDigest !== scope.digest)
|
|
1557
|
+
return { ok: false, why: "the approval is for different scope text" };
|
|
1558
|
+
if (scope.approvalBasis !== kind)
|
|
1559
|
+
return { ok: false, why: kind === "auto-green" ? "not approved automatically" : "not checking automatically" };
|
|
1560
|
+
if ((scope.candidate ?? null) === null || (scope.releaseTerms ?? null) === null || ref === null || ref.repo === null)
|
|
1561
|
+
return { ok: false, why: "only a release task is approved automatically" };
|
|
1562
|
+
const evidence = autoEvidenceOf(store, taskId, scope.modeDigest, kind);
|
|
1563
|
+
if (evidence === null || evidence.digest !== scope.approvedDigest || evidence.candidate !== scope.candidate || evidence.by !== scope.approvedBy)
|
|
1564
|
+
return { ok: false, why: "no record shows what approved it automatically" };
|
|
1565
|
+
if (!releaseAutoApproves(store, ref.repo))
|
|
1566
|
+
return { ok: false, why: "approving releases automatically was turned off" };
|
|
1567
|
+
const account = store.accountOf(scope.approvedBy);
|
|
1568
|
+
if (account === null || account.revokedAt !== null || account.role !== "approver" || !store.accountCanAccess(scope.approvedBy, ref.repo))
|
|
1569
|
+
return { ok: false, why: `${scope.approvedBy}, who turned on automatic release approval, can no longer approve in this project` };
|
|
1570
|
+
return { ok: true, by: scope.approvedBy, at: scope.approvedAt, evidence };
|
|
1571
|
+
}
|
|
1572
|
+
/** A release's approval: a person's (personApprovalOf, unchanged), or the plane's own `auto-green`. Only release
|
|
1573
|
+
* readers ask this (liveReleaseOf, and the deploy gates inside `toolroll release`); everything else asks for a person. */
|
|
1574
|
+
export function releaseApprovalOf(store, taskId, _now = new Date()) {
|
|
1575
|
+
const person = personApprovalOf(store, taskId);
|
|
1576
|
+
if (person.ok)
|
|
1577
|
+
return person;
|
|
1578
|
+
if (store.getScope(taskId)?.approvalBasis !== "auto-green")
|
|
1579
|
+
return person;
|
|
1580
|
+
const auto = autoReleaseApprovalOf(store, taskId, "auto-green");
|
|
1581
|
+
return auto.ok ? { ok: true, by: auto.by, at: auto.at, method: "auto-green" } : auto;
|
|
1582
|
+
}
|
package/dist/scout.js
CHANGED
|
@@ -18,6 +18,7 @@ import { mkdtempSync, realpathSync, rmSync, unlinkSync } from "node:fs";
|
|
|
18
18
|
import { homedir, tmpdir } from "node:os";
|
|
19
19
|
import { dirname, join, relative, isAbsolute } from "node:path";
|
|
20
20
|
import { run } from "./exec.js";
|
|
21
|
+
import { checkoutGit } from "./teardown-git.js";
|
|
21
22
|
import { auditOf } from "./provider.js";
|
|
22
23
|
import { currentClaim, heartbeat } from "./claim.js";
|
|
23
24
|
import { heartbeat as runnerHeartbeat } from "./runner.js";
|
|
@@ -225,7 +226,10 @@ export async function scout(store, request) {
|
|
|
225
226
|
}
|
|
226
227
|
}
|
|
227
228
|
async function scoutWith(store, request, outputDir) {
|
|
228
|
-
const { taskId, taskRef, runner, worktree, branch, now, agent, git = run, timeoutMs = DEFAULT_SCOUT_TIMEOUT_MS, maxTurns = DEFAULT_SCOUT_TURNS, } = request;
|
|
229
|
+
const { taskId, taskRef, runner, worktree, branch, now, agent, git: givenGit = run, timeoutMs = DEFAULT_SCOUT_TIMEOUT_MS, maxTurns = DEFAULT_SCOUT_TURNS, } = request;
|
|
230
|
+
// The plane's git on the scout's checkout (agent-written, and its repository shared with every build) runs hardened, against
|
|
231
|
+
// a frozen view of the repository's config and info (teardown-git.ts checkoutGit); an injected runner is used as given.
|
|
232
|
+
const git = givenGit === run ? checkoutGit(run, "build", { repo: store.getWorktree(worktree)?.repo ?? null }) : givenGit;
|
|
229
233
|
const claim = currentClaim(store, taskRef, now);
|
|
230
234
|
if (claim === null || claim.runner !== runner || claim.leaseId !== request.leaseId) {
|
|
231
235
|
return { ok: false, kind: "failure", reason: "not-yours", message: `${taskId} is not held under ${request.leaseId}` };
|
|
@@ -232,6 +232,11 @@ export declare function agentsStripHtml(view: RouteView | null, taskId: string):
|
|
|
232
232
|
/** A ceremony nonce as stored: the sha256 hex of the value the form carried. */
|
|
233
233
|
export declare function nonceHashOf(value: string): string;
|
|
234
234
|
/** The approve POST's refusal, in words a person can act on. */
|
|
235
|
+
/** A release task's automatic approval, in words (docs/plans/auto-approve-green.md). */
|
|
236
|
+
export declare function autoReleaseWords(auto: {
|
|
237
|
+
kind: "checking" | "approved";
|
|
238
|
+
candidate: string;
|
|
239
|
+
}): string;
|
|
235
240
|
/** " (password remembered from 10:42)" when the approval stood on a remembered password or sign-in; nothing otherwise. */
|
|
236
241
|
export declare function approvalProofWords(proof: {
|
|
237
242
|
method: string;
|
|
@@ -523,6 +528,11 @@ export declare function taskBodyParts(data: {
|
|
|
523
528
|
method: string;
|
|
524
529
|
at: string;
|
|
525
530
|
} | null;
|
|
531
|
+
/** A release task the plane approved (or is checking) itself (docs/plans/auto-approve-green.md): its candidate. */
|
|
532
|
+
autoRelease?: {
|
|
533
|
+
kind: "checking" | "approved";
|
|
534
|
+
candidate: string;
|
|
535
|
+
} | null;
|
|
526
536
|
/** One approval per release (release-once.md): the release this task belongs to, and whether this reader may stop it. */
|
|
527
537
|
release?: ReleaseView | null;
|
|
528
538
|
canStopRelease?: boolean;
|
|
@@ -746,6 +746,11 @@ export function nonceHashOf(value) {
|
|
|
746
746
|
return createHash("sha256").update(value).digest("hex");
|
|
747
747
|
}
|
|
748
748
|
/** The approve POST's refusal, in words a person can act on. */
|
|
749
|
+
/** A release task's automatic approval, in words (docs/plans/auto-approve-green.md). */
|
|
750
|
+
export function autoReleaseWords(auto) {
|
|
751
|
+
return auto.kind === "approved" ? `Approved automatically: CI and release check passed on ${auto.candidate.slice(0, 7)}`
|
|
752
|
+
: `Checking automatically: approves itself once CI and the release check pass on ${auto.candidate.slice(0, 7)}`;
|
|
753
|
+
}
|
|
749
754
|
/** " (password remembered from 10:42)" when the approval stood on a remembered password or sign-in; nothing otherwise. */
|
|
750
755
|
export function approvalProofWords(proof) {
|
|
751
756
|
if (proof == null)
|
|
@@ -1497,11 +1502,13 @@ export function taskBodyParts(data) {
|
|
|
1497
1502
|
html `<p><strong>Quality</strong> ${qualityModeTitle(scope.qualityMode ?? "default")}</p>`,
|
|
1498
1503
|
acceptanceCeremonyHtml(scope.acceptance),
|
|
1499
1504
|
scope.releaseTerms == null ? "" : html `<p><strong>Release</strong></p><p class="recap">${releaseTermsSentence(scope.releaseTerms)}</p>${scope.releaseTerms.policy.carry ? html `<details><summary>What asks again</summary><ul>${joinHtml(releaseAsksAgainWords(scope.releaseTerms).map(line => html `<li>${line}</li>`), "")}</ul></details>` : ""}`,
|
|
1500
|
-
approval.approved && data.
|
|
1501
|
-
? html `<p class="meta scope-seal"
|
|
1502
|
-
: approval.approved
|
|
1503
|
-
? html `<p class="meta scope-seal">
|
|
1504
|
-
:
|
|
1505
|
+
approval.approved && data.autoRelease != null
|
|
1506
|
+
? html `<p class="meta scope-seal">${autoReleaseWords(data.autoRelease)} · <span class="seal">signs ${shortDigest(scope.digest)}</span></p>`
|
|
1507
|
+
: approval.approved && data.release?.kind === "carried"
|
|
1508
|
+
? html `<p class="meta scope-seal">Carried from ${data.release.approver ?? "a person"}'s release approval · <a href="${taskHref(data.release.anchorTask)}">Release ${data.release.version}</a> · <span class="seal">signs ${shortDigest(scope.digest)}</span></p>`
|
|
1509
|
+
: approval.approved
|
|
1510
|
+
? html `<p class="meta scope-seal">Approved by ${approval.by}${approvalProofWords(data.approvalProof)} · ${whenTime(approval.at)} · <span class="seal">signs ${shortDigest(scope.digest)}</span><span class="so-sr-only"> — approval binds to this exact wording</span></p>`
|
|
1511
|
+
: html `<p class="meta"><span class="seal">signs ${shortDigest(scope.digest)}</span> — approval binds to this exact wording</p><p class="meta">Not approved${approval.reason === "changed" ? " — approved once, then rewritten" : ""}</p>`,
|
|
1505
1512
|
html `</div>`,
|
|
1506
1513
|
], "\n");
|
|
1507
1514
|
// The scout's report (mate arc §10): title, summary, the document inert,
|
|
@@ -1792,11 +1799,13 @@ export function taskBodyParts(data) {
|
|
|
1792
1799
|
: "";
|
|
1793
1800
|
const scopeRow = scope === null
|
|
1794
1801
|
? prop("scope", "none yet")
|
|
1795
|
-
: approval.approved && data.
|
|
1796
|
-
? prop("approved scope", html `<span class="seal">signs ${shortDigest(scope.digest)}</span> ·
|
|
1797
|
-
: approval.approved
|
|
1798
|
-
? prop("approved scope", html `<span class="seal">signs ${shortDigest(scope.digest)}</span> · ${
|
|
1799
|
-
:
|
|
1802
|
+
: approval.approved && data.autoRelease != null
|
|
1803
|
+
? prop("approved scope", html `<span class="seal">signs ${shortDigest(scope.digest)}</span> · ${autoReleaseWords(data.autoRelease)}`)
|
|
1804
|
+
: approval.approved && data.release?.kind === "carried"
|
|
1805
|
+
? prop("approved scope", html `<span class="seal">signs ${shortDigest(scope.digest)}</span> · carried from ${data.release.approver ?? "a person"}'s release approval · <a href="${taskHref(data.release.anchorTask)}">Release ${data.release.version}</a>`)
|
|
1806
|
+
: approval.approved
|
|
1807
|
+
? prop("approved scope", html `<span class="seal">signs ${shortDigest(scope.digest)}</span> · ${qualityModeTitle(scope.qualityMode ?? "default")} · approved by ${approval.by}${approvalProofWords(data.approvalProof)} · ${whenTime(approval.at)}`)
|
|
1808
|
+
: prop("scope", approval.reason === "changed" ? "rewritten since its approval — needs a new yes" : "not approved");
|
|
1800
1809
|
const strikesRow = data.strikes > 0 ? prop("strikes", `${data.strikes} failed attempt(s)`) : "";
|
|
1801
1810
|
const propsCard = html `<div class="card props">${workerRow}${queueRow}${scopeRow}${publishesRow}${economics}${strikesRow}</div>`;
|
|
1802
1811
|
// The same facts for the rebuilt page, row for row.
|
|
@@ -1811,9 +1820,10 @@ export function taskBodyParts(data) {
|
|
|
1811
1820
|
facts.push({ label: "Queue", parts: [`${data.position.position} of ${data.position.total}${data.position.column === null ? " in the shared queue" : ` in ${data.position.column}'s queue`} · `, { label: "Reorder", href: "/board?view=order" }] });
|
|
1812
1821
|
}
|
|
1813
1822
|
facts.push(scope === null ? { label: "Scope", parts: ["none yet"] }
|
|
1814
|
-
: approval.approved && data.
|
|
1815
|
-
: approval.approved ? { label: "Approved scope", parts: [
|
|
1816
|
-
: { label: "
|
|
1823
|
+
: approval.approved && data.autoRelease != null ? { label: "Approved scope", parts: [autoReleaseWords(data.autoRelease)] }
|
|
1824
|
+
: approval.approved && data.release?.kind === "carried" ? { label: "Approved scope", parts: [`Carried from ${data.release.approver ?? "a person"}'s release approval · `, { label: `Release ${data.release.version}`, href: taskHref(data.release.anchorTask) }] }
|
|
1825
|
+
: approval.approved ? { label: "Approved scope", parts: [{ seal: `signs ${scope.digest.length <= 12 ? scope.digest : `${scope.digest.slice(0, 12)}…`}` }, ` · ${qualityModeTitle(scope.qualityMode ?? "default")} · approved by ${approval.by}${approvalProofWords(data.approvalProof)}`, ...(approval.at === null ? [] : [" · ", { at: approval.at }])] }
|
|
1826
|
+
: { label: "Scope", parts: [approval.reason === "changed" ? "rewritten since its approval — needs a new yes" : "not approved"] });
|
|
1817
1827
|
if (data.release?.kind === "anchor")
|
|
1818
1828
|
facts.push({ label: "Release", parts: [`Approved for release ${data.release.version}`, ...(data.release.landed.length === 0 ? [] : [` · later commits: ${data.release.landed.map(one => one.sha.slice(0, 7)).join(", ")}`]), ...(data.release.carrying ? [] : [` · later commits ask again`])] });
|
|
1819
1829
|
facts.push({ label: "Publishes as", parts: [publishesAs] });
|
|
@@ -303,6 +303,9 @@ post("tasks", "sweep.cancel-rest", "^\\/sweeps\\/[1-9][0-9]{0,9}\\/cancel-rest$"
|
|
|
303
303
|
// (the handler proves it as a credential act); turning it off is one click, so the row itself is not a step-up.
|
|
304
304
|
get("settings", "settings.passwordless", "/settings/passwordless", { ...free, ...operator("An instance operator sets Passwordless.", "/settings") });
|
|
305
305
|
post("settings", "settings.passwordless-send", "/settings/passwordless", { callers: ["cookie"], ...operator("An instance operator sets Passwordless.", "/settings") });
|
|
306
|
+
// Release approval (docs/plans/auto-approve-green.md): an approver of the project, from a browser. Approving releases
|
|
307
|
+
// automatically takes a typed password (the handler proves it as a credential act); asking again is one click.
|
|
308
|
+
post("settings", "settings.release-approval-send", "/settings/release-approval", { callers: ["cookie"], ...approver("An approver sets how releases are approved.", "/settings/approval") });
|
|
306
309
|
post("pages", "ledger.checkpoint", "/ledger/checkpoint", operator("An instance operator makes ledger checkpoints.", "/ledger"));
|
|
307
310
|
const SETTINGS_ACT_ROLES = {
|
|
308
311
|
policy: operator("An instance operator sets the policy.", "/settings/policy"),
|
package/dist/server/settings.js
CHANGED
|
@@ -39,7 +39,8 @@ import { browserCaller } from "./session.js";
|
|
|
39
39
|
import { DEFAULT_ACCENT, normalHex } from "../accent-colors.js";
|
|
40
40
|
import { resolvePhaseAgent } from "../agentconfig.js";
|
|
41
41
|
import { parseProtectedPaths } from "../approval-policy.js";
|
|
42
|
-
import { approvalRulesHtml } from "../approval-rules-ui.js";
|
|
42
|
+
import { approvalRulesHtml, releaseApprovalHtml } from "../approval-rules-ui.js";
|
|
43
|
+
import { releaseApprovalSetting, setReleaseApproval } from "../release-auto.js";
|
|
43
44
|
import { backupHtml } from "../backup-ui.js";
|
|
44
45
|
import { BACKUP_EVERY_HOURS, backupFolderOf, backupNow, checkBackupFolder, defaultBackupFolder, MAX_KEEP } from "../backup.js";
|
|
45
46
|
import { projectBatchChecks, setProjectBatchChecks } from '../batch-policy.js';
|
|
@@ -366,7 +367,8 @@ ${connectionHtml(asked, connection, task)}${command === null ? "" : html `<p>On
|
|
|
366
367
|
return refuse(response, who, 403, "That project is outside your access.", "/settings/approval");
|
|
367
368
|
const selector = projects.length > 1 ? html `<form class="approval-project" method="get" action="/settings/approval"><label>Project<select name="repo">${projects.map(p => html `<option value="${p}"${p === chosen ? " selected" : ""}>${projectName(p)}</option>`)}</select></label><button>Show</button></form>` : "";
|
|
368
369
|
const approvers = chosen === "" ? 0 : store.listApprovers().filter(one => { const account = store.accountOf(one.name); return account !== null && account.revokedAt === null && account.role === "approver" && store.accountCanAccess(one.name, chosen); }).length;
|
|
369
|
-
const
|
|
370
|
+
const release = chosen === "" ? null : releaseApprovalSetting(store, chosen);
|
|
371
|
+
const content = chosen === "" ? html `<p>Add a project to set its approval rules.</p>` : html `${approvalRulesHtml({ repo: chosen, name: projectName(chosen), rules: store.approvalRules(chosen), canChange: who.via === "cookie" && store.isInstanceOperator(who.name), approvers }, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") })}${releaseApprovalHtml({ repo: chosen, auto: release.auto, setBy: release.setBy, at: release.at, canChange: who.via === "cookie" && who.role === "approver" && !store.isDemo() && store.accountCanAccess(who.name, chosen) })}`;
|
|
370
372
|
return sendScreen(response, 200, screen("Approval rules", html `<p><a href="/settings">Settings</a></p><h1>Approval rules</h1>${selector}${content}`, { chrome: chromeFor(chosen || project, "settings") }));
|
|
371
373
|
}
|
|
372
374
|
// Sprint 8: Settings → Policy, the organisation policy and its history. Anyone signed in reads it; an instance operator changes it.
|
|
@@ -2538,6 +2540,7 @@ ${modeWords(terms)
|
|
|
2538
2540
|
"settings.sign-in": signInPage,
|
|
2539
2541
|
"settings.passwordless": passwordlessPage,
|
|
2540
2542
|
"settings.passwordless-send": passwordlessSave,
|
|
2543
|
+
"settings.release-approval-send": releaseApprovalSave,
|
|
2541
2544
|
"settings.lead": leadPage,
|
|
2542
2545
|
"settings.teams": teamsPage,
|
|
2543
2546
|
"settings.discord": discordPage,
|
|
@@ -2725,5 +2728,29 @@ ${modeWords(terms)
|
|
|
2725
2728
|
return back("problem", "An approver for this project changes its sandbox. Nothing changed.");
|
|
2726
2729
|
return back("said", JSON.stringify(changed.before) === JSON.stringify(changed.after) ? "Nothing changed." : "Saved. Tasks scoped from now on use it; tasks already approved keep what they were approved with.");
|
|
2727
2730
|
}
|
|
2731
|
+
// Release approval (docs/plans/auto-approve-green.md): an approver of the project. Approving automatically is a
|
|
2732
|
+
// credential act (a password typed now, never a session); asking again is one click and takes back automatic seals.
|
|
2733
|
+
async function releaseApprovalSave(ctx) {
|
|
2734
|
+
const { response, now, posted } = ctx;
|
|
2735
|
+
const who = browserCaller(ctx.who);
|
|
2736
|
+
const body = readForm(posted, CONSOLE_FORMS.releaseApproval);
|
|
2737
|
+
const repo = body.get("repo") ?? "";
|
|
2738
|
+
if (!consoleProjects().includes(repo) || !visible(repo))
|
|
2739
|
+
return refuse(response, who, 403, "That project is outside your access.", "/settings/approval");
|
|
2740
|
+
const back = sayTo(response, `/settings/approval?repo=${encodeURIComponent(repo)}`);
|
|
2741
|
+
if (who.role !== "approver" || store.isDemo() || !store.accountCanAccess(who.name, repo))
|
|
2742
|
+
return back("problem", "An approver of this project sets how its releases are approved.");
|
|
2743
|
+
const action = body.get("action");
|
|
2744
|
+
if (action === "ask") {
|
|
2745
|
+
const changed = setReleaseApproval(store, repo, false, who.name, now);
|
|
2746
|
+
return back("said", !changed.changed ? "Saved." : `Releases ask you again.${changed.withdrawn.length === 0 ? "" : ` ${changed.withdrawn.length} automatic approval${changed.withdrawn.length === 1 ? " was" : "s were"} taken back.`}`);
|
|
2747
|
+
}
|
|
2748
|
+
if (action !== "auto")
|
|
2749
|
+
return back("problem", "Choose Ask me or Approve automatically when checks pass.");
|
|
2750
|
+
if (!authenticateCredential(who, body.get("password") ?? "", repo).ok)
|
|
2751
|
+
return back("problem", "Enter your Toolroll password to approve releases automatically.");
|
|
2752
|
+
const changed = setReleaseApproval(store, repo, true, who.name, now);
|
|
2753
|
+
return back("said", changed.changed ? "Releases approve themselves once CI and the release check pass." : "Saved.");
|
|
2754
|
+
}
|
|
2728
2755
|
return { registrations, modelsView };
|
|
2729
2756
|
}
|
|
@@ -496,6 +496,7 @@ export function createTaskViews(runtime) {
|
|
|
496
496
|
steeringRun: ref === null ? null : runtime.store.steeringLiveBuild(ref.id, now),
|
|
497
497
|
release: ref === null ? null : releaseViewOf(runtime.store, ref.externalId, now, authority => releaseObligationsOf(runtime.store, runtime.evidenceRoot, authority)),
|
|
498
498
|
canStopRelease: who.role === "approver" && who.via === "cookie",
|
|
499
|
+
autoRelease: ref === null ? null : autoReleaseOf(runtime.store.getScope(ref.externalId)),
|
|
499
500
|
approvalProof: (() => {
|
|
500
501
|
// The proof of the approval shown: its approver, its exact sealed digest (approvedDigest === digest when approved).
|
|
501
502
|
const scope = ref === null ? null : runtime.store.getScope(ref.externalId);
|
|
@@ -1139,3 +1140,10 @@ export function createTaskViews(runtime) {
|
|
|
1139
1140
|
}
|
|
1140
1141
|
return { guided, projectFamilyPeek, familiesInView, familyTasksInView, revisionDestination, earlierLiveVersions, familyHistory, freshAssignment, workRowOf, taskListPane, runIsLive, runIsTaskResult, revisionViewOf, taskViewData, routeViewOf, taskChatFocus, pullRequestTargetOf, taskPullRequestOf, failureOf, explainAttempt, runChecksHere, taskScreen, armTaskResume, planViewOf, planContractViewOf, revisionDocOf, revisionLedgerOf, progressOf, resultDetailOf };
|
|
1141
1142
|
}
|
|
1143
|
+
/** A release task the plane approved (or is checking) itself, for the task page (docs/plans/auto-approve-green.md). */
|
|
1144
|
+
function autoReleaseOf(scope) {
|
|
1145
|
+
if (scope === null || scope.approvedDigest !== scope.digest || (scope.candidate ?? null) === null)
|
|
1146
|
+
return null;
|
|
1147
|
+
return scope.approvalBasis === "auto-green" ? { kind: "approved", candidate: scope.candidate }
|
|
1148
|
+
: scope.approvalBasis === "auto-check" ? { kind: "checking", candidate: scope.candidate } : null;
|
|
1149
|
+
}
|
package/dist/store.d.ts
CHANGED
|
@@ -2355,7 +2355,7 @@ export declare class Store {
|
|
|
2355
2355
|
* exactly what the signed digest was bound to. Returns false when the
|
|
2356
2356
|
* scope vanished mid-ceremony. */
|
|
2357
2357
|
sealScopeApproval(taskId: string, by: string, now: Date, mutation?: Mutation, basis?: {
|
|
2358
|
-
kind: "mode" | "chat" | "grant" | "schedule";
|
|
2358
|
+
kind: "mode" | "chat" | "grant" | "schedule" | "auto-check" | "auto-green";
|
|
2359
2359
|
modeDigest: string;
|
|
2360
2360
|
/** A chat seal of a deploy gate: the release card it came from (chat-management.md §0a.5). */
|
|
2361
2361
|
card?: "release";
|
package/dist/store.js
CHANGED
|
@@ -7014,7 +7014,17 @@ export class Store {
|
|
|
7014
7014
|
const deployGate = (typeof working["candidate"] === "string" && working["candidate"] !== "") || working["release_terms_json"] != null;
|
|
7015
7015
|
if (deployGate && basis?.kind === "chat" && !(basis.card === "release" && typeof basis.cardDigest === "string" && /^[0-9a-f]{64}$/.test(basis.cardDigest)))
|
|
7016
7016
|
return false;
|
|
7017
|
-
|
|
7017
|
+
// AUTOMATIC RELEASE SEALS (auto-approve-green.md): only on a release task (a candidate and release terms signed
|
|
7018
|
+
// in), only naming the evidence row the plane wrote for exactly this task, kind, digest and candidate.
|
|
7019
|
+
const automatic = basis?.kind === "auto-check" || basis?.kind === "auto-green";
|
|
7020
|
+
if (automatic) {
|
|
7021
|
+
if (terms.terms === null || typeof working["candidate"] !== "string" || working["candidate"] === "")
|
|
7022
|
+
return false;
|
|
7023
|
+
const evidence = autoEvidenceOf(this, taskId, basis.modeDigest, basis.kind);
|
|
7024
|
+
if (evidence === null || evidence.digest !== String(working["digest"]) || evidence.candidate !== working["candidate"] || evidence.by !== by)
|
|
7025
|
+
return false;
|
|
7026
|
+
}
|
|
7027
|
+
if (terms.terms !== null && basis !== undefined && basis.kind !== "chat" && !automatic)
|
|
7018
7028
|
return false;
|
|
7019
7029
|
if (terms.terms !== null && releaseSealProblem(this, terms.terms) !== null)
|
|
7020
7030
|
return false;
|
|
@@ -7033,7 +7043,8 @@ export class Store {
|
|
|
7033
7043
|
approved_route_json = proposed_route_json
|
|
7034
7044
|
WHERE task_id = ?`)
|
|
7035
7045
|
.run(now.toISOString(), by, basis === undefined ? "password" : basis.kind, basis === undefined ? null : basis.modeDigest, taskId);
|
|
7036
|
-
|
|
7046
|
+
// `auto-check` only lets the check run: it opens no release authority (only `auto-green` and a person's yes do).
|
|
7047
|
+
if (changed.changes > 0 && terms.ok && terms.terms !== null && basis?.kind !== "auto-check") {
|
|
7037
7048
|
if (openReleaseAuthority(this, { taskId, digest: String(working["digest"]), terms: terms.terms, by, now }) === null)
|
|
7038
7049
|
throw new ReleaseSealRefused();
|
|
7039
7050
|
}
|
|
@@ -7066,7 +7077,11 @@ export class Store {
|
|
|
7066
7077
|
? "Approved automatically: its flow runs on its own. A connected worker can take it next."
|
|
7067
7078
|
: basis.kind === "schedule"
|
|
7068
7079
|
? "Approved by its standing order. A connected worker can take it next."
|
|
7069
|
-
:
|
|
7080
|
+
: basis.kind === "auto-check"
|
|
7081
|
+
? "Checking automatically: it approves itself once CI and the release check pass."
|
|
7082
|
+
: basis.kind === "auto-green"
|
|
7083
|
+
? "Approved automatically: CI and the release check passed."
|
|
7084
|
+
: "Approved automatically under the operating mode. A connected worker can take it next.",
|
|
7070
7085
|
link: chatControlHref("task", taskId),
|
|
7071
7086
|
}, now);
|
|
7072
7087
|
}
|
|
@@ -20422,7 +20437,7 @@ export function serializeReleaseTerms(terms) {
|
|
|
20422
20437
|
}
|
|
20423
20438
|
// ---- one approval per release: the authority record (release-once.md, part 1) --------------------------------------
|
|
20424
20439
|
export { LIVE_RELEASE_STATES, belongsToRelease, newestReleaseGeneration, outstandingReleaseOperations, releaseAuthority, releaseAuthorityOfTask, releaseCarries, releaseCarryTo, releaseOperation, releaseOperations, releaseSealProblem, } from "./release-authority.js";
|
|
20425
|
-
import { LIVE_RELEASE_STATES, belongsToRelease, newestReleaseGeneration, outstandingReleaseOperations, releaseAuthority, releaseCarryTo, releaseOperation, releaseSealProblem, } from "./release-authority.js";
|
|
20440
|
+
import { LIVE_RELEASE_STATES, autoEvidenceOf, belongsToRelease, newestReleaseGeneration, outstandingReleaseOperations, releaseAuthority, releaseCarryTo, releaseOperation, releaseSealProblem, } from "./release-authority.js";
|
|
20426
20441
|
/**
|
|
20427
20442
|
* Open a release's authority for a person's seal of its terms (inside the seal's transaction): the next generation,
|
|
20428
20443
|
* superseding the live one. False when it can't (releaseSealProblem), which refuses the seal.
|
package/dist/task-base.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { execFileSync } from "node:child_process";
|
|
8
8
|
export const readGit = (cwd, args) => {
|
|
9
9
|
try {
|
|
10
|
-
return execFileSync("git", ["--no-optional-locks", ...args], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000, maxBuffer: 65_536 }).trim();
|
|
10
|
+
return execFileSync("git", ["--no-optional-locks", "--no-lazy-fetch", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", ...args], { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000, maxBuffer: 65_536 }).trim();
|
|
11
11
|
}
|
|
12
12
|
catch {
|
|
13
13
|
return null;
|
package/dist/teardown-git.d.ts
CHANGED
|
@@ -6,14 +6,24 @@ export declare const TEARDOWN_FLAGS: readonly string[];
|
|
|
6
6
|
/** Exit code of a refused command (git's own fatal code, so no caller reads it as "not found" or "no"). */
|
|
7
7
|
export declare const REFUSED_CODE = 128;
|
|
8
8
|
/** The command-running setting a `git config --list --show-scope -z` listing sets, or null. Only local and worktree
|
|
9
|
-
* scope count: teardown commands never read system or global config.
|
|
10
|
-
|
|
9
|
+
* scope count: teardown commands never read system or global config. `lfsChecked`: the caller refuses a checkout whose
|
|
10
|
+
* `.lfsconfig` names a git-lfs extension (lfsExtensionIn), so git-lfs's own exact filters may stand (runsCommand).
|
|
11
|
+
* Fails closed: a listing that can't be classified names a setting. */
|
|
12
|
+
export declare function commandSetting(listing: string, options?: {
|
|
13
|
+
lfsChecked?: boolean;
|
|
14
|
+
}): string | null;
|
|
11
15
|
/** The `-c` settings that carry the operator's own values (CARRIED_VALUES, LFS_FILTER) from a global/system listing. */
|
|
12
16
|
export declare function carriedSettings(listing: string): string[];
|
|
13
17
|
/** Why a teardown command was refused, or null when `result` isn't a refusal. */
|
|
14
18
|
export declare function refusal(result: ExecResult): string | null;
|
|
15
|
-
/** The one way teardown runs git, including the target checkout immediately before removal.
|
|
16
|
-
|
|
19
|
+
/** The one way teardown runs git, including the target checkout immediately before removal. Every command runs
|
|
20
|
+
* against a write view of its repository (gitView), whose config is the one inspected and whose info is a copy, so a
|
|
21
|
+
* setting or attributes file an agent adds after the inspection is never read (a build's process can outlive its turn:
|
|
22
|
+
* on macOS containment is observational, src/containment.ts, and a sibling build shares the repository). A removal's
|
|
23
|
+
* target is bound to the same repository as the folder it is removed from. A removal without --force runs under the
|
|
24
|
+
* target's own view (its own config.worktree folded in), so git's last check of whether it is clean reads it as the
|
|
25
|
+
* target itself would. `binding.repo`: the project's main checkout, from the plane's records, when the caller has it. */
|
|
26
|
+
export declare function teardownGit(runner?: Runner, binding?: GitBinding): Git;
|
|
17
27
|
/**
|
|
18
28
|
* Git on a build's checkout, as the plane runs it outside every fence (staging, committing and inspecting the build's
|
|
19
29
|
* work): the same fixed hardening as teardown (no hooks, fsmonitor, ext transport, credential helpers or automatic
|
|
@@ -29,14 +39,24 @@ export declare function teardownGit(runner?: Runner): Git;
|
|
|
29
39
|
* that can't be read so counts as including.
|
|
30
40
|
*/
|
|
31
41
|
export declare function checkoutIncludes(checkout: string): boolean;
|
|
42
|
+
export type GitBinding = {
|
|
43
|
+
repo?: string | null;
|
|
44
|
+
};
|
|
45
|
+
/** Settings fixed on every write through a view: `worktree add` records absolute paths (relative ones would need a
|
|
46
|
+
* config write, which a view keeps); no transport at all (a caller that fetches allows its one protocol by name, as the
|
|
47
|
+
* nightly check's local fetch does with protocol.file.allow); and no split index (its shared index files would be
|
|
48
|
+
* written in the plane's GIT_DIR). */
|
|
49
|
+
export declare const WRITE_VIEW_FLAGS: readonly string[];
|
|
32
50
|
/** Whether the checkout's .lfsconfig (read as git parses it, that file only) sets any git-lfs key that could name a
|
|
33
51
|
* program (anything but INERT_LFS). A file that
|
|
34
52
|
* isn't a plain one, or doesn't parse, counts as naming one. */
|
|
35
53
|
export declare function lfsExtensionIn(checkout: string): boolean;
|
|
36
|
-
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
|
|
54
|
+
/** Every command runs against a view of the checkout's repository (gitView) whose config and info are copies the
|
|
55
|
+
* plane made, inspected there, so neither can change between the inspection and the run. `mode` "read" is for reading
|
|
56
|
+
* a checkout an agent may still be writing (the code page): every command also takes readFlags(), and the view holds
|
|
57
|
+
* no info/attributes. "build" is the plane's staging and committing of a build's work, which writes the repository
|
|
58
|
+
* through the view (gitView). */
|
|
59
|
+
export declare function checkoutGit(inner?: Runner, mode?: "build" | "read", binding?: GitBinding): Runner;
|
|
40
60
|
/**
|
|
41
61
|
* Flags for reading a checkout an agent writes while a page waits (docs/plans/get-side-effects.md). Command-line
|
|
42
62
|
* settings win over anything the checkout's config says, even one changed after it was inspected: no fsmonitor, hooks,
|
|
@@ -47,16 +67,24 @@ export declare const PAGE_READ_FLAGS: readonly string[];
|
|
|
47
67
|
/** The read flags, for every read of an agent-written checkout, whatever the HTTP method (security review of
|
|
48
68
|
* 01bf3114): the worktree's attributes come from the empty tree. Git still reads the repository's info/attributes and
|
|
49
69
|
* its config as it starts, so the flags alone don't stop a filter planted in both after the inspection: every read
|
|
50
|
-
* also runs against the checkout's
|
|
70
|
+
* also runs against a view of the checkout's repository (gitView; checkoutGitSync, checkoutGit "read"), whose config and info are the plane's. The cost is cosmetic: a checkout's line-ending or encoding rules are not applied, so
|
|
51
71
|
* a file they would normalize can read as changed (fail closed: shipping then asks for a clean commit). */
|
|
52
72
|
export declare function readFlags(): readonly string[];
|
|
53
73
|
/**
|
|
54
74
|
* checkoutGit, synchronously, for reads a page shows from a checkout an agent writes (docs/plans/get-side-effects.md):
|
|
55
75
|
* the same refusals (an include, an .lfsconfig extension, a config key that can run a command), no system or global
|
|
56
|
-
* config, the operator's own values carried, PAGE_READ_FLAGS and the checkout's
|
|
76
|
+
* config, the operator's own values carried, PAGE_READ_FLAGS and a view of the checkout's repository (gitView: they hold even if its config
|
|
57
77
|
* or info/attributes change after they were read), submodules skipped, bounded. A refusal throws.
|
|
58
78
|
*/
|
|
59
79
|
export declare function checkoutGitSync(cwd: string, args: readonly string[], options?: {
|
|
60
80
|
timeoutMs?: number;
|
|
61
81
|
maxBuffer?: number;
|
|
82
|
+
binding?: GitBinding;
|
|
62
83
|
}): string;
|
|
84
|
+
/**
|
|
85
|
+
* checkoutGit ("build": the repository written through its view) for callers that name the repository with a leading
|
|
86
|
+
* `git -C <dir>` instead of a cwd: worktree add and remove in a project's repository, a merge in a checkout of it.
|
|
87
|
+
* The project's repository is shared with every build, which can write its config, info and hooks; nothing here runs
|
|
88
|
+
* what they name. Anything but git goes to `inner` as given.
|
|
89
|
+
*/
|
|
90
|
+
export declare function hardenedGit(inner?: Runner, mode?: "build" | "read", binding?: GitBinding): Runner;
|