toolroll 0.9.56 → 0.9.58
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 +16 -0
- package/dist/action-ledger.js +0 -4
- package/dist/actor.d.ts +0 -10
- package/dist/actor.js +1 -18
- package/dist/api-tokens.d.ts +33 -2
- package/dist/api-tokens.js +39 -11
- package/dist/assignment-adapters.d.ts +5 -0
- package/dist/assignment-delivery.js +1 -1
- package/dist/assignment-status.js +1 -1
- package/dist/assignment.d.ts +24 -3
- package/dist/assignment.js +39 -12
- package/dist/browser-workspace.js +1 -1
- package/dist/chat-delivery.js +3 -3
- package/dist/chat-voice.js +1 -1
- package/dist/check-levels.d.ts +3 -0
- package/dist/check-levels.js +5 -0
- package/dist/cli-http.d.ts +7 -0
- package/dist/cli-http.js +30 -4
- package/dist/cli.js +2 -1
- package/dist/coordinator.d.ts +2 -1
- package/dist/coordinator.js +22 -31
- package/dist/desktop-update.d.ts +1 -1
- package/dist/dispose.js +0 -5
- package/dist/export.js +2 -2
- package/dist/lead-status.d.ts +1 -1
- package/dist/lead-status.js +2 -2
- package/dist/mcp-http.js +5 -5
- package/dist/operate.js +19 -9
- package/dist/password-bearer.d.ts +27 -0
- package/dist/password-bearer.js +59 -0
- package/dist/proof.d.ts +9 -0
- package/dist/proof.js +12 -0
- package/dist/release-coverage.d.ts +16 -0
- package/dist/release-coverage.js +92 -0
- package/dist/remote-command.js +0 -2
- package/dist/remote-exec.d.ts +19 -0
- package/dist/remote-exec.js +9 -8
- package/dist/request-budget.d.ts +1 -1
- package/dist/scope.js +2 -1
- package/dist/serve.js +15 -0
- package/dist/server/chat.js +8 -6
- package/dist/server/edge-refusal.d.ts +2 -0
- package/dist/server/edge-refusal.js +8 -6
- package/dist/server/guards.js +5 -4
- package/dist/server/http.d.ts +2 -0
- package/dist/server/remote.js +6 -13
- package/dist/server/render-results.d.ts +4 -1
- package/dist/server/render-results.js +23 -9
- package/dist/server/route-table.js +0 -3
- package/dist/server/tasks.js +28 -25
- package/dist/session-cli.d.ts +38 -9
- package/dist/session-cli.js +141 -154
- package/dist/session-contract.js +2 -2
- package/dist/session-remote.d.ts +19 -0
- package/dist/session-remote.js +86 -0
- package/dist/store.d.ts +31 -11
- package/dist/store.js +276 -95
- package/dist/task-outcome-cli.js +3 -1
- package/dist/task-status.d.ts +59 -7
- package/dist/task-status.js +132 -16
- package/dist/team-cli.d.ts +3 -0
- package/dist/team-cli.js +23 -3
- package/dist/team-runtime.js +1 -1
- package/dist/telegram-status.d.ts +11 -4
- package/dist/telegram-status.js +17 -10
- package/dist/telegram.js +4 -4
- package/dist/toolroll-update.js +3 -1
- package/dist/work-index.d.ts +6 -1
- package/dist/work-index.js +50 -8
- package/dist/workspace-ui.js +2 -1
- package/package.json +1 -1
- package/dist/session-http.d.ts +0 -24
- package/dist/session-http.js +0 -170
- package/dist/session-server.d.ts +0 -17
- package/dist/session-server.js +0 -39
package/dist/cli.js
CHANGED
|
@@ -424,7 +424,8 @@ async function dispatch(argv, write, mainOptions) {
|
|
|
424
424
|
if (first === "session") {
|
|
425
425
|
// D5: native coding sessions are deprecated. They still run this release; each use says so once, on stderr.
|
|
426
426
|
(mainOptions.session?.stderr ?? (line => process.stderr.write(`${line}\n`)))(deprecationWarning(`session ${rest.find(arg => !arg.startsWith("-")) ?? ""}`.trim(), "session"));
|
|
427
|
-
|
|
427
|
+
// D7: it runs on the saved connection over /api/cli, signed in with that profile's API token.
|
|
428
|
+
return runSessionCommand(rest, write, { ...(mainOptions.remote ?? mainOptions.team), ...mainOptions.session });
|
|
428
429
|
}
|
|
429
430
|
if (first === "demo")
|
|
430
431
|
return runDemoCommand(rest, write);
|
package/dist/coordinator.d.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* The coordinator principal (MCP gateway spec v6; DESIGN.md §9b).
|
|
3
3
|
*
|
|
4
4
|
* A coordinator is the ONE credential the MCP surface accepts: minted by
|
|
5
|
-
* an operator password ceremony, hashed at rest
|
|
5
|
+
* an operator password ceremony, hashed at rest (a `credential` row of kind
|
|
6
|
+
* 'coordinator', v118: the shared codec and table), repo-scoped at the mint,
|
|
6
7
|
* rate-limited, revocable — and able to do exactly one thing: file a
|
|
7
8
|
* proposal through the canonical door. What it files is an ordinary
|
|
8
9
|
* UNAPPROVED task; admission is the operator's existing scope ceremony,
|
package/dist/coordinator.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* The coordinator principal (MCP gateway spec v6; DESIGN.md §9b).
|
|
3
3
|
*
|
|
4
4
|
* A coordinator is the ONE credential the MCP surface accepts: minted by
|
|
5
|
-
* an operator password ceremony, hashed at rest
|
|
5
|
+
* an operator password ceremony, hashed at rest (a `credential` row of kind
|
|
6
|
+
* 'coordinator', v118: the shared codec and table), repo-scoped at the mint,
|
|
6
7
|
* rate-limited, revocable — and able to do exactly one thing: file a
|
|
7
8
|
* proposal through the canonical door. What it files is an ordinary
|
|
8
9
|
* UNAPPROVED task; admission is the operator's existing scope ceremony,
|
|
@@ -14,7 +15,8 @@
|
|
|
14
15
|
* the exported-cast mistake is named in the spec and not repeated here.
|
|
15
16
|
* Steering and every operator-speech surface cannot accept this type.
|
|
16
17
|
*/
|
|
17
|
-
import { createHash
|
|
18
|
+
import { createHash } from "node:crypto";
|
|
19
|
+
import { hashSecret, mintCredential } from "./api-tokens.js";
|
|
18
20
|
import { canonicalRepos } from "./runner.js";
|
|
19
21
|
import { fileTaskProposal } from "./proposal.js";
|
|
20
22
|
import { describeScope } from "./scope.js";
|
|
@@ -27,18 +29,7 @@ function verified(row) {
|
|
|
27
29
|
return row;
|
|
28
30
|
}
|
|
29
31
|
const NAME = /^[a-z0-9-]{1,32}$/;
|
|
30
|
-
const CID_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
31
32
|
const KEY = /^[\x21-\x7e]{8,64}$/;
|
|
32
|
-
function hashSecret(token) {
|
|
33
|
-
return createHash("sha256").update(token, "utf8").digest("hex");
|
|
34
|
-
}
|
|
35
|
-
function mintCid() {
|
|
36
|
-
const bytes = randomBytes(12);
|
|
37
|
-
let cid = "";
|
|
38
|
-
for (const b of bytes)
|
|
39
|
-
cid += CID_ALPHABET[b % CID_ALPHABET.length];
|
|
40
|
-
return cid;
|
|
41
|
-
}
|
|
42
33
|
export function mintCoordinator(store, input) {
|
|
43
34
|
const days = input.days ?? 90;
|
|
44
35
|
if (!Number.isInteger(days) || days < 1 || days > 365)
|
|
@@ -53,15 +44,16 @@ export function mintCoordinator(store, input) {
|
|
|
53
44
|
if (input.repos.length === 0)
|
|
54
45
|
return { ok: false, reason: "no-repos" };
|
|
55
46
|
const repos = canonicalRepos(input.repos);
|
|
56
|
-
const token = (input.newToken ?? (() =>
|
|
47
|
+
const token = (input.newToken ?? (() => mintCredential("coordinator").token))();
|
|
57
48
|
return store.transact(() => {
|
|
58
49
|
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
59
|
-
const cid = (input.newCid ??
|
|
50
|
+
const cid = (input.newCid ?? (() => mintCredential("coordinator").id))();
|
|
60
51
|
try {
|
|
52
|
+
// Its generation: which coordinator of this name it is (revoke-and-remint keeps the earlier one's history apart).
|
|
61
53
|
store.handle
|
|
62
|
-
.prepare(`INSERT INTO
|
|
63
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
64
|
-
.run(cid, name, hashSecret(token), JSON.stringify(repos), perHour, input.by, input.now.toISOString(), expiresAt);
|
|
54
|
+
.prepare(`INSERT INTO credential (id, kind, public_id, account, name, secret_hash, access, projects_json, per_hour, created_by, created_at, expires_at, generation)
|
|
55
|
+
VALUES (?, 'coordinator', ?, ?, ?, ?, 'act', ?, ?, ?, ?, ?, 1 + (SELECT count(*) FROM credential WHERE kind = 'coordinator' AND name = ?))`)
|
|
56
|
+
.run(cid, cid, input.by, name, hashSecret(token), JSON.stringify(repos), perHour, input.by, input.now.toISOString(), expiresAt, name);
|
|
65
57
|
return { ok: true, cid, token, repos };
|
|
66
58
|
}
|
|
67
59
|
catch (error) {
|
|
@@ -81,9 +73,8 @@ export function mintCoordinator(store, input) {
|
|
|
81
73
|
* the stored hash; the caller re-runs this INSIDE any mutating
|
|
82
74
|
* transaction (the startup check is a courtesy; the txn check is law). */
|
|
83
75
|
export function authenticateCoordinator(store, token) {
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
.get(hashSecret(token));
|
|
76
|
+
const found = store.presentedCredential(token);
|
|
77
|
+
const row = found?.kind === "coordinator" ? store.handle.prepare("SELECT * FROM credential WHERE id = ? AND kind = 'coordinator'").get(found.id) : undefined;
|
|
87
78
|
if (row === undefined)
|
|
88
79
|
return { ok: false, reason: "unknown" };
|
|
89
80
|
if (row["revoked_at"] !== null)
|
|
@@ -94,23 +85,23 @@ export function authenticateCoordinator(store, token) {
|
|
|
94
85
|
return {
|
|
95
86
|
ok: true,
|
|
96
87
|
who: verified({
|
|
97
|
-
cid: String(row["
|
|
88
|
+
cid: String(row["id"]),
|
|
98
89
|
name: String(row["name"]),
|
|
99
|
-
repos: JSON.parse(String(row["
|
|
90
|
+
repos: JSON.parse(String(row["projects_json"])),
|
|
100
91
|
perHour: Number(row["per_hour"]),
|
|
101
92
|
}),
|
|
102
93
|
};
|
|
103
94
|
}
|
|
104
95
|
export function revokeCoordinator(store, cid, by, now) {
|
|
105
96
|
return store.transact(() => {
|
|
106
|
-
const row = store.handle.prepare("SELECT revoked_at FROM
|
|
97
|
+
const row = store.handle.prepare("SELECT revoked_at FROM credential WHERE id = ? AND kind = 'coordinator'").get(cid);
|
|
107
98
|
if (row === undefined)
|
|
108
99
|
return { ok: false, reason: "unknown" };
|
|
109
100
|
if (row["revoked_at"] !== null)
|
|
110
101
|
return { ok: false, reason: "already-revoked" };
|
|
111
102
|
store.handle
|
|
112
|
-
.prepare("UPDATE
|
|
113
|
-
.run(now.toISOString(), cid);
|
|
103
|
+
.prepare("UPDATE credential SET revoked_at = ?, revoked_by = ? WHERE id = ? AND kind = 'coordinator'")
|
|
104
|
+
.run(now.toISOString(), by, cid);
|
|
114
105
|
// The revocation event rides the SAME transaction as the state change,
|
|
115
106
|
// and the credential's pending proposals expire with it (mate arc v3).
|
|
116
107
|
store.handle
|
|
@@ -122,13 +113,13 @@ export function revokeCoordinator(store, cid, by, now) {
|
|
|
122
113
|
}
|
|
123
114
|
export function listCoordinators(store) {
|
|
124
115
|
return store.handle
|
|
125
|
-
.prepare(`SELECT c.*, (SELECT MAX(e.created_at) FROM coordinator_event e WHERE e.cid = c.
|
|
126
|
-
FROM
|
|
116
|
+
.prepare(`SELECT c.*, (SELECT MAX(e.created_at) FROM coordinator_event e WHERE e.cid = c.id AND e.kind = 'filed') AS last_filed
|
|
117
|
+
FROM credential c WHERE c.kind = 'coordinator' ORDER BY c.created_at, c.id`)
|
|
127
118
|
.all()
|
|
128
119
|
.map(row => ({
|
|
129
|
-
cid: String(row["
|
|
120
|
+
cid: String(row["id"]),
|
|
130
121
|
name: String(row["name"]),
|
|
131
|
-
repos: JSON.parse(String(row["
|
|
122
|
+
repos: JSON.parse(String(row["projects_json"])),
|
|
132
123
|
perHour: Number(row["per_hour"]),
|
|
133
124
|
createdBy: String(row["created_by"]),
|
|
134
125
|
createdAt: String(row["created_at"]),
|
|
@@ -253,7 +244,7 @@ export function fileCoordinatorProposal(store, token, input, now) {
|
|
|
253
244
|
],
|
|
254
245
|
}),
|
|
255
246
|
...(input.deliverable === undefined ? {} : { deliverable: input.deliverable }),
|
|
256
|
-
filedVia: `mcp:${who.name}`, filedBy: { name: store.handle.prepare("SELECT created_by FROM
|
|
247
|
+
filedVia: `mcp:${who.name}`, filedBy: { name: store.handle.prepare("SELECT created_by FROM credential WHERE id = ? AND kind = 'coordinator'").get(who.cid)?.["created_by"] ?? null, kind: "coordinator" },
|
|
257
248
|
admittedRepos: who.repos,
|
|
258
249
|
}, now);
|
|
259
250
|
if (!made.ok)
|
package/dist/desktop-update.d.ts
CHANGED
|
@@ -94,6 +94,7 @@ export declare function prepareDesktopUpdate(stateDir: string, installed: string
|
|
|
94
94
|
export declare function sqliteLock(file: string, busyTimeoutMs?: number): DatabaseSync | null;
|
|
95
95
|
export declare const installationLock: (j: UpdateJournal) => DatabaseSync | null;
|
|
96
96
|
export declare function desktopUpdateStatus(stateDir: string): {
|
|
97
|
+
version?: never;
|
|
97
98
|
active: boolean;
|
|
98
99
|
phase: string;
|
|
99
100
|
detail: string;
|
|
@@ -107,7 +108,6 @@ export declare function desktopUpdateStatus(stateDir: string): {
|
|
|
107
108
|
wasRunning?: never;
|
|
108
109
|
backupPath?: never;
|
|
109
110
|
updateId?: never;
|
|
110
|
-
version?: never;
|
|
111
111
|
buildId?: never;
|
|
112
112
|
workDir?: never;
|
|
113
113
|
} | {
|
package/dist/dispose.js
CHANGED
|
@@ -707,10 +707,6 @@ export function maybeTriggerRepair(store, repo, evidenceRoot, sourceRunId, verdi
|
|
|
707
707
|
};
|
|
708
708
|
const briefBytes = Buffer.from(JSON.stringify(draftBrief, null, 2), "utf8");
|
|
709
709
|
const key = writeEvidenceFile(evidenceRoot, sourceRunId, `repair-brief-${sourceRunId}-${attempt}.json`, briefBytes);
|
|
710
|
-
// Suffixes survive truncation (the CI-repair rule, verbatim): the prefix
|
|
711
|
-
// gives way, the identity-bearing tail never does.
|
|
712
|
-
const suffix = observation ? `-evidence-${attempt}` : `-fix-${attempt}`;
|
|
713
|
-
const draftId = `${rootTask.slice(0, 64 - suffix.length)}${suffix}`;
|
|
714
710
|
// The draft files through the ONE revision boundary: the source's goal,
|
|
715
711
|
// exclusions, touches, rubric, risk, quality, posture, budget, overrides
|
|
716
712
|
// and pins are read from the SOURCE ROWS inside the seal — the scope
|
|
@@ -719,7 +715,6 @@ export function maybeTriggerRepair(store, repo, evidenceRoot, sourceRunId, verdi
|
|
|
719
715
|
source: { task: ref.externalId, run: sourceRunId, scopeDigest: scope.digest },
|
|
720
716
|
brief: { evidenceRoot, key, sha256: createHash("sha256").update(briefBytes).digest("hex"), bytes: briefBytes.length, capture: "machine-authored repair brief (exit 0)" },
|
|
721
717
|
child: {
|
|
722
|
-
id: draftId,
|
|
723
718
|
title: observation ? `Collect missing evidence for ${ref.externalId}` : verification === undefined ? `repair ${ref.externalId}: ${unresolved.length} criteri${unresolved.length === 1 ? "on" : "a"} unmet` : `Fix failed checks for ${ref.externalId}`,
|
|
724
719
|
repair: observation ? `Collect only the missing observations for ${unresolved.join(", ")}. Keep the saved candidate unchanged. The machine runs supported focused test observations and reuses its intact passing gate; a fresh review assesses the added evidence.` : verification === undefined ? `repair exactly the unmet criteria named below; a comment cannot widen the scope. Unmet: ${unresolved.join(", ")}.` : "Diagnose the saved failed project check and repair within the original scope. Preserve the acceptance criteria and verification command; the revision brief binds the exact failure evidence.",
|
|
725
720
|
},
|
package/dist/export.js
CHANGED
|
@@ -33,7 +33,7 @@ const SECRET_TABLES = new Set(["web_session", "ceremony_nonce", "chat_pair", "oa
|
|
|
33
33
|
const DERIVED_TABLE = /^(sqlite_|memory_search)/;
|
|
34
34
|
/** Columns that hold a secret (or what stands in for one): never exported. */
|
|
35
35
|
const SECRET_COLUMNS = new Set([
|
|
36
|
-
"
|
|
36
|
+
"credential.secret_hash", "approver.credential_hash", "runner.credential_hash", "invite.token_hash",
|
|
37
37
|
"flow_trigger.hook_hash", "push_subscription.endpoint", "push_subscription.p256dh", "push_subscription.auth",
|
|
38
38
|
"chat_turn.credential_key", "lead_session.credential_key", "mate_turn.credential_key", "chat_runtime.push_url", "workflow_preview.token",
|
|
39
39
|
"quota.credential_fp", "oauth_grant.renew_hash", "oauth_client.source_hash",
|
|
@@ -56,7 +56,7 @@ export function categoryOf(table) {
|
|
|
56
56
|
[/^subagent/, "subagents"],
|
|
57
57
|
[/^(flow|workflow_)/, "flows"],
|
|
58
58
|
[/^(chat_|lead_|team_|notification|push_)/, "chats"],
|
|
59
|
-
[/^(approver|
|
|
59
|
+
[/^(approver|credential$|invite|sso_identity|coordinator_|oauth_)/, "people"],
|
|
60
60
|
[/^(run|artifact|claim|worktree$|execution_slot|criterion_review|diff_comment|proof_|review_request|repair_chain|incident|publication$|merge_|side_spend|knowledge_snapshot|learning_snapshot|skill_snapshot)/, "runs"],
|
|
61
61
|
[/^(task|hold$|plan_|scope_|tool_seal|decision|external_|skill_test)/, "tasks"],
|
|
62
62
|
[/^(project|approval_policy|capability|verify_command|worktree_setup|backend_grant|intake_grant|publication_grant|operating_mode|knowledge_|learning_|memory_|skill_)/, "projects"],
|
package/dist/lead-status.d.ts
CHANGED
|
@@ -105,7 +105,7 @@ export declare function repairStaleStatuses(store: Store, now: Date): {
|
|
|
105
105
|
* `repos`: the only projects to report (a remote person limited to some projects); null is the whole installation.
|
|
106
106
|
* A limited report counts only those projects' tasks and leaves out what belongs to the installation as a whole:
|
|
107
107
|
* the release check, plan windows and the lead line are empty, never another project's. */
|
|
108
|
-
export declare function installationStatus(store: Store, now: Date, viewer?: string | null, repos?: readonly string[] | null): InstallationStatus;
|
|
108
|
+
export declare function installationStatus(store: Store, now: Date, viewer?: string | null, repos?: readonly string[] | null, root?: string): InstallationStatus;
|
|
109
109
|
/** `limited`: a report over some projects (installationStatus's `repos`), which has no release check or plan windows to state. */
|
|
110
110
|
export declare function renderInstallationStatus(status: InstallationStatus, limited?: boolean): string[];
|
|
111
111
|
export declare function renderTaskWait(snapshot: TaskWaitSnapshot, outcome?: string): string;
|
package/dist/lead-status.js
CHANGED
|
@@ -183,7 +183,7 @@ export function repairStaleStatuses(store, now) {
|
|
|
183
183
|
* `repos`: the only projects to report (a remote person limited to some projects); null is the whole installation.
|
|
184
184
|
* A limited report counts only those projects' tasks and leaves out what belongs to the installation as a whole:
|
|
185
185
|
* the release check, plan windows and the lead line are empty, never another project's. */
|
|
186
|
-
export function installationStatus(store, now, viewer = currentActor()?.account ?? null, repos = null) {
|
|
186
|
+
export function installationStatus(store, now, viewer = currentActor()?.account ?? null, repos = null, root) {
|
|
187
187
|
const admitted = (row) => repos === null || typeof row["repo"] === "string" && repos.includes(row["repo"]);
|
|
188
188
|
const access = { principal: "operator", repos, includeUnplaced: repos === null };
|
|
189
189
|
const runningRows = store.handle.prepare(`SELECT run.id, run.role, run.phase, ref.id AS task_ref, ref.external_id AS task, ref.repo
|
|
@@ -262,7 +262,7 @@ export function installationStatus(store, now, viewer = currentActor()?.account
|
|
|
262
262
|
observedAt: String(row["observed_at"]),
|
|
263
263
|
})),
|
|
264
264
|
signIn: pauses.map(one => ({ provider: one.provider, reason: signInReason(one), command: signInCommand(one), since: one.openedAt })),
|
|
265
|
-
tasks: workIndexPage(store, now, { ...access, viewer }, { limit: 8 }).items
|
|
265
|
+
tasks: workIndexPage(store, now, { ...access, viewer }, { limit: 8, ...(root === undefined ? {} : { root }) }).items
|
|
266
266
|
.map(one => ({ task: one.rootId, title: one.title, headline: one.status.label, sentence: one.status.detail })),
|
|
267
267
|
lead: repos !== null ? null : (() => {
|
|
268
268
|
// Only the asker's own lead: another person's lead is never "Your lead".
|
package/dist/mcp-http.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { adapterPolicy } from "./server/route-policy.js";
|
|
2
2
|
import { PACKAGE_VERSION } from "./version.js";
|
|
3
|
-
import {
|
|
3
|
+
import { tokenLive, tokenProjects } from "./api-tokens.js";
|
|
4
4
|
import { oauthProjects, oauthTokenAllowed, RESOURCE_PATH } from "./mcp-oauth.js";
|
|
5
5
|
import { authenticateCoordinator } from "./coordinator.js";
|
|
6
6
|
import { LEGACY, MAX_REQUEST, META_CAPABILITIES, META_SERVER, META_VERSION, MODERN, TOOLS, UNSUPPORTED_VERSION, callCoordinatorTool, capabilitiesShape, readMessage, shapeResult, toolsPayload, } from "./mcp-core.js";
|
|
@@ -26,10 +26,10 @@ export function createMcpHttp(options) {
|
|
|
26
26
|
if (presented.startsWith("so_")) {
|
|
27
27
|
if (!options.signedIn(request))
|
|
28
28
|
return null;
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
if (
|
|
29
|
+
// The secret is checked here too (the one credential check), so this door never rests on how signedIn reads the request.
|
|
30
|
+
const found = store.presentedCredential(presented);
|
|
31
|
+
const kept = found?.kind === "person" ? store.apiTokenSecret(found.id) : null;
|
|
32
|
+
if (kept === null)
|
|
33
33
|
return null;
|
|
34
34
|
if (!tokenLive(kept.row, options.clock().getTime()))
|
|
35
35
|
return null;
|
package/dist/operate.js
CHANGED
|
@@ -96,7 +96,8 @@ import { deleteProject, holdingsWords, projectHoldings, projectRunning } from ".
|
|
|
96
96
|
import { pushPass } from "./push.js";
|
|
97
97
|
import { chmodSync, closeSync, constants as fsConstants, existsSync, fstatSync, fsyncSync, openSync, readFileSync, readSync, realpathSync, renameSync, unlinkSync, writeSync, writeFileSync } from "node:fs";
|
|
98
98
|
import { BRANCH_PREFIX, envTwins, envValue, existingOrFirst, taskBranches } from "./names.js";
|
|
99
|
-
import { claimActor, currentActor,
|
|
99
|
+
import { claimActor, currentActor, withActor } from "./actor.js";
|
|
100
|
+
import { parseLeadToken } from "./api-tokens.js";
|
|
100
101
|
import { CLI_PASSWORD_SOURCE, withPasswordSource } from "./sign-in-guard.js";
|
|
101
102
|
import { admissionRecorded, admitProject, commandLineActor } from "./project-admission.js";
|
|
102
103
|
import { leadClaim, leadSay, noteLeadWork } from "./lead-voice.js";
|
|
@@ -179,7 +180,7 @@ import { CHAT_APPROVE_ALL, presetTerms, modeTermsJson, modeDigestOf, modeTermsFr
|
|
|
179
180
|
import { WorktreePool } from "./worktree.js";
|
|
180
181
|
import { requestTaskStop, resumeTaskStop, taskControlOf } from "./task-control.js";
|
|
181
182
|
import { worktreeAdoptionNotice } from "./worktree-notices.js";
|
|
182
|
-
import { workIndexPage, WorkIndexCursorError } from "./work-index.js";
|
|
183
|
+
import { workIndexPage, workIndexTask, WorkIndexCursorError } from "./work-index.js";
|
|
183
184
|
import { readExecutorOf } from "./read-executor.js";
|
|
184
185
|
import { parseWorkView } from "./workspace-ui.js";
|
|
185
186
|
import { taskWorkSummaryOf } from "./work-summary.js";
|
|
@@ -8307,10 +8308,10 @@ async function statusCommand(positional, flags, context) {
|
|
|
8307
8308
|
// that belongs to the installation or other projects (updates, integrations, builds by project, unsent replies).
|
|
8308
8309
|
const lens = context.principal?.lens ?? null;
|
|
8309
8310
|
if (lens !== null) {
|
|
8310
|
-
const status = installationStatus(context.store, context.clock(), viewer, lens);
|
|
8311
|
+
const status = installationStatus(context.store, context.clock(), viewer, lens, context.evidenceRoot);
|
|
8311
8312
|
return succeed(context.write, context.json, command, { ...status, projects: [] }, () => renderInstallationStatus(status, true));
|
|
8312
8313
|
}
|
|
8313
|
-
const status = installationStatus(context.store, context.clock(), viewer);
|
|
8314
|
+
const status = installationStatus(context.store, context.clock(), viewer, null, context.evidenceRoot);
|
|
8314
8315
|
// One line, only when a newer Toolroll exists; offline or switched off says nothing.
|
|
8315
8316
|
const current = PACKAGE_VERSION;
|
|
8316
8317
|
const method = installMethod(context.installBin);
|
|
@@ -8927,6 +8928,7 @@ function listTasks(flags, context) {
|
|
|
8927
8928
|
view: parseWorkView(rawView ?? null), limit, cursor: text(flags, 'cursor') ?? null,
|
|
8928
8929
|
...(wanted === undefined ? {} : { state: wanted }),
|
|
8929
8930
|
...(text(flags, 'repo') === undefined ? {} : { project: text(flags, 'repo') }),
|
|
8931
|
+
root: context.evidenceRoot,
|
|
8930
8932
|
};
|
|
8931
8933
|
const refused = (error) => {
|
|
8932
8934
|
if (!(error instanceof WorkIndexCursorError))
|
|
@@ -8990,10 +8992,18 @@ function showTask(positional, context) {
|
|
|
8990
8992
|
const repo = store.lookupRef(blocker)?.repo;
|
|
8991
8993
|
return repo != null && lens.includes(repo);
|
|
8992
8994
|
});
|
|
8995
|
+
const work = taskWorkSummaryOf(store, id, now, access);
|
|
8996
|
+
const current = assignmentOf(store, id, now, access, context.evidenceRoot);
|
|
8997
|
+
// Read the same saved facts and statusOf path as task list. File health remains
|
|
8998
|
+
// in the assignment diagnostics and action-time guards; it cannot rewrite history.
|
|
8999
|
+
const listed = workIndexTask(store, id, now, access, context.evidenceRoot);
|
|
9000
|
+
const shared = listed === null ? null : { headline: listed.status.label, sentence: listed.status.detail };
|
|
9001
|
+
const primaryAction = listed === null ? work?.primaryAction ?? null : listed.primaryAction;
|
|
8993
9002
|
const detail = {
|
|
8994
9003
|
task,
|
|
8995
|
-
work:
|
|
8996
|
-
|
|
9004
|
+
work: work === null || listed === null ? work : { ...work, status: { ...work.status, label: listed.status.label, detail: listed.status.detail, tone: listed.status.tone }, primaryAction },
|
|
9005
|
+
status: shared === null ? null : { headline: shared.headline, sentence: shared.sentence },
|
|
9006
|
+
assignment: assignmentBrief(current),
|
|
8997
9007
|
ref: ref.id,
|
|
8998
9008
|
// One phrase for every inaccessible blocker, so even their number stays private.
|
|
8999
9009
|
blockedBy: visibleBlockers.length === blockers.length ? visibleBlockers : [...visibleBlockers, "a task in another project"],
|
|
@@ -9033,8 +9043,8 @@ function showTask(positional, context) {
|
|
|
9033
9043
|
`${task.id} ${task.state}${ref.deliverable === "report" ? " (scout — delivers a report)" : ""}`,
|
|
9034
9044
|
` ${task.title}`,
|
|
9035
9045
|
...(detail.work === null ? [] : [
|
|
9036
|
-
` status: ${detail.work.status.label}`,
|
|
9037
|
-
...(
|
|
9046
|
+
` status: ${shared?.headline ?? detail.work.status.label}${shared === null ? "" : ` — ${shared.sentence}`}`,
|
|
9047
|
+
...(primaryAction === null ? [] : [` next: ${primaryAction.label}${primaryAction.target.decisionId === null ? "" : ` — decision #${primaryAction.target.decisionId}`}`]),
|
|
9038
9048
|
]),
|
|
9039
9049
|
// The closed machine-authored verdict (Priority 2), computed once at
|
|
9040
9050
|
// completion — never re-derived here. Same words `verdictWords`
|
|
@@ -9042,7 +9052,7 @@ function showTask(positional, context) {
|
|
|
9042
9052
|
...(task.state !== "done" || detail.proofVerdict === null
|
|
9043
9053
|
? []
|
|
9044
9054
|
: [
|
|
9045
|
-
` proof: ${proofVerdictWords(detail.proofVerdict, detail.proofReasons).word}${detail.proofAccepted ? " (accepted)" : ""}`,
|
|
9055
|
+
` proof: ${proofVerdictWords(current?.receipt?.runId === latestFinished?.id ? current?.readiness?.verdict ?? detail.proofVerdict : detail.proofVerdict, detail.proofReasons).word}${detail.proofAccepted ? " (accepted)" : ""}`,
|
|
9046
9056
|
...(detail.proofReasons.length > 0 ? [` ${detail.proofReasons.join("; ")}`] : []),
|
|
9047
9057
|
...matrixWords(detail.proofMatrix),
|
|
9048
9058
|
...(detail.semanticCoverage === null ? [] : coverageWords(detail.semanticCoverage).map(line => ` ${line}`)),
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D7: a password on each request (`Authorization: Bearer <name>:<password>`) is deprecated. This release still
|
|
3
|
+
* accepts it everywhere it was accepted, with the same per-address tries, locks and budgets, but every answer carries
|
|
4
|
+
* a deprecation warning, and every refusal of such a request names the replacement: an API token. From
|
|
5
|
+
* PASSWORD_BEARER_REFUSED_FROM on, the server refuses it before checking the password.
|
|
6
|
+
*/
|
|
7
|
+
import type { ServerResponse } from "node:http";
|
|
8
|
+
/** The one replacement every machine-credential refusal names. */
|
|
9
|
+
export declare const TOKEN_REPLACEMENT = "Create an API token with `toolroll tokens create`";
|
|
10
|
+
/** The first release that refuses a password on a request: one release after this one (0.9.58) deprecated it. */
|
|
11
|
+
export declare const PASSWORD_BEARER_REFUSED_FROM = "0.9.59";
|
|
12
|
+
export declare const PASSWORD_BEARER_WARNING = "Signing in with a password on each request is deprecated and stops working in Toolroll 0.9.59. Create an API token with `toolroll tokens create` and send it as the bearer instead.";
|
|
13
|
+
/** What the CLI says on stderr when a saved connection signs in with a password instead of an API token. */
|
|
14
|
+
export declare const PASSWORD_PROFILE_WARNING = "Warning: this connection signs in with your password on each request, which is deprecated and stops working in Toolroll 0.9.59. Create an API token with `toolroll tokens create`, then connect again with --token-stdin.";
|
|
15
|
+
export declare const PASSWORD_BEARER_REFUSED = "Toolroll no longer accepts a password on a request. Create an API token with `toolroll tokens create` and send it as the bearer instead.";
|
|
16
|
+
/** Whether an Authorization header is a password bearer: a name and a secret, never an API token. */
|
|
17
|
+
export declare function isPasswordBearer(authorization: string | undefined): boolean;
|
|
18
|
+
/** Whether this build still accepts a password bearer: only a release before PASSWORD_BEARER_REFUSED_FROM does. */
|
|
19
|
+
export declare function passwordBearerAccepted(version?: string): boolean;
|
|
20
|
+
/** A refusal's words for this request: unchanged, or with the replacement when it came with a password bearer. */
|
|
21
|
+
export declare function withReplacement(authorization: string | undefined, message: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* The refusal boundary for one password-bearer request, whichever adapter answers it: the deprecation is announced in
|
|
24
|
+
* headers (RFC 9745 Deprecation, and a Warning naming the replacement), and any refusal's words, plain text or a JSON
|
|
25
|
+
* `message`, gain the replacement. Successful answers are left byte for byte.
|
|
26
|
+
*/
|
|
27
|
+
export declare function announcePasswordBearer(response: ServerResponse): void;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { PACKAGE_VERSION } from "./version.js";
|
|
2
|
+
/** The one replacement every machine-credential refusal names. */
|
|
3
|
+
export const TOKEN_REPLACEMENT = "Create an API token with `toolroll tokens create`";
|
|
4
|
+
/** The first release that refuses a password on a request: one release after this one (0.9.58) deprecated it. */
|
|
5
|
+
export const PASSWORD_BEARER_REFUSED_FROM = "0.9.59";
|
|
6
|
+
export const PASSWORD_BEARER_WARNING = `Signing in with a password on each request is deprecated and stops working in Toolroll ${PASSWORD_BEARER_REFUSED_FROM}. ${TOKEN_REPLACEMENT} and send it as the bearer instead.`;
|
|
7
|
+
/** What the CLI says on stderr when a saved connection signs in with a password instead of an API token. */
|
|
8
|
+
export const PASSWORD_PROFILE_WARNING = `Warning: this connection signs in with your password on each request, which is deprecated and stops working in Toolroll ${PASSWORD_BEARER_REFUSED_FROM}. ${TOKEN_REPLACEMENT}, then connect again with --token-stdin.`;
|
|
9
|
+
export const PASSWORD_BEARER_REFUSED = `Toolroll no longer accepts a password on a request. ${TOKEN_REPLACEMENT} and send it as the bearer instead.`;
|
|
10
|
+
/** Whether an Authorization header is a password bearer: a name and a secret, never an API token. */
|
|
11
|
+
export function isPasswordBearer(authorization) {
|
|
12
|
+
return authorization !== undefined && /^Bearer (.+):(.+)$/.test(authorization) && !/^Bearer so_/.test(authorization);
|
|
13
|
+
}
|
|
14
|
+
const parts = (version) => (/^(\d+)\.(\d+)\.(\d+)/.exec(version) ?? [0, 0, 0, 0]).slice(1).map(Number);
|
|
15
|
+
/** Whether this build still accepts a password bearer: only a release before PASSWORD_BEARER_REFUSED_FROM does. */
|
|
16
|
+
export function passwordBearerAccepted(version = PACKAGE_VERSION) {
|
|
17
|
+
const have = parts(version), limit = parts(PASSWORD_BEARER_REFUSED_FROM);
|
|
18
|
+
for (let index = 0; index < 3; index++)
|
|
19
|
+
if (have[index] !== limit[index])
|
|
20
|
+
return have[index] < limit[index];
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
/** A refusal's words for this request: unchanged, or with the replacement when it came with a password bearer. */
|
|
24
|
+
export function withReplacement(authorization, message) {
|
|
25
|
+
return !isPasswordBearer(authorization) || message.includes("toolroll tokens create") ? message : `${message} ${PASSWORD_BEARER_WARNING}`;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The refusal boundary for one password-bearer request, whichever adapter answers it: the deprecation is announced in
|
|
29
|
+
* headers (RFC 9745 Deprecation, and a Warning naming the replacement), and any refusal's words, plain text or a JSON
|
|
30
|
+
* `message`, gain the replacement. Successful answers are left byte for byte.
|
|
31
|
+
*/
|
|
32
|
+
export function announcePasswordBearer(response) {
|
|
33
|
+
response.setHeader("Deprecation", "true");
|
|
34
|
+
response.setHeader("Warning", `299 toolroll "${PASSWORD_BEARER_WARNING.replace(/"/g, "'")}"`);
|
|
35
|
+
const end = response.end.bind(response);
|
|
36
|
+
response.end = ((...args) => {
|
|
37
|
+
const [chunk] = args;
|
|
38
|
+
if (response.statusCode >= 400 && typeof chunk === "string" && !chunk.includes("toolroll tokens create"))
|
|
39
|
+
args[0] = annotated(chunk);
|
|
40
|
+
return end(...args);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
function annotated(body) {
|
|
44
|
+
if (/^\s*\{/.test(body)) {
|
|
45
|
+
try {
|
|
46
|
+
const value = JSON.parse(body);
|
|
47
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
48
|
+
const row = value;
|
|
49
|
+
if (typeof row["message"] === "string")
|
|
50
|
+
return JSON.stringify({ ...row, message: `${row["message"]} ${PASSWORD_BEARER_WARNING}` });
|
|
51
|
+
if (typeof row["error_description"] === "string")
|
|
52
|
+
return JSON.stringify({ ...row, error_description: `${row["error_description"]} ${PASSWORD_BEARER_WARNING}` });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch { /* not JSON after all: plain text below */ }
|
|
56
|
+
return body;
|
|
57
|
+
}
|
|
58
|
+
return body.trimStart().startsWith("<") ? body : `${body.replace(/\s+$/, "")}\n${PASSWORD_BEARER_WARNING}`;
|
|
59
|
+
}
|
package/dist/proof.d.ts
CHANGED
|
@@ -437,6 +437,15 @@ export declare function matrixWords(matrix: readonly CriterionMatrixRow[]): stri
|
|
|
437
437
|
/** One criterion's context standing in words — shared by the CLI matrix
|
|
438
438
|
* and the console badge title so the two never drift. */
|
|
439
439
|
export declare function coverageStateWords(coverage: CriterionCoverage): string;
|
|
440
|
+
/** The verdict a result shows today, read from the stored one and the facts recorded since, never written back: a
|
|
441
|
+
* failed check or an unresolved HIGH review finding refutes; a pending review, or any acceptance criterion that has
|
|
442
|
+
* not passed, keeps it short. It only ever lowers the stored verdict. */
|
|
443
|
+
export declare function shownVerdictOf(stored: ProofVerdict | null, facts: {
|
|
444
|
+
checkFailed: boolean;
|
|
445
|
+
highFindings: number;
|
|
446
|
+
reviewPending: boolean;
|
|
447
|
+
criteriaPassed: boolean;
|
|
448
|
+
}): ProofVerdict | null;
|
|
440
449
|
/** Plain words for a verdict, shared by every surface (the `summary.ts`
|
|
441
450
|
* pattern) so the task page, run page, board, CLI, and chat cannot say
|
|
442
451
|
* three different things about the same run. */
|
package/dist/proof.js
CHANGED
|
@@ -976,6 +976,18 @@ export function coverageStateWords(coverage) {
|
|
|
976
976
|
return `GAP — inherited context is missing: ${coverage.gaps.join("; ")}${coverage.items.length > 0 ? ` (partial: ${coverage.items.join(", ")})` : ""}${prior}`;
|
|
977
977
|
}
|
|
978
978
|
}
|
|
979
|
+
/** The verdict a result shows today, read from the stored one and the facts recorded since, never written back: a
|
|
980
|
+
* failed check or an unresolved HIGH review finding refutes; a pending review, or any acceptance criterion that has
|
|
981
|
+
* not passed, keeps it short. It only ever lowers the stored verdict. */
|
|
982
|
+
export function shownVerdictOf(stored, facts) {
|
|
983
|
+
if (facts.checkFailed || facts.highFindings > 0 || stored === "refuted")
|
|
984
|
+
return "refuted";
|
|
985
|
+
if (stored === null)
|
|
986
|
+
return null;
|
|
987
|
+
if (facts.reviewPending || !facts.criteriaPassed)
|
|
988
|
+
return "short";
|
|
989
|
+
return stored;
|
|
990
|
+
}
|
|
979
991
|
/** Plain words for a verdict, shared by every surface (the `summary.ts`
|
|
980
992
|
* pattern) so the task page, run page, board, CLI, and chat cannot say
|
|
981
993
|
* three different things about the same run. */
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Store } from "./store.js";
|
|
2
|
+
export type ReleaseCoverage = {
|
|
3
|
+
run: number;
|
|
4
|
+
head: string;
|
|
5
|
+
};
|
|
6
|
+
/** How many of the newest passing release checks one read considers. */
|
|
7
|
+
export declare const RELEASE_SCAN_LIMIT = 20;
|
|
8
|
+
export type IsAncestor = (repo: string, ancestor: string, descendant: string) => boolean | null;
|
|
9
|
+
/** `git merge-base --is-ancestor`: true, false, or null when Git couldn't say. */
|
|
10
|
+
export declare const gitIsAncestor: IsAncestor;
|
|
11
|
+
export type ReleaseMemo = Map<string, ReleaseCoverage[]>;
|
|
12
|
+
/** The release check covering `head` in `repo`, or null. `memo` keeps one read's receipts. */
|
|
13
|
+
export declare function releaseCoverageOf(store: Store, root: string | undefined, repo: string | null, head: string | null, options?: {
|
|
14
|
+
memo?: ReleaseMemo;
|
|
15
|
+
isAncestor?: IsAncestor;
|
|
16
|
+
}): ReleaseCoverage | null;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** Whether a release check already covers a result whose checks were Off: a
|
|
2
|
+
* passing FULL release check (run_check.release) on the same project whose
|
|
3
|
+
* checked commit contains the result's commit. Read at read time from the
|
|
4
|
+
* existing records and Git; nothing is written back, so no proof, receipt,
|
|
5
|
+
* scope digest or schema changes. Fails closed: a receipt that can't be read,
|
|
6
|
+
* a missing commit or any Git error leaves the result uncovered. */
|
|
7
|
+
import { execFileSync } from "node:child_process";
|
|
8
|
+
import { verificationEvidence } from "./verification-evidence.js";
|
|
9
|
+
import { runCheckLevel } from "./check-levels.js";
|
|
10
|
+
/** How many of the newest passing release checks one read considers. */
|
|
11
|
+
export const RELEASE_SCAN_LIMIT = 20;
|
|
12
|
+
const SHA = /^[a-f0-9]{40}$/;
|
|
13
|
+
/** Ancestry between two fixed commits never changes, so a settled answer is kept; errors are not. */
|
|
14
|
+
const ancestry = new Map();
|
|
15
|
+
const ANCESTRY_LIMIT = 4096;
|
|
16
|
+
/** `git merge-base --is-ancestor`: true, false, or null when Git couldn't say. */
|
|
17
|
+
export const gitIsAncestor = (repo, ancestor, descendant) => {
|
|
18
|
+
if (!SHA.test(ancestor) || !SHA.test(descendant))
|
|
19
|
+
return null;
|
|
20
|
+
if (ancestor === descendant)
|
|
21
|
+
return true;
|
|
22
|
+
const key = `${repo}\0${ancestor}\0${descendant}`;
|
|
23
|
+
const known = ancestry.get(key);
|
|
24
|
+
if (known !== undefined)
|
|
25
|
+
return known;
|
|
26
|
+
let answer;
|
|
27
|
+
try {
|
|
28
|
+
execFileSync("git", ["-C", repo, "merge-base", "--is-ancestor", ancestor, descendant], { stdio: "ignore", timeout: 5_000 });
|
|
29
|
+
answer = true;
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
// Exit 1 is Git's "no"; anything else (a missing object, no repository, a timeout) is no answer.
|
|
33
|
+
answer = error.status === 1 ? false : null;
|
|
34
|
+
}
|
|
35
|
+
if (answer !== null) {
|
|
36
|
+
if (ancestry.size >= ANCESTRY_LIMIT)
|
|
37
|
+
ancestry.clear();
|
|
38
|
+
ancestry.set(key, answer);
|
|
39
|
+
}
|
|
40
|
+
return answer;
|
|
41
|
+
};
|
|
42
|
+
/** The newest passing full release checks of one project, each with its sealed, verified commit. Read once per
|
|
43
|
+
* caller-held memo, so a list of results reads the receipts once. */
|
|
44
|
+
function passingReleases(store, root, repo) {
|
|
45
|
+
let rows;
|
|
46
|
+
try {
|
|
47
|
+
rows = store.handle.prepare(`SELECT rc.run FROM run_check AS rc INDEXED BY run_check_release
|
|
48
|
+
JOIN run ON run.id = rc.run JOIN task_ref AS ref ON ref.id = run.task_ref
|
|
49
|
+
WHERE rc.release = 1 AND rc.status = 'passed' AND rc.exit_code = 0 AND ref.repo = ?
|
|
50
|
+
ORDER BY rc.run DESC LIMIT ${RELEASE_SCAN_LIMIT}`).all(repo);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
return rows.flatMap(row => {
|
|
56
|
+
const run = Number(row["run"]);
|
|
57
|
+
try {
|
|
58
|
+
// Full only: a quick or Off release check covers nothing.
|
|
59
|
+
const level = runCheckLevel(store, run);
|
|
60
|
+
if (level !== null && level !== "full")
|
|
61
|
+
return [];
|
|
62
|
+
const gate = verificationEvidence(store, root, run);
|
|
63
|
+
if (!gate.ok || gate.bytes === null)
|
|
64
|
+
return [];
|
|
65
|
+
const receipt = JSON.parse(gate.bytes);
|
|
66
|
+
const head = store.getRun(run)?.headRevision ?? null;
|
|
67
|
+
if (receipt.run !== run || receipt.result?.ran !== true || receipt.result.exitCode !== 0)
|
|
68
|
+
return [];
|
|
69
|
+
if (typeof receipt.command?.repo === "string" && receipt.command.repo.startsWith("quick:"))
|
|
70
|
+
return [];
|
|
71
|
+
if (typeof receipt.head !== "string" || !SHA.test(receipt.head) || receipt.head !== head)
|
|
72
|
+
return [];
|
|
73
|
+
return [{ run, head: receipt.head }];
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/** The release check covering `head` in `repo`, or null. `memo` keeps one read's receipts. */
|
|
81
|
+
export function releaseCoverageOf(store, root, repo, head, options = {}) {
|
|
82
|
+
if (root === undefined || repo === null || head === null || !SHA.test(head))
|
|
83
|
+
return null;
|
|
84
|
+
const memo = options.memo ?? new Map();
|
|
85
|
+
let releases = memo.get(repo);
|
|
86
|
+
if (releases === undefined) {
|
|
87
|
+
releases = passingReleases(store, root, repo);
|
|
88
|
+
memo.set(repo, releases);
|
|
89
|
+
}
|
|
90
|
+
const isAncestor = options.isAncestor ?? gitIsAncestor;
|
|
91
|
+
return releases.find(one => isAncestor(repo, head, one.head) === true) ?? null;
|
|
92
|
+
}
|
package/dist/remote-command.js
CHANGED
|
@@ -34,8 +34,6 @@ export const REMOTE_PATH_ARGUMENTS = [
|
|
|
34
34
|
...['link', 'unlink'].map(invocation => ({ invocation, argument: { flag: '--to' }, policy: 'refuse' })),
|
|
35
35
|
...['repos add', 'repos remove', 'pulls', 'graph', ''].map(invocation => ({ invocation, argument: { position: 0, name: 'path', rest: true }, policy: 'refuse' })),
|
|
36
36
|
{ invocation: 'project use', argument: { position: 0, name: 'checkout path' }, policy: 'refuse' },
|
|
37
|
-
// These local HTTP clients read prompts themselves; they do not consume runOperateAs's files map.
|
|
38
|
-
...['session start', 'session send'].map(invocation => ({ invocation, argument: { flag: '--file' }, policy: 'refuse' })),
|
|
39
37
|
];
|
|
40
38
|
/**
|
|
41
39
|
* Parse once using declared arity, so a flag's value cannot be mistaken for another flag or a
|
package/dist/remote-exec.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type TeamCliOptions } from './team-cli.js';
|
|
2
|
+
import { type CliRequest } from './cli-http.js';
|
|
2
3
|
import { type RemoteCommandLookup } from './remote-command.js';
|
|
3
4
|
export { contractRow, STEP_UP_MESSAGE, type RemoteMode } from './remote-command.js';
|
|
4
5
|
export type RemoteExecOptions = TeamCliOptions & {
|
|
@@ -9,6 +10,24 @@ export type RemoteExecOptions = TeamCliOptions & {
|
|
|
9
10
|
/** Injected by tests: the contract row lookup (default: COMMAND_GUIDE's `remote` field). */
|
|
10
11
|
modeOf?: RemoteCommandLookup;
|
|
11
12
|
};
|
|
13
|
+
type Answer = {
|
|
14
|
+
exitCode: number;
|
|
15
|
+
stdout: string;
|
|
16
|
+
stderr: string;
|
|
17
|
+
};
|
|
18
|
+
export type Outcome = {
|
|
19
|
+
kind: 'answer';
|
|
20
|
+
answer: Answer;
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'refused';
|
|
23
|
+
status: number;
|
|
24
|
+
code: string;
|
|
25
|
+
message: string;
|
|
26
|
+
} | {
|
|
27
|
+
kind: 'unconfirmed';
|
|
28
|
+
};
|
|
29
|
+
/** One request, never retried. */
|
|
30
|
+
export declare function postCli(origin: string, token: string, request: CliRequest, options: RemoteExecOptions): Promise<Outcome>;
|
|
12
31
|
/**
|
|
13
32
|
* null: run locally with argv unchanged. `{ local }`: run locally with this argv (`--local` removed).
|
|
14
33
|
* A number: the command ran remotely (or was refused before sending) and this is its exit code.
|