toolroll 0.9.89 → 0.9.91

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.
Files changed (138) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +16 -10
  3. package/THIRD_PARTY_NOTICES.md +37 -0
  4. package/dist/agent-board.d.ts +9 -0
  5. package/dist/agent-board.js +20 -0
  6. package/dist/agent-fence.js +20 -2
  7. package/dist/approval-authority.d.ts +7 -0
  8. package/dist/approval-authority.js +13 -2
  9. package/dist/browser/THIRD_PARTY_NOTICES.txt +692 -28
  10. package/dist/browser/terminal.js +42 -42
  11. package/dist/browser/workspace.css +1 -1
  12. package/dist/browser/workspace.js +32 -32
  13. package/dist/browser-workspace.d.ts +13 -0
  14. package/dist/builder.js +5 -2
  15. package/dist/chat-actions.d.ts +5 -0
  16. package/dist/chat-actions.js +7 -1
  17. package/dist/chat-delivery.js +2 -0
  18. package/dist/chat-task-actions.d.ts +1 -0
  19. package/dist/check-access.d.ts +3 -0
  20. package/dist/check-access.js +4 -1
  21. package/dist/cli-help.d.ts +1 -1
  22. package/dist/cli-help.js +6 -2
  23. package/dist/cli.d.ts +6 -0
  24. package/dist/cli.js +23 -0
  25. package/dist/coding-ui.js +4 -2
  26. package/dist/connect-cli.js +2 -1
  27. package/dist/console-instance.d.ts +34 -1
  28. package/dist/console-instance.js +124 -15
  29. package/dist/contracts/cli.d.ts +72 -0
  30. package/dist/contracts/cli.js +13 -0
  31. package/dist/contracts/console-api.d.ts +9 -2
  32. package/dist/contracts/console-api.js +9 -2
  33. package/dist/desktop-host.js +8 -3
  34. package/dist/desktop-update.js +2 -1
  35. package/dist/dispatch.d.ts +3 -0
  36. package/dist/dispatch.js +22 -2
  37. package/dist/easy-start-args.d.ts +13 -0
  38. package/dist/easy-start-args.js +24 -0
  39. package/dist/easy-start.d.ts +77 -0
  40. package/dist/easy-start.js +321 -0
  41. package/dist/first-run.d.ts +77 -0
  42. package/dist/first-run.js +45 -0
  43. package/dist/guides.js +1 -1
  44. package/dist/invoke.js +11 -6
  45. package/dist/laptop-background.d.ts +171 -0
  46. package/dist/laptop-background.js +550 -0
  47. package/dist/laptop-service.d.ts +111 -0
  48. package/dist/laptop-service.js +268 -0
  49. package/dist/lead-cli.js +2 -1
  50. package/dist/lead-doors.d.ts +6 -0
  51. package/dist/lead-doors.js +20 -2
  52. package/dist/live-records.d.ts +1 -1
  53. package/dist/live-records.js +3 -12
  54. package/dist/local-owner.d.ts +124 -0
  55. package/dist/local-owner.js +212 -0
  56. package/dist/login-secrets.d.ts +60 -0
  57. package/dist/login-secrets.js +195 -0
  58. package/dist/mcp-oauth.d.ts +4 -0
  59. package/dist/mcp-oauth.js +13 -7
  60. package/dist/model-choices.d.ts +43 -0
  61. package/dist/model-choices.js +80 -0
  62. package/dist/operate.d.ts +46 -1
  63. package/dist/operate.js +461 -40
  64. package/dist/person-proof.js +3 -0
  65. package/dist/project-knowledge.d.ts +7 -7
  66. package/dist/quick-start.d.ts +3 -1
  67. package/dist/quick-start.js +6 -1
  68. package/dist/release-adapters.d.ts +2 -0
  69. package/dist/release-adapters.js +30 -1
  70. package/dist/release-cli.js +2 -1
  71. package/dist/runner-process.d.ts +9 -0
  72. package/dist/runner-process.js +18 -0
  73. package/dist/sample-project.d.ts +31 -0
  74. package/dist/sample-project.js +141 -0
  75. package/dist/scope.d.ts +24 -2
  76. package/dist/scope.js +74 -3
  77. package/dist/serve.js +129 -22
  78. package/dist/server/chat.js +10 -3
  79. package/dist/server/chrome.d.ts +10 -2
  80. package/dist/server/chrome.js +7 -5
  81. package/dist/server/console.css +10 -0
  82. package/dist/server/guards.d.ts +3 -0
  83. package/dist/server/guards.js +22 -2
  84. package/dist/server/http.d.ts +22 -0
  85. package/dist/server/http.js +58 -0
  86. package/dist/server/live.js +4 -14
  87. package/dist/server/mutation-key.d.ts +70 -0
  88. package/dist/server/mutation-key.js +142 -0
  89. package/dist/server/pages.js +10 -7
  90. package/dist/server/people-tokens.js +240 -20
  91. package/dist/server/remote.d.ts +2 -0
  92. package/dist/server/remote.js +19 -4
  93. package/dist/server/render-new-task.js +5 -1
  94. package/dist/server/render-pages.d.ts +2 -1
  95. package/dist/server/render-pages.js +1 -1
  96. package/dist/server/render-people.d.ts +36 -0
  97. package/dist/server/render-people.js +65 -1
  98. package/dist/server/render-projects.js +4 -2
  99. package/dist/server/render-tasks.d.ts +12 -0
  100. package/dist/server/render-tasks.js +12 -4
  101. package/dist/server/request-context.d.ts +6 -0
  102. package/dist/server/route-table.d.ts +16 -0
  103. package/dist/server/route-table.js +121 -0
  104. package/dist/server/runtime.d.ts +6 -4
  105. package/dist/server/session.d.ts +31 -1
  106. package/dist/server/session.js +12 -2
  107. package/dist/server/settings.js +63 -8
  108. package/dist/server/task-views.js +10 -1
  109. package/dist/server/tasks.d.ts +10 -0
  110. package/dist/server/tasks.js +59 -9
  111. package/dist/server/terminals.js +15 -2
  112. package/dist/server/welcome.d.ts +22 -16
  113. package/dist/server/welcome.js +150 -52
  114. package/dist/settings-groups-ui.d.ts +7 -1
  115. package/dist/settings-groups-ui.js +23 -2
  116. package/dist/settings-proofs.js +1 -0
  117. package/dist/setup-guide.d.ts +2 -2
  118. package/dist/store.d.ts +124 -1
  119. package/dist/store.js +245 -12
  120. package/dist/surface.d.ts +10 -0
  121. package/dist/surface.js +40 -1
  122. package/dist/task-text.d.ts +6 -0
  123. package/dist/task-text.js +17 -0
  124. package/dist/telegram-lead.js +2 -0
  125. package/dist/terminal-text.d.ts +11 -0
  126. package/dist/terminal-text.js +26 -0
  127. package/dist/toolroll-update.d.ts +8 -1
  128. package/dist/toolroll-update.js +151 -4
  129. package/dist/tui-contract.d.ts +118 -0
  130. package/dist/tui-contract.js +9 -0
  131. package/dist/tui-source.d.ts +14 -0
  132. package/dist/tui-source.js +182 -0
  133. package/dist/tui.js +129 -0
  134. package/dist/up-processes.d.ts +13 -1
  135. package/dist/up-processes.js +13 -3
  136. package/dist/welcome-facts.d.ts +18 -0
  137. package/dist/welcome-facts.js +79 -0
  138. package/package.json +4 -2
@@ -38,6 +38,9 @@ export function createRemoteHandlers(runtime) {
38
38
  const account = who === null ? null : store.accountOf(who.name);
39
39
  if (!who || !account || account.revokedAt !== null)
40
40
  return null;
41
+ // Easy-start C1: team conversations are a remote feature; a laptop's local owner (no password) never reaches them.
42
+ if (who.via === 'cookie' && who.authority !== 'full')
43
+ return null;
41
44
  if (who.via === 'bearer')
42
45
  passwordProvedAt.set(request, clock().getTime());
43
46
  return { name: who.name, generation: account.generation, role: account.role };
@@ -71,7 +74,7 @@ export function createRemoteHandlers(runtime) {
71
74
  return true;
72
75
  }
73
76
  const who = identify(request, false);
74
- return who?.name === actor.name && who.via === 'cookie' && who.session.generation === actor.generation;
77
+ return who?.name === actor.name && who.via === 'cookie' && who.authority === 'full' && who.session.generation === actor.generation;
75
78
  },
76
79
  authorizeMutation: (request, actor) => {
77
80
  if (actor.principal !== undefined)
@@ -81,11 +84,15 @@ export function createRemoteHandlers(runtime) {
81
84
  return false;
82
85
  if (who.via === 'bearer')
83
86
  return request.headers.origin === undefined;
87
+ if (who.authority !== 'full')
88
+ return false;
84
89
  const origin = request.headers.origin, referer = request.headers.referer;
85
90
  const named = typeof origin === 'string' && origin !== 'null' ? origin : typeof referer === 'string' ? referer : null;
86
91
  if (named !== null && !allowedHost(named.replace(/^https?:\/\//, '').split('/')[0]))
87
92
  return false;
88
- return request.headers['x-csrf-token'] === who.session.csrf;
93
+ // Easy-start C2: and, on a protected loopback install, the session's key (its header), which a cookie holder on
94
+ // another port can't read.
95
+ return request.headers['x-csrf-token'] === who.session.csrf && runtime.browserKeys.check(who, request, null) === null;
89
96
  },
90
97
  execute: async (actor, input) => {
91
98
  const reply = await team.execute(actor, input);
@@ -112,7 +119,8 @@ export function createRemoteHandlers(runtime) {
112
119
  requesterKey: joinSourceOf,
113
120
  session: request => {
114
121
  const who = identify(request, false);
115
- if (who === null || who.via !== "cookie")
122
+ // Easy-start C1: connecting an outside app is a credential act; a local owner's session is never one that consents.
123
+ if (who === null || who.via !== "cookie" || who.authority !== "full")
116
124
  return null;
117
125
  return { name: who.name, role: who.role, csrf: who.session.csrf, generation: who.session.generation,
118
126
  sso: who.session.sso === undefined ? null : { label: ssoSettings()?.label ?? "your identity provider",
@@ -123,13 +131,20 @@ export function createRemoteHandlers(runtime) {
123
131
  const named = typeof origin === "string" && origin !== "null" ? origin : typeof referer === "string" ? referer : null;
124
132
  return named !== null && allowedHost(named.replace(/^https?:\/\//, "").split("/")[0]);
125
133
  },
134
+ // Easy-start C2: the consent's own post presents the session's key too (its form carries it, filled by the page's
135
+ // one nonce'd script while the key is required).
136
+ keyed: (request, form) => {
137
+ const who = identify(request, false);
138
+ return who !== null && who.via === "cookie" && runtime.browserKeys.check(who, request, form) === null;
139
+ },
140
+ keyFill: () => runtime.browserKeys.required(),
126
141
  // The step-up for a credential: the person's password, or (empty) a fresh identity-provider check. What they may
127
142
  // grant (their role, their projects) is checked beside it.
128
143
  // Proved on this request's live session at the moment of consent (signed in here still, same generation), through
129
144
  // the one account step-up: a password typed now, or an explicit re-check (spent). Never a remembered password.
130
145
  confirm: (request, session, typed) => {
131
146
  const who = identify(request, false);
132
- if (who === null || who.via !== "cookie" || who.name !== session.name || who.session.generation !== session.generation)
147
+ if (who === null || who.via !== "cookie" || who.authority !== "full" || who.name !== session.name || who.session.generation !== session.generation)
133
148
  return false;
134
149
  // Connecting an outside app is a credential act: never a session (Passwordless) or a remembered password.
135
150
  return freshFacts.run(runtime.freshFactsOf(request, who), () => { const proved = proveAccountNow(store, who.name, typed, { credential: true }); return proved.ok && credentialProof(proved.method); });
@@ -8,6 +8,7 @@ import { html, joinHtml, postForm } from "../html.js";
8
8
  import { projectName } from "../project.js";
9
9
  import { screen } from "./chrome.js";
10
10
  import { btn, composer, disclose, doc, docHeader, dock, field, ideas, input, note, phoneHeader, radios, select, sendButton, textarea, toggleChip } from "./kit.js";
11
+ import { TASK_TEXT_LIMITS } from "../task-text.js";
11
12
  const FORM_ID = "nt-form";
12
13
  const PLAN_ON = "The agent reads the code and asks you anything unclear, then writes the plan.";
13
14
  const PLAN_OFF = "The agent writes the plan straight from your words. Faster for small, obvious changes.";
@@ -59,7 +60,10 @@ suggestions = []) {
59
60
  value: text, rows: 4, maxRows: 10, before: bar, error: problem,
60
61
  send: sendButton({ label: "File task" }),
61
62
  note: html `<span class="k-on-note">${PLAN_ON}</span><span class="k-off-note">${PLAN_OFF}</span>`,
62
- textAttrs: { maxlength: 200, autofocus: true },
63
+ // The box holds what a title holds: a task's words are its title, and the planner reads nothing else of them when no
64
+ // goal is given, so longer words would be lost, not kept. Filing's whole-word cut (task-text.ts titleWithin) is a
65
+ // belt for other callers only.
66
+ textAttrs: { maxlength: TASK_TEXT_LIMITS.title, autofocus: true },
63
67
  });
64
68
  const options = html `<div class="k-form">\
65
69
  ${field({ id: "nt-quality", label: "Checks", control: select({ id: "nt-quality", name: "quality-mode", items: [["default", "Default checks"], ["strict", "Strict (release)"]], current: quality, wide: true }) })}\
@@ -341,7 +341,8 @@ export type OnboardCardState = {
341
341
  why: string;
342
342
  } | {
343
343
  enabled: true;
344
- roots: readonly string[];
344
+ roots: readonly string[]; /** Easy-start: the local owner's own session (no password to type; the prover's local-owner branch proves the clone). */
345
+ localOwner?: boolean;
345
346
  record: [string, {
346
347
  nameWithOwner: string;
347
348
  rootIndex: number;
@@ -881,7 +881,7 @@ export function queueScript() {
881
881
  `function problem(task,text){var card=region.querySelector('.queue-card[data-task="'+task.replace(/["\\\\]/g,"")+'"]');if(!card)return false;` +
882
882
  `var old=card.querySelector(".queue-problem");if(old)old.remove();` +
883
883
  `var row=document.createElement("p");row.className="problem queue-problem";row.setAttribute("role","alert");row.textContent=text;card.appendChild(row);return true;}` +
884
- `fetch("/queue/move",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:post.toString(),redirect:"manual"})` +
884
+ `fetch("/queue/move",{method:"POST",headers:(window.soKeyHeaders||function(h){return h})({"Content-Type":"application/x-www-form-urlencoded"}),body:post.toString(),redirect:"manual"})` +
885
885
  `.then(function(r){if(r.ok)return fetch("/queue?fragment=1",{redirect:"manual",cache:"no-store"}).then(function(f){` +
886
886
  `if(f.type==="opaqueredirect"||f.status===401||f.status===403){location.href="/login";return false;}return f.ok?f.text():null;});` +
887
887
  `if(r.type==="opaqueredirect"||r.status===401||r.status===403){location.href="/login";return false;}` +
@@ -27,10 +27,46 @@ export declare function ssoStepUps(page: Html, sso: {
27
27
  label: string;
28
28
  fresh: boolean;
29
29
  }, returnTo: string): Html;
30
+ /**
31
+ * Easy start: for a local owner who has no password yet, every step-up password field (type=password with
32
+ * autocomplete="current-password", typed-only ones included) becomes "Set a password to do this." and the Set a
33
+ * password link, carrying this page on: there is no password to type. A new password, a secret or a key field is
34
+ * never one. The field's own label and hint go with it.
35
+ */
36
+ export declare function localOwnerStepUps(page: Html, setHref: string): Html;
30
37
  export declare function loginPage(problem: string | null, returnTo?: string, sso?: {
31
38
  label: string;
32
39
  operatorsOnly: boolean;
33
40
  } | null): Html;
41
+ /**
42
+ * Easy-start C2: reconnect this tab after a restart (or when it lost its key): the password again for a full session, an
43
+ * identity-provider sign-in for one that signs in there, or the terminal for a local owner (no password exists).
44
+ */
45
+ export declare function reconnectPage(problem: string | null, returnTo: string, how: {
46
+ kind: "password";
47
+ name: string;
48
+ } | {
49
+ kind: "sso";
50
+ label: string;
51
+ } | {
52
+ kind: "command";
53
+ }): Html;
54
+ /** Which /password/set field a refusal is about (null: the page itself). */
55
+ export type PasswordSetProblem = {
56
+ field: "password" | "again" | null;
57
+ text: string;
58
+ };
59
+ /**
60
+ * Easy-start C4: the local owner's first password. Two fields and one button; the form carries the session's CSRF token
61
+ * (render it inside withFormToken) and, through the page's key fill, the browser key and the act that was refused, to continue there. Neither typed
62
+ * password ever comes back on a refusal.
63
+ */
64
+ export declare function passwordSetPage(view: {
65
+ returnTo: string;
66
+ problem: PasswordSetProblem | null;
67
+ }): Html;
68
+ /** /password/set once a password exists (set earlier, or just now in another tab): sign in with it, and come back. */
69
+ export declare function passwordAlreadySetPage(title: string, returnTo: string, signedIn?: boolean): Html;
34
70
  /**
35
71
  * The page for an address the console doesn't answer to (onboarding): what was opened, where it answers, and the exact
36
72
  * command that admits this address. It stands alone (styles inline, no script, no font), because every other asset
@@ -54,6 +54,19 @@ export function ssoStepUps(page, sso, returnTo) {
54
54
  const labelled = replaceMarkup(page, /<label>[^<]*<input\b([^>]*)>\s*<\/label>/g, (_whole, attributes) => stepUp(attributes) ? swap(attributes) : null);
55
55
  return replaceMarkup(labelled, /<input\b([^>]*)>/g, (_whole, attributes) => stepUp(attributes) ? swap(attributes) : null);
56
56
  }
57
+ /**
58
+ * Easy start: for a local owner who has no password yet, every step-up password field (type=password with
59
+ * autocomplete="current-password", typed-only ones included) becomes "Set a password to do this." and the Set a
60
+ * password link, carrying this page on: there is no password to type. A new password, a secret or a key field is
61
+ * never one. The field's own label and hint go with it.
62
+ */
63
+ export function localOwnerStepUps(page, setHref) {
64
+ const stepUp = (attributes) => /\btype="password"/.test(attributes) && /\bautocomplete="current-password"/.test(attributes);
65
+ const instead = () => html `<p class="k-note" data-local-step-up>Set a password to do this. <a class="k-link" href="${setHref}">Set a password</a></p>`;
66
+ const fields = replaceMarkup(page, /<div class="k-field"><label class="k-label" for="[^"]*">[^<]*<\/label><input\b([^>]*)>(?:<p class="k-hint"[^>]*>[^<]*<\/p>)?(?:<span class="k-err"[^>]*>[^<]*<\/span>)?<\/div>/g, (_whole, attributes) => stepUp(attributes) ? instead() : null);
67
+ const labelled = replaceMarkup(fields, /<label\b[^>]*>[^<]*(?:<span\b[^>]*>[^<]*<\/span>)?[^<]*<input\b([^>]*)>\s*<\/label>/g, (_whole, attributes) => stepUp(attributes) ? instead() : null);
68
+ return replaceMarkup(labelled, /<input\b([^>]*)>/g, (_whole, attributes) => stepUp(attributes) ? instead() : null);
69
+ }
57
70
  /** An expiry as a person reads it: "Thu 5 Nov, 10:42". */
58
71
  function expiryWords(iso) {
59
72
  const at = new Date(iso);
@@ -77,7 +90,58 @@ export function loginPage(problem, returnTo = "/", sso = null) {
77
90
  ]);
78
91
  return shell("Sign in · Toolroll", outPage({
79
92
  brand: BRAND_HTML, brandHref: "/login", title: "Sign in", body,
80
- foot: html `${sso === null ? html `Forgot it? Your first login was shown when Toolroll started, and saved beside its database as <code>up-login.txt</code> on the computer running it.<br>` : ""}No account? Ask whoever runs this Toolroll for an invite link.`,
93
+ foot: html `${sso === null ? html `Forgot it? Your first login was shown when Toolroll started, and saved beside its database in <code>.toolroll-secrets/up-login.txt</code> on the computer running it.<br>` : ""}No account? Ask whoever runs this Toolroll for an invite link.`,
94
+ }), { nav: false });
95
+ }
96
+ /**
97
+ * Easy-start C2: reconnect this tab after a restart (or when it lost its key): the password again for a full session, an
98
+ * identity-provider sign-in for one that signs in there, or the terminal for a local owner (no password exists).
99
+ */
100
+ export function reconnectPage(problem, returnTo, how) {
101
+ const back = returnTo === "/" ? null : returnTo;
102
+ const body = how.kind === "command"
103
+ ? html `<p>Run <code>toolroll open</code> on this computer to reconnect.</p>`
104
+ : how.kind === "sso"
105
+ ? btn(`Sign in with ${how.label}`, { variant: "primary", href: `/login/sso${back === null ? "" : `?return=${encodeURIComponent(back)}`}`, className: "k-wide" })
106
+ : postForm("/login/rekey", joinHtml([
107
+ outField({ id: "rekey-password", label: `Password for ${how.name}`, name: "token", type: "password", attrs: { autocomplete: "current-password", required: true, autofocus: true } }),
108
+ btn("Reconnect", { variant: "primary", type: "submit", className: "k-wide" }),
109
+ ]), { signedOut: true, returnTo: back, attrs: { class: "k-form" } });
110
+ return shell("Reconnect · Toolroll", outPage({
111
+ brand: BRAND_HTML, brandHref: "/", title: "Reconnect",
112
+ body: joinHtml([problem === null ? "" : html `<div class="k-refusal">${notice(problem)}</div>`, body]),
113
+ foot: html `Toolroll restarted, so this tab needs a fresh connection before it can make changes.`,
114
+ }), { nav: false });
115
+ }
116
+ /**
117
+ * Easy-start C4: the local owner's first password. Two fields and one button; the form carries the session's CSRF token
118
+ * (render it inside withFormToken) and, through the page's key fill, the browser key and the act that was refused, to continue there. Neither typed
119
+ * password ever comes back on a refusal.
120
+ */
121
+ export function passwordSetPage(view) {
122
+ const problem = view.problem;
123
+ const error = (field) => problem !== null && problem.field === field ? problem.text : null;
124
+ const back = view.returnTo === "/" ? null : view.returnTo;
125
+ const form = postForm("/password/set", joinHtml([
126
+ outField({ id: "password-new", label: "Password", name: "password", type: "password", hint: `At least ${SIGNUP_PASSWORD_MIN} characters.`, error: error("password"),
127
+ attrs: { autocomplete: "new-password", required: true, minlength: SIGNUP_PASSWORD_MIN, ...(problem === null ? { autofocus: true } : {}) } }),
128
+ outField({ id: "password-again", label: "Type it again", name: "again", type: "password", error: error("again"),
129
+ attrs: { autocomplete: "new-password", required: true, minlength: SIGNUP_PASSWORD_MIN } }),
130
+ btn("Set password", { variant: "primary", type: "submit", className: "k-wide" }),
131
+ ]), { hidden: { next: back }, attrs: { class: "k-form" } });
132
+ return shell("Set a password · Toolroll", outPage({
133
+ brand: BRAND_HTML, brandHref: "/", title: "Set a password",
134
+ sub: "Agents can work on their own, merge and release only after you set a password.",
135
+ body: joinHtml([problem !== null && problem.field === null ? html `<div class="k-refusal">${notice(problem.text)}</div>` : "", form,
136
+ html `<div class="k-acts">${btn("Not now", { variant: "ghost", href: view.returnTo, className: "k-wide" })}</div>`]),
137
+ }), { nav: false });
138
+ }
139
+ /** /password/set once a password exists (set earlier, or just now in another tab): sign in with it, and come back. */
140
+ export function passwordAlreadySetPage(title, returnTo, signedIn = false) {
141
+ return shell(`${title} · Toolroll`, outPage({
142
+ brand: BRAND_HTML, brandHref: "/", title, sub: signedIn ? "You're signed in with it." : "Sign in with it to continue.",
143
+ body: html `<div class="k-acts">${signedIn ? btn("Continue", { variant: "primary", href: returnTo, className: "k-wide" })
144
+ : btn("Sign in", { variant: "primary", href: returnTo === "/" ? "/login" : `/login?return=${encodeURIComponent(returnTo)}`, className: "k-wide" })}</div>`,
81
145
  }), { nav: false });
82
146
  }
83
147
  /**
@@ -73,7 +73,9 @@ export function cloneReviewHtml(onboard, problem) {
73
73
  return null;
74
74
  const [nonce, record] = onboard.record;
75
75
  const kind = "step-up";
76
- const factsNow = approvalFactsNow();
76
+ // The local owner has no password to type: the clone is its own act (add-project), proved by the session at confirm.
77
+ const local = onboard.localOwner === true;
78
+ const factsNow = local ? { ...approvalFactsNow(), passwordless: true } : approvalFactsNow();
77
79
  const size = record.diskUsageKib === null ? "Unknown" : `${Math.max(1, Math.round(record.diskUsageKib / 1024))} MiB`;
78
80
  const bigOk = record.large
79
81
  ? html `<div class="k-checks"><label><input type="checkbox" name="big-ok" value="1" required>${record.nameWithOwner} is ${record.diskUsageKib === null ? "of unknown size" : size}. Clone it anyway</label></div>`
@@ -82,7 +84,7 @@ export function cloneReviewHtml(onboard, problem) {
82
84
  status: html `<h2>Clone ${record.nameWithOwner}</h2>`,
83
85
  text: html `Review before anything is downloaded. Clones into ${mono(shortPathOf(record.target))} and adds it as a project. Large-file (LFS) objects aren’t downloaded.`,
84
86
  acts: approveForm({ action: "/projects/onboard-confirm", kind, facts: factsNow, label: "Clone and add", form: { hidden: { nonce } }, before: bigOk }),
85
- terms: `${approvalTerms(kind, factsNow)} The review expires in 10 minutes.`,
87
+ terms: `${local ? "No password needed on this computer." : approvalTerms(kind, factsNow)} The review expires in 10 minutes.`,
86
88
  err: problem === null ? null : refused(problem),
87
89
  attrs: { "aria-label": `Clone ${record.nameWithOwner}` },
88
90
  })}${facts([["Repository", mono(`github.com/${record.nameWithOwner}`)], ["Size", size], ["Into", mono(shortPathOf(record.target))]])}`;
@@ -119,6 +119,16 @@ export type ApprovalSheetInput = {
119
119
  } | null;
120
120
  /** Cockpit B2: this person's password is remembered (when, ISO) and these terms may stand on it. */
121
121
  freshSince?: string | null;
122
+ /** Easy-start C5: a laptop's local owner — eligible (no password field), or why not and where to go. */
123
+ localOwner?: LocalApprovalView | null;
124
+ };
125
+ export type LocalApprovalView = {
126
+ eligible: true;
127
+ } | {
128
+ eligible: false;
129
+ message: string;
130
+ href: string;
131
+ action: string;
122
132
  };
123
133
  /** The approval sheet: one form that restates every term it signs, the password, and Approve & start. */
124
134
  export declare function approvalSheetHtml(input: ApprovalSheetInput): Html;
@@ -548,6 +558,8 @@ export declare function taskBodyParts(data: {
548
558
  steerRecent?: number[];
549
559
  /** Cockpit B2: when this person's remembered password was given, when it may approve these terms. */
550
560
  approvalFresh?: string | null;
561
+ /** Easy-start C5: the viewer is a laptop's local owner — whether this plan is one it approves without a password. */
562
+ approvalLocal?: LocalApprovalView | null;
551
563
  /** Cockpit B2: how the current approval's person was proved (approval proved ledger), when it was a remembered one. */
552
564
  approvalProof?: {
553
565
  method: string;
@@ -239,9 +239,11 @@ export function approvalSheetHtml(input) {
239
239
  parts.lead,
240
240
  parts.rows,
241
241
  html `<div class="approval-act" id="${input.surface === "task" ? "approval-confirm" : "chat-approval-confirm"}">`,
242
- html `${reauth(html `<label class="approval-password"><span class="visually-hidden">Your password</span><input type="password" name="token" autocomplete="current-password" placeholder="Your password" aria-describedby="${parts.passwordNote}"></label>`)}`,
243
- input.freshSince == null ? html `<p class="approval-password-note" id="${parts.passwordNote}">Your password signs this approval.</p>`
244
- : html `<p class="approval-password-note" id="${parts.passwordNote}" data-approval-fresh>Your password from ${whenTime(input.freshSince)} is remembered on this computer — leave this blank to approve with it, or type it again.</p>`,
242
+ input.localOwner != null ? html `` : html `${reauth(html `<label class="approval-password"><span class="visually-hidden">Your password</span><input type="password" name="token" autocomplete="current-password" placeholder="Your password" aria-describedby="${parts.passwordNote}"></label>`)}`,
243
+ input.localOwner?.eligible === true ? html ``
244
+ : input.localOwner != null ? html `<p class="approval-password-note" id="${parts.passwordNote}">${input.localOwner.message} <a href="${input.localOwner.href}">${input.localOwner.action}</a></p>`
245
+ : input.freshSince == null ? html `<p class="approval-password-note" id="${parts.passwordNote}">Your password signs this approval.</p>`
246
+ : html `<p class="approval-password-note" id="${parts.passwordNote}" data-approval-fresh>Your password from ${whenTime(input.freshSince)} is remembered on this computer — leave this blank to approve with it, or type it again.</p>`,
245
247
  html `<button type="submit" data-primary-action>${parts.submit}</button></div>`,
246
248
  html `<p class="approval-after">${parts.after}</p>`,
247
249
  html `<div class="approval-secondary">${parts.editor ?? html `<a class="approval-link" href="${input.editHref}">Edit plan</a>`}<a class="approval-link" href="${input.notNowHref}">Not now</a></div>`,
@@ -782,6 +784,9 @@ export function approvalProofWords(proof) {
782
784
  return " (from your paired chat)";
783
785
  if (proof.method === "carried")
784
786
  return " (carried from the release approval)";
787
+ // Easy-start C5: this computer's owner approving attended work in local mode, with no password.
788
+ if (proof.method === "local-owner")
789
+ return " (on this computer, no password)";
785
790
  return "";
786
791
  }
787
792
  const whenText = (iso) => new Intl.DateTimeFormat("en-US", { hour: "numeric", minute: "2-digit" }).format(new Date(iso));
@@ -1653,6 +1658,7 @@ export function taskBodyParts(data) {
1653
1658
  nonce: data.nonce,
1654
1659
  digest: data.approvalDigest ?? scope.digest,
1655
1660
  freshSince: data.approvalFresh ?? null,
1661
+ localOwner: data.approvalLocal ?? null,
1656
1662
  returnTo: null,
1657
1663
  scope,
1658
1664
  planDocument: data.plan === "drafted" ? data.planDocument : null,
@@ -2554,8 +2560,10 @@ export function approveKitOf(input, freshSince) {
2554
2560
  heading: rebuild !== null ? "The plan changed" : input.deliverable === "report" ? "Investigation ready" : "Plan ready",
2555
2561
  text: `Approving signs plan version ${input.scope.digest.slice(0, 12)} and ${after}.${allowing}`,
2556
2562
  kind: "standard",
2557
- facts: { passwordless: passwordlessRendering(), remembered: until !== null },
2563
+ facts: { passwordless: passwordlessRendering() || input.localOwner?.eligible === true, remembered: until !== null },
2558
2564
  rememberedUntil: until,
2565
+ // Easy-start C5: the owner approving on this computer needs no password, and the line says so (not "Passwordless").
2566
+ ...(input.localOwner?.eligible === true ? { termsWords: "No password needed on this computer." } : {}),
2559
2567
  planHtml: htmlString(joinHtml([parts.lead, parts.rows, parts.editor ?? "", parts.details, parts.editorForm])),
2560
2568
  };
2561
2569
  }
@@ -45,6 +45,12 @@ export type RequestFacts = {
45
45
  passwordless?: boolean;
46
46
  /** The browser session's open project (display only: the sidebar's picker names it). */
47
47
  openProject?: string | null;
48
+ /** Easy start: a local owner with no password yet: every step-up field is the Set a password link instead (this href). */
49
+ localStepUp?: string;
50
+ /** Easy-start C2: the browser mutation key is required here, so every page that posts carries the key fill script. */
51
+ keyFill?: boolean;
52
+ /** Easy-start C2: this session's key is from an earlier console start (or it has none): how to reconnect. */
53
+ reconnect?: import("./mutation-key.js").Reconnect | null;
48
54
  };
49
55
  /**
50
56
  * The request's own session facts, readable from anywhere below the
@@ -16,6 +16,7 @@
16
16
  * admission: passwords, nonces, webhook signatures, object membership, stale
17
17
  * sessions and revoked tokens.
18
18
  */
19
+ import type { LocalOwnerAct } from "../local-owner.js";
19
20
  /** Who presents the request. `service` is a verified external sender (a signed webhook, an OAuth return carrying its one-time state). */
20
21
  export type Caller = "anonymous" | "service" | "cookie" | "bearer" | "oauth" | "coordinator" | "lead";
21
22
  /** What the caller must hold: nothing, read, act (an approver or an `act` token), or a fresh password/provider check on top. */
@@ -86,7 +87,22 @@ export interface RouteDeclaration {
86
87
  /** Cockpit B2: whether a password remembered on this session may stand in here (`allowlist`), or never. Only the
87
88
  * allowlisted acts run their check inside `withFreshness` (scope.ts); every other step-up is `never`. */
88
89
  readonly freshness: "never" | "allowlist";
90
+ /**
91
+ * Easy-start C1: whether a local owner's session (a laptop install's passwordless owner) may use this row, decided for
92
+ * every row when the table loads (localMarkOf below). `allow` rows name the act they are (local-owner.ts
93
+ * LOCAL_OWNER_ACTS); the console's admission refuses a local owner everywhere else before the handler runs.
94
+ */
95
+ readonly local: "allow" | "deny";
96
+ readonly localAct: LocalOwnerAct | null;
89
97
  }
98
+ /** Every edge row, decided: allow where a local owner's cookie may reach it, deny where its adapter refuses one (or no
99
+ * cookie is ever read there). A new edge row fails the table's load until it is listed here. */
100
+ export declare const EDGE_LOCAL: Readonly<Record<string, {
101
+ local: "allow" | "deny";
102
+ why: string;
103
+ }>>;
104
+ /** The ids the local marks name, for the inventory test (a mark naming no row is stale). */
105
+ export declare const LOCAL_MARKED_IDS: readonly string[];
90
106
  /** The sole route table, in match order. */
91
107
  export declare const ROUTES: readonly RouteDeclaration[];
92
108
  /** The first row that declares this method and path, or null: an undeclared request never reaches a handler. */
@@ -45,8 +45,114 @@ function row(stage, domain, id, method, pattern, options = {}) {
45
45
  ...(options.roleRefusal === undefined ? {} : { roleRefusal: options.roleRefusal }),
46
46
  ...(options.bodyCap === undefined ? {} : { bodyCap: options.bodyCap }),
47
47
  freshness: options.freshness ?? "never",
48
+ ...localMarkOf(stage, method, id),
48
49
  });
49
50
  }
51
+ // ---- easy-start C1: the local owner's marks --------------------------------------------------------------------------
52
+ // Decided here for every row, failing the table's load for an edge row nobody decided. A console POST is refused to a
53
+ // local owner unless LOCAL_POSTS names its act; a console GET is a read (`read`) unless LOCAL_GET_DENY names it.
54
+ /** The console POST rows a local owner may use, and the act each is. Everything else is refused. */
55
+ const LOCAL_POSTS = Object.freeze({
56
+ "tasks.add": "file-task",
57
+ "chat.file": "file-task",
58
+ "chat.send": "lead-send",
59
+ "chat.queued-remove": "lead-send",
60
+ "chat.mate-mint": "lead-send",
61
+ "chat.mate-follow": "lead-send",
62
+ "chat.mate-end": "lead-send",
63
+ "chat.mate-stop": "lead-send",
64
+ // The confirm door narrows the act to the card's own (lead-doors.ts: LOCAL_OWNER_PROPOSALS and the operation's act).
65
+ "chat.proposal.confirm": "lead-send",
66
+ "chat.proposal.dismiss": "lead-send",
67
+ "task.act.steer": "lead-send",
68
+ "task.act.approve": "approve-plan",
69
+ "decision.answer": "answer-question",
70
+ "task.act.reply": "answer-question",
71
+ "task.act.stop": "stop-run",
72
+ "task.act.cancel": "stop-run",
73
+ "task.act.hold": "pause-run",
74
+ "task.act.pause": "pause-run",
75
+ "task.act.unhold": "resume-run",
76
+ "task.act.resume-arm": "resume-run",
77
+ "task.act.resume": "resume-run",
78
+ "task.act.complete": "complete-task",
79
+ "run.act.revise": "request-revision",
80
+ "run.act.note": "request-revision",
81
+ "run.act.comment": "request-revision",
82
+ "task.act.archive": "archive-task",
83
+ "task.act.restore": "archive-task",
84
+ "projects.open": "add-project",
85
+ "projects.github-list": "add-project",
86
+ "projects.onboard-preview": "add-project",
87
+ "projects.onboard-confirm": "add-project",
88
+ // The welcome's sample project, added the way Projects adds a folder (the same admission and its re-check).
89
+ "welcome.sample": "add-project",
90
+ // The welcome's "Use this one" (and Settings → Lead's turn-on): the lead runs on Claude Code or Codex signed in here.
91
+ "settings.lead-on": "choose-default-agent",
92
+ // Choosing which open project the pages show: a view filter, never authorization.
93
+ "projects.select": "read",
94
+ "agents.seen": "mark-seen",
95
+ "agents.watch": "watch-agent",
96
+ });
97
+ /** Console GET rows refused to a local owner (none so far: every GET is a read; adapter-inventory.test.ts proves no GET
98
+ * writes beyond the allowlisted session touch and review nonce). */
99
+ const LOCAL_GET_DENY = new Set([]);
100
+ /** Every edge row, decided: allow where a local owner's cookie may reach it, deny where its adapter refuses one (or no
101
+ * cookie is ever read there). A new edge row fails the table's load until it is listed here. */
102
+ export const EDGE_LOCAL = Object.freeze({
103
+ "edge.healthz": { local: "allow", why: "public read" },
104
+ "edge.instance": { local: "allow", why: "public read (the CLI proves the console)" },
105
+ "edge.telegram-hook": { local: "deny", why: "signed webhook; never a cookie" },
106
+ "edge.flow-form": { local: "deny", why: "a flow's public form; never a cookie" },
107
+ "edge.flow-form-send": { local: "deny", why: "a flow's public form; never a cookie" },
108
+ "edge.flow-hook": { local: "deny", why: "signed webhook; never a cookie" },
109
+ "edge.mcp": { local: "deny", why: "bearer only" },
110
+ "edge.oauth-discovery": { local: "allow", why: "public read" },
111
+ "edge.oauth-register": { local: "deny", why: "no cookie; registers a client only" },
112
+ "edge.oauth-token": { local: "deny", why: "OAuth client only" },
113
+ "edge.oauth-authorize": { local: "deny", why: "consent is a credential act: the adapter refuses a local owner's session" },
114
+ "edge.oauth-consent": { local: "deny", why: "consent is a credential act: the adapter refuses a local owner's session" },
115
+ "edge.google-callback": { local: "deny", why: "bound to state a refused settings POST starts" },
116
+ "edge.connect-callback": { local: "deny", why: "bound to state a refused settings POST starts" },
117
+ "edge.team-read": { local: "deny", why: "team conversations are a remote feature: authenticate refuses a local owner" },
118
+ "edge.team-send": { local: "deny", why: "team conversations are a remote feature: authenticate refuses a local owner" },
119
+ "edge.cli": { local: "deny", why: "bearer only" },
120
+ "edge.teams": { local: "deny", why: "signed service only" },
121
+ "edge.browser-asset": { local: "allow", why: "public asset" },
122
+ "edge.workspace-style": { local: "allow", why: "public asset" },
123
+ "edge.install-asset": { local: "allow", why: "public asset" },
124
+ "edge.font": { local: "allow", why: "public asset" },
125
+ "edge.desktop-health": { local: "allow", why: "public read" },
126
+ "edge.login-page": { local: "allow", why: "before sign-in" },
127
+ "edge.login-link": { local: "allow", why: "before sign-in: a one-time link" },
128
+ "edge.sso-start": { local: "allow", why: "before sign-in" },
129
+ "edge.sso-callback": { local: "allow", why: "before sign-in" },
130
+ "edge.sso-finish": { local: "allow", why: "before sign-in" },
131
+ "edge.signup": { local: "allow", why: "before sign-in" },
132
+ "edge.login": { local: "allow", why: "before sign-in: a password typed now makes a full session" },
133
+ "edge.logout": { local: "allow", why: "only narrows: ends this session" },
134
+ "edge.join": { local: "allow", why: "before sign-in: an invitation" },
135
+ "edge.join-send": { local: "allow", why: "before sign-in: an invitation" },
136
+ "edge.rekey-page": { local: "allow", why: "reads only: a local owner is told to run `toolroll open`" },
137
+ "edge.rekey": { local: "allow", why: "only a password typed now re-keys a full session; a local owner is told to run `toolroll open` and gets no key" },
138
+ "edge.password-set-page": { local: "allow", why: "reads only: the local owner's form for a first password, or \"already set — sign in\"" },
139
+ "edge.password-set": { local: "allow", why: "the one credential act a local owner may make: its first password (Origin, CSRF and key proved by the handler; enrollOwnerPassword re-checks local mode, the owner and password_set=0 in its own transaction); a session with a password set is refused" },
140
+ live: { local: "allow", why: "observational only; the agent watch lease is a keyed POST (agents.watch)" },
141
+ });
142
+ function localMarkOf(stage, method, id) {
143
+ if (stage === "edge") {
144
+ const decided = Object.prototype.hasOwnProperty.call(EDGE_LOCAL, id) ? EDGE_LOCAL[id] : undefined;
145
+ if (decided === undefined)
146
+ throw new Error(`route table: edge row ${id} has no local-owner mark (EDGE_LOCAL)`);
147
+ return { local: decided.local, localAct: decided.local === "allow" ? "read" : null };
148
+ }
149
+ if (method !== "POST")
150
+ return LOCAL_GET_DENY.has(id) ? { local: "deny", localAct: null } : { local: "allow", localAct: "read" };
151
+ const act = Object.prototype.hasOwnProperty.call(LOCAL_POSTS, id) ? LOCAL_POSTS[id] : null;
152
+ return act === null ? { local: "deny", localAct: null } : { local: "allow", localAct: act };
153
+ }
154
+ /** The ids the local marks name, for the inventory test (a mark naming no row is stale). */
155
+ export const LOCAL_MARKED_IDS = Object.freeze([...Object.keys(LOCAL_POSTS), ...LOCAL_GET_DENY, ...Object.keys(EDGE_LOCAL)]);
50
156
  const edge = (domain, id, method, path, options = {}) => row("edge", domain, id, method, path.startsWith("^") ? path : exact(path), options);
51
157
  const get = (domain, id, path, options = {}) => row("console", domain, id, "GET", path.startsWith("^") ? path : exact(path), options);
52
158
  const post = (domain, id, path, options = {}) => row("console", domain, id, "POST", path.startsWith("^") ? path : exact(path), options);
@@ -100,6 +206,12 @@ edge("people", "edge.sso-finish", "GET", "/login/sso/finish");
100
206
  edge("people", "edge.signup", "POST", "/signup");
101
207
  edge("people", "edge.login", "POST", "/login");
102
208
  edge("people", "edge.logout", "POST", "/logout", { callers: ["anonymous", "cookie"] });
209
+ // Easy-start C2: a browser session's fresh mutation key after a restart: its own password again (the handler proves both).
210
+ edge("people", "edge.rekey-page", "GET", "/login/rekey", { callers: ["anonymous", "cookie"] });
211
+ edge("people", "edge.rekey", "POST", "/login/rekey", { callers: ["anonymous", "cookie"] });
212
+ // Easy-start C4: the local owner sets a first password (only while none is set); the answer is a fresh full session.
213
+ edge("people", "edge.password-set-page", "GET", "/password/set", { callers: ["anonymous", "cookie"] });
214
+ edge("people", "edge.password-set", "POST", "/password/set", { callers: ["anonymous", "cookie"] });
103
215
  edge("people", "edge.join", "GET", "^\\/join\\/[A-Za-z0-9_-]{16,64}$", { sample: "/join/abcdefghijklmnop" });
104
216
  edge("people", "edge.join-send", "POST", "^\\/join\\/[A-Za-z0-9_-]{16,64}$", { sample: "/join/abcdefghijklmnop" });
105
217
  // ---- console reads ---------------------------------------------------------------------------------------------
@@ -165,6 +277,8 @@ get("pages", "fleet", "/fleet", free);
165
277
  get("tasks", "tasks.new", "/tasks/new", { limited: "collection", ...free });
166
278
  // The first run's own page (Wave E): five guided steps, reads only; it leads Home once the first run is over.
167
279
  get("tasks", "welcome", "/welcome", { limited: "collection", ...free });
280
+ // Try a sample project (first-run.ts, sample-project.ts): made in the project folder, then added the way Projects adds a folder.
281
+ post("tasks", "welcome.sample", "/welcome/sample", { callers: ["cookie"], project: "none", ...approver("An approver adds projects.", "/welcome") });
168
282
  get("tasks", "task.page", `^\\/t\\/${TASK}$`, { project: "task", limited: "resource", ...free, sample: "/t/one" });
169
283
  get("tasks", "task.evidence", `^\\/t\\/${TASK}\\/evidence$`, { project: "task", limited: "resource", ...free, sample: "/t/one/evidence" });
170
284
  get("pages", "ledger.export", "/ledger/export", { limited: "unscoped", ...free });
@@ -329,6 +443,8 @@ post("tasks", "sweep.cancel-rest", "^\\/sweeps\\/[1-9][0-9]{0,9}\\/cancel-rest$"
329
443
  // (the handler proves it as a credential act); turning it off is one click, so the row itself is not a step-up.
330
444
  get("settings", "settings.passwordless", "/settings/passwordless", { ...free, ...operator("An instance operator sets Passwordless.", "/settings") });
331
445
  post("settings", "settings.passwordless-send", "/settings/passwordless", { callers: ["cookie"], ...operator("An instance operator sets Passwordless.", "/settings") });
446
+ // Easy-start C5: turning local mode off: an instance operator's step-up from a full session (a local owner is refused).
447
+ post("settings", "settings.local-mode-off", "/settings/local-mode/off", { callers: ["cookie"], scope: "step-up", ...operator("An instance operator turns off local mode.", "/settings/access") });
332
448
  // Release approval (docs/plans/auto-approve-green.md): an approver of the project, from a browser. Approving releases
333
449
  // automatically takes a typed password (the handler proves it as a credential act); asking again is one click.
334
450
  post("settings", "settings.release-approval-send", "/settings/release-approval", { callers: ["cookie"], ...approver("An approver sets how releases are approved.", "/settings/approval") });
@@ -473,6 +589,11 @@ for (const one of ["note", "comment", "revise", "draft-repair", "checks", "add-t
473
589
  // Cockpit E: reveal a running attempt's proved checkout in Finder (nothing is launched in it).
474
590
  post("tasks", "run.act.reveal", `^\\/r\\/${ID}\\/reveal$`, { project: "run", limited: "resource", callers: ["cookie"], sample: "/r/1/reveal", ...approver("Only an approver can show an agent's files.") });
475
591
  post("tasks", "session.editor-links", "/session/editor-links", { viewer: true });
592
+ // Easy-start C1 (N1): reads never write. The shell says a finished result was opened (it leaves the person's unread
593
+ // Done), and holds or releases the short lease that quiets chat pings while an agent's page is open — keyed POSTs, never
594
+ // a page load or the live stream. A watch-only account keeps its own seen marks and watches too.
595
+ post("tasks", "agents.seen", "/agents/seen", { callers: ["cookie"], project: "none", limited: "unscoped", viewer: true });
596
+ post("tasks", "agents.watch", "/agents/watch", { callers: ["cookie"], project: "none", limited: "unscoped", viewer: true });
476
597
  post("tasks", "incident.resolve", `^\\/i\\/${ID}\\/resolve$`, { project: "incident", sample: "/i/1/resolve" });
477
598
  /** The sole route table, in match order. */
478
599
  export const ROUTES = Object.freeze(rows.map(one => Object.freeze(one)));
@@ -210,6 +210,7 @@ export interface ServerRuntime {
210
210
  authorizeMutation: (request: IncomingMessage, who: Who, body: FormView<FormFieldOf<"mutationGuard">>) => {
211
211
  status: number;
212
212
  message: string;
213
+ reconnect?: import("./mutation-key.js").Reconnect;
213
214
  } | null;
214
215
  leadSaid: Map<string, {
215
216
  turn: number | null;
@@ -245,10 +246,11 @@ export interface ServerRuntime {
245
246
  label: string;
246
247
  operatorsOnly: boolean;
247
248
  } | null;
248
- signInLinks: Map<string, {
249
- account: string;
250
- expires: number;
251
- }>;
249
+ signInLinks: Map<string, import("./session.js").SignInLink>;
250
+ /** v147: this console start's key epoch (easy-start C2). */
251
+ keyEpoch: string;
252
+ /** Easy-start C2: the browser mutation key (server/mutation-key.ts): the one check, issuing and delivering a key. */
253
+ browserKeys: import("./mutation-key.js").BrowserKeys;
252
254
  linkKey: (code: string) => string;
253
255
  fromThisComputer: (request: IncomingMessage) => boolean;
254
256
  /** Cockpit B2: the request's live password-freshness facts (null off a browser session). */