toolroll 0.9.41 → 0.9.43
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 +37 -0
- package/dist/chat-actions.js +1 -1
- package/dist/chat-approval.d.ts +45 -0
- package/dist/chat-approval.js +81 -0
- package/dist/chat-channel.js +5 -0
- package/dist/chat-decide.d.ts +18 -6
- package/dist/chat-decide.js +54 -20
- package/dist/chat-flow.js +2 -2
- package/dist/cli.js +4 -2
- package/dist/coding-types.d.ts +2 -24
- package/dist/coding-workspace.js +12 -11
- package/dist/contracts/cli.d.ts +736 -0
- package/dist/contracts/cli.js +223 -0
- package/dist/contracts/coding-activity.d.ts +64 -0
- package/dist/contracts/coding-activity.js +58 -0
- package/dist/contracts/console-api.d.ts +484 -0
- package/dist/contracts/console-api.js +274 -0
- package/dist/contracts/diff-stat.d.ts +58 -0
- package/dist/contracts/diff-stat.js +59 -0
- package/dist/contracts/integration-metadata.d.ts +99 -0
- package/dist/contracts/integration-metadata.js +101 -0
- package/dist/contracts/lead-tools.d.ts +11 -0
- package/dist/contracts/lead-tools.js +2 -0
- package/dist/contracts/model-catalog.d.ts +21 -0
- package/dist/contracts/model-catalog.js +14 -0
- package/dist/contracts/native-census.d.ts +42 -0
- package/dist/contracts/native-census.js +36 -0
- package/dist/contracts/observation-request.d.ts +37 -0
- package/dist/contracts/observation-request.js +25 -0
- package/dist/contracts/provider.d.ts +30 -0
- package/dist/contracts/provider.js +15 -0
- package/dist/contracts/recipes.d.ts +166 -0
- package/dist/contracts/recipes.js +52 -0
- package/dist/contracts/retention.d.ts +42 -0
- package/dist/contracts/retention.js +24 -0
- package/dist/contracts/storage.d.ts +23 -0
- package/dist/contracts/storage.js +10 -0
- package/dist/contracts/store-json.d.ts +315 -0
- package/dist/contracts/store-json.js +196 -0
- package/dist/contracts/trigger-payloads.d.ts +369 -0
- package/dist/contracts/trigger-payloads.js +144 -0
- package/dist/contracts/update-journal.d.ts +254 -0
- package/dist/contracts/update-journal.js +155 -0
- package/dist/contracts/verification-view.d.ts +93 -0
- package/dist/contracts/verification-view.js +43 -0
- package/dist/control-setup.d.ts +1 -1
- package/dist/desktop-update.d.ts +7 -40
- package/dist/desktop-update.js +36 -8
- package/dist/evidence.d.ts +3 -19
- package/dist/flow-engine.d.ts +2 -0
- package/dist/flow-engine.js +10 -2
- package/dist/flow-triggers.js +61 -60
- package/dist/flows-cli.d.ts +28 -0
- package/dist/flows-cli.js +43 -1
- package/dist/guides.js +2 -0
- package/dist/mate-contract.js +1 -1
- package/dist/mate-tools.js +43 -1
- package/dist/mcp-connect.js +37 -22
- package/dist/model-catalog.js +8 -6
- package/dist/observations.d.ts +2 -6
- package/dist/observations.js +13 -9
- package/dist/operate.js +55 -4
- package/dist/process-recovery-eligibility.d.ts +1 -1
- package/dist/process-recovery-native.d.ts +2 -13
- package/dist/process-recovery-native.js +8 -5
- package/dist/project-tools.d.ts +6 -24
- package/dist/project-tools.js +22 -18
- package/dist/provider.d.ts +3 -8
- package/dist/provider.js +15 -11
- package/dist/recipes.d.ts +3 -28
- package/dist/recipes.js +79 -52
- package/dist/retention.d.ts +4 -5
- package/dist/retention.js +5 -4
- package/dist/scope.d.ts +4 -2
- package/dist/serve.js +193 -30
- package/dist/storage.d.ts +2 -1
- package/dist/storage.js +12 -9
- package/dist/store.d.ts +11 -16
- package/dist/store.js +122 -160
- package/dist/surface.d.ts +6 -54
- package/dist/surface.js +10 -2
- package/dist/teammate-work.js +1 -1
- package/dist/telegram-flow.d.ts +41 -1
- package/dist/telegram-flow.js +96 -12
- package/dist/telegram.js +27 -3
- package/dist/toolroll-update.d.ts +8 -99
- package/dist/toolroll-update.js +28 -18
- package/dist/verification-evidence.js +27 -12
- package/package.json +1 -1
package/dist/serve.js
CHANGED
|
@@ -56,6 +56,7 @@ import { addCardToFlow, advanceFlows, cancelFlowCard, crossProjectProblem, decid
|
|
|
56
56
|
import { chooseFlowCard } from "./flow-send.js";
|
|
57
57
|
import { FlowContractError, FLOW_TEMPLATES, validateFlowDefinition, withZoneNames } from "./flows.js";
|
|
58
58
|
import { stageReferenceProblems } from "./contracts/stage-output.js";
|
|
59
|
+
import { CONSOLE_FORMS, checkResponse, readForm } from "./contracts/console-api.js";
|
|
59
60
|
import { TOOL_CATALOG, addToolTo, catalogTool, discoverTools, localAppOf, projectToolsOf, removeToolFrom, secretsSetFor, setToolSecret, splitCommandLine, testToolOf } from "./project-tools.js";
|
|
60
61
|
import { changeLearning, learningView } from "./project-learning.js";
|
|
61
62
|
import { applySavedKnowledge, changeKnowledge, knowledgeView, knowledgeVersion, readKnowledgeSnapshot } from "./project-knowledge.js";
|
|
@@ -153,6 +154,7 @@ import { MODELS_CSS, modelsHtml, modelsScript } from "./models-ui.js";
|
|
|
153
154
|
import { ASSISTANTS, modelChoices, detectPreparation, previewProjectInstructions, addProjectInstructions } from "./setup-guide.js";
|
|
154
155
|
import { previewSetup, approveSetup } from "./control-setup.js";
|
|
155
156
|
import { controlSetupHtml, setupPreviewHtml, connectionHtml, connectionWords, hiddenFields } from "./control-ui.js";
|
|
157
|
+
import { ALL_PROJECTS as CHAT_APPROVAL_ALL, chatApprovalWords, setChatApproval } from "./chat-approval.js";
|
|
156
158
|
import { composerSchedule, scheduleEditorHtml, scheduleEditorScript } from "./task-composer.js";
|
|
157
159
|
import { listCoordinators } from "./coordinator.js";
|
|
158
160
|
import { diagnoseTaskDispatch, withDispatchDiagnoses } from "./dispatch.js";
|
|
@@ -1412,7 +1414,7 @@ export function createDecisionServer(options) {
|
|
|
1412
1414
|
if (setupAttemptsLeft <= 0) {
|
|
1413
1415
|
return page(response, 403, signupPage("too many wrong codes — restart the server to get a fresh code", 0));
|
|
1414
1416
|
}
|
|
1415
|
-
const body = await form(request);
|
|
1417
|
+
const body = readForm(await form(request), CONSOLE_FORMS.signup);
|
|
1416
1418
|
const code = (body.get("code") ?? "").trim();
|
|
1417
1419
|
const name = (body.get("name") ?? "").trim();
|
|
1418
1420
|
const password = body.get("password") ?? "";
|
|
@@ -1451,7 +1453,7 @@ export function createDecisionServer(options) {
|
|
|
1451
1453
|
return redirect(response, "/");
|
|
1452
1454
|
}
|
|
1453
1455
|
if (url.pathname === "/login" && method === "POST") {
|
|
1454
|
-
const body = await form(request);
|
|
1456
|
+
const body = readForm(await form(request), CONSOLE_FORMS.login);
|
|
1455
1457
|
const name = body.get("name");
|
|
1456
1458
|
const token = body.get("token");
|
|
1457
1459
|
// The destination a deep link asked for rides the form as a same-site
|
|
@@ -1528,7 +1530,7 @@ export function createDecisionServer(options) {
|
|
|
1528
1530
|
}
|
|
1529
1531
|
let body;
|
|
1530
1532
|
try {
|
|
1531
|
-
body = await form(request);
|
|
1533
|
+
body = readForm(await form(request), CONSOLE_FORMS.join);
|
|
1532
1534
|
}
|
|
1533
1535
|
catch {
|
|
1534
1536
|
return respond(response, 413, "text/plain; charset=utf-8", "body too large");
|
|
@@ -1607,7 +1609,7 @@ export function createDecisionServer(options) {
|
|
|
1607
1609
|
return freshIdentitySignIn.run({ actor: requestFacts.sso?.fresh === true ? who.name : null }, () => requestContext.run(requestFacts, async () => {
|
|
1608
1610
|
const taskTextForm = url.pathname === "/tasks/add" || /^\/t\/[^/]+\/scope$/.test(url.pathname);
|
|
1609
1611
|
const body = method === "POST" ? await form(request, url.pathname === "/settings/skills/import" ? 2 * 1024 * 1024 : url.pathname === "/flows/import" ? 1024 * 1024 : taskTextForm ? TASK_FORM_BODY_CAP : BODY_CAP) : null;
|
|
1610
|
-
const target = actionTarget(url, who, request, body);
|
|
1612
|
+
const target = actionTarget(url, who, request, body === null ? null : readForm(body, CONSOLE_FORMS.ledgerTarget));
|
|
1611
1613
|
const execute = async () => {
|
|
1612
1614
|
if (!projectRequestAllowed(url, who, request, response))
|
|
1613
1615
|
return;
|
|
@@ -1720,7 +1722,7 @@ export function createDecisionServer(options) {
|
|
|
1720
1722
|
const ref = run === null ? null : store.refForId(run.taskRef);
|
|
1721
1723
|
return { repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId, action: `${resource[1] === "d" ? "decision" : resource[1] === "i" ? "incident" : "run"} ${resource[3] ?? "view"}` };
|
|
1722
1724
|
}
|
|
1723
|
-
const known = new Set(["/recipes/prepare", "/recipes/preview", "/recipes/import", "/recipes/save", "/recipes/launch", "/tasks/add", "/queue/move", "/queue/note", "/routines/add", "/people/invite", "/people/invite-revoke", "/people/revoke", "/people/projects", "/projects/select", "/projects/open", "/mode/confirm", "/mode/sign", "/mode/revoke"]);
|
|
1725
|
+
const known = new Set(["/recipes/prepare", "/recipes/preview", "/recipes/import", "/recipes/save", "/recipes/launch", "/tasks/add", "/queue/move", "/queue/note", "/routines/add", "/people/invite", "/people/invite-revoke", "/people/revoke", "/people/projects", "/projects/select", "/projects/open", "/mode/confirm", "/mode/sign", "/mode/revoke", "/settings/chat-approval/save", "/settings/chat-approval/off"]);
|
|
1724
1726
|
const placed = ["/tasks/add", "/routines/add"].includes(url.pathname) ? body?.get("repo")?.trim() : null;
|
|
1725
1727
|
return { repo: url.pathname.startsWith("/people/") ? null : placed ? canonicalProject(placed) ?? placed : projectOf(who, request) ?? null, taskId: null, runId: null,
|
|
1726
1728
|
action: known.has(url.pathname) ? url.pathname.slice(1).replaceAll("/", " ") : "console request" };
|
|
@@ -1748,6 +1750,9 @@ export function createDecisionServer(options) {
|
|
|
1748
1750
|
if ((request.method === "GET" && path === "/settings/telegram") ||
|
|
1749
1751
|
(request.method === "POST" && ["/settings/telegram/pair", "/settings/telegram/unpair", "/settings/telegram/retry"].includes(path)))
|
|
1750
1752
|
return true;
|
|
1753
|
+
// A person's own chat-approval setting; the route proves each project it names.
|
|
1754
|
+
if (request.method === "POST" && ["/settings/chat-approval/confirm", "/settings/chat-approval/save", "/settings/chat-approval/off"].includes(path))
|
|
1755
|
+
return true;
|
|
1751
1756
|
// Coding routes require an instance operator, then prove saved ownership and project access.
|
|
1752
1757
|
if (path === "/code" || path.startsWith("/code/"))
|
|
1753
1758
|
return true;
|
|
@@ -2633,7 +2638,7 @@ export function createDecisionServer(options) {
|
|
|
2633
2638
|
latest: operator && latest !== null ? latest : null, csrf: operator ? who.session.csrf : null, today: now.toISOString().slice(0, 10) };
|
|
2634
2639
|
const rows = store.actionLedger({ ...query, ...(before === null ? {} : { before: Number(before) }), limit: 51 });
|
|
2635
2640
|
if (url.searchParams.get("format") === "json")
|
|
2636
|
-
return respond(response, 200, "application/json; charset=utf-8", JSON.stringify({ chain: { ok: report.ok, entries: report.entries, through: report.through, head: report.head, problem: report.problem }, entries: rows.slice(0, 50), nextBefore: rows.length > 50 ? rows[49].id : null }));
|
|
2641
|
+
return respond(response, 200, "application/json; charset=utf-8", JSON.stringify(checkResponse("ledgerPage", { chain: { ok: report.ok, entries: report.entries, through: report.through, head: report.head, problem: report.problem }, entries: rows.slice(0, 50), nextBefore: rows.length > 50 ? rows[49].id : null })));
|
|
2637
2642
|
return sendScreen(response, 200, screen("Action ledger", ledgerBody(rows, projects, url.searchParams, chainView), { chrome: chromeFor(chosen === "" ? null : chosen, "ledger", undefined, chosen === "" ? "all" : "project") }));
|
|
2638
2643
|
}
|
|
2639
2644
|
if (url.pathname === "/activity") {
|
|
@@ -3010,7 +3015,7 @@ export function createDecisionServer(options) {
|
|
|
3010
3015
|
if (url.searchParams.get("format") === "json") {
|
|
3011
3016
|
response.writeHead(200, { "content-type": "application/json; charset=utf-8", "content-disposition": `attachment; filename="evidence-${family.root.id.replace(/[^A-Za-z0-9._-]/g, "_")}.json"`,
|
|
3012
3017
|
"cache-control": "no-store", "x-content-type-options": "nosniff" });
|
|
3013
|
-
return void response.end(JSON.stringify(pack, null, 2));
|
|
3018
|
+
return void response.end(JSON.stringify(checkResponse("evidencePack", pack), null, 2));
|
|
3014
3019
|
}
|
|
3015
3020
|
return sendScreen(response, 200, screen(`Evidence pack · ${family.root.id}`, evidencePackHtml(pack), { chrome: chromeFor(family.root.repo, "tasks") }));
|
|
3016
3021
|
}
|
|
@@ -3358,7 +3363,7 @@ export function createDecisionServer(options) {
|
|
|
3358
3363
|
const view = flowView(store, flow, { name: who.name, approver: who.via === "cookie" && who.role === "approver" }, Number.isSafeInteger(selected) && selected > 0 ? selected : null, { dir: options.configDir ?? null, repos: consoleProjects(), startTrigger: Number.isSafeInteger(start) && start > 0 ? start : null,
|
|
3359
3364
|
sortReady: keyStatus("openrouter", providerHome).set, toolHome });
|
|
3360
3365
|
if (url.searchParams.get("format") === "json")
|
|
3361
|
-
return respond(response, 200, "application/json; charset=utf-8", JSON.stringify(view));
|
|
3366
|
+
return respond(response, 200, "application/json; charset=utf-8", JSON.stringify(checkResponse("flowView", view)));
|
|
3362
3367
|
return sendScreen(response, 200, screen(flow.name, `<p><a href="/flows">Flows</a></p><h1>${escape(flow.name)}</h1>${flowFallbackHtml(view)}`, { chrome: chromeFor(flow.repo, "flows"), workspace: { view } }));
|
|
3363
3368
|
}
|
|
3364
3369
|
if (url.pathname === "/recipes" || url.pathname.startsWith("/recipes/")) {
|
|
@@ -6611,7 +6616,7 @@ export function createDecisionServer(options) {
|
|
|
6611
6616
|
}
|
|
6612
6617
|
return respond(response, 200, "application/json; charset=utf-8", JSON.stringify({ ok: true, beaten }));
|
|
6613
6618
|
}
|
|
6614
|
-
async function handlePost(url, who, request, response,
|
|
6619
|
+
async function handlePost(url, who, request, response, posted) {
|
|
6615
6620
|
// The attended beat answers BEFORE the shared mutation guard (v28): it
|
|
6616
6621
|
// carries no parameters, so there is no csrf token to check — its OWN
|
|
6617
6622
|
// guard is complete and STRICTER for the browsers this console
|
|
@@ -6623,12 +6628,13 @@ export function createDecisionServer(options) {
|
|
|
6623
6628
|
if (url.pathname === "/session/attended-beats") {
|
|
6624
6629
|
return attendedBeats(who, request, response, clock());
|
|
6625
6630
|
}
|
|
6626
|
-
const denied = authorizeMutation(request, who,
|
|
6631
|
+
const denied = authorizeMutation(request, who, readForm(posted, CONSOLE_FORMS.mutationGuard));
|
|
6627
6632
|
if (denied !== null)
|
|
6628
6633
|
return refuse(response, who, denied.status, denied.message);
|
|
6629
6634
|
// The scripted demo lead's acts: ask, approve, change a plan, complete or send back a result.
|
|
6630
6635
|
const demoAct = /^\/chat\/demo\/(?:ask|([0-9]{1,9})\/(approve|change|revise|complete))$/.exec(url.pathname);
|
|
6631
6636
|
if (demoAct !== null) {
|
|
6637
|
+
const body = readForm(posted, CONSOLE_FORMS.demo);
|
|
6632
6638
|
const lead = demoLeadHere();
|
|
6633
6639
|
if (lead === null || who.via !== "cookie")
|
|
6634
6640
|
return refuse(response, who, 404, "There's no page at this address.", "/chat");
|
|
@@ -6677,6 +6683,7 @@ export function createDecisionServer(options) {
|
|
|
6677
6683
|
}
|
|
6678
6684
|
// The update notice is dismissed per browser and per version: a newer release shows it again.
|
|
6679
6685
|
if (url.pathname === "/settings/updates/dismiss") {
|
|
6686
|
+
const body = readForm(posted, CONSOLE_FORMS.updatesDismiss);
|
|
6680
6687
|
const version = body.get("version") ?? "";
|
|
6681
6688
|
if (!/^[0-9A-Za-z.-]{1,60}$/.test(version))
|
|
6682
6689
|
return refuse(response, who, 400, "Choose a version to dismiss.", "/settings#updates");
|
|
@@ -6690,6 +6697,7 @@ export function createDecisionServer(options) {
|
|
|
6690
6697
|
}
|
|
6691
6698
|
// The daily check's switch: an installation setting, so an operator's. (Its own address: POST /settings/updates starts an update.)
|
|
6692
6699
|
if (url.pathname === "/settings/updates/checks") {
|
|
6700
|
+
const body = readForm(posted, CONSOLE_FORMS.updatesChecks);
|
|
6693
6701
|
if (options.configDir === undefined)
|
|
6694
6702
|
return refuse(response, who, 404, "Updates are not set up on this console.", "/settings");
|
|
6695
6703
|
if (!store.isInstanceOperator(who.name))
|
|
@@ -6699,6 +6707,7 @@ export function createDecisionServer(options) {
|
|
|
6699
6707
|
return redirect(response, `/settings?said=${encodeURIComponent(on ? "Toolroll looks for a newer version once a day." : "Update checks are off.")}#updates`);
|
|
6700
6708
|
}
|
|
6701
6709
|
if (url.pathname === '/code' || url.pathname.startsWith('/code/')) {
|
|
6710
|
+
const body = readForm(posted, CONSOLE_FORMS.code);
|
|
6702
6711
|
const wantsJson = request.headers.accept?.includes('application/json') === true;
|
|
6703
6712
|
const fail = (status, message, delivery = 'rejected', sessionId) => wantsJson
|
|
6704
6713
|
? respond(response, status, 'application/json', JSON.stringify({ ok: false, error: message, delivery, ...(sessionId ? { sessionId } : {}) }))
|
|
@@ -6707,7 +6716,7 @@ export function createDecisionServer(options) {
|
|
|
6707
6716
|
return fail(403, 'Coding sessions require installation-wide operator access. You can manage project tasks in Work.');
|
|
6708
6717
|
if (!coding)
|
|
6709
6718
|
return fail(409, codingProblem || 'Open Toolroll on the machine with your installed coding agent.');
|
|
6710
|
-
if ([...new Set(body.keys())].some(key => body.getAll(key).length !== 1))
|
|
6719
|
+
if ([...new Set(body.keys())].some(key => body.sent.getAll(key).length !== 1))
|
|
6711
6720
|
return fail(400, 'Submit one value for each field.');
|
|
6712
6721
|
const actor = { name: who.name, generation: who.session.generation };
|
|
6713
6722
|
const permitted = (repo) => visible(repo) && codingProjectAllowed(repo);
|
|
@@ -6779,7 +6788,7 @@ export function createDecisionServer(options) {
|
|
|
6779
6788
|
if (ship && !wantsJson) {
|
|
6780
6789
|
try {
|
|
6781
6790
|
const preview = codingHandoffPreview(store, { sessionId: ship[1], actor: who.name });
|
|
6782
|
-
return sendScreen(response, 409, screen('Review for shipping', codingShippingHtml(preview, who.session.csrf, body, message), { chrome: chromeFor(preview.repo, 'code') }));
|
|
6791
|
+
return sendScreen(response, 409, screen('Review for shipping', codingShippingHtml(preview, who.session.csrf, body.sent, message), { chrome: chromeFor(preview.repo, 'code') }));
|
|
6783
6792
|
}
|
|
6784
6793
|
catch { }
|
|
6785
6794
|
}
|
|
@@ -6789,7 +6798,7 @@ export function createDecisionServer(options) {
|
|
|
6789
6798
|
if (!projectRequestAllowed(url, who, request, response))
|
|
6790
6799
|
return;
|
|
6791
6800
|
if (restricted()) {
|
|
6792
|
-
const repos =
|
|
6801
|
+
const repos = readForm(posted, CONSOLE_FORMS.projectCeiling).getAll("repo");
|
|
6793
6802
|
if (repos.length > 1 || repos.some(repo => repo.trim() !== "" && !visible(repo.trim())))
|
|
6794
6803
|
return refuse(response, who, 403, "That project is outside your access.", "/projects");
|
|
6795
6804
|
}
|
|
@@ -6809,6 +6818,7 @@ export function createDecisionServer(options) {
|
|
|
6809
6818
|
// re-counts within five seconds either way, this just makes it exact.
|
|
6810
6819
|
bustBadge();
|
|
6811
6820
|
if (url.pathname === "/settings/skills/revise") {
|
|
6821
|
+
const body = readForm(posted, CONSOLE_FORMS.skillsRevise);
|
|
6812
6822
|
if (who.via !== 'cookie' || who.role !== 'approver')
|
|
6813
6823
|
return refuse(response, who, 403, 'Sign in to revise a test.', '/settings/skills');
|
|
6814
6824
|
try {
|
|
@@ -6828,12 +6838,13 @@ export function createDecisionServer(options) {
|
|
|
6828
6838
|
}
|
|
6829
6839
|
}
|
|
6830
6840
|
if (url.pathname === "/settings/skills/import" || url.pathname === "/settings/skills/change") {
|
|
6841
|
+
const body = readForm(posted, CONSOLE_FORMS.skillsChange);
|
|
6831
6842
|
if (who.via !== 'cookie' || who.role !== 'approver')
|
|
6832
6843
|
return refuse(response, who, 403, 'Sign in as an approver to manage skills.', '/settings/skills');
|
|
6833
6844
|
const repo = body.get('repo') ?? '', back = `/settings/skills?repo=${encodeURIComponent(repo)}`;
|
|
6834
6845
|
if (!visible(repo) || !consoleProjects().includes(repo))
|
|
6835
6846
|
return refuse(response, who, 403, 'That project is outside your access.', '/projects');
|
|
6836
|
-
if ([...new Set(body.keys())].some(k => body.getAll(k).length !== 1))
|
|
6847
|
+
if ([...new Set(body.keys())].some(k => body.sent.getAll(k).length !== 1))
|
|
6837
6848
|
return refuse(response, who, 400, 'Invalid skills form.', back);
|
|
6838
6849
|
try {
|
|
6839
6850
|
const view = skillsView(store, repo, who.name);
|
|
@@ -6883,6 +6894,7 @@ export function createDecisionServer(options) {
|
|
|
6883
6894
|
// setting a starter kit up, trying its sample card, and bringing GitHub issues in.
|
|
6884
6895
|
const kitPost = /^\/kits\/([a-z-]{1,40})\/(setup|sample|github)$/.exec(url.pathname);
|
|
6885
6896
|
if (kitPost !== null) {
|
|
6897
|
+
const body = readForm(posted, CONSOLE_FORMS.kit);
|
|
6886
6898
|
const now = clock();
|
|
6887
6899
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
6888
6900
|
return refuse(response, who, 403, "Sign in as an approver to set kits up.", "/kits");
|
|
@@ -6914,6 +6926,7 @@ export function createDecisionServer(options) {
|
|
|
6914
6926
|
const teammatePost = url.pathname === "/teammates/new" ? ["", "0", "new"] : /^\/teammates\/([1-9][0-9]{0,9})\/(soul|state|note|settings|summary|tools|memory|routines|week)$/.exec(url.pathname);
|
|
6915
6927
|
const questionPost = /^\/teammates\/questions\/([1-9][0-9]{0,9})\/answer$/.exec(url.pathname);
|
|
6916
6928
|
if (teammatePost !== null || questionPost !== null) {
|
|
6929
|
+
const body = readForm(posted, CONSOLE_FORMS.teammates);
|
|
6917
6930
|
const now = clock();
|
|
6918
6931
|
const wantsJson = (request.headers.accept ?? "").includes("application/json");
|
|
6919
6932
|
if (who.via !== "cookie")
|
|
@@ -6989,7 +7002,7 @@ export function createDecisionServer(options) {
|
|
|
6989
7002
|
const grant = store.teammateGrant(mate.id, tool);
|
|
6990
7003
|
const changed = op === "grant" ? await grantTool(store, mate, tool, who.name, now, { toolHome })
|
|
6991
7004
|
: op === "revoke" ? revokeTool(store, mate, tool, who.name, now)
|
|
6992
|
-
: grant === null ? { ok: false, said: `${nameOf(mate)} doesn't use ${tool}.` } : setToolRules(store, mate, tool, rulesFromForm(grant, key => body.get(key)), who.name, now);
|
|
7005
|
+
: grant === null ? { ok: false, said: `${nameOf(mate)} doesn't use ${tool}.` } : setToolRules(store, mate, tool, rulesFromForm(grant, key => body.sent.get(key)), who.name, now);
|
|
6993
7006
|
return redirect(response, `${back}?${changed.ok ? "said" : "problem"}=${encodeURIComponent(changed.said)}#tools`);
|
|
6994
7007
|
}
|
|
6995
7008
|
const done = action === "state" ? setTeammateState(store, mate, body.get("state") === "removed" ? "removed" : body.get("state") === "paused" ? "paused" : "active", who.name, now)
|
|
@@ -7003,6 +7016,7 @@ export function createDecisionServer(options) {
|
|
|
7003
7016
|
// Flows → New → a template: preview these answers, or create exactly what was previewed.
|
|
7004
7017
|
const galleryPost = /^\/flows\/new\/([a-z-]{1,40})$/.exec(url.pathname);
|
|
7005
7018
|
if (galleryPost !== null) {
|
|
7019
|
+
const body = readForm(posted, CONSOLE_FORMS.galleryUse);
|
|
7006
7020
|
const template = galleryTemplateOf(galleryPost[1]);
|
|
7007
7021
|
const projects = consoleProjects();
|
|
7008
7022
|
if (template === null)
|
|
@@ -7034,6 +7048,7 @@ export function createDecisionServer(options) {
|
|
|
7034
7048
|
const flowPost = /^\/flows\/([1-9][0-9]{0,9})\/(save|cards|archive|triggers|linear-key|hooks-address|scripts|secrets)$/.exec(url.pathname) ?? /^\/flows\/([1-9][0-9]{0,9})\/cards\/([1-9][0-9]{0,9})\/(move|decide|choose|cancel|comment|assign|watch)$/.exec(url.pathname);
|
|
7035
7049
|
const triggerPost = /^\/flows\/([1-9][0-9]{0,9})\/triggers\/([1-9][0-9]{0,9})\/(pause|resume|remove|check|press|renew|secret|share|unshare)$/.exec(url.pathname);
|
|
7036
7050
|
if (url.pathname === "/flows/new" || url.pathname === "/flows/example" || url.pathname === "/flows/import" || flowPost !== null || triggerPost !== null) {
|
|
7051
|
+
const body = readForm(posted, CONSOLE_FORMS.flows);
|
|
7037
7052
|
const now = clock();
|
|
7038
7053
|
const answer = (status, payload) => respond(response, status, "application/json; charset=utf-8", JSON.stringify(payload));
|
|
7039
7054
|
const projects = consoleProjects();
|
|
@@ -7276,7 +7291,7 @@ export function createDecisionServer(options) {
|
|
|
7276
7291
|
const note = (body.get("note") ?? "").trim() || null;
|
|
7277
7292
|
if (note !== null && note.length > LIMITS.note)
|
|
7278
7293
|
return answer(400, { ok: false, said: `Keep the note to ${LIMITS.note.toLocaleString("en-US")} characters; this is ${note.length.toLocaleString("en-US")}.` });
|
|
7279
|
-
const decided = decideFlowCard(store, { card: target.id, decision, note, actor: who.name, repos: projects, evidenceRoot, draft: body.get("draft") }, now);
|
|
7294
|
+
const decided = decideFlowCard(store, { card: target.id, decision, note, actor: who.name, repos: projects, evidenceRoot, draft: body.get("draft"), where: "the console" }, now);
|
|
7280
7295
|
return decided.ok ? settle(decided.said) : answer(409, { ok: false, said: decided.message });
|
|
7281
7296
|
}
|
|
7282
7297
|
if (verb === "choose") {
|
|
@@ -7305,6 +7320,7 @@ export function createDecisionServer(options) {
|
|
|
7305
7320
|
// v103: an instance operator records a checkpoint of the ledger chain (its head, to copy off the machine).
|
|
7306
7321
|
// v105: set or remove a monthly budget. An instance operator, with a step-up; the ledger keeps before → after.
|
|
7307
7322
|
if (url.pathname === "/spend/budget") {
|
|
7323
|
+
const body = readForm(posted, CONSOLE_FORMS.spendBudget);
|
|
7308
7324
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
|
|
7309
7325
|
return refuse(response, who, 403, "An instance operator sets budgets.", "/spend");
|
|
7310
7326
|
const back = (key, words) => redirect(response, `/spend?${key}=${encodeURIComponent(words)}`);
|
|
@@ -7336,6 +7352,7 @@ export function createDecisionServer(options) {
|
|
|
7336
7352
|
// Delete a project, in two steps: its name, then what goes and the password. An instance operator; the ledger keeps who and what.
|
|
7337
7353
|
// Settings → Project → Builds at once: an approver for the project sets it; the ledger keeps before → after.
|
|
7338
7354
|
if (url.pathname === "/settings/project/concurrency") {
|
|
7355
|
+
const body = readForm(posted, CONSOLE_FORMS.projectConcurrency);
|
|
7339
7356
|
const repo = body.get("repo") ?? "";
|
|
7340
7357
|
const known = consoleProjects();
|
|
7341
7358
|
if (!known.includes(repo))
|
|
@@ -7355,6 +7372,7 @@ export function createDecisionServer(options) {
|
|
|
7355
7372
|
return back("said", changed.before === changed.after ? "Nothing changed." : `Saved. Up to ${n} at once.`);
|
|
7356
7373
|
}
|
|
7357
7374
|
if (url.pathname === "/settings/project/delete") {
|
|
7375
|
+
const body = readForm(posted, CONSOLE_FORMS.projectDelete);
|
|
7358
7376
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
|
|
7359
7377
|
return refuse(response, who, 403, "An instance operator deletes projects.", "/settings/project");
|
|
7360
7378
|
const repo = body.get("repo") ?? "";
|
|
@@ -7395,6 +7413,7 @@ export function createDecisionServer(options) {
|
|
|
7395
7413
|
}
|
|
7396
7414
|
// Sprint 8: an instance operator saves the organisation policy, with a step-up; the ledger keeps each rule's before → after.
|
|
7397
7415
|
if (url.pathname === "/settings/policy") {
|
|
7416
|
+
const body = readForm(posted, CONSOLE_FORMS.policy);
|
|
7398
7417
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
|
|
7399
7418
|
return refuse(response, who, 403, "An instance operator sets the policy.", "/settings/policy");
|
|
7400
7419
|
const back = (key, words) => redirect(response, `/settings/policy?${key}=${encodeURIComponent(words)}`);
|
|
@@ -7411,6 +7430,7 @@ export function createDecisionServer(options) {
|
|
|
7411
7430
|
}
|
|
7412
7431
|
// v102: an instance operator changes a project's approval rules, with a step-up; the ledger keeps before → after.
|
|
7413
7432
|
if (url.pathname === "/settings/approval") {
|
|
7433
|
+
const body = readForm(posted, CONSOLE_FORMS.approval);
|
|
7414
7434
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
|
|
7415
7435
|
return refuse(response, who, 403, "An instance operator sets approval rules.", "/settings/approval");
|
|
7416
7436
|
const repo = body.get("repo") ?? "";
|
|
@@ -7431,6 +7451,7 @@ export function createDecisionServer(options) {
|
|
|
7431
7451
|
return back("said", `Saved. ${rulesSummary(next)}`);
|
|
7432
7452
|
}
|
|
7433
7453
|
if (url.pathname === "/settings/sessions") {
|
|
7454
|
+
const body = readForm(posted, CONSOLE_FORMS.sessions);
|
|
7434
7455
|
if (who.via !== "cookie")
|
|
7435
7456
|
return refuse(response, who, 403, "Sign in in a browser to manage sessions and tokens.", "/settings");
|
|
7436
7457
|
const operator = store.isInstanceOperator(who.name);
|
|
@@ -7489,6 +7510,7 @@ export function createDecisionServer(options) {
|
|
|
7489
7510
|
return back("problem", "Choose an action.");
|
|
7490
7511
|
}
|
|
7491
7512
|
if (url.pathname === "/settings/sign-in") {
|
|
7513
|
+
const body = readForm(posted, CONSOLE_FORMS.signIn);
|
|
7492
7514
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name) || !options.configDir)
|
|
7493
7515
|
return refuse(response, who, 403, "An instance operator sets up sign-in.", "/settings");
|
|
7494
7516
|
const back = (key, words) => redirect(response, `/settings/sign-in?${key}=${encodeURIComponent(words)}`);
|
|
@@ -7526,6 +7548,7 @@ export function createDecisionServer(options) {
|
|
|
7526
7548
|
}
|
|
7527
7549
|
// Settings → Updates: start the `toolroll update` job. A step-up; the job writes the ledger entries.
|
|
7528
7550
|
if (url.pathname === "/settings/updates" || url.pathname === "/settings/updates/seen" || url.pathname === "/settings/updates/cancel") {
|
|
7551
|
+
const body = readForm(posted, CONSOLE_FORMS.updates);
|
|
7529
7552
|
const databaseFile = store.databaseFile();
|
|
7530
7553
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name) || databaseFile === null)
|
|
7531
7554
|
return refuse(response, who, 403, "An instance operator updates Toolroll.", "/settings");
|
|
@@ -7572,6 +7595,7 @@ export function createDecisionServer(options) {
|
|
|
7572
7595
|
}
|
|
7573
7596
|
// v105: change how long each kind of data is kept. A step-up; the ledger keeps before → after for each kind.
|
|
7574
7597
|
if (url.pathname === "/settings/retention") {
|
|
7598
|
+
const body = readForm(posted, CONSOLE_FORMS.retention);
|
|
7575
7599
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
|
|
7576
7600
|
return refuse(response, who, 403, "An instance operator sets retention.", "/settings");
|
|
7577
7601
|
const back = (key, words) => redirect(response, `/settings/retention?${key}=${encodeURIComponent(words)}`);
|
|
@@ -7591,6 +7615,7 @@ export function createDecisionServer(options) {
|
|
|
7591
7615
|
// Settings → Storage: when a finished task's clean checkout goes, cleaning up what the preview showed, and throwing
|
|
7592
7616
|
// away a checkout kept for its changes. Each takes the password; the ledger keeps every change and removal.
|
|
7593
7617
|
if (url.pathname === "/settings/storage" || url.pathname === "/settings/storage/clean" || url.pathname === "/settings/storage/discard") {
|
|
7618
|
+
const body = readForm(posted, CONSOLE_FORMS.storage);
|
|
7594
7619
|
const databaseFile = store.databaseFile();
|
|
7595
7620
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name) || databaseFile === null)
|
|
7596
7621
|
return refuse(response, who, 403, "An instance operator looks after storage.", "/settings");
|
|
@@ -7624,6 +7649,7 @@ export function createDecisionServer(options) {
|
|
|
7624
7649
|
// Pull requests for one project: turn on (the checks run again and must match what the page showed), merge
|
|
7625
7650
|
// settings, or turn off. Each takes the password and lands in the ledger.
|
|
7626
7651
|
if (url.pathname === "/settings/pull-requests") {
|
|
7652
|
+
const body = readForm(posted, CONSOLE_FORMS.pullRequests);
|
|
7627
7653
|
const repo = body.get("repo") ?? "";
|
|
7628
7654
|
const known = [...new Set([...managedRepos(), ...store.knownRepos(), ...store.listProjects().map(one => one.path)])];
|
|
7629
7655
|
if (who.via !== "cookie" || !known.includes(repo) || !visible(repo))
|
|
@@ -7661,6 +7687,7 @@ export function createDecisionServer(options) {
|
|
|
7661
7687
|
// Checks for one project: the level, or the quick command (approved like `verify set --quick`). Each takes
|
|
7662
7688
|
// the password and lands in the ledger.
|
|
7663
7689
|
if (url.pathname === "/settings/checks") {
|
|
7690
|
+
const body = readForm(posted, CONSOLE_FORMS.checks);
|
|
7664
7691
|
const repo = body.get("repo") ?? "";
|
|
7665
7692
|
const known = [...new Set([...managedRepos(), ...store.knownRepos(), ...store.listProjects().map(one => one.path)])];
|
|
7666
7693
|
if (who.via !== "cookie" || !known.includes(repo) || !visible(repo))
|
|
@@ -7722,6 +7749,7 @@ export function createDecisionServer(options) {
|
|
|
7722
7749
|
}
|
|
7723
7750
|
// Sprint 8: change the backup schedule. A step-up; the ledger keeps before → after.
|
|
7724
7751
|
if (url.pathname === "/settings/backups") {
|
|
7752
|
+
const body = readForm(posted, CONSOLE_FORMS.backups);
|
|
7725
7753
|
const databaseFile = store.databaseFile();
|
|
7726
7754
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name) || databaseFile === null)
|
|
7727
7755
|
return refuse(response, who, 403, "An instance operator looks after backups.", "/settings");
|
|
@@ -7748,6 +7776,7 @@ export function createDecisionServer(options) {
|
|
|
7748
7776
|
}
|
|
7749
7777
|
// v105: download everything as a .zip. An instance operator, with a step-up; the ledger records it.
|
|
7750
7778
|
if (url.pathname === "/settings/data") {
|
|
7779
|
+
const body = readForm(posted, CONSOLE_FORMS.data);
|
|
7751
7780
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
|
|
7752
7781
|
return refuse(response, who, 403, "An instance operator exports data.", "/settings");
|
|
7753
7782
|
if (!authenticateApprover(store, who.name, body.get("password") ?? "").ok) {
|
|
@@ -7762,6 +7791,7 @@ export function createDecisionServer(options) {
|
|
|
7762
7791
|
}
|
|
7763
7792
|
// Settings → Flows → Switch on: a starter flow's zones and trigger, made together.
|
|
7764
7793
|
if (url.pathname === "/settings/flows/on") {
|
|
7794
|
+
const body = readForm(posted, CONSOLE_FORMS.flowsOn);
|
|
7765
7795
|
const repo = body.get("repo") ?? "";
|
|
7766
7796
|
const back = (key, words) => redirect(response, `/settings/flows?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}`);
|
|
7767
7797
|
const known = [...new Set([...(admissionList() ?? []), ...managedRepos(), ...store.knownRepos()])];
|
|
@@ -7777,6 +7807,7 @@ export function createDecisionServer(options) {
|
|
|
7777
7807
|
}
|
|
7778
7808
|
// Settings → Integrations → Send test: one harmless read-only check of one integration, now.
|
|
7779
7809
|
if (url.pathname === "/settings/integrations/test") {
|
|
7810
|
+
const body = readForm(posted, CONSOLE_FORMS.integrationsTest);
|
|
7780
7811
|
if (who.via !== "cookie" || who.role !== "approver" || restricted() || !options.configDir)
|
|
7781
7812
|
return refuse(response, who, 403, "An installation approver tests integrations.", "/settings");
|
|
7782
7813
|
const key = body.get("key") ?? "";
|
|
@@ -7789,6 +7820,7 @@ export function createDecisionServer(options) {
|
|
|
7789
7820
|
}
|
|
7790
7821
|
// v104: change where monitoring goes. A step-up; the ledger keeps before → after (addresses, never secrets).
|
|
7791
7822
|
if (url.pathname === "/settings/monitoring") {
|
|
7823
|
+
const body = readForm(posted, CONSOLE_FORMS.monitoring);
|
|
7792
7824
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name) || !options.configDir)
|
|
7793
7825
|
return refuse(response, who, 403, "An instance operator sets up monitoring.", "/settings");
|
|
7794
7826
|
const back = (key, words) => redirect(response, `/settings/monitoring?${key}=${encodeURIComponent(words)}`);
|
|
@@ -7812,6 +7844,7 @@ export function createDecisionServer(options) {
|
|
|
7812
7844
|
return back("said", "Saved. Deliveries start within a few seconds.");
|
|
7813
7845
|
}
|
|
7814
7846
|
if (url.pathname === "/settings/tools/connect") {
|
|
7847
|
+
const body = readForm(posted, CONSOLE_FORMS.toolsConnect);
|
|
7815
7848
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
7816
7849
|
return refuse(response, who, 403, "Sign in as an approver to connect tools.", "/settings/tools");
|
|
7817
7850
|
const repo = body.get("repo") ?? "";
|
|
@@ -7887,6 +7920,7 @@ export function createDecisionServer(options) {
|
|
|
7887
7920
|
return goOutside(response, started.go, `Going to ${service.label} to sign in…`, { state: started.state, path: CONNECT_CALLBACK, secure: origin.startsWith("https:") });
|
|
7888
7921
|
}
|
|
7889
7922
|
if (url.pathname === "/settings/tools/change") {
|
|
7923
|
+
const body = readForm(posted, CONSOLE_FORMS.toolsChange);
|
|
7890
7924
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
7891
7925
|
return refuse(response, who, 403, "Sign in as an approver to manage tools.", "/settings/tools");
|
|
7892
7926
|
const repo = body.get("repo") ?? "";
|
|
@@ -7974,6 +8008,7 @@ export function createDecisionServer(options) {
|
|
|
7974
8008
|
}
|
|
7975
8009
|
}
|
|
7976
8010
|
if (url.pathname === "/settings/appearance") {
|
|
8011
|
+
const body = readForm(posted, CONSOLE_FORMS.appearance);
|
|
7977
8012
|
// A per-browser preference, not a shared setting: it lives in this
|
|
7978
8013
|
// person's own cookie and never in the database.
|
|
7979
8014
|
const accentField = body.get("accent");
|
|
@@ -8002,6 +8037,7 @@ export function createDecisionServer(options) {
|
|
|
8002
8037
|
return redirect(response, back);
|
|
8003
8038
|
}
|
|
8004
8039
|
if (url.pathname.startsWith("/settings/models/")) {
|
|
8040
|
+
const body = readForm(posted, CONSOLE_FORMS.models);
|
|
8005
8041
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
8006
8042
|
return refuse(response, who, 403, "Sign in as an approver to change models.", "/settings/models");
|
|
8007
8043
|
const back = (said, bad = false) => redirect(response, `/settings/models?${bad ? "problem" : "said"}=${encodeURIComponent(said)}`);
|
|
@@ -8056,6 +8092,7 @@ export function createDecisionServer(options) {
|
|
|
8056
8092
|
return refuse(response, who, 404, "No such models action.", "/settings/models");
|
|
8057
8093
|
}
|
|
8058
8094
|
if (url.pathname === "/settings/knowledge/refresh") {
|
|
8095
|
+
const body = readForm(posted, CONSOLE_FORMS.knowledgeRefresh);
|
|
8059
8096
|
if (who.via !== 'cookie' || who.role !== 'approver')
|
|
8060
8097
|
return refuse(response, who, 403, 'Sign in as an approver to refresh project context.', '/settings/knowledge');
|
|
8061
8098
|
const repo = body.get('repo') ?? '';
|
|
@@ -8065,6 +8102,7 @@ export function createDecisionServer(options) {
|
|
|
8065
8102
|
return sendScreen(response, refreshed.index.status === 'ready' ? 200 : 503, screen('Project context', `<h1>Project context</h1><p>${refreshed.index.status === 'ready' ? 'Code index refreshed.' : 'The index is unavailable. Source search still works.'}</p><a class="button-link" href="/settings/knowledge?repo=${encodeURIComponent(repo)}">Open knowledge</a>`, { chrome: chromeFor(repo, 'settings') }));
|
|
8066
8103
|
}
|
|
8067
8104
|
if (url.pathname === "/settings/knowledge/proposal") {
|
|
8105
|
+
const body = readForm(posted, CONSOLE_FORMS.knowledgeProposal);
|
|
8068
8106
|
if (who.via !== 'cookie' || who.role !== 'approver')
|
|
8069
8107
|
return refuse(response, who, 403, 'Sign in as an approver to decide memory proposals.', '/settings/knowledge');
|
|
8070
8108
|
const repo = body.get('repo') ?? '', decision = body.get('decision') ?? '';
|
|
@@ -8081,6 +8119,7 @@ export function createDecisionServer(options) {
|
|
|
8081
8119
|
return redirect(response, `/settings/knowledge?repo=${encodeURIComponent(repo)}&saved=1`);
|
|
8082
8120
|
}
|
|
8083
8121
|
if (url.pathname === "/settings/knowledge/decision") {
|
|
8122
|
+
const body = readForm(posted, CONSOLE_FORMS.knowledgeDecision);
|
|
8084
8123
|
if (who.via !== 'cookie' || who.role !== 'approver')
|
|
8085
8124
|
return refuse(response, who, 403, 'Sign in as an approver to change project decisions.', '/settings/knowledge');
|
|
8086
8125
|
const repo = body.get('repo') ?? '', action = body.get('action') ?? '';
|
|
@@ -8108,6 +8147,7 @@ export function createDecisionServer(options) {
|
|
|
8108
8147
|
return redirect(response, `/settings/knowledge?repo=${encodeURIComponent(repo)}&saved=1`);
|
|
8109
8148
|
}
|
|
8110
8149
|
if (url.pathname === "/settings/knowledge/change") {
|
|
8150
|
+
const body = readForm(posted, CONSOLE_FORMS.knowledgeChange);
|
|
8111
8151
|
if (who.via !== 'cookie' || who.role !== 'approver')
|
|
8112
8152
|
return refuse(response, who, 403, 'Sign in as an approver to change project knowledge.', '/settings/knowledge');
|
|
8113
8153
|
const repo = body.get('repo') ?? '', action = body.get('action') ?? '';
|
|
@@ -8133,6 +8173,7 @@ export function createDecisionServer(options) {
|
|
|
8133
8173
|
return redirect(response, `/settings/knowledge?repo=${encodeURIComponent(repo)}&saved=1`);
|
|
8134
8174
|
}
|
|
8135
8175
|
if (url.pathname === "/settings/learning/change") {
|
|
8176
|
+
const body = readForm(posted, CONSOLE_FORMS.learningChange);
|
|
8136
8177
|
if (who.via !== "cookie")
|
|
8137
8178
|
return refuse(response, who, 403, "Sign in to change learning.", "/settings/learning");
|
|
8138
8179
|
const repo = body.get("repo") ?? "", action = body.get("action") ?? "";
|
|
@@ -8149,6 +8190,7 @@ export function createDecisionServer(options) {
|
|
|
8149
8190
|
return redirect(response, `/settings/learning?repo=${encodeURIComponent(repo)}`);
|
|
8150
8191
|
}
|
|
8151
8192
|
if (["/control/setup-preview", "/control/setup-approve", "/control/instructions-preview", "/control/instructions-approve"].includes(url.pathname)) {
|
|
8193
|
+
const body = readForm(posted, CONSOLE_FORMS.projectSetup);
|
|
8152
8194
|
const project = projectOf(who, request);
|
|
8153
8195
|
if (project == null || body.get("repo") !== project || !visible(project))
|
|
8154
8196
|
return refuse(response, who, 409, "The selected project changed. Open project setup again.", "/control");
|
|
@@ -8188,6 +8230,7 @@ export function createDecisionServer(options) {
|
|
|
8188
8230
|
return redirect(response, "/control");
|
|
8189
8231
|
}
|
|
8190
8232
|
if (["connect", "pair", "unpair", "disconnect", "alerts"].some(action => url.pathname === `/settings/slack/${action}`)) {
|
|
8233
|
+
const body = readForm(posted, CONSOLE_FORMS.slack);
|
|
8191
8234
|
if (who.via !== "cookie" || who.role !== "approver" || restricted() || !options.configDir)
|
|
8192
8235
|
return refuse(response, who, 403, "An installation approver can connect Slack.", "/settings");
|
|
8193
8236
|
const dir = options.configDir, state = new SlackState(store), action = url.pathname.split("/").at(-1);
|
|
@@ -8248,7 +8291,53 @@ export function createDecisionServer(options) {
|
|
|
8248
8291
|
store.retryTelegramReplies(who.name, now);
|
|
8249
8292
|
return redirect(response, "/settings/telegram");
|
|
8250
8293
|
}
|
|
8294
|
+
// Approving from chat, as the person's lasting setting (chat-approval.ts): for all their projects or one. Turning it
|
|
8295
|
+
// on or changing its limits is the mode's ceremony (the exact terms, a single-use nonce bound to them, the password
|
|
8296
|
+
// typed again); turning it off is one step, csrf only. Every change is a ledger line.
|
|
8297
|
+
if (url.pathname === "/settings/chat-approval/confirm" || url.pathname === "/settings/chat-approval/save" || url.pathname === "/settings/chat-approval/off") {
|
|
8298
|
+
const body = readForm(posted, CONSOLE_FORMS.chatApproval);
|
|
8299
|
+
if (who.via !== "cookie" || who.role !== "approver")
|
|
8300
|
+
return refuse(response, who, 403, "An approver turns on approving from their own chat.", "/settings");
|
|
8301
|
+
if (["scope", "full-access", "cap-usd", "nonce", "digest", "token"].some(key => body.getAll(key).length > 1))
|
|
8302
|
+
return refuse(response, who, 400, "Submit one value for each field.", "/settings");
|
|
8303
|
+
const given = (body.get("scope") ?? "all").trim();
|
|
8304
|
+
const scope = given === "all" || given === "" ? CHAT_APPROVAL_ALL : canonicalProject(given) ?? given;
|
|
8305
|
+
if (scope !== CHAT_APPROVAL_ALL && !(visible(scope) && store.accountCanAccess(who.name, scope)))
|
|
8306
|
+
return refuse(response, who, 403, "That isn't one of your projects.", "/settings");
|
|
8307
|
+
const where = scope === CHAT_APPROVAL_ALL ? "all your projects" : projectName(scope);
|
|
8308
|
+
if (url.pathname.endsWith("/off")) {
|
|
8309
|
+
const saved = setChatApproval(store, { approver: who.name, scope, enabled: false, via: "the console" }, now);
|
|
8310
|
+
return saved.ok ? redirect(response, `/settings?said=${encodeURIComponent(saved.said)}`) : refuse(response, who, 400, saved.message, "/settings");
|
|
8311
|
+
}
|
|
8312
|
+
const usd = (body.get("cap-usd") ?? "").trim();
|
|
8313
|
+
const cap = usd === "" ? null : Number(usd);
|
|
8314
|
+
if (cap !== null && !(Number.isFinite(cap) && cap >= 0 && cap <= 10_000))
|
|
8315
|
+
return refuse(response, who, 400, "Give the attempt limit in dollars, like 5.", "/settings");
|
|
8316
|
+
const limits = { fullAccess: body.get("full-access") === "1", capMicrousd: cap === null ? null : Math.round(cap * 1_000_000) };
|
|
8317
|
+
const digest = createHash("sha256").update(JSON.stringify({ scope, ...limits })).digest("hex");
|
|
8318
|
+
if (url.pathname.endsWith("/confirm")) {
|
|
8319
|
+
const nonce = mintApprovalNonce(who.name, "chat-approval", digest);
|
|
8320
|
+
const html = `<h1 style="overflow-wrap:anywhere">Approve from chat in ${escape(where)}</h1>` +
|
|
8321
|
+
`<form method="post" action="/settings/chat-approval/save" class="card approve-form">` +
|
|
8322
|
+
hiddenFields({ csrf: who.session.csrf, nonce, digest, scope: scope === CHAT_APPROVAL_ALL ? "all" : scope, "full-access": limits.fullAccess ? "1" : "", "cap-usd": usd }) +
|
|
8323
|
+
`<p>Your own taps in your paired chat approve plans and merges until you turn this off. It doesn't expire.</p>` +
|
|
8324
|
+
`<p class="recap">Limits: ${escape(chatApprovalWords(limits))}.</p>` +
|
|
8325
|
+
`<p class="recap">Plans written for you, protected paths, two-person rules and your organisation's policy still open in Toolroll.</p>` +
|
|
8326
|
+
`<label>Your password<input type="password" name="token" autocomplete="current-password" required></label>` +
|
|
8327
|
+
`<div class="sticky-actions"><button type="submit">Turn on</button></div></form>` +
|
|
8328
|
+
`<p class="meta"><a href="/settings">Back</a></p>`;
|
|
8329
|
+
return sendScreen(response, 200, screen("Approve from chat", html, { chrome: chromeFor(projectOf(who, request) ?? null, "settings") }));
|
|
8330
|
+
}
|
|
8331
|
+
if (!consumeApprovalNonce(body.get("nonce") ?? "", who.name, "chat-approval", body.get("digest") ?? "") || body.get("digest") !== digest)
|
|
8332
|
+
return refuse(response, who, 409, "That form is stale. Read the terms again.", "/settings");
|
|
8333
|
+
const proved = authenticateAccount(store, who.name, body.get("token") ?? "");
|
|
8334
|
+
if (!proved.ok || proved.role !== "approver" || proved.generation !== who.session.generation)
|
|
8335
|
+
return refuse(response, who, 403, "Turning on approving from chat takes your password, typed again.", "/settings");
|
|
8336
|
+
const saved = setChatApproval(store, { approver: who.name, scope, enabled: true, limits, via: "the console" }, now);
|
|
8337
|
+
return saved.ok ? redirect(response, `/settings?said=${encodeURIComponent(saved.said)}`) : refuse(response, who, 400, saved.message, "/settings");
|
|
8338
|
+
}
|
|
8251
8339
|
if (url.pathname === "/settings/telegram/pair" || url.pathname === "/settings/telegram/unpair") {
|
|
8340
|
+
const body = readForm(posted, CONSOLE_FORMS.telegramPair);
|
|
8252
8341
|
// The person's own pairing, under their password: a code minted for
|
|
8253
8342
|
// them alone, or their own chats revoked. Teammates' pairings are
|
|
8254
8343
|
// never touched from here.
|
|
@@ -8274,6 +8363,7 @@ export function createDecisionServer(options) {
|
|
|
8274
8363
|
return sendScreen(response, 200, screen("Pair Telegram", telegramSettingsHtml(store, botId, who.name, who.session.csrf, { code }), { chrome: chromeFor(projectOf(who, request) ?? null, "settings"), forceSensitive: true }));
|
|
8275
8364
|
}
|
|
8276
8365
|
if (["connect", "pair", "unpair", "disconnect", "alerts"].some(action => url.pathname === `/settings/teams/${action}`)) {
|
|
8366
|
+
const body = readForm(posted, CONSOLE_FORMS.teams);
|
|
8277
8367
|
if (who.via !== "cookie" || who.role !== "approver" || restricted() || !options.configDir)
|
|
8278
8368
|
return refuse(response, who, 403, "An installation approver can connect Teams.", "/settings");
|
|
8279
8369
|
const dir = options.configDir, state = new ChatState(store, "teams"), action = url.pathname.split("/").at(-1);
|
|
@@ -8328,6 +8418,7 @@ export function createDecisionServer(options) {
|
|
|
8328
8418
|
return redirect(response, "/settings/teams");
|
|
8329
8419
|
}
|
|
8330
8420
|
if (["connect", "pair", "unpair", "disconnect", "alerts"].some(action => url.pathname === `/settings/discord/${action}`)) {
|
|
8421
|
+
const body = readForm(posted, CONSOLE_FORMS.discord);
|
|
8331
8422
|
if (who.via !== "cookie" || who.role !== "approver" || restricted() || !options.configDir)
|
|
8332
8423
|
return refuse(response, who, 403, "An installation approver can connect Discord.", "/settings");
|
|
8333
8424
|
const dir = options.configDir, state = new ChatState(store, "discord"), action = url.pathname.split("/").at(-1);
|
|
@@ -8382,6 +8473,7 @@ export function createDecisionServer(options) {
|
|
|
8382
8473
|
return redirect(response, "/settings/discord");
|
|
8383
8474
|
}
|
|
8384
8475
|
if (url.pathname === "/settings/messaging" && options.configDir !== undefined && options.telegramTokenFile !== undefined) {
|
|
8476
|
+
const body = readForm(posted, CONSOLE_FORMS.messaging);
|
|
8385
8477
|
const wanted = (body.get("primary") ?? "").trim();
|
|
8386
8478
|
const facts = effectivePrimary(process.env, options.configDir, loadBotToken(process.env, options.telegramTokenFile) !== null);
|
|
8387
8479
|
// Only a CONFIGURED service may page — choosing silence is not a
|
|
@@ -8393,6 +8485,7 @@ export function createDecisionServer(options) {
|
|
|
8393
8485
|
return redirect(response, "/settings");
|
|
8394
8486
|
}
|
|
8395
8487
|
if (url.pathname === "/settings/permission-default" && options.telegramTokenFile !== undefined) {
|
|
8488
|
+
const body = readForm(posted, CONSOLE_FORMS.permissionDefault);
|
|
8396
8489
|
const wanted = body.get("permission-mode");
|
|
8397
8490
|
if (wanted !== "auto" && wanted !== "bypassPermissions") {
|
|
8398
8491
|
return refuse(response, who, 400, "permissions must be Auto or Full access", "/settings");
|
|
@@ -8403,6 +8496,7 @@ export function createDecisionServer(options) {
|
|
|
8403
8496
|
: "New tasks now start with Auto. Approved tasks keep their setting.")}`);
|
|
8404
8497
|
}
|
|
8405
8498
|
if (url.pathname === "/settings/quality-default" && options.telegramTokenFile !== undefined) {
|
|
8499
|
+
const body = readForm(posted, CONSOLE_FORMS.qualityDefault);
|
|
8406
8500
|
const wanted = body.get("quality-mode");
|
|
8407
8501
|
if (!isQualityMode(wanted)) {
|
|
8408
8502
|
return refuse(response, who, 400, "quality must be Default or Strict / release", "/settings");
|
|
@@ -8413,6 +8507,7 @@ export function createDecisionServer(options) {
|
|
|
8413
8507
|
: "New tasks now use Default quality. Approved tasks keep their setting.")}`);
|
|
8414
8508
|
}
|
|
8415
8509
|
if (url.pathname === "/settings/notifications") {
|
|
8510
|
+
const body = readForm(posted, CONSOLE_FORMS.notifications);
|
|
8416
8511
|
// Each person's own choice: a closed list of modes and a 24-hour HH:MM, or off.
|
|
8417
8512
|
const mode = body.get("mode"), time = (body.get("digest") ?? "").trim(), shots = body.get("screenshots");
|
|
8418
8513
|
if (mode !== null && mode !== "quiet" && mode !== "all")
|
|
@@ -8432,6 +8527,7 @@ export function createDecisionServer(options) {
|
|
|
8432
8527
|
return redirect(response, `/settings?said=${encodeURIComponent(said)}`);
|
|
8433
8528
|
}
|
|
8434
8529
|
if (url.pathname === "/settings/notifications/mute") {
|
|
8530
|
+
const body = readForm(posted, CONSOLE_FORMS.notificationsMute);
|
|
8435
8531
|
// Each person's own pings: a muted project stays in Tasks and the evening digest.
|
|
8436
8532
|
const repo = body.get("repo") ?? "";
|
|
8437
8533
|
const project = notificationProjects(store, who.name).find(one => one.repo === repo);
|
|
@@ -8442,6 +8538,7 @@ export function createDecisionServer(options) {
|
|
|
8442
8538
|
return redirect(response, `/settings?said=${encodeURIComponent(muted ? `${project.name} muted. It still shows in Tasks and your evening digest.` : `${project.name} pings you again.`)}#notifications`);
|
|
8443
8539
|
}
|
|
8444
8540
|
if (url.pathname === "/settings/telegram-digest" && options.telegramTokenFile !== undefined) {
|
|
8541
|
+
const body = readForm(posted, CONSOLE_FORMS.telegramDigest);
|
|
8445
8542
|
// The cadence is a closed list of minutes — never a free number from
|
|
8446
8543
|
// a form; "off" clears it. Any approver session may set it.
|
|
8447
8544
|
const wanted = (body.get("every") ?? "").trim();
|
|
@@ -8453,6 +8550,7 @@ export function createDecisionServer(options) {
|
|
|
8453
8550
|
return redirect(response, `/settings?said=${encodeURIComponent(minutes === null ? "Digest off: each update arrives as it happens." : `Digest every ${minutes >= 60 ? `${minutes / 60}h` : `${minutes}m`}. Anything that needs you still arrives at once.`)}`);
|
|
8454
8551
|
}
|
|
8455
8552
|
if (url.pathname === "/settings/provider-key" || url.pathname === "/settings/provider-key-clear") {
|
|
8553
|
+
const body = readForm(posted, CONSOLE_FORMS.providerKey);
|
|
8456
8554
|
// The central gate already required an ACTIVE approver; the value is
|
|
8457
8555
|
// write-only from here — status pages say set/not-set, never bytes.
|
|
8458
8556
|
const provider = body.get("provider") ?? "";
|
|
@@ -8505,6 +8603,7 @@ export function createDecisionServer(options) {
|
|
|
8505
8603
|
return redirect(response, `/settings?said=${encodeURIComponent((verdict.ok ? `${stored} and verified — it works` : `${stored}. ${verdictWords(provider, verdict)}`) + modeNote)}`);
|
|
8506
8604
|
}
|
|
8507
8605
|
if (url.pathname === "/settings/telegram-token" && options.telegramTokenFile !== undefined) {
|
|
8606
|
+
const body = readForm(posted, CONSOLE_FORMS.telegramToken);
|
|
8508
8607
|
const value = body.get("token") ?? "";
|
|
8509
8608
|
const saved = saveBotToken(options.telegramTokenFile, value);
|
|
8510
8609
|
if (!saved.ok) {
|
|
@@ -8520,6 +8619,7 @@ export function createDecisionServer(options) {
|
|
|
8520
8619
|
}
|
|
8521
8620
|
// v87: the mail server Send email steps use, and a test email to its own address.
|
|
8522
8621
|
if ((url.pathname === "/settings/email" || url.pathname === "/settings/email-test" || url.pathname === "/settings/email-read-test") && who.role === "approver" && options.configDir !== undefined) {
|
|
8622
|
+
const body = readForm(posted, CONSOLE_FORMS.email);
|
|
8523
8623
|
const said = (words) => redirect(response, `/settings?said=${encodeURIComponent(words)}#email`);
|
|
8524
8624
|
if (url.pathname === "/settings/email") {
|
|
8525
8625
|
const saved = saveEmailSettings(options.configDir, { host: body.get("host"), port: body.get("port"), secure: body.get("secure"), user: body.get("user"), from: body.get("from"), password: body.get("password"), imapHost: body.get("imapHost"), imapPort: body.get("imapPort") });
|
|
@@ -8542,6 +8642,7 @@ export function createDecisionServer(options) {
|
|
|
8542
8642
|
}
|
|
8543
8643
|
// v89: a Google account for email: save the OAuth client and go to Google's consent screen, or disconnect.
|
|
8544
8644
|
if ((url.pathname === "/settings/google" || url.pathname === "/settings/google/disconnect") && who.role === "approver" && options.configDir !== undefined) {
|
|
8645
|
+
const body = readForm(posted, CONSOLE_FORMS.google);
|
|
8545
8646
|
const said = (words) => redirect(response, `/settings?said=${encodeURIComponent(words)}#email`);
|
|
8546
8647
|
if (url.pathname === "/settings/google/disconnect") {
|
|
8547
8648
|
await disconnectGoogle(options.configDir, options.googleFetch ?? fetch);
|
|
@@ -8561,6 +8662,7 @@ export function createDecisionServer(options) {
|
|
|
8561
8662
|
return goOutside(response, consent.url, "Going to Google to sign in…", { state: consent.visit.state, path: GOOGLE_CALLBACK, secure: origin.startsWith("https:") });
|
|
8562
8663
|
}
|
|
8563
8664
|
if (url.pathname === "/projects/select") {
|
|
8665
|
+
const body = readForm(posted, CONSOLE_FORMS.projectsSelect);
|
|
8564
8666
|
// Session-only project switching (E2): reads the registry, writes
|
|
8565
8667
|
// ONLY session.project — the durable upsert lives in /projects/open,
|
|
8566
8668
|
// which stays an approver's act. This is the one project verb a
|
|
@@ -8579,6 +8681,7 @@ export function createDecisionServer(options) {
|
|
|
8579
8681
|
return redirect(response, safeReturn(body.get("return")));
|
|
8580
8682
|
}
|
|
8581
8683
|
if (url.pathname === "/projects/remove") {
|
|
8684
|
+
const body = readForm(posted, CONSOLE_FORMS.projectsRemove);
|
|
8582
8685
|
// The reversible remove: off every list and the builder; tasks, results and settings stay. Deleting is
|
|
8583
8686
|
// Settings → Project → Delete project. The same admission code as `repos remove` (project-admission.ts).
|
|
8584
8687
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
|
|
@@ -8598,6 +8701,7 @@ export function createDecisionServer(options) {
|
|
|
8598
8701
|
return redirect(response, `/settings/project?said=${encodeURIComponent(`Removed ${projectName(repo)}. Its tasks and results stay saved.`)}`);
|
|
8599
8702
|
}
|
|
8600
8703
|
if (url.pathname === "/projects/open") {
|
|
8704
|
+
const body = readForm(posted, CONSOLE_FORMS.projectsOpen);
|
|
8601
8705
|
// Sessions only: a bearer caller names its project per request and has
|
|
8602
8706
|
// no session to mutate — refusing here keeps that boundary legible.
|
|
8603
8707
|
if (who.via !== "cookie") {
|
|
@@ -8637,6 +8741,7 @@ export function createDecisionServer(options) {
|
|
|
8637
8741
|
return redirect(response, safeReturn(body.get("return")));
|
|
8638
8742
|
}
|
|
8639
8743
|
if (url.pathname === "/tasks/add") {
|
|
8744
|
+
const body = readForm(posted, CONSOLE_FORMS.tasksAdd);
|
|
8640
8745
|
const project = projectOf(who, request);
|
|
8641
8746
|
if (project === undefined) {
|
|
8642
8747
|
return refuse(response, who, 403, "that project is outside what this server was configured to show");
|
|
@@ -8734,7 +8839,7 @@ export function createDecisionServer(options) {
|
|
|
8734
8839
|
});
|
|
8735
8840
|
if (!made.ok) {
|
|
8736
8841
|
const csrf = who.via === "cookie" ? who.session.csrf : "";
|
|
8737
|
-
return sendScreen(response, made.reason === "backlog-full" ? 429 : 400, tasksPage(chromeFor(project, "tasks"), familyTasksInView(project).slice(0, 200), null, csrf, made.message, project, { title, goal, not: notThis, touches: body.get("touches") ?? "", acceptance: body.get("acceptance") ?? "", values: body }, store.permissionDefault().mode, store.qualityDefault().mode, store.replacements()));
|
|
8842
|
+
return sendScreen(response, made.reason === "backlog-full" ? 429 : 400, tasksPage(chromeFor(project, "tasks"), familyTasksInView(project).slice(0, 200), null, csrf, made.message, project, { title, goal, not: notThis, touches: body.get("touches") ?? "", acceptance: body.get("acceptance") ?? "", values: body.sent }, store.permissionDefault().mode, store.qualityDefault().mode, store.replacements()));
|
|
8738
8843
|
}
|
|
8739
8844
|
const actionContext = requestContext.getStore();
|
|
8740
8845
|
if (actionContext !== undefined)
|
|
@@ -8748,6 +8853,7 @@ export function createDecisionServer(options) {
|
|
|
8748
8853
|
return redirect(response, taskHref(made.id));
|
|
8749
8854
|
}
|
|
8750
8855
|
if (url.pathname === "/queue/move" || url.pathname === "/queue/note") {
|
|
8856
|
+
const body = readForm(posted, CONSOLE_FORMS.queue);
|
|
8751
8857
|
const project = projectOf(who, request);
|
|
8752
8858
|
if (project === undefined) {
|
|
8753
8859
|
return refuse(response, who, 403, "that project is outside what this server was configured to show");
|
|
@@ -8862,6 +8968,7 @@ export function createDecisionServer(options) {
|
|
|
8862
8968
|
// hash — the page that shows it renders no self-refreshing script, so
|
|
8863
8969
|
// the token is never re-rendered.
|
|
8864
8970
|
if (url.pathname === "/fleet/runner/register") {
|
|
8971
|
+
const body = readForm(posted, CONSOLE_FORMS.runnerRegister);
|
|
8865
8972
|
if (who.via !== "cookie")
|
|
8866
8973
|
return refuse(response, who, 403, "runner registration is a browser surface");
|
|
8867
8974
|
const token = body.get("token") ?? "";
|
|
@@ -8891,6 +8998,7 @@ export function createDecisionServer(options) {
|
|
|
8891
8998
|
return sendScreen(response, 200, tokenScreen);
|
|
8892
8999
|
}
|
|
8893
9000
|
if (url.pathname === "/mode/confirm" || url.pathname === "/mode/sign") {
|
|
9001
|
+
const body = readForm(posted, CONSOLE_FORMS.mode);
|
|
8894
9002
|
if (who.via !== "cookie")
|
|
8895
9003
|
return refuse(response, who, 403, "signing a mode is a browser ceremony");
|
|
8896
9004
|
const project = projectOf(who, request);
|
|
@@ -8991,6 +9099,7 @@ export function createDecisionServer(options) {
|
|
|
8991
9099
|
return redirect(response, ended ? "/mode?said=the%20mode%20is%20ended%20—%20every%20act%20falls%20back%20to%20its%20own%20ceremony" : "/mode");
|
|
8992
9100
|
}
|
|
8993
9101
|
if (url.pathname === "/people/projects") {
|
|
9102
|
+
const body = readForm(posted, CONSOLE_FORMS.peopleProjects);
|
|
8994
9103
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
|
|
8995
9104
|
return refuse(response, who, 403, "An instance operator manages project access.", "/people");
|
|
8996
9105
|
if (!authenticateApprover(store, who.name, body.get("token") ?? "", null).ok)
|
|
@@ -9005,6 +9114,7 @@ export function createDecisionServer(options) {
|
|
|
9005
9114
|
return redirect(response, "/people?said=Project%20access%20saved");
|
|
9006
9115
|
}
|
|
9007
9116
|
if (url.pathname === "/people/invite") {
|
|
9117
|
+
const body = readForm(posted, CONSOLE_FORMS.peopleInvite);
|
|
9008
9118
|
if (who.via !== "cookie")
|
|
9009
9119
|
return refuse(response, who, 403, "inviting is a browser surface");
|
|
9010
9120
|
const token = body.get("token") ?? "";
|
|
@@ -9035,6 +9145,7 @@ export function createDecisionServer(options) {
|
|
|
9035
9145
|
return sendScreen(response, 200, linkScreen);
|
|
9036
9146
|
}
|
|
9037
9147
|
if (url.pathname === "/people/invite-revoke") {
|
|
9148
|
+
const body = readForm(posted, CONSOLE_FORMS.peopleInviteRevoke);
|
|
9038
9149
|
if (who.via !== "cookie")
|
|
9039
9150
|
return refuse(response, who, 403, "inviting is a browser surface");
|
|
9040
9151
|
const token = body.get("token") ?? "";
|
|
@@ -9046,6 +9157,7 @@ export function createDecisionServer(options) {
|
|
|
9046
9157
|
return redirect(response, `/people?said=${encodeURIComponent(revoked ? "the invite is cancelled — its link is dead" : "that invite was already gone")}`);
|
|
9047
9158
|
}
|
|
9048
9159
|
if (url.pathname === "/people/revoke") {
|
|
9160
|
+
const body = readForm(posted, CONSOLE_FORMS.peopleRevoke);
|
|
9049
9161
|
if (who.via !== "cookie")
|
|
9050
9162
|
return refuse(response, who, 403, "removing a person is a browser surface");
|
|
9051
9163
|
const token = body.get("token") ?? "";
|
|
@@ -9065,6 +9177,7 @@ export function createDecisionServer(options) {
|
|
|
9065
9177
|
return redirect(response, `/people?said=${encodeURIComponent(`${name} can no longer sign in — their sessions, invites, and signed modes ended with them; history stays`)}`);
|
|
9066
9178
|
}
|
|
9067
9179
|
if (url.pathname === "/fleet/runner/retire") {
|
|
9180
|
+
const body = readForm(posted, CONSOLE_FORMS.runnerRetire);
|
|
9068
9181
|
if (who.via !== "cookie")
|
|
9069
9182
|
return refuse(response, who, 403, "runner retirement is a browser surface");
|
|
9070
9183
|
const token = body.get("token") ?? "";
|
|
@@ -9079,6 +9192,7 @@ export function createDecisionServer(options) {
|
|
|
9079
9192
|
}
|
|
9080
9193
|
const answer = /^\/d\/([0-9]{1,15})\/answer$/.exec(url.pathname);
|
|
9081
9194
|
if (answer !== null) {
|
|
9195
|
+
const body = readForm(posted, CONSOLE_FORMS.decisionAnswer);
|
|
9082
9196
|
const id = Number(answer[1]);
|
|
9083
9197
|
const requestedReturn = body.get("return");
|
|
9084
9198
|
const decisionBack = requestedReturn === "next"
|
|
@@ -9118,6 +9232,7 @@ export function createDecisionServer(options) {
|
|
|
9118
9232
|
}
|
|
9119
9233
|
const contestAct = /^\/contest\/([0-9]{1,15})\/(arm|pick|abandon)$/.exec(url.pathname);
|
|
9120
9234
|
if (contestAct !== null) {
|
|
9235
|
+
const body = readForm(posted, CONSOLE_FORMS.contest);
|
|
9121
9236
|
const contestId = Number(contestAct[1]);
|
|
9122
9237
|
const verb = contestAct[2];
|
|
9123
9238
|
const data = contestData(contestId);
|
|
@@ -9192,10 +9307,11 @@ export function createDecisionServer(options) {
|
|
|
9192
9307
|
}
|
|
9193
9308
|
const attendAct = matchTaskPath(url.pathname, "/(attend-preview|attend|attend-revoke)$");
|
|
9194
9309
|
if (attendAct !== null) {
|
|
9195
|
-
return attendMutation(response, who, attendAct.taskId, attendAct.verb,
|
|
9310
|
+
return attendMutation(response, who, attendAct.taskId, attendAct.verb, readForm(posted, CONSOLE_FORMS.attend), now);
|
|
9196
9311
|
}
|
|
9197
9312
|
const act = matchTaskPath(url.pathname, "/(hold|unhold|requeue|cancel|scope|approve|plan|plan-edit|block|unblock|repair-dependency|next|reopen|steer|follow-up|accept-proof|accept-revision|reject-revision|route|retry-review|complete|merge|confirm-stopped|stop|resume-arm|resume)$");
|
|
9198
9313
|
if (act !== null && act.verb === "confirm-stopped") {
|
|
9314
|
+
const body = readForm(posted, CONSOLE_FORMS.confirmStopped);
|
|
9199
9315
|
// Confirming a finished build stopped is an approver's act behind the password, typed again — the same
|
|
9200
9316
|
// record as `toolroll run settle`, refused while anything of the run may still be running.
|
|
9201
9317
|
const back = taskHref(act.taskId);
|
|
@@ -9227,6 +9343,7 @@ export function createDecisionServer(options) {
|
|
|
9227
9343
|
return redirect(response, body.get("return") ? to : back);
|
|
9228
9344
|
}
|
|
9229
9345
|
if (act !== null && act.verb === "merge") {
|
|
9346
|
+
const body = readForm(posted, CONSOLE_FORMS.merge);
|
|
9230
9347
|
// Merging is a person's act behind their password, typed again: a browser session, an approver, the exact
|
|
9231
9348
|
// result's pull request. The flow re-reads GitHub before merging and records who merged in the ledger.
|
|
9232
9349
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
@@ -9248,9 +9365,10 @@ export function createDecisionServer(options) {
|
|
|
9248
9365
|
return redirect(response, `${taskHref(act.taskId)}#merge`);
|
|
9249
9366
|
}
|
|
9250
9367
|
if (act !== null) {
|
|
9251
|
-
return taskMutation(response, who, act.taskId, act.verb,
|
|
9368
|
+
return taskMutation(response, who, act.taskId, act.verb, posted, now);
|
|
9252
9369
|
}
|
|
9253
9370
|
if (url.pathname === "/projects/onboard-preview" || url.pathname === "/projects/onboard-confirm") {
|
|
9371
|
+
const body = readForm(posted, CONSOLE_FORMS.onboard);
|
|
9254
9372
|
// Repo onboarding (findings 1-39): bearer and demo refuse BEFORE any
|
|
9255
9373
|
// gh call; the console flow exists only on root-configured serves.
|
|
9256
9374
|
if (who.via !== "cookie")
|
|
@@ -9375,6 +9493,7 @@ export function createDecisionServer(options) {
|
|
|
9375
9493
|
: `${admitted} is ready and open`, 200);
|
|
9376
9494
|
}
|
|
9377
9495
|
if (url.pathname === "/push/subscribe") {
|
|
9496
|
+
const body = readForm(posted, CONSOLE_FORMS.pushSubscribe);
|
|
9378
9497
|
// Enrolling a durable notification sink is a CEREMONY (arc 3 finding
|
|
9379
9498
|
// 6): browser session + CSRF + the password typed again; bearer
|
|
9380
9499
|
// identities are machines and are refused outright.
|
|
@@ -9414,6 +9533,7 @@ export function createDecisionServer(options) {
|
|
|
9414
9533
|
return redirect(response, `/settings?said=${encodeURIComponent("this device now gets a buzz when the plane needs a person")}`);
|
|
9415
9534
|
}
|
|
9416
9535
|
if (url.pathname === "/push/remove") {
|
|
9536
|
+
const body = readForm(posted, CONSOLE_FORMS.pushRemove);
|
|
9417
9537
|
if (who.via !== "cookie")
|
|
9418
9538
|
return refuse(response, who, 403, "push removal is a browser session's act");
|
|
9419
9539
|
const id = Number(body.get("id") ?? "");
|
|
@@ -9424,6 +9544,7 @@ export function createDecisionServer(options) {
|
|
|
9424
9544
|
return redirect(response, `/settings?said=${encodeURIComponent("removed — that device stops receiving pushes")}`);
|
|
9425
9545
|
}
|
|
9426
9546
|
if (url.pathname === "/onboarding/phone/dismiss") {
|
|
9547
|
+
const body = readForm(posted, CONSOLE_FORMS.phoneDismiss);
|
|
9427
9548
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
9428
9549
|
return refuse(response, who, 403, "An approver puts this away.", "/chat");
|
|
9429
9550
|
store.recordInstallationFact(PHONE_CARD_FACT, who.name, now);
|
|
@@ -9433,6 +9554,7 @@ export function createDecisionServer(options) {
|
|
|
9433
9554
|
// conversation it needs no second password; the full form (Advanced) still asks for one.
|
|
9434
9555
|
// Settings → Lead: the name and persona this person's lead speaks with.
|
|
9435
9556
|
if (url.pathname === "/settings/lead/identity") {
|
|
9557
|
+
const body = readForm(posted, CONSOLE_FORMS.leadIdentity);
|
|
9436
9558
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
9437
9559
|
return refuse(response, who, 403, "An approver names their lead.", "/settings/lead");
|
|
9438
9560
|
const checked = checkLeadIdentity(body.get("name") ?? "", body.get("persona") ?? "");
|
|
@@ -9443,6 +9565,7 @@ export function createDecisionServer(options) {
|
|
|
9443
9565
|
}
|
|
9444
9566
|
// Settings → Lead: what this person's lead knows about them, one line each.
|
|
9445
9567
|
if (url.pathname === "/settings/lead/about") {
|
|
9568
|
+
const body = readForm(posted, CONSOLE_FORMS.leadAbout);
|
|
9446
9569
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
9447
9570
|
return refuse(response, who, 403, "An approver edits what their lead knows about them.", "/settings/lead");
|
|
9448
9571
|
const checked = checkAboutYou(body.get("about") ?? "");
|
|
@@ -9453,6 +9576,7 @@ export function createDecisionServer(options) {
|
|
|
9453
9576
|
}
|
|
9454
9577
|
// Settings → Lead: stop the lead following up on one of your promises.
|
|
9455
9578
|
if (url.pathname === "/settings/lead/promise/cancel") {
|
|
9579
|
+
const body = readForm(posted, CONSOLE_FORMS.leadPromiseCancel);
|
|
9456
9580
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
9457
9581
|
return refuse(response, who, 403, "An approver manages the lead's promises.", "/settings/lead");
|
|
9458
9582
|
const id = Number(body.get("promise"));
|
|
@@ -9471,6 +9595,7 @@ export function createDecisionServer(options) {
|
|
|
9471
9595
|
return redirect(response, "/chat");
|
|
9472
9596
|
}
|
|
9473
9597
|
if (url.pathname === "/chat/config") {
|
|
9598
|
+
const body = readForm(posted, CONSOLE_FORMS.chatConfig);
|
|
9474
9599
|
// The console's own door into `config set chat` (operator request:
|
|
9475
9600
|
// chat lives mainly in the web UI). Same ceremony weight as the CLI
|
|
9476
9601
|
// verb: the password typed again authenticates the approver, the
|
|
@@ -9549,6 +9674,7 @@ export function createDecisionServer(options) {
|
|
|
9549
9674
|
}
|
|
9550
9675
|
// ---- the mate (mate arc §5) ------------------------------------------
|
|
9551
9676
|
if (url.pathname === "/chat/mate/mint") {
|
|
9677
|
+
const body = readForm(posted, CONSOLE_FORMS.mateMint);
|
|
9552
9678
|
if (who.via !== "cookie")
|
|
9553
9679
|
return refuse(response, who, 403, "the mate is a browser surface");
|
|
9554
9680
|
const back = safeChatReturn(body.get("return"));
|
|
@@ -9573,6 +9699,7 @@ export function createDecisionServer(options) {
|
|
|
9573
9699
|
return redirect(response, back);
|
|
9574
9700
|
}
|
|
9575
9701
|
if (url.pathname === "/chat/mate/follow") {
|
|
9702
|
+
const body = readForm(posted, CONSOLE_FORMS.mateFollow);
|
|
9576
9703
|
if (who.via !== "cookie")
|
|
9577
9704
|
return refuse(response, who, 403, "Open the conversation to change automatic updates.");
|
|
9578
9705
|
const principal = matePrincipal(who), session = store.activeMateSession(who.name), thread = store.liveMateThreadFor(who.name);
|
|
@@ -9581,6 +9708,7 @@ export function createDecisionServer(options) {
|
|
|
9581
9708
|
return redirect(response, safeChatReturn(body.get("return")));
|
|
9582
9709
|
}
|
|
9583
9710
|
if (url.pathname === "/chat/mate/end") {
|
|
9711
|
+
const body = readForm(posted, CONSOLE_FORMS.mateEnd);
|
|
9584
9712
|
if (who.via !== "cookie")
|
|
9585
9713
|
return refuse(response, who, 403, "the mate is a browser surface");
|
|
9586
9714
|
const back = safeChatReturn(body.get("return"));
|
|
@@ -9593,6 +9721,7 @@ export function createDecisionServer(options) {
|
|
|
9593
9721
|
return redirect(response, back);
|
|
9594
9722
|
}
|
|
9595
9723
|
if (url.pathname === "/chat/mate/stop") {
|
|
9724
|
+
const body = readForm(posted, CONSOLE_FORMS.mateStop);
|
|
9596
9725
|
if (who.via !== "cookie")
|
|
9597
9726
|
return refuse(response, who, 403, "the mate is a browser surface");
|
|
9598
9727
|
const back = safeChatReturn(body.get("return"));
|
|
@@ -9611,6 +9740,7 @@ export function createDecisionServer(options) {
|
|
|
9611
9740
|
}
|
|
9612
9741
|
const mateProposal = /^\/chat\/proposal\/([0-9]{1,15})\/(confirm|dismiss)$/.exec(url.pathname);
|
|
9613
9742
|
if (mateProposal !== null) {
|
|
9743
|
+
const body = readForm(posted, CONSOLE_FORMS.mateProposal);
|
|
9614
9744
|
if (who.via !== "cookie")
|
|
9615
9745
|
return refuse(response, who, 403, "the mate is a browser surface");
|
|
9616
9746
|
const back = safeChatReturn(body.get("return"));
|
|
@@ -9696,6 +9826,7 @@ export function createDecisionServer(options) {
|
|
|
9696
9826
|
// ceiling admits the repo; the card lives on /chat and on the task.
|
|
9697
9827
|
const coordinatorProposal = /^\/proposals\/([0-9]{1,15})\/(confirm|dismiss)$/.exec(url.pathname);
|
|
9698
9828
|
if (coordinatorProposal !== null) {
|
|
9829
|
+
const body = readForm(posted, CONSOLE_FORMS.coordinatorProposal);
|
|
9699
9830
|
if (who.via !== "cookie")
|
|
9700
9831
|
return refuse(response, who, 403, "proposals are confirmed from the browser or the CLI");
|
|
9701
9832
|
const principal = matePrincipal(who);
|
|
@@ -9715,6 +9846,7 @@ export function createDecisionServer(options) {
|
|
|
9715
9846
|
return redirect(response, outcome.ok || outcome.reason !== "needs-confirm" ? back : `${back}${back.includes("?") ? "&" : "?"}said=${encodeURIComponent(outcome.said)}`);
|
|
9716
9847
|
}
|
|
9717
9848
|
if (url.pathname === "/chat") {
|
|
9849
|
+
const body = readForm(posted, CONSOLE_FORMS.chat);
|
|
9718
9850
|
if (who.via !== "cookie")
|
|
9719
9851
|
return refuse(response, who, 403, "chat is a browser surface");
|
|
9720
9852
|
const requestedTask = body.get("task");
|
|
@@ -9860,6 +9992,7 @@ export function createDecisionServer(options) {
|
|
|
9860
9992
|
}
|
|
9861
9993
|
const chatFile = /^\/chat\/file\/([0-9a-f]{32})$/.exec(url.pathname);
|
|
9862
9994
|
if (chatFile !== null) {
|
|
9995
|
+
const body = readForm(posted, CONSOLE_FORMS.chatFile);
|
|
9863
9996
|
if (who.via !== "cookie")
|
|
9864
9997
|
return refuse(response, who, 403, "chat is a browser surface");
|
|
9865
9998
|
const key = chatFile[1];
|
|
@@ -9933,6 +10066,7 @@ export function createDecisionServer(options) {
|
|
|
9933
10066
|
}
|
|
9934
10067
|
const chatAckPost = /^\/chat\/ack\/([0-9]{1,15})$/.exec(url.pathname);
|
|
9935
10068
|
if (chatAckPost !== null) {
|
|
10069
|
+
const body = readForm(posted, CONSOLE_FORMS.chatAck);
|
|
9936
10070
|
if (who.via !== "cookie")
|
|
9937
10071
|
return refuse(response, who, 403, "acknowledgement is a browser ceremony");
|
|
9938
10072
|
const turn = store.getChatTurn(Number(chatAckPost[1]));
|
|
@@ -9952,6 +10086,7 @@ export function createDecisionServer(options) {
|
|
|
9952
10086
|
return redirect(response, "/chat");
|
|
9953
10087
|
}
|
|
9954
10088
|
if (["/recipes/prepare", "/recipes/preview", "/recipes/import", "/recipes/launch", "/recipes/save"].includes(url.pathname)) {
|
|
10089
|
+
const body = readForm(posted, CONSOLE_FORMS.recipes);
|
|
9955
10090
|
const project = projectOf(who, request);
|
|
9956
10091
|
try {
|
|
9957
10092
|
if (project === null || project === undefined || !visible(project))
|
|
@@ -9964,7 +10099,7 @@ export function createDecisionServer(options) {
|
|
|
9964
10099
|
const rootMode = !unscopedMode && ceiling.roots.length > 0;
|
|
9965
10100
|
if (rootMode && !(await authorizedProject(liveCeiling(), project)))
|
|
9966
10101
|
throw new RecipeError("The project is outside this server's access.", 403);
|
|
9967
|
-
const rechecked = authorizeMutation(request, who,
|
|
10102
|
+
const rechecked = authorizeMutation(request, who, readForm(posted, CONSOLE_FORMS.mutationGuard));
|
|
9968
10103
|
if (rechecked !== null)
|
|
9969
10104
|
throw new RecipeError(rechecked.message, rechecked.status);
|
|
9970
10105
|
if (projectOf(who, request) !== project)
|
|
@@ -9979,13 +10114,13 @@ export function createDecisionServer(options) {
|
|
|
9979
10114
|
if (recipe === null || recipe.repo === null)
|
|
9980
10115
|
throw new RecipeError("This recipe is not available in this project.", 404);
|
|
9981
10116
|
try {
|
|
9982
|
-
const preview = prepareRecipeRun(store, who.name, project, id, Number(version), recipeAnswersFromForm(body), now);
|
|
10117
|
+
const preview = prepareRecipeRun(store, who.name, project, id, Number(version), recipeAnswersFromForm(body.sent), now);
|
|
9983
10118
|
return redirect(response, `/recipes/preview?preview=${preview.token}`);
|
|
9984
10119
|
}
|
|
9985
10120
|
catch (error) {
|
|
9986
10121
|
if (!(error instanceof RecipeError))
|
|
9987
10122
|
throw error;
|
|
9988
|
-
return sendScreen(response, error.status, screen("Use recipe", recipeRunHtml(recipe, project, csrf, revision, error.message, body), { chrome: chromeFor(project, "recipes"), functional: { script: recipeScript() } }));
|
|
10123
|
+
return sendScreen(response, error.status, screen("Use recipe", recipeRunHtml(recipe, project, csrf, revision, error.message, body.sent), { chrome: chromeFor(project, "recipes"), functional: { script: recipeScript() } }));
|
|
9989
10124
|
}
|
|
9990
10125
|
}
|
|
9991
10126
|
if (url.pathname === "/recipes/import") {
|
|
@@ -10005,13 +10140,13 @@ export function createDecisionServer(options) {
|
|
|
10005
10140
|
throw new RecipeError("That recipe version is not available in this project.", 404);
|
|
10006
10141
|
let document;
|
|
10007
10142
|
try {
|
|
10008
|
-
document = recipeFromForm(body);
|
|
10143
|
+
document = recipeFromForm(body.sent);
|
|
10009
10144
|
}
|
|
10010
10145
|
catch (error) {
|
|
10011
10146
|
if (!(error instanceof RecipeError))
|
|
10012
10147
|
throw error;
|
|
10013
10148
|
const fallback = original ?? { ...starterRecipes().find(one => one.id === "small-feature"), id: source };
|
|
10014
|
-
return sendScreen(response, error.status, screen("Customize recipe", recipeEditorHtml(fallback, project, csrf, revision, error.message, body, purpose === "recipe"), { chrome: chromeFor(project, "recipes"), functional: { script: recipeScript() } }));
|
|
10149
|
+
return sendScreen(response, error.status, screen("Customize recipe", recipeEditorHtml(fallback, project, csrf, revision, error.message, body.sent, purpose === "recipe"), { chrome: chromeFor(project, "recipes"), functional: { script: recipeScript() } }));
|
|
10015
10150
|
}
|
|
10016
10151
|
const preview = createWorkflowPreview(store, who.name, project, document, source, now);
|
|
10017
10152
|
return redirect(response, `/recipes/preview?preview=${preview.token}${purpose === "recipe" || document.version === 2 ? "&purpose=recipe" : ""}`);
|
|
@@ -10040,6 +10175,7 @@ export function createDecisionServer(options) {
|
|
|
10040
10175
|
}
|
|
10041
10176
|
}
|
|
10042
10177
|
if (url.pathname === "/routines/add") {
|
|
10178
|
+
const body = readForm(posted, CONSOLE_FORMS.routinesAdd);
|
|
10043
10179
|
// A standing order files into the OPEN project — never a typed path,
|
|
10044
10180
|
// so the ceiling question never even arises — and lands on its own
|
|
10045
10181
|
// screen where the approval step-up already lives: filing is cheap,
|
|
@@ -10075,11 +10211,11 @@ export function createDecisionServer(options) {
|
|
|
10075
10211
|
// 7): the service validates, canonicalizes, digests, and stamps
|
|
10076
10212
|
// provenance; the admission list makes the ceiling explicit even
|
|
10077
10213
|
// though `project` was already proved inside it.
|
|
10078
|
-
const scheduled = body.has("repeat") ? composerSchedule(body) : { ok: true, schedule: (body.get("schedule") ?? "").trim() };
|
|
10214
|
+
const scheduled = body.has("repeat") ? composerSchedule(body.sent) : { ok: true, schedule: (body.get("schedule") ?? "").trim() };
|
|
10079
10215
|
if (!scheduled.ok || scheduled.schedule === null) {
|
|
10080
10216
|
return sendScreen(response, 400, routinesPage(chromeFor(project, "routines"), store.routineTracks(project, now).filter(track => visible(track.routine.repo)), {
|
|
10081
10217
|
csrf: who.via === "cookie" ? who.session.csrf : "", revision: who.via === "cookie" ? who.session.projectRevision : 0,
|
|
10082
|
-
problem: scheduled.ok ? "Choose a recurring schedule." : scheduled.message, values: body,
|
|
10218
|
+
problem: scheduled.ok ? "Choose a recurring schedule." : scheduled.message, values: body.sent,
|
|
10083
10219
|
}));
|
|
10084
10220
|
}
|
|
10085
10221
|
const created = fileRoutineProposal(store, {
|
|
@@ -10102,17 +10238,18 @@ export function createDecisionServer(options) {
|
|
|
10102
10238
|
return sendScreen(response, created.reason === "duplicate" ? 409 : 400, routinesPage(chromeFor(project, "routines"), tracks, {
|
|
10103
10239
|
csrf: who.via === "cookie" ? who.session.csrf : "",
|
|
10104
10240
|
revision: who.via === "cookie" ? who.session.projectRevision : 0,
|
|
10105
|
-
problem: created.message, values: body,
|
|
10241
|
+
problem: created.message, values: body.sent,
|
|
10106
10242
|
}));
|
|
10107
10243
|
}
|
|
10108
10244
|
return redirect(response, `/routines/${created.id}`);
|
|
10109
10245
|
}
|
|
10110
10246
|
const routineAct = /^\/routines\/([0-9]{1,15})\/(approve|refresh|pause|resume|run-now)$/.exec(url.pathname);
|
|
10111
10247
|
if (routineAct !== null) {
|
|
10112
|
-
return routineMutation(response, who, Number(routineAct[1]), routineAct[2],
|
|
10248
|
+
return routineMutation(response, who, Number(routineAct[1]), routineAct[2], readForm(posted, CONSOLE_FORMS.routine), now);
|
|
10113
10249
|
}
|
|
10114
10250
|
const runNote = /^\/r\/([0-9]{1,15})\/note$/.exec(url.pathname);
|
|
10115
10251
|
if (runNote !== null) {
|
|
10252
|
+
const body = readForm(posted, CONSOLE_FORMS.runNote);
|
|
10116
10253
|
// An operator's verdict beside the machine's record (M6): immutable,
|
|
10117
10254
|
// bounded by the same validator as decision notes, ceiling-checked
|
|
10118
10255
|
// like every run resource. Ordinary authenticated mutation — no
|
|
@@ -10130,6 +10267,7 @@ export function createDecisionServer(options) {
|
|
|
10130
10267
|
}
|
|
10131
10268
|
const diffComment = /^\/r\/([0-9]{1,15})\/comment$/.exec(url.pathname);
|
|
10132
10269
|
if (diffComment !== null) {
|
|
10270
|
+
const body = readForm(posted, CONSOLE_FORMS.diffComment);
|
|
10133
10271
|
// A review comment on the IMMUTABLE terminal diff (M6.8): bound to
|
|
10134
10272
|
// the exact artifact and its hash. Ordinary authenticated mutation —
|
|
10135
10273
|
// the nonce belongs to the approval screen that later restates the
|
|
@@ -10216,6 +10354,7 @@ export function createDecisionServer(options) {
|
|
|
10216
10354
|
return redirect(response, `${back}${back.includes("?") ? "&" : "?"}noted=${receipt}#request-changes`);
|
|
10217
10355
|
}
|
|
10218
10356
|
if (url.pathname === "/session/editor-links") {
|
|
10357
|
+
const body = readForm(posted, CONSOLE_FORMS.editorLinks);
|
|
10219
10358
|
// The session half of the editor-link activation (arc 6, finding 1):
|
|
10220
10359
|
// only the person at the browser can say "this device holds the
|
|
10221
10360
|
// worktrees". Per-session, dies with the session, grants nothing —
|
|
@@ -10230,6 +10369,7 @@ export function createDecisionServer(options) {
|
|
|
10230
10369
|
}
|
|
10231
10370
|
const turnAct = /^\/r\/([0-9]{1,15})\/turn$/.exec(url.pathname);
|
|
10232
10371
|
if (turnAct !== null) {
|
|
10372
|
+
const body = readForm(posted, CONSOLE_FORMS.turn);
|
|
10233
10373
|
// The operator's turn (Phase 2E, v2 S1g): cookie-only — a watching
|
|
10234
10374
|
// person, never a bearer machine — and every hard gate (custody,
|
|
10235
10375
|
// lease, cap, budget, open decision, single flight) re-proves
|
|
@@ -10266,6 +10406,7 @@ export function createDecisionServer(options) {
|
|
|
10266
10406
|
}
|
|
10267
10407
|
const revise = /^\/r\/([0-9]{1,15})\/revise$/.exec(url.pathname);
|
|
10268
10408
|
if (revise !== null) {
|
|
10409
|
+
const body = readForm(posted, CONSOLE_FORMS.revise);
|
|
10269
10410
|
// Seal the live comment batch into ONE unapproved revision task with
|
|
10270
10411
|
// an immutable brief (M6.8). Deterministic — no model reads anything
|
|
10271
10412
|
// here — and every revision takes its own approval: comments can
|
|
@@ -10288,6 +10429,7 @@ export function createDecisionServer(options) {
|
|
|
10288
10429
|
// approved command once and seals the log), or file a task to add tests.
|
|
10289
10430
|
const followUp = /^\/r\/([0-9]{1,15})\/(checks|add-tests)$/.exec(url.pathname);
|
|
10290
10431
|
if (followUp !== null) {
|
|
10432
|
+
const body = readForm(posted, CONSOLE_FORMS.followUp);
|
|
10291
10433
|
const id = Number(followUp[1]);
|
|
10292
10434
|
const found = store.getRun(id);
|
|
10293
10435
|
if (found === null || !visible(taskRepoOf(found.taskRef)))
|
|
@@ -10385,6 +10527,7 @@ export function createDecisionServer(options) {
|
|
|
10385
10527
|
}
|
|
10386
10528
|
const resolve = /^\/i\/([0-9]{1,15})\/resolve$/.exec(url.pathname);
|
|
10387
10529
|
if (resolve !== null) {
|
|
10530
|
+
const body = readForm(posted, CONSOLE_FORMS.resolveIncident);
|
|
10388
10531
|
const id = Number(resolve[1]);
|
|
10389
10532
|
// The ceiling applies to incident mutation exactly as to every other
|
|
10390
10533
|
// resource (v3 review, finding 3): resolve incident → run → task, and
|
|
@@ -10668,7 +10811,7 @@ export function createDecisionServer(options) {
|
|
|
10668
10811
|
store.beatAuthorization(minted.authorization.id, now);
|
|
10669
10812
|
return redirect(response, taskHref(taskId));
|
|
10670
10813
|
}
|
|
10671
|
-
function taskMutation(response, who, taskId, verb,
|
|
10814
|
+
function taskMutation(response, who, taskId, verb, posted, now) {
|
|
10672
10815
|
const ref = store.lookupRef(taskId);
|
|
10673
10816
|
if (ref === null || store.getTask(taskId) === null) {
|
|
10674
10817
|
return refuse(response, who, 404, "no such task", "/tasks");
|
|
@@ -10678,6 +10821,7 @@ export function createDecisionServer(options) {
|
|
|
10678
10821
|
}
|
|
10679
10822
|
switch (verb) {
|
|
10680
10823
|
case "steer": {
|
|
10824
|
+
const body = readForm(posted, CONSOLE_FORMS.taskSteer);
|
|
10681
10825
|
// Steering is a browser session's act, explicitly (arc 1 v2 §3):
|
|
10682
10826
|
// identify() accepts bearer credentials generically, and those are
|
|
10683
10827
|
// for machines — a person watching steers, cookie + CSRF only.
|
|
@@ -10699,6 +10843,7 @@ export function createDecisionServer(options) {
|
|
|
10699
10843
|
return redirect(response, taskHref(taskId));
|
|
10700
10844
|
}
|
|
10701
10845
|
case "hold": {
|
|
10846
|
+
const body = readForm(posted, CONSOLE_FORMS.taskHold);
|
|
10702
10847
|
const reason = (body.get("reason") ?? "").trim() || "held from the console";
|
|
10703
10848
|
if (reason.length > 200 || hasForbiddenControls(reason)) {
|
|
10704
10849
|
return taskScreen(response, who, taskId, "that reason will not render, so it will not store", 400);
|
|
@@ -10710,6 +10855,7 @@ export function createDecisionServer(options) {
|
|
|
10710
10855
|
return redirect(response, taskHref(taskId));
|
|
10711
10856
|
}
|
|
10712
10857
|
case "block": {
|
|
10858
|
+
const body = readForm(posted, CONSOLE_FORMS.taskBlock);
|
|
10713
10859
|
// Chains are scheduling, not authority (chains-and-next review,
|
|
10714
10860
|
// finding 2): the edge decides WHEN the ready set admits the task;
|
|
10715
10861
|
// approval still decides WHAT may build. Both ends are re-proved
|
|
@@ -10731,6 +10877,7 @@ export function createDecisionServer(options) {
|
|
|
10731
10877
|
return redirect(response, taskHref(taskId));
|
|
10732
10878
|
}
|
|
10733
10879
|
case "unblock": {
|
|
10880
|
+
const body = readForm(posted, CONSOLE_FORMS.taskBlock);
|
|
10734
10881
|
const on = (body.get("on") ?? "").trim();
|
|
10735
10882
|
if (store.openContestFor(ref.id) !== null) {
|
|
10736
10883
|
return taskScreen(response, who, taskId, "a tournament is running on this task — let it finish, then pick or abandon it", 409);
|
|
@@ -10742,6 +10889,7 @@ export function createDecisionServer(options) {
|
|
|
10742
10889
|
return redirect(response, taskHref(taskId));
|
|
10743
10890
|
}
|
|
10744
10891
|
case "repair-dependency": {
|
|
10892
|
+
const body = readForm(posted, CONSOLE_FORMS.taskRepairDependency);
|
|
10745
10893
|
const blocker = (body.get("blocker") ?? "").trim();
|
|
10746
10894
|
const operation = (body.get("operation") ?? "").trim();
|
|
10747
10895
|
const blockerTask = blocker === "" || !store.blockers(taskId).includes(blocker) ? null : store.getTask(blocker);
|
|
@@ -10785,6 +10933,7 @@ export function createDecisionServer(options) {
|
|
|
10785
10933
|
return taskScreen(response, who, taskId, "choose whether to try that task again, wait for a different task, or continue without it", 400);
|
|
10786
10934
|
}
|
|
10787
10935
|
case "next": {
|
|
10936
|
+
const body = readForm(posted, CONSOLE_FORMS.taskNext);
|
|
10788
10937
|
if (body.get("undo") !== null) {
|
|
10789
10938
|
const cleared = store.clearTaskPriority(taskId);
|
|
10790
10939
|
if (!cleared.ok)
|
|
@@ -10805,6 +10954,7 @@ export function createDecisionServer(options) {
|
|
|
10805
10954
|
return redirect(response, taskHref(taskId));
|
|
10806
10955
|
}
|
|
10807
10956
|
case "reopen": {
|
|
10957
|
+
const body = readForm(posted, CONSOLE_FORMS.taskReopen);
|
|
10808
10958
|
// Authenticated like every approving act: the session alone may
|
|
10809
10959
|
// read; resuming external work takes the password, typed again.
|
|
10810
10960
|
const token = (body.get("token") ?? "").trim();
|
|
@@ -10834,6 +10984,7 @@ export function createDecisionServer(options) {
|
|
|
10834
10984
|
return redirect(response, taskHref(taskId));
|
|
10835
10985
|
}
|
|
10836
10986
|
case "requeue": {
|
|
10987
|
+
const body = readForm(posted, CONSOLE_FORMS.taskRequeue);
|
|
10837
10988
|
// Retry may carry a note for the next attempt: the same steering note, checked before anything changes.
|
|
10838
10989
|
const note = (body.get("note") ?? "").trim();
|
|
10839
10990
|
if (note !== "" && who.via !== "cookie")
|
|
@@ -10853,6 +11004,7 @@ export function createDecisionServer(options) {
|
|
|
10853
11004
|
return redirect(response, body.get("return") === "inbox" ? "/inbox" : body.get("return") === "next" ? "/next" : taskHref(taskId));
|
|
10854
11005
|
}
|
|
10855
11006
|
case "follow-up": {
|
|
11007
|
+
const body = readForm(posted, CONSOLE_FORMS.taskFollowUp);
|
|
10856
11008
|
// A scout's proposed follow-up, filed by the operator's tap (mate
|
|
10857
11009
|
// arc §10): the ONE filing door, this task's repository, the scope
|
|
10858
11010
|
// text stamped as the scout's — mode coverage never seals it.
|
|
@@ -10910,6 +11062,7 @@ export function createDecisionServer(options) {
|
|
|
10910
11062
|
return redirect(response, taskHref(taskId));
|
|
10911
11063
|
}
|
|
10912
11064
|
case "plan-edit": {
|
|
11065
|
+
const body = readForm(posted, CONSOLE_FORMS.taskPlanEdit);
|
|
10913
11066
|
if (who.via !== "cookie") {
|
|
10914
11067
|
return refuse(response, who, 403, "editing a plan is a browser session's act");
|
|
10915
11068
|
}
|
|
@@ -10967,6 +11120,7 @@ export function createDecisionServer(options) {
|
|
|
10967
11120
|
}
|
|
10968
11121
|
case "accept-revision":
|
|
10969
11122
|
case "reject-revision": {
|
|
11123
|
+
const body = readForm(posted, CONSOLE_FORMS.taskRevision);
|
|
10970
11124
|
// Adaptive execution plans (v44): resolving a revision the builder
|
|
10971
11125
|
// filed while its authority snapshot no longer matched the run it
|
|
10972
11126
|
// started under — the ONLY road a 'blocked' plan_revision reaches,
|
|
@@ -10997,6 +11151,7 @@ export function createDecisionServer(options) {
|
|
|
10997
11151
|
return redirect(response, taskHref(taskId));
|
|
10998
11152
|
}
|
|
10999
11153
|
case "cancel": {
|
|
11154
|
+
const body = readForm(posted, CONSOLE_FORMS.taskCancel);
|
|
11000
11155
|
const reason = body.get("reason") ?? undefined;
|
|
11001
11156
|
const cancelled = withActor({ account: who.name, lead: false }, () => store.cancelTask(taskId, now, reason));
|
|
11002
11157
|
if (!cancelled.ok) {
|
|
@@ -11010,6 +11165,7 @@ export function createDecisionServer(options) {
|
|
|
11010
11165
|
return redirect(response, taskHref(taskId));
|
|
11011
11166
|
}
|
|
11012
11167
|
case "route": {
|
|
11168
|
+
const body = readForm(posted, CONSOLE_FORMS.taskRoute);
|
|
11013
11169
|
// THE AGENTS EDIT (v47): an approver's session declares the risk,
|
|
11014
11170
|
// overrides one role to an exact agent, or clears an override — ONE
|
|
11015
11171
|
// authenticated transaction in the store that CAS-checks the scope
|
|
@@ -11095,6 +11251,7 @@ export function createDecisionServer(options) {
|
|
|
11095
11251
|
return redirect(response, toChat ? target : `${target}#agents`);
|
|
11096
11252
|
}
|
|
11097
11253
|
case "scope": {
|
|
11254
|
+
const body = readForm(posted, CONSOLE_FORMS.taskScope);
|
|
11098
11255
|
const sawDigest = body.get("sawDigest");
|
|
11099
11256
|
const permissionGiven = body.get("permission-mode");
|
|
11100
11257
|
if (permissionGiven !== null && permissionGiven !== "" && permissionGiven !== "auto" && permissionGiven !== "bypassPermissions") {
|
|
@@ -11290,7 +11447,7 @@ export function createDecisionServer(options) {
|
|
|
11290
11447
|
if (!saved.ok) {
|
|
11291
11448
|
// Refused because the plan moved on: the draft comes back with the version now on file, so saving it
|
|
11292
11449
|
// again (having read the reason) can succeed instead of being refused the same way.
|
|
11293
|
-
const draft = new URLSearchParams(body);
|
|
11450
|
+
const draft = new URLSearchParams(body.sent);
|
|
11294
11451
|
if (saved.status === 409)
|
|
11295
11452
|
draft.set("sawDigest", store.getScope(taskId)?.digest ?? "");
|
|
11296
11453
|
return taskScreen(response, who, taskId, saved.message, saved.status, draft);
|
|
@@ -11298,6 +11455,7 @@ export function createDecisionServer(options) {
|
|
|
11298
11455
|
return redirect(response, taskHref(taskId));
|
|
11299
11456
|
}
|
|
11300
11457
|
case "approve": {
|
|
11458
|
+
const body = readForm(posted, CONSOLE_FORMS.taskApprove);
|
|
11301
11459
|
const requestedReturn = body.get("return");
|
|
11302
11460
|
const approvalBack = requestedReturn === "next"
|
|
11303
11461
|
? "/next"
|
|
@@ -11384,6 +11542,7 @@ export function createDecisionServer(options) {
|
|
|
11384
11542
|
return redirect(response, approvalBack);
|
|
11385
11543
|
}
|
|
11386
11544
|
case "accept-proof": {
|
|
11545
|
+
const body = readForm(posted, CONSOLE_FORMS.taskAcceptProof);
|
|
11387
11546
|
// Accepting is a person's act, like approving a scope (Priority
|
|
11388
11547
|
// 2): a cookie session only, never a bearer credential.
|
|
11389
11548
|
if (who.via !== "cookie") {
|
|
@@ -11420,6 +11579,7 @@ export function createDecisionServer(options) {
|
|
|
11420
11579
|
return redirect(response, body.get("return") === null ? taskHref(taskId) : safeReturn(body.get("return")));
|
|
11421
11580
|
}
|
|
11422
11581
|
case "complete": {
|
|
11582
|
+
const body = readForm(posted, CONSOLE_FORMS.taskComplete);
|
|
11423
11583
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
11424
11584
|
return refuse(response, who, 403, "Only an approver can mark a result complete.");
|
|
11425
11585
|
const principal = matePrincipal(who);
|
|
@@ -11465,6 +11625,7 @@ export function createDecisionServer(options) {
|
|
|
11465
11625
|
return taskScreen(response, who, taskId, "Separate agent reviews have retired. Open the saved result to mark it complete or request a revision.", 410);
|
|
11466
11626
|
}
|
|
11467
11627
|
case "stop": {
|
|
11628
|
+
const body = readForm(posted, CONSOLE_FORMS.taskStop);
|
|
11468
11629
|
// The exact-run stop (v52): a browser session's act, an approver's
|
|
11469
11630
|
// act, naming ONE run. The store's transaction proves the run is
|
|
11470
11631
|
// this task's current live attempt and records who asked before
|
|
@@ -11496,9 +11657,11 @@ export function createDecisionServer(options) {
|
|
|
11496
11657
|
return redirect(response, back === "chat" ? `${taskChatHref(taskId)}#task-control` : `${taskHref(taskId)}#task-control`);
|
|
11497
11658
|
}
|
|
11498
11659
|
case "resume-arm": {
|
|
11660
|
+
const body = readForm(posted, CONSOLE_FORMS.taskResume);
|
|
11499
11661
|
return armTaskResume(response, who, taskId, (body.get("run") ?? "").trim(), body.get("return") === "chat" ? "chat" : "task", now);
|
|
11500
11662
|
}
|
|
11501
11663
|
case "resume": {
|
|
11664
|
+
const body = readForm(posted, CONSOLE_FORMS.taskResume);
|
|
11502
11665
|
// The resume itself (v52): password typed again + the durable nonce
|
|
11503
11666
|
// consumed INSIDE the store's resume transaction against the digest
|
|
11504
11667
|
// re-derived from live state. A replay finds the nonce spent; a
|