toolroll 0.9.40 → 0.9.42
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 +28 -0
- package/dist/cli.js +3 -1
- 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 +483 -0
- package/dist/contracts/console-api.js +273 -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/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-triggers.js +61 -60
- package/dist/mcp-connect.d.ts +0 -1
- package/dist/mcp-connect.js +41 -23
- package/dist/model-catalog.js +8 -6
- package/dist/observations.d.ts +2 -6
- package/dist/observations.js +13 -9
- package/dist/operate.js +2 -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/serve.js +142 -28
- package/dist/storage.d.ts +2 -1
- package/dist/storage.js +12 -9
- package/dist/store.d.ts +3 -15
- package/dist/store.js +80 -158
- package/dist/surface.d.ts +6 -54
- package/dist/surface.js +6 -1
- 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";
|
|
@@ -1412,7 +1413,7 @@ export function createDecisionServer(options) {
|
|
|
1412
1413
|
if (setupAttemptsLeft <= 0) {
|
|
1413
1414
|
return page(response, 403, signupPage("too many wrong codes — restart the server to get a fresh code", 0));
|
|
1414
1415
|
}
|
|
1415
|
-
const body = await form(request);
|
|
1416
|
+
const body = readForm(await form(request), CONSOLE_FORMS.signup);
|
|
1416
1417
|
const code = (body.get("code") ?? "").trim();
|
|
1417
1418
|
const name = (body.get("name") ?? "").trim();
|
|
1418
1419
|
const password = body.get("password") ?? "";
|
|
@@ -1451,7 +1452,7 @@ export function createDecisionServer(options) {
|
|
|
1451
1452
|
return redirect(response, "/");
|
|
1452
1453
|
}
|
|
1453
1454
|
if (url.pathname === "/login" && method === "POST") {
|
|
1454
|
-
const body = await form(request);
|
|
1455
|
+
const body = readForm(await form(request), CONSOLE_FORMS.login);
|
|
1455
1456
|
const name = body.get("name");
|
|
1456
1457
|
const token = body.get("token");
|
|
1457
1458
|
// The destination a deep link asked for rides the form as a same-site
|
|
@@ -1528,7 +1529,7 @@ export function createDecisionServer(options) {
|
|
|
1528
1529
|
}
|
|
1529
1530
|
let body;
|
|
1530
1531
|
try {
|
|
1531
|
-
body = await form(request);
|
|
1532
|
+
body = readForm(await form(request), CONSOLE_FORMS.join);
|
|
1532
1533
|
}
|
|
1533
1534
|
catch {
|
|
1534
1535
|
return respond(response, 413, "text/plain; charset=utf-8", "body too large");
|
|
@@ -1607,7 +1608,7 @@ export function createDecisionServer(options) {
|
|
|
1607
1608
|
return freshIdentitySignIn.run({ actor: requestFacts.sso?.fresh === true ? who.name : null }, () => requestContext.run(requestFacts, async () => {
|
|
1608
1609
|
const taskTextForm = url.pathname === "/tasks/add" || /^\/t\/[^/]+\/scope$/.test(url.pathname);
|
|
1609
1610
|
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);
|
|
1611
|
+
const target = actionTarget(url, who, request, body === null ? null : readForm(body, CONSOLE_FORMS.ledgerTarget));
|
|
1611
1612
|
const execute = async () => {
|
|
1612
1613
|
if (!projectRequestAllowed(url, who, request, response))
|
|
1613
1614
|
return;
|
|
@@ -2633,7 +2634,7 @@ export function createDecisionServer(options) {
|
|
|
2633
2634
|
latest: operator && latest !== null ? latest : null, csrf: operator ? who.session.csrf : null, today: now.toISOString().slice(0, 10) };
|
|
2634
2635
|
const rows = store.actionLedger({ ...query, ...(before === null ? {} : { before: Number(before) }), limit: 51 });
|
|
2635
2636
|
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 }));
|
|
2637
|
+
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
2638
|
return sendScreen(response, 200, screen("Action ledger", ledgerBody(rows, projects, url.searchParams, chainView), { chrome: chromeFor(chosen === "" ? null : chosen, "ledger", undefined, chosen === "" ? "all" : "project") }));
|
|
2638
2639
|
}
|
|
2639
2640
|
if (url.pathname === "/activity") {
|
|
@@ -3010,7 +3011,7 @@ export function createDecisionServer(options) {
|
|
|
3010
3011
|
if (url.searchParams.get("format") === "json") {
|
|
3011
3012
|
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
3013
|
"cache-control": "no-store", "x-content-type-options": "nosniff" });
|
|
3013
|
-
return void response.end(JSON.stringify(pack, null, 2));
|
|
3014
|
+
return void response.end(JSON.stringify(checkResponse("evidencePack", pack), null, 2));
|
|
3014
3015
|
}
|
|
3015
3016
|
return sendScreen(response, 200, screen(`Evidence pack · ${family.root.id}`, evidencePackHtml(pack), { chrome: chromeFor(family.root.repo, "tasks") }));
|
|
3016
3017
|
}
|
|
@@ -3358,7 +3359,7 @@ export function createDecisionServer(options) {
|
|
|
3358
3359
|
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
3360
|
sortReady: keyStatus("openrouter", providerHome).set, toolHome });
|
|
3360
3361
|
if (url.searchParams.get("format") === "json")
|
|
3361
|
-
return respond(response, 200, "application/json; charset=utf-8", JSON.stringify(view));
|
|
3362
|
+
return respond(response, 200, "application/json; charset=utf-8", JSON.stringify(checkResponse("flowView", view)));
|
|
3362
3363
|
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
3364
|
}
|
|
3364
3365
|
if (url.pathname === "/recipes" || url.pathname.startsWith("/recipes/")) {
|
|
@@ -6611,7 +6612,7 @@ export function createDecisionServer(options) {
|
|
|
6611
6612
|
}
|
|
6612
6613
|
return respond(response, 200, "application/json; charset=utf-8", JSON.stringify({ ok: true, beaten }));
|
|
6613
6614
|
}
|
|
6614
|
-
async function handlePost(url, who, request, response,
|
|
6615
|
+
async function handlePost(url, who, request, response, posted) {
|
|
6615
6616
|
// The attended beat answers BEFORE the shared mutation guard (v28): it
|
|
6616
6617
|
// carries no parameters, so there is no csrf token to check — its OWN
|
|
6617
6618
|
// guard is complete and STRICTER for the browsers this console
|
|
@@ -6623,12 +6624,13 @@ export function createDecisionServer(options) {
|
|
|
6623
6624
|
if (url.pathname === "/session/attended-beats") {
|
|
6624
6625
|
return attendedBeats(who, request, response, clock());
|
|
6625
6626
|
}
|
|
6626
|
-
const denied = authorizeMutation(request, who,
|
|
6627
|
+
const denied = authorizeMutation(request, who, readForm(posted, CONSOLE_FORMS.mutationGuard));
|
|
6627
6628
|
if (denied !== null)
|
|
6628
6629
|
return refuse(response, who, denied.status, denied.message);
|
|
6629
6630
|
// The scripted demo lead's acts: ask, approve, change a plan, complete or send back a result.
|
|
6630
6631
|
const demoAct = /^\/chat\/demo\/(?:ask|([0-9]{1,9})\/(approve|change|revise|complete))$/.exec(url.pathname);
|
|
6631
6632
|
if (demoAct !== null) {
|
|
6633
|
+
const body = readForm(posted, CONSOLE_FORMS.demo);
|
|
6632
6634
|
const lead = demoLeadHere();
|
|
6633
6635
|
if (lead === null || who.via !== "cookie")
|
|
6634
6636
|
return refuse(response, who, 404, "There's no page at this address.", "/chat");
|
|
@@ -6677,6 +6679,7 @@ export function createDecisionServer(options) {
|
|
|
6677
6679
|
}
|
|
6678
6680
|
// The update notice is dismissed per browser and per version: a newer release shows it again.
|
|
6679
6681
|
if (url.pathname === "/settings/updates/dismiss") {
|
|
6682
|
+
const body = readForm(posted, CONSOLE_FORMS.updatesDismiss);
|
|
6680
6683
|
const version = body.get("version") ?? "";
|
|
6681
6684
|
if (!/^[0-9A-Za-z.-]{1,60}$/.test(version))
|
|
6682
6685
|
return refuse(response, who, 400, "Choose a version to dismiss.", "/settings#updates");
|
|
@@ -6690,6 +6693,7 @@ export function createDecisionServer(options) {
|
|
|
6690
6693
|
}
|
|
6691
6694
|
// The daily check's switch: an installation setting, so an operator's. (Its own address: POST /settings/updates starts an update.)
|
|
6692
6695
|
if (url.pathname === "/settings/updates/checks") {
|
|
6696
|
+
const body = readForm(posted, CONSOLE_FORMS.updatesChecks);
|
|
6693
6697
|
if (options.configDir === undefined)
|
|
6694
6698
|
return refuse(response, who, 404, "Updates are not set up on this console.", "/settings");
|
|
6695
6699
|
if (!store.isInstanceOperator(who.name))
|
|
@@ -6699,6 +6703,7 @@ export function createDecisionServer(options) {
|
|
|
6699
6703
|
return redirect(response, `/settings?said=${encodeURIComponent(on ? "Toolroll looks for a newer version once a day." : "Update checks are off.")}#updates`);
|
|
6700
6704
|
}
|
|
6701
6705
|
if (url.pathname === '/code' || url.pathname.startsWith('/code/')) {
|
|
6706
|
+
const body = readForm(posted, CONSOLE_FORMS.code);
|
|
6702
6707
|
const wantsJson = request.headers.accept?.includes('application/json') === true;
|
|
6703
6708
|
const fail = (status, message, delivery = 'rejected', sessionId) => wantsJson
|
|
6704
6709
|
? respond(response, status, 'application/json', JSON.stringify({ ok: false, error: message, delivery, ...(sessionId ? { sessionId } : {}) }))
|
|
@@ -6707,7 +6712,7 @@ export function createDecisionServer(options) {
|
|
|
6707
6712
|
return fail(403, 'Coding sessions require installation-wide operator access. You can manage project tasks in Work.');
|
|
6708
6713
|
if (!coding)
|
|
6709
6714
|
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))
|
|
6715
|
+
if ([...new Set(body.keys())].some(key => body.sent.getAll(key).length !== 1))
|
|
6711
6716
|
return fail(400, 'Submit one value for each field.');
|
|
6712
6717
|
const actor = { name: who.name, generation: who.session.generation };
|
|
6713
6718
|
const permitted = (repo) => visible(repo) && codingProjectAllowed(repo);
|
|
@@ -6779,7 +6784,7 @@ export function createDecisionServer(options) {
|
|
|
6779
6784
|
if (ship && !wantsJson) {
|
|
6780
6785
|
try {
|
|
6781
6786
|
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') }));
|
|
6787
|
+
return sendScreen(response, 409, screen('Review for shipping', codingShippingHtml(preview, who.session.csrf, body.sent, message), { chrome: chromeFor(preview.repo, 'code') }));
|
|
6783
6788
|
}
|
|
6784
6789
|
catch { }
|
|
6785
6790
|
}
|
|
@@ -6789,7 +6794,7 @@ export function createDecisionServer(options) {
|
|
|
6789
6794
|
if (!projectRequestAllowed(url, who, request, response))
|
|
6790
6795
|
return;
|
|
6791
6796
|
if (restricted()) {
|
|
6792
|
-
const repos =
|
|
6797
|
+
const repos = readForm(posted, CONSOLE_FORMS.projectCeiling).getAll("repo");
|
|
6793
6798
|
if (repos.length > 1 || repos.some(repo => repo.trim() !== "" && !visible(repo.trim())))
|
|
6794
6799
|
return refuse(response, who, 403, "That project is outside your access.", "/projects");
|
|
6795
6800
|
}
|
|
@@ -6809,6 +6814,7 @@ export function createDecisionServer(options) {
|
|
|
6809
6814
|
// re-counts within five seconds either way, this just makes it exact.
|
|
6810
6815
|
bustBadge();
|
|
6811
6816
|
if (url.pathname === "/settings/skills/revise") {
|
|
6817
|
+
const body = readForm(posted, CONSOLE_FORMS.skillsRevise);
|
|
6812
6818
|
if (who.via !== 'cookie' || who.role !== 'approver')
|
|
6813
6819
|
return refuse(response, who, 403, 'Sign in to revise a test.', '/settings/skills');
|
|
6814
6820
|
try {
|
|
@@ -6828,12 +6834,13 @@ export function createDecisionServer(options) {
|
|
|
6828
6834
|
}
|
|
6829
6835
|
}
|
|
6830
6836
|
if (url.pathname === "/settings/skills/import" || url.pathname === "/settings/skills/change") {
|
|
6837
|
+
const body = readForm(posted, CONSOLE_FORMS.skillsChange);
|
|
6831
6838
|
if (who.via !== 'cookie' || who.role !== 'approver')
|
|
6832
6839
|
return refuse(response, who, 403, 'Sign in as an approver to manage skills.', '/settings/skills');
|
|
6833
6840
|
const repo = body.get('repo') ?? '', back = `/settings/skills?repo=${encodeURIComponent(repo)}`;
|
|
6834
6841
|
if (!visible(repo) || !consoleProjects().includes(repo))
|
|
6835
6842
|
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))
|
|
6843
|
+
if ([...new Set(body.keys())].some(k => body.sent.getAll(k).length !== 1))
|
|
6837
6844
|
return refuse(response, who, 400, 'Invalid skills form.', back);
|
|
6838
6845
|
try {
|
|
6839
6846
|
const view = skillsView(store, repo, who.name);
|
|
@@ -6883,6 +6890,7 @@ export function createDecisionServer(options) {
|
|
|
6883
6890
|
// setting a starter kit up, trying its sample card, and bringing GitHub issues in.
|
|
6884
6891
|
const kitPost = /^\/kits\/([a-z-]{1,40})\/(setup|sample|github)$/.exec(url.pathname);
|
|
6885
6892
|
if (kitPost !== null) {
|
|
6893
|
+
const body = readForm(posted, CONSOLE_FORMS.kit);
|
|
6886
6894
|
const now = clock();
|
|
6887
6895
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
6888
6896
|
return refuse(response, who, 403, "Sign in as an approver to set kits up.", "/kits");
|
|
@@ -6914,6 +6922,7 @@ export function createDecisionServer(options) {
|
|
|
6914
6922
|
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
6923
|
const questionPost = /^\/teammates\/questions\/([1-9][0-9]{0,9})\/answer$/.exec(url.pathname);
|
|
6916
6924
|
if (teammatePost !== null || questionPost !== null) {
|
|
6925
|
+
const body = readForm(posted, CONSOLE_FORMS.teammates);
|
|
6917
6926
|
const now = clock();
|
|
6918
6927
|
const wantsJson = (request.headers.accept ?? "").includes("application/json");
|
|
6919
6928
|
if (who.via !== "cookie")
|
|
@@ -6989,7 +6998,7 @@ export function createDecisionServer(options) {
|
|
|
6989
6998
|
const grant = store.teammateGrant(mate.id, tool);
|
|
6990
6999
|
const changed = op === "grant" ? await grantTool(store, mate, tool, who.name, now, { toolHome })
|
|
6991
7000
|
: 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);
|
|
7001
|
+
: 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
7002
|
return redirect(response, `${back}?${changed.ok ? "said" : "problem"}=${encodeURIComponent(changed.said)}#tools`);
|
|
6994
7003
|
}
|
|
6995
7004
|
const done = action === "state" ? setTeammateState(store, mate, body.get("state") === "removed" ? "removed" : body.get("state") === "paused" ? "paused" : "active", who.name, now)
|
|
@@ -7003,6 +7012,7 @@ export function createDecisionServer(options) {
|
|
|
7003
7012
|
// Flows → New → a template: preview these answers, or create exactly what was previewed.
|
|
7004
7013
|
const galleryPost = /^\/flows\/new\/([a-z-]{1,40})$/.exec(url.pathname);
|
|
7005
7014
|
if (galleryPost !== null) {
|
|
7015
|
+
const body = readForm(posted, CONSOLE_FORMS.galleryUse);
|
|
7006
7016
|
const template = galleryTemplateOf(galleryPost[1]);
|
|
7007
7017
|
const projects = consoleProjects();
|
|
7008
7018
|
if (template === null)
|
|
@@ -7034,6 +7044,7 @@ export function createDecisionServer(options) {
|
|
|
7034
7044
|
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
7045
|
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
7046
|
if (url.pathname === "/flows/new" || url.pathname === "/flows/example" || url.pathname === "/flows/import" || flowPost !== null || triggerPost !== null) {
|
|
7047
|
+
const body = readForm(posted, CONSOLE_FORMS.flows);
|
|
7037
7048
|
const now = clock();
|
|
7038
7049
|
const answer = (status, payload) => respond(response, status, "application/json; charset=utf-8", JSON.stringify(payload));
|
|
7039
7050
|
const projects = consoleProjects();
|
|
@@ -7305,6 +7316,7 @@ export function createDecisionServer(options) {
|
|
|
7305
7316
|
// v103: an instance operator records a checkpoint of the ledger chain (its head, to copy off the machine).
|
|
7306
7317
|
// v105: set or remove a monthly budget. An instance operator, with a step-up; the ledger keeps before → after.
|
|
7307
7318
|
if (url.pathname === "/spend/budget") {
|
|
7319
|
+
const body = readForm(posted, CONSOLE_FORMS.spendBudget);
|
|
7308
7320
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
|
|
7309
7321
|
return refuse(response, who, 403, "An instance operator sets budgets.", "/spend");
|
|
7310
7322
|
const back = (key, words) => redirect(response, `/spend?${key}=${encodeURIComponent(words)}`);
|
|
@@ -7336,6 +7348,7 @@ export function createDecisionServer(options) {
|
|
|
7336
7348
|
// Delete a project, in two steps: its name, then what goes and the password. An instance operator; the ledger keeps who and what.
|
|
7337
7349
|
// Settings → Project → Builds at once: an approver for the project sets it; the ledger keeps before → after.
|
|
7338
7350
|
if (url.pathname === "/settings/project/concurrency") {
|
|
7351
|
+
const body = readForm(posted, CONSOLE_FORMS.projectConcurrency);
|
|
7339
7352
|
const repo = body.get("repo") ?? "";
|
|
7340
7353
|
const known = consoleProjects();
|
|
7341
7354
|
if (!known.includes(repo))
|
|
@@ -7355,6 +7368,7 @@ export function createDecisionServer(options) {
|
|
|
7355
7368
|
return back("said", changed.before === changed.after ? "Nothing changed." : `Saved. Up to ${n} at once.`);
|
|
7356
7369
|
}
|
|
7357
7370
|
if (url.pathname === "/settings/project/delete") {
|
|
7371
|
+
const body = readForm(posted, CONSOLE_FORMS.projectDelete);
|
|
7358
7372
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
|
|
7359
7373
|
return refuse(response, who, 403, "An instance operator deletes projects.", "/settings/project");
|
|
7360
7374
|
const repo = body.get("repo") ?? "";
|
|
@@ -7395,6 +7409,7 @@ export function createDecisionServer(options) {
|
|
|
7395
7409
|
}
|
|
7396
7410
|
// Sprint 8: an instance operator saves the organisation policy, with a step-up; the ledger keeps each rule's before → after.
|
|
7397
7411
|
if (url.pathname === "/settings/policy") {
|
|
7412
|
+
const body = readForm(posted, CONSOLE_FORMS.policy);
|
|
7398
7413
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
|
|
7399
7414
|
return refuse(response, who, 403, "An instance operator sets the policy.", "/settings/policy");
|
|
7400
7415
|
const back = (key, words) => redirect(response, `/settings/policy?${key}=${encodeURIComponent(words)}`);
|
|
@@ -7411,6 +7426,7 @@ export function createDecisionServer(options) {
|
|
|
7411
7426
|
}
|
|
7412
7427
|
// v102: an instance operator changes a project's approval rules, with a step-up; the ledger keeps before → after.
|
|
7413
7428
|
if (url.pathname === "/settings/approval") {
|
|
7429
|
+
const body = readForm(posted, CONSOLE_FORMS.approval);
|
|
7414
7430
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
|
|
7415
7431
|
return refuse(response, who, 403, "An instance operator sets approval rules.", "/settings/approval");
|
|
7416
7432
|
const repo = body.get("repo") ?? "";
|
|
@@ -7431,6 +7447,7 @@ export function createDecisionServer(options) {
|
|
|
7431
7447
|
return back("said", `Saved. ${rulesSummary(next)}`);
|
|
7432
7448
|
}
|
|
7433
7449
|
if (url.pathname === "/settings/sessions") {
|
|
7450
|
+
const body = readForm(posted, CONSOLE_FORMS.sessions);
|
|
7434
7451
|
if (who.via !== "cookie")
|
|
7435
7452
|
return refuse(response, who, 403, "Sign in in a browser to manage sessions and tokens.", "/settings");
|
|
7436
7453
|
const operator = store.isInstanceOperator(who.name);
|
|
@@ -7489,6 +7506,7 @@ export function createDecisionServer(options) {
|
|
|
7489
7506
|
return back("problem", "Choose an action.");
|
|
7490
7507
|
}
|
|
7491
7508
|
if (url.pathname === "/settings/sign-in") {
|
|
7509
|
+
const body = readForm(posted, CONSOLE_FORMS.signIn);
|
|
7492
7510
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name) || !options.configDir)
|
|
7493
7511
|
return refuse(response, who, 403, "An instance operator sets up sign-in.", "/settings");
|
|
7494
7512
|
const back = (key, words) => redirect(response, `/settings/sign-in?${key}=${encodeURIComponent(words)}`);
|
|
@@ -7526,6 +7544,7 @@ export function createDecisionServer(options) {
|
|
|
7526
7544
|
}
|
|
7527
7545
|
// Settings → Updates: start the `toolroll update` job. A step-up; the job writes the ledger entries.
|
|
7528
7546
|
if (url.pathname === "/settings/updates" || url.pathname === "/settings/updates/seen" || url.pathname === "/settings/updates/cancel") {
|
|
7547
|
+
const body = readForm(posted, CONSOLE_FORMS.updates);
|
|
7529
7548
|
const databaseFile = store.databaseFile();
|
|
7530
7549
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name) || databaseFile === null)
|
|
7531
7550
|
return refuse(response, who, 403, "An instance operator updates Toolroll.", "/settings");
|
|
@@ -7572,6 +7591,7 @@ export function createDecisionServer(options) {
|
|
|
7572
7591
|
}
|
|
7573
7592
|
// v105: change how long each kind of data is kept. A step-up; the ledger keeps before → after for each kind.
|
|
7574
7593
|
if (url.pathname === "/settings/retention") {
|
|
7594
|
+
const body = readForm(posted, CONSOLE_FORMS.retention);
|
|
7575
7595
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
|
|
7576
7596
|
return refuse(response, who, 403, "An instance operator sets retention.", "/settings");
|
|
7577
7597
|
const back = (key, words) => redirect(response, `/settings/retention?${key}=${encodeURIComponent(words)}`);
|
|
@@ -7591,6 +7611,7 @@ export function createDecisionServer(options) {
|
|
|
7591
7611
|
// Settings → Storage: when a finished task's clean checkout goes, cleaning up what the preview showed, and throwing
|
|
7592
7612
|
// away a checkout kept for its changes. Each takes the password; the ledger keeps every change and removal.
|
|
7593
7613
|
if (url.pathname === "/settings/storage" || url.pathname === "/settings/storage/clean" || url.pathname === "/settings/storage/discard") {
|
|
7614
|
+
const body = readForm(posted, CONSOLE_FORMS.storage);
|
|
7594
7615
|
const databaseFile = store.databaseFile();
|
|
7595
7616
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name) || databaseFile === null)
|
|
7596
7617
|
return refuse(response, who, 403, "An instance operator looks after storage.", "/settings");
|
|
@@ -7624,6 +7645,7 @@ export function createDecisionServer(options) {
|
|
|
7624
7645
|
// Pull requests for one project: turn on (the checks run again and must match what the page showed), merge
|
|
7625
7646
|
// settings, or turn off. Each takes the password and lands in the ledger.
|
|
7626
7647
|
if (url.pathname === "/settings/pull-requests") {
|
|
7648
|
+
const body = readForm(posted, CONSOLE_FORMS.pullRequests);
|
|
7627
7649
|
const repo = body.get("repo") ?? "";
|
|
7628
7650
|
const known = [...new Set([...managedRepos(), ...store.knownRepos(), ...store.listProjects().map(one => one.path)])];
|
|
7629
7651
|
if (who.via !== "cookie" || !known.includes(repo) || !visible(repo))
|
|
@@ -7661,6 +7683,7 @@ export function createDecisionServer(options) {
|
|
|
7661
7683
|
// Checks for one project: the level, or the quick command (approved like `verify set --quick`). Each takes
|
|
7662
7684
|
// the password and lands in the ledger.
|
|
7663
7685
|
if (url.pathname === "/settings/checks") {
|
|
7686
|
+
const body = readForm(posted, CONSOLE_FORMS.checks);
|
|
7664
7687
|
const repo = body.get("repo") ?? "";
|
|
7665
7688
|
const known = [...new Set([...managedRepos(), ...store.knownRepos(), ...store.listProjects().map(one => one.path)])];
|
|
7666
7689
|
if (who.via !== "cookie" || !known.includes(repo) || !visible(repo))
|
|
@@ -7722,6 +7745,7 @@ export function createDecisionServer(options) {
|
|
|
7722
7745
|
}
|
|
7723
7746
|
// Sprint 8: change the backup schedule. A step-up; the ledger keeps before → after.
|
|
7724
7747
|
if (url.pathname === "/settings/backups") {
|
|
7748
|
+
const body = readForm(posted, CONSOLE_FORMS.backups);
|
|
7725
7749
|
const databaseFile = store.databaseFile();
|
|
7726
7750
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name) || databaseFile === null)
|
|
7727
7751
|
return refuse(response, who, 403, "An instance operator looks after backups.", "/settings");
|
|
@@ -7748,6 +7772,7 @@ export function createDecisionServer(options) {
|
|
|
7748
7772
|
}
|
|
7749
7773
|
// v105: download everything as a .zip. An instance operator, with a step-up; the ledger records it.
|
|
7750
7774
|
if (url.pathname === "/settings/data") {
|
|
7775
|
+
const body = readForm(posted, CONSOLE_FORMS.data);
|
|
7751
7776
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
|
|
7752
7777
|
return refuse(response, who, 403, "An instance operator exports data.", "/settings");
|
|
7753
7778
|
if (!authenticateApprover(store, who.name, body.get("password") ?? "").ok) {
|
|
@@ -7762,6 +7787,7 @@ export function createDecisionServer(options) {
|
|
|
7762
7787
|
}
|
|
7763
7788
|
// Settings → Flows → Switch on: a starter flow's zones and trigger, made together.
|
|
7764
7789
|
if (url.pathname === "/settings/flows/on") {
|
|
7790
|
+
const body = readForm(posted, CONSOLE_FORMS.flowsOn);
|
|
7765
7791
|
const repo = body.get("repo") ?? "";
|
|
7766
7792
|
const back = (key, words) => redirect(response, `/settings/flows?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}`);
|
|
7767
7793
|
const known = [...new Set([...(admissionList() ?? []), ...managedRepos(), ...store.knownRepos()])];
|
|
@@ -7777,6 +7803,7 @@ export function createDecisionServer(options) {
|
|
|
7777
7803
|
}
|
|
7778
7804
|
// Settings → Integrations → Send test: one harmless read-only check of one integration, now.
|
|
7779
7805
|
if (url.pathname === "/settings/integrations/test") {
|
|
7806
|
+
const body = readForm(posted, CONSOLE_FORMS.integrationsTest);
|
|
7780
7807
|
if (who.via !== "cookie" || who.role !== "approver" || restricted() || !options.configDir)
|
|
7781
7808
|
return refuse(response, who, 403, "An installation approver tests integrations.", "/settings");
|
|
7782
7809
|
const key = body.get("key") ?? "";
|
|
@@ -7789,6 +7816,7 @@ export function createDecisionServer(options) {
|
|
|
7789
7816
|
}
|
|
7790
7817
|
// v104: change where monitoring goes. A step-up; the ledger keeps before → after (addresses, never secrets).
|
|
7791
7818
|
if (url.pathname === "/settings/monitoring") {
|
|
7819
|
+
const body = readForm(posted, CONSOLE_FORMS.monitoring);
|
|
7792
7820
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name) || !options.configDir)
|
|
7793
7821
|
return refuse(response, who, 403, "An instance operator sets up monitoring.", "/settings");
|
|
7794
7822
|
const back = (key, words) => redirect(response, `/settings/monitoring?${key}=${encodeURIComponent(words)}`);
|
|
@@ -7812,6 +7840,7 @@ export function createDecisionServer(options) {
|
|
|
7812
7840
|
return back("said", "Saved. Deliveries start within a few seconds.");
|
|
7813
7841
|
}
|
|
7814
7842
|
if (url.pathname === "/settings/tools/connect") {
|
|
7843
|
+
const body = readForm(posted, CONSOLE_FORMS.toolsConnect);
|
|
7815
7844
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
7816
7845
|
return refuse(response, who, 403, "Sign in as an approver to connect tools.", "/settings/tools");
|
|
7817
7846
|
const repo = body.get("repo") ?? "";
|
|
@@ -7887,6 +7916,7 @@ export function createDecisionServer(options) {
|
|
|
7887
7916
|
return goOutside(response, started.go, `Going to ${service.label} to sign in…`, { state: started.state, path: CONNECT_CALLBACK, secure: origin.startsWith("https:") });
|
|
7888
7917
|
}
|
|
7889
7918
|
if (url.pathname === "/settings/tools/change") {
|
|
7919
|
+
const body = readForm(posted, CONSOLE_FORMS.toolsChange);
|
|
7890
7920
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
7891
7921
|
return refuse(response, who, 403, "Sign in as an approver to manage tools.", "/settings/tools");
|
|
7892
7922
|
const repo = body.get("repo") ?? "";
|
|
@@ -7974,6 +8004,7 @@ export function createDecisionServer(options) {
|
|
|
7974
8004
|
}
|
|
7975
8005
|
}
|
|
7976
8006
|
if (url.pathname === "/settings/appearance") {
|
|
8007
|
+
const body = readForm(posted, CONSOLE_FORMS.appearance);
|
|
7977
8008
|
// A per-browser preference, not a shared setting: it lives in this
|
|
7978
8009
|
// person's own cookie and never in the database.
|
|
7979
8010
|
const accentField = body.get("accent");
|
|
@@ -8002,6 +8033,7 @@ export function createDecisionServer(options) {
|
|
|
8002
8033
|
return redirect(response, back);
|
|
8003
8034
|
}
|
|
8004
8035
|
if (url.pathname.startsWith("/settings/models/")) {
|
|
8036
|
+
const body = readForm(posted, CONSOLE_FORMS.models);
|
|
8005
8037
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
8006
8038
|
return refuse(response, who, 403, "Sign in as an approver to change models.", "/settings/models");
|
|
8007
8039
|
const back = (said, bad = false) => redirect(response, `/settings/models?${bad ? "problem" : "said"}=${encodeURIComponent(said)}`);
|
|
@@ -8056,6 +8088,7 @@ export function createDecisionServer(options) {
|
|
|
8056
8088
|
return refuse(response, who, 404, "No such models action.", "/settings/models");
|
|
8057
8089
|
}
|
|
8058
8090
|
if (url.pathname === "/settings/knowledge/refresh") {
|
|
8091
|
+
const body = readForm(posted, CONSOLE_FORMS.knowledgeRefresh);
|
|
8059
8092
|
if (who.via !== 'cookie' || who.role !== 'approver')
|
|
8060
8093
|
return refuse(response, who, 403, 'Sign in as an approver to refresh project context.', '/settings/knowledge');
|
|
8061
8094
|
const repo = body.get('repo') ?? '';
|
|
@@ -8065,6 +8098,7 @@ export function createDecisionServer(options) {
|
|
|
8065
8098
|
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
8099
|
}
|
|
8067
8100
|
if (url.pathname === "/settings/knowledge/proposal") {
|
|
8101
|
+
const body = readForm(posted, CONSOLE_FORMS.knowledgeProposal);
|
|
8068
8102
|
if (who.via !== 'cookie' || who.role !== 'approver')
|
|
8069
8103
|
return refuse(response, who, 403, 'Sign in as an approver to decide memory proposals.', '/settings/knowledge');
|
|
8070
8104
|
const repo = body.get('repo') ?? '', decision = body.get('decision') ?? '';
|
|
@@ -8081,6 +8115,7 @@ export function createDecisionServer(options) {
|
|
|
8081
8115
|
return redirect(response, `/settings/knowledge?repo=${encodeURIComponent(repo)}&saved=1`);
|
|
8082
8116
|
}
|
|
8083
8117
|
if (url.pathname === "/settings/knowledge/decision") {
|
|
8118
|
+
const body = readForm(posted, CONSOLE_FORMS.knowledgeDecision);
|
|
8084
8119
|
if (who.via !== 'cookie' || who.role !== 'approver')
|
|
8085
8120
|
return refuse(response, who, 403, 'Sign in as an approver to change project decisions.', '/settings/knowledge');
|
|
8086
8121
|
const repo = body.get('repo') ?? '', action = body.get('action') ?? '';
|
|
@@ -8108,6 +8143,7 @@ export function createDecisionServer(options) {
|
|
|
8108
8143
|
return redirect(response, `/settings/knowledge?repo=${encodeURIComponent(repo)}&saved=1`);
|
|
8109
8144
|
}
|
|
8110
8145
|
if (url.pathname === "/settings/knowledge/change") {
|
|
8146
|
+
const body = readForm(posted, CONSOLE_FORMS.knowledgeChange);
|
|
8111
8147
|
if (who.via !== 'cookie' || who.role !== 'approver')
|
|
8112
8148
|
return refuse(response, who, 403, 'Sign in as an approver to change project knowledge.', '/settings/knowledge');
|
|
8113
8149
|
const repo = body.get('repo') ?? '', action = body.get('action') ?? '';
|
|
@@ -8133,6 +8169,7 @@ export function createDecisionServer(options) {
|
|
|
8133
8169
|
return redirect(response, `/settings/knowledge?repo=${encodeURIComponent(repo)}&saved=1`);
|
|
8134
8170
|
}
|
|
8135
8171
|
if (url.pathname === "/settings/learning/change") {
|
|
8172
|
+
const body = readForm(posted, CONSOLE_FORMS.learningChange);
|
|
8136
8173
|
if (who.via !== "cookie")
|
|
8137
8174
|
return refuse(response, who, 403, "Sign in to change learning.", "/settings/learning");
|
|
8138
8175
|
const repo = body.get("repo") ?? "", action = body.get("action") ?? "";
|
|
@@ -8149,6 +8186,7 @@ export function createDecisionServer(options) {
|
|
|
8149
8186
|
return redirect(response, `/settings/learning?repo=${encodeURIComponent(repo)}`);
|
|
8150
8187
|
}
|
|
8151
8188
|
if (["/control/setup-preview", "/control/setup-approve", "/control/instructions-preview", "/control/instructions-approve"].includes(url.pathname)) {
|
|
8189
|
+
const body = readForm(posted, CONSOLE_FORMS.projectSetup);
|
|
8152
8190
|
const project = projectOf(who, request);
|
|
8153
8191
|
if (project == null || body.get("repo") !== project || !visible(project))
|
|
8154
8192
|
return refuse(response, who, 409, "The selected project changed. Open project setup again.", "/control");
|
|
@@ -8188,6 +8226,7 @@ export function createDecisionServer(options) {
|
|
|
8188
8226
|
return redirect(response, "/control");
|
|
8189
8227
|
}
|
|
8190
8228
|
if (["connect", "pair", "unpair", "disconnect", "alerts"].some(action => url.pathname === `/settings/slack/${action}`)) {
|
|
8229
|
+
const body = readForm(posted, CONSOLE_FORMS.slack);
|
|
8191
8230
|
if (who.via !== "cookie" || who.role !== "approver" || restricted() || !options.configDir)
|
|
8192
8231
|
return refuse(response, who, 403, "An installation approver can connect Slack.", "/settings");
|
|
8193
8232
|
const dir = options.configDir, state = new SlackState(store), action = url.pathname.split("/").at(-1);
|
|
@@ -8249,6 +8288,7 @@ export function createDecisionServer(options) {
|
|
|
8249
8288
|
return redirect(response, "/settings/telegram");
|
|
8250
8289
|
}
|
|
8251
8290
|
if (url.pathname === "/settings/telegram/pair" || url.pathname === "/settings/telegram/unpair") {
|
|
8291
|
+
const body = readForm(posted, CONSOLE_FORMS.telegramPair);
|
|
8252
8292
|
// The person's own pairing, under their password: a code minted for
|
|
8253
8293
|
// them alone, or their own chats revoked. Teammates' pairings are
|
|
8254
8294
|
// never touched from here.
|
|
@@ -8274,6 +8314,7 @@ export function createDecisionServer(options) {
|
|
|
8274
8314
|
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
8315
|
}
|
|
8276
8316
|
if (["connect", "pair", "unpair", "disconnect", "alerts"].some(action => url.pathname === `/settings/teams/${action}`)) {
|
|
8317
|
+
const body = readForm(posted, CONSOLE_FORMS.teams);
|
|
8277
8318
|
if (who.via !== "cookie" || who.role !== "approver" || restricted() || !options.configDir)
|
|
8278
8319
|
return refuse(response, who, 403, "An installation approver can connect Teams.", "/settings");
|
|
8279
8320
|
const dir = options.configDir, state = new ChatState(store, "teams"), action = url.pathname.split("/").at(-1);
|
|
@@ -8328,6 +8369,7 @@ export function createDecisionServer(options) {
|
|
|
8328
8369
|
return redirect(response, "/settings/teams");
|
|
8329
8370
|
}
|
|
8330
8371
|
if (["connect", "pair", "unpair", "disconnect", "alerts"].some(action => url.pathname === `/settings/discord/${action}`)) {
|
|
8372
|
+
const body = readForm(posted, CONSOLE_FORMS.discord);
|
|
8331
8373
|
if (who.via !== "cookie" || who.role !== "approver" || restricted() || !options.configDir)
|
|
8332
8374
|
return refuse(response, who, 403, "An installation approver can connect Discord.", "/settings");
|
|
8333
8375
|
const dir = options.configDir, state = new ChatState(store, "discord"), action = url.pathname.split("/").at(-1);
|
|
@@ -8382,6 +8424,7 @@ export function createDecisionServer(options) {
|
|
|
8382
8424
|
return redirect(response, "/settings/discord");
|
|
8383
8425
|
}
|
|
8384
8426
|
if (url.pathname === "/settings/messaging" && options.configDir !== undefined && options.telegramTokenFile !== undefined) {
|
|
8427
|
+
const body = readForm(posted, CONSOLE_FORMS.messaging);
|
|
8385
8428
|
const wanted = (body.get("primary") ?? "").trim();
|
|
8386
8429
|
const facts = effectivePrimary(process.env, options.configDir, loadBotToken(process.env, options.telegramTokenFile) !== null);
|
|
8387
8430
|
// Only a CONFIGURED service may page — choosing silence is not a
|
|
@@ -8393,6 +8436,7 @@ export function createDecisionServer(options) {
|
|
|
8393
8436
|
return redirect(response, "/settings");
|
|
8394
8437
|
}
|
|
8395
8438
|
if (url.pathname === "/settings/permission-default" && options.telegramTokenFile !== undefined) {
|
|
8439
|
+
const body = readForm(posted, CONSOLE_FORMS.permissionDefault);
|
|
8396
8440
|
const wanted = body.get("permission-mode");
|
|
8397
8441
|
if (wanted !== "auto" && wanted !== "bypassPermissions") {
|
|
8398
8442
|
return refuse(response, who, 400, "permissions must be Auto or Full access", "/settings");
|
|
@@ -8403,6 +8447,7 @@ export function createDecisionServer(options) {
|
|
|
8403
8447
|
: "New tasks now start with Auto. Approved tasks keep their setting.")}`);
|
|
8404
8448
|
}
|
|
8405
8449
|
if (url.pathname === "/settings/quality-default" && options.telegramTokenFile !== undefined) {
|
|
8450
|
+
const body = readForm(posted, CONSOLE_FORMS.qualityDefault);
|
|
8406
8451
|
const wanted = body.get("quality-mode");
|
|
8407
8452
|
if (!isQualityMode(wanted)) {
|
|
8408
8453
|
return refuse(response, who, 400, "quality must be Default or Strict / release", "/settings");
|
|
@@ -8413,6 +8458,7 @@ export function createDecisionServer(options) {
|
|
|
8413
8458
|
: "New tasks now use Default quality. Approved tasks keep their setting.")}`);
|
|
8414
8459
|
}
|
|
8415
8460
|
if (url.pathname === "/settings/notifications") {
|
|
8461
|
+
const body = readForm(posted, CONSOLE_FORMS.notifications);
|
|
8416
8462
|
// Each person's own choice: a closed list of modes and a 24-hour HH:MM, or off.
|
|
8417
8463
|
const mode = body.get("mode"), time = (body.get("digest") ?? "").trim(), shots = body.get("screenshots");
|
|
8418
8464
|
if (mode !== null && mode !== "quiet" && mode !== "all")
|
|
@@ -8432,6 +8478,7 @@ export function createDecisionServer(options) {
|
|
|
8432
8478
|
return redirect(response, `/settings?said=${encodeURIComponent(said)}`);
|
|
8433
8479
|
}
|
|
8434
8480
|
if (url.pathname === "/settings/notifications/mute") {
|
|
8481
|
+
const body = readForm(posted, CONSOLE_FORMS.notificationsMute);
|
|
8435
8482
|
// Each person's own pings: a muted project stays in Tasks and the evening digest.
|
|
8436
8483
|
const repo = body.get("repo") ?? "";
|
|
8437
8484
|
const project = notificationProjects(store, who.name).find(one => one.repo === repo);
|
|
@@ -8442,6 +8489,7 @@ export function createDecisionServer(options) {
|
|
|
8442
8489
|
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
8490
|
}
|
|
8444
8491
|
if (url.pathname === "/settings/telegram-digest" && options.telegramTokenFile !== undefined) {
|
|
8492
|
+
const body = readForm(posted, CONSOLE_FORMS.telegramDigest);
|
|
8445
8493
|
// The cadence is a closed list of minutes — never a free number from
|
|
8446
8494
|
// a form; "off" clears it. Any approver session may set it.
|
|
8447
8495
|
const wanted = (body.get("every") ?? "").trim();
|
|
@@ -8453,6 +8501,7 @@ export function createDecisionServer(options) {
|
|
|
8453
8501
|
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
8502
|
}
|
|
8455
8503
|
if (url.pathname === "/settings/provider-key" || url.pathname === "/settings/provider-key-clear") {
|
|
8504
|
+
const body = readForm(posted, CONSOLE_FORMS.providerKey);
|
|
8456
8505
|
// The central gate already required an ACTIVE approver; the value is
|
|
8457
8506
|
// write-only from here — status pages say set/not-set, never bytes.
|
|
8458
8507
|
const provider = body.get("provider") ?? "";
|
|
@@ -8505,6 +8554,7 @@ export function createDecisionServer(options) {
|
|
|
8505
8554
|
return redirect(response, `/settings?said=${encodeURIComponent((verdict.ok ? `${stored} and verified — it works` : `${stored}. ${verdictWords(provider, verdict)}`) + modeNote)}`);
|
|
8506
8555
|
}
|
|
8507
8556
|
if (url.pathname === "/settings/telegram-token" && options.telegramTokenFile !== undefined) {
|
|
8557
|
+
const body = readForm(posted, CONSOLE_FORMS.telegramToken);
|
|
8508
8558
|
const value = body.get("token") ?? "";
|
|
8509
8559
|
const saved = saveBotToken(options.telegramTokenFile, value);
|
|
8510
8560
|
if (!saved.ok) {
|
|
@@ -8520,6 +8570,7 @@ export function createDecisionServer(options) {
|
|
|
8520
8570
|
}
|
|
8521
8571
|
// v87: the mail server Send email steps use, and a test email to its own address.
|
|
8522
8572
|
if ((url.pathname === "/settings/email" || url.pathname === "/settings/email-test" || url.pathname === "/settings/email-read-test") && who.role === "approver" && options.configDir !== undefined) {
|
|
8573
|
+
const body = readForm(posted, CONSOLE_FORMS.email);
|
|
8523
8574
|
const said = (words) => redirect(response, `/settings?said=${encodeURIComponent(words)}#email`);
|
|
8524
8575
|
if (url.pathname === "/settings/email") {
|
|
8525
8576
|
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 +8593,7 @@ export function createDecisionServer(options) {
|
|
|
8542
8593
|
}
|
|
8543
8594
|
// v89: a Google account for email: save the OAuth client and go to Google's consent screen, or disconnect.
|
|
8544
8595
|
if ((url.pathname === "/settings/google" || url.pathname === "/settings/google/disconnect") && who.role === "approver" && options.configDir !== undefined) {
|
|
8596
|
+
const body = readForm(posted, CONSOLE_FORMS.google);
|
|
8545
8597
|
const said = (words) => redirect(response, `/settings?said=${encodeURIComponent(words)}#email`);
|
|
8546
8598
|
if (url.pathname === "/settings/google/disconnect") {
|
|
8547
8599
|
await disconnectGoogle(options.configDir, options.googleFetch ?? fetch);
|
|
@@ -8561,6 +8613,7 @@ export function createDecisionServer(options) {
|
|
|
8561
8613
|
return goOutside(response, consent.url, "Going to Google to sign in…", { state: consent.visit.state, path: GOOGLE_CALLBACK, secure: origin.startsWith("https:") });
|
|
8562
8614
|
}
|
|
8563
8615
|
if (url.pathname === "/projects/select") {
|
|
8616
|
+
const body = readForm(posted, CONSOLE_FORMS.projectsSelect);
|
|
8564
8617
|
// Session-only project switching (E2): reads the registry, writes
|
|
8565
8618
|
// ONLY session.project — the durable upsert lives in /projects/open,
|
|
8566
8619
|
// which stays an approver's act. This is the one project verb a
|
|
@@ -8579,6 +8632,7 @@ export function createDecisionServer(options) {
|
|
|
8579
8632
|
return redirect(response, safeReturn(body.get("return")));
|
|
8580
8633
|
}
|
|
8581
8634
|
if (url.pathname === "/projects/remove") {
|
|
8635
|
+
const body = readForm(posted, CONSOLE_FORMS.projectsRemove);
|
|
8582
8636
|
// The reversible remove: off every list and the builder; tasks, results and settings stay. Deleting is
|
|
8583
8637
|
// Settings → Project → Delete project. The same admission code as `repos remove` (project-admission.ts).
|
|
8584
8638
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
|
|
@@ -8598,6 +8652,7 @@ export function createDecisionServer(options) {
|
|
|
8598
8652
|
return redirect(response, `/settings/project?said=${encodeURIComponent(`Removed ${projectName(repo)}. Its tasks and results stay saved.`)}`);
|
|
8599
8653
|
}
|
|
8600
8654
|
if (url.pathname === "/projects/open") {
|
|
8655
|
+
const body = readForm(posted, CONSOLE_FORMS.projectsOpen);
|
|
8601
8656
|
// Sessions only: a bearer caller names its project per request and has
|
|
8602
8657
|
// no session to mutate — refusing here keeps that boundary legible.
|
|
8603
8658
|
if (who.via !== "cookie") {
|
|
@@ -8637,6 +8692,7 @@ export function createDecisionServer(options) {
|
|
|
8637
8692
|
return redirect(response, safeReturn(body.get("return")));
|
|
8638
8693
|
}
|
|
8639
8694
|
if (url.pathname === "/tasks/add") {
|
|
8695
|
+
const body = readForm(posted, CONSOLE_FORMS.tasksAdd);
|
|
8640
8696
|
const project = projectOf(who, request);
|
|
8641
8697
|
if (project === undefined) {
|
|
8642
8698
|
return refuse(response, who, 403, "that project is outside what this server was configured to show");
|
|
@@ -8734,7 +8790,7 @@ export function createDecisionServer(options) {
|
|
|
8734
8790
|
});
|
|
8735
8791
|
if (!made.ok) {
|
|
8736
8792
|
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()));
|
|
8793
|
+
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
8794
|
}
|
|
8739
8795
|
const actionContext = requestContext.getStore();
|
|
8740
8796
|
if (actionContext !== undefined)
|
|
@@ -8748,6 +8804,7 @@ export function createDecisionServer(options) {
|
|
|
8748
8804
|
return redirect(response, taskHref(made.id));
|
|
8749
8805
|
}
|
|
8750
8806
|
if (url.pathname === "/queue/move" || url.pathname === "/queue/note") {
|
|
8807
|
+
const body = readForm(posted, CONSOLE_FORMS.queue);
|
|
8751
8808
|
const project = projectOf(who, request);
|
|
8752
8809
|
if (project === undefined) {
|
|
8753
8810
|
return refuse(response, who, 403, "that project is outside what this server was configured to show");
|
|
@@ -8862,6 +8919,7 @@ export function createDecisionServer(options) {
|
|
|
8862
8919
|
// hash — the page that shows it renders no self-refreshing script, so
|
|
8863
8920
|
// the token is never re-rendered.
|
|
8864
8921
|
if (url.pathname === "/fleet/runner/register") {
|
|
8922
|
+
const body = readForm(posted, CONSOLE_FORMS.runnerRegister);
|
|
8865
8923
|
if (who.via !== "cookie")
|
|
8866
8924
|
return refuse(response, who, 403, "runner registration is a browser surface");
|
|
8867
8925
|
const token = body.get("token") ?? "";
|
|
@@ -8891,6 +8949,7 @@ export function createDecisionServer(options) {
|
|
|
8891
8949
|
return sendScreen(response, 200, tokenScreen);
|
|
8892
8950
|
}
|
|
8893
8951
|
if (url.pathname === "/mode/confirm" || url.pathname === "/mode/sign") {
|
|
8952
|
+
const body = readForm(posted, CONSOLE_FORMS.mode);
|
|
8894
8953
|
if (who.via !== "cookie")
|
|
8895
8954
|
return refuse(response, who, 403, "signing a mode is a browser ceremony");
|
|
8896
8955
|
const project = projectOf(who, request);
|
|
@@ -8991,6 +9050,7 @@ export function createDecisionServer(options) {
|
|
|
8991
9050
|
return redirect(response, ended ? "/mode?said=the%20mode%20is%20ended%20—%20every%20act%20falls%20back%20to%20its%20own%20ceremony" : "/mode");
|
|
8992
9051
|
}
|
|
8993
9052
|
if (url.pathname === "/people/projects") {
|
|
9053
|
+
const body = readForm(posted, CONSOLE_FORMS.peopleProjects);
|
|
8994
9054
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
|
|
8995
9055
|
return refuse(response, who, 403, "An instance operator manages project access.", "/people");
|
|
8996
9056
|
if (!authenticateApprover(store, who.name, body.get("token") ?? "", null).ok)
|
|
@@ -9005,6 +9065,7 @@ export function createDecisionServer(options) {
|
|
|
9005
9065
|
return redirect(response, "/people?said=Project%20access%20saved");
|
|
9006
9066
|
}
|
|
9007
9067
|
if (url.pathname === "/people/invite") {
|
|
9068
|
+
const body = readForm(posted, CONSOLE_FORMS.peopleInvite);
|
|
9008
9069
|
if (who.via !== "cookie")
|
|
9009
9070
|
return refuse(response, who, 403, "inviting is a browser surface");
|
|
9010
9071
|
const token = body.get("token") ?? "";
|
|
@@ -9035,6 +9096,7 @@ export function createDecisionServer(options) {
|
|
|
9035
9096
|
return sendScreen(response, 200, linkScreen);
|
|
9036
9097
|
}
|
|
9037
9098
|
if (url.pathname === "/people/invite-revoke") {
|
|
9099
|
+
const body = readForm(posted, CONSOLE_FORMS.peopleInviteRevoke);
|
|
9038
9100
|
if (who.via !== "cookie")
|
|
9039
9101
|
return refuse(response, who, 403, "inviting is a browser surface");
|
|
9040
9102
|
const token = body.get("token") ?? "";
|
|
@@ -9046,6 +9108,7 @@ export function createDecisionServer(options) {
|
|
|
9046
9108
|
return redirect(response, `/people?said=${encodeURIComponent(revoked ? "the invite is cancelled — its link is dead" : "that invite was already gone")}`);
|
|
9047
9109
|
}
|
|
9048
9110
|
if (url.pathname === "/people/revoke") {
|
|
9111
|
+
const body = readForm(posted, CONSOLE_FORMS.peopleRevoke);
|
|
9049
9112
|
if (who.via !== "cookie")
|
|
9050
9113
|
return refuse(response, who, 403, "removing a person is a browser surface");
|
|
9051
9114
|
const token = body.get("token") ?? "";
|
|
@@ -9065,6 +9128,7 @@ export function createDecisionServer(options) {
|
|
|
9065
9128
|
return redirect(response, `/people?said=${encodeURIComponent(`${name} can no longer sign in — their sessions, invites, and signed modes ended with them; history stays`)}`);
|
|
9066
9129
|
}
|
|
9067
9130
|
if (url.pathname === "/fleet/runner/retire") {
|
|
9131
|
+
const body = readForm(posted, CONSOLE_FORMS.runnerRetire);
|
|
9068
9132
|
if (who.via !== "cookie")
|
|
9069
9133
|
return refuse(response, who, 403, "runner retirement is a browser surface");
|
|
9070
9134
|
const token = body.get("token") ?? "";
|
|
@@ -9079,6 +9143,7 @@ export function createDecisionServer(options) {
|
|
|
9079
9143
|
}
|
|
9080
9144
|
const answer = /^\/d\/([0-9]{1,15})\/answer$/.exec(url.pathname);
|
|
9081
9145
|
if (answer !== null) {
|
|
9146
|
+
const body = readForm(posted, CONSOLE_FORMS.decisionAnswer);
|
|
9082
9147
|
const id = Number(answer[1]);
|
|
9083
9148
|
const requestedReturn = body.get("return");
|
|
9084
9149
|
const decisionBack = requestedReturn === "next"
|
|
@@ -9118,6 +9183,7 @@ export function createDecisionServer(options) {
|
|
|
9118
9183
|
}
|
|
9119
9184
|
const contestAct = /^\/contest\/([0-9]{1,15})\/(arm|pick|abandon)$/.exec(url.pathname);
|
|
9120
9185
|
if (contestAct !== null) {
|
|
9186
|
+
const body = readForm(posted, CONSOLE_FORMS.contest);
|
|
9121
9187
|
const contestId = Number(contestAct[1]);
|
|
9122
9188
|
const verb = contestAct[2];
|
|
9123
9189
|
const data = contestData(contestId);
|
|
@@ -9192,10 +9258,11 @@ export function createDecisionServer(options) {
|
|
|
9192
9258
|
}
|
|
9193
9259
|
const attendAct = matchTaskPath(url.pathname, "/(attend-preview|attend|attend-revoke)$");
|
|
9194
9260
|
if (attendAct !== null) {
|
|
9195
|
-
return attendMutation(response, who, attendAct.taskId, attendAct.verb,
|
|
9261
|
+
return attendMutation(response, who, attendAct.taskId, attendAct.verb, readForm(posted, CONSOLE_FORMS.attend), now);
|
|
9196
9262
|
}
|
|
9197
9263
|
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
9264
|
if (act !== null && act.verb === "confirm-stopped") {
|
|
9265
|
+
const body = readForm(posted, CONSOLE_FORMS.confirmStopped);
|
|
9199
9266
|
// Confirming a finished build stopped is an approver's act behind the password, typed again — the same
|
|
9200
9267
|
// record as `toolroll run settle`, refused while anything of the run may still be running.
|
|
9201
9268
|
const back = taskHref(act.taskId);
|
|
@@ -9227,6 +9294,7 @@ export function createDecisionServer(options) {
|
|
|
9227
9294
|
return redirect(response, body.get("return") ? to : back);
|
|
9228
9295
|
}
|
|
9229
9296
|
if (act !== null && act.verb === "merge") {
|
|
9297
|
+
const body = readForm(posted, CONSOLE_FORMS.merge);
|
|
9230
9298
|
// Merging is a person's act behind their password, typed again: a browser session, an approver, the exact
|
|
9231
9299
|
// result's pull request. The flow re-reads GitHub before merging and records who merged in the ledger.
|
|
9232
9300
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
@@ -9248,9 +9316,10 @@ export function createDecisionServer(options) {
|
|
|
9248
9316
|
return redirect(response, `${taskHref(act.taskId)}#merge`);
|
|
9249
9317
|
}
|
|
9250
9318
|
if (act !== null) {
|
|
9251
|
-
return taskMutation(response, who, act.taskId, act.verb,
|
|
9319
|
+
return taskMutation(response, who, act.taskId, act.verb, posted, now);
|
|
9252
9320
|
}
|
|
9253
9321
|
if (url.pathname === "/projects/onboard-preview" || url.pathname === "/projects/onboard-confirm") {
|
|
9322
|
+
const body = readForm(posted, CONSOLE_FORMS.onboard);
|
|
9254
9323
|
// Repo onboarding (findings 1-39): bearer and demo refuse BEFORE any
|
|
9255
9324
|
// gh call; the console flow exists only on root-configured serves.
|
|
9256
9325
|
if (who.via !== "cookie")
|
|
@@ -9375,6 +9444,7 @@ export function createDecisionServer(options) {
|
|
|
9375
9444
|
: `${admitted} is ready and open`, 200);
|
|
9376
9445
|
}
|
|
9377
9446
|
if (url.pathname === "/push/subscribe") {
|
|
9447
|
+
const body = readForm(posted, CONSOLE_FORMS.pushSubscribe);
|
|
9378
9448
|
// Enrolling a durable notification sink is a CEREMONY (arc 3 finding
|
|
9379
9449
|
// 6): browser session + CSRF + the password typed again; bearer
|
|
9380
9450
|
// identities are machines and are refused outright.
|
|
@@ -9414,6 +9484,7 @@ export function createDecisionServer(options) {
|
|
|
9414
9484
|
return redirect(response, `/settings?said=${encodeURIComponent("this device now gets a buzz when the plane needs a person")}`);
|
|
9415
9485
|
}
|
|
9416
9486
|
if (url.pathname === "/push/remove") {
|
|
9487
|
+
const body = readForm(posted, CONSOLE_FORMS.pushRemove);
|
|
9417
9488
|
if (who.via !== "cookie")
|
|
9418
9489
|
return refuse(response, who, 403, "push removal is a browser session's act");
|
|
9419
9490
|
const id = Number(body.get("id") ?? "");
|
|
@@ -9424,6 +9495,7 @@ export function createDecisionServer(options) {
|
|
|
9424
9495
|
return redirect(response, `/settings?said=${encodeURIComponent("removed — that device stops receiving pushes")}`);
|
|
9425
9496
|
}
|
|
9426
9497
|
if (url.pathname === "/onboarding/phone/dismiss") {
|
|
9498
|
+
const body = readForm(posted, CONSOLE_FORMS.phoneDismiss);
|
|
9427
9499
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
9428
9500
|
return refuse(response, who, 403, "An approver puts this away.", "/chat");
|
|
9429
9501
|
store.recordInstallationFact(PHONE_CARD_FACT, who.name, now);
|
|
@@ -9433,6 +9505,7 @@ export function createDecisionServer(options) {
|
|
|
9433
9505
|
// conversation it needs no second password; the full form (Advanced) still asks for one.
|
|
9434
9506
|
// Settings → Lead: the name and persona this person's lead speaks with.
|
|
9435
9507
|
if (url.pathname === "/settings/lead/identity") {
|
|
9508
|
+
const body = readForm(posted, CONSOLE_FORMS.leadIdentity);
|
|
9436
9509
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
9437
9510
|
return refuse(response, who, 403, "An approver names their lead.", "/settings/lead");
|
|
9438
9511
|
const checked = checkLeadIdentity(body.get("name") ?? "", body.get("persona") ?? "");
|
|
@@ -9443,6 +9516,7 @@ export function createDecisionServer(options) {
|
|
|
9443
9516
|
}
|
|
9444
9517
|
// Settings → Lead: what this person's lead knows about them, one line each.
|
|
9445
9518
|
if (url.pathname === "/settings/lead/about") {
|
|
9519
|
+
const body = readForm(posted, CONSOLE_FORMS.leadAbout);
|
|
9446
9520
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
9447
9521
|
return refuse(response, who, 403, "An approver edits what their lead knows about them.", "/settings/lead");
|
|
9448
9522
|
const checked = checkAboutYou(body.get("about") ?? "");
|
|
@@ -9453,6 +9527,7 @@ export function createDecisionServer(options) {
|
|
|
9453
9527
|
}
|
|
9454
9528
|
// Settings → Lead: stop the lead following up on one of your promises.
|
|
9455
9529
|
if (url.pathname === "/settings/lead/promise/cancel") {
|
|
9530
|
+
const body = readForm(posted, CONSOLE_FORMS.leadPromiseCancel);
|
|
9456
9531
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
9457
9532
|
return refuse(response, who, 403, "An approver manages the lead's promises.", "/settings/lead");
|
|
9458
9533
|
const id = Number(body.get("promise"));
|
|
@@ -9471,6 +9546,7 @@ export function createDecisionServer(options) {
|
|
|
9471
9546
|
return redirect(response, "/chat");
|
|
9472
9547
|
}
|
|
9473
9548
|
if (url.pathname === "/chat/config") {
|
|
9549
|
+
const body = readForm(posted, CONSOLE_FORMS.chatConfig);
|
|
9474
9550
|
// The console's own door into `config set chat` (operator request:
|
|
9475
9551
|
// chat lives mainly in the web UI). Same ceremony weight as the CLI
|
|
9476
9552
|
// verb: the password typed again authenticates the approver, the
|
|
@@ -9549,6 +9625,7 @@ export function createDecisionServer(options) {
|
|
|
9549
9625
|
}
|
|
9550
9626
|
// ---- the mate (mate arc §5) ------------------------------------------
|
|
9551
9627
|
if (url.pathname === "/chat/mate/mint") {
|
|
9628
|
+
const body = readForm(posted, CONSOLE_FORMS.mateMint);
|
|
9552
9629
|
if (who.via !== "cookie")
|
|
9553
9630
|
return refuse(response, who, 403, "the mate is a browser surface");
|
|
9554
9631
|
const back = safeChatReturn(body.get("return"));
|
|
@@ -9573,6 +9650,7 @@ export function createDecisionServer(options) {
|
|
|
9573
9650
|
return redirect(response, back);
|
|
9574
9651
|
}
|
|
9575
9652
|
if (url.pathname === "/chat/mate/follow") {
|
|
9653
|
+
const body = readForm(posted, CONSOLE_FORMS.mateFollow);
|
|
9576
9654
|
if (who.via !== "cookie")
|
|
9577
9655
|
return refuse(response, who, 403, "Open the conversation to change automatic updates.");
|
|
9578
9656
|
const principal = matePrincipal(who), session = store.activeMateSession(who.name), thread = store.liveMateThreadFor(who.name);
|
|
@@ -9581,6 +9659,7 @@ export function createDecisionServer(options) {
|
|
|
9581
9659
|
return redirect(response, safeChatReturn(body.get("return")));
|
|
9582
9660
|
}
|
|
9583
9661
|
if (url.pathname === "/chat/mate/end") {
|
|
9662
|
+
const body = readForm(posted, CONSOLE_FORMS.mateEnd);
|
|
9584
9663
|
if (who.via !== "cookie")
|
|
9585
9664
|
return refuse(response, who, 403, "the mate is a browser surface");
|
|
9586
9665
|
const back = safeChatReturn(body.get("return"));
|
|
@@ -9593,6 +9672,7 @@ export function createDecisionServer(options) {
|
|
|
9593
9672
|
return redirect(response, back);
|
|
9594
9673
|
}
|
|
9595
9674
|
if (url.pathname === "/chat/mate/stop") {
|
|
9675
|
+
const body = readForm(posted, CONSOLE_FORMS.mateStop);
|
|
9596
9676
|
if (who.via !== "cookie")
|
|
9597
9677
|
return refuse(response, who, 403, "the mate is a browser surface");
|
|
9598
9678
|
const back = safeChatReturn(body.get("return"));
|
|
@@ -9611,6 +9691,7 @@ export function createDecisionServer(options) {
|
|
|
9611
9691
|
}
|
|
9612
9692
|
const mateProposal = /^\/chat\/proposal\/([0-9]{1,15})\/(confirm|dismiss)$/.exec(url.pathname);
|
|
9613
9693
|
if (mateProposal !== null) {
|
|
9694
|
+
const body = readForm(posted, CONSOLE_FORMS.mateProposal);
|
|
9614
9695
|
if (who.via !== "cookie")
|
|
9615
9696
|
return refuse(response, who, 403, "the mate is a browser surface");
|
|
9616
9697
|
const back = safeChatReturn(body.get("return"));
|
|
@@ -9696,6 +9777,7 @@ export function createDecisionServer(options) {
|
|
|
9696
9777
|
// ceiling admits the repo; the card lives on /chat and on the task.
|
|
9697
9778
|
const coordinatorProposal = /^\/proposals\/([0-9]{1,15})\/(confirm|dismiss)$/.exec(url.pathname);
|
|
9698
9779
|
if (coordinatorProposal !== null) {
|
|
9780
|
+
const body = readForm(posted, CONSOLE_FORMS.coordinatorProposal);
|
|
9699
9781
|
if (who.via !== "cookie")
|
|
9700
9782
|
return refuse(response, who, 403, "proposals are confirmed from the browser or the CLI");
|
|
9701
9783
|
const principal = matePrincipal(who);
|
|
@@ -9715,6 +9797,7 @@ export function createDecisionServer(options) {
|
|
|
9715
9797
|
return redirect(response, outcome.ok || outcome.reason !== "needs-confirm" ? back : `${back}${back.includes("?") ? "&" : "?"}said=${encodeURIComponent(outcome.said)}`);
|
|
9716
9798
|
}
|
|
9717
9799
|
if (url.pathname === "/chat") {
|
|
9800
|
+
const body = readForm(posted, CONSOLE_FORMS.chat);
|
|
9718
9801
|
if (who.via !== "cookie")
|
|
9719
9802
|
return refuse(response, who, 403, "chat is a browser surface");
|
|
9720
9803
|
const requestedTask = body.get("task");
|
|
@@ -9860,6 +9943,7 @@ export function createDecisionServer(options) {
|
|
|
9860
9943
|
}
|
|
9861
9944
|
const chatFile = /^\/chat\/file\/([0-9a-f]{32})$/.exec(url.pathname);
|
|
9862
9945
|
if (chatFile !== null) {
|
|
9946
|
+
const body = readForm(posted, CONSOLE_FORMS.chatFile);
|
|
9863
9947
|
if (who.via !== "cookie")
|
|
9864
9948
|
return refuse(response, who, 403, "chat is a browser surface");
|
|
9865
9949
|
const key = chatFile[1];
|
|
@@ -9933,6 +10017,7 @@ export function createDecisionServer(options) {
|
|
|
9933
10017
|
}
|
|
9934
10018
|
const chatAckPost = /^\/chat\/ack\/([0-9]{1,15})$/.exec(url.pathname);
|
|
9935
10019
|
if (chatAckPost !== null) {
|
|
10020
|
+
const body = readForm(posted, CONSOLE_FORMS.chatAck);
|
|
9936
10021
|
if (who.via !== "cookie")
|
|
9937
10022
|
return refuse(response, who, 403, "acknowledgement is a browser ceremony");
|
|
9938
10023
|
const turn = store.getChatTurn(Number(chatAckPost[1]));
|
|
@@ -9952,6 +10037,7 @@ export function createDecisionServer(options) {
|
|
|
9952
10037
|
return redirect(response, "/chat");
|
|
9953
10038
|
}
|
|
9954
10039
|
if (["/recipes/prepare", "/recipes/preview", "/recipes/import", "/recipes/launch", "/recipes/save"].includes(url.pathname)) {
|
|
10040
|
+
const body = readForm(posted, CONSOLE_FORMS.recipes);
|
|
9955
10041
|
const project = projectOf(who, request);
|
|
9956
10042
|
try {
|
|
9957
10043
|
if (project === null || project === undefined || !visible(project))
|
|
@@ -9964,7 +10050,7 @@ export function createDecisionServer(options) {
|
|
|
9964
10050
|
const rootMode = !unscopedMode && ceiling.roots.length > 0;
|
|
9965
10051
|
if (rootMode && !(await authorizedProject(liveCeiling(), project)))
|
|
9966
10052
|
throw new RecipeError("The project is outside this server's access.", 403);
|
|
9967
|
-
const rechecked = authorizeMutation(request, who,
|
|
10053
|
+
const rechecked = authorizeMutation(request, who, readForm(posted, CONSOLE_FORMS.mutationGuard));
|
|
9968
10054
|
if (rechecked !== null)
|
|
9969
10055
|
throw new RecipeError(rechecked.message, rechecked.status);
|
|
9970
10056
|
if (projectOf(who, request) !== project)
|
|
@@ -9979,13 +10065,13 @@ export function createDecisionServer(options) {
|
|
|
9979
10065
|
if (recipe === null || recipe.repo === null)
|
|
9980
10066
|
throw new RecipeError("This recipe is not available in this project.", 404);
|
|
9981
10067
|
try {
|
|
9982
|
-
const preview = prepareRecipeRun(store, who.name, project, id, Number(version), recipeAnswersFromForm(body), now);
|
|
10068
|
+
const preview = prepareRecipeRun(store, who.name, project, id, Number(version), recipeAnswersFromForm(body.sent), now);
|
|
9983
10069
|
return redirect(response, `/recipes/preview?preview=${preview.token}`);
|
|
9984
10070
|
}
|
|
9985
10071
|
catch (error) {
|
|
9986
10072
|
if (!(error instanceof RecipeError))
|
|
9987
10073
|
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() } }));
|
|
10074
|
+
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
10075
|
}
|
|
9990
10076
|
}
|
|
9991
10077
|
if (url.pathname === "/recipes/import") {
|
|
@@ -10005,13 +10091,13 @@ export function createDecisionServer(options) {
|
|
|
10005
10091
|
throw new RecipeError("That recipe version is not available in this project.", 404);
|
|
10006
10092
|
let document;
|
|
10007
10093
|
try {
|
|
10008
|
-
document = recipeFromForm(body);
|
|
10094
|
+
document = recipeFromForm(body.sent);
|
|
10009
10095
|
}
|
|
10010
10096
|
catch (error) {
|
|
10011
10097
|
if (!(error instanceof RecipeError))
|
|
10012
10098
|
throw error;
|
|
10013
10099
|
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() } }));
|
|
10100
|
+
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
10101
|
}
|
|
10016
10102
|
const preview = createWorkflowPreview(store, who.name, project, document, source, now);
|
|
10017
10103
|
return redirect(response, `/recipes/preview?preview=${preview.token}${purpose === "recipe" || document.version === 2 ? "&purpose=recipe" : ""}`);
|
|
@@ -10040,6 +10126,7 @@ export function createDecisionServer(options) {
|
|
|
10040
10126
|
}
|
|
10041
10127
|
}
|
|
10042
10128
|
if (url.pathname === "/routines/add") {
|
|
10129
|
+
const body = readForm(posted, CONSOLE_FORMS.routinesAdd);
|
|
10043
10130
|
// A standing order files into the OPEN project — never a typed path,
|
|
10044
10131
|
// so the ceiling question never even arises — and lands on its own
|
|
10045
10132
|
// screen where the approval step-up already lives: filing is cheap,
|
|
@@ -10075,11 +10162,11 @@ export function createDecisionServer(options) {
|
|
|
10075
10162
|
// 7): the service validates, canonicalizes, digests, and stamps
|
|
10076
10163
|
// provenance; the admission list makes the ceiling explicit even
|
|
10077
10164
|
// though `project` was already proved inside it.
|
|
10078
|
-
const scheduled = body.has("repeat") ? composerSchedule(body) : { ok: true, schedule: (body.get("schedule") ?? "").trim() };
|
|
10165
|
+
const scheduled = body.has("repeat") ? composerSchedule(body.sent) : { ok: true, schedule: (body.get("schedule") ?? "").trim() };
|
|
10079
10166
|
if (!scheduled.ok || scheduled.schedule === null) {
|
|
10080
10167
|
return sendScreen(response, 400, routinesPage(chromeFor(project, "routines"), store.routineTracks(project, now).filter(track => visible(track.routine.repo)), {
|
|
10081
10168
|
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,
|
|
10169
|
+
problem: scheduled.ok ? "Choose a recurring schedule." : scheduled.message, values: body.sent,
|
|
10083
10170
|
}));
|
|
10084
10171
|
}
|
|
10085
10172
|
const created = fileRoutineProposal(store, {
|
|
@@ -10102,17 +10189,18 @@ export function createDecisionServer(options) {
|
|
|
10102
10189
|
return sendScreen(response, created.reason === "duplicate" ? 409 : 400, routinesPage(chromeFor(project, "routines"), tracks, {
|
|
10103
10190
|
csrf: who.via === "cookie" ? who.session.csrf : "",
|
|
10104
10191
|
revision: who.via === "cookie" ? who.session.projectRevision : 0,
|
|
10105
|
-
problem: created.message, values: body,
|
|
10192
|
+
problem: created.message, values: body.sent,
|
|
10106
10193
|
}));
|
|
10107
10194
|
}
|
|
10108
10195
|
return redirect(response, `/routines/${created.id}`);
|
|
10109
10196
|
}
|
|
10110
10197
|
const routineAct = /^\/routines\/([0-9]{1,15})\/(approve|refresh|pause|resume|run-now)$/.exec(url.pathname);
|
|
10111
10198
|
if (routineAct !== null) {
|
|
10112
|
-
return routineMutation(response, who, Number(routineAct[1]), routineAct[2],
|
|
10199
|
+
return routineMutation(response, who, Number(routineAct[1]), routineAct[2], readForm(posted, CONSOLE_FORMS.routine), now);
|
|
10113
10200
|
}
|
|
10114
10201
|
const runNote = /^\/r\/([0-9]{1,15})\/note$/.exec(url.pathname);
|
|
10115
10202
|
if (runNote !== null) {
|
|
10203
|
+
const body = readForm(posted, CONSOLE_FORMS.runNote);
|
|
10116
10204
|
// An operator's verdict beside the machine's record (M6): immutable,
|
|
10117
10205
|
// bounded by the same validator as decision notes, ceiling-checked
|
|
10118
10206
|
// like every run resource. Ordinary authenticated mutation — no
|
|
@@ -10130,6 +10218,7 @@ export function createDecisionServer(options) {
|
|
|
10130
10218
|
}
|
|
10131
10219
|
const diffComment = /^\/r\/([0-9]{1,15})\/comment$/.exec(url.pathname);
|
|
10132
10220
|
if (diffComment !== null) {
|
|
10221
|
+
const body = readForm(posted, CONSOLE_FORMS.diffComment);
|
|
10133
10222
|
// A review comment on the IMMUTABLE terminal diff (M6.8): bound to
|
|
10134
10223
|
// the exact artifact and its hash. Ordinary authenticated mutation —
|
|
10135
10224
|
// the nonce belongs to the approval screen that later restates the
|
|
@@ -10216,6 +10305,7 @@ export function createDecisionServer(options) {
|
|
|
10216
10305
|
return redirect(response, `${back}${back.includes("?") ? "&" : "?"}noted=${receipt}#request-changes`);
|
|
10217
10306
|
}
|
|
10218
10307
|
if (url.pathname === "/session/editor-links") {
|
|
10308
|
+
const body = readForm(posted, CONSOLE_FORMS.editorLinks);
|
|
10219
10309
|
// The session half of the editor-link activation (arc 6, finding 1):
|
|
10220
10310
|
// only the person at the browser can say "this device holds the
|
|
10221
10311
|
// worktrees". Per-session, dies with the session, grants nothing —
|
|
@@ -10230,6 +10320,7 @@ export function createDecisionServer(options) {
|
|
|
10230
10320
|
}
|
|
10231
10321
|
const turnAct = /^\/r\/([0-9]{1,15})\/turn$/.exec(url.pathname);
|
|
10232
10322
|
if (turnAct !== null) {
|
|
10323
|
+
const body = readForm(posted, CONSOLE_FORMS.turn);
|
|
10233
10324
|
// The operator's turn (Phase 2E, v2 S1g): cookie-only — a watching
|
|
10234
10325
|
// person, never a bearer machine — and every hard gate (custody,
|
|
10235
10326
|
// lease, cap, budget, open decision, single flight) re-proves
|
|
@@ -10266,6 +10357,7 @@ export function createDecisionServer(options) {
|
|
|
10266
10357
|
}
|
|
10267
10358
|
const revise = /^\/r\/([0-9]{1,15})\/revise$/.exec(url.pathname);
|
|
10268
10359
|
if (revise !== null) {
|
|
10360
|
+
const body = readForm(posted, CONSOLE_FORMS.revise);
|
|
10269
10361
|
// Seal the live comment batch into ONE unapproved revision task with
|
|
10270
10362
|
// an immutable brief (M6.8). Deterministic — no model reads anything
|
|
10271
10363
|
// here — and every revision takes its own approval: comments can
|
|
@@ -10288,6 +10380,7 @@ export function createDecisionServer(options) {
|
|
|
10288
10380
|
// approved command once and seals the log), or file a task to add tests.
|
|
10289
10381
|
const followUp = /^\/r\/([0-9]{1,15})\/(checks|add-tests)$/.exec(url.pathname);
|
|
10290
10382
|
if (followUp !== null) {
|
|
10383
|
+
const body = readForm(posted, CONSOLE_FORMS.followUp);
|
|
10291
10384
|
const id = Number(followUp[1]);
|
|
10292
10385
|
const found = store.getRun(id);
|
|
10293
10386
|
if (found === null || !visible(taskRepoOf(found.taskRef)))
|
|
@@ -10385,6 +10478,7 @@ export function createDecisionServer(options) {
|
|
|
10385
10478
|
}
|
|
10386
10479
|
const resolve = /^\/i\/([0-9]{1,15})\/resolve$/.exec(url.pathname);
|
|
10387
10480
|
if (resolve !== null) {
|
|
10481
|
+
const body = readForm(posted, CONSOLE_FORMS.resolveIncident);
|
|
10388
10482
|
const id = Number(resolve[1]);
|
|
10389
10483
|
// The ceiling applies to incident mutation exactly as to every other
|
|
10390
10484
|
// resource (v3 review, finding 3): resolve incident → run → task, and
|
|
@@ -10668,7 +10762,7 @@ export function createDecisionServer(options) {
|
|
|
10668
10762
|
store.beatAuthorization(minted.authorization.id, now);
|
|
10669
10763
|
return redirect(response, taskHref(taskId));
|
|
10670
10764
|
}
|
|
10671
|
-
function taskMutation(response, who, taskId, verb,
|
|
10765
|
+
function taskMutation(response, who, taskId, verb, posted, now) {
|
|
10672
10766
|
const ref = store.lookupRef(taskId);
|
|
10673
10767
|
if (ref === null || store.getTask(taskId) === null) {
|
|
10674
10768
|
return refuse(response, who, 404, "no such task", "/tasks");
|
|
@@ -10678,6 +10772,7 @@ export function createDecisionServer(options) {
|
|
|
10678
10772
|
}
|
|
10679
10773
|
switch (verb) {
|
|
10680
10774
|
case "steer": {
|
|
10775
|
+
const body = readForm(posted, CONSOLE_FORMS.taskSteer);
|
|
10681
10776
|
// Steering is a browser session's act, explicitly (arc 1 v2 §3):
|
|
10682
10777
|
// identify() accepts bearer credentials generically, and those are
|
|
10683
10778
|
// for machines — a person watching steers, cookie + CSRF only.
|
|
@@ -10699,6 +10794,7 @@ export function createDecisionServer(options) {
|
|
|
10699
10794
|
return redirect(response, taskHref(taskId));
|
|
10700
10795
|
}
|
|
10701
10796
|
case "hold": {
|
|
10797
|
+
const body = readForm(posted, CONSOLE_FORMS.taskHold);
|
|
10702
10798
|
const reason = (body.get("reason") ?? "").trim() || "held from the console";
|
|
10703
10799
|
if (reason.length > 200 || hasForbiddenControls(reason)) {
|
|
10704
10800
|
return taskScreen(response, who, taskId, "that reason will not render, so it will not store", 400);
|
|
@@ -10710,6 +10806,7 @@ export function createDecisionServer(options) {
|
|
|
10710
10806
|
return redirect(response, taskHref(taskId));
|
|
10711
10807
|
}
|
|
10712
10808
|
case "block": {
|
|
10809
|
+
const body = readForm(posted, CONSOLE_FORMS.taskBlock);
|
|
10713
10810
|
// Chains are scheduling, not authority (chains-and-next review,
|
|
10714
10811
|
// finding 2): the edge decides WHEN the ready set admits the task;
|
|
10715
10812
|
// approval still decides WHAT may build. Both ends are re-proved
|
|
@@ -10731,6 +10828,7 @@ export function createDecisionServer(options) {
|
|
|
10731
10828
|
return redirect(response, taskHref(taskId));
|
|
10732
10829
|
}
|
|
10733
10830
|
case "unblock": {
|
|
10831
|
+
const body = readForm(posted, CONSOLE_FORMS.taskBlock);
|
|
10734
10832
|
const on = (body.get("on") ?? "").trim();
|
|
10735
10833
|
if (store.openContestFor(ref.id) !== null) {
|
|
10736
10834
|
return taskScreen(response, who, taskId, "a tournament is running on this task — let it finish, then pick or abandon it", 409);
|
|
@@ -10742,6 +10840,7 @@ export function createDecisionServer(options) {
|
|
|
10742
10840
|
return redirect(response, taskHref(taskId));
|
|
10743
10841
|
}
|
|
10744
10842
|
case "repair-dependency": {
|
|
10843
|
+
const body = readForm(posted, CONSOLE_FORMS.taskRepairDependency);
|
|
10745
10844
|
const blocker = (body.get("blocker") ?? "").trim();
|
|
10746
10845
|
const operation = (body.get("operation") ?? "").trim();
|
|
10747
10846
|
const blockerTask = blocker === "" || !store.blockers(taskId).includes(blocker) ? null : store.getTask(blocker);
|
|
@@ -10785,6 +10884,7 @@ export function createDecisionServer(options) {
|
|
|
10785
10884
|
return taskScreen(response, who, taskId, "choose whether to try that task again, wait for a different task, or continue without it", 400);
|
|
10786
10885
|
}
|
|
10787
10886
|
case "next": {
|
|
10887
|
+
const body = readForm(posted, CONSOLE_FORMS.taskNext);
|
|
10788
10888
|
if (body.get("undo") !== null) {
|
|
10789
10889
|
const cleared = store.clearTaskPriority(taskId);
|
|
10790
10890
|
if (!cleared.ok)
|
|
@@ -10805,6 +10905,7 @@ export function createDecisionServer(options) {
|
|
|
10805
10905
|
return redirect(response, taskHref(taskId));
|
|
10806
10906
|
}
|
|
10807
10907
|
case "reopen": {
|
|
10908
|
+
const body = readForm(posted, CONSOLE_FORMS.taskReopen);
|
|
10808
10909
|
// Authenticated like every approving act: the session alone may
|
|
10809
10910
|
// read; resuming external work takes the password, typed again.
|
|
10810
10911
|
const token = (body.get("token") ?? "").trim();
|
|
@@ -10834,6 +10935,7 @@ export function createDecisionServer(options) {
|
|
|
10834
10935
|
return redirect(response, taskHref(taskId));
|
|
10835
10936
|
}
|
|
10836
10937
|
case "requeue": {
|
|
10938
|
+
const body = readForm(posted, CONSOLE_FORMS.taskRequeue);
|
|
10837
10939
|
// Retry may carry a note for the next attempt: the same steering note, checked before anything changes.
|
|
10838
10940
|
const note = (body.get("note") ?? "").trim();
|
|
10839
10941
|
if (note !== "" && who.via !== "cookie")
|
|
@@ -10853,6 +10955,7 @@ export function createDecisionServer(options) {
|
|
|
10853
10955
|
return redirect(response, body.get("return") === "inbox" ? "/inbox" : body.get("return") === "next" ? "/next" : taskHref(taskId));
|
|
10854
10956
|
}
|
|
10855
10957
|
case "follow-up": {
|
|
10958
|
+
const body = readForm(posted, CONSOLE_FORMS.taskFollowUp);
|
|
10856
10959
|
// A scout's proposed follow-up, filed by the operator's tap (mate
|
|
10857
10960
|
// arc §10): the ONE filing door, this task's repository, the scope
|
|
10858
10961
|
// text stamped as the scout's — mode coverage never seals it.
|
|
@@ -10910,6 +11013,7 @@ export function createDecisionServer(options) {
|
|
|
10910
11013
|
return redirect(response, taskHref(taskId));
|
|
10911
11014
|
}
|
|
10912
11015
|
case "plan-edit": {
|
|
11016
|
+
const body = readForm(posted, CONSOLE_FORMS.taskPlanEdit);
|
|
10913
11017
|
if (who.via !== "cookie") {
|
|
10914
11018
|
return refuse(response, who, 403, "editing a plan is a browser session's act");
|
|
10915
11019
|
}
|
|
@@ -10967,6 +11071,7 @@ export function createDecisionServer(options) {
|
|
|
10967
11071
|
}
|
|
10968
11072
|
case "accept-revision":
|
|
10969
11073
|
case "reject-revision": {
|
|
11074
|
+
const body = readForm(posted, CONSOLE_FORMS.taskRevision);
|
|
10970
11075
|
// Adaptive execution plans (v44): resolving a revision the builder
|
|
10971
11076
|
// filed while its authority snapshot no longer matched the run it
|
|
10972
11077
|
// started under — the ONLY road a 'blocked' plan_revision reaches,
|
|
@@ -10997,6 +11102,7 @@ export function createDecisionServer(options) {
|
|
|
10997
11102
|
return redirect(response, taskHref(taskId));
|
|
10998
11103
|
}
|
|
10999
11104
|
case "cancel": {
|
|
11105
|
+
const body = readForm(posted, CONSOLE_FORMS.taskCancel);
|
|
11000
11106
|
const reason = body.get("reason") ?? undefined;
|
|
11001
11107
|
const cancelled = withActor({ account: who.name, lead: false }, () => store.cancelTask(taskId, now, reason));
|
|
11002
11108
|
if (!cancelled.ok) {
|
|
@@ -11010,6 +11116,7 @@ export function createDecisionServer(options) {
|
|
|
11010
11116
|
return redirect(response, taskHref(taskId));
|
|
11011
11117
|
}
|
|
11012
11118
|
case "route": {
|
|
11119
|
+
const body = readForm(posted, CONSOLE_FORMS.taskRoute);
|
|
11013
11120
|
// THE AGENTS EDIT (v47): an approver's session declares the risk,
|
|
11014
11121
|
// overrides one role to an exact agent, or clears an override — ONE
|
|
11015
11122
|
// authenticated transaction in the store that CAS-checks the scope
|
|
@@ -11095,6 +11202,7 @@ export function createDecisionServer(options) {
|
|
|
11095
11202
|
return redirect(response, toChat ? target : `${target}#agents`);
|
|
11096
11203
|
}
|
|
11097
11204
|
case "scope": {
|
|
11205
|
+
const body = readForm(posted, CONSOLE_FORMS.taskScope);
|
|
11098
11206
|
const sawDigest = body.get("sawDigest");
|
|
11099
11207
|
const permissionGiven = body.get("permission-mode");
|
|
11100
11208
|
if (permissionGiven !== null && permissionGiven !== "" && permissionGiven !== "auto" && permissionGiven !== "bypassPermissions") {
|
|
@@ -11290,7 +11398,7 @@ export function createDecisionServer(options) {
|
|
|
11290
11398
|
if (!saved.ok) {
|
|
11291
11399
|
// Refused because the plan moved on: the draft comes back with the version now on file, so saving it
|
|
11292
11400
|
// again (having read the reason) can succeed instead of being refused the same way.
|
|
11293
|
-
const draft = new URLSearchParams(body);
|
|
11401
|
+
const draft = new URLSearchParams(body.sent);
|
|
11294
11402
|
if (saved.status === 409)
|
|
11295
11403
|
draft.set("sawDigest", store.getScope(taskId)?.digest ?? "");
|
|
11296
11404
|
return taskScreen(response, who, taskId, saved.message, saved.status, draft);
|
|
@@ -11298,6 +11406,7 @@ export function createDecisionServer(options) {
|
|
|
11298
11406
|
return redirect(response, taskHref(taskId));
|
|
11299
11407
|
}
|
|
11300
11408
|
case "approve": {
|
|
11409
|
+
const body = readForm(posted, CONSOLE_FORMS.taskApprove);
|
|
11301
11410
|
const requestedReturn = body.get("return");
|
|
11302
11411
|
const approvalBack = requestedReturn === "next"
|
|
11303
11412
|
? "/next"
|
|
@@ -11384,6 +11493,7 @@ export function createDecisionServer(options) {
|
|
|
11384
11493
|
return redirect(response, approvalBack);
|
|
11385
11494
|
}
|
|
11386
11495
|
case "accept-proof": {
|
|
11496
|
+
const body = readForm(posted, CONSOLE_FORMS.taskAcceptProof);
|
|
11387
11497
|
// Accepting is a person's act, like approving a scope (Priority
|
|
11388
11498
|
// 2): a cookie session only, never a bearer credential.
|
|
11389
11499
|
if (who.via !== "cookie") {
|
|
@@ -11420,6 +11530,7 @@ export function createDecisionServer(options) {
|
|
|
11420
11530
|
return redirect(response, body.get("return") === null ? taskHref(taskId) : safeReturn(body.get("return")));
|
|
11421
11531
|
}
|
|
11422
11532
|
case "complete": {
|
|
11533
|
+
const body = readForm(posted, CONSOLE_FORMS.taskComplete);
|
|
11423
11534
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
11424
11535
|
return refuse(response, who, 403, "Only an approver can mark a result complete.");
|
|
11425
11536
|
const principal = matePrincipal(who);
|
|
@@ -11465,6 +11576,7 @@ export function createDecisionServer(options) {
|
|
|
11465
11576
|
return taskScreen(response, who, taskId, "Separate agent reviews have retired. Open the saved result to mark it complete or request a revision.", 410);
|
|
11466
11577
|
}
|
|
11467
11578
|
case "stop": {
|
|
11579
|
+
const body = readForm(posted, CONSOLE_FORMS.taskStop);
|
|
11468
11580
|
// The exact-run stop (v52): a browser session's act, an approver's
|
|
11469
11581
|
// act, naming ONE run. The store's transaction proves the run is
|
|
11470
11582
|
// this task's current live attempt and records who asked before
|
|
@@ -11496,9 +11608,11 @@ export function createDecisionServer(options) {
|
|
|
11496
11608
|
return redirect(response, back === "chat" ? `${taskChatHref(taskId)}#task-control` : `${taskHref(taskId)}#task-control`);
|
|
11497
11609
|
}
|
|
11498
11610
|
case "resume-arm": {
|
|
11611
|
+
const body = readForm(posted, CONSOLE_FORMS.taskResume);
|
|
11499
11612
|
return armTaskResume(response, who, taskId, (body.get("run") ?? "").trim(), body.get("return") === "chat" ? "chat" : "task", now);
|
|
11500
11613
|
}
|
|
11501
11614
|
case "resume": {
|
|
11615
|
+
const body = readForm(posted, CONSOLE_FORMS.taskResume);
|
|
11502
11616
|
// The resume itself (v52): password typed again + the durable nonce
|
|
11503
11617
|
// consumed INSIDE the store's resume transaction against the digest
|
|
11504
11618
|
// re-derived from live state. A replay finds the nonce spent; a
|