toolroll 0.9.88 → 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 +65 -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 +30 -30
- 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-channel.js +11 -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/egress.js +5 -0
- package/dist/first-run.d.ts +77 -0
- package/dist/first-run.js +45 -0
- package/dist/flow-starters.js +3 -0
- package/dist/flow-triggers.js +4 -0
- package/dist/folder-gate.d.ts +32 -0
- package/dist/folder-gate.js +43 -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-access.js +15 -1
- package/dist/project-admission.d.ts +6 -1
- package/dist/project-admission.js +44 -7
- package/dist/project-identity.d.ts +8 -0
- package/dist/project-identity.js +43 -0
- package/dist/project-knowledge.d.ts +7 -7
- package/dist/project.d.ts +1 -0
- package/dist/project.js +6 -0
- package/dist/proposal.js +11 -1
- package/dist/quick-start.d.ts +3 -1
- package/dist/quick-start.js +6 -1
- package/dist/recipes.js +12 -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 +157 -28
- 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 +12 -9
- 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 +16 -6
- 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/setup-guide.js +6 -0
- 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/teardown-git.js +16 -1
- 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/dist/youiyoux-intake.js +11 -1
- package/package.json +4 -2
|
@@ -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.
|
|
244
|
-
: html `<p class="approval-password-note" id="${parts.passwordNote}"
|
|
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));
|
|
@@ -1083,8 +1088,10 @@ export function decisionAnswerScript() {
|
|
|
1083
1088
|
`var f=e.target;if(!f||!f.classList||!f.classList.contains("decide-inline"))return;` +
|
|
1084
1089
|
`e.preventDefault();` +
|
|
1085
1090
|
`var card=f.closest("[data-decision-id]");` +
|
|
1086
|
-
|
|
1087
|
-
`
|
|
1091
|
+
// The action attribute, not f.action: a field named "action" would shadow that property (DOM clobbering).
|
|
1092
|
+
`var to=new URL(HTMLFormElement.prototype.getAttribute.call(f,"action")||"",location.href).href;` +
|
|
1093
|
+
`var page=to.replace(/\\/answer$/,"");` +
|
|
1094
|
+
`fetch(to,{method:"POST",credentials:"same-origin",` +
|
|
1088
1095
|
`headers:{"content-type":"application/x-www-form-urlencoded"},` +
|
|
1089
1096
|
`body:new URLSearchParams(new FormData(f)).toString()})` +
|
|
1090
1097
|
`.then(function(r){return r.text().then(function(t){return{r:r,t:t}})})` +
|
|
@@ -1651,6 +1658,7 @@ export function taskBodyParts(data) {
|
|
|
1651
1658
|
nonce: data.nonce,
|
|
1652
1659
|
digest: data.approvalDigest ?? scope.digest,
|
|
1653
1660
|
freshSince: data.approvalFresh ?? null,
|
|
1661
|
+
localOwner: data.approvalLocal ?? null,
|
|
1654
1662
|
returnTo: null,
|
|
1655
1663
|
scope,
|
|
1656
1664
|
planDocument: data.plan === "drafted" ? data.planDocument : null,
|
|
@@ -2552,8 +2560,10 @@ export function approveKitOf(input, freshSince) {
|
|
|
2552
2560
|
heading: rebuild !== null ? "The plan changed" : input.deliverable === "report" ? "Investigation ready" : "Plan ready",
|
|
2553
2561
|
text: `Approving signs plan version ${input.scope.digest.slice(0, 12)} and ${after}.${allowing}`,
|
|
2554
2562
|
kind: "standard",
|
|
2555
|
-
facts: { passwordless: passwordlessRendering(), remembered: until !== null },
|
|
2563
|
+
facts: { passwordless: passwordlessRendering() || input.localOwner?.eligible === true, remembered: until !== null },
|
|
2556
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." } : {}),
|
|
2557
2567
|
planHtml: htmlString(joinHtml([parts.lead, parts.rows, parts.editor ?? "", parts.details, parts.editorForm])),
|
|
2558
2568
|
};
|
|
2559
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)));
|
package/dist/server/runtime.d.ts
CHANGED
|
@@ -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
|
-
|
|
250
|
-
|
|
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). */
|
package/dist/server/session.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { type ChatDraft } from "../converse.js";
|
|
3
3
|
import { type Principal } from "../operate-remote.js";
|
|
4
4
|
import { type Store } from "../store.js";
|
|
5
|
+
import { type Authority } from "../local-owner.js";
|
|
5
6
|
import { type Server } from "node:http";
|
|
6
7
|
/** A user agent, reduced to safe display words — never echoed raw. */
|
|
7
8
|
export declare function oneLineUa(raw: string | string[] | undefined): string;
|
|
@@ -27,6 +28,14 @@ export declare const NO_TOUCH_FRAGMENTS: ReadonlySet<string>;
|
|
|
27
28
|
export type Session = {
|
|
28
29
|
name: string;
|
|
29
30
|
csrf: string;
|
|
31
|
+
/** v147 (easy-start C1): full, or the local owner's restricted authority. Set when the session is made; never changed. */
|
|
32
|
+
authority: Authority;
|
|
33
|
+
/** v147: a local owner's session stands only while local mode is on at this generation (null for a full session). */
|
|
34
|
+
localGeneration: number | null;
|
|
35
|
+
/** v147 (C2, filled in by the browser-key wave): the console start whose mutation key this session holds, and a hash of
|
|
36
|
+
* that key (never the key). Null: no key yet, or a session saved before v147 — stale, so mutations ask to reconnect. */
|
|
37
|
+
keyEpoch: string | null;
|
|
38
|
+
keyHash: string | null;
|
|
30
39
|
/** v29: the account's standing at login — the central gate reads it;
|
|
31
40
|
* the revocation cascade kills the session outright. */
|
|
32
41
|
role: "approver" | "viewer";
|
|
@@ -108,7 +117,8 @@ export type Who = {
|
|
|
108
117
|
name: string;
|
|
109
118
|
via: "cookie";
|
|
110
119
|
session: Session;
|
|
111
|
-
role: "approver" | "viewer";
|
|
120
|
+
role: "approver" | "viewer"; /** v147: the session's own authority. */
|
|
121
|
+
authority: Authority;
|
|
112
122
|
} | {
|
|
113
123
|
name: string;
|
|
114
124
|
via: "bearer";
|
|
@@ -117,6 +127,11 @@ export type Who = {
|
|
|
117
127
|
/** The complete proved API-token authority, retained across body reads. */ principal?: Principal;
|
|
118
128
|
generation: number;
|
|
119
129
|
};
|
|
130
|
+
/** A full session's authority fields when it is made (a password, an identity-provider sign-in, a sign-up or join).
|
|
131
|
+
* The browser-key wave adds the key epoch and hash here. */
|
|
132
|
+
export declare const FULL_SESSION: Pick<Session, "authority" | "localGeneration" | "keyEpoch" | "keyHash">;
|
|
133
|
+
/** A caller's authority: a browser session's own, or full for a bearer credential (an API token or a password). */
|
|
134
|
+
export declare function authorityOfWho(who: Who): Authority;
|
|
120
135
|
/** The browser session of a caller its route admits only through a browser sign-in (the row's callers or role). */
|
|
121
136
|
export declare function browserCaller(who: Who): Extract<Who, {
|
|
122
137
|
via: "cookie";
|
|
@@ -157,6 +172,21 @@ export declare class PersistentSessions extends Map<string, Session> {
|
|
|
157
172
|
* closeCoding starts the coding shutdown on its own, ahead of the rest of a stop; close() awaits the same promise. */
|
|
158
173
|
export type DecisionServer = Server & {
|
|
159
174
|
mintSignInLink(account: string): string | null;
|
|
175
|
+
mintLocalLink(): string | null;
|
|
176
|
+
readonly keyEpoch: string;
|
|
160
177
|
closeCoding(): Promise<void>;
|
|
161
178
|
};
|
|
179
|
+
/** A one-time sign-in link as the console holds it (only a hash of its code is the key): a full one for an account,
|
|
180
|
+
* or a local owner's (easy-start C4), bound to local mode's generation and the owner's account generation when minted. */
|
|
181
|
+
export type SignInLink = {
|
|
182
|
+
kind: "full";
|
|
183
|
+
account: string;
|
|
184
|
+
expires: number;
|
|
185
|
+
} | {
|
|
186
|
+
kind: "local-owner";
|
|
187
|
+
account: string;
|
|
188
|
+
localGeneration: number;
|
|
189
|
+
accountGeneration: number;
|
|
190
|
+
expires: number;
|
|
191
|
+
};
|
|
162
192
|
export type SsoIntent = "sign-in" | "reauth" | "link";
|
package/dist/server/session.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import {} from "../converse.js";
|
|
3
3
|
import {} from "../operate-remote.js";
|
|
4
4
|
import {} from "../store.js";
|
|
5
|
+
import {} from "../local-owner.js";
|
|
5
6
|
import { createHash } from "node:crypto";
|
|
6
7
|
import {} from "node:http";
|
|
7
8
|
/** A user agent, reduced to safe display words — never echoed raw. */
|
|
@@ -33,6 +34,13 @@ export const NONCE_TTL_MS = 15 * 60_000;
|
|
|
33
34
|
export const NONCE_CAP = 500;
|
|
34
35
|
/** Read-only fragment polls that must never refresh session activity (arc 1). */
|
|
35
36
|
export const NO_TOUCH_FRAGMENTS = new Set(["1", "facts", "peek", "rail", "transcript"]);
|
|
37
|
+
/** A full session's authority fields when it is made (a password, an identity-provider sign-in, a sign-up or join).
|
|
38
|
+
* The browser-key wave adds the key epoch and hash here. */
|
|
39
|
+
export const FULL_SESSION = Object.freeze({ authority: "full", localGeneration: null, keyEpoch: null, keyHash: null });
|
|
40
|
+
/** A caller's authority: a browser session's own, or full for a bearer credential (an API token or a password). */
|
|
41
|
+
export function authorityOfWho(who) {
|
|
42
|
+
return who.via === "cookie" ? who.authority : "full";
|
|
43
|
+
}
|
|
36
44
|
/** The browser session of a caller its route admits only through a browser sign-in (the row's callers or role). */
|
|
37
45
|
export function browserCaller(who) {
|
|
38
46
|
if (who.via !== "cookie")
|
|
@@ -59,7 +67,8 @@ export class PersistentSessions extends Map {
|
|
|
59
67
|
this.ids.set(session, id);
|
|
60
68
|
this.savedAt.set(session, Date.now());
|
|
61
69
|
this.db.saveWebSession({ idHash: PersistentSessions.hash(id), account: session.name, csrf: session.csrf, role: session.role, generation: session.generation, createdAt: session.createdAt,
|
|
62
|
-
lastSeen: session.lastSeen, project: session.project, projectRevision: session.projectRevision, ssoAt: session.sso?.at ?? null, agent: session.agent ?? null, address: session.address ?? null
|
|
70
|
+
lastSeen: session.lastSeen, project: session.project, projectRevision: session.projectRevision, ssoAt: session.sso?.at ?? null, agent: session.agent ?? null, address: session.address ?? null,
|
|
71
|
+
authority: session.authority, localGeneration: session.localGeneration, keyEpoch: session.keyEpoch, keyHash: session.keyHash });
|
|
63
72
|
return this;
|
|
64
73
|
}
|
|
65
74
|
delete(id) {
|
|
@@ -89,7 +98,8 @@ export class PersistentSessions extends Map {
|
|
|
89
98
|
if (row === null)
|
|
90
99
|
return null;
|
|
91
100
|
const session = { name: row.account, csrf: row.csrf, role: row.role, generation: row.generation, createdAt: row.createdAt, lastSeen: row.lastSeen, sawBoardAt: null,
|
|
92
|
-
project: row.project, projectRevision: row.projectRevision, agent: row.agent, address: row.address, ...(row.ssoAt === null ? {} : { sso: { at: row.ssoAt } })
|
|
101
|
+
project: row.project, projectRevision: row.projectRevision, agent: row.agent, address: row.address, ...(row.ssoAt === null ? {} : { sso: { at: row.ssoAt } }),
|
|
102
|
+
authority: row.authority, localGeneration: row.localGeneration, keyEpoch: row.keyEpoch, keyHash: row.keyHash };
|
|
93
103
|
super.set(id, session);
|
|
94
104
|
this.ids.set(session, id);
|
|
95
105
|
this.savedAt.set(session, Date.now());
|
package/dist/server/settings.js
CHANGED
|
@@ -76,7 +76,7 @@ import { startConnect } from "../mcp-connect.js";
|
|
|
76
76
|
import { decideProposal, listProposals, memoryStatus } from "../memory-pass.js";
|
|
77
77
|
import { checkModels, isNewModel, modelOptions, modelWords, RUNTIME_TOOLS, runtimeStates, seenModels, setWatch, updateRuntime, watchState } from "../model-catalog.js";
|
|
78
78
|
import { modelsHtml, modelsScript } from "../models-ui.js";
|
|
79
|
-
import { ALL_SLOTS, applyModelChoices, browserWords, checkScope, DEFAULT_PRESET, isPresetName, newerModels, PRESETS, presetChange, presetInUse, previewModelChange, recentRuns, rowAt, slotKey, SLOTS, slotWords, stateDigest, switchChange } from "../model-choices.js";
|
|
79
|
+
import { ALL_SLOTS, applyModelChoices, browserWords, checkScope, DEFAULT_PRESET, defaultAgentOf, fillDefaultRouting, isPresetName, newerModels, PRESETS, presetChange, presetInUse, previewModelChange, recentRuns, rowAt, slotKey, SLOTS, slotWords, stateDigest, switchChange } from "../model-choices.js";
|
|
80
80
|
import { HEADROOM_FIVE_HOUR_LIMIT, HEADROOM_WEEKLY_LIMIT, exactModelId } from "../phase-routing.js";
|
|
81
81
|
import { RISKY_MEANS, SIZE_MEANS, SIZING_CLAUDE_MODEL } from "../task-sizing.js";
|
|
82
82
|
import { CHAT_APPROVE_ALL, MODE_MAX_DAYS, modeDigestOf, modeTermsJson, modeWords, presetTerms } from "../modes.js";
|
|
@@ -89,6 +89,7 @@ import { parseProjectConcurrency, saveProjectConcurrency } from "../project-conc
|
|
|
89
89
|
import { MAX_NETWORK_HOSTS, normalizeHost } from "../egress.js";
|
|
90
90
|
import { isWritesMode } from "../writes-mode.js";
|
|
91
91
|
import { isCheckAccess, signInsUnavailableWords, widerThan } from "../check-access.js";
|
|
92
|
+
import { LOCAL_MODE_CONFINES, localModeOn } from "../local-owner.js";
|
|
92
93
|
import { parseMachLookup, parseReadDeny } from "../project-sandbox.js";
|
|
93
94
|
import { projectDeleteConfirmHtml, projectSettingsHtml } from "../project-delete-ui.js";
|
|
94
95
|
import { deleteProject, holdingsWords } from "../project-delete.js";
|
|
@@ -126,7 +127,7 @@ import { sweepSchedule } from "../storage-sweep-schedule.js";
|
|
|
126
127
|
import { storageHtml } from "../storage-ui.js";
|
|
127
128
|
import { landedSummary } from "../landed-cleanup.js";
|
|
128
129
|
import { bytesWords, parseCleanup } from "../storage.js";
|
|
129
|
-
import { isDigestTime, liveBackupState, RESULT_SCREENSHOTS, setLiveBackupSettings } from "../store.js";
|
|
130
|
+
import { isDigestTime, liveBackupState, localModeOf, RESULT_SCREENSHOTS, setLiveBackupSettings, setLocalModeOff } from "../store.js";
|
|
130
131
|
import { defaultLiveFolder, liveFolderOf, liveFolderProblem } from "../live-backup.js";
|
|
131
132
|
import { LIVE_EVERY } from "../backup-ui.js";
|
|
132
133
|
import { checkTeamsCredentials, clearTeamsCredentials, loadTeamsCredentials, saveTeamsCredentials, TeamsError } from "../teams-api.js";
|
|
@@ -775,6 +776,12 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
|
|
|
775
776
|
return null;
|
|
776
777
|
} })() : null,
|
|
777
778
|
access: { sessions: store.webSessions(who.name).length, tokens: store.apiTokens(who.name).filter(one => one.revokedAt === null && Date.parse(one.expiresAt) > now.getTime()).length },
|
|
779
|
+
local: (() => {
|
|
780
|
+
const mode = localModeOf(store);
|
|
781
|
+
if (mode === null || !mode.enabled || who.via !== "cookie")
|
|
782
|
+
return null;
|
|
783
|
+
return { owner: who.authority !== "full", passwordSet: store.accountOf(who.name)?.passwordSet === true, canTurnOff: who.authority === "full" && operator };
|
|
784
|
+
})(),
|
|
778
785
|
backup: lastBackup === null ? null : lastBackup.ok === false
|
|
779
786
|
? { failed: true, words: `Failed ${when(lastBackup.startedAt)}${lastBackup.error === null ? "" : `: ${lastBackup.error}`}` }
|
|
780
787
|
: { failed: false, words: lastBackup.finishedAt === null ? "Backing up now" : `Backed up ${when(lastBackup.finishedAt)}` },
|
|
@@ -2448,14 +2455,35 @@ ${postForm("/settings/chat-approval/save", html `\
|
|
|
2448
2455
|
}
|
|
2449
2456
|
// Turn the lead on with the agent signed in on this computer: its membership spends no dollars, so like starting a
|
|
2450
2457
|
// conversation it needs no second password; the full form (Advanced) still asks for one.
|
|
2458
|
+
// The welcome's "Use this one" names the agent (`agent`, claude or codex: only one signed in with its account here)
|
|
2459
|
+
// and comes back to the welcome (`return`, that one address only).
|
|
2451
2460
|
async function leadOn(ctx) {
|
|
2452
|
-
const { who, response, now } = ctx;
|
|
2461
|
+
const { who, response, now, posted } = ctx;
|
|
2462
|
+
const body = readForm(posted, CONSOLE_FORMS.leadOn);
|
|
2463
|
+
const asked = body.get("agent");
|
|
2464
|
+
if (body.sent.getAll("agent").length > 1 || (asked !== null && asked !== "claude" && asked !== "codex"))
|
|
2465
|
+
return refuse(response, who, 400, "Choose Claude Code or Codex.", "/settings/lead");
|
|
2466
|
+
const back = body.get("return") === "/welcome" ? "/welcome" : null;
|
|
2453
2467
|
await checkLocalAgents(true);
|
|
2454
|
-
const
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2468
|
+
const states = runtime.localSignIn?.states;
|
|
2469
|
+
const provider = asked !== null ? (states?.[asked] === "connected" ? `${asked}-subscription` : null)
|
|
2470
|
+
: states?.claude === "connected" ? "claude-subscription" : states?.codex === "connected" ? "codex-subscription" : null;
|
|
2471
|
+
if (provider === null) {
|
|
2472
|
+
const words = asked === null ? `No agent is signed in on this computer yet. Run ${agentSignInCommand()}, then try again.` : `${asked === "claude" ? "Claude Code" : "Codex"} isn't signed in on this computer yet.`;
|
|
2473
|
+
return redirect(response, back ?? chatReturnWithSaid("/settings/lead", words));
|
|
2474
|
+
}
|
|
2475
|
+
// Choosing the one already in use changes nothing (its daily turns and the rest stay as set). The local owner's
|
|
2476
|
+
// choice also starts the installation's models on that agent's default preset, in the same write, while nothing
|
|
2477
|
+
// is chosen yet (model-choices.ts fillDefaultRouting): its first task can then be planned and approved without a
|
|
2478
|
+
// password. A full session's choice is as before: its models are Settings → Models'.
|
|
2479
|
+
store.transact(() => {
|
|
2480
|
+
if (store.getChatConfig()?.provider !== provider)
|
|
2481
|
+
store.setChatConfig({ provider, model: "default", dailyTurns: 50, weeklyCeilingMicrousd: 0, priceInMicrousd: 0, priceOutMicrousd: 0 }, who.name, now);
|
|
2482
|
+
const agent = defaultAgentOf(provider);
|
|
2483
|
+
if (who.via === "cookie" && who.authority === "local-owner" && agent !== null)
|
|
2484
|
+
fillDefaultRouting(store, agent, who.name, { kind: "local-owner" }, now);
|
|
2485
|
+
});
|
|
2486
|
+
return redirect(response, back ?? "/chat");
|
|
2459
2487
|
}
|
|
2460
2488
|
/** Settings → Models, read from the saved catalog and the role rows at the scope being edited (the installation,
|
|
2461
2489
|
* or one project and what it inherits). */
|
|
@@ -2648,6 +2676,7 @@ ${postForm("/settings/chat-approval/save", html `\
|
|
|
2648
2676
|
"settings.sign-in": signInPage,
|
|
2649
2677
|
"settings.passwordless": passwordlessPage,
|
|
2650
2678
|
"settings.passwordless-send": passwordlessSave,
|
|
2679
|
+
"settings.local-mode-off": localModeOff,
|
|
2651
2680
|
"settings.release-approval-send": releaseApprovalSave,
|
|
2652
2681
|
"settings.lead": leadPage,
|
|
2653
2682
|
"settings.teams": teamsPage,
|
|
@@ -2804,6 +2833,9 @@ ${postForm("/settings/chat-approval/save", html `\
|
|
|
2804
2833
|
if (unavailable !== null)
|
|
2805
2834
|
return back("problem", `${unavailable} Nothing changed.`);
|
|
2806
2835
|
const widening = widerThan(wanted, store.projectCheckAccess(repo));
|
|
2836
|
+
// Easy-start C5: while local mode is on, this installation always confines; said before the password.
|
|
2837
|
+
if (wanted === "unsandboxed" && localModeOn(store))
|
|
2838
|
+
return back("problem", `${LOCAL_MODE_CONFINES} Nothing changed.`);
|
|
2807
2839
|
// Sign-ins with an open network: a second, explicit yes, said before the password.
|
|
2808
2840
|
if (wanted === "sign-ins" && widening && store.projectNetwork(repo).mode === "open" && body.get("confirm-open") !== "yes") {
|
|
2809
2841
|
return back("problem", "This project's network is open: confirm that its checks could send your sign-ins to any host. Nothing changed.");
|
|
@@ -2950,5 +2982,28 @@ ${postForm("/settings/chat-approval/save", html `\
|
|
|
2950
2982
|
const said = modelsSaid(result, isPresetName(preset) ? PRESETS[preset].label : `Switched to ${to}`);
|
|
2951
2983
|
return back(said.key, said.words);
|
|
2952
2984
|
}
|
|
2985
|
+
// Easy-start C5: turning local mode off (Settings → People and sign-in), from a full session only: an instance
|
|
2986
|
+
// operator's step-up, proved and written in one transaction. It bumps local mode's generation, so every local-owner
|
|
2987
|
+
// session, link and approval bound to the old one stops standing. It is never turned back on here.
|
|
2988
|
+
async function localModeOff(ctx) {
|
|
2989
|
+
const { response, now, posted } = ctx;
|
|
2990
|
+
const who = browserCaller(ctx.who);
|
|
2991
|
+
const body = readForm(posted, CONSOLE_FORMS.localModeOff);
|
|
2992
|
+
const back = sayTo(response, "/settings/access", "#local-mode");
|
|
2993
|
+
if (body.sent.getAll("password").length > 1)
|
|
2994
|
+
return back("problem", "That form was sent twice over. Nothing changed.");
|
|
2995
|
+
if (who.authority !== "full")
|
|
2996
|
+
return back("problem", "Sign in with your password to turn off local mode.");
|
|
2997
|
+
const turned = store.transact(() => {
|
|
2998
|
+
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
2999
|
+
return null;
|
|
3000
|
+
return setLocalModeOff(store, { name: who.name, authority: who.authority }, now);
|
|
3001
|
+
});
|
|
3002
|
+
if (turned === null)
|
|
3003
|
+
return back("problem", "Enter your Toolroll password to turn off local mode.");
|
|
3004
|
+
if (!turned.ok)
|
|
3005
|
+
return back("problem", turned.reason === "not-on" ? "Local mode is already off." : "Only an instance operator can turn off local mode.");
|
|
3006
|
+
return back("said", "Local mode is off. This computer's browser signs in with your password now.");
|
|
3007
|
+
}
|
|
2953
3008
|
return { registrations, modelsView };
|
|
2954
3009
|
}
|
|
@@ -5,7 +5,8 @@ import { RELEASE_STEPS } from "../release.js";
|
|
|
5
5
|
import { releaseProgressFor, shipChecksForTask } from "../store.js";
|
|
6
6
|
import { releaseCiRunningOf } from "../release-ci-hold.js";
|
|
7
7
|
import { releaseObligationsOf } from "../release-recheck.js";
|
|
8
|
-
import { approvalProofOf, freshlyApprovable, freshnessFor, withFreshness } from "../scope.js";
|
|
8
|
+
import { approvalProofOf, freshlyApprovable, freshnessFor, localApprovalEligible, withFreshness } from "../scope.js";
|
|
9
|
+
import { localOwnerRefusal } from "../local-owner.js";
|
|
9
10
|
import { liveAttemptOf, steerControlsFor } from "../steer-controls.js";
|
|
10
11
|
import { originOf } from "../task-source.js";
|
|
11
12
|
import { recentGitReads, taskBaseOf } from "../task-base.js";
|
|
@@ -526,6 +527,14 @@ export function createTaskViews(runtime) {
|
|
|
526
527
|
const gate = runtime.store.approvalGate(ref.externalId, who.name, "person");
|
|
527
528
|
return freshlyApprovable(runtime.store, ref.externalId, scope, gate.verdict === "seal" && !gate.protectedWork) ? new Date(fresh.at).toISOString() : null;
|
|
528
529
|
})(),
|
|
530
|
+
// Easy-start C5: a laptop's local owner approves eligible work with no password (the seal applies the same rule).
|
|
531
|
+
approvalLocal: (() => {
|
|
532
|
+
if (ref === null || who?.via !== "cookie" || who.authority !== "local-owner")
|
|
533
|
+
return null;
|
|
534
|
+
if (localApprovalEligible(runtime.store, who.name, ref.externalId).ok)
|
|
535
|
+
return { eligible: true };
|
|
536
|
+
return { eligible: false, ...localOwnerRefusal(runtime.store.accountOf(who.name)?.passwordSet === true, taskHref(ref.externalId)) };
|
|
537
|
+
})(),
|
|
529
538
|
steerRecent: ref === null ? [] : runtime.store.runsFor(ref.id).filter(one => one.outcome !== null).map(one => one.id).sort((a, b) => b - a).slice(0, 5),
|
|
530
539
|
// The running attempt's composer, for an approver on a browser session (steer-controls.ts).
|
|
531
540
|
steerComposer: (() => {
|
package/dist/server/tasks.d.ts
CHANGED
|
@@ -54,3 +54,13 @@ export declare function createTidyHandlers(runtime: ServerRuntime): {
|
|
|
54
54
|
export declare function retiredTasksAddress(route: "board" | "done", url: URL): string | null;
|
|
55
55
|
/** How many cards a Board column shows; the column's count covers them all and its "View all" opens the list. */
|
|
56
56
|
export declare const BOARD_COLUMN_LIMIT = 40;
|
|
57
|
+
/**
|
|
58
|
+
* Easy-start C1 (N1): the two writes a page load used to make, as keyed POSTs the shell sends (wave 1c wires the
|
|
59
|
+
* browser). `agents.seen`: a finished result this person opened leaves their unread Done. `agents.watch`: hold (on each
|
|
60
|
+
* keep-alive) or release (on unload) the short lease that quiets chat pings while this person's page shows the agent.
|
|
61
|
+
* Each re-proves the run is in the person's projects; a reply carries nothing else.
|
|
62
|
+
*/
|
|
63
|
+
export declare function createAgentSignalHandlers(runtime: ServerRuntime): {
|
|
64
|
+
"agents.seen": (ctx: HandlerContext) => Promise<void>;
|
|
65
|
+
"agents.watch": (ctx: HandlerContext) => Promise<void>;
|
|
66
|
+
};
|