toolroll 0.9.81 → 0.9.83
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 +35 -0
- package/dist/cli.js +1 -0
- package/dist/commit-extract.d.ts +18 -0
- package/dist/commit-extract.js +39 -1
- package/dist/converse.d.ts +12 -0
- package/dist/converse.js +18 -2
- package/dist/deploy-gate.js +8 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/eval-gate.d.ts +51 -0
- package/dist/eval-gate.js +199 -0
- package/dist/eval-hooks.d.ts +39 -0
- package/dist/eval-hooks.js +477 -0
- package/dist/eval-oracle-main.d.ts +1 -0
- package/dist/eval-oracle-main.js +31 -0
- package/dist/eval-oracle.d.ts +60 -0
- package/dist/eval-oracle.js +217 -0
- package/dist/eval-world.d.ts +17 -0
- package/dist/eval-world.js +97 -0
- package/dist/evals.d.ts +299 -0
- package/dist/evals.js +964 -0
- package/dist/exec.js +15 -1
- package/dist/first-tasks-child.d.ts +1 -0
- package/dist/first-tasks-child.js +11 -0
- package/dist/flow-draft.js +4 -0
- package/dist/flow-sort.js +4 -0
- package/dist/folder-look.d.ts +107 -0
- package/dist/folder-look.js +302 -0
- package/dist/github-jobs.d.ts +25 -0
- package/dist/github-jobs.js +22 -0
- package/dist/invoke.d.ts +1 -1
- package/dist/invoke.js +14 -0
- package/dist/lead.js +4 -1
- 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/memory-pass.js +4 -0
- package/dist/nightly-check.d.ts +4 -1
- package/dist/nightly-check.js +5 -2
- package/dist/nightly-github.d.ts +316 -0
- package/dist/nightly-github.js +1022 -0
- package/dist/operate.d.ts +123 -1
- package/dist/operate.js +344 -29
- package/dist/project-identity.d.ts +37 -0
- package/dist/project-identity.js +106 -0
- package/dist/project-knowledge.d.ts +4 -4
- 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/provider.d.ts +3 -0
- package/dist/provider.js +9 -2
- package/dist/release-adapters.d.ts +21 -21
- package/dist/release-adapters.js +52 -19
- package/dist/release-auto.js +26 -0
- package/dist/release-card.js +3 -0
- package/dist/release-cli.js +59 -3
- package/dist/release-view.d.ts +3 -0
- package/dist/release-view.js +2 -0
- package/dist/release.d.ts +16 -0
- package/dist/release.js +16 -0
- package/dist/reported-usage.d.ts +69 -0
- package/dist/reported-usage.js +97 -0
- package/dist/repos.d.ts +7 -0
- package/dist/repos.js +57 -0
- package/dist/serve.js +35 -8
- package/dist/server/chat.js +4 -0
- package/dist/server/flows.js +9 -3
- package/dist/server/guards.d.ts +4 -0
- package/dist/server/guards.js +12 -3
- package/dist/server/http.d.ts +11 -0
- package/dist/server/http.js +3 -0
- package/dist/server/pages.js +62 -88
- package/dist/server/render-pages.d.ts +2 -16
- package/dist/server/render-pages.js +1 -55
- package/dist/server/render-tasks.js +1 -0
- package/dist/server/runtime.d.ts +4 -0
- package/dist/server/runtime.js +1 -0
- package/dist/server/settings.js +14 -3
- package/dist/server/tasks.js +15 -4
- package/dist/store.d.ts +109 -1
- package/dist/store.js +394 -4
- package/dist/subagents.js +4 -0
- package/dist/subscription-chat.d.ts +2 -0
- package/dist/subscription-chat.js +52 -13
- package/dist/task-sizing.js +4 -0
- package/dist/teardown-git.d.ts +9 -2
- package/dist/teardown-git.js +56 -33
- package/dist/up-processes.d.ts +13 -0
- package/dist/up-processes.js +6 -2
- package/evals/cases/catalog/case.json +25 -0
- package/evals/cases/catalog/fixture/README.md +3 -0
- package/evals/cases/catalog/fixture/package.json +8 -0
- package/evals/cases/catalog/fixture/test/catalog.test.mjs +4 -0
- package/evals/cases/catalog/oracle.mjs +23 -0
- package/evals/cases/catalog/probe.mjs +10 -0
- package/evals/cases/catalog/source.md +9 -0
- package/evals/cases/csv-quoting/case.json +23 -0
- package/evals/cases/csv-quoting/fixture/README.md +3 -0
- package/evals/cases/csv-quoting/fixture/csv.js +1 -0
- package/evals/cases/csv-quoting/fixture/package.json +8 -0
- package/evals/cases/csv-quoting/fixture/test/csv.test.mjs +5 -0
- package/evals/cases/csv-quoting/oracle.mjs +14 -0
- package/evals/cases/csv-quoting/probe.mjs +9 -0
- package/evals/cases/csv-quoting/source.md +9 -0
- package/evals/cases/dedupe/case.json +23 -0
- package/evals/cases/dedupe/fixture/README.md +3 -0
- package/evals/cases/dedupe/fixture/dedupe.js +1 -0
- package/evals/cases/dedupe/fixture/package.json +8 -0
- package/evals/cases/dedupe/fixture/test/dedupe.test.mjs +5 -0
- package/evals/cases/dedupe/oracle.mjs +16 -0
- package/evals/cases/dedupe/probe.mjs +13 -0
- package/evals/cases/dedupe/source.md +9 -0
- package/evals/cases/endpoint/case.json +25 -0
- package/evals/cases/endpoint/fixture/README.md +3 -0
- package/evals/cases/endpoint/fixture/package.json +8 -0
- package/evals/cases/endpoint/fixture/test/endpoint.test.mjs +4 -0
- package/evals/cases/endpoint/oracle.mjs +27 -0
- package/evals/cases/endpoint/probe.mjs +10 -0
- package/evals/cases/endpoint/source.md +11 -0
- package/evals/cases/math-divide/case.json +23 -0
- package/evals/cases/math-divide/fixture/README.md +3 -0
- package/evals/cases/math-divide/fixture/package.json +8 -0
- package/evals/cases/math-divide/fixture/src/math.js +1 -0
- package/evals/cases/math-divide/fixture/test/math.test.js +5 -0
- package/evals/cases/math-divide/oracle.mjs +13 -0
- package/evals/cases/math-divide/probe.mjs +9 -0
- package/evals/cases/math-divide/source.md +9 -0
- package/evals/cases/math-multiply/case.json +23 -0
- package/evals/cases/math-multiply/fixture/README.md +3 -0
- package/evals/cases/math-multiply/fixture/package.json +8 -0
- package/evals/cases/math-multiply/fixture/src/math.js +2 -0
- package/evals/cases/math-multiply/fixture/test/math.test.js +5 -0
- package/evals/cases/math-multiply/oracle.mjs +12 -0
- package/evals/cases/math-multiply/probe.mjs +9 -0
- package/evals/cases/math-multiply/source.md +9 -0
- package/evals/cases/math-subtract/case.json +23 -0
- package/evals/cases/math-subtract/fixture/README.md +3 -0
- package/evals/cases/math-subtract/fixture/package.json +8 -0
- package/evals/cases/math-subtract/fixture/src/math.js +1 -0
- package/evals/cases/math-subtract/fixture/test/math.test.js +5 -0
- package/evals/cases/math-subtract/oracle.mjs +12 -0
- package/evals/cases/math-subtract/probe.mjs +9 -0
- package/evals/cases/math-subtract/source.md +9 -0
- package/evals/cases/merge-config/case.json +23 -0
- package/evals/cases/merge-config/fixture/README.md +3 -0
- package/evals/cases/merge-config/fixture/config.js +1 -0
- package/evals/cases/merge-config/fixture/package.json +8 -0
- package/evals/cases/merge-config/fixture/test/config.test.mjs +5 -0
- package/evals/cases/merge-config/oracle.mjs +28 -0
- package/evals/cases/merge-config/probe.mjs +15 -0
- package/evals/cases/merge-config/source.md +9 -0
- package/evals/cases/paginate/case.json +23 -0
- package/evals/cases/paginate/fixture/README.md +3 -0
- package/evals/cases/paginate/fixture/package.json +8 -0
- package/evals/cases/paginate/fixture/paginate.js +1 -0
- package/evals/cases/paginate/fixture/test/paginate.test.mjs +5 -0
- package/evals/cases/paginate/oracle.mjs +18 -0
- package/evals/cases/paginate/probe.mjs +13 -0
- package/evals/cases/paginate/source.md +9 -0
- package/evals/cases/round2-asks/case.json +24 -0
- package/evals/cases/round2-asks/fixture/README.md +3 -0
- package/evals/cases/round2-asks/fixture/package.json +8 -0
- package/evals/cases/round2-asks/fixture/src/math.js +1 -0
- package/evals/cases/round2-asks/fixture/test/math.test.js +5 -0
- package/evals/cases/round2-asks/source.md +11 -0
- package/evals/manifest.json +15 -0
- package/package.json +2 -1
package/dist/server/chat.js
CHANGED
|
@@ -1396,6 +1396,8 @@ export function createChatHandlers(runtime) {
|
|
|
1396
1396
|
const pinnedSettle = settleForPrice(enabled.price, result.answer.tokensIn, result.answer.tokensOut);
|
|
1397
1397
|
const settled = Math.max(pinnedSettle, result.answer.reportedCostMicrousd ?? 0);
|
|
1398
1398
|
const envelope = parseAssistantEnvelope(result.answer.text);
|
|
1399
|
+
// v140: what the provider's response body itself reported (absent: unreported, kept NULL).
|
|
1400
|
+
const reported = { modelReported: result.answer.modelReported ?? null, reportedCostUsd: result.answer.reportedCostUsd ?? null };
|
|
1399
1401
|
if (!envelope.ok) {
|
|
1400
1402
|
finish({
|
|
1401
1403
|
state: "failed",
|
|
@@ -1403,6 +1405,7 @@ export function createChatHandlers(runtime) {
|
|
|
1403
1405
|
tokensIn: result.answer.tokensIn,
|
|
1404
1406
|
tokensOut: result.answer.tokensOut,
|
|
1405
1407
|
settledMicrousd: settled,
|
|
1408
|
+
...reported,
|
|
1406
1409
|
});
|
|
1407
1410
|
tell(null, "the model's answer was malformed and was discarded");
|
|
1408
1411
|
return;
|
|
@@ -1437,6 +1440,7 @@ export function createChatHandlers(runtime) {
|
|
|
1437
1440
|
settledMicrousd: settled,
|
|
1438
1441
|
replyBytes: Buffer.byteLength(envelope.envelope.reply, "utf8"),
|
|
1439
1442
|
candidateCount: kept,
|
|
1443
|
+
...reported,
|
|
1440
1444
|
});
|
|
1441
1445
|
tell(envelope.envelope.reply, null, envelope.proposalsDiscarded);
|
|
1442
1446
|
}
|
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,8 +839,14 @@ 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
|
-
if (rootMode
|
|
843
|
-
|
|
842
|
+
if (rootMode) {
|
|
843
|
+
// A mutation's target is resolved now, off the console's thread, and must still be the project it names.
|
|
844
|
+
const resolved = await identities.resolveNow(project);
|
|
845
|
+
if (resolved === "no-answer")
|
|
846
|
+
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);
|
|
847
|
+
if (resolved !== project || !(await authorizedProject(liveCeiling(), resolved)))
|
|
848
|
+
throw new RecipeError("The project is outside this server's access.", 403);
|
|
849
|
+
}
|
|
844
850
|
const rechecked = authorizeMutation(request, who, readForm(posted, CONSOLE_FORMS.mutationGuard));
|
|
845
851
|
if (rechecked !== null)
|
|
846
852
|
throw new RecipeError(rechecked.message, rechecked.status);
|
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;
|
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;
|
package/dist/server/pages.js
CHANGED
|
@@ -5,10 +5,11 @@ import { handlersOf } from './handler-registry.js';
|
|
|
5
5
|
import { html, joinHtml, postForm, reauth } from "../html.js";
|
|
6
6
|
/** pages handlers, moved without changing their route bodies. */
|
|
7
7
|
import { createHmac, randomBytes } from "node:crypto";
|
|
8
|
-
import {
|
|
8
|
+
import { realpathSync } from "node:fs";
|
|
9
9
|
import {} from "node:http";
|
|
10
10
|
import { homedir, hostname } from "node:os";
|
|
11
|
-
import { dirname, join } from "node:path";
|
|
11
|
+
import { dirname, join, resolve } from "node:path";
|
|
12
|
+
import { listFolder, localOrigins } from "../folder-look.js";
|
|
12
13
|
import { Readable } from "node:stream";
|
|
13
14
|
import { pipeline } from "node:stream/promises";
|
|
14
15
|
import { setImmediate as yieldEventLoop } from "node:timers/promises";
|
|
@@ -49,13 +50,13 @@ import { refuse, regionScript, screen, settingsRows, WORKSPACE_STYLE, workToolRo
|
|
|
49
50
|
import { redirect, respond, safeReturn, SAFETY, taskHref } from "./http.js";
|
|
50
51
|
import { assignmentOf } from "../assignment.js";
|
|
51
52
|
import { assignmentTaskStatusOf } from "../assignment-presentation.js";
|
|
52
|
-
import { browsePage, fleetBody, fleetScript, githubIdentityOf, githubReposPage, homePage,
|
|
53
|
+
import { browsePage, fleetBody, fleetScript, githubIdentityOf, githubReposPage, homePage, portfolioOverview, projectsPage, systemPage, workbenchRail } from "./render-pages.js";
|
|
53
54
|
import { capsPage } from "./render-settings.js";
|
|
54
55
|
import { decisionAnswerScript, taskBody } from "./render-tasks.js";
|
|
55
56
|
import {} from "./session.js";
|
|
56
57
|
import { rereadBrowser } from "./live.js";
|
|
57
58
|
export function createPagesHandlers(runtime) {
|
|
58
|
-
const { clock, lastChecks, store, visible, consoleProjects, restricted, ceiling, options, projectFamilyPeek, sendScreen, chromeFor, unscopedMode, admissionList, taskRepoOf, taskViewData, runVisible, evidenceRoot, workAccess, identify, liveCeiling, sessions, deletedRepos, authenticateApprover, authenticateCredential, projectOf, lookupSession } = runtime;
|
|
59
|
+
const { identities, clock, lastChecks, store, visible, consoleProjects, restricted, ceiling, options, projectFamilyPeek, sendScreen, chromeFor, unscopedMode, admissionList, taskRepoOf, taskViewData, runVisible, evidenceRoot, workAccess, identify, liveCeiling, sessions, deletedRepos, authenticateApprover, authenticateCredential, projectOf, lookupSession } = runtime;
|
|
59
60
|
// The filesystem browser (operator request): pick a project folder by
|
|
60
61
|
// looking, not typing. CONFINED to exactly what opening allows —
|
|
61
62
|
// explicit --repo mode has an enumerable list and gets no browser at
|
|
@@ -67,40 +68,32 @@ export function createPagesHandlers(runtime) {
|
|
|
67
68
|
const { url, who, response } = ctx;
|
|
68
69
|
if (who.via !== "cookie")
|
|
69
70
|
return refuse(response, who, 403, "browsing feeds a browser session's act");
|
|
70
|
-
const browseRoots = ceiling.roots.length > 0 ? [...ceiling.roots] : unscopedMode ? [
|
|
71
|
+
const browseRoots = ceiling.roots.length > 0 ? [...ceiling.roots] : unscopedMode ? [identities.hot(homedir())] : [];
|
|
71
72
|
if (browseRoots.length === 0) {
|
|
72
73
|
return refuse(response, who, 404, "this server was configured with an explicit repo list — the openable projects are all on the projects page", "/projects");
|
|
73
74
|
}
|
|
74
75
|
const asked = url.searchParams.get("at") ?? browseRoots[0];
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (canonical === null || !inside) {
|
|
76
|
+
const within = (path) => browseRoots.some(root => path === root || path.startsWith(`${root}/`));
|
|
77
|
+
// Spelled outside every root: refused before anything is read. Inside, the look resolves links and checks again.
|
|
78
|
+
if (!within(resolve(asked))) {
|
|
79
79
|
return refuse(response, who, 403, "that path is outside what this server may browse", "/projects/browse");
|
|
80
80
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
.map(one => {
|
|
88
|
-
const path = join(canonical, one.name);
|
|
89
|
-
// Resolve NOW so a symlink pointing outside the fence renders
|
|
90
|
-
// as nothing rather than as a door.
|
|
91
|
-
const resolved = canonicalProject(path);
|
|
92
|
-
if (resolved === null || !browseRoots.some(root => resolved === root || resolved.startsWith(`${root}/`))) {
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
return { name: one.name, path: resolved, git: existsSync(join(resolved, ".git")) };
|
|
96
|
-
})
|
|
97
|
-
.filter((one) => one !== null)
|
|
98
|
-
.sort((a, b) => Number(b.git) - Number(a.git) || a.name.localeCompare(b.name))
|
|
99
|
-
.slice(0, 200);
|
|
81
|
+
// Read off the console's thread (folder-look.ts): a folder that doesn't answer never freezes the console.
|
|
82
|
+
const looked = await listFolder(asked, browseRoots, { ...(options.folderLook === undefined ? {} : { runner: options.folderLook }), ...(options.folderLookMs === undefined ? {} : { deadlineMs: options.folderLookMs }) });
|
|
83
|
+
if (!looked.ok) {
|
|
84
|
+
return looked.why === "no-answer"
|
|
85
|
+
? refuse(response, who, 503, "This folder didn't answer. It may be on a drive that's disconnected, or waiting on macOS. Try again in a moment.", "/projects")
|
|
86
|
+
: refuse(response, who, 404, "that directory cannot be read", "/projects/browse");
|
|
100
87
|
}
|
|
101
|
-
|
|
102
|
-
|
|
88
|
+
const canonical = looked.value.at;
|
|
89
|
+
if (canonical === null || !within(canonical)) {
|
|
90
|
+
return refuse(response, who, 403, "that path is outside what this server may browse", "/projects/browse");
|
|
103
91
|
}
|
|
92
|
+
// The look kept only folders inside a root; checked here again, as before.
|
|
93
|
+
const entries = looked.value.entries
|
|
94
|
+
.filter(one => within(one.path))
|
|
95
|
+
.sort((a, b) => Number(b.git) - Number(a.git) || a.name.localeCompare(b.name))
|
|
96
|
+
.slice(0, 200);
|
|
104
97
|
const root = browseRoots.find(one => canonical === one || canonical.startsWith(`${one}/`));
|
|
105
98
|
const parent = canonical === root ? null : canonical.slice(0, canonical.lastIndexOf("/")) || root;
|
|
106
99
|
const csrf = who.session.csrf;
|
|
@@ -133,48 +126,28 @@ export function createPagesHandlers(runtime) {
|
|
|
133
126
|
// (GitHub's rule); the FIRST match wins.
|
|
134
127
|
const localByIdentity = new Map();
|
|
135
128
|
const registered = new Set();
|
|
136
|
-
for (const project of store.listProjects())
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
registered.add(project.path);
|
|
140
|
-
const identity = githubIdentityOf(originUrlOf(project.path) ?? "");
|
|
141
|
-
if (identity !== null && !localByIdentity.has(identity.toLowerCase())) {
|
|
142
|
-
localByIdentity.set(identity.toLowerCase(), project.path);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
129
|
+
for (const project of store.listProjects())
|
|
130
|
+
if (visible(project.path))
|
|
131
|
+
registered.add(project.path);
|
|
145
132
|
// Roots are walked ONLY when there are listed repos to match against
|
|
146
|
-
// (finding 1),
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
children.push(join(root, entry.name));
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
finally {
|
|
163
|
-
dir.closeSync();
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
catch {
|
|
167
|
-
// an unreadable root offers nothing
|
|
168
|
-
}
|
|
169
|
-
for (const child of children) {
|
|
170
|
-
if (!existsSync(join(child, ".git")))
|
|
171
|
-
continue;
|
|
172
|
-
const identity = githubIdentityOf(originUrlOf(child) ?? "");
|
|
173
|
-
if (identity !== null && !localByIdentity.has(identity.toLowerCase())) {
|
|
174
|
-
localByIdentity.set(identity.toLowerCase(), child);
|
|
175
|
-
}
|
|
133
|
+
// (finding 1), at most 400 folders a root. Every read runs off the
|
|
134
|
+
// console's thread (folder-look.ts): a folder that doesn't answer
|
|
135
|
+
// leaves its copies unmatched and the page says so.
|
|
136
|
+
const roots = listed !== null && listed.ok && listed.repos.length > 0 ? [...ceiling.roots] : [];
|
|
137
|
+
let localUnanswered = false;
|
|
138
|
+
if (registered.size > 0 || roots.length > 0) {
|
|
139
|
+
const looked = await localOrigins([...registered], roots, { ...(options.folderLook === undefined ? {} : { runner: options.folderLook }), ...(options.folderLookMs === undefined ? {} : { deadlineMs: options.folderLookMs }) });
|
|
140
|
+
if (looked.ok) {
|
|
141
|
+
const children = looked.value.found.filter(one => roots.some(root => one.path.startsWith(`${root}/`)));
|
|
142
|
+
for (const one of [...looked.value.registered.filter(row => registered.has(row.path)), ...children]) {
|
|
143
|
+
const identity = githubIdentityOf(one.url ?? "");
|
|
144
|
+
if (identity !== null && !localByIdentity.has(identity.toLowerCase()))
|
|
145
|
+
localByIdentity.set(identity.toLowerCase(), one.path);
|
|
176
146
|
}
|
|
177
147
|
}
|
|
148
|
+
else {
|
|
149
|
+
localUnanswered = looked.why === "no-answer";
|
|
150
|
+
}
|
|
178
151
|
}
|
|
179
152
|
return sendScreen(response, 200, githubReposPage(chromeFor(who.via === "cookie" ? who.session.project : null, "projects"), {
|
|
180
153
|
listed,
|
|
@@ -183,6 +156,7 @@ export function createPagesHandlers(runtime) {
|
|
|
183
156
|
csrf: who.via === "cookie" ? who.session.csrf : "",
|
|
184
157
|
cloneReady: ceiling.roots.length > 0,
|
|
185
158
|
openProject: who.via === "cookie" ? who.session.project ?? null : null,
|
|
159
|
+
localUnanswered,
|
|
186
160
|
}));
|
|
187
161
|
}
|
|
188
162
|
// List repositories / Refresh list: ask the GitHub CLI signed in on this machine, then show the page.
|
|
@@ -354,7 +328,7 @@ export function createPagesHandlers(runtime) {
|
|
|
354
328
|
runners: store.listRunners(),
|
|
355
329
|
worktrees: store
|
|
356
330
|
.listWorktrees()
|
|
357
|
-
.filter(one => project === null ||
|
|
331
|
+
.filter(one => project === null || one.repo === project || identities.hot(one.repo) === identities.hot(project)),
|
|
358
332
|
episode: project === null ? null : store.latestWatchEpisode(project),
|
|
359
333
|
now,
|
|
360
334
|
}));
|
|
@@ -378,7 +352,7 @@ export function createPagesHandlers(runtime) {
|
|
|
378
352
|
}),
|
|
379
353
|
building: store.liveClaims(project, now),
|
|
380
354
|
runners: store.listRunners(),
|
|
381
|
-
worktrees: store.listWorktrees().filter(one => project === null ||
|
|
355
|
+
worktrees: store.listWorktrees().filter(one => project === null || one.repo === project || identities.hot(one.repo) === identities.hot(project)),
|
|
382
356
|
episode: project === null ? null : store.latestWatchEpisode(project),
|
|
383
357
|
outboxPending: store.pendingForAttention().length,
|
|
384
358
|
// External work at a glance (arc 3 finding 24): every dispatch
|
|
@@ -665,7 +639,11 @@ export function createPagesHandlers(runtime) {
|
|
|
665
639
|
return refuse(response, who, 403, "selecting a project is a browser session's act");
|
|
666
640
|
}
|
|
667
641
|
const askedPath = (body.get("path") ?? "").trim();
|
|
668
|
-
|
|
642
|
+
// Resolved off the console's thread (project-identity.ts): a folder that doesn't answer is said, never waited on.
|
|
643
|
+
const picked = askedPath === "" ? null : await identities.resolveNow(askedPath);
|
|
644
|
+
if (picked === "no-answer")
|
|
645
|
+
return refuse(response, who, 503, "That project's folder didn't answer. It may be on a drive that's disconnected, or waiting on macOS. Try again in a moment.");
|
|
646
|
+
const canonicalPick = picked;
|
|
669
647
|
if ((askedPath !== "" && canonicalPick === null) || (canonicalPick !== null && (!visible(canonicalPick) || !(await authorizedProject(liveCeiling(), canonicalPick))))) {
|
|
670
648
|
return refuse(response, who, 403, "that project is outside this console's reach");
|
|
671
649
|
}
|
|
@@ -702,7 +680,11 @@ export function createPagesHandlers(runtime) {
|
|
|
702
680
|
return refuse(response, who, 403, "opening a project is a browser session's act");
|
|
703
681
|
}
|
|
704
682
|
const asked = (body.get("path") ?? "").trim();
|
|
705
|
-
const
|
|
683
|
+
const resolved = asked === "" ? null : await identities.resolveNow(asked);
|
|
684
|
+
if (resolved === "no-answer") {
|
|
685
|
+
return void projectsScreen(response, who, "that folder didn't answer — it may be on a drive that's disconnected, or waiting on macOS; try again in a moment", 503, safeReturn(body.get("return")));
|
|
686
|
+
}
|
|
687
|
+
const canonical = resolved;
|
|
706
688
|
if (canonical === null) {
|
|
707
689
|
return void projectsScreen(response, who, "that path does not exist on this server", 400, safeReturn(body.get("return")));
|
|
708
690
|
}
|
|
@@ -807,13 +789,9 @@ export function createPagesHandlers(runtime) {
|
|
|
807
789
|
const rootRaw = Number.isInteger(rootIndex) ? ceiling.roots[rootIndex] : undefined;
|
|
808
790
|
if (rootRaw === undefined)
|
|
809
791
|
return projectsScreen(response, who, "pick one of the configured roots", 400);
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
root = realpathSync(rootRaw);
|
|
813
|
-
}
|
|
814
|
-
catch {
|
|
792
|
+
const root = await identities.resolveNow(rootRaw);
|
|
793
|
+
if (root === null || root === "no-answer")
|
|
815
794
|
return projectsScreen(response, who, "that projects root does not resolve right now", 400);
|
|
816
|
-
}
|
|
817
795
|
const previewed = await (options.ghPreview ?? previewGithubRepo)(shape.owner, shape.name);
|
|
818
796
|
if (!previewed.ok)
|
|
819
797
|
return projectsScreen(response, who, previewed.message, 400);
|
|
@@ -886,13 +864,9 @@ export function createPagesHandlers(runtime) {
|
|
|
886
864
|
const rootRaw = ceiling.roots[record.rootIndex];
|
|
887
865
|
if (rootRaw === undefined)
|
|
888
866
|
return projectsScreen(response, who, "the configured roots changed — preview again", 400);
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
root = realpathSync(rootRaw);
|
|
892
|
-
}
|
|
893
|
-
catch {
|
|
867
|
+
const root = await identities.resolveNow(rootRaw);
|
|
868
|
+
if (root === null || root === "no-answer")
|
|
894
869
|
return projectsScreen(response, who, "that projects root does not resolve right now", 400);
|
|
895
|
-
}
|
|
896
870
|
if (dirname(record.target) !== root)
|
|
897
871
|
return projectsScreen(response, who, "the configured roots changed — preview again", 400);
|
|
898
872
|
const cloned = await (options.ghClone ?? cloneGithubRepo)(record.nameWithOwner, root, async (cwd) => {
|
|
@@ -908,8 +882,8 @@ export function createPagesHandlers(runtime) {
|
|
|
908
882
|
return projectsScreen(response, who, "the clone answered with a different path than the preview promised — refused; nothing was enrolled", 400);
|
|
909
883
|
}
|
|
910
884
|
// authorizedProject AFTER the repository exists (finding 20).
|
|
911
|
-
const
|
|
912
|
-
const admitted =
|
|
885
|
+
const clonedAt = await identities.resolveNow(cloned.target);
|
|
886
|
+
const admitted = typeof clonedAt === "string" && clonedAt !== "no-answer" && (await authorizedProject(liveCeiling(), clonedAt)) ? clonedAt : null;
|
|
913
887
|
if (admitted === null) {
|
|
914
888
|
return projectsScreen(response, who, `the clone landed at ${cloned.target} but did not prove under the ceiling — it was left in place; enroll it by hand`, 400);
|
|
915
889
|
}
|
|
@@ -937,7 +911,7 @@ export function createPagesHandlers(runtime) {
|
|
|
937
911
|
const recentPaths = new Set(recent.map(one => one.path));
|
|
938
912
|
const candidates = new Set();
|
|
939
913
|
for (const path of consoleProjects()) {
|
|
940
|
-
const canonical =
|
|
914
|
+
const canonical = identities.hot(path);
|
|
941
915
|
if (!recentPaths.has(canonical) && visible(canonical))
|
|
942
916
|
candidates.add(canonical);
|
|
943
917
|
}
|
|
@@ -422,22 +422,6 @@ export type ProjectPeek = {
|
|
|
422
422
|
* GitHub identity. Everything else is null.
|
|
423
423
|
*/
|
|
424
424
|
export declare function githubIdentityOf(remoteUrl: string): string | null;
|
|
425
|
-
/**
|
|
426
|
-
* The `origin` remote's url from a repository's OWN .git/config. BOUNDED
|
|
427
|
-
* I/O by construction (ghlist review, finding 1): the final component may
|
|
428
|
-
* not be a symlink (O_NOFOLLOW + lstat), must be a REGULAR file under a
|
|
429
|
-
* size cap, and at most 64 KiB are ever read — a sparse monster or a fifo
|
|
430
|
-
* planted as a "config" reads as null, never as a hang. Parsed LINE BY
|
|
431
|
-
* LINE with real section tracking (finding 2): a `[remote "origin"]`
|
|
432
|
-
* embedded inside some other value never opens the section. null when
|
|
433
|
-
* unreadable or origin-less; a worktree-style `.git` FILE (gitdir pointer)
|
|
434
|
-
* reads null too, which is honest — its identity lives elsewhere. The
|
|
435
|
-
* answer is ADVISORY metadata for offering a button: the /projects/open
|
|
436
|
-
* road re-proves path, ceiling, and git-ness before anything mutates, and
|
|
437
|
-
* a config that lies about its origin can only mislabel a repository the
|
|
438
|
-
* operator was already allowed to open.
|
|
439
|
-
*/
|
|
440
|
-
export declare function originUrlOf(repoPath: string): string | null;
|
|
441
425
|
/** The GitHub listing page: the account's repositories with ONE honest
|
|
442
426
|
* action each. Names, descriptions, and paths are gh/filesystem DATA —
|
|
443
427
|
* escaped at the sink like everything else. */
|
|
@@ -450,6 +434,8 @@ export declare function githubReposPage(chrome: Chrome, data: {
|
|
|
450
434
|
csrf: string;
|
|
451
435
|
cloneReady: boolean;
|
|
452
436
|
openProject: string | null;
|
|
437
|
+
/** A projects folder didn't answer in time: local copies weren't matched this time. */
|
|
438
|
+
localUnanswered?: boolean;
|
|
453
439
|
}): Screen;
|
|
454
440
|
export declare function projectsPage(chrome: Chrome, recent: {
|
|
455
441
|
path: string;
|
|
@@ -22,7 +22,6 @@ import { runCostWords, spendLine, tally } from "../summary.js";
|
|
|
22
22
|
import {} from "../work-index.js";
|
|
23
23
|
import { WORK_VIEWS } from "../workspace-ui.js";
|
|
24
24
|
import { createHash } from "node:crypto";
|
|
25
|
-
import { closeSync, constants as fsConstants, lstatSync, openSync, readSync } from "node:fs";
|
|
26
25
|
import { homedir } from "node:os";
|
|
27
26
|
import { join } from "node:path";
|
|
28
27
|
import { buildsViews, CHEVRON_ICON, consoleSpend, FOLDER_PATHS, GRIP_HANDLE, isOverdue, projectChip, relativeAge, safePrUrl, screen, sentenceCase, strokeIcon, TO_FRONT_ICON, when, whenTime } from "./chrome.js";
|
|
@@ -865,60 +864,6 @@ export function githubIdentityOf(remoteUrl) {
|
|
|
865
864
|
return `${ssh[1]}/${ssh[2]}`;
|
|
866
865
|
return null;
|
|
867
866
|
}
|
|
868
|
-
/**
|
|
869
|
-
* The `origin` remote's url from a repository's OWN .git/config. BOUNDED
|
|
870
|
-
* I/O by construction (ghlist review, finding 1): the final component may
|
|
871
|
-
* not be a symlink (O_NOFOLLOW + lstat), must be a REGULAR file under a
|
|
872
|
-
* size cap, and at most 64 KiB are ever read — a sparse monster or a fifo
|
|
873
|
-
* planted as a "config" reads as null, never as a hang. Parsed LINE BY
|
|
874
|
-
* LINE with real section tracking (finding 2): a `[remote "origin"]`
|
|
875
|
-
* embedded inside some other value never opens the section. null when
|
|
876
|
-
* unreadable or origin-less; a worktree-style `.git` FILE (gitdir pointer)
|
|
877
|
-
* reads null too, which is honest — its identity lives elsewhere. The
|
|
878
|
-
* answer is ADVISORY metadata for offering a button: the /projects/open
|
|
879
|
-
* road re-proves path, ceiling, and git-ness before anything mutates, and
|
|
880
|
-
* a config that lies about its origin can only mislabel a repository the
|
|
881
|
-
* operator was already allowed to open.
|
|
882
|
-
*/
|
|
883
|
-
export function originUrlOf(repoPath) {
|
|
884
|
-
const file = join(repoPath, ".git", "config");
|
|
885
|
-
let fd = null;
|
|
886
|
-
try {
|
|
887
|
-
const stat = lstatSync(file);
|
|
888
|
-
if (!stat.isFile() || stat.size > 1024 * 1024)
|
|
889
|
-
return null;
|
|
890
|
-
fd = openSync(file, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
891
|
-
const buffer = Buffer.alloc(64 * 1024);
|
|
892
|
-
const read = readSync(fd, buffer, 0, buffer.length, 0);
|
|
893
|
-
const config = buffer.toString("utf8", 0, read);
|
|
894
|
-
let inOrigin = false;
|
|
895
|
-
for (const line of config.split("\n")) {
|
|
896
|
-
if (/^\s*\[/.test(line)) {
|
|
897
|
-
inOrigin = /^\s*\[remote "origin"\]\s*$/.test(line);
|
|
898
|
-
continue;
|
|
899
|
-
}
|
|
900
|
-
if (!inOrigin)
|
|
901
|
-
continue;
|
|
902
|
-
const url = /^\s*url\s*=\s*(.+)$/.exec(line)?.[1];
|
|
903
|
-
if (url !== undefined)
|
|
904
|
-
return url.trim();
|
|
905
|
-
}
|
|
906
|
-
return null;
|
|
907
|
-
}
|
|
908
|
-
catch {
|
|
909
|
-
return null;
|
|
910
|
-
}
|
|
911
|
-
finally {
|
|
912
|
-
if (fd !== null) {
|
|
913
|
-
try {
|
|
914
|
-
closeSync(fd);
|
|
915
|
-
}
|
|
916
|
-
catch {
|
|
917
|
-
// already closed
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
867
|
/** The GitHub listing page: the account's repositories with ONE honest
|
|
923
868
|
* action each. Names, descriptions, and paths are gh/filesystem DATA —
|
|
924
869
|
* escaped at the sink like everything else. */
|
|
@@ -955,6 +900,7 @@ export function githubReposPage(chrome, data) {
|
|
|
955
900
|
return screen("projects", joinHtml([
|
|
956
901
|
html `<h1>Your GitHub repositories</h1>`,
|
|
957
902
|
html `<p class="meta">Repositories available to the GitHub account signed in on this machine — open one you already have, or clone a new one after a quick preview.</p>`,
|
|
903
|
+
data.localUnanswered === true ? html `<p class="meta">Your projects folder didn't answer, so copies already on this machine aren't matched yet. Try again in a moment.</p>` : null,
|
|
958
904
|
rows,
|
|
959
905
|
data.listed === null ? null : list("Refresh list"),
|
|
960
906
|
html `<p class="row" style="margin-top:.6rem"><a class="badge" href="/projects">← back to projects</a></p>`,
|
|
@@ -2519,6 +2519,7 @@ export function releaseCardHtml(taskId, release, canStop) {
|
|
|
2519
2519
|
html `<details><summary>Later commits (${release.landed.length})</summary>${landed}</details>`,
|
|
2520
2520
|
release.obligations.length === 0 ? "" : html `<details><summary>Still to pass (${release.obligations.length})</summary><ul>${joinHtml(release.obligations.map(one => html `<li>${one}</li>`), "")}</ul></details>`,
|
|
2521
2521
|
release.nightly === null ? "" : html `<p class="meta">Nightly check of main: ${release.nightly.outcome ?? "running"} at ${release.nightly.sha.slice(0, 7)}${release.openMainFailures > 0 ? ` · ${release.openMainFailures} open failure${release.openMainFailures === 1 ? "" : "s"}` : ""}</p>`,
|
|
2522
|
+
...release.githubNightly.map(line => html `<p class="meta">${line}</p>`),
|
|
2522
2523
|
release.carrying && canStop ? postForm(`${taskHref(taskId)}/stop-carrying`, html `<button type="submit" class="secondary">Stop carrying later commits</button>`, { attrs: { class: "inline" } }) : "",
|
|
2523
2524
|
html `</div>`,
|
|
2524
2525
|
], "\n");
|
package/dist/server/runtime.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ProjectIdentities } from "../project-identity.js";
|
|
1
2
|
import type { FreshFacts } from '../scope.js';
|
|
2
3
|
import type { createPersonalQueueRuntime } from '../lead-queue-runtime.js';
|
|
3
4
|
import type { FlowRooms } from '../flow-live.js';
|
|
@@ -95,7 +96,10 @@ export interface ServerRuntime {
|
|
|
95
96
|
liveCeiling: () => {
|
|
96
97
|
repos: string[];
|
|
97
98
|
roots: readonly string[];
|
|
99
|
+
canon?: (path: string) => string | null;
|
|
98
100
|
};
|
|
101
|
+
/** Project identities without reading folders on the console's thread (project-identity.ts). */
|
|
102
|
+
identities: ProjectIdentities;
|
|
99
103
|
taskScreen: (response: ServerResponse, who: Who, taskId: string, problem: string | null, status: number, scopeDraft?: URLSearchParams, cancelDraft?: string, editPlan?: boolean, page?: TaskPageExtras) => void;
|
|
100
104
|
runVisible: (run: Run) => boolean;
|
|
101
105
|
runIsLive: (run: Pick<Run, "outcome" | "leaseId" | "taskRef">) => boolean;
|
package/dist/server/runtime.js
CHANGED
package/dist/server/settings.js
CHANGED
|
@@ -9,6 +9,7 @@ import { CONNECT_CALLBACK, connectionsOf, finishConnect, localConnectOf, oneClic
|
|
|
9
9
|
import { learningView } from "../project-learning.js";
|
|
10
10
|
import { skillTestResult } from "../project-skills.js";
|
|
11
11
|
import { discoverTools, projectToolsOf } from "../project-tools.js";
|
|
12
|
+
import { lookProjectMcp } from "../folder-look.js";
|
|
12
13
|
import { publishingOf } from '../pull-request-flow.js';
|
|
13
14
|
import { skillTestFeedbackHtml } from "../skills-ui.js";
|
|
14
15
|
import { labelOf, nameOf } from "../subagent-admin.js";
|
|
@@ -132,7 +133,7 @@ import { personChip, ROLE_TITLES } from "./chrome.js";
|
|
|
132
133
|
import { chatReturnWithSaid, goOutside } from "./http.js";
|
|
133
134
|
import { leadSettingsHtml, notificationProjects, SETTINGS_AUTOSAVE_SCRIPT, settingsPage, settingsWorkers, telegramDeliveryWords, telegramTrouble } from "./render-settings.js";
|
|
134
135
|
export function createSettingsHandlers(runtime) {
|
|
135
|
-
const { store, providerHome, managedRepos, consoleProjects, sendScreen, chromeFor, connectionCheck, connectionKnown, lastChecks, checkRelease, clock, modelCatalog, modelSeams, toolsViewOf, codexServers, projectViewOf, sessions, restricted, options, integrations, consoleOrigin, evidenceRoot, storagePool, visible, ssoSettings, checkLocalAgents, chatKeyFor, chatCatalog, chatCatalogKnown, leadWords, agentSignInCommand, settingsUpdates, phoneSetup, authenticateApprover, authenticateCredential, deletedRepos, projectOf, providerFor, admissionList, toolHome, connectVisits, mintApprovalNonce, consumeApprovalNonce, defaultProject, googleVisits } = runtime;
|
|
136
|
+
const { identities, store, providerHome, managedRepos, consoleProjects, sendScreen, chromeFor, connectionCheck, connectionKnown, lastChecks, checkRelease, clock, modelCatalog, modelSeams, toolsViewOf, codexServers, projectViewOf, sessions, restricted, options, integrations, consoleOrigin, evidenceRoot, storagePool, visible, ssoSettings, checkLocalAgents, chatKeyFor, chatCatalog, chatCatalogKnown, leadWords, agentSignInCommand, settingsUpdates, phoneSetup, authenticateApprover, authenticateCredential, deletedRepos, projectOf, providerFor, admissionList, toolHome, connectVisits, mintApprovalNonce, consumeApprovalNonce, defaultProject, googleVisits } = runtime;
|
|
136
137
|
// v105: what agent work cost, by month, and the monthly budgets. An instance operator's page.
|
|
137
138
|
async function spendPage(ctx) {
|
|
138
139
|
const { url, who, response, now } = ctx;
|
|
@@ -1547,7 +1548,15 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
|
|
|
1547
1548
|
const wanted = body.getAll("import");
|
|
1548
1549
|
if (wanted.length === 0)
|
|
1549
1550
|
return back("problem", "Choose at least one tool to add.");
|
|
1550
|
-
|
|
1551
|
+
// The project's own .mcp.json read now, off the console's thread (folder-look.ts).
|
|
1552
|
+
let projectConfig;
|
|
1553
|
+
if (options.identities !== undefined) {
|
|
1554
|
+
const looked = await lookProjectMcp(repo);
|
|
1555
|
+
if (!looked.ok)
|
|
1556
|
+
return back("problem", "This project's folder didn't answer. It may be on a drive that's disconnected, or waiting on macOS. Try again in a moment.");
|
|
1557
|
+
projectConfig = looked.value;
|
|
1558
|
+
}
|
|
1559
|
+
const found = discoverTools(repo, await codexServers(repo), toolHome, process.env, projectConfig);
|
|
1551
1560
|
const added = [], problems = [];
|
|
1552
1561
|
for (const one of wanted) {
|
|
1553
1562
|
const match = found.find(tool => tool.spec.name === one);
|
|
@@ -1887,7 +1896,9 @@ ${repositoryContextHtml(chosen, query, result, who.role === 'approver' ? csrf :
|
|
|
1887
1896
|
if (["scope", "full-access", "cap-usd", "nonce", "digest", "token"].some(key => body.getAll(key).length > 1))
|
|
1888
1897
|
return refuse(response, who, 400, "Submit one value for each field.", "/settings");
|
|
1889
1898
|
const given = (body.get("scope") ?? "all").trim();
|
|
1890
|
-
const scope = given === "all" || given === "" ? CHAT_APPROVAL_ALL :
|
|
1899
|
+
const scope = given === "all" || given === "" ? CHAT_APPROVAL_ALL : identities.vouched(given, consoleProjects) ?? null;
|
|
1900
|
+
if (scope === null)
|
|
1901
|
+
return refuse(response, who, 403, "That isn't one of your projects.", "/settings");
|
|
1891
1902
|
if (scope !== CHAT_APPROVAL_ALL && !(visible(scope) && store.accountCanAccess(who.name, scope)))
|
|
1892
1903
|
return refuse(response, who, 403, "That isn't one of your projects.", "/settings");
|
|
1893
1904
|
const where = scope === CHAT_APPROVAL_ALL ? "all your projects" : projectName(scope);
|