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.
- package/CHANGELOG.md +63 -0
- package/README.md +16 -10
- package/THIRD_PARTY_NOTICES.md +37 -0
- package/dist/agent-board.d.ts +9 -0
- package/dist/agent-board.js +20 -0
- package/dist/agent-fence.js +20 -2
- package/dist/approval-authority.d.ts +7 -0
- package/dist/approval-authority.js +13 -2
- package/dist/browser/THIRD_PARTY_NOTICES.txt +692 -28
- package/dist/browser/terminal.js +42 -42
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +32 -32
- package/dist/browser-workspace.d.ts +13 -0
- package/dist/builder.js +5 -2
- package/dist/chat-actions.d.ts +5 -0
- package/dist/chat-actions.js +7 -1
- package/dist/chat-delivery.js +2 -0
- package/dist/chat-task-actions.d.ts +1 -0
- package/dist/check-access.d.ts +3 -0
- package/dist/check-access.js +4 -1
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +6 -2
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +23 -0
- package/dist/coding-ui.js +4 -2
- package/dist/connect-cli.js +2 -1
- package/dist/console-instance.d.ts +34 -1
- package/dist/console-instance.js +124 -15
- package/dist/contracts/cli.d.ts +72 -0
- package/dist/contracts/cli.js +13 -0
- package/dist/contracts/console-api.d.ts +9 -2
- package/dist/contracts/console-api.js +9 -2
- package/dist/desktop-host.js +8 -3
- package/dist/desktop-update.js +2 -1
- package/dist/dispatch.d.ts +3 -0
- package/dist/dispatch.js +22 -2
- package/dist/easy-start-args.d.ts +13 -0
- package/dist/easy-start-args.js +24 -0
- package/dist/easy-start.d.ts +77 -0
- package/dist/easy-start.js +321 -0
- package/dist/first-run.d.ts +77 -0
- package/dist/first-run.js +45 -0
- package/dist/guides.js +1 -1
- package/dist/invoke.js +11 -6
- package/dist/laptop-background.d.ts +171 -0
- package/dist/laptop-background.js +550 -0
- package/dist/laptop-service.d.ts +111 -0
- package/dist/laptop-service.js +268 -0
- package/dist/lead-cli.js +2 -1
- package/dist/lead-doors.d.ts +6 -0
- package/dist/lead-doors.js +20 -2
- package/dist/live-records.d.ts +1 -1
- package/dist/live-records.js +3 -12
- package/dist/local-owner.d.ts +124 -0
- package/dist/local-owner.js +212 -0
- package/dist/login-secrets.d.ts +60 -0
- package/dist/login-secrets.js +195 -0
- package/dist/mcp-oauth.d.ts +4 -0
- package/dist/mcp-oauth.js +13 -7
- package/dist/model-choices.d.ts +43 -0
- package/dist/model-choices.js +80 -0
- package/dist/operate.d.ts +46 -1
- package/dist/operate.js +461 -40
- package/dist/person-proof.js +3 -0
- package/dist/project-knowledge.d.ts +7 -7
- package/dist/quick-start.d.ts +3 -1
- package/dist/quick-start.js +6 -1
- package/dist/release-adapters.d.ts +2 -0
- package/dist/release-adapters.js +30 -1
- package/dist/release-cli.js +2 -1
- package/dist/runner-process.d.ts +9 -0
- package/dist/runner-process.js +18 -0
- package/dist/sample-project.d.ts +31 -0
- package/dist/sample-project.js +141 -0
- package/dist/scope.d.ts +24 -2
- package/dist/scope.js +74 -3
- package/dist/serve.js +129 -22
- package/dist/server/chat.js +10 -3
- package/dist/server/chrome.d.ts +10 -2
- package/dist/server/chrome.js +7 -5
- package/dist/server/console.css +10 -0
- package/dist/server/guards.d.ts +3 -0
- package/dist/server/guards.js +22 -2
- package/dist/server/http.d.ts +22 -0
- package/dist/server/http.js +58 -0
- package/dist/server/live.js +4 -14
- package/dist/server/mutation-key.d.ts +70 -0
- package/dist/server/mutation-key.js +142 -0
- package/dist/server/pages.js +10 -7
- package/dist/server/people-tokens.js +240 -20
- package/dist/server/remote.d.ts +2 -0
- package/dist/server/remote.js +19 -4
- package/dist/server/render-new-task.js +5 -1
- package/dist/server/render-pages.d.ts +2 -1
- package/dist/server/render-pages.js +1 -1
- package/dist/server/render-people.d.ts +36 -0
- package/dist/server/render-people.js +65 -1
- package/dist/server/render-projects.js +4 -2
- package/dist/server/render-tasks.d.ts +12 -0
- package/dist/server/render-tasks.js +12 -4
- package/dist/server/request-context.d.ts +6 -0
- package/dist/server/route-table.d.ts +16 -0
- package/dist/server/route-table.js +121 -0
- package/dist/server/runtime.d.ts +6 -4
- package/dist/server/session.d.ts +31 -1
- package/dist/server/session.js +12 -2
- package/dist/server/settings.js +63 -8
- package/dist/server/task-views.js +10 -1
- package/dist/server/tasks.d.ts +10 -0
- package/dist/server/tasks.js +59 -9
- package/dist/server/terminals.js +15 -2
- package/dist/server/welcome.d.ts +22 -16
- package/dist/server/welcome.js +150 -52
- package/dist/settings-groups-ui.d.ts +7 -1
- package/dist/settings-groups-ui.js +23 -2
- package/dist/settings-proofs.js +1 -0
- package/dist/setup-guide.d.ts +2 -2
- package/dist/store.d.ts +124 -1
- package/dist/store.js +245 -12
- package/dist/surface.d.ts +10 -0
- package/dist/surface.js +40 -1
- package/dist/task-text.d.ts +6 -0
- package/dist/task-text.js +17 -0
- package/dist/telegram-lead.js +2 -0
- package/dist/terminal-text.d.ts +11 -0
- package/dist/terminal-text.js +26 -0
- package/dist/toolroll-update.d.ts +8 -1
- package/dist/toolroll-update.js +151 -4
- package/dist/tui-contract.d.ts +118 -0
- package/dist/tui-contract.js +9 -0
- package/dist/tui-source.d.ts +14 -0
- package/dist/tui-source.js +182 -0
- package/dist/tui.js +129 -0
- package/dist/up-processes.d.ts +13 -1
- package/dist/up-processes.js +13 -3
- package/dist/welcome-facts.d.ts +18 -0
- package/dist/welcome-facts.js +79 -0
- package/package.json +4 -2
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
|
+
};
|
package/dist/server/tasks.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { titleWithin } from "../task-text.js";
|
|
1
2
|
import { parseFeedbackImage, sameFeedbackImage } from "../result-review.js";
|
|
2
3
|
import { archiveTaskFamily, dropFeedbackImage, releaseAuthorityOfTask, restoreTaskFamily, revokeRelease, runLiveCommands } from "../store.js";
|
|
3
4
|
import { applyTidy, approverStandsNow, familyRepoOf, releaseFactsOf, tidyDays, tidyDigest, tidyPreview, TIDY_DEFAULT_DAYS } from "../tidy.js";
|
|
@@ -58,14 +59,14 @@ import { structuredHandoffView, terminalDiffView } from "../result-evidence-read
|
|
|
58
59
|
import { fileAddTestsTask, requestFollowUpChecks } from '../result-follow-ups.js';
|
|
59
60
|
import { commentSourceKey, isRevisionFeedback, parseResultTab, resultReturnTarget } from "../result-review.js";
|
|
60
61
|
import { isAlive as runnerAlive } from "../runner.js";
|
|
61
|
-
import { passwordlessSessionFor, acceptanceLinesToInput, acceptanceToLines, approvalOf, approve as approveScope, freshnessFor, spendingOnlyOnSuccess, ssoNowAvailable, withFreshness, modeFilingCoverage, PLACEHOLDER_RUBRIC, proposeGuarded } from "../scope.js";
|
|
62
|
+
import { passwordlessSessionFor, acceptanceLinesToInput, acceptanceToLines, approvalOf, approve as approveScope, freshnessFor, localApprovalEligible, spendingOnlyOnSuccess, ssoNowAvailable, withFreshness, modeFilingCoverage, PLACEHOLDER_RUBRIC, proposeGuarded } from "../scope.js";
|
|
62
63
|
import { subagentNames as subagentNamesOf } from "../spend.js";
|
|
63
64
|
import { verifiedAuthor } from "../store.js";
|
|
64
65
|
import { runCostWords } from "../summary.js";
|
|
65
66
|
import { requestTaskStop, resumeTaskStop } from "../task-control.js";
|
|
66
67
|
import { templateByName } from "../templates.js";
|
|
67
68
|
import { TEXT_LIMITS } from "../text-limits.js";
|
|
68
|
-
import { agentQuestion, answerAgentQuestion, markAgentSeen } from "../agent-board.js";
|
|
69
|
+
import { AGENT_VIEW_ID, agentQuestion, answerAgentQuestion, holdOwnAgentView, markAgentSeen, releaseOwnAgentView } from "../agent-board.js";
|
|
69
70
|
import { WorkIndexCursorError, workIndexPage } from "../work-index.js";
|
|
70
71
|
import { buildProgressOf, parseWorkView } from "../workspace-ui.js";
|
|
71
72
|
import { requestContext } from "./request-context.js";
|
|
@@ -573,10 +574,8 @@ export function createTasksHandlers(runtime) {
|
|
|
573
574
|
return sendScreen(response, 409, screen("Result changed", html `<h1>Result changed</h1><p>This acceptance link no longer matches the current result. Review the current task before accepting.</p><p class="refusal-back"><a class="button-link" href="/review">Review results</a></p>`, { chrome: chromeFor(project, "runs") }));
|
|
574
575
|
}
|
|
575
576
|
const selected = picked !== null ? picked.selected : selectedRow === null ? null : reviewCockpitViewOf(selectedRow, who, now);
|
|
576
|
-
// Opening a run's own result page is viewing it:
|
|
577
|
-
//
|
|
578
|
-
if (wantedId !== null && selected?.run != null && store.getRun(selected.run.id)?.finishedAt != null)
|
|
579
|
-
markAgentSeen(store, who.name, selected.run.id, now);
|
|
577
|
+
// Opening a run's own result page is viewing it: the shell says so with a keyed POST /agents/seen (easy-start N1:
|
|
578
|
+
// a page load never writes), which leaves this person's unread Done once the run has finished.
|
|
580
579
|
const reviewPage = reviewCockpitPage(chromeFor(wantedId === null ? project : chosenProject, "runs"), {
|
|
581
580
|
queue: ranked,
|
|
582
581
|
queueCap: REVIEW_QUEUE_CAP,
|
|
@@ -1004,7 +1003,11 @@ export function createTasksHandlers(runtime) {
|
|
|
1004
1003
|
}
|
|
1005
1004
|
}
|
|
1006
1005
|
const id = (body.get("id") ?? "").trim();
|
|
1007
|
-
|
|
1006
|
+
// The New task box stops at a title's length (render-new-task.ts): the title is all the planner reads of a no-goal
|
|
1007
|
+
// filing. A direct POST longer than that is cut at a whole word, with an ellipsis (task-text.ts titleWithin), as a
|
|
1008
|
+
// belt; a refusal keeps the person's own words (the draft below).
|
|
1009
|
+
const typed = body.get("title") ?? "";
|
|
1010
|
+
const title = titleWithin(typed);
|
|
1008
1011
|
// The EFFECTIVE placement (repo onboarding, findings 15/35): the
|
|
1009
1012
|
// trimmed, nonempty posted repo, else the open project — an empty
|
|
1010
1013
|
// input falls through correctly. In root mode the effective path is
|
|
@@ -1021,7 +1024,7 @@ export function createTasksHandlers(runtime) {
|
|
|
1021
1024
|
// direct POST with no project open must not mint an unplaced task
|
|
1022
1025
|
// under a ceiling. Unscoped mode keeps its historic unplaced filings.
|
|
1023
1026
|
// A refusal from here on is the add page again with the person's own words (the old task list kept them too).
|
|
1024
|
-
const draft = { title, goal: body.get("goal") ?? "", not: body.get("not") ?? "", touches: body.get("touches") ?? "", acceptance: body.get("acceptance") ?? "", values: body.sent };
|
|
1027
|
+
const draft = { title: typed, goal: body.get("goal") ?? "", not: body.get("not") ?? "", touches: body.get("touches") ?? "", acceptance: body.get("acceptance") ?? "", values: body.sent };
|
|
1025
1028
|
if (!unscopedMode && effective === "") {
|
|
1026
1029
|
return newTaskScreen(ctx, 400, "name a repository — no project is open, so the task must say where it belongs", draft);
|
|
1027
1030
|
}
|
|
@@ -2473,7 +2476,9 @@ export function createTasksHandlers(runtime) {
|
|
|
2473
2476
|
}
|
|
2474
2477
|
// An empty password stands for an explicit identity-provider re-check not yet spent, or (cockpit B2) a password
|
|
2475
2478
|
// remembered on this session; approve() decides whether these terms may stand on a remembered one.
|
|
2476
|
-
|
|
2479
|
+
// Easy-start C5: a laptop's local owner approves eligible work with no password (approve() proves it again).
|
|
2480
|
+
if (token === "" && !passwordlessSessionFor(store, who.name) && !ssoNowAvailable(who.name) && withFreshness("approve-standard", () => freshnessFor(who.name)) === null
|
|
2481
|
+
&& !(who.authority === "local-owner" && localApprovalEligible(store, who.name, taskId).ok)) {
|
|
2477
2482
|
return approvalProblem("approval requires your password, typed again", 400);
|
|
2478
2483
|
}
|
|
2479
2484
|
const scopeRow = store.getScope(taskId);
|
|
@@ -3087,6 +3092,7 @@ export function createTasksHandlers(runtime) {
|
|
|
3087
3092
|
}
|
|
3088
3093
|
const registrations = handlersOf("tasks", {
|
|
3089
3094
|
...tidyHandlers,
|
|
3095
|
+
...createAgentSignalHandlers(runtime),
|
|
3090
3096
|
"code.page": showCode("workspace"),
|
|
3091
3097
|
"code.session": showCode("session"),
|
|
3092
3098
|
"code.state": showCode("state"),
|
|
@@ -3261,3 +3267,47 @@ export function retiredTasksAddress(route, url) {
|
|
|
3261
3267
|
}
|
|
3262
3268
|
/** How many cards a Board column shows; the column's count covers them all and its "View all" opens the list. */
|
|
3263
3269
|
export const BOARD_COLUMN_LIMIT = 40;
|
|
3270
|
+
/**
|
|
3271
|
+
* Easy-start C1 (N1): the two writes a page load used to make, as keyed POSTs the shell sends (wave 1c wires the
|
|
3272
|
+
* browser). `agents.seen`: a finished result this person opened leaves their unread Done. `agents.watch`: hold (on each
|
|
3273
|
+
* keep-alive) or release (on unload) the short lease that quiets chat pings while this person's page shows the agent.
|
|
3274
|
+
* Each re-proves the run is in the person's projects; a reply carries nothing else.
|
|
3275
|
+
*/
|
|
3276
|
+
export function createAgentSignalHandlers(runtime) {
|
|
3277
|
+
const { store, runVisible } = runtime;
|
|
3278
|
+
const answer = (response, status, ok) => respond(response, status, "application/json", JSON.stringify({ ok }));
|
|
3279
|
+
const visibleRun = (who, raw) => {
|
|
3280
|
+
const id = raw !== null && /^[1-9][0-9]{0,14}$/.test(raw) ? Number(raw) : null;
|
|
3281
|
+
const run = id === null ? null : store.getRun(id);
|
|
3282
|
+
const repo = run === null ? null : store.refForId(run.taskRef)?.repo ?? null;
|
|
3283
|
+
return run === null || !runVisible(run) || !store.accountCanAccess(who.name, repo) ? null : run;
|
|
3284
|
+
};
|
|
3285
|
+
async function agentSeen(ctx) {
|
|
3286
|
+
const { who, response, now, posted } = ctx;
|
|
3287
|
+
const run = visibleRun(who, readForm(posted, CONSOLE_FORMS.agentSeen).get("run"));
|
|
3288
|
+
if (run === null)
|
|
3289
|
+
return answer(response, 404, false);
|
|
3290
|
+
// Only once it finished: a page shown while it ran saw no finished version (cockpit §3).
|
|
3291
|
+
if (run.finishedAt === null)
|
|
3292
|
+
return answer(response, 409, false);
|
|
3293
|
+
markAgentSeen(store, who.name, run.id, now);
|
|
3294
|
+
return answer(response, 200, true);
|
|
3295
|
+
}
|
|
3296
|
+
async function agentWatch(ctx) {
|
|
3297
|
+
const { who, response, now, posted } = ctx;
|
|
3298
|
+
const body = readForm(posted, CONSOLE_FORMS.agentWatch);
|
|
3299
|
+
const view = body.get("view") ?? "", op = body.get("op");
|
|
3300
|
+
if (!AGENT_VIEW_ID.test(view) || (op !== "hold" && op !== "release"))
|
|
3301
|
+
return answer(response, 400, false);
|
|
3302
|
+
if (op === "release") {
|
|
3303
|
+
releaseOwnAgentView(store, view, who.name, now);
|
|
3304
|
+
return answer(response, 200, true);
|
|
3305
|
+
}
|
|
3306
|
+
const run = visibleRun(who, body.get("run"));
|
|
3307
|
+
if (run === null)
|
|
3308
|
+
return answer(response, 404, false);
|
|
3309
|
+
const held = holdOwnAgentView(store, view, who.name, run.id, now);
|
|
3310
|
+
return answer(response, held ? 200 : 409, held);
|
|
3311
|
+
}
|
|
3312
|
+
return { "agents.seen": agentSeen, "agents.watch": agentWatch };
|
|
3313
|
+
}
|
package/dist/server/terminals.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import { randomBytes } from "node:crypto";
|
|
18
18
|
import { STATUS_CODES } from "node:http";
|
|
19
19
|
import { dirname, join } from "node:path";
|
|
20
|
+
import { KEY_PROTOCOL } from "./mutation-key.js";
|
|
20
21
|
import { WebSocketServer } from "ws";
|
|
21
22
|
import { readForm, CONSOLE_FORMS } from "../contracts/console-api.js";
|
|
22
23
|
import { html, htmlString } from "../html.js";
|
|
@@ -55,9 +56,14 @@ export function createTerminalHandlers(runtime, deps) {
|
|
|
55
56
|
const transportOfRequest = (request) => transportOf({ peer: request.socket.remoteAddress, joinSource: deps.forwardedSourceOf(request),
|
|
56
57
|
forwardedProto: request.headers["x-forwarded-proto"], forwarded: request.headers["forwarded"] });
|
|
57
58
|
const sockets = new Set();
|
|
58
|
-
|
|
59
|
+
// Easy-start C2: a page offers `toolroll` and, on a protected loopback install, `toolroll.key.<key>`; only `toolroll` is
|
|
60
|
+
// ever selected, so the key is never echoed.
|
|
61
|
+
const wss = new WebSocketServer({ noServer: true, maxPayload: MAX_FRAME_BYTES, perMessageDeflate: false,
|
|
62
|
+
handleProtocols: protocols => protocols.has(KEY_PROTOCOL) ? KEY_PROTOCOL : false });
|
|
59
63
|
/** An operator signed in with a browser, of the generation they signed in with; else null. */
|
|
60
|
-
const operatorOf = (who) =>
|
|
64
|
+
const operatorOf = (who) =>
|
|
65
|
+
// Easy-start C1: never a local owner's session (a laptop's passwordless owner), for a new terminal or an existing one.
|
|
66
|
+
who?.via === "cookie" && who.authority === "full" && who.role === "approver" && !store.isDemo() && codingActorAllowed({ name: who.name, generation: who.session.generation }) ? who : null;
|
|
61
67
|
/** The project a terminal works in is one the coding workspace admits for this person, now. */
|
|
62
68
|
const projectAllowed = (name, repo) => rowVisible(liveCeiling(), repo) && store.accountCanAccess(name, repo) && codingProjectAllowed(repo);
|
|
63
69
|
const owned = (who, terminal) => terminal !== null && terminal.owner === who.name && terminal.generation === who.session.generation;
|
|
@@ -232,6 +238,13 @@ export function createTerminalHandlers(runtime, deps) {
|
|
|
232
238
|
refuseUpgrade(socket, 401, "Sign in as an installation-wide operator.");
|
|
233
239
|
return true;
|
|
234
240
|
}
|
|
241
|
+
// Easy-start C2: typing into a terminal is a mutation: on a protected loopback install the socket offers the session's
|
|
242
|
+
// key (its protocol list), which a cookie holder on another port can't read.
|
|
243
|
+
const keyed = runtime.browserKeys.check(who, request, null);
|
|
244
|
+
if (keyed !== null) {
|
|
245
|
+
refuseUpgrade(socket, keyed.status, keyed.message);
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
235
248
|
const terminal = deps.host?.get(id) ?? null;
|
|
236
249
|
if (!owned(who, terminal) || !projectAllowed(who.name, terminal.repo)) {
|
|
237
250
|
refuseUpgrade(socket, 404, "No such terminal.");
|
package/dist/server/welcome.d.ts
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import { type
|
|
3
|
-
import type { ServerRuntime } from "./runtime.js";
|
|
1
|
+
import { type WelcomeModel } from "../first-run.js";
|
|
2
|
+
import { type Html } from "../html.js";
|
|
4
3
|
import { type Chrome, type Screen } from "./chrome.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
import type { ServerRuntime } from "./runtime.js";
|
|
5
|
+
/** Why the sample project wasn't added, as a fixed key in the address (never words from it). */
|
|
6
|
+
export declare const SAMPLE_PROBLEMS: {
|
|
7
|
+
readonly "no-root": "There's no project folder to put the sample in. Add a folder instead.";
|
|
8
|
+
readonly exists: "A folder named toolroll-sample is already there and wasn't made here, so nothing was changed.";
|
|
9
|
+
readonly failed: "The sample project couldn't be made. Nothing was added. Try again, or add a folder instead.";
|
|
10
|
+
readonly "not-added": "The sample project was made but couldn't be added. Try again, or add it from Projects.";
|
|
10
11
|
};
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
export type SampleProblem = keyof typeof SAMPLE_PROBLEMS;
|
|
13
|
+
/** The page body for one model, and the screen's one primary (for the phone dock). `problem`: why the sample project
|
|
14
|
+
* wasn't added, from the address. `canAdd`: this person may add projects here. */
|
|
15
|
+
export declare function welcomeBody(model: WelcomeModel, options: {
|
|
16
|
+
problem?: SampleProblem | null;
|
|
17
|
+
canAdd: boolean;
|
|
18
|
+
}): {
|
|
19
|
+
body: Html;
|
|
20
|
+
primary: Html | null;
|
|
18
21
|
};
|
|
19
|
-
export declare function welcomePage(chrome: Chrome,
|
|
22
|
+
export declare function welcomePage(chrome: Chrome, model: WelcomeModel, options: {
|
|
23
|
+
problem?: SampleProblem | null;
|
|
24
|
+
canAdd: boolean;
|
|
25
|
+
}): Screen;
|
|
20
26
|
export declare function createWelcomeHandlers(runtime: ServerRuntime): {
|
|
21
27
|
registrations: import("./handler-registry.js").Registration[];
|
|
22
28
|
};
|