toolroll 0.9.82 → 0.9.84
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 +41 -0
- package/README.md +4 -3
- package/dist/assignment-ui.js +5 -1
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +22 -22
- package/dist/browser-crew.js +5 -1
- package/dist/browser-workspace.d.ts +7 -0
- package/dist/contracts/console-api.d.ts +2 -1
- package/dist/contracts/console-api.js +5 -1
- package/dist/control-setup.d.ts +9 -0
- package/dist/control-setup.js +31 -4
- package/dist/control-ui.d.ts +7 -1
- package/dist/control-ui.js +3 -2
- package/dist/dispatch.d.ts +15 -0
- package/dist/dispatch.js +26 -4
- package/dist/first-run.d.ts +5 -2
- package/dist/first-run.js +13 -2
- package/dist/first-tasks-child.d.ts +1 -0
- package/dist/first-tasks-child.js +11 -0
- package/dist/folder-look.d.ts +110 -0
- package/dist/folder-look.js +306 -0
- package/dist/guides.js +4 -3
- package/dist/learning-child.d.ts +1 -0
- package/dist/learning-child.js +34 -0
- package/dist/learning-look.d.ts +25 -0
- package/dist/learning-look.js +72 -0
- package/dist/model-catalog.d.ts +5 -0
- package/dist/model-catalog.js +11 -2
- package/dist/model-choices.d.ts +188 -0
- package/dist/model-choices.js +405 -0
- package/dist/model-names.d.ts +6 -0
- package/dist/model-names.js +34 -0
- package/dist/models-ui.d.ts +69 -5
- package/dist/models-ui.js +53 -10
- package/dist/needs-you.d.ts +8 -0
- package/dist/needs-you.js +8 -0
- package/dist/operate.d.ts +53 -0
- package/dist/operate.js +277 -49
- package/dist/project-admission.d.ts +4 -1
- package/dist/project-admission.js +23 -2
- package/dist/project-identity.d.ts +44 -0
- package/dist/project-identity.js +153 -0
- package/dist/project-knowledge.d.ts +6 -6
- package/dist/project-tools.d.ts +4 -1
- package/dist/project-tools.js +7 -2
- package/dist/project.d.ts +4 -1
- package/dist/project.js +11 -4
- package/dist/repos.d.ts +7 -0
- package/dist/repos.js +57 -0
- package/dist/serve.js +91 -17
- package/dist/server/chat.js +5 -4
- package/dist/server/chrome.d.ts +3 -0
- package/dist/server/chrome.js +5 -2
- package/dist/server/console.css +5 -2
- package/dist/server/flows.js +13 -3
- package/dist/server/guards.d.ts +4 -0
- package/dist/server/guards.js +12 -3
- package/dist/server/http.d.ts +12 -0
- package/dist/server/http.js +9 -0
- package/dist/server/pages.js +102 -92
- package/dist/server/people-tokens.js +40 -20
- package/dist/server/render-pages.d.ts +12 -16
- package/dist/server/render-pages.js +30 -69
- package/dist/server/render-people.d.ts +14 -2
- package/dist/server/render-people.js +35 -12
- package/dist/server/render-settings.js +1 -0
- package/dist/server/render-tasks.js +7 -14
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/route-table.js +8 -1
- package/dist/server/runtime.d.ts +4 -0
- package/dist/server/runtime.js +1 -0
- package/dist/server/settings.d.ts +58 -1
- package/dist/server/settings.js +211 -71
- package/dist/server/signup-gate.d.ts +9 -0
- package/dist/server/signup-gate.js +40 -0
- package/dist/server/tasks.js +19 -5
- package/dist/settings-kit-script.d.ts +21 -0
- package/dist/settings-kit-script.js +67 -0
- package/dist/settings-kit.d.ts +137 -0
- package/dist/settings-kit.js +186 -0
- package/dist/settings-proofs.d.ts +64 -0
- package/dist/settings-proofs.js +101 -0
- package/dist/setup-guide.js +4 -2
- package/dist/spend-report.d.ts +87 -0
- package/dist/spend-report.js +265 -0
- package/dist/spend-ui.d.ts +29 -8
- package/dist/spend-ui.js +74 -39
- package/dist/task-sizing.d.ts +5 -3
- package/dist/task-sizing.js +4 -4
- package/dist/up-processes.d.ts +13 -0
- package/dist/up-processes.js +6 -2
- package/package.json +1 -1
package/dist/serve.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { secretFolder } from "./tool-secret-store.js";
|
|
2
2
|
import { acceptedIdentities, recoverLearning } from "./project-learning.js";
|
|
3
|
+
import { syncProjectIdentities } from "./project-identity.js";
|
|
4
|
+
import { firstTasksOffThread, recoverLearningOffThread } from "./learning-look.js";
|
|
3
5
|
import { loadOrCreateVapidKeys } from "./push.js";
|
|
4
6
|
import { checkoutPlan } from "./checkout-cleanup.js";
|
|
5
7
|
import { latestReleaseFor } from "./server/render-settings.js";
|
|
@@ -38,7 +40,8 @@ import { asReadOnlyRequest } from './read-only-request.js';
|
|
|
38
40
|
import { connectionsOf, localConnectOf, oneClickOf } from "./mcp-connect.js";
|
|
39
41
|
import { MOBILE_VIEWPORT_SCRIPT } from "./mobile-viewport.js";
|
|
40
42
|
import { projectAuthority } from "./project-access.js";
|
|
41
|
-
import { discoverTools, projectToolsOf, secretsSetFor, TOOL_CATALOG, toolSecretStorage, toolStanding } from "./project-tools.js";
|
|
43
|
+
import { discoverTools, projectToolsOf, savedProjectMcp, secretsSetFor, TOOL_CATALOG, toolSecretStorage, toolStanding } from "./project-tools.js";
|
|
44
|
+
import { lookProjectMcp } from "./folder-look.js";
|
|
42
45
|
import { toolRefusal } from "./policy.js";
|
|
43
46
|
import { isEdgeAddress, refuseEdge } from "./server/edge-refusal.js";
|
|
44
47
|
import { announcePasswordBearer, isPasswordBearer, PASSWORD_BEARER_REFUSED, passwordBearerAccepted } from "./password-bearer.js";
|
|
@@ -143,6 +146,7 @@ import { createChatHandlers } from './server/chat.js';
|
|
|
143
146
|
import { createLiveHandlers } from './server/live.js';
|
|
144
147
|
import { createTerminalHandlers, terminalHostFor } from './server/terminals.js';
|
|
145
148
|
import { passwordlessOn } from './passwordless.js';
|
|
149
|
+
import { keepTypedFields, openProjectFor, settingsCrumbs } from './settings-kit.js';
|
|
146
150
|
import { createSweepHandlers } from './server/sweeps.js';
|
|
147
151
|
import { createFlowsHandlers } from './server/flows.js';
|
|
148
152
|
import { createPagesHandlers } from './server/pages.js';
|
|
@@ -166,6 +170,9 @@ import { loadConsoleUrl } from "./webhooks.js";
|
|
|
166
170
|
import { reviewFactsOf } from "./workspace-ui.js";
|
|
167
171
|
import { WorktreePool } from "./worktree.js";
|
|
168
172
|
export function createDecisionServer(options) {
|
|
173
|
+
// Identities come from what the console's start resolved off its thread (startConsole); a path that didn't answer
|
|
174
|
+
// stays unresolved, kept restrictive. Without them (embedders, tests) the old synchronous reads apply.
|
|
175
|
+
const identities = options.identities ?? syncProjectIdentities();
|
|
169
176
|
const { projectFamilyPeek, familiesInView, familyTasksInView, revisionDestination, runIsLive, runIsTaskResult, revisionViewOf, taskViewData, routeViewOf, taskChatFocus, pullRequestTargetOf, failureOf, explainAttempt, taskScreen, armTaskResume, planViewOf, planContractViewOf, revisionDocOf, revisionLedgerOf, resultDetailOf } = createTaskViews({
|
|
170
177
|
get store() { return store; },
|
|
171
178
|
get projectCounts() { return projectCounts; },
|
|
@@ -188,12 +195,13 @@ export function createDecisionServer(options) {
|
|
|
188
195
|
const { authenticateApprover, authenticateCredential, authorizeMutation, projectOf, projectRequestAllowed, identify, lookupSession, restricted, visible, admissionList, codingActorAllowed, codingProjectAllowed, workAccess, familyOf, taskRepoOf, runVisible } = createSharedGuards({
|
|
189
196
|
get store() { return store; },
|
|
190
197
|
get liveCeiling() { return liveCeiling; },
|
|
198
|
+
identities,
|
|
191
199
|
get codingProjects() { return codingProjects; },
|
|
192
200
|
get unscopedMode() { return unscopedMode; },
|
|
193
201
|
get managedRepos() { return managedRepos; },
|
|
194
202
|
get ceiling() { return ceiling; },
|
|
195
203
|
get allowedHost() { return allowedHost; },
|
|
196
|
-
get defaultProject() { return defaultProject; },
|
|
204
|
+
get defaultProject() { return defaultProject(); },
|
|
197
205
|
get actionTarget() { return actionTarget; },
|
|
198
206
|
get team() { return team; },
|
|
199
207
|
get joinSourceOf() { return joinSourceOf; },
|
|
@@ -446,20 +454,21 @@ export function createDecisionServer(options) {
|
|
|
446
454
|
};
|
|
447
455
|
let joinGlobal = { tokens: 30, refilledAt: Date.now() };
|
|
448
456
|
const approvalNonces = new Map();
|
|
449
|
-
//
|
|
450
|
-
// Native additions come only from the controller after its admission checks.
|
|
451
|
-
const { ceiling, unresolved: unresolvedRepos } =
|
|
452
|
-
/** The project every fresh session opens with: the sole configured repo, else none. */
|
|
453
|
-
const defaultProject = ceiling.repos.length === 1 && ceiling.roots.length === 0 ? ceiling.repos[0] : null;
|
|
457
|
+
// Startup authority: the configured repos and roots as their identities say (startupCeiling). Missing or unresolved
|
|
458
|
+
// paths remain restrictive. Native additions come only from the controller after its admission checks.
|
|
459
|
+
const { ceiling, unresolved: unresolvedRepos, defaultProject, configured, configuredPaths } = startupCeiling([...(options.repo === undefined ? [] : [options.repo]), ...(options.repos ?? [])], options.projectRoots ?? [], options.identities === undefined ? undefined : identities, identities.strict);
|
|
454
460
|
/** No ceiling configured at all: the legacy trust-everything mode, named. */
|
|
455
|
-
|
|
461
|
+
// An `up` console (it is handed its admitted set) is never unscoped: with every saved project away it is empty.
|
|
462
|
+
// Judged by what was configured, never by what has resolved so far: a console whose folders haven't answered yet is
|
|
463
|
+
// scoped and empty, not unscoped.
|
|
464
|
+
const unscopedMode = !configured && options.admittedRepos === undefined;
|
|
456
465
|
/** Per-row visibility under the ceiling — the authorization question for reads. */
|
|
457
466
|
/** The exact repositories this console serves now: `up`'s live admitted set when it supplies one, else startup's. */
|
|
458
467
|
const exactRepos = () => options.admittedRepos?.() ?? ceiling.repos;
|
|
459
468
|
const liveCeiling = () => {
|
|
460
469
|
const repos = [...exactRepos(), ...(options.additionalProjectRepos?.() ?? [])];
|
|
461
470
|
// An admitted set that is empty for now is an empty ceiling, never the legacy unscoped mode.
|
|
462
|
-
return { repos: repos.length === 0 && ceiling.roots.length === 0 && !unscopedMode ? [NO_PROJECT] : repos, roots: ceiling.roots };
|
|
471
|
+
return { repos: repos.length === 0 && ceiling.roots.length === 0 && !unscopedMode ? [NO_PROJECT] : repos, roots: ceiling.roots, canon: identities.strict };
|
|
463
472
|
};
|
|
464
473
|
/** The explicit, currently proved project list. The callback is supplied
|
|
465
474
|
* only by `up`, after it has independently checked git-ness and the root
|
|
@@ -479,7 +488,7 @@ export function createDecisionServer(options) {
|
|
|
479
488
|
const repos = [];
|
|
480
489
|
const gone = goneRepos();
|
|
481
490
|
for (const path of [...exactRepos(), ...(options.currentRepos?.() ?? [])]) {
|
|
482
|
-
const canonical =
|
|
491
|
+
const canonical = identities.hot(path);
|
|
483
492
|
if (seen.has(canonical) || !visible(canonical) || gone.has(canonical))
|
|
484
493
|
continue;
|
|
485
494
|
seen.add(canonical);
|
|
@@ -568,7 +577,8 @@ export function createDecisionServer(options) {
|
|
|
568
577
|
// A service that connects by signing in (or an app's own Connect) is offered only that way.
|
|
569
578
|
catalog: TOOL_CATALOG.filter(one => !names.has(one.name) && oneClickOf(one.name) === null && localConnectOf(one.name) === null),
|
|
570
579
|
connections: connectionsOf(store, repo, toolHome), kit, wanted,
|
|
571
|
-
|
|
580
|
+
// The project's own .mcp.json as the background pass last read it off the console's thread (never read here).
|
|
581
|
+
found: discoverTools(repo, codex, toolHome, process.env, options.identities === undefined ? undefined : savedProjectMcp.get(repo) ?? null).filter(one => !names.has(one.spec.name)),
|
|
572
582
|
storage: toolSecretStorage(),
|
|
573
583
|
};
|
|
574
584
|
};
|
|
@@ -879,6 +889,7 @@ export function createDecisionServer(options) {
|
|
|
879
889
|
browser: who.via === 'cookie',
|
|
880
890
|
// Passwordless (read once for rendering; every act proves it again at the act): ceremony fields render as nothing.
|
|
881
891
|
passwordless: who.via === 'cookie' && passwordlessOn(store),
|
|
892
|
+
openProject: who.via === 'cookie' ? who.session.project : null,
|
|
882
893
|
// v100: signed in with the identity provider: its label, and whether it checked them recently enough to stand in for a password.
|
|
883
894
|
// Cockpit B2: "fresh" is an explicit re-check made on request and not yet spent (read live; one step-up uses it).
|
|
884
895
|
sso: who.via === 'cookie' && who.session.sso !== undefined ? { label: ssoSettings()?.label ?? "your identity provider",
|
|
@@ -1027,7 +1038,7 @@ export function createDecisionServer(options) {
|
|
|
1027
1038
|
}
|
|
1028
1039
|
const known = new Set(["/recipes/prepare", "/recipes/preview", "/recipes/import", "/recipes/save", "/recipes/launch", "/tasks/add", "/queue/move", "/queue/note", "/people/invite", "/people/invite-revoke", "/people/revoke", "/people/projects", "/projects/select", "/projects/open", "/mode/confirm", "/mode/sign", "/mode/revoke", "/settings/chat-approval/save", "/settings/chat-approval/off", "/settings/project/live-view"]);
|
|
1029
1040
|
const placed = url.pathname === "/tasks/add" ? body?.get("repo")?.trim() : null;
|
|
1030
|
-
return { repo: url.pathname.startsWith("/people/") ? null : placed ?
|
|
1041
|
+
return { repo: url.pathname.startsWith("/people/") ? null : placed ? identities.strict(placed) ?? placed : projectOf(who, request) ?? null, taskId: null, runId: null,
|
|
1031
1042
|
action: known.has(url.pathname) ? url.pathname.slice(1).replaceAll("/", " ") : "console request" };
|
|
1032
1043
|
}
|
|
1033
1044
|
async function handleGet(route, url, who, request, response) {
|
|
@@ -1317,8 +1328,8 @@ export function createDecisionServer(options) {
|
|
|
1317
1328
|
return { ok: false, code: "demo", why: "this is a demo database — chat cannot contact an external model" };
|
|
1318
1329
|
if (unscopedMode)
|
|
1319
1330
|
return { ok: false, code: "unscoped", why: "chat needs at least one added project" };
|
|
1320
|
-
if (options.currentRepos === undefined && unresolvedRepos.length > 0) {
|
|
1321
|
-
return { ok: false, code: "unresolved", why: "a configured project
|
|
1331
|
+
if (options.currentRepos === undefined && unresolvedRepos().length > 0) {
|
|
1332
|
+
return { ok: false, code: "unresolved", why: "a configured project folder hasn't been found yet — chat runs once it answers; if its path is wrong, fix it and restart" };
|
|
1322
1333
|
}
|
|
1323
1334
|
if (managedRepos().length === 0)
|
|
1324
1335
|
return { ok: false, code: "empty", why: "add a project first — chat will include it automatically" };
|
|
@@ -1473,13 +1484,20 @@ export function createDecisionServer(options) {
|
|
|
1473
1484
|
const ssoFacts = requestContext.getStore()?.sso;
|
|
1474
1485
|
if (ssoFacts !== undefined) {
|
|
1475
1486
|
const back = requestContext.getStore()?.returnTo ?? "/";
|
|
1476
|
-
|
|
1487
|
+
// A typed-only proof (settings-kit.ts) stays a password field: its handler takes nothing else.
|
|
1488
|
+
s = { ...s, body: keepTypedFields(s.body, ssoStepUps, ssoFacts, back), ...(s.workspace?.pageHtml == null ? {} : { workspace: { ...s.workspace, pageHtml: keepTypedFields(s.workspace.pageHtml, ssoStepUps, ssoFacts, back) } }) };
|
|
1477
1489
|
if (s.workspace?.view?.kind === "flow")
|
|
1478
1490
|
s = { ...s, workspace: { ...s.workspace, view: { ...s.workspace.view, stepUp: { label: ssoFacts.label, fresh: ssoFacts.fresh, confirmHref: `/login/sso?reauth=1&return=${encodeURIComponent(back)}` } } } };
|
|
1479
1491
|
}
|
|
1480
1492
|
// D5: team chat (the central team service) is deprecated; /chat?team=1 still opens this release, but nothing links to it.
|
|
1481
1493
|
const sensitive = authored || s.forceSensitive === true;
|
|
1482
1494
|
const requestFacts = requestContext.getStore();
|
|
1495
|
+
// The settings frame (settings-kit.ts): its breadcrumb, for the shell and the workspace alike.
|
|
1496
|
+
if (s.chrome !== undefined && s.chrome.crumbs === undefined) {
|
|
1497
|
+
const crumbs = settingsCrumbs(new URL(requestFacts?.returnTo ?? '/', 'http://standing-orders.local').pathname, s.title, htmlString(s.body));
|
|
1498
|
+
if (crumbs !== undefined)
|
|
1499
|
+
s = { ...s, chrome: { ...s.chrome, crumbs } };
|
|
1500
|
+
}
|
|
1483
1501
|
if (requestFacts?.browser && s.chrome && !s.forceSensitive && (requestFacts.workspaceRead || browserAssetsAvailable())) {
|
|
1484
1502
|
const path = new URL(requestFacts.returnTo, 'http://standing-orders.local');
|
|
1485
1503
|
path.searchParams.delete('format');
|
|
@@ -1554,6 +1572,10 @@ export function createDecisionServer(options) {
|
|
|
1554
1572
|
const request = requestFacts.workspaceRequest;
|
|
1555
1573
|
const workspace = {
|
|
1556
1574
|
version: 1, path: currentPath, title: s.title, user: requestFacts.actor ?? '', csrf: requestFacts.csrf, sensitive,
|
|
1575
|
+
// The project picker names the open project; the settings frame carries its breadcrumb.
|
|
1576
|
+
// Only a project this person may see now: never the raw saved path (settings-kit.ts openProjectFor).
|
|
1577
|
+
openProject: openProjectFor(requestFacts.openProject ?? null, s.chrome.project, s.chrome.projects ?? []),
|
|
1578
|
+
...(s.chrome.crumbs === undefined ? {} : { crumbs: s.chrome.crumbs }),
|
|
1557
1579
|
...(requestFacts.passwordless === true ? { passwordless: true } : {}),
|
|
1558
1580
|
...(requestFacts.actor ? { leadName: leadNameOf(store, requestFacts.actor) } : {}),
|
|
1559
1581
|
refreshUrl: path.pathname + path.search,
|
|
@@ -1955,6 +1977,8 @@ export function createDecisionServer(options) {
|
|
|
1955
1977
|
for (const provider of PROVIDER_IDS.filter(isKeyedProvider).filter(one => one !== "claude" && one !== "codex"))
|
|
1956
1978
|
waits.push(connectionCheck(provider).catch(() => undefined));
|
|
1957
1979
|
}
|
|
1980
|
+
// Project identities, resolved off the thread: what pages asked about, and answers gone stale.
|
|
1981
|
+
waits.push(identities.refresh([...configuredPaths, ...exactRepos(), ...(options.currentRepos?.() ?? []), ...store.listProjects().map(one => one.path), ...store.knownRepos()]));
|
|
1958
1982
|
integrations.refreshIfDue();
|
|
1959
1983
|
const sso = ssoSettings();
|
|
1960
1984
|
if (sso !== null && (ssoProvider === null || ssoProvider.issuer !== sso.issuer || Date.now() - ssoProvider.at >= 10 * 60_000))
|
|
@@ -1991,13 +2015,22 @@ export function createDecisionServer(options) {
|
|
|
1991
2015
|
continue;
|
|
1992
2016
|
const reading = { until: Date.now() + 10 * 60_000, found: entry?.found ?? null, reading: true };
|
|
1993
2017
|
firstTasks.set(repo, reading);
|
|
1994
|
-
|
|
2018
|
+
// A test's runner reads in-process; otherwise in a process of its own (learning-look.ts), so the hardened
|
|
2019
|
+
// git's synchronous reads of the project's folder never hold the console's thread.
|
|
2020
|
+
waits.push((options.firstTaskRunner !== undefined ? findFirstTasks(repo, options.firstTaskRunner) : firstTasksOffThread(repo)).then(found => { reading.found = found; }, () => { }).finally(() => { reading.reading = false; }));
|
|
1995
2021
|
}
|
|
1996
2022
|
}
|
|
1997
2023
|
// Learning recovered from reviewed runs (and a project's saved rows carried to its current identity), which
|
|
1998
2024
|
// Settings → Learning and Knowledge used to do as they opened.
|
|
1999
2025
|
if (lastChecks.due("learning", 10 * 60_000)) {
|
|
2000
2026
|
waits.push(lastChecks.run("learning", async () => {
|
|
2027
|
+
// Its reads of each project's Git folder are synchronous, so with a database file they run in a process of
|
|
2028
|
+
// their own (learning-look.ts): a folder that doesn't answer costs its own recovery, never the console. An
|
|
2029
|
+
// in-memory database (a test) can't be shared, so it is read here.
|
|
2030
|
+
if (databaseFile !== null) {
|
|
2031
|
+
await recoverLearningOffThread({ databaseFile, evidenceRoot, repos: managedRepos() }, options.learningRunner === undefined ? {} : { runner: options.learningRunner });
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2001
2034
|
for (const repo of managedRepos()) {
|
|
2002
2035
|
try {
|
|
2003
2036
|
acceptedIdentities(store, repo);
|
|
@@ -2012,6 +2045,11 @@ export function createDecisionServer(options) {
|
|
|
2012
2045
|
}
|
|
2013
2046
|
// What Codex has configured, per project, for Settings → Tools to offer.
|
|
2014
2047
|
for (const repo of managedRepos()) {
|
|
2048
|
+
// The project's own .mcp.json, read off the console's thread (folder-look.ts) for the Tools page and the lead.
|
|
2049
|
+
if (options.identities !== undefined && lastChecks.due(`project-mcp:${repo}`, 10 * 60_000)) {
|
|
2050
|
+
waits.push(lastChecks.run(`project-mcp:${repo}`, async () => { const looked = await lookProjectMcp(repo); if (looked.ok)
|
|
2051
|
+
savedProjectMcp.set(repo, looked.value); return looked.ok; }));
|
|
2052
|
+
}
|
|
2015
2053
|
if (lastChecks.due(`codex-tools:${repo}`, 10 * 60_000))
|
|
2016
2054
|
waits.push(lastChecks.run(`codex-tools:${repo}`, () => codexServers(repo)));
|
|
2017
2055
|
}
|
|
@@ -2178,7 +2216,7 @@ export function createDecisionServer(options) {
|
|
|
2178
2216
|
get providerFor() { return providerFor; },
|
|
2179
2217
|
get providerKnown() { return providerKnown; },
|
|
2180
2218
|
get connectVisits() { return connectVisits; },
|
|
2181
|
-
get defaultProject() { return defaultProject; },
|
|
2219
|
+
get defaultProject() { return defaultProject(); },
|
|
2182
2220
|
get googleVisits() { return googleVisits; },
|
|
2183
2221
|
get setupAttemptsLeft() { return setupAttemptsLeft; },
|
|
2184
2222
|
set setupAttemptsLeft(value) { setupAttemptsLeft = value; },
|
|
@@ -2220,6 +2258,7 @@ export function createDecisionServer(options) {
|
|
|
2220
2258
|
get revisionLedgerOf() { return revisionLedgerOf; },
|
|
2221
2259
|
get firstTasks() { return firstTasks; },
|
|
2222
2260
|
get authenticateCredential() { return authenticateCredential; },
|
|
2261
|
+
get identities() { return identities; },
|
|
2223
2262
|
};
|
|
2224
2263
|
const tasksHandlers = createTasksHandlers(handlerRuntime);
|
|
2225
2264
|
const pagesHandlers = createPagesHandlers(handlerRuntime);
|
|
@@ -2297,3 +2336,38 @@ export { diffFileAnchor, editorFileHref, needActionOf, orderChangedFiles, plainC
|
|
|
2297
2336
|
export { settingsGroups, settingsTiles } from "./server/render-settings.js";
|
|
2298
2337
|
export { decisionAnswerScript, earlierVersionsWords, revisionLineageWords } from "./server/render-tasks.js";
|
|
2299
2338
|
export { SIGN_IN_LINK_MS, SIGN_IN_LINK_PATH } from "./server/session.js";
|
|
2339
|
+
/**
|
|
2340
|
+
* The console's configured repos and roots. Without identities (embedders and tests) they resolve once, synchronously,
|
|
2341
|
+
* as before. With them, the console resolves them in the background after it listens (operate.ts startConsole), so
|
|
2342
|
+
* each is read from the last answer: a configured path counts by its real path from the moment its folder answers,
|
|
2343
|
+
* and until then only by its spelling, which the strict `canon` never matches for an unresolved row.
|
|
2344
|
+
*/
|
|
2345
|
+
function startupCeiling(repos, roots, identities, canon) {
|
|
2346
|
+
const configured = repos.length > 0 || roots.length > 0;
|
|
2347
|
+
const configuredPaths = [...repos, ...roots];
|
|
2348
|
+
if (identities === undefined) {
|
|
2349
|
+
const once = resolveCeiling(repos, roots);
|
|
2350
|
+
return {
|
|
2351
|
+
ceiling: { ...once.ceiling, canon },
|
|
2352
|
+
unresolved: () => once.unresolved,
|
|
2353
|
+
defaultProject: () => once.ceiling.repos.length === 1 && once.ceiling.roots.length === 0 ? once.ceiling.repos[0] : null,
|
|
2354
|
+
configured,
|
|
2355
|
+
configuredPaths,
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
// Only resolved entries authorize: an unresolved configured path is in no list, so it matches nothing (not even its
|
|
2359
|
+
// own spelling) and is no session's default until its folder answers.
|
|
2360
|
+
const resolved = (paths) => [...new Set(paths.flatMap(path => { const real = identities.known(path); return typeof real === "string" ? [real] : []; }))];
|
|
2361
|
+
const ceiling = {
|
|
2362
|
+
get repos() { return resolved(repos); },
|
|
2363
|
+
get roots() { return resolved(roots); },
|
|
2364
|
+
canon,
|
|
2365
|
+
};
|
|
2366
|
+
return {
|
|
2367
|
+
ceiling,
|
|
2368
|
+
unresolved: () => [...repos, ...roots].filter(path => typeof identities.known(path) !== "string"),
|
|
2369
|
+
defaultProject: () => repos.length === 1 && roots.length === 0 ? resolved(repos)[0] ?? null : null,
|
|
2370
|
+
configured,
|
|
2371
|
+
configuredPaths,
|
|
2372
|
+
};
|
|
2373
|
+
}
|
package/dist/server/chat.js
CHANGED
|
@@ -12,10 +12,10 @@ import { CHAT_ACTIONS, mintSharedActionReview, sharedActionPayload, sharedAction
|
|
|
12
12
|
import { CONSOLE_FORMS, readForm } from "../contracts/console-api.js";
|
|
13
13
|
import { buildDataDocument, composeRequest, isSubscriptionChatProvider, parseAssistantEnvelope, performChatRequest, plausibleChatKey, priceOf, settleForPrice, TURN_WALL_CLOCK_MS, worstCaseForPrice } from "../converse.js";
|
|
14
14
|
import { DEMO_CHAT_SCRIPT, demoChatHtml, demoThreadHtml } from "../demo-chat.js";
|
|
15
|
-
import { withDispatchDiagnoses } from "../dispatch.js";
|
|
15
|
+
import { builderReadyFor, firstRunBuilderRepo, withDispatchDiagnoses } from "../dispatch.js";
|
|
16
16
|
import { readVerifiedArtifact, scanForSecrets } from "../evidence.js";
|
|
17
17
|
import { run as execRun } from "../exec.js";
|
|
18
|
-
import { findFirstTasks, firstTaskSuggestions, HOW_IT_WORKS, SANDBOX_COMMAND } from "../first-run.js";
|
|
18
|
+
import { findFirstTasks, firstTaskSuggestions, HOW_IT_WORKS, SANDBOX_COMMAND, withBuilderStep } from "../first-run.js";
|
|
19
19
|
import { leadBriefHtml } from '../lead-context.js';
|
|
20
20
|
import { configureLeadFollow, leadFollowStatus } from '../lead-follow.js';
|
|
21
21
|
import { leadActivity } from "../lead-voice.js";
|
|
@@ -1158,11 +1158,12 @@ export function createChatHandlers(runtime) {
|
|
|
1158
1158
|
}
|
|
1159
1159
|
/** Chat's first run: the steps, first tasks until the first task exists, and the sandbox while no agent is signed in. */
|
|
1160
1160
|
function chatFirstRun(now, project) {
|
|
1161
|
-
const
|
|
1161
|
+
const repo = project ?? managedRepos()[0] ?? null;
|
|
1162
|
+
// Done only when a task in this project could start: a live builder bound to it, as dispatch decides.
|
|
1163
|
+
const steps = withBuilderStep(firstRunStepsNow(now), builderReadyFor(store, firstRunBuilderRepo(store, project, visible, managedRepos()), now));
|
|
1162
1164
|
if (steps === null)
|
|
1163
1165
|
return undefined;
|
|
1164
1166
|
const taskFiled = steps.find(one => one.key === "task")?.done ?? true;
|
|
1165
|
-
const repo = project ?? managedRepos()[0] ?? null;
|
|
1166
1167
|
const agent = steps.find(one => one.key === "agent");
|
|
1167
1168
|
const lead = leadWords();
|
|
1168
1169
|
return {
|
package/dist/server/chrome.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type BrowserSignIn, type BrowserUpdateNotice, type BrowserWorkspace } from "../browser-workspace.js";
|
|
2
|
+
import { type Crumb } from "../settings-kit.js";
|
|
2
3
|
import { type Decision, type Run } from "../store.js";
|
|
3
4
|
import { type tally } from "../summary.js";
|
|
4
5
|
import { type ServerResponse } from "node:http";
|
|
@@ -115,6 +116,8 @@ export type Chrome = {
|
|
|
115
116
|
csrf?: string;
|
|
116
117
|
/** Where a switch made on this screen returns to. */
|
|
117
118
|
returnTo?: string;
|
|
119
|
+
/** The settings frame's breadcrumb ("Settings / <page>", settings-kit.ts), set by sendScreen; absent elsewhere. */
|
|
120
|
+
crumbs?: Crumb[];
|
|
118
121
|
};
|
|
119
122
|
/**
|
|
120
123
|
* A server-rendered region that stays current: when the page's live stream
|
package/dist/server/chrome.js
CHANGED
|
@@ -33,6 +33,7 @@ import { PULL_REQUEST_SETTINGS_CSS } from "../pull-request-ui.js";
|
|
|
33
33
|
import { RECIPE_CSS } from "../recipe-ui.js";
|
|
34
34
|
import { REQUEST_LIMITS_CSS } from "../request-budget-ui.js";
|
|
35
35
|
import { RETENTION_CSS } from "../retention-ui.js";
|
|
36
|
+
import { crumbsHtml, SETTINGS_KIT_CSS } from "../settings-kit.js";
|
|
36
37
|
import { SKILLS_CSS } from "../skills-ui.js";
|
|
37
38
|
import { SPEND_CSS } from "../spend-ui.js";
|
|
38
39
|
import { SSO_CSS } from "../sso-ui.js";
|
|
@@ -148,7 +149,7 @@ export const INBOX_TABS_CSS = '.inbox-tabs{display:inline-flex;gap:2px;max-width
|
|
|
148
149
|
'.inbox-ask{margin:18px 0 0}.inbox-ask>h2{display:flex;align-items:baseline;gap:8px;margin:0 0 4px;font-size:15px;font-weight:600}.inbox-ask>h2 .count{font:500 12px var(--so-mono,ui-monospace,monospace);font-variant-numeric:tabular-nums;color:var(--so-muted)}.inbox-ask h3{font-size:13px;font-weight:600;margin:12px 0 4px}' +
|
|
149
150
|
'.inbox-unread{display:none;position:absolute;top:4px;right:3px;width:6px;height:6px;border-radius:50%;background:var(--so-signal)}' +
|
|
150
151
|
'@media (max-width:760px){.inbox-tabs{display:flex;width:100%}.inbox-tabs a{flex:1;justify-content:center;min-height:44px;padding:0 6px}.inbox-unread{display:block}}';
|
|
151
|
-
export const WORKSPACE_STYLE = styleAsset(STYLE + BRAND_MARK_CSS + INBOX_TABS_CSS + APPROVAL_RULES_CSS + SPEND_CSS + RETENTION_CSS + STORAGE_CSS + UPDATES_CSS + LIMITS_CSS + MONITORING_CSS + INTEGRATIONS_CSS + BACKUP_CSS + EXPORT_CSS + PROJECT_DELETE_CSS + POLICY_CSS + EVIDENCE_PACK_CSS + THEME_CONTROLS_CSS + CODING_CSS + CODING_SHIPPING_CSS + RECIPE_CSS + SKILLS_CSS + TOOLS_CSS + FLOWS_CSS + SUBAGENT_CSS + KITS_CSS + STARTERS_CSS + GALLERY_CSS + SSO_CSS + CREDENTIALS_CSS + REQUEST_LIMITS_CSS + PEOPLE_AUDIT_CSS + KNOWLEDGE_CSS + MODELS_CSS + CHAT_POLISH_CSS + TRANSITIONS_CSS + WORKSPACE_MOTION_CSS + ASSIGNMENT_CSS + TASK_STATUS_CSS + LEAD_CONTEXT_CSS + PULL_REQUEST_SETTINGS_CSS + CHECK_SETTINGS_CSS + DISCLOSURE_CSS + '.learning{min-width:0;overflow-wrap:anywhere}.learning .card{min-width:0}.learning code,.learning blockquote,.learning pre{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}.learning button,.learning summary,.learning .button-link{min-height:44px}.learning button{white-space:nowrap}.learning summary{padding:12px 0;cursor:pointer}.learning form{margin:12px 0}.learning select{max-width:100%}.learning blockquote{margin:8px 0}.learning ul{padding-left:20px}');
|
|
152
|
+
export const WORKSPACE_STYLE = styleAsset(STYLE + BRAND_MARK_CSS + INBOX_TABS_CSS + APPROVAL_RULES_CSS + SPEND_CSS + RETENTION_CSS + STORAGE_CSS + UPDATES_CSS + LIMITS_CSS + MONITORING_CSS + INTEGRATIONS_CSS + BACKUP_CSS + EXPORT_CSS + PROJECT_DELETE_CSS + POLICY_CSS + EVIDENCE_PACK_CSS + THEME_CONTROLS_CSS + CODING_CSS + CODING_SHIPPING_CSS + RECIPE_CSS + SKILLS_CSS + TOOLS_CSS + FLOWS_CSS + SUBAGENT_CSS + KITS_CSS + STARTERS_CSS + GALLERY_CSS + SSO_CSS + CREDENTIALS_CSS + REQUEST_LIMITS_CSS + PEOPLE_AUDIT_CSS + KNOWLEDGE_CSS + MODELS_CSS + CHAT_POLISH_CSS + TRANSITIONS_CSS + WORKSPACE_MOTION_CSS + ASSIGNMENT_CSS + TASK_STATUS_CSS + LEAD_CONTEXT_CSS + PULL_REQUEST_SETTINGS_CSS + CHECK_SETTINGS_CSS + DISCLOSURE_CSS + SETTINGS_KIT_CSS + '.learning{min-width:0;overflow-wrap:anywhere}.learning .card{min-width:0}.learning code,.learning blockquote,.learning pre{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}.learning button,.learning summary,.learning .button-link{min-height:44px}.learning button{white-space:nowrap}.learning summary{padding:12px 0;cursor:pointer}.learning form{margin:12px 0}.learning select{max-width:100%}.learning blockquote{margin:8px 0}.learning ul{padding-left:20px}');
|
|
152
153
|
/**
|
|
153
154
|
* A server-rendered region that stays current: when the page's live stream
|
|
154
155
|
* says something changed (GET /live, live-bus.ts), fetch this page's own
|
|
@@ -579,8 +580,10 @@ ${update === undefined ? "" : html `<div class="banner update-banner" data-updat
|
|
|
579
580
|
${postForm(update.dismissHref, html `<button type="submit">Dismiss</button>`, { attrs: { class: "inline" }, hidden: { version: update.version } })}</div>`}`;
|
|
580
581
|
// The scope bar sits between the banners and the main/split body, so it
|
|
581
582
|
// can never disappear with a responsive pane (portfolio arc §1).
|
|
583
|
+
// The settings frame's breadcrumb sits above the page's own h1 (its only h1).
|
|
584
|
+
const crumbs = chrome.crumbs === undefined ? "" : crumbsHtml(chrome.crumbs);
|
|
582
585
|
const content = chrome.listPane === undefined
|
|
583
|
-
? html `<div class="content">${demoBanner}${scopeBar}<main>${main}</main></div>`
|
|
586
|
+
? html `<div class="content">${demoBanner}${scopeBar}${crumbs}<main>${main}</main></div>`
|
|
584
587
|
: html `<div class="content">${demoBanner}${scopeBar}<div class="split">\
|
|
585
588
|
<div class="list-pane">${chrome.listPane}</div>\
|
|
586
589
|
<div class="detail"><main>${main}</main></div>\
|
package/dist/server/console.css
CHANGED
|
@@ -265,12 +265,15 @@
|
|
|
265
265
|
.project-card .project-name:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; border-radius: .375rem; }
|
|
266
266
|
|
|
267
267
|
label { display: block; font-size: 0.8125rem; font-weight: 500; margin: .75rem 0 0; color: var(--foreground); }
|
|
268
|
-
input[type=text], input[type=password], input[type=number], input[type=url], input[type=email], input[type=search], input[type=time], textarea, select {
|
|
269
|
-
width: 100%; margin: .35rem 0 0; padding: .45rem .7rem;
|
|
268
|
+
:where(input[type=text], input[type=password], input[type=number], input[type=url], input[type=email], input[type=search], input[type=time], textarea, select) {
|
|
269
|
+
width: 100%; margin: .35rem 0 0; padding: .45rem .7rem;
|
|
270
270
|
color: var(--foreground); background: var(--card); min-height: 2.125rem;
|
|
271
271
|
border: 1px solid var(--input); border-radius: calc(var(--radius) - 3px);
|
|
272
272
|
transition: border-color .15s, box-shadow .15s;
|
|
273
273
|
}
|
|
274
|
+
/* The box above is in :where() so a surface's own rules (the workspace's 44px phone targets) win; the type keeps
|
|
275
|
+
its old weight, so the phone's 16px rule still loses to it as before. */
|
|
276
|
+
input[type=text], input[type=password], input[type=number], input[type=url], input[type=email], input[type=search], input[type=time], textarea, select { font: 400 0.875rem/1.4 var(--font-sans); }
|
|
274
277
|
@media (hover: hover) and (pointer: fine) { input:hover, textarea:hover, select:hover { border-color: var(--muted-foreground); } }
|
|
275
278
|
input[type=number] { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
|
|
276
279
|
input[type=radio], input[type=checkbox] { accent-color: var(--ring); }
|
package/dist/server/flows.js
CHANGED
|
@@ -43,7 +43,7 @@ import { redirect, respond, SAFETY, taskHref } from "./http.js";
|
|
|
43
43
|
import { html } from "../html.js";
|
|
44
44
|
import {} from "./session.js";
|
|
45
45
|
export function createFlowsHandlers(runtime) {
|
|
46
|
-
const { store, clock, sendScreen, chromeFor, consoleProjects, options, visible, liveCeiling, providerHome, toolHome, restricted, evidenceRoot, authenticateApprover, authenticateCredential, projectOf, unscopedMode, ceiling, authorizeMutation } = runtime;
|
|
46
|
+
const { identities, store, clock, sendScreen, chromeFor, consoleProjects, options, visible, liveCeiling, providerHome, toolHome, restricted, evidenceRoot, authenticateApprover, authenticateCredential, projectOf, unscopedMode, ceiling, authorizeMutation } = runtime;
|
|
47
47
|
// ---- reads ---------------------------------------------------------------
|
|
48
48
|
/** A gallery template's page (Flows → New → Use this): its questions, the preview of these answers, and Create. */
|
|
49
49
|
function sendGalleryUse(response, who, template, projects, repo, given, name, notice, said = null) {
|
|
@@ -839,13 +839,23 @@ export function createFlowsHandlers(runtime) {
|
|
|
839
839
|
if (body.get("repo") !== project || (who.via === "cookie" && body.get("projectRevision") !== String(who.session.projectRevision)))
|
|
840
840
|
throw new RecipeError("The open project changed. Reopen this recipe in the intended project.", 409);
|
|
841
841
|
const rootMode = !unscopedMode && ceiling.roots.length > 0;
|
|
842
|
-
|
|
843
|
-
|
|
842
|
+
const decidedOn = identities.generation();
|
|
843
|
+
if (rootMode) {
|
|
844
|
+
// A mutation's target is resolved now, off the console's thread, and must still be the project it names.
|
|
845
|
+
const resolved = await identities.resolveNow(project);
|
|
846
|
+
if (resolved === "no-answer")
|
|
847
|
+
throw new RecipeError("The project's folder didn't answer. It may be on a drive that's disconnected, or waiting on macOS. Try again in a moment.", 503);
|
|
848
|
+
if (resolved !== project || !(await authorizedProject(liveCeiling(), resolved)))
|
|
849
|
+
throw new RecipeError("The project is outside this server's access.", 403);
|
|
850
|
+
}
|
|
844
851
|
const rechecked = authorizeMutation(request, who, readForm(posted, CONSOLE_FORMS.mutationGuard));
|
|
845
852
|
if (rechecked !== null)
|
|
846
853
|
throw new RecipeError(rechecked.message, rechecked.status);
|
|
847
854
|
if (projectOf(who, request) !== project)
|
|
848
855
|
throw new RecipeError("The open project changed. Preview the intended project again.", 409);
|
|
856
|
+
// An identity that changed during the awaits above undoes what they proved.
|
|
857
|
+
if (identities.generation() !== decidedOn)
|
|
858
|
+
throw new RecipeError("A project folder changed while this was being checked. Try again.", 409);
|
|
849
859
|
const revision = who.via === "cookie" ? who.session.projectRevision : 0;
|
|
850
860
|
if (step === "prepare") {
|
|
851
861
|
const id = body.get("recipe") ?? "", version = body.get("recipeRevision") ?? "";
|
package/dist/server/guards.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ProjectIdentities } from "../project-identity.js";
|
|
1
2
|
import { type FormFieldOf, type FormView } from "../contracts/console-api.js";
|
|
2
3
|
import type { TaskFamily } from "../store.js";
|
|
3
4
|
import { type IncomingMessage, type ServerResponse } from "node:http";
|
|
@@ -13,8 +14,11 @@ export interface GuardsRuntime {
|
|
|
13
14
|
liveCeiling: () => {
|
|
14
15
|
repos: string[];
|
|
15
16
|
roots: readonly string[];
|
|
17
|
+
canon?: (path: string) => string | null;
|
|
16
18
|
};
|
|
17
19
|
codingProjects: () => string[];
|
|
20
|
+
/** Project identities read without touching folders (project-identity.ts); absent: the synchronous reads. */
|
|
21
|
+
identities?: ProjectIdentities;
|
|
18
22
|
unscopedMode: boolean;
|
|
19
23
|
managedRepos: () => string[];
|
|
20
24
|
ceiling: import("../project.js").ProjectCeiling;
|
package/dist/server/guards.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { syncProjectIdentities } from "../project-identity.js";
|
|
1
2
|
import { OWNER_ACTIONS, sharedActionPayload } from '../chat-actions.js';
|
|
2
3
|
import { CONSOLE_FORMS, readForm } from "../contracts/console-api.js";
|
|
3
4
|
import { timingSafeEqual } from "node:crypto";
|
|
@@ -14,6 +15,7 @@ import { refuse } from "./chrome.js";
|
|
|
14
15
|
import { matchTaskPath } from "./http.js";
|
|
15
16
|
import { PersistentSessions, SESSION_COOKIE, sessionWithinLifetime } from "./session.js";
|
|
16
17
|
export function createSharedGuards(runtime) {
|
|
18
|
+
const syncIdentities = syncProjectIdentities();
|
|
17
19
|
// v99: wrong passwords. One address guessing across names runs out of tries here; each name
|
|
18
20
|
// locks on its own (sign-in-guard.ts), and every lock, whatever road it came by, is kept.
|
|
19
21
|
/**
|
|
@@ -130,8 +132,9 @@ export function createSharedGuards(runtime) {
|
|
|
130
132
|
return fallback();
|
|
131
133
|
if (Array.isArray(header))
|
|
132
134
|
return undefined;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
+
// A request's path, never resolved on this thread (project-identity.ts): a known identity or exactly a saved one.
|
|
136
|
+
const canonical = identities().vouched(header, savedIdentities);
|
|
137
|
+
if (canonical === undefined || !visible(canonical))
|
|
135
138
|
return undefined;
|
|
136
139
|
return canonical;
|
|
137
140
|
}
|
|
@@ -152,7 +155,8 @@ export function createSharedGuards(runtime) {
|
|
|
152
155
|
case 'form-path': {
|
|
153
156
|
const path = body?.get('path')?.trim() ?? '';
|
|
154
157
|
// Invalid/missing paths keep the opener's existing 400 response and recoverable form.
|
|
155
|
-
|
|
158
|
+
// Never resolved here: an unknown path passes on to the opener, which resolves it off the thread and checks again.
|
|
159
|
+
return path === '' ? null : identities().vouched(path, savedIdentities) ?? null;
|
|
156
160
|
}
|
|
157
161
|
case 'task': {
|
|
158
162
|
const task = matchTaskPath(url.pathname, '(?:/[a-z-]+)?$');
|
|
@@ -370,5 +374,10 @@ export function createSharedGuards(runtime) {
|
|
|
370
374
|
function runVisible(run) {
|
|
371
375
|
return visible(taskRepoOf(run.taskRef));
|
|
372
376
|
}
|
|
377
|
+
// Defined last, so the reviewed functions above keep their places (password-freshness.test.ts).
|
|
378
|
+
/** Project identities read without touching a folder (project-identity.ts); absent: the synchronous reads. */
|
|
379
|
+
function identities() { return runtime.identities ?? syncIdentities; }
|
|
380
|
+
/** The projects `up` proved and admitted (each its own identity): what a request may name without a read. */
|
|
381
|
+
function savedIdentities() { return runtime.managedRepos(); }
|
|
373
382
|
return { authenticateApprover, authenticateCredential, authorizeMutation, projectOf, resolveRouteProject, projectRequestAllowed, identify, lookupSession, restricted, visible, admissionList, codingActorAllowed, codingProjectAllowed, workAccess, familyOf, taskRepoOf, runVisible };
|
|
374
383
|
}
|
package/dist/server/http.d.ts
CHANGED
|
@@ -17,6 +17,9 @@ import { type Store } from "../store.js";
|
|
|
17
17
|
import { type SubscriptionLeadRunner } from "../subscription-chat.js";
|
|
18
18
|
import { type launchRuntimeUpdate } from "../toolroll-update.js";
|
|
19
19
|
import { type IncomingMessage, type ServerResponse } from "node:http";
|
|
20
|
+
import { type FolderLookRunner } from "../folder-look.js";
|
|
21
|
+
import { type ProjectIdentities } from "../project-identity.js";
|
|
22
|
+
import { type LearningRunner } from "../learning-look.js";
|
|
20
23
|
export type ServeOptions = {
|
|
21
24
|
/** Tests: runs a person's command for the HTTP MCP gateway instead of operate.ts's. */
|
|
22
25
|
runOperateAs?: import("../mcp-person.js").RunOperateAs;
|
|
@@ -172,6 +175,14 @@ export type ServeOptions = {
|
|
|
172
175
|
* how often they run and ask this computer's agents again. */
|
|
173
176
|
backgroundChecks?: boolean;
|
|
174
177
|
backgroundChecksMs?: number;
|
|
178
|
+
/** Tests: how a person's folders are read off the console's thread (folder-look.ts), and how long a read may take. */
|
|
179
|
+
folderLook?: FolderLookRunner;
|
|
180
|
+
folderLookMs?: number;
|
|
181
|
+
/** Project identities resolved off the console's thread (project-identity.ts); `up` and `serve` pass them. Absent:
|
|
182
|
+
* the old synchronous reads (embedders and tests). */
|
|
183
|
+
identities?: ProjectIdentities;
|
|
184
|
+
/** Tests: how the background pass runs one project's learning recovery in a process of its own. */
|
|
185
|
+
learningRunner?: LearningRunner;
|
|
175
186
|
connectionHome?: string;
|
|
176
187
|
/** Test seam for Chat's first tasks: the `gh` and `git grep` reads. */
|
|
177
188
|
firstTaskRunner?: typeof execRun;
|
|
@@ -215,6 +226,7 @@ export declare function matchTaskPath(pathname: string, suffixPattern: string):
|
|
|
215
226
|
verb: string;
|
|
216
227
|
} | null;
|
|
217
228
|
export declare function taskHref(taskId: string): string;
|
|
229
|
+
export declare function runHref(runId: number): string;
|
|
218
230
|
export declare const SAFETY: {
|
|
219
231
|
readonly "Content-Security-Policy": "default-src 'none'; style-src 'self' 'unsafe-inline'; font-src 'self'; manifest-src 'self'; worker-src 'self'; img-src 'self'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'";
|
|
220
232
|
readonly "X-Content-Type-Options": "nosniff";
|
package/dist/server/http.js
CHANGED
|
@@ -19,6 +19,9 @@ import {} from "../toolroll-update.js";
|
|
|
19
19
|
import { timingSafeEqual } from "node:crypto";
|
|
20
20
|
import {} from "node:http";
|
|
21
21
|
import { SIGN_IN_LINK_PATH } from "./session.js";
|
|
22
|
+
import {} from "../folder-look.js";
|
|
23
|
+
import {} from "../project-identity.js";
|
|
24
|
+
import {} from "../learning-look.js";
|
|
22
25
|
// A note is at most NOTE_BYTE_CAP (16,000) UTF-8 bytes, which URL encoding
|
|
23
26
|
// can triple: room for one whole, before canonical text validation.
|
|
24
27
|
export const BODY_CAP = 64 * 1024;
|
|
@@ -48,6 +51,9 @@ export function matchTaskPath(pathname, suffixPattern) {
|
|
|
48
51
|
export function taskHref(taskId) {
|
|
49
52
|
return `/t/${encodeURIComponent(taskId)}`;
|
|
50
53
|
}
|
|
54
|
+
export function runHref(runId) {
|
|
55
|
+
return `/r/${Math.trunc(runId)}`;
|
|
56
|
+
}
|
|
51
57
|
// ---- rendering -------------------------------------------------------------
|
|
52
58
|
export const SAFETY = {
|
|
53
59
|
"Content-Security-Policy": "default-src 'none'; style-src 'self' 'unsafe-inline'; font-src 'self'; manifest-src 'self'; worker-src 'self'; img-src 'self'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'",
|
|
@@ -61,10 +67,13 @@ export function respond(response, status, type, body) {
|
|
|
61
67
|
// writeHead's headers would otherwise win.
|
|
62
68
|
const own = response.getHeader("content-security-policy");
|
|
63
69
|
const cache = response.getHeader("cache-control");
|
|
70
|
+
// The first-account form's page sends `same-origin` so its own post names this console (signup-gate.ts).
|
|
71
|
+
const referrer = response.getHeader("referrer-policy");
|
|
64
72
|
response.writeHead(status, {
|
|
65
73
|
...SAFETY,
|
|
66
74
|
...(own === undefined ? {} : { "content-security-policy": own }),
|
|
67
75
|
...(cache === undefined ? {} : { "Cache-Control": cache }),
|
|
76
|
+
...(referrer === "same-origin" ? { "Referrer-Policy": "same-origin" } : {}),
|
|
68
77
|
"Content-Type": type,
|
|
69
78
|
});
|
|
70
79
|
response.end(isHtml(body) ? htmlString(body) : body);
|