toolroll 0.9.89 → 0.9.90
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 +44 -0
- package/README.md +16 -10
- package/THIRD_PARTY_NOTICES.md +37 -0
- package/dist/agent-board.d.ts +9 -0
- package/dist/agent-board.js +20 -0
- package/dist/agent-fence.js +20 -2
- package/dist/approval-authority.d.ts +7 -0
- package/dist/approval-authority.js +13 -2
- package/dist/browser/THIRD_PARTY_NOTICES.txt +692 -28
- package/dist/browser/terminal.js +42 -42
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +32 -32
- package/dist/browser-workspace.d.ts +13 -0
- package/dist/builder.js +5 -2
- package/dist/chat-actions.d.ts +5 -0
- package/dist/chat-actions.js +7 -1
- package/dist/chat-delivery.js +2 -0
- package/dist/chat-task-actions.d.ts +1 -0
- package/dist/check-access.d.ts +3 -0
- package/dist/check-access.js +4 -1
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +6 -2
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +23 -0
- package/dist/coding-ui.js +4 -2
- package/dist/connect-cli.js +2 -1
- package/dist/console-instance.d.ts +34 -1
- package/dist/console-instance.js +124 -15
- package/dist/contracts/cli.d.ts +72 -0
- package/dist/contracts/cli.js +13 -0
- package/dist/contracts/console-api.d.ts +9 -2
- package/dist/contracts/console-api.js +9 -2
- package/dist/desktop-host.js +8 -3
- package/dist/desktop-update.js +2 -1
- package/dist/dispatch.d.ts +3 -0
- package/dist/dispatch.js +22 -2
- package/dist/easy-start-args.d.ts +13 -0
- package/dist/easy-start-args.js +24 -0
- package/dist/easy-start.d.ts +77 -0
- package/dist/easy-start.js +321 -0
- package/dist/first-run.d.ts +77 -0
- package/dist/first-run.js +45 -0
- package/dist/guides.js +1 -1
- package/dist/invoke.js +11 -6
- package/dist/laptop-background.d.ts +171 -0
- package/dist/laptop-background.js +550 -0
- package/dist/laptop-service.d.ts +111 -0
- package/dist/laptop-service.js +268 -0
- package/dist/lead-cli.js +2 -1
- package/dist/lead-doors.d.ts +6 -0
- package/dist/lead-doors.js +20 -2
- package/dist/live-records.d.ts +1 -1
- package/dist/live-records.js +3 -12
- package/dist/local-owner.d.ts +124 -0
- package/dist/local-owner.js +212 -0
- package/dist/login-secrets.d.ts +60 -0
- package/dist/login-secrets.js +195 -0
- package/dist/mcp-oauth.d.ts +4 -0
- package/dist/mcp-oauth.js +13 -7
- package/dist/model-choices.d.ts +43 -0
- package/dist/model-choices.js +80 -0
- package/dist/operate.d.ts +46 -1
- package/dist/operate.js +461 -40
- package/dist/person-proof.js +3 -0
- package/dist/project-knowledge.d.ts +7 -7
- package/dist/quick-start.d.ts +3 -1
- package/dist/quick-start.js +6 -1
- package/dist/release-cli.js +2 -1
- package/dist/runner-process.d.ts +9 -0
- package/dist/runner-process.js +18 -0
- package/dist/sample-project.d.ts +31 -0
- package/dist/sample-project.js +141 -0
- package/dist/scope.d.ts +24 -2
- package/dist/scope.js +74 -3
- package/dist/serve.js +129 -22
- package/dist/server/chat.js +10 -3
- package/dist/server/chrome.d.ts +10 -2
- package/dist/server/chrome.js +7 -5
- package/dist/server/console.css +10 -0
- package/dist/server/guards.d.ts +3 -0
- package/dist/server/guards.js +22 -2
- package/dist/server/http.d.ts +22 -0
- package/dist/server/http.js +58 -0
- package/dist/server/live.js +4 -14
- package/dist/server/mutation-key.d.ts +70 -0
- package/dist/server/mutation-key.js +142 -0
- package/dist/server/pages.js +10 -7
- package/dist/server/people-tokens.js +240 -20
- package/dist/server/remote.d.ts +2 -0
- package/dist/server/remote.js +19 -4
- package/dist/server/render-new-task.js +5 -1
- package/dist/server/render-pages.d.ts +2 -1
- package/dist/server/render-pages.js +1 -1
- package/dist/server/render-people.d.ts +36 -0
- package/dist/server/render-people.js +65 -1
- package/dist/server/render-projects.js +4 -2
- package/dist/server/render-tasks.d.ts +12 -0
- package/dist/server/render-tasks.js +12 -4
- package/dist/server/request-context.d.ts +6 -0
- package/dist/server/route-table.d.ts +16 -0
- package/dist/server/route-table.js +121 -0
- package/dist/server/runtime.d.ts +6 -4
- package/dist/server/session.d.ts +31 -1
- package/dist/server/session.js +12 -2
- package/dist/server/settings.js +63 -8
- package/dist/server/task-views.js +10 -1
- package/dist/server/tasks.d.ts +10 -0
- package/dist/server/tasks.js +59 -9
- package/dist/server/terminals.js +15 -2
- package/dist/server/welcome.d.ts +22 -16
- package/dist/server/welcome.js +150 -52
- package/dist/settings-groups-ui.d.ts +7 -1
- package/dist/settings-groups-ui.js +23 -2
- package/dist/settings-proofs.js +1 -0
- package/dist/setup-guide.d.ts +2 -2
- package/dist/store.d.ts +124 -1
- package/dist/store.js +245 -12
- package/dist/surface.d.ts +10 -0
- package/dist/surface.js +40 -1
- package/dist/task-text.d.ts +6 -0
- package/dist/task-text.js +17 -0
- package/dist/telegram-lead.js +2 -0
- package/dist/terminal-text.d.ts +11 -0
- package/dist/terminal-text.js +26 -0
- package/dist/toolroll-update.d.ts +8 -1
- package/dist/toolroll-update.js +151 -4
- package/dist/tui-contract.d.ts +118 -0
- package/dist/tui-contract.js +9 -0
- package/dist/tui-source.d.ts +14 -0
- package/dist/tui-source.js +182 -0
- package/dist/tui.js +129 -0
- package/dist/up-processes.d.ts +13 -1
- package/dist/up-processes.js +13 -3
- package/dist/welcome-facts.d.ts +18 -0
- package/dist/welcome-facts.js +79 -0
- package/package.json +4 -2
|
@@ -517,6 +517,8 @@ export type BrowserTaskView = {
|
|
|
517
517
|
/** When a remembered password stops counting (ISO); the page words it in the viewer's zone. */
|
|
518
518
|
rememberedUntil: string | null;
|
|
519
519
|
planHtml: string;
|
|
520
|
+
/** The terms line in words, when the rule isn't one of the kit's (easy-start C5: a laptop's own owner). */
|
|
521
|
+
termsWords?: string;
|
|
520
522
|
} | null;
|
|
521
523
|
/** Ship: the pull request's checks, each on one time axis (task ship's twin). Null: no checks read yet. */
|
|
522
524
|
ship?: {
|
|
@@ -1628,6 +1630,15 @@ export type BrowserWorkspace = {
|
|
|
1628
1630
|
phone?: BrowserPhoneCard;
|
|
1629
1631
|
/** The Chat landing's live view: who is working now, four counts, and Catch up in tabs. */
|
|
1630
1632
|
home?: BrowserHome | null;
|
|
1633
|
+
/** Easy-start C2: this tab's mutation key is from an earlier Toolroll start (or it has none): it reads, but needs to
|
|
1634
|
+
* reconnect before it can change anything (the password again, or `toolroll open` for a local owner). */
|
|
1635
|
+
reconnect?: {
|
|
1636
|
+
kind: 'password';
|
|
1637
|
+
href: string;
|
|
1638
|
+
} | {
|
|
1639
|
+
kind: 'command';
|
|
1640
|
+
command: string;
|
|
1641
|
+
};
|
|
1631
1642
|
};
|
|
1632
1643
|
/** Home ("/"): one sentence that answers "what needs me?", four counts that agree with the Tasks filters they open,
|
|
1633
1644
|
* what waits on this person grouped by what it asks, what is running, and the selected item answered in place. */
|
|
@@ -1669,6 +1680,8 @@ export type BrowserHomeView = {
|
|
|
1669
1680
|
title: string;
|
|
1670
1681
|
text: string;
|
|
1671
1682
|
} | null;
|
|
1683
|
+
/** While the first run is open (no first result yet): the welcome's address, linked as Finish setup. */
|
|
1684
|
+
setup?: string | null;
|
|
1672
1685
|
};
|
|
1673
1686
|
export type BrowserHomeRow = {
|
|
1674
1687
|
id: string;
|
package/dist/builder.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { legacyNetwork, repositoryEcosystems } from "./egress.js";
|
|
2
|
-
import { CHECK_ACCESS_DEFAULT, checkAccessHere, checkNetworkOf } from "./check-access.js";
|
|
2
|
+
import { CHECK_ACCESS_DEFAULT, checkAccessHere, checkNetworkOf, localModeCap } from "./check-access.js";
|
|
3
|
+
import { localModeOn } from "./local-owner.js";
|
|
3
4
|
import { checkoutGit } from "./teardown-git.js";
|
|
4
5
|
import { isAcpProvider } from "./acp-agents.js";
|
|
5
6
|
import { OBSERVATION_MAILBOX, observationBrief, parseObservationCases, collectObservations } from "./observations.js";
|
|
@@ -337,7 +338,9 @@ function commandRunner(store, runId, profile, inner) {
|
|
|
337
338
|
* run sandboxed while sign-ins are off, and the run is stamped with that, what actually ran), and under sign-ins the network its
|
|
338
339
|
* approval sealed less its agent's own provider hosts (never its checks'). */
|
|
339
340
|
export function checkLaunchOf(store, runId, profile, platform = process.platform) {
|
|
340
|
-
|
|
341
|
+
// Easy-start C5: while local mode is on, a sealed unsandboxed level launches with at most sign-ins (re-read here, at launch).
|
|
342
|
+
const asked = checkAccessHere(profile.checkAccess ?? CHECK_ACCESS_DEFAULT, platform);
|
|
343
|
+
const access = localModeOn(store) ? localModeCap(asked) : asked;
|
|
341
344
|
if (access !== "sign-ins")
|
|
342
345
|
return { access, network: null };
|
|
343
346
|
const taskRef = store.getRun(runId)?.taskRef;
|
package/dist/chat-actions.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/** Shared, exact-state actions. Models may prepare; only the existing human
|
|
2
|
+
* confirmation door executes. Protected actions use a one-use review receipt. */
|
|
3
|
+
import { type Authority } from "./local-owner.js";
|
|
1
4
|
import { type Store } from "./store.js";
|
|
2
5
|
import { type VerifiedApprover } from "./principal.js";
|
|
3
6
|
import { CHAT_ACTION_FIELDS, type SharedAction } from "./contracts/chat-actions.js";
|
|
@@ -292,6 +295,8 @@ export declare function mintSharedActionReview(store: Store, who: VerifiedApprov
|
|
|
292
295
|
};
|
|
293
296
|
export type SharedActionOptions = {
|
|
294
297
|
via: "web" | "cli" | "telegram" | "slack" | "discord" | "teams";
|
|
298
|
+
/** Easy-start C5: a laptop's local owner confirms (the door decided the operation is one of its acts). */
|
|
299
|
+
authority?: Authority;
|
|
295
300
|
root?: string;
|
|
296
301
|
review?: {
|
|
297
302
|
nonce: string;
|
package/dist/chat-actions.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Shared, exact-state actions. Models may prepare; only the existing human
|
|
2
2
|
* confirmation door executes. Protected actions use a one-use review receipt. */
|
|
3
|
+
import { CHAT_ACTION_LOCAL_ACTS, currentLocalOwner } from "./local-owner.js";
|
|
3
4
|
import { fileTaskProposal } from "./proposal.js";
|
|
4
5
|
import { heuristicSizing } from "./task-sizing.js";
|
|
5
6
|
import { cancelSweepRest, sweepOpen, sweepTable, VERDICT_RULE } from "./lead-sweep.js";
|
|
@@ -13,7 +14,7 @@ import { leadTurnKeepsProposals, verifiedAuthor } from "./store.js";
|
|
|
13
14
|
import { isVerifiedApprover, reproveApprover, } from "./principal.js";
|
|
14
15
|
import { changeSkills, importSkill, skillsView, skillsVersion, testSkill, validateSkill, } from "./project-skills.js";
|
|
15
16
|
import { changeKnowledge, knowledgeVersion, knowledgeView, } from "./project-knowledge.js";
|
|
16
|
-
import { passwordlessSessionFor, approve, authenticateApprover, describeScope, scopeAuthorityOf, freshnessFor, spendingOnlyOnSuccess, ssoNowAvailable, withFreshness, } from "./scope.js";
|
|
17
|
+
import { passwordlessSessionFor, approve, authenticateApprover, describeScope, scopeAuthorityOf, freshnessFor, localApprovalEligible, spendingOnlyOnSuccess, ssoNowAvailable, withFreshness, } from "./scope.js";
|
|
17
18
|
import { readVerifiedArtifact, scanForSecrets } from "./evidence.js";
|
|
18
19
|
import { readAuthModeStrict } from "./keys.js";
|
|
19
20
|
import { acceptAndCompleteAsOperator, assignmentOf } from "./assignment.js";
|
|
@@ -1114,6 +1115,9 @@ export function executeSharedAction(store, who, id, payload, now, options) {
|
|
|
1114
1115
|
const refuse = (reason, said) => ({ ok: false, reason, said });
|
|
1115
1116
|
if (store.isDemo())
|
|
1116
1117
|
return refuse("refused", "The demo authorizes nothing.");
|
|
1118
|
+
// Easy-start C1, a belt under the confirm door's own gate: a local owner runs only an operation that is one of its acts.
|
|
1119
|
+
if ((options.authority === "local-owner" || currentLocalOwner() !== null) && (!isChatAction(payload.operation) || CHAT_ACTION_LOCAL_ACTS[payload.operation] === null))
|
|
1120
|
+
return refuse("refused", "Set a password to do this.");
|
|
1117
1121
|
let live;
|
|
1118
1122
|
try {
|
|
1119
1123
|
const saved = savedActionContext(store, who, id, "confirming");
|
|
@@ -1152,6 +1156,8 @@ export function executeSharedAction(store, who, id, payload, now, options) {
|
|
|
1152
1156
|
if (config.password &&
|
|
1153
1157
|
(emptyApproval
|
|
1154
1158
|
? !passwordlessSessionFor(store, who.name) && !ssoNowAvailable(who.name) && withFreshness("approve-standard", () => freshnessFor(who.name)) === null
|
|
1159
|
+
// Easy-start C5: a local owner's eligible approval takes no password (approve() proves it again in the seal).
|
|
1160
|
+
&& !(options.authority === "local-owner" && currentLocalOwner()?.act === "approve-plan" && typeof req["task"] === "string" && localApprovalEligible(store, who.name, req["task"]).ok)
|
|
1155
1161
|
: proved?.ok !== true))
|
|
1156
1162
|
return refuse("needs-confirm", "Confirm with your password in the secure screen.");
|
|
1157
1163
|
}
|
package/dist/chat-delivery.js
CHANGED
|
@@ -652,6 +652,8 @@ export function applyChatAction(options, event, binding, repos) {
|
|
|
652
652
|
}
|
|
653
653
|
else {
|
|
654
654
|
const outcome = confirmLeadProposal(store, verified.who, proposal.id, now, {
|
|
655
|
+
// A paired chat seat speaks for its account's full authority (pairing is a full session's act).
|
|
656
|
+
authority: "full",
|
|
655
657
|
via: options.state.provider,
|
|
656
658
|
evidenceRoot: options.evidenceRoot,
|
|
657
659
|
confirm: phase === "yes",
|
|
@@ -39,6 +39,7 @@ export declare const CHAT_TASK_ACTIONS: {
|
|
|
39
39
|
readonly detail: "Files a small task to write tests for this change. Nothing runs until it is approved.";
|
|
40
40
|
};
|
|
41
41
|
};
|
|
42
|
+
export type ChatTaskAction = keyof typeof CHAT_TASK_ACTIONS;
|
|
42
43
|
export declare function isChatTaskAction(value: unknown): value is keyof typeof CHAT_TASK_ACTIONS;
|
|
43
44
|
/** Bind cards to a specific execution and its current state, not the family's moving pointer. */
|
|
44
45
|
export declare function chatTaskStamp(store: Store, who: VerifiedApprover, task: string): string | null;
|
package/dist/check-access.d.ts
CHANGED
|
@@ -48,4 +48,7 @@ export declare function checkAccessRunsAs(level: CheckAccess, here: {
|
|
|
48
48
|
platform: NodeJS.Platform;
|
|
49
49
|
fenced: boolean;
|
|
50
50
|
unconfinedAllowed: boolean;
|
|
51
|
+
localMode?: boolean;
|
|
51
52
|
}): CheckAccess;
|
|
53
|
+
/** Easy-start C5: while local mode is on, a project's checks reach at most its sign-ins, never unsandboxed. */
|
|
54
|
+
export declare const localModeCap: (level: CheckAccess) => CheckAccess;
|
package/dist/check-access.js
CHANGED
|
@@ -73,5 +73,8 @@ export const checkAccessHere = (level, platform = process.platform) => level ===
|
|
|
73
73
|
* on this machine and unconfined running allowed by an operator, without a sandbox whatever is saved (never said to be
|
|
74
74
|
* sandboxed); otherwise the saved level as this platform can give it. */
|
|
75
75
|
export function checkAccessRunsAs(level, here) {
|
|
76
|
-
|
|
76
|
+
const runs = !here.fenced && here.unconfinedAllowed ? "unsandboxed" : checkAccessHere(level, here.platform);
|
|
77
|
+
return here.localMode === true ? localModeCap(runs) : runs;
|
|
77
78
|
}
|
|
79
|
+
/** Easy-start C5: while local mode is on, a project's checks reach at most its sign-ins, never unsandboxed. */
|
|
80
|
+
export const localModeCap = (level) => level === "unsandboxed" ? "sign-ins" : level;
|
package/dist/cli-help.d.ts
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* The top-level usage text, on its own so `toolroll --help` can print it
|
|
3
3
|
* without loading every command family (see cli-fast.ts).
|
|
4
4
|
*/
|
|
5
|
-
export declare const HELP = "toolroll \u2014 a control plane for unattended coding agents\n\nUsage\n toolroll
|
|
5
|
+
export declare const HELP = "toolroll \u2014 a control plane for unattended coding agents\n\nUsage\n toolroll at your terminal: start Toolroll here (or attach) and open it in the terminal\n (--web: the browser; --project-root <dir>: where projects live)\n in a repository without a terminal, under an agent or with --json: only says what it would do\n toolroll open [--web] open this computer's Toolroll again, signed in\n toolroll welcome the three steps to a first result (--json: the model)\n toolroll password set add a password (typed twice, at your terminal)\n toolroll scan [path...] report what is in flight (also bare `toolroll` outside a repository)\n toolroll [path...] the same report for the paths given\n toolroll pulls report what is waiting on a person\n toolroll graph report which work graph is already here\n toolroll repos list connected repositories, and how to adjust\n toolroll repos add <path> add a project; it is in the console at once (no path: the repo you are in)\n toolroll repos remove <path>\n take it out of the console; its tasks and results stay\n toolroll repos add-from-github <owner/name> --root <dir>\n preview, then clone and connect (--yes)\n toolroll link put `toolroll` on your PATH\n toolroll unlink take it off again\n toolroll update update to the latest release: verified, drained, undoable\n (preview first; --yes, --now, --at HH:MM, --rollback)\n toolroll release <branch> release Toolroll itself: gate, your approval, deploy, merge, tag, publish\n (resumable: rerun to continue; --help)\n toolroll contract the machine contract: envelope version + capabilities\n (--commands dumps the declared command guide)\n toolroll skills install --claude-code [--dir <path>]\n install this binary's guides for Claude Code (preview first)\n toolroll skills install teach a repo's agents this queue exists (preview first)\n toolroll skills list the guides this exact binary serves\n toolroll skills get <name> print one guide (version-matched, never stale)\n toolroll demo a seeded throwaway sandbox \u2014 see it working in 90 seconds\n toolroll up app + builder for every saved project \u2014 the normal start\n toolroll onboard make your agent Toolroll's lead: add this repo, install its skill (--yes), offer starter flows (--starter)\n toolroll status running, queued, ready results, release check and plan windows\n toolroll health recent server latency, write waits and live streams\n toolroll integrations which integrations work, and what to do about the ones that don't (--json)\n toolroll lead token mint your lead agent's credential: its own work pings nobody\n toolroll lead say \"<text>\" (the lead) one short message in its person's chat\n\nOperating the queue \u2014 `toolroll task` prints the whole surface,\nand any queue command + --help prints it too\n toolroll approver add <name>\n mint the credential that lets a person say yes\n toolroll ready what could be dispatched right now\n toolroll check-progress <run> current or final approved check progress\n toolroll task wait <id> [--timeout <seconds>]\n wait for this attempt to finish or need a person\n toolroll task add <title> queue work\n toolroll task scope <id> --goal <text>\n state what success is; approval binds to it\n toolroll task approve <id>\n the yes \u2014 nothing builds without one\n toolroll claim <id> --runner <name>\n toolroll heartbeat <lease> / worker release <lease> / reap\n toolroll tick --runner <name> --token <t> --repo <path>\n one unattended pass over the ready set\n toolroll flows list | show <id> | create | edit | trigger | script | card | archive\n set up and inspect flows (writes preview until --yes)\n toolroll chat --as <you> --token <t>\n talk to your lead: one conversation across every\n project; it proposes, you confirm (--say \"\u2026\" for one turn)\n toolroll serve --repo <path> advanced: run only the console\n (--editor vscode with --runner: file links\n open in VS Code on the device you browse from)\n toolroll watch / daemon install advanced: run only the builder\n toolroll doctor check that Toolroll's service holds no key a build could read\n toolroll reconcile recover what the last stretch left behind\n\nWith nothing connected it reports everything it can find below the working\ndirectory. Once you connect repositories it reports those instead.\n\nOptions\n --all report everything discoverable, ignoring connected repos\n --depth <n> directory levels to descend (default: 6)\n --hidden include dot-directories, which are skipped by default\n --dirty also read each working tree for uncommitted files\n --local branches only: no network, no pull requests, no issues\n --json emit a machine-readable envelope instead of a report\n -o <file> also write that envelope to a file (requires --json)\n -h, --help show this\n\nNothing is written, installed, or configured. Every repository is read\nthrough git with --no-optional-locks, so a scan never contends for the\nindex lock with an editor you have open.\n\n--dirty is off by default because reading a working tree is unbounded\nwork: on a large or cloud-synced repo `git status` can take minutes,\nwhere listing branches takes milliseconds.\n\nlink shows what it would do and changes nothing; add --yes to apply it,\nor --to <dir> to choose where. It never uses sudo, never leaves your\nhome directory, and never touches a file it did not create.";
|
package/dist/cli-help.js
CHANGED
|
@@ -6,8 +6,12 @@ import { DEFAULT_MAX_DEPTH } from "./scan.js";
|
|
|
6
6
|
export const HELP = `toolroll — a control plane for unattended coding agents
|
|
7
7
|
|
|
8
8
|
Usage
|
|
9
|
-
toolroll
|
|
10
|
-
(
|
|
9
|
+
toolroll at your terminal: start Toolroll here (or attach) and open it in the terminal
|
|
10
|
+
(--web: the browser; --project-root <dir>: where projects live)
|
|
11
|
+
in a repository without a terminal, under an agent or with --json: only says what it would do
|
|
12
|
+
toolroll open [--web] open this computer's Toolroll again, signed in
|
|
13
|
+
toolroll welcome the three steps to a first result (--json: the model)
|
|
14
|
+
toolroll password set add a password (typed twice, at your terminal)
|
|
11
15
|
toolroll scan [path...] report what is in flight (also bare \`toolroll\` outside a repository)
|
|
12
16
|
toolroll [path...] the same report for the paths given
|
|
13
17
|
toolroll pulls report what is waiting on a person
|
package/dist/cli.d.ts
CHANGED
|
@@ -65,6 +65,12 @@ export type MainOptions = {
|
|
|
65
65
|
release?: ReleaseCliDeps;
|
|
66
66
|
/** Tests: bare `toolroll` in a repository (quick-start.ts) — its repository, agent check, proofs and actions. */
|
|
67
67
|
quickStart?: Partial<QuickStartSeams>;
|
|
68
|
+
/** Tests: bare `toolroll` at a person's terminal (easy-start.ts) — the terminal and agent checks, and its seams. */
|
|
69
|
+
easyStart?: {
|
|
70
|
+
terminal?: boolean;
|
|
71
|
+
underAgent?: boolean;
|
|
72
|
+
seams?: Partial<import("./easy-start.js").EasyStartSeams>;
|
|
73
|
+
};
|
|
68
74
|
};
|
|
69
75
|
/**
|
|
70
76
|
* Strip `-o <file>` / `--output <file>` before dispatch. The flag belongs to
|
package/dist/cli.js
CHANGED
|
@@ -25,6 +25,8 @@ import { GUIDES, guideNamed } from "./guides.js";
|
|
|
25
25
|
import { parseGithubRepo, previewGithubRepo, cloneGithubRepo, isLargeRepo } from "./onboard.js";
|
|
26
26
|
import { run as execRun } from "./exec.js";
|
|
27
27
|
import { isBareInvocation, runQuickStart } from "./quick-start.js";
|
|
28
|
+
import { easyStartApplies, easyStartArgs } from "./easy-start-args.js";
|
|
29
|
+
import { underAgent } from "./prompt.js";
|
|
28
30
|
import { COMMAND_GUIDE, SURFACE_NOTES, SURFACE_SCHEMA_VERSION } from "./surface.js";
|
|
29
31
|
import { hasCentralProfile, maybeRunConnectCommand } from "./connect-cli.js";
|
|
30
32
|
import { removedCommand, removedMessage } from "./deprecations.js";
|
|
@@ -158,6 +160,11 @@ export const OPERATE_COMMANDS = new Set([
|
|
|
158
160
|
"serve",
|
|
159
161
|
"watch",
|
|
160
162
|
"daemon",
|
|
163
|
+
"background",
|
|
164
|
+
"stop",
|
|
165
|
+
"open",
|
|
166
|
+
"welcome",
|
|
167
|
+
"password",
|
|
161
168
|
"bridge",
|
|
162
169
|
"publish",
|
|
163
170
|
"reconcile",
|
|
@@ -356,6 +363,22 @@ async function dispatch(argv, write, mainOptions) {
|
|
|
356
363
|
if (first !== undefined && OPERATE_COMMANDS.has(first)) {
|
|
357
364
|
return runOperate(first, rest, write, mainOptions.operate ?? {});
|
|
358
365
|
}
|
|
366
|
+
// Easy start (A): bare `toolroll` (or with --web / --project-root) at a person's terminal — no --json, not under a
|
|
367
|
+
// coding agent — starts this computer's Toolroll and opens the terminal UI. Anything else keeps its behaviour below.
|
|
368
|
+
if (first !== "scan" && first !== "--json" && easyStartArgs(argv) !== null) {
|
|
369
|
+
const terminal = mainOptions.easyStart?.terminal ?? (process.stdin.isTTY === true && process.stdout.isTTY === true);
|
|
370
|
+
const agent = terminal && (mainOptions.easyStart?.underAgent ?? underAgent(process.env));
|
|
371
|
+
if (easyStartApplies(argv, { stdin: terminal, stdout: terminal, agent })) {
|
|
372
|
+
const databaseFile = mainOptions.operate?.databaseFile ?? databasePath(process.env, homedir());
|
|
373
|
+
const easy = await import("./easy-start.js");
|
|
374
|
+
const seams = easy.defaultEasyStartSeams({
|
|
375
|
+
databaseFile, write, build: (await import("./monitoring.js")).packageVersion(),
|
|
376
|
+
...(mainOptions.operate === undefined ? {} : { operate: mainOptions.operate }),
|
|
377
|
+
admit: repo => runReposCommand(["add", repo], write, mainOptions.onboard),
|
|
378
|
+
});
|
|
379
|
+
return easy.runEasyStart("start", easyStartArgs(argv), { ...seams, ...mainOptions.easyStart?.seams });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
359
382
|
// Cockpit B1: the scan by name, and bare `toolroll` in a repository starting (or attaching to) its console.
|
|
360
383
|
if (first === "scan")
|
|
361
384
|
argv = rest;
|
package/dist/coding-ui.js
CHANGED
|
@@ -142,6 +142,8 @@ export function codingWorkspaceScript() {
|
|
|
142
142
|
const draftKey = () => 'standing-orders:coding-draft:' + root.dataset.codingOwner + ':' + (session || 'new:' + (composer.querySelector('[name="repo"]')?.value || root.dataset.codingProject));
|
|
143
143
|
const storage = (operation, key, value) => { try { return sessionStorage[operation](key, value); } catch { return null; } };
|
|
144
144
|
const requestSignal = () => typeof AbortSignal.timeout === 'function' ? AbortSignal.timeout(30000) : undefined;
|
|
145
|
+
// Easy-start C2: the browser mutation key, when the page's key fill offers one.
|
|
146
|
+
const keyed = headers => typeof window.soKeyHeaders === 'function' ? window.soKeyHeaders(headers) : headers;
|
|
145
147
|
const saveDraft = () => {
|
|
146
148
|
if (!prompt || !root.dataset.codingOwner) return;
|
|
147
149
|
storage('setItem', draftKey(), JSON.stringify({text:prompt.value, requestId:requestField.value, pending:pendingSend, at:Date.now()}));
|
|
@@ -276,7 +278,7 @@ export function codingWorkspaceScript() {
|
|
|
276
278
|
const area = root.querySelector('#coding-change-content');
|
|
277
279
|
const button = root.querySelector('#coding-refresh-changes'); button.disabled = true;
|
|
278
280
|
try {
|
|
279
|
-
const response = await fetch(base + '/changes', {method:'POST',headers:{accept:'application/json'},body:new URLSearchParams({csrf}),credentials:'same-origin',cache:'no-store',signal:requestSignal()});
|
|
281
|
+
const response = await fetch(base + '/changes', {method:'POST',headers:keyed({accept:'application/json'}),body:new URLSearchParams({csrf}),credentials:'same-origin',cache:'no-store',signal:requestSignal()});
|
|
280
282
|
if (response.status === 401 || response.redirected) { signedOut(); return; }
|
|
281
283
|
if (!response.ok) throw new Error('Could not load changes. Try Refresh changes.');
|
|
282
284
|
const changes = await response.json();
|
|
@@ -338,7 +340,7 @@ export function codingWorkspaceScript() {
|
|
|
338
340
|
form.dataset.busy = 'true';
|
|
339
341
|
const buttons = Array.from(form.querySelectorAll('button')); buttons.forEach(button => button.disabled = true);
|
|
340
342
|
try {
|
|
341
|
-
const response = await fetch(form.getAttribute('action'), {method:'POST',headers:{accept:'application/json','content-type':'application/x-www-form-urlencoded'},credentials:'same-origin',body:data.toString(),signal:requestSignal()});
|
|
343
|
+
const response = await fetch(form.getAttribute('action'), {method:'POST',headers:keyed({accept:'application/json','content-type':'application/x-www-form-urlencoded'}),credentials:'same-origin',body:data.toString(),signal:requestSignal()});
|
|
342
344
|
if (response.status === 401 || response.redirected) { signedOut(); return; }
|
|
343
345
|
const result = await response.json();
|
|
344
346
|
if (!response.ok || !result.ok) {
|
package/dist/connect-cli.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { constants, closeSync, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
8
|
import { dirname, join } from 'node:path';
|
|
9
|
+
import { savedLoginFile } from './login-secrets.js';
|
|
9
10
|
import { randomBytes } from 'node:crypto';
|
|
10
11
|
import { envelopeJson } from './envelope.js';
|
|
11
12
|
import { databasePath } from './store.js';
|
|
@@ -299,7 +300,7 @@ export async function maybeRunConnectCommand(argv, write, options = {}) {
|
|
|
299
300
|
throw new UsageError('--login-file requires --local-login.');
|
|
300
301
|
let account = value(flags, 'as');
|
|
301
302
|
if (flags['local-login']) {
|
|
302
|
-
const login = privateFile(value(flags, 'login-file') ||
|
|
303
|
+
const login = privateFile(value(flags, 'login-file') || savedLoginFile(dirname(databasePath(options.env ?? process.env, options.home ?? homedir()))), 8192).trim().split(/\s+/);
|
|
303
304
|
if (login.length !== 2 || account && account !== login[0])
|
|
304
305
|
throw new UsageError('The local sign-in does not match the requested account.');
|
|
305
306
|
account = login[0];
|
|
@@ -35,6 +35,8 @@ export declare function readInstanceState(store: Store): {
|
|
|
35
35
|
record: InstanceRecord;
|
|
36
36
|
};
|
|
37
37
|
export declare function readInstance(store: Store): InstanceRecord | null;
|
|
38
|
+
/** v147: the key epoch the running console recorded with its claim (easy-start C2), or null: none recorded. */
|
|
39
|
+
export declare function instanceKeyEpoch(store: Store): string | null;
|
|
38
40
|
/** A stopping console forgets itself — only its own record, never a newer start's. */
|
|
39
41
|
export declare function clearInstance(store: Store, secret: string): void;
|
|
40
42
|
export declare const socketProof: (secret: string, challenge: string) => string;
|
|
@@ -70,8 +72,15 @@ export declare function askSocket(socketPath: string, request: object, timeoutMs
|
|
|
70
72
|
* The console's discovery socket: one line in, one line out. `hello` (with a challenge) answers the public facts and
|
|
71
73
|
* the socket proof; every other verb is refused. It reads nothing else and changes nothing. Bound under a fresh name
|
|
72
74
|
* no other process has, so it never clears or replaces anything; its own listener removes it on close.
|
|
75
|
+
*
|
|
76
|
+
* Easy start D: the one other verb is `link`, and only when the console passes `link`. The console answers with a fresh
|
|
77
|
+
* challenge of its own; the asker must answer it with HMAC(secret, "link:" + challenge) — the secret is in the database
|
|
78
|
+
* (behind the agent fence), so only a process that read it can — and then gets one local owner's one-time sign-in path.
|
|
79
|
+
* Each challenge is spent by the first answer on its own connection, right or wrong; the "link:" domain keeps a hello or
|
|
80
|
+
* HTTP proof from ever counting here. The request names no account: what the link signs in as is the console's own
|
|
81
|
+
* decision (`link` returns null unless local mode is on).
|
|
73
82
|
*/
|
|
74
|
-
export declare function openInstanceSocket(databaseFile: string, facts: HelloFacts, secret: () => string | null): Promise<{
|
|
83
|
+
export declare function openInstanceSocket(databaseFile: string, facts: HelloFacts, secret: () => string | null, link?: () => string | null): Promise<{
|
|
75
84
|
ok: true;
|
|
76
85
|
name: string;
|
|
77
86
|
close: () => void;
|
|
@@ -79,6 +88,29 @@ export declare function openInstanceSocket(databaseFile: string, facts: HelloFac
|
|
|
79
88
|
ok: false;
|
|
80
89
|
problem: string;
|
|
81
90
|
}>;
|
|
91
|
+
/** The `link` verb's answer to the console's challenge: bound to this verb, so no other proof ever counts for it. */
|
|
92
|
+
export declare const linkProof: (secret: string, challenge: string) => string;
|
|
93
|
+
/**
|
|
94
|
+
* Ask the console on `socketPath` for a local owner's one-time sign-in path with the record's secret: `link`, the
|
|
95
|
+
* console's challenge, then the proof. Null when it gives none (local mode off, a wrong or late answer, anything
|
|
96
|
+
* malformed). The caller proves the console live first (proveLiveConsole) and opens the path on its proved address.
|
|
97
|
+
*/
|
|
98
|
+
export declare function askLink(socketPath: string, secret: string, timeoutMs?: number, connect?: (path: string) => Socket): Promise<string | null>;
|
|
99
|
+
/**
|
|
100
|
+
* A local owner's one-time sign-in address from this installation's live console (easy start D, `toolroll open`): the
|
|
101
|
+
* console is proved live on both channels first, then asked over its socket with the record's secret. The address is
|
|
102
|
+
* always the proved console's own; null when there is no proved console or it gives no link.
|
|
103
|
+
*/
|
|
104
|
+
export declare function requestLocalLink(databaseFile: string, store: Store, build: string, seams?: AttachSeams & {
|
|
105
|
+
askLink?: typeof askLink;
|
|
106
|
+
}): Promise<{
|
|
107
|
+
ok: true;
|
|
108
|
+
url: string;
|
|
109
|
+
console: string;
|
|
110
|
+
} | {
|
|
111
|
+
ok: false;
|
|
112
|
+
message: string;
|
|
113
|
+
}>;
|
|
82
114
|
/**
|
|
83
115
|
* A record's console: "live" (its socket answers a fresh challenge with the record's secret and pid), "gone" (nobody
|
|
84
116
|
* is listening on its socket, or the socket isn't there), or "unknown" (anything else: slow, malformed, a wrong proof).
|
|
@@ -95,6 +127,7 @@ export declare function claimInstance(store: Store, databaseFile: string, facts:
|
|
|
95
127
|
port: number;
|
|
96
128
|
build: string;
|
|
97
129
|
socket: string;
|
|
130
|
+
keyEpoch?: string;
|
|
98
131
|
}, now: Date, liveness?: (record: InstanceRecord) => Promise<"live" | "gone" | "unknown">): Promise<string | null>;
|
|
99
132
|
export type AttachProof = {
|
|
100
133
|
ok: true;
|
package/dist/console-instance.js
CHANGED
|
@@ -69,6 +69,11 @@ export function readInstance(store) {
|
|
|
69
69
|
const read = readInstanceState(store);
|
|
70
70
|
return read.state === "ok" ? read.record : null;
|
|
71
71
|
}
|
|
72
|
+
/** v147: the key epoch the running console recorded with its claim (easy-start C2), or null: none recorded. */
|
|
73
|
+
export function instanceKeyEpoch(store) {
|
|
74
|
+
const row = store.handle.prepare("SELECT key_epoch FROM console_instance WHERE id = 1").get();
|
|
75
|
+
return row === undefined || row["key_epoch"] == null ? null : String(row["key_epoch"]);
|
|
76
|
+
}
|
|
72
77
|
/** A stopping console forgets itself — only its own record, never a newer start's. */
|
|
73
78
|
export function clearInstance(store, secret) {
|
|
74
79
|
store.handle.prepare("DELETE FROM console_instance WHERE id = 1 AND secret = ?").run(secret);
|
|
@@ -189,8 +194,15 @@ export function askSocket(socketPath, request, timeoutMs = 2_000, connect = path
|
|
|
189
194
|
* The console's discovery socket: one line in, one line out. `hello` (with a challenge) answers the public facts and
|
|
190
195
|
* the socket proof; every other verb is refused. It reads nothing else and changes nothing. Bound under a fresh name
|
|
191
196
|
* no other process has, so it never clears or replaces anything; its own listener removes it on close.
|
|
197
|
+
*
|
|
198
|
+
* Easy start D: the one other verb is `link`, and only when the console passes `link`. The console answers with a fresh
|
|
199
|
+
* challenge of its own; the asker must answer it with HMAC(secret, "link:" + challenge) — the secret is in the database
|
|
200
|
+
* (behind the agent fence), so only a process that read it can — and then gets one local owner's one-time sign-in path.
|
|
201
|
+
* Each challenge is spent by the first answer on its own connection, right or wrong; the "link:" domain keeps a hello or
|
|
202
|
+
* HTTP proof from ever counting here. The request names no account: what the link signs in as is the console's own
|
|
203
|
+
* decision (`link` returns null unless local mode is on).
|
|
192
204
|
*/
|
|
193
|
-
export async function openInstanceSocket(databaseFile, facts, secret) {
|
|
205
|
+
export async function openInstanceSocket(databaseFile, facts, secret, link) {
|
|
194
206
|
const dir = instanceRunDir(databaseFile);
|
|
195
207
|
const problem = prepareRunDir(dir);
|
|
196
208
|
if (problem !== null)
|
|
@@ -201,30 +213,60 @@ export async function openInstanceSocket(databaseFile, facts, secret) {
|
|
|
201
213
|
return { ok: false, problem: pathProblem };
|
|
202
214
|
const server = createServer(socket => {
|
|
203
215
|
let buffer = "";
|
|
216
|
+
// The link challenge this connection was given, until its one answer arrives.
|
|
217
|
+
let challenged = null;
|
|
218
|
+
let done = false;
|
|
204
219
|
socket.setEncoding("utf8");
|
|
205
220
|
socket.setTimeout(5_000, () => socket.destroy());
|
|
206
|
-
const reply = (body) => { socket.end(`${JSON.stringify(body)}\n`); };
|
|
207
|
-
|
|
208
|
-
socket.on("data", chunk => {
|
|
209
|
-
buffer += chunk;
|
|
210
|
-
if (buffer.length > LINE_CAP)
|
|
211
|
-
return void socket.destroy();
|
|
212
|
-
const end = buffer.indexOf("\n");
|
|
213
|
-
if (end < 0)
|
|
214
|
-
return;
|
|
221
|
+
const reply = (body) => { done = true; socket.end(`${JSON.stringify(body)}\n`); };
|
|
222
|
+
const answerLine = (line) => {
|
|
215
223
|
let asked;
|
|
216
224
|
try {
|
|
217
|
-
asked = JSON.parse(
|
|
225
|
+
asked = JSON.parse(line);
|
|
218
226
|
}
|
|
219
227
|
catch {
|
|
220
228
|
return reply({ error: "refused" });
|
|
221
229
|
}
|
|
222
|
-
const
|
|
223
|
-
const challenge = asked !== null && typeof asked === "object" ? asked.challenge : undefined;
|
|
230
|
+
const field = (key) => asked !== null && typeof asked === "object" && !Array.isArray(asked) ? asked[key] : undefined;
|
|
224
231
|
const key = secret();
|
|
232
|
+
if (challenged !== null) {
|
|
233
|
+
// The one answer to this connection's challenge: spent now, whatever it says.
|
|
234
|
+
const expected = key === null ? null : linkProof(key, challenged);
|
|
235
|
+
challenged = null;
|
|
236
|
+
if (expected === null || field("verb") !== "link-proof" || !same(field("proof"), expected))
|
|
237
|
+
return reply({ error: "refused" });
|
|
238
|
+
let path = null;
|
|
239
|
+
try {
|
|
240
|
+
path = link?.() ?? null;
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
path = null;
|
|
244
|
+
}
|
|
245
|
+
return reply(path === null ? { error: "no-link" } : { link: path });
|
|
246
|
+
}
|
|
247
|
+
const verb = field("verb");
|
|
248
|
+
if (verb === "link" && link !== undefined && key !== null) {
|
|
249
|
+
challenged = randomBytes(32).toString("hex");
|
|
250
|
+
socket.write(`${JSON.stringify({ challenge: challenged })}\n`);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const challenge = field("challenge");
|
|
225
254
|
if (verb !== "hello" || typeof challenge !== "string" || !CHALLENGE.test(challenge) || key === null)
|
|
226
255
|
return reply({ error: "refused" });
|
|
227
256
|
reply({ ...facts, proof: socketProof(key, challenge) });
|
|
257
|
+
};
|
|
258
|
+
socket.on("error", () => socket.destroy());
|
|
259
|
+
socket.on("data", chunk => {
|
|
260
|
+
if (done)
|
|
261
|
+
return;
|
|
262
|
+
buffer += chunk;
|
|
263
|
+
if (buffer.length > LINE_CAP)
|
|
264
|
+
return void socket.destroy();
|
|
265
|
+
for (let end = buffer.indexOf("\n"); end >= 0 && !done; end = buffer.indexOf("\n")) {
|
|
266
|
+
const line = buffer.slice(0, end);
|
|
267
|
+
buffer = buffer.slice(end + 1);
|
|
268
|
+
answerLine(line);
|
|
269
|
+
}
|
|
228
270
|
});
|
|
229
271
|
});
|
|
230
272
|
// A Unix bind never replaces an existing path (EADDRINUSE).
|
|
@@ -233,6 +275,72 @@ export async function openInstanceSocket(databaseFile, facts, secret) {
|
|
|
233
275
|
return { ok: false, problem: `couldn't listen on ${socketPath}` };
|
|
234
276
|
return { ok: true, name, close: () => { server.close(); } };
|
|
235
277
|
}
|
|
278
|
+
/** The `link` verb's answer to the console's challenge: bound to this verb, so no other proof ever counts for it. */
|
|
279
|
+
export const linkProof = (secret, challenge) => createHmac("sha256", secret).update(`link:${challenge}`).digest("hex");
|
|
280
|
+
const LINK_PATH = /^\/login\/once\/[A-Za-z0-9_-]{16,128}$/;
|
|
281
|
+
/**
|
|
282
|
+
* Ask the console on `socketPath` for a local owner's one-time sign-in path with the record's secret: `link`, the
|
|
283
|
+
* console's challenge, then the proof. Null when it gives none (local mode off, a wrong or late answer, anything
|
|
284
|
+
* malformed). The caller proves the console live first (proveLiveConsole) and opens the path on its proved address.
|
|
285
|
+
*/
|
|
286
|
+
export function askLink(socketPath, secret, timeoutMs = 5_000, connect = path => createConnection(path)) {
|
|
287
|
+
return new Promise(resolve => {
|
|
288
|
+
let settled = false, buffer = "", sentProof = false;
|
|
289
|
+
const done = (value) => { if (settled)
|
|
290
|
+
return; settled = true; clearTimeout(timer); socket.destroy(); resolve(value); };
|
|
291
|
+
const socket = connect(socketPath);
|
|
292
|
+
const timer = setTimeout(() => done(null), timeoutMs);
|
|
293
|
+
socket.setEncoding("utf8");
|
|
294
|
+
socket.on("error", () => done(null));
|
|
295
|
+
socket.on("connect", () => socket.write(`${JSON.stringify({ verb: "link" })}\n`));
|
|
296
|
+
socket.on("data", chunk => {
|
|
297
|
+
buffer += chunk;
|
|
298
|
+
if (buffer.length > LINE_CAP)
|
|
299
|
+
return done(null);
|
|
300
|
+
for (let end = buffer.indexOf("\n"); end >= 0 && !settled; end = buffer.indexOf("\n")) {
|
|
301
|
+
const line = buffer.slice(0, end);
|
|
302
|
+
buffer = buffer.slice(end + 1);
|
|
303
|
+
let parsed;
|
|
304
|
+
try {
|
|
305
|
+
parsed = JSON.parse(line);
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
return done(null);
|
|
309
|
+
}
|
|
310
|
+
const body = parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
311
|
+
if (!sentProof) {
|
|
312
|
+
const challenge = body["challenge"];
|
|
313
|
+
if (typeof challenge !== "string" || !/^[0-9a-f]{64}$/.test(challenge))
|
|
314
|
+
return done(null);
|
|
315
|
+
sentProof = true;
|
|
316
|
+
socket.write(`${JSON.stringify({ verb: "link-proof", proof: linkProof(secret, challenge) })}\n`);
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
const path = body["link"];
|
|
320
|
+
return done(typeof path === "string" && LINK_PATH.test(path) ? path : null);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
socket.on("end", () => done(null));
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* A local owner's one-time sign-in address from this installation's live console (easy start D, `toolroll open`): the
|
|
329
|
+
* console is proved live on both channels first, then asked over its socket with the record's secret. The address is
|
|
330
|
+
* always the proved console's own; null when there is no proved console or it gives no link.
|
|
331
|
+
*/
|
|
332
|
+
export async function requestLocalLink(databaseFile, store, build, seams = {}) {
|
|
333
|
+
const proof = await proveLiveConsole(databaseFile, store, build, seams);
|
|
334
|
+
if (!proof.ok)
|
|
335
|
+
return { ok: false, message: proof.message };
|
|
336
|
+
const record = readInstance(store);
|
|
337
|
+
if (record === null || record.pid !== proof.pid || record.port !== proof.port)
|
|
338
|
+
return { ok: false, message: "the console changed while it was being checked" };
|
|
339
|
+
const path = await (seams.askLink ?? askLink)(join(instanceRunDir(databaseFile), record.socket), record.secret);
|
|
340
|
+
if (path === null)
|
|
341
|
+
return { ok: false, message: "the console gave no sign-in link (local mode is off, or it is still starting)" };
|
|
342
|
+
return { ok: true, url: new URL(path, proof.url).toString(), console: proof.url };
|
|
343
|
+
}
|
|
236
344
|
/**
|
|
237
345
|
* A record's console: "live" (its socket answers a fresh challenge with the record's secret and pid), "gone" (nobody
|
|
238
346
|
* is listening on its socket, or the socket isn't there), or "unknown" (anything else: slow, malformed, a wrong proof).
|
|
@@ -268,8 +376,9 @@ export async function claimInstance(store, databaseFile, facts, now, liveness =
|
|
|
268
376
|
// Exactly the record that was checked (or none): anything else is another start's newer claim.
|
|
269
377
|
if (rawSecret() !== checked)
|
|
270
378
|
return null;
|
|
271
|
-
|
|
272
|
-
|
|
379
|
+
// v147: the start's key epoch (easy-start C2) is recorded with its claim; a claim without one records none.
|
|
380
|
+
store.handle.prepare("INSERT INTO console_instance (id, secret, pid, port, build, socket, started_at, key_epoch) VALUES (1, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET secret = excluded.secret, pid = excluded.pid, port = excluded.port, build = excluded.build, socket = excluded.socket, started_at = excluded.started_at, key_epoch = excluded.key_epoch")
|
|
381
|
+
.run(secret, facts.pid, facts.port, facts.build, facts.socket, now.toISOString(), facts.keyEpoch ?? null);
|
|
273
382
|
return secret;
|
|
274
383
|
});
|
|
275
384
|
}
|