toolroll 0.9.45 → 0.9.47
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 +33 -0
- package/dist/activity-line.d.ts +26 -0
- package/dist/activity-line.js +47 -0
- package/dist/api-tokens.d.ts +33 -0
- package/dist/api-tokens.js +39 -0
- package/dist/audit-cli.d.ts +17 -0
- package/dist/audit-cli.js +86 -0
- package/dist/browser/workspace.js +25 -25
- package/dist/browser-workspace.d.ts +18 -1
- package/dist/browser-workspace.js +9 -3
- package/dist/builder.js +4 -2
- package/dist/cli-http.d.ts +5 -0
- package/dist/cli-http.js +12 -2
- package/dist/cli.js +7 -1
- package/dist/contracts/cli.d.ts +47 -0
- package/dist/contracts/cli.js +11 -0
- package/dist/contracts/console-api.d.ts +1 -0
- package/dist/contracts/console-api.js +1 -0
- package/dist/contracts/mcp-oauth.d.ts +53 -0
- package/dist/contracts/mcp-oauth.js +40 -0
- package/dist/credentials-ui.d.ts +5 -0
- package/dist/credentials-ui.js +16 -4
- package/dist/export.js +4 -3
- package/dist/flow-card-task.d.ts +2 -0
- package/dist/flow-card-task.js +57 -0
- package/dist/flow-live.d.ts +2 -2
- package/dist/flow-live.js +69 -8
- package/dist/flows-ui.js +15 -1
- package/dist/live.d.ts +14 -0
- package/dist/live.js +71 -22
- package/dist/mcp-http.d.ts +6 -1
- package/dist/mcp-http.js +22 -4
- package/dist/mcp-oauth.d.ts +82 -0
- package/dist/mcp-oauth.js +479 -0
- package/dist/mcp-person.js +1 -1
- package/dist/operate-remote-arguments.js +2 -0
- package/dist/operate-remote.d.ts +1 -1
- package/dist/operate-remote.js +7 -1
- package/dist/operate.d.ts +3 -0
- package/dist/operate.js +58 -2
- package/dist/people-audit-ui.d.ts +14 -0
- package/dist/people-audit-ui.js +38 -0
- package/dist/planner.js +4 -3
- package/dist/public-access.d.ts +50 -0
- package/dist/public-access.js +87 -0
- package/dist/public-check.d.ts +37 -0
- package/dist/public-check.js +170 -0
- package/dist/remote-audit.d.ts +79 -0
- package/dist/remote-audit.js +161 -0
- package/dist/request-budget-ui.d.ts +11 -0
- package/dist/request-budget-ui.js +31 -0
- package/dist/request-budget.d.ts +96 -0
- package/dist/request-budget.js +227 -0
- package/dist/serve.d.ts +9 -1
- package/dist/serve.js +211 -52
- package/dist/setup-guide.d.ts +2 -2
- package/dist/store.d.ts +134 -2
- package/dist/store.js +270 -7
- package/dist/surface.js +19 -2
- package/dist/task-activity.d.ts +27 -0
- package/dist/task-activity.js +53 -0
- package/dist/task-live.d.ts +17 -0
- package/dist/task-live.js +144 -0
- package/dist/token-notices.d.ts +22 -0
- package/dist/token-notices.js +52 -0
- package/dist/tokens-cli.d.ts +21 -0
- package/dist/tokens-cli.js +184 -0
- package/package.json +1 -1
package/dist/serve.js
CHANGED
|
@@ -8,7 +8,7 @@ import { projectBatchChecks, setProjectBatchChecks } from './batch-policy.js';
|
|
|
8
8
|
import { CHECK_LEVEL_WORDS, isCheckLevel, liveQuickCommand, projectCheckLevel, quickVerifyKey, setProjectCheckLevel, suggestQuickCommand } from './check-levels.js';
|
|
9
9
|
import { repositoryContextHtml } from './repository-context-ui.js';
|
|
10
10
|
import { browserAssetsAvailable, browserWorkspaceDocument, serveBrowserAsset } from './browser-shell.js';
|
|
11
|
-
import { browserCrewOf, browserCrewFromIndex, browserWorkActionHref, browserProjectsOf, browserNavigationOf } from './browser-workspace.js';
|
|
11
|
+
import { browserCrewOf, browserCrewFromIndex, browserWorkActionHref, browserProjectsOf, browserNavigationOf, needsYouLabelOf } from './browser-workspace.js';
|
|
12
12
|
import { configureLeadFollow, leadFollowStatus, runLeadFollowPass } from './lead-follow.js';
|
|
13
13
|
import { cancelCommitment, conditionWords, openCommitments } from './lead-commitments.js';
|
|
14
14
|
import { startMaintenance } from './maintenance.js';
|
|
@@ -44,6 +44,8 @@ import { addRoutine, removeRoutine, runRoutine } from "./teammate-desk.js";
|
|
|
44
44
|
import { sendTeammateWeeklies, undoCall } from "./teammate-week.js";
|
|
45
45
|
import { answerTeammateQuestion } from "./teammate-work.js";
|
|
46
46
|
import { createFlowRooms, flowFingerprint } from "./flow-live.js";
|
|
47
|
+
import { createTaskRooms, taskFingerprint } from "./task-live.js";
|
|
48
|
+
import { runActivityOf } from "./task-activity.js";
|
|
47
49
|
import { disconnectGoogle, finishGoogleConsent, GOOGLE_CALLBACK, googleConnected, googleConsent, readGoogleMail, saveGoogleClient } from "./google-mail.js";
|
|
48
50
|
import { mailboxAccess, readThroughImap } from "./mailbox.js";
|
|
49
51
|
import { readEmailSettings, saveEmailSettings, sendingAccount, sendThroughServer, setFlowSecret } from "./flow-actions.js";
|
|
@@ -262,9 +264,15 @@ import { LIMITS_CSS, limitsHtml, limitsView } from "./limits-ui.js";
|
|
|
262
264
|
import { budgetHoldWords, budgetLabel, budgetStates, monthNamed, monthOf, spendItems, teammateNames as teammateNamesOf, usd as spendUsd } from "./spend.js";
|
|
263
265
|
import { targetOf } from "./monitoring.js";
|
|
264
266
|
import { SSO_CSS, ssoSettingsHtml } from "./sso-ui.js";
|
|
265
|
-
import { mintApiToken, parseApiToken, secretMatches, TOKEN_DAYS } from "./api-tokens.js";
|
|
267
|
+
import { mintApiToken, parseApiToken, secretMatches, TOKEN_DAYS, tokenLive, tokenProjects } from "./api-tokens.js";
|
|
266
268
|
import { createMcpHttp } from "./mcp-http.js";
|
|
269
|
+
import { FLUSH_MS as REQUEST_BUDGET_FLUSH_MS, parseLimit, PER_DAY_MAX as REQUEST_BUDGET_PER_DAY_MAX, PER_MINUTE_MAX as REQUEST_BUDGET_PER_MINUTE_MAX, RequestBudget, setLimitOverride } from "./request-budget.js";
|
|
270
|
+
import { hstsFor, LOOPBACK_PEERS, plainHttpRefusal, transportOf } from "./public-access.js";
|
|
271
|
+
import { createOAuthHttp, oauthTokenAllowed, resourceMetadataUrl } from "./mcp-oauth.js";
|
|
267
272
|
import { CREDENTIALS_CSS, credentialsHtml, tokenShownHtml } from "./credentials-ui.js";
|
|
273
|
+
import { REQUEST_LIMITS_CSS, requestLimitsHtml, tokenLimitWords } from "./request-budget-ui.js";
|
|
274
|
+
import { PEOPLE_AUDIT_CSS, personAuditHtml, personHref } from "./people-audit-ui.js";
|
|
275
|
+
import { cursorOf as auditCursorOf, remoteAudit } from "./remote-audit.js";
|
|
268
276
|
import { logEvent } from "./log.js";
|
|
269
277
|
import { modeTermsFromJson, modeWords, presetTerms, modeTermsJson, modeDigestOf, MODE_MAX_DAYS } from "./modes.js";
|
|
270
278
|
import { PROVIDER_KEY_ENV, SUBSCRIPTION_CAPABLE, clearProviderKey, readProviderKey, keyStatus, plausibleKey, readAuthMode, readAuthModeStrict, saveProviderKey, setAuthMode, verifyProviderKey, verdictWords } from "./keys.js";
|
|
@@ -535,7 +543,7 @@ export function createDecisionServer(options) {
|
|
|
535
543
|
* the trusted proxy itself appended; earlier entries are client-typed). */
|
|
536
544
|
function joinSourceOf(request) {
|
|
537
545
|
const peer = request.socket.remoteAddress ?? "unknown";
|
|
538
|
-
const loopback =
|
|
546
|
+
const loopback = LOOPBACK_PEERS.has(peer);
|
|
539
547
|
if (!loopback)
|
|
540
548
|
return peer;
|
|
541
549
|
const forwarded = request.headers["x-forwarded-for"];
|
|
@@ -581,7 +589,7 @@ export function createDecisionServer(options) {
|
|
|
581
589
|
/** This computer, and nothing in front of it: a loopback peer that names a loopback address, with no forwarding proxy. */
|
|
582
590
|
const fromThisComputer = (request) => {
|
|
583
591
|
const peer = request.socket.remoteAddress ?? "";
|
|
584
|
-
const loopback =
|
|
592
|
+
const loopback = LOOPBACK_PEERS.has(peer);
|
|
585
593
|
return loopback && request.headers["x-forwarded-for"] === undefined && request.headers["forwarded"] === undefined
|
|
586
594
|
&& /^(localhost|127\.0\.0\.1|\[::1\]):[0-9]{1,5}$/.test(request.headers.host ?? "");
|
|
587
595
|
};
|
|
@@ -743,6 +751,7 @@ export function createDecisionServer(options) {
|
|
|
743
751
|
const connectVisits = new Map();
|
|
744
752
|
/** Flows open in a browser (v88): who's here, and a nudge when one changes. */
|
|
745
753
|
const flowRooms = createFlowRooms(flow => flowFingerprint(store, flow));
|
|
754
|
+
const taskRooms = createTaskRooms(root => taskFingerprint(store, root, clock()));
|
|
746
755
|
const teamChatProvider = () => { const enabled = chatEnablement(); return enabled.ok ? { config: enabled.config, key: enabled.key } : null; };
|
|
747
756
|
const team = createTeamRuntime({ store, repos: codingProjects, evidenceRoot, clock, workspaceRevision,
|
|
748
757
|
...(options.chatFetcher ? { fetcher: options.chatFetcher } : {}),
|
|
@@ -797,16 +806,19 @@ export function createDecisionServer(options) {
|
|
|
797
806
|
return who?.via === 'cookie' ? teamBrowserReply(reply, actor, who.session.csrf) : reply;
|
|
798
807
|
}, cursor: team.cursor, streams: teamStreams,
|
|
799
808
|
});
|
|
809
|
+
// v112: one request budget per server for person API tokens, shared by /api/cli and /mcp (request-budget.ts).
|
|
810
|
+
const requestBudget = new RequestBudget({ store, ...(options.requestBudgetClock === undefined ? {} : { clock: options.requestBudgetClock }) });
|
|
800
811
|
// Remote CLI: one live API token names the person; the shared command boundary decides everything else.
|
|
801
812
|
const cliEndpoint = (request, response) => handleCliHttp(request, response, {
|
|
813
|
+
admit: principal => requestBudget.admit(principal.tokenId, "api"),
|
|
802
814
|
authenticate: request => {
|
|
803
|
-
const who = identify(request, false);
|
|
815
|
+
const who = identify(request, false, true);
|
|
804
816
|
const id = parseApiToken(/^Bearer (so_\S+)$/.exec(request.headers.authorization ?? "")?.[1] ?? "")?.id;
|
|
805
817
|
const row = id === undefined ? undefined : store.apiTokenSecret(id)?.row;
|
|
806
818
|
const account = who === null ? null : store.accountOf(who.name);
|
|
807
819
|
if (who?.via !== "bearer" || who.token === undefined || row === undefined || row.account !== who.name || account === null || account.revokedAt !== null)
|
|
808
820
|
return null;
|
|
809
|
-
return { kind: "person", account: who.name, generation: account.generation, scope: row.access, tokenId: row.id, projects: account.projects
|
|
821
|
+
return { kind: "person", account: who.name, generation: account.generation, scope: row.access, tokenId: row.id, projects: tokenProjects(account.projects, row.projects) };
|
|
810
822
|
},
|
|
811
823
|
run: async () => options.cliRunner ?? (await import("./operate.js")).runOperateAs ?? null,
|
|
812
824
|
modeOf: options.cliModeOf,
|
|
@@ -876,6 +888,15 @@ export function createDecisionServer(options) {
|
|
|
876
888
|
timer.unref?.();
|
|
877
889
|
server.on("close", () => clearInterval(timer));
|
|
878
890
|
}
|
|
891
|
+
{
|
|
892
|
+
const flushBudget = () => { try {
|
|
893
|
+
requestBudget.flush();
|
|
894
|
+
}
|
|
895
|
+
catch { /* the next admit refuses if the database is unwell */ } };
|
|
896
|
+
const timer = setInterval(flushBudget, REQUEST_BUDGET_FLUSH_MS);
|
|
897
|
+
timer.unref?.();
|
|
898
|
+
server.on("close", () => { clearInterval(timer); flushBudget(); });
|
|
899
|
+
}
|
|
879
900
|
const servedPort = () => {
|
|
880
901
|
const address = server.address();
|
|
881
902
|
return typeof address === "object" && address !== null ? address.port : null;
|
|
@@ -1237,6 +1258,17 @@ export function createDecisionServer(options) {
|
|
|
1237
1258
|
if (!allowedHost(request.headers.host)) {
|
|
1238
1259
|
return respond(response, 421, "text/html; charset=utf-8", wrongHostPage(wrongHost(request.headers.host)));
|
|
1239
1260
|
}
|
|
1261
|
+
// A real domain (public-access.ts): HSTS for the public https host reached over HTTPS, and no API token over
|
|
1262
|
+
// plain HTTP from outside this computer and the tailnet — refused before anything reads or checks it.
|
|
1263
|
+
const transport = transportOf({ peer: request.socket.remoteAddress, joinSource: joinSourceOf(request), forwardedProto: request.headers["x-forwarded-proto"], forwarded: request.headers["forwarded"] });
|
|
1264
|
+
const hsts = hstsFor(publicOrigin?.host ?? null, request.headers.host, transport);
|
|
1265
|
+
if (hsts !== null)
|
|
1266
|
+
response.setHeader("Strict-Transport-Security", hsts);
|
|
1267
|
+
const insecure = plainHttpRefusal(transport, hook.pathname, request.headers.authorization);
|
|
1268
|
+
if (insecure !== null) {
|
|
1269
|
+
request.resume();
|
|
1270
|
+
return respond(response, 403, "text/plain; charset=utf-8", insecure);
|
|
1271
|
+
}
|
|
1240
1272
|
const url = new URL(request.url ?? "/", "http://placeholder");
|
|
1241
1273
|
// A token in a URL is a token in history, logs, and referers. Refused
|
|
1242
1274
|
// outright rather than ignored, so nobody learns the habit works.
|
|
@@ -1246,6 +1278,9 @@ export function createDecisionServer(options) {
|
|
|
1246
1278
|
// The MCP gateway over HTTP (mcp-http.ts): coordinators and people's own API tokens, after the Host check.
|
|
1247
1279
|
if (url.pathname === "/mcp")
|
|
1248
1280
|
return mcpHttp(request, response);
|
|
1281
|
+
// MCP sign-in (mcp-oauth.ts): its discovery documents, client registration, consent and tokens.
|
|
1282
|
+
if (await oauthHttp(request, response, url))
|
|
1283
|
+
return;
|
|
1249
1284
|
// Back from Google's consent screen (v89). The session cookie is SameSite=Strict and stays behind on
|
|
1250
1285
|
// a return from another site: the visit's one-time state (made by a signed-in approver) is the proof.
|
|
1251
1286
|
if (url.pathname === GOOGLE_CALLBACK && request.method === "GET")
|
|
@@ -1799,9 +1834,9 @@ export function createDecisionServer(options) {
|
|
|
1799
1834
|
}
|
|
1800
1835
|
const read = new Set(["/settings/flows", "/settings/skills", "/settings/knowledge", "/settings", "/settings/learning", "/recipes", "/recipes/run", "/recipes/start", "/recipes/new", "/recipes/edit", "/recipes/from-task", "/recipes/preview", "/recipes/export", "/", "/inbox", "/work", "/projects", "/people", "/ledger", "/ledger/export", "/next", "/board", "/tasks", "/tasks/new", "/runs", "/review", "/done", "/routines", "/menu"]);
|
|
1801
1836
|
const write = new Set(["/settings/flows/on", "/settings/skills/import", "/settings/skills/change", "/settings/skills/revise", "/settings/knowledge/change", "/settings/knowledge/refresh", "/settings/learning/change", "/recipes/prepare", "/recipes/preview", "/recipes/import", "/recipes/save", "/recipes/launch", "/projects/select", "/tasks/add", "/routines/add"]);
|
|
1802
|
-
const task = matchTaskPath(path, request.method === "GET" ? "(/evidence)?$" : "/(hold|unhold|requeue|cancel|scope|approve|plan|plan-edit|next|reopen|steer|accept-proof|accept-revision|reject-revision|route|retry-review|complete|merge|confirm-stopped|stop|resume-arm|resume)$");
|
|
1837
|
+
const task = matchTaskPath(path, request.method === "GET" ? "(/evidence|/live)?$" : "/(hold|unhold|requeue|cancel|scope|approve|plan|plan-edit|next|reopen|steer|accept-proof|accept-revision|reject-revision|route|retry-review|complete|merge|confirm-stopped|stop|resume-arm|resume)$");
|
|
1803
1838
|
const resource = request.method === "GET"
|
|
1804
|
-
? /^\/(?:r|d)\/[0-9]{1,15}(?:\/evidence\/[0-9]{1,15})?$/.test(path) || /^\/routines\/[0-9]{1,15}$/.test(path) || path === "/flows" || /^\/flows\/[0-9]{1,15}(\/insights|\/export|\/runs\/[0-9]{1,15}\/[0-9]{1,15})?$/.test(path) || path === "/flows/new" || /^\/flows\/new\/[a-z-]{1,40}$/.test(path)
|
|
1839
|
+
? /^\/(?:r|d)\/[0-9]{1,15}(?:\/evidence\/[0-9]{1,15})?$/.test(path) || /^\/routines\/[0-9]{1,15}$/.test(path) || path === "/flows" || /^\/flows\/[0-9]{1,15}(\/insights|\/export|\/live|\/runs\/[0-9]{1,15}\/[0-9]{1,15})?$/.test(path) || path === "/flows/new" || /^\/flows\/new\/[a-z-]{1,40}$/.test(path)
|
|
1805
1840
|
: /^\/d\/[0-9]{1,15}\/answer$/.test(path) || /^\/routines\/[0-9]{1,15}\/(approve|refresh|pause|resume|run-now)$/.test(path) || path === "/flows/new" || /^\/flows\/new\/[a-z-]{1,40}$/.test(path) || path === "/flows/import" || /^\/flows\/[0-9]{1,15}\/(save|cards|archive|scripts)$/.test(path) || /^\/flows\/[0-9]{1,15}\/cards\/[0-9]{1,15}\/(move|decide|cancel|comment|assign|watch)$/.test(path) || /^\/flows\/[0-9]{1,15}\/triggers(\/[0-9]{1,15}\/(pause|resume|remove|check|press|renew|secret|share|unshare))?$/.test(path) || /^\/r\/[0-9]{1,15}\/(note|comment|revise|draft-repair|checks|add-tests)$/.test(path);
|
|
1806
1841
|
if (!(request.method === "GET" ? read : write).has(path) && task === null && !resource) {
|
|
1807
1842
|
refuse(response, who, 403, "This area requires instance access. Your account operates within its assigned projects.", "/projects");
|
|
@@ -1859,6 +1894,10 @@ export function createDecisionServer(options) {
|
|
|
1859
1894
|
}
|
|
1860
1895
|
project = wanted;
|
|
1861
1896
|
}
|
|
1897
|
+
// Every page's Tasks count covers this one project view, whatever project the page itself shows.
|
|
1898
|
+
const facts = requestContext.getStore();
|
|
1899
|
+
if (facts !== undefined)
|
|
1900
|
+
facts.lens = project;
|
|
1862
1901
|
// Exact result links carry their own read context. Check the stored
|
|
1863
1902
|
// placement against both account and instance access before using it;
|
|
1864
1903
|
// viewing a result never changes the session's selected project, and
|
|
@@ -1943,7 +1982,7 @@ export function createDecisionServer(options) {
|
|
|
1943
1982
|
url.pathname !== "/menu" &&
|
|
1944
1983
|
url.pathname !== "/recipes" &&
|
|
1945
1984
|
// A flow names its own project; the list spans every project.
|
|
1946
|
-
url.pathname !== "/flows" && url.pathname !== "/flows/import" && !/^\/flows\/[0-9]{1,15}(\/insights|\/export|\/runs\/[0-9]{1,15}\/[0-9]{1,15})?$/.test(url.pathname) &&
|
|
1985
|
+
url.pathname !== "/flows" && url.pathname !== "/flows/import" && !/^\/flows\/[0-9]{1,15}(\/insights|\/export|\/live|\/runs\/[0-9]{1,15}\/[0-9]{1,15})?$/.test(url.pathname) &&
|
|
1947
1986
|
// The gallery (Flows → New) asks for its project on each template's page.
|
|
1948
1987
|
url.pathname !== "/flows/new" && !/^\/flows\/new\/[a-z-]{1,40}$/.test(url.pathname) &&
|
|
1949
1988
|
// Teammates (v92) name their own project, like flows.
|
|
@@ -1951,7 +1990,7 @@ export function createDecisionServer(options) {
|
|
|
1951
1990
|
// New work names its project in the form (a dropdown of known
|
|
1952
1991
|
// projects); /tasks/add still admits the posted repo on its own.
|
|
1953
1992
|
url.pathname !== "/tasks/new" && url.pathname !== "/tasks/add" &&
|
|
1954
|
-
!/^\/t\/[^/]+(\/evidence)?$/.test(url.pathname) &&
|
|
1993
|
+
!/^\/t\/[^/]+(\/evidence|\/live)?$/.test(url.pathname) &&
|
|
1955
1994
|
!/^\/r\/[0-9]{1,15}(?:\/evidence\/[0-9]{1,15})?$/.test(url.pathname) &&
|
|
1956
1995
|
!url.pathname.startsWith("/d/") && !url.pathname.startsWith("/contest/") &&
|
|
1957
1996
|
url.pathname !== "/projects" &&
|
|
@@ -2244,6 +2283,8 @@ export function createDecisionServer(options) {
|
|
|
2244
2283
|
const docked = projectThread === null ? null : dockedConversation(who, null, projectThread, now, `/work?project=${encodeURIComponent(projectThread)}`);
|
|
2245
2284
|
if (docked !== null)
|
|
2246
2285
|
page.workspace = { ...page.workspace, conversation: docked, pageHtml: page.body };
|
|
2286
|
+
// Without a docked chat the list reads itself: 10 s while any of its tasks is building, else 30 s.
|
|
2287
|
+
page.refreshSeconds = work.totals.running > 0 ? 10 : 30;
|
|
2247
2288
|
return sendScreen(response, 200, page);
|
|
2248
2289
|
}
|
|
2249
2290
|
if (url.pathname === "/next") {
|
|
@@ -2752,11 +2793,26 @@ export function createDecisionServer(options) {
|
|
|
2752
2793
|
return sendScreen(response, 200, screen("mode", [`<h1>Operating mode</h1><p>Authorize this project's automatic approvals once, for up to ${MODE_MAX_DAYS} days. Select the touchpoints below; the signature covers the exact choices. Existing modes keep their original terms.</p><p class="meta">Scope changes, agent questions, and requested revisions still require your attention. Automatic merging also needs a publication grant.</p>`, current, signForm].join("\n"), { chrome: chromeFor(project, "mode") }));
|
|
2753
2794
|
}
|
|
2754
2795
|
if (url.pathname === "/people") {
|
|
2796
|
+
// People → a person: their API tokens and remote activity (people-audit-ui.ts). An approver may open anyone, within
|
|
2797
|
+
// their projects; anyone else only themselves. Another person reads exactly like nobody.
|
|
2798
|
+
const person = url.searchParams.get("person");
|
|
2799
|
+
if (person !== null) {
|
|
2800
|
+
const token = (url.searchParams.get("token-name") ?? "").slice(0, 60) || null, before = url.searchParams.get("before");
|
|
2801
|
+
const cursor = before === null ? null : auditCursorOf(before);
|
|
2802
|
+
if (before !== null && cursor === null)
|
|
2803
|
+
return refuse(response, who, 400, "Invalid activity cursor.", "/people");
|
|
2804
|
+
const page = who.role === "approver" || person === who.name
|
|
2805
|
+
? remoteAudit(store, { self: who.name, everyone: who.role === "approver", repos: admissionList(), unplaced: store.isInstanceOperator(who.name) }, { person, token, source: null, since: null }, { before: cursor })
|
|
2806
|
+
: null;
|
|
2807
|
+
if (page === null || !page.ok)
|
|
2808
|
+
return refuse(response, who, 404, "No such person.", "/people");
|
|
2809
|
+
return sendScreen(response, 200, screen("people", personAuditHtml({ name: person, tokens: store.apiTokens(person), actions: page.actions, token, nextCursor: page.nextCursor, now }), { chrome: chromeFor(project, "people") }));
|
|
2810
|
+
}
|
|
2755
2811
|
// Approvers see everyone (D7's ceiling: every fact already passed
|
|
2756
2812
|
// the process admission); a viewer sees exactly themselves (U3).
|
|
2757
2813
|
if (restricted()) {
|
|
2758
2814
|
const projects = admissionList() ?? [];
|
|
2759
|
-
return sendScreen(response, 200, screen("people", `<h1>Your access</h1><p>${personChip(who.name)} · ${who.role === "approver" ? "operator" : "viewer"}</p><p>${who.role === "approver" ? "You can create, manage, and approve work in these projects." : "You can read work in these projects."}</p><ul>${projects.map(repo => `<li>${escape(projectName(repo))}</li>`).join("")}</ul><p class="meta">An instance operator manages invitations and project access.</p>`, { chrome: chromeFor(project, "people") }));
|
|
2815
|
+
return sendScreen(response, 200, screen("people", `<h1>Your access</h1><p>${personChip(who.name)} · ${who.role === "approver" ? "operator" : "viewer"}</p><p>${who.role === "approver" ? "You can create, manage, and approve work in these projects." : "You can read work in these projects."}</p><ul>${projects.map(repo => `<li>${escape(projectName(repo))}</li>`).join("")}</ul><p class="meta">An instance operator manages invitations and project access.</p><p><a href="${escape(personHref(who.name))}">Your remote activity</a></p>`, { chrome: chromeFor(project, "people") }));
|
|
2760
2816
|
}
|
|
2761
2817
|
const approverView = store.isInstanceOperator(who.name);
|
|
2762
2818
|
const accounts = store.accountFacts().filter(one => approverView || one.name === who.name);
|
|
@@ -2787,6 +2843,7 @@ export function createDecisionServer(options) {
|
|
|
2787
2843
|
`<p class="meta">${one.projects === null ? "All projects" : one.projects.length === 0 ? "No project access" : one.projects.map(repo => escape(projectName(repo))).join(", ")}</p>`,
|
|
2788
2844
|
approverView && one.revokedAt === null ? `<details><summary>Edit project access</summary><form method="post" action="/people/projects">${hiddenFields({ csrf, name: one.name })}${accessFields(one.projects)}<label>Your password<input type="password" name="token" autocomplete="current-password" required></label><p class="meta">Changing access signs this person out and ends their derived sessions and modes. Previously approved work stays recorded.</p><button type="submit">Save project access</button></form></details>` : "",
|
|
2789
2845
|
`<p class="meta">${seen === null ? "not signed in right now" : `signed in \u2014 active ${escape(new Date(seen).toISOString().slice(11, 16))} UTC`} \u00b7 joined ${escape(one.addedAt.slice(0, 10))}</p>`,
|
|
2846
|
+
`<p><a href="${escape(personHref(one.name))}">Tokens and remote activity</a></p>`,
|
|
2790
2847
|
attended.length === 0
|
|
2791
2848
|
? ""
|
|
2792
2849
|
: `<p class="row">watching now: ${attended.map(session => `<a href="/t/${escape(session.taskId)}">${escape(session.taskId)}</a>`).join(", ")}</p>`,
|
|
@@ -3009,6 +3066,26 @@ export function createDecisionServer(options) {
|
|
|
3009
3066
|
const chrome = chromeFor(project, "tasks");
|
|
3010
3067
|
return sendScreen(response, 200, newTaskPage(chrome, project, csrf, revision, null, chainable, store.permissionDefault().mode, store.qualityDefault().mode, chrome.projects ?? []));
|
|
3011
3068
|
}
|
|
3069
|
+
const taskLive = matchTaskPath(url.pathname, "/live$");
|
|
3070
|
+
if (taskLive !== null) {
|
|
3071
|
+
// The live task page: a nudge the moment the task changes, and who else
|
|
3072
|
+
// has it open. Hints only — the page reads the task the usual way.
|
|
3073
|
+
if (who.via !== "cookie")
|
|
3074
|
+
return respond(response, 403, "application/json", JSON.stringify({ error: "session" }));
|
|
3075
|
+
const family = familyOf(taskLive.taskId);
|
|
3076
|
+
if (family === null || family.problem !== null)
|
|
3077
|
+
return respond(response, 404, "application/json", JSON.stringify({ error: "task" }));
|
|
3078
|
+
response.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-store", "x-content-type-options": "nosniff", "x-accel-buffering": "no" });
|
|
3079
|
+
const name = who.name, repo = family.root.repo;
|
|
3080
|
+
taskRooms.join(family.root.id, {
|
|
3081
|
+
name, response,
|
|
3082
|
+
// Rechecked while open, from the cookie and the account (never a request's context): a sign-out or a narrowed account stops it.
|
|
3083
|
+
valid: () => { const again = identify(request, false); return again !== null && again.name === name && rowVisible(liveCeiling(), repo) && store.accountCanAccess(name, repo); },
|
|
3084
|
+
});
|
|
3085
|
+
request.once("close", () => { if (!response.writableEnded)
|
|
3086
|
+
response.end(); });
|
|
3087
|
+
return;
|
|
3088
|
+
}
|
|
3012
3089
|
const task = matchTaskPath(url.pathname, "");
|
|
3013
3090
|
if (task !== null) {
|
|
3014
3091
|
const family = familyOf(task.taskId);
|
|
@@ -3065,7 +3142,7 @@ export function createDecisionServer(options) {
|
|
|
3065
3142
|
if (tokenId === null)
|
|
3066
3143
|
return true;
|
|
3067
3144
|
const token = store.apiTokenSecret(tokenId)?.row;
|
|
3068
|
-
return token !== undefined &&
|
|
3145
|
+
return token !== undefined && tokenLive(token, Date.now());
|
|
3069
3146
|
};
|
|
3070
3147
|
// The whole walk it starts from is shared by exports in the same minute; a reader who stops reading for a minute is let go.
|
|
3071
3148
|
const pieces = ledgerExportChunks(store, { from, to }, { repos: access.repos, instance: store.isInstanceOperator(who.name) }, access, who.name, now, evidenceRoot);
|
|
@@ -3793,7 +3870,8 @@ export function createDecisionServer(options) {
|
|
|
3793
3870
|
// Console v2: the landing's live view (who is working, four counts, Catch up in tabs).
|
|
3794
3871
|
const home = focusTask === null && chatProject === null && !roomId ? chatHomeOf(who, repos, now) : null;
|
|
3795
3872
|
const withFirstRun = (shown) => firstRun === undefined && phone === undefined && home === null ? shown
|
|
3796
|
-
: { ...shown,
|
|
3873
|
+
: { ...shown, ...(home === null ? {} : { refreshSeconds: liveRefreshSeconds() }),
|
|
3874
|
+
workspace: { ...shown.workspace, ...(firstRun === undefined ? {} : { firstRun }), ...(phone === undefined ? {} : { phone }), ...(home === null ? {} : { home }) } };
|
|
3797
3875
|
if (enabled.ok && mateSession !== null && principal !== null && !ceilingStale) {
|
|
3798
3876
|
{
|
|
3799
3877
|
const said = takeMateNote(who.session.csrf, mateSession.id);
|
|
@@ -4034,7 +4112,8 @@ export function createDecisionServer(options) {
|
|
|
4034
4112
|
const everyone = url.searchParams.get("everyone") === "1" && store.isInstanceOperator(who.name);
|
|
4035
4113
|
const here = sessions.hashOf(who.session);
|
|
4036
4114
|
const view = { who: who.name, everyone, canSeeEveryone: store.isInstanceOperator(who.name), now: Date.now(),
|
|
4037
|
-
sessions: store.webSessions(everyone ? null : who.name).map(one => ({ ...one, here: one.idHash === here })), tokens: store.apiTokens(everyone ? null : who.name)
|
|
4115
|
+
sessions: store.webSessions(everyone ? null : who.name).map(one => ({ ...one, here: one.idHash === here })), tokens: store.apiTokens(everyone ? null : who.name),
|
|
4116
|
+
limits: { note: tokenLimitWords.bind(null, store.handle), section: store.isInstanceOperator(who.name) ? requestLimitsHtml(store.handle, store.apiTokens(null), who.session.csrf, Date.now()) : "" } };
|
|
4038
4117
|
return sendScreen(response, 200, screen("Sessions & tokens", `<p><a href="/settings">Settings</a></p><h1>Sessions & tokens</h1>${credentialsHtml(view, who.session.csrf, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") })}`, { chrome: chromeFor(project, "settings") }));
|
|
4039
4118
|
}
|
|
4040
4119
|
// Settings → Flows: the starter flows, each switched on with one yes. ?starter= marks the one a task's
|
|
@@ -5124,17 +5203,24 @@ export function createDecisionServer(options) {
|
|
|
5124
5203
|
catch {
|
|
5125
5204
|
notices.push('Crew updates are unavailable. Open Tasks to inspect saved work.');
|
|
5126
5205
|
}
|
|
5127
|
-
//
|
|
5128
|
-
// waiting on a person behind its Tasks link — the open project's, or
|
|
5129
|
-
// everything this person may see — so it matches that page's Needs you tab.
|
|
5130
|
-
let needsYou = 0;
|
|
5206
|
+
// A running row says what its agent did last, and when (the task page's own line).
|
|
5131
5207
|
try {
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5208
|
+
const now = clock(), live = new Map();
|
|
5209
|
+
if (crew.crew.length > 0)
|
|
5210
|
+
for (const run of store.liveRuns(now)) {
|
|
5211
|
+
const root = familyOf(run.taskId)?.root.id;
|
|
5212
|
+
if (root !== undefined && !live.has(root))
|
|
5213
|
+
live.set(root, runActivityOf(store, run, now));
|
|
5214
|
+
}
|
|
5215
|
+
if (live.size > 0)
|
|
5216
|
+
crew = { ...crew, crew: crew.crew.map(item => { const activity = live.get(item.id); return activity === undefined ? item : { ...item, activity }; }) };
|
|
5217
|
+
}
|
|
5218
|
+
catch { /* the rows keep their status; the line returns on the next read */ }
|
|
5219
|
+
// "Wake me only for these": the navigation carries the sidebar's own
|
|
5220
|
+
// Tasks count (chromeFor) and links to the project view it covers, so
|
|
5221
|
+
// it matches that page's Needs you tab on every screen.
|
|
5222
|
+
const needsYou = s.chrome.inboxCount;
|
|
5223
|
+
const tasksProject = s.chrome.inboxProject === undefined ? s.chrome.project : s.chrome.inboxProject;
|
|
5138
5224
|
// The person's own project and task conversations (v77), for the
|
|
5139
5225
|
// sidebar's chat list; a thread whose project is out of view is left out.
|
|
5140
5226
|
let chats = [];
|
|
@@ -5170,7 +5256,7 @@ export function createDecisionServer(options) {
|
|
|
5170
5256
|
catchUpHtml: extras.catchUpHtml ?? '', controlsHtml: extras.controlsHtml ?? '', notices, view: extras.view ?? null,
|
|
5171
5257
|
...(s.chrome.demo ? { demo: { text: DEMO_BANNER, short: DEMO_BANNER_SHORT } } : {}),
|
|
5172
5258
|
pageHtml: extras.pageHtml === undefined ? (conversation === null ? pageHtml : null) : extras.pageHtml,
|
|
5173
|
-
navigation: [...browserNavigationOf(currentPath, s.chrome.project, needsYou), { label: 'Workspace tools', href: '/menu', active: path.pathname === '/menu' }],
|
|
5259
|
+
navigation: [...browserNavigationOf(currentPath, s.chrome.project, needsYou, tasksProject, s.chrome.inboxLabel), { label: 'Workspace tools', href: '/menu', active: path.pathname === '/menu' }],
|
|
5174
5260
|
chats,
|
|
5175
5261
|
...(s.refreshSeconds === undefined ? {} : { refreshSeconds: Math.max(5, Math.floor(s.refreshSeconds)) }),
|
|
5176
5262
|
...(s.chrome.signIn === undefined ? {} : { signIn: s.chrome.signIn }),
|
|
@@ -5225,12 +5311,6 @@ export function createDecisionServer(options) {
|
|
|
5225
5311
|
// functional script polls.
|
|
5226
5312
|
return page(response, status, html, nonce, s.functional?.fetches === true || chromeLayer || sensitiveChrome !== "");
|
|
5227
5313
|
}
|
|
5228
|
-
/** The badge cache: five seconds per project — mutations invalidate it. */
|
|
5229
|
-
const badgeCache = new Map();
|
|
5230
|
-
/** The needs-you count every surface wears for a project (workspace
|
|
5231
|
-
* package 1): Work's own Needs-you membership, cached per viewer and
|
|
5232
|
-
* project, so the rail, the phone tab, the scope bar, the project cards,
|
|
5233
|
-
* the chat overview, and the Needs-you tab never disagree. */
|
|
5234
5314
|
/** The Chat landing's live view: each agent at work now with its task and
|
|
5235
5315
|
* phase, four counts, plan-window use (never dollars: subscriptions don't
|
|
5236
5316
|
* bill per run) and Catch up's items, each tagged with its tab. Reads only. */
|
|
@@ -5240,7 +5320,8 @@ export function createDecisionServer(options) {
|
|
|
5240
5320
|
const agents = store.liveRuns(now).filter(run => admitted(run.repo)).map(run => {
|
|
5241
5321
|
const root = familyOf(run.taskId)?.root ?? null;
|
|
5242
5322
|
return { runId: run.id, taskId: root?.id ?? run.taskId, title: root?.title ?? run.title, href: taskHref(root?.id ?? run.taskId),
|
|
5243
|
-
agent: `${providerName(run.provider)} on ${run.runner}`, phase: homePhaseWords(run), project: run.repo === null ? null : projectName(run.repo), since: run.startedAt
|
|
5323
|
+
agent: `${providerName(run.provider)} on ${run.runner}`, phase: homePhaseWords(run), project: run.repo === null ? null : projectName(run.repo), since: run.startedAt,
|
|
5324
|
+
activity: runActivityOf(store, run, now) };
|
|
5244
5325
|
});
|
|
5245
5326
|
const all = workIndexPage(store, now, access, { view: "all", limit: WORK_INDEX_MAX_LIMIT });
|
|
5246
5327
|
const needs = workIndexPage(store, now, access, { view: "needs-you", limit: WORK_INDEX_MAX_LIMIT });
|
|
@@ -5274,19 +5355,13 @@ export function createDecisionServer(options) {
|
|
|
5274
5355
|
planUse: (windows?.tiles ?? []).map(one => ({ name: one.name, window: one.window, percent: one.percent, detail: one.detail, tone: one.tone })),
|
|
5275
5356
|
};
|
|
5276
5357
|
}
|
|
5358
|
+
/** The needs-you count every surface wears, read from this request's own
|
|
5359
|
+
* Work counts (one query per request, never a cached earlier one), so a
|
|
5360
|
+
* live page's badge and its Needs you tab always agree. */
|
|
5277
5361
|
function needsYouBadge(project) {
|
|
5278
|
-
|
|
5279
|
-
const key = `${actor ?? ""}:${actor === undefined ? "" : store.accountOf(actor)?.generation}:${project ?? ""}`;
|
|
5280
|
-
const cached = badgeCache.get(key);
|
|
5281
|
-
if (cached !== undefined && Date.now() - cached.at <= 5_000)
|
|
5282
|
-
return cached;
|
|
5283
|
-
const counted = needsYouCount(project);
|
|
5284
|
-
const badge = { at: Date.now(), count: counted.count, saturated: counted.saturated };
|
|
5285
|
-
badgeCache.set(key, badge);
|
|
5286
|
-
return badge;
|
|
5362
|
+
return needsYouCount(project);
|
|
5287
5363
|
}
|
|
5288
5364
|
const bustBadge = () => {
|
|
5289
|
-
badgeCache.clear();
|
|
5290
5365
|
paletteCache.clear();
|
|
5291
5366
|
};
|
|
5292
5367
|
/** The sidebar's facts for this request. */
|
|
@@ -5294,7 +5369,12 @@ export function createDecisionServer(options) {
|
|
|
5294
5369
|
if (restricted() && !visible(project))
|
|
5295
5370
|
project = null;
|
|
5296
5371
|
const actor = requestContext.getStore()?.actor;
|
|
5297
|
-
|
|
5372
|
+
// One Tasks count per request: Needs you in the request's project view
|
|
5373
|
+
// (the open project, else every admitted project), never the page's own
|
|
5374
|
+
// project — a flow or task in another project shows the same number.
|
|
5375
|
+
const lens = requestContext.getStore()?.lens;
|
|
5376
|
+
const inboxProject = lens !== undefined && (!restricted() || visible(lens)) ? lens : project;
|
|
5377
|
+
const badge = needsYouBadge(inboxProject);
|
|
5298
5378
|
const liveMode = project === null ? null : store.activeMode(project, clock());
|
|
5299
5379
|
const liveModeTerms = liveMode === null ? null : modeTermsFromJson(liveMode.termsJson);
|
|
5300
5380
|
let projectPeek = project === null ? null : undefined;
|
|
@@ -5334,6 +5414,9 @@ export function createDecisionServer(options) {
|
|
|
5334
5414
|
...(facts === undefined ? {} : { csrf: facts.csrf, returnTo: facts.returnTo }),
|
|
5335
5415
|
inboxCount: badge.count,
|
|
5336
5416
|
inboxSaturated: badge.saturated,
|
|
5417
|
+
inboxProject,
|
|
5418
|
+
inboxLabel: needsYouLabelOf(badge.count, inboxProject === null ? null
|
|
5419
|
+
: store.listProjects().find(one => one.path === inboxProject)?.name ?? projectName(inboxProject), badge.saturated),
|
|
5337
5420
|
settings: true,
|
|
5338
5421
|
...(store.isDemo() ? { demo: true } : {}),
|
|
5339
5422
|
...(() => { const signIn = signInNotices(store); return signIn.length === 0 ? {} : { signIn }; })(),
|
|
@@ -5387,6 +5470,10 @@ export function createDecisionServer(options) {
|
|
|
5387
5470
|
.reduce((sum, one) => sum + one.totals['needs-you'], 0);
|
|
5388
5471
|
return { count, saturated: false };
|
|
5389
5472
|
}
|
|
5473
|
+
/** A live page's own beat without a docked chat: 10 s while any admitted task is building, else 30 s. */
|
|
5474
|
+
function liveRefreshSeconds() {
|
|
5475
|
+
return projectCounts(clock()).some(one => one.totals.running > 0) ? 10 : 30;
|
|
5476
|
+
}
|
|
5390
5477
|
function projectFamilyPeek(repo, now) {
|
|
5391
5478
|
const row = projectCounts(now).find(one => one.repo === repo);
|
|
5392
5479
|
return { waiting: row?.totals['needs-you'] ?? 0, running: row?.totals.running ?? 0,
|
|
@@ -6190,6 +6277,9 @@ export function createDecisionServer(options) {
|
|
|
6190
6277
|
// What went wrong reads the family's latest finished attempt, as the Tasks row does.
|
|
6191
6278
|
presentedData.failure = failureOf(presentedData.family?.runs ?? data.runs, data.assignment ?? null);
|
|
6192
6279
|
presentedData.runChecks = runChecksHere(data.assignment?.receipt?.runId ?? null, who, taskHref(taskId));
|
|
6280
|
+
const liveRun = data.liveRunId == null ? undefined : data.runs.find(one => one.id === data.liveRunId);
|
|
6281
|
+
if (liveRun !== undefined)
|
|
6282
|
+
presentedData.activity = runActivityOf(store, liveRun, clock());
|
|
6193
6283
|
presentedData.demo = store.isDemo();
|
|
6194
6284
|
const paneProject = restricted() ? store.lookupRef(taskId)?.repo ?? null : who.via === "cookie" ? who.session.project : null;
|
|
6195
6285
|
const page = taskPage(paneProject === null && !unscopedMode
|
|
@@ -6200,11 +6290,19 @@ export function createDecisionServer(options) {
|
|
|
6200
6290
|
const docked = focus === null ? null : dockedConversation(who, focus, null, clock(), taskHref(focus.id));
|
|
6201
6291
|
// The conversation is the page's own thread here, so the page drops its Ask tab;
|
|
6202
6292
|
// only an approver is offered a road to message the agent.
|
|
6203
|
-
const taskView = page.workspace?.view?.kind === "task" ? { ...page.workspace.view, ...(docked === null ? {} : { tabs: [] }), ...(who.role === "approver" ? {} : { chatHref: null })
|
|
6293
|
+
const taskView = page.workspace?.view?.kind === "task" ? { ...page.workspace.view, ...(docked === null ? {} : { tabs: [] }), ...(who.role === "approver" ? {} : { chatHref: null }),
|
|
6294
|
+
// A signed-in page hears the moment the task changes, and who else has it open.
|
|
6295
|
+
...(who.via === "cookie" ? { live: { href: `${taskHref(family?.root.id ?? taskId)}/live`, at: (() => { try {
|
|
6296
|
+
return taskFingerprint(store, family?.root.id ?? taskId, clock());
|
|
6297
|
+
}
|
|
6298
|
+
catch {
|
|
6299
|
+
return null;
|
|
6300
|
+
} })() } } : {}) } : page.workspace?.view;
|
|
6204
6301
|
if (docked !== null)
|
|
6205
6302
|
page.workspace = { ...page.workspace, ...(taskView === undefined ? {} : { view: taskView }), conversation: docked, pageHtml: page.body };
|
|
6206
6303
|
else if (taskView !== undefined && page.workspace !== undefined)
|
|
6207
6304
|
page.workspace = { ...page.workspace, view: taskView };
|
|
6305
|
+
page.refreshSeconds = liveRefreshSeconds();
|
|
6208
6306
|
return sendScreen(response, status, page);
|
|
6209
6307
|
}
|
|
6210
6308
|
/** The routine screen: the standing order restated, its verbs, its ledger. */
|
|
@@ -7472,6 +7570,31 @@ export function createDecisionServer(options) {
|
|
|
7472
7570
|
store.setApprovalRules(repo, next, who.name, now);
|
|
7473
7571
|
return back("said", `Saved. ${rulesSummary(next)}`);
|
|
7474
7572
|
}
|
|
7573
|
+
// v112: request limits for API tokens (request-budget.ts). An instance operator, in a browser, with their password;
|
|
7574
|
+
// never over /api/cli or /mcp (a token never reaches a console form).
|
|
7575
|
+
if (url.pathname === "/settings/request-limits") {
|
|
7576
|
+
const body = readForm(posted, CONSOLE_FORMS.requestLimits);
|
|
7577
|
+
if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
|
|
7578
|
+
return refuse(response, who, 403, "An instance operator sets request limits.", "/settings/sessions");
|
|
7579
|
+
const back = (key, words) => redirect(response, `/settings/sessions?${key}=${encodeURIComponent(words)}`);
|
|
7580
|
+
const target = body.get("target") ?? "";
|
|
7581
|
+
const token = target === "*" ? null : store.apiTokens(null).find(one => one.id === target && one.revokedAt === null) ?? null;
|
|
7582
|
+
if (target !== "*" && token === null)
|
|
7583
|
+
return back("problem", "That token is no longer live. Nothing changed.");
|
|
7584
|
+
if (!authenticateApprover(store, who.name, body.get("password") ?? "").ok)
|
|
7585
|
+
return back("problem", "Enter your Toolroll password to change request limits.");
|
|
7586
|
+
const read = (name) => parseLimit(body.get(name) ?? "", name === "per-day" ? REQUEST_BUDGET_PER_DAY_MAX : REQUEST_BUDGET_PER_MINUTE_MAX);
|
|
7587
|
+
const clear = body.get("action") === "clear";
|
|
7588
|
+
const fields = clear ? { readPerMinute: null, actPerMinute: null, perDay: null }
|
|
7589
|
+
: token === null ? { readPerMinute: read("read-per-minute"), actPerMinute: read("act-per-minute"), perDay: read("per-day") }
|
|
7590
|
+
: { readPerMinute: token.access === "read" ? read("per-minute") : null, actPerMinute: token.access === "act" ? read("per-minute") : null, perDay: read("per-day") };
|
|
7591
|
+
const problem = Object.values(fields).find((value) => value !== null && typeof value === "object");
|
|
7592
|
+
if (problem !== undefined)
|
|
7593
|
+
return back("problem", `${problem.problem} Nothing changed.`);
|
|
7594
|
+
setLimitOverride(store, target, fields, who.name, now);
|
|
7595
|
+
const label = token === null ? "Everyone's tokens" : token.name;
|
|
7596
|
+
return back("said", clear || Object.values(fields).every(value => value === null) ? `${label}: default limits.` : `${label}: limits saved.`);
|
|
7597
|
+
}
|
|
7475
7598
|
if (url.pathname === "/settings/sessions") {
|
|
7476
7599
|
const body = readForm(posted, CONSOLE_FORMS.sessions);
|
|
7477
7600
|
if (who.via !== "cookie")
|
|
@@ -11780,10 +11903,32 @@ export function createDecisionServer(options) {
|
|
|
11780
11903
|
approved: (() => { const scope = store.getScope(taskId); return scope !== null && approvalOf(scope).approved; })(),
|
|
11781
11904
|
}));
|
|
11782
11905
|
}
|
|
11783
|
-
const mcpHttp = createMcpHttp({ store, clock, evidenceRoot, signedIn: request => identify(request) !== null,
|
|
11906
|
+
const mcpHttp = createMcpHttp({ store, clock, evidenceRoot, signedIn: request => identify(request, true, true) !== null, admit: person => requestBudget.admit(person.principal.tokenId, "mcp"),
|
|
11907
|
+
resourceMetadata: request => { const origin = consoleOrigin(request.headers.host); return origin === null ? null : resourceMetadataUrl(origin); },
|
|
11784
11908
|
enrolled: () => [...new Set([...managedRepos(), ...store.listProjects().map(project => project.path)])], ...(options.runOperateAs === undefined ? {} : { runAs: options.runOperateAs }) });
|
|
11909
|
+
// MCP sign-in: the console's own sign-in and step-up, a same-site consent form, and the person's current projects.
|
|
11910
|
+
const oauthHttp = createOAuthHttp({ store, clock,
|
|
11911
|
+
originOf: request => consoleOrigin(request.headers.host),
|
|
11912
|
+
requesterKey: joinSourceOf,
|
|
11913
|
+
session: request => {
|
|
11914
|
+
const who = identify(request, false);
|
|
11915
|
+
if (who === null || who.via !== "cookie")
|
|
11916
|
+
return null;
|
|
11917
|
+
return { name: who.name, role: who.role, csrf: who.session.csrf,
|
|
11918
|
+
sso: who.session.sso === undefined ? null : { label: ssoSettings()?.label ?? "your identity provider", fresh: Date.now() - who.session.sso.at < SSO_FRESH_MS } };
|
|
11919
|
+
},
|
|
11920
|
+
sameSite: request => {
|
|
11921
|
+
const origin = request.headers.origin, referer = request.headers.referer;
|
|
11922
|
+
const named = typeof origin === "string" && origin !== "null" ? origin : typeof referer === "string" ? referer : null;
|
|
11923
|
+
return named !== null && allowedHost(named.replace(/^https?:\/\//, "").split("/")[0]);
|
|
11924
|
+
},
|
|
11925
|
+
// The step-up for a credential: the person's password, or (empty) a fresh identity-provider check. What they may
|
|
11926
|
+
// grant (their role, their projects) is checked beside it.
|
|
11927
|
+
confirm: (session, typed) => typed === "" ? session.sso?.fresh === true : authenticateAccount(store, session.name, typed).ok,
|
|
11928
|
+
projectsFor: account => store.knownRepos().filter(repo => store.accountCanAccess(account, repo)).sort(),
|
|
11929
|
+
});
|
|
11785
11930
|
// ---- identity ------------------------------------------------------------
|
|
11786
|
-
function identify(request, touch = true) {
|
|
11931
|
+
function identify(request, touch = true, limited = false) {
|
|
11787
11932
|
// v101: an API token. Wrong ones spend the address's tries like a wrong password; expired or revoked ones, and a removed account's, name no one.
|
|
11788
11933
|
const presented = /^Bearer (so_\S+)$/.exec(request.headers.authorization ?? "")?.[1];
|
|
11789
11934
|
if (presented !== undefined) {
|
|
@@ -11796,11 +11941,18 @@ export function createDecisionServer(options) {
|
|
|
11796
11941
|
signInBudget.failed(source, at);
|
|
11797
11942
|
return null;
|
|
11798
11943
|
}
|
|
11799
|
-
if (
|
|
11944
|
+
if (!tokenLive(kept.row, at))
|
|
11945
|
+
return null;
|
|
11946
|
+
// v111: a token limited to some projects signs in only where its limit travels with it (the remote CLI and MCP,
|
|
11947
|
+
// through Principal.projects); the console's own pages and APIs check the account's access alone, so it is refused there.
|
|
11948
|
+
if (kept.row.projects !== null && !limited)
|
|
11800
11949
|
return null;
|
|
11801
11950
|
const account = store.accountOf(kept.row.account);
|
|
11802
11951
|
if (account === null || account.revokedAt !== null)
|
|
11803
11952
|
return null;
|
|
11953
|
+
// v113: an MCP sign-in's token works only at /mcp, while its access secret is fresh (mcp-oauth.ts).
|
|
11954
|
+
if (!oauthTokenAllowed(store, kept.row.id, new URL(request.url ?? "/", "http://placeholder").pathname, new Date(at)))
|
|
11955
|
+
return null;
|
|
11804
11956
|
store.touchApiToken(kept.row.id, new Date(at));
|
|
11805
11957
|
return { name: kept.row.account, via: "bearer", role: kept.row.access === "read" ? "viewer" : account.role, token: kept.row.name };
|
|
11806
11958
|
}
|
|
@@ -12395,6 +12547,10 @@ export function createDecisionServer(options) {
|
|
|
12395
12547
|
const closeServer = server.close.bind(server);
|
|
12396
12548
|
server.close = ((callback) => {
|
|
12397
12549
|
leadClosing = true;
|
|
12550
|
+
try {
|
|
12551
|
+
requestBudget.flush();
|
|
12552
|
+
}
|
|
12553
|
+
catch { /* saved again on the close event when it can be */ }
|
|
12398
12554
|
for (const stream of teamStreams)
|
|
12399
12555
|
stream.end();
|
|
12400
12556
|
teamStreams.clear();
|
|
@@ -12402,6 +12558,7 @@ export function createDecisionServer(options) {
|
|
|
12402
12558
|
stream.end();
|
|
12403
12559
|
chatStreams.clear();
|
|
12404
12560
|
flowRooms.close();
|
|
12561
|
+
taskRooms.close();
|
|
12405
12562
|
void Promise.all([closeCoding(), bounded(team.close()), bounded(leadMaintenance?.stop())]).then(() => closeServer(callback)).catch(error => {
|
|
12406
12563
|
if (callback)
|
|
12407
12564
|
callback(error instanceof Error ? error : Error('Coding session shutdown failed.'));
|
|
@@ -16304,7 +16461,7 @@ const INBOX_TABS_CSS = '.inbox-tabs{display:inline-flex;gap:2px;max-width:100%;o
|
|
|
16304
16461
|
'.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}' +
|
|
16305
16462
|
'.inbox-unread{display:none;position:absolute;top:4px;right:3px;width:6px;height:6px;border-radius:50%;background:var(--so-signal)}' +
|
|
16306
16463
|
'@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}}';
|
|
16307
|
-
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 + TEAMMATE_CSS + KITS_CSS + STARTERS_CSS + GALLERY_CSS + SSO_CSS + CREDENTIALS_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}');
|
|
16464
|
+
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 + TEAMMATE_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}');
|
|
16308
16465
|
/**
|
|
16309
16466
|
* The board's liveness: fetch this page's own fragment on a timer and swap
|
|
16310
16467
|
* it in place — no flicker, no scroll reset, no long-lived stream to manage,
|
|
@@ -16674,9 +16831,9 @@ function shell(title, body, options = {}) {
|
|
|
16674
16831
|
// destination it now lives under. The count rides Work: it is the
|
|
16675
16832
|
// saturated needs-you count, exactly as the inbox row wore it.
|
|
16676
16833
|
const primary = chrome.active === "code" ? "work" : primaryDestinationOf(chrome.active);
|
|
16677
|
-
const primaryItem = (key, href, label, count) => `<a href="${href}" aria-label="${escape(label)}" title="${escape(label)}"${primary === key ? ' class="active" aria-current="page"' : ""}${key === "work" && count !== undefined ? ` data-waiting="${count}"` : ""}>` +
|
|
16834
|
+
const primaryItem = (key, href, label, count) => `<a href="${href}" aria-label="${escape(key === "work" && count !== undefined && count > 0 && chrome.inboxLabel !== undefined ? `${label}, ${chrome.inboxLabel}` : label)}" title="${escape(label)}"${primary === key ? ' class="active" aria-current="page"' : ""}${key === "work" && count !== undefined ? ` data-waiting="${count}"` : ""}>` +
|
|
16678
16835
|
`<span class="glyph">${NAV_ICONS[key] ?? ""}</span>${label}` +
|
|
16679
|
-
`${count !== undefined && count > 0 ? ` <span class="count badge badge-open">${count}${chrome.inboxSaturated ? "+" : ""}</span>` : ""}</a>`;
|
|
16836
|
+
`${count !== undefined && count > 0 ? ` <span${chrome.inboxLabel === undefined ? "" : ` aria-label="${escape(chrome.inboxLabel)}" title="${escape(chrome.inboxLabel)}"`} class="count badge badge-open">${count}${chrome.inboxSaturated ? "+" : ""}</span>` : ""}</a>`;
|
|
16680
16837
|
const side = [
|
|
16681
16838
|
`<aside class="side">`,
|
|
16682
16839
|
`<div class="side-head"><a class="brand" href="${chrome.chat === true ? "/chat" : "/work"}"><span class="brand-long">Toolroll</span><span class="brand-short">T</span></a>`,
|
|
@@ -16762,7 +16919,7 @@ function shell(title, body, options = {}) {
|
|
|
16762
16919
|
// A phone tab says THAT something waits, with a dot; the number is on
|
|
16763
16920
|
// the Work views themselves (Linear Mobile's rule — one tap away).
|
|
16764
16921
|
`<a href="${href}"${primary === key ? ' class="active" aria-current="page"' : ""}><span class="glyph">${TAB_ICONS[key]}</span>${label}` +
|
|
16765
|
-
`${count !== undefined && count > 0 ? `<span class="dot-badge" role="img" aria-label="${count} waiting"></span>` : ""}</a>`;
|
|
16922
|
+
`${count !== undefined && count > 0 ? `<span class="dot-badge" role="img" aria-label="${escape(chrome.inboxLabel ?? `${count} waiting`)}"></span>` : ""}</a>`;
|
|
16766
16923
|
// Three primary tabs, the same three as the rail: chat where allowed,
|
|
16767
16924
|
// work, projects. Tools and settings sit behind the header's menu action.
|
|
16768
16925
|
const tabbar = [
|
|
@@ -20132,7 +20289,7 @@ function safeReturn(raw) {
|
|
|
20132
20289
|
* button can name. Nothing here is a second redirect framework: one
|
|
20133
20290
|
* same-site path, or "/".
|
|
20134
20291
|
*/
|
|
20135
|
-
const LOGIN_RETURN_PAGES = /^\/(t\/[^/]+|r\/[1-9]\d*|code(?:\/[a-f0-9]{32}(?:\/ship)?)?|review|chat|work|board|projects|routines|recipes|fleet|settings(\/[a-z-]+)?|mode)$/;
|
|
20292
|
+
const LOGIN_RETURN_PAGES = /^\/(oauth\/authorize|t\/[^/]+|r\/[1-9]\d*|code(?:\/[a-f0-9]{32}(?:\/ship)?)?|review|chat|work|board|projects|routines|recipes|fleet|settings(\/[a-z-]+)?|mode)$/;
|
|
20136
20293
|
function loginReturn(raw) {
|
|
20137
20294
|
const safe = safeReturn(raw);
|
|
20138
20295
|
if (safe === "/")
|
|
@@ -21930,6 +22087,8 @@ function taskBodyParts(data) {
|
|
|
21930
22087
|
retry,
|
|
21931
22088
|
runChecks,
|
|
21932
22089
|
progress,
|
|
22090
|
+
// One quiet line under the step: what the agent did last, and when.
|
|
22091
|
+
activity: liveRun === undefined ? null : data.activity ?? null,
|
|
21933
22092
|
stop,
|
|
21934
22093
|
record,
|
|
21935
22094
|
earlier,
|
package/dist/setup-guide.d.ts
CHANGED
|
@@ -15,10 +15,10 @@ export declare function detectPreparation(repo: string): {
|
|
|
15
15
|
} | null;
|
|
16
16
|
/** Browser installation may write only this known file inside its project. */
|
|
17
17
|
export declare function previewProjectInstructions(repo: string): {
|
|
18
|
+
content?: never;
|
|
18
19
|
ok: false;
|
|
19
20
|
message: string;
|
|
20
21
|
plan?: never;
|
|
21
|
-
content?: never;
|
|
22
22
|
fingerprint?: never;
|
|
23
23
|
installed?: never;
|
|
24
24
|
} | {
|
|
@@ -30,10 +30,10 @@ export declare function previewProjectInstructions(repo: string): {
|
|
|
30
30
|
installed: boolean;
|
|
31
31
|
};
|
|
32
32
|
export declare function addProjectInstructions(repo: string, fingerprint: string): {
|
|
33
|
+
content?: never;
|
|
33
34
|
ok: false;
|
|
34
35
|
message: string;
|
|
35
36
|
plan?: never;
|
|
36
|
-
content?: never;
|
|
37
37
|
fingerprint?: never;
|
|
38
38
|
installed?: never;
|
|
39
39
|
} | import("./skills.js").InstallResult;
|