toolroll 0.9.52 → 0.9.53
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 +8 -0
- package/README.md +55 -80
- package/dist/agentconfig.d.ts +11 -36
- package/dist/agentconfig.js +3 -62
- package/dist/approval-policy.d.ts +1 -1
- package/dist/approval-policy.js +1 -1
- package/dist/board.d.ts +3 -19
- package/dist/board.js +4 -42
- package/dist/browser/workspace.js +18 -18
- package/dist/browser-workspace.d.ts +2 -1
- package/dist/builder.d.ts +9 -40
- package/dist/builder.js +50 -257
- package/dist/chat-actions.js +3 -14
- package/dist/chat-controls.d.ts +0 -4
- package/dist/chat-controls.js +0 -1
- package/dist/chat-decide.js +1 -3
- package/dist/chat-delivery.d.ts +0 -2
- package/dist/chat-delivery.js +0 -1
- package/dist/chat-quiet.js +2 -2
- package/dist/chat-task-actions.d.ts +1 -1
- package/dist/chat-task-actions.js +0 -2
- package/dist/claim.d.ts +79 -244
- package/dist/claim.js +688 -1023
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +10 -2
- package/dist/contracts/cli.d.ts +0 -13
- package/dist/contracts/cli.js +0 -2
- package/dist/contracts/console-api.d.ts +5 -10
- package/dist/contracts/console-api.js +4 -10
- package/dist/contracts/flow.d.ts +50 -0
- package/dist/contracts/flow.js +26 -1
- package/dist/contracts/lead-tools.d.ts +0 -13
- package/dist/contracts/lead-tools.js +3 -3
- package/dist/contracts/store-json.d.ts +1 -25
- package/dist/contracts/store-json.js +3 -13
- package/dist/converse.d.ts +1 -11
- package/dist/converse.js +6 -38
- package/dist/coordinator.d.ts +0 -1
- package/dist/coordinator.js +3 -5
- package/dist/demo.js +16 -26
- package/dist/desktop-update-gate.js +0 -1
- package/dist/desktop-update.js +1 -3
- package/dist/dispatch.d.ts +1 -1
- package/dist/dispatch.js +1 -1
- package/dist/dispose.d.ts +2 -3
- package/dist/dispose.js +17 -92
- package/dist/envelope.js +0 -3
- package/dist/exec.d.ts +0 -64
- package/dist/exec.js +3 -356
- package/dist/exhaustion.d.ts +22 -76
- package/dist/exhaustion.js +14 -140
- package/dist/export.js +4 -4
- package/dist/flow-engine.js +1 -1
- package/dist/flow-schedule.d.ts +154 -0
- package/dist/flow-schedule.js +321 -0
- package/dist/flow-share.js +1 -1
- package/dist/flow-triggers.d.ts +6 -1
- package/dist/flow-triggers.js +65 -10
- package/dist/flows-cli.js +8 -4
- package/dist/flows-ui.js +2 -0
- package/dist/flows.js +1 -1
- package/dist/guides.js +9 -29
- package/dist/invoke.d.ts +1 -24
- package/dist/invoke.js +17 -259
- package/dist/lead-status.js +2 -4
- package/dist/lead-voice.js +1 -1
- package/dist/mate-doors.d.ts +1 -2
- package/dist/mate-doors.js +12 -25
- package/dist/mate-tools.d.ts +2 -2
- package/dist/mate-tools.js +10 -22
- package/dist/modes.d.ts +6 -7
- package/dist/modes.js +7 -13
- package/dist/operate-remote-arguments.js +2 -6
- package/dist/operate-remote.js +6 -7
- package/dist/operate.d.ts +0 -6
- package/dist/operate.js +222 -2247
- package/dist/phase-routing.d.ts +13 -23
- package/dist/phase-routing.js +13 -35
- package/dist/plan-auto.js +2 -2
- package/dist/planner-source.js +2 -1
- package/dist/policy.d.ts +1 -4
- package/dist/policy.js +0 -8
- package/dist/process-recovery-eligibility.js +2 -3
- package/dist/project-delete.js +3 -15
- package/dist/proposal.d.ts +4 -24
- package/dist/proposal.js +3 -88
- package/dist/provider-auth.d.ts +3 -3
- package/dist/provider-auth.js +4 -6
- package/dist/provider.d.ts +11 -25
- package/dist/provider.js +4 -36
- package/dist/push.js +1 -1
- package/dist/recipe-ui.js +6 -6
- package/dist/recipes.d.ts +3 -3
- package/dist/recipes.js +27 -23
- package/dist/run-closeout.d.ts +1 -1
- package/dist/run-closeout.js +1 -4
- package/dist/scope.d.ts +11 -65
- package/dist/scope.js +17 -82
- package/dist/serve.js +15 -24
- package/dist/server/chat.js +7 -30
- package/dist/server/flows.js +11 -10
- package/dist/server/guards.js +0 -1
- package/dist/server/pages-assets.js +1 -1
- package/dist/server/pages.js +0 -2
- package/dist/server/people-tokens.js +0 -4
- package/dist/server/route-table.d.ts +1 -1
- package/dist/server/route-table.js +2 -17
- package/dist/server/settings.js +1 -8
- package/dist/server/shared.d.ts +28 -212
- package/dist/server/shared.js +71 -780
- package/dist/server/task-views.js +9 -76
- package/dist/server/tasks.d.ts +1 -50
- package/dist/server/tasks.js +36 -946
- package/dist/setup-guide.d.ts +2 -2
- package/dist/slack-chat.d.ts +0 -2
- package/dist/slack-chat.js +0 -1
- package/dist/store.d.ts +105 -1340
- package/dist/store.js +706 -4010
- package/dist/surface.js +10 -24
- package/dist/task-composer.d.ts +1 -1
- package/dist/task-composer.js +1 -1
- package/dist/task-control.d.ts +3 -10
- package/dist/task-control.js +2 -7
- package/dist/teammate-desk.js +1 -1
- package/dist/teammates-ui.js +1 -1
- package/dist/telegram-mate.d.ts +0 -3
- package/dist/telegram-mate.js +0 -1
- package/dist/templates.d.ts +11 -7
- package/dist/templates.js +12 -9
- package/dist/toolroll-update.d.ts +10 -0
- package/dist/toolroll-update.js +56 -4
- package/dist/work-index.js +2 -6
- package/dist/workspace-ui.js +1 -1
- package/package.json +1 -1
- package/scripts/recovery-canary.mjs +3 -3
- package/dist/contest.d.ts +0 -318
- package/dist/contest.js +0 -753
- package/dist/held.d.ts +0 -150
- package/dist/held.js +0 -799
- package/dist/liveness.d.ts +0 -30
- package/dist/liveness.js +0 -42
- package/dist/pricing.d.ts +0 -45
- package/dist/pricing.js +0 -77
- package/dist/routine.d.ts +0 -228
- package/dist/routine.js +0 -764
- package/dist/supervisor.mjs +0 -297
package/dist/scope.js
CHANGED
|
@@ -36,7 +36,7 @@ import { z } from "zod";
|
|
|
36
36
|
import {} from "./contracts/acceptance-terms.js";
|
|
37
37
|
import { parseContract } from "./contracts/contract.js";
|
|
38
38
|
import { readAcceptance, readScopeTerms, storedRubricSchema } from "./contracts/scope.js";
|
|
39
|
-
import { NO_READINESS, exactModelId,
|
|
39
|
+
import { NO_READINESS, exactModelId, legOf, projectRoute, routeDigestOf, routeFromJson, routeJsonProblem, routeWords, } from "./phase-routing.js";
|
|
40
40
|
/** Same shape as a runner's credential, for the same reasons. */
|
|
41
41
|
function mintToken() {
|
|
42
42
|
return randomBytes(32).toString("base64url");
|
|
@@ -129,9 +129,10 @@ export function profileDigestOf(profile) {
|
|
|
129
129
|
.digest("hex")
|
|
130
130
|
.slice(0, 32);
|
|
131
131
|
}
|
|
132
|
-
// ---- fallback chains (v30)
|
|
133
|
-
//
|
|
134
|
-
//
|
|
132
|
+
// ---- fallback chains (v30-v114; removed in v115) --------------------------
|
|
133
|
+
// History only: an approval sealed over a fallback chain still decodes and
|
|
134
|
+
// its digest still re-derives, so old tasks verify and render. Nothing
|
|
135
|
+
// files, seals, or dispatches a chain any more.
|
|
135
136
|
export const CHAIN_DIGEST_VERSION = 1;
|
|
136
137
|
/** The stored snapshot bytes for a chain: the version rides IN the
|
|
137
138
|
* snapshot, exactly like a single profile's. */
|
|
@@ -148,19 +149,6 @@ export function chainDigestOf(entries) {
|
|
|
148
149
|
.digest("hex")
|
|
149
150
|
.slice(0, 32);
|
|
150
151
|
}
|
|
151
|
-
/**
|
|
152
|
-
* One chain entry's BINDING digest (E3d): the profile digest + auth mode
|
|
153
|
-
* under their own domain. A run row pins this at admission (or base-cycle
|
|
154
|
-
* open), and the dispatch proof re-derives it from the approved chain at
|
|
155
|
-
* the run's index — so nothing downstream can swap WHICH entry a run
|
|
156
|
-
* spends as, in model or in credential.
|
|
157
|
-
*/
|
|
158
|
-
export function entryDigestOf(entry) {
|
|
159
|
-
return createHash("sha256")
|
|
160
|
-
.update(`standing-orders:chain-entry:v${CHAIN_DIGEST_VERSION}:${profileDigestOf(entry.profile)}:${entry.authMode}`, "utf8")
|
|
161
|
-
.digest("hex")
|
|
162
|
-
.slice(0, 32);
|
|
163
|
-
}
|
|
164
152
|
/** Strict re-hydration of a stored chain — every entry proved through the
|
|
165
153
|
* single-profile rehydrator; anything unexpected is null. */
|
|
166
154
|
export function chainFromJson(json) {
|
|
@@ -409,7 +397,7 @@ export function profileFromJson(json) {
|
|
|
409
397
|
}
|
|
410
398
|
/**
|
|
411
399
|
* Parse a rubric from already-JSON-parsed input (the planner's handoff, a
|
|
412
|
-
* console form, a
|
|
400
|
+
* console form, a template or a schedule's terms): fail closed, every
|
|
413
401
|
* problem reported at once, each naming its path — the acceptance contract
|
|
414
402
|
* (contracts/scope.ts) reads it. An absent or empty `value` parses to `[]`
|
|
415
403
|
* with no problems: whether that is ALLOWED is a question for the caller
|
|
@@ -569,7 +557,7 @@ route) {
|
|
|
569
557
|
.slice(0, 32);
|
|
570
558
|
}
|
|
571
559
|
export function propose(store, input) {
|
|
572
|
-
const { taskId, goal, outOfScope = null, touches = [], budgetMicrousd = null, acceptance = [], qualityMode = "default", now, mutation = {}, profile, permissionMode, posture, proposedVia = null
|
|
560
|
+
const { taskId, goal, outOfScope = null, touches = [], budgetMicrousd = null, acceptance = [], qualityMode = "default", now, mutation = {}, profile, permissionMode, posture, proposedVia = null } = input;
|
|
573
561
|
const draft = { goal, outOfScope, touches: [...touches], budgetMicrousd, acceptance: [...acceptance], qualityMode, candidate: input.candidate ?? null };
|
|
574
562
|
const previous = store.getScope(taskId);
|
|
575
563
|
const scope = {
|
|
@@ -590,7 +578,6 @@ export function propose(store, input) {
|
|
|
590
578
|
...(profile === undefined ? {} : { profile }),
|
|
591
579
|
...(permissionMode === undefined ? {} : { permissionMode }),
|
|
592
580
|
...(input.qualityMode === undefined ? {} : { qualityMode: input.qualityMode }),
|
|
593
|
-
...(riskLevel === undefined || !isRiskLevel(riskLevel) ? {} : { riskLevel }),
|
|
594
581
|
...(posture === undefined ? {} : { posture }),
|
|
595
582
|
proposedVia,
|
|
596
583
|
});
|
|
@@ -755,7 +742,7 @@ export function authenticateAccount(store, by, secret) {
|
|
|
755
742
|
* v100: someone who signed in with the identity provider moments ago has no
|
|
756
743
|
* password to type again. The console marks such a request with this, for
|
|
757
744
|
* that person only, and an EMPTY password then stands for the fresh sign-in
|
|
758
|
-
* at every step-up that reaches `authenticateApprover` (scope
|
|
745
|
+
* at every step-up that reaches `authenticateApprover` (scope
|
|
759
746
|
* approval, chat-action review, the console's own ceremonies). A typed
|
|
760
747
|
* password is always checked, and nothing else stands in.
|
|
761
748
|
*/
|
|
@@ -990,16 +977,10 @@ export function scopeAuthorityOf(scope, env = {}) {
|
|
|
990
977
|
if (profile === null) {
|
|
991
978
|
return { ok: false, reason: "profile", problem: profileJson === null ? "the scope carries no agent profile" : "the scope's agent profile cannot be read exactly (unknown keys, an unsafe number, or a clock no timer can hold)" };
|
|
992
979
|
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
if (
|
|
996
|
-
return { ok: false, reason: "chain", problem: "
|
|
997
|
-
}
|
|
998
|
-
if (chain !== null) {
|
|
999
|
-
const base = chain[0];
|
|
1000
|
-
if (base === undefined || profileDigestOf(base.profile) !== profileDigestOf(profile)) {
|
|
1001
|
-
return { ok: false, reason: "parity", problem: "the scope's agent profile is not its fallback chain's first entry" };
|
|
1002
|
-
}
|
|
980
|
+
// A scope filed with fallback agents (v30-v114) holds no authority now
|
|
981
|
+
// that fallback chains are gone: saving it again chooses its agents.
|
|
982
|
+
if (scope.proposedChainJson != null) {
|
|
983
|
+
return { ok: false, reason: "chain", problem: "this scope named fallback agents, which Toolroll no longer uses — save the scope again to choose its agents" };
|
|
1003
984
|
}
|
|
1004
985
|
const routeJson = scope.proposedRouteJson ?? null;
|
|
1005
986
|
const route = routeFromJson(routeJson);
|
|
@@ -1009,7 +990,7 @@ export function scopeAuthorityOf(scope, env = {}) {
|
|
|
1009
990
|
const parity = routeParityProblem(route, profile, { riskLevel: scope.riskLevel ?? "routine", qualityMode: scope.qualityMode ?? "default" });
|
|
1010
991
|
if (parity !== null)
|
|
1011
992
|
return { ok: false, reason: "parity", problem: parity };
|
|
1012
|
-
const digest = digestOf({ goal: scope.goal, outOfScope: scope.outOfScope, touches: scope.touches, budgetMicrousd: scope.budgetMicrousd, acceptance: scope.acceptance, candidate: scope.candidate ?? null, qualityMode: scope.qualityMode ?? "default" },
|
|
993
|
+
const digest = digestOf({ goal: scope.goal, outOfScope: scope.outOfScope, touches: scope.touches, budgetMicrousd: scope.budgetMicrousd, acceptance: scope.acceptance, candidate: scope.candidate ?? null, qualityMode: scope.qualityMode ?? "default" }, profile, route);
|
|
1013
994
|
if (digest !== scope.digest) {
|
|
1014
995
|
return { ok: false, reason: "digest", problem: "the scope's reference does not re-derive from its own terms, agents, and route — file it again" };
|
|
1015
996
|
}
|
|
@@ -1018,31 +999,15 @@ export function scopeAuthorityOf(scope, env = {}) {
|
|
|
1018
999
|
// strict reader — a present mode file that says neither word closes
|
|
1019
1000
|
// the door in its words, never the default a lenient read would coerce
|
|
1020
1001
|
// it to (the spawn reads the same way, so nothing sealed here spends
|
|
1021
|
-
// on a credential the operator never named).
|
|
1022
|
-
// its base entry's pinned mode and must agree with the live file too.
|
|
1002
|
+
// on a credential the operator never named).
|
|
1023
1003
|
let authMode = null;
|
|
1024
1004
|
if (env.authMode !== undefined) {
|
|
1025
1005
|
const read = env.authMode(profile.provider);
|
|
1026
1006
|
if (!read.ok)
|
|
1027
1007
|
return { ok: false, reason: "auth-mode", problem: read.problem };
|
|
1028
1008
|
authMode = read.mode;
|
|
1029
|
-
// PINNED AND LIVE AUTH AGREE, or nothing (final authority closure): a
|
|
1030
|
-
// chain filed under one credential for its base entry, read beside a
|
|
1031
|
-
// live mode file that now says the other, is a scope whose signed
|
|
1032
|
-
// words no longer describe what would spend — a subscription chain
|
|
1033
|
-
// approved after the operator moved to an API key would bill the
|
|
1034
|
-
// key. It holds no authority until it is filed again under today's
|
|
1035
|
-
// mode.
|
|
1036
|
-
const base = chain === null ? null : chain[0] ?? null;
|
|
1037
|
-
if (base !== null && base.authMode !== authMode) {
|
|
1038
|
-
return {
|
|
1039
|
-
ok: false,
|
|
1040
|
-
reason: "auth-mode",
|
|
1041
|
-
problem: `the scope's fallback chain pins its base entry to ${base.authMode === "subscription" ? "your subscription" : "your API key"} for ${profile.provider}, but the live auth mode is now ${authMode === "subscription" ? "your subscription" : "your API key"} — re-file the scope under today's mode`,
|
|
1042
|
-
};
|
|
1043
|
-
}
|
|
1044
1009
|
}
|
|
1045
|
-
return { ok: true, profile,
|
|
1010
|
+
return { ok: true, profile, route, digest, authMode };
|
|
1046
1011
|
}
|
|
1047
1012
|
/** The rubric's approval-card lines (v39): one per criterion, the id in
|
|
1048
1013
|
* front (the exact string a proof must answer by) and its required
|
|
@@ -1075,32 +1040,13 @@ export function describeScope(scope, readiness = NO_READINESS) {
|
|
|
1075
1040
|
...(scope.budgetMicrousd === null
|
|
1076
1041
|
? []
|
|
1077
1042
|
: [` budget $${(scope.budgetMicrousd / 1_000_000).toFixed(2)} per build attempt — the agent is stopped at this figure`]),
|
|
1078
|
-
// The
|
|
1079
|
-
//
|
|
1080
|
-
// the WHOLE ordered chain — so the card says every entry, credential
|
|
1081
|
-
// included, before anyone signs.
|
|
1082
|
-
...chainWords(chainFromJson(scope.proposedChainJson ?? null)),
|
|
1083
|
-
// The ROUTE, in the words the yes agrees to (v47): risk, posture, and
|
|
1084
|
-
// every leg with its reason and readiness — before anyone signs.
|
|
1085
|
-
...(scope.riskLevel === undefined || scope.riskLevel === "routine" ? [] : [` risk ${scope.riskLevel}`]),
|
|
1043
|
+
// The ROUTE, in the words the yes agrees to (v47): posture and every
|
|
1044
|
+
// leg with its reason and readiness — before anyone signs.
|
|
1086
1045
|
...scopeRouteWords(scope, readiness),
|
|
1087
1046
|
` reference ${scope.digest}`,
|
|
1088
1047
|
` approved ${describeApproval(approval)}`,
|
|
1089
1048
|
];
|
|
1090
1049
|
}
|
|
1091
|
-
/** The chain's approval-card lines; empty for a single-profile scope. */
|
|
1092
|
-
export function chainWords(chain) {
|
|
1093
|
-
if (chain === null || chain.length < 2)
|
|
1094
|
-
return [];
|
|
1095
|
-
const credential = (mode) => mode === "subscription" ? "your subscription" : "your API key";
|
|
1096
|
-
const lines = [];
|
|
1097
|
-
const base = chain[0];
|
|
1098
|
-
lines.push(` runs on ${base.profile.provider} (${base.profile.model}) — ${credential(base.authMode)}`);
|
|
1099
|
-
for (const entry of chain.slice(1)) {
|
|
1100
|
-
lines.push(` if that runs out falls back to ${entry.profile.provider} (${entry.profile.model}) — ${credential(entry.authMode)}${entry.authMode === "api-key" ? "; spend moves to that account" : ""}`);
|
|
1101
|
-
}
|
|
1102
|
-
return lines;
|
|
1103
|
-
}
|
|
1104
1050
|
function describeApproval(approval) {
|
|
1105
1051
|
if (approval.approved)
|
|
1106
1052
|
return `yes, by ${approval.by} at ${approval.at}`;
|
|
@@ -1109,14 +1055,3 @@ function describeApproval(approval) {
|
|
|
1109
1055
|
}
|
|
1110
1056
|
return "no — nothing will build this until somebody agrees to it";
|
|
1111
1057
|
}
|
|
1112
|
-
/** Deterministic bytes: sorted keys, undefined dropped — the signed text. */
|
|
1113
|
-
export function attendedTermsJson(terms) {
|
|
1114
|
-
return canonicalJson(terms);
|
|
1115
|
-
}
|
|
1116
|
-
/** The composite digest one password signs (ruling 12), domain-separated. */
|
|
1117
|
-
export function attendedDigestOf(terms) {
|
|
1118
|
-
return createHash("sha256")
|
|
1119
|
-
.update(`standing-orders:attended:${attendedTermsJson(terms)}`)
|
|
1120
|
-
.digest("hex")
|
|
1121
|
-
.slice(0, 32);
|
|
1122
|
-
}
|
package/dist/serve.js
CHANGED
|
@@ -115,7 +115,7 @@ import { createFlowsHandlers } from './server/flows.js';
|
|
|
115
115
|
import { createPagesHandlers } from './server/pages.js';
|
|
116
116
|
import { createPeopleHandlers } from './server/people-tokens.js';
|
|
117
117
|
import { createSettingsHandlers } from './server/settings.js';
|
|
118
|
-
import {
|
|
118
|
+
import { BODY_CAP, chromeScript, decisionsFor, DEMO_BANNER, DEMO_BANNER_SHORT, escape, focusDocument, form, KBD_HELP, LEAD_BY_DEFAULT_FACT, loginHref, matchTaskPath, mateBrowserMessages, mateChatVersion, NO_PROJECT, NO_TOUCH_FRAGMENTS, NONCE_CAP, NONCE_TTL_MS, page, PersistentSessions, pinnedTheme, projectChatHref, QUEUE_VIEW, redactedPath, redirect, refuse, requestContext, respond, safeReturn, screen, SENSITIVE_INPUT, SESSION_ABSOLUTE_MS, SESSION_IDLE_MS, shell, SHUTDOWN_WAIT_MS, sidebarScript, SIGN_IN_LINK_MS, SIGN_IN_LINK_PATH, ssoStepUps, TASK_FORM_BODY_CAP, taskChatHref, teamProposalCardParts, wrongHostPage } from "./server/shared.js";
|
|
119
119
|
import { createTasksHandlers } from './server/tasks.js';
|
|
120
120
|
import { DEFAULT_GUARD_POLICY, passwordGuardOf, provenPasswordAccount, SourceBudget, withPasswordSource } from "./sign-in-guard.js";
|
|
121
121
|
import { sourceKey } from "./source-key.js";
|
|
@@ -738,7 +738,7 @@ export function createDecisionServer(options) {
|
|
|
738
738
|
}
|
|
739
739
|
return handlePost(route, url, who, request, response, body);
|
|
740
740
|
};
|
|
741
|
-
if (method === "GET"
|
|
741
|
+
if (method === "GET")
|
|
742
742
|
return projectAuthority.run({ actor: who.name, repo: target.repo }, execute);
|
|
743
743
|
// Request acceptance is separate from durable work completion. Never
|
|
744
744
|
// persist a body, query string, token, password, or arbitrary URL.
|
|
@@ -797,18 +797,16 @@ export function createDecisionServer(options) {
|
|
|
797
797
|
const task = matchTaskPath(url.pathname, "(?:/([a-z-]+))?$");
|
|
798
798
|
if (task !== null)
|
|
799
799
|
return { repo: store.lookupRef(task.taskId)?.repo ?? null, taskId: task.taskId, runId: null, action: `task ${task.verb || "view"}` };
|
|
800
|
-
const resource = /^\/(r|d|i
|
|
800
|
+
const resource = /^\/(r|d|i)\/([0-9]{1,15})(?:\/([a-z-]+)(?:\/[0-9]+)?)?$/.exec(url.pathname);
|
|
801
801
|
if (resource !== null) {
|
|
802
802
|
const id = Number(resource[2]);
|
|
803
|
-
if (resource[1] === "routines")
|
|
804
|
-
return { repo: store.getRoutine(id)?.repo ?? null, taskId: null, runId: null, action: `routine ${resource[3] ?? "view"}` };
|
|
805
803
|
const runId = resource[1] === "r" ? id : resource[1] === "d" ? store.getDecision(id)?.run ?? null : store.openIncidents().find(one => one.id === id)?.run ?? null;
|
|
806
804
|
const run = runId === null ? null : store.getRun(runId);
|
|
807
805
|
const ref = run === null ? null : store.refForId(run.taskRef);
|
|
808
806
|
return { repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId, action: `${resource[1] === "d" ? "decision" : resource[1] === "i" ? "incident" : "run"} ${resource[3] ?? "view"}` };
|
|
809
807
|
}
|
|
810
|
-
const known = new Set(["/recipes/prepare", "/recipes/preview", "/recipes/import", "/recipes/save", "/recipes/launch", "/tasks/add", "/queue/move", "/queue/note", "/
|
|
811
|
-
const placed =
|
|
808
|
+
const known = new Set(["/recipes/prepare", "/recipes/preview", "/recipes/import", "/recipes/save", "/recipes/launch", "/tasks/add", "/queue/move", "/queue/note", "/people/invite", "/people/invite-revoke", "/people/revoke", "/people/projects", "/projects/select", "/projects/open", "/mode/confirm", "/mode/sign", "/mode/revoke", "/settings/chat-approval/save", "/settings/chat-approval/off"]);
|
|
809
|
+
const placed = url.pathname === "/tasks/add" ? body?.get("repo")?.trim() : null;
|
|
812
810
|
return { repo: url.pathname.startsWith("/people/") ? null : placed ? canonicalProject(placed) ?? placed : projectOf(who, request) ?? null, taskId: null, runId: null,
|
|
813
811
|
action: known.has(url.pathname) ? url.pathname.slice(1).replaceAll("/", " ") : "console request" };
|
|
814
812
|
}
|
|
@@ -1170,7 +1168,7 @@ export function createDecisionServer(options) {
|
|
|
1170
1168
|
{ label: "board", href: "/board?scope=all" },
|
|
1171
1169
|
{ label: "queue", href: QUEUE_VIEW },
|
|
1172
1170
|
{ label: "workbench", href: "/workbench" },
|
|
1173
|
-
{ label: "
|
|
1171
|
+
{ label: "flows", href: "/flows" },
|
|
1174
1172
|
{ label: "done", href: "/done" },
|
|
1175
1173
|
{ label: "review cockpit", href: "/review" },
|
|
1176
1174
|
{ label: "task list", href: "/tasks" },
|
|
@@ -1188,7 +1186,7 @@ export function createDecisionServer(options) {
|
|
|
1188
1186
|
entries.push({ label: `${one.id} — ${one.title}`, href: `/t/${encodeURIComponent(one.id)}` });
|
|
1189
1187
|
}
|
|
1190
1188
|
const saturated = open.length > 200;
|
|
1191
|
-
const safeEntries = restricted() ? entries.filter(one => ["/work", "/work?view=needs-you", "/", "/board", "/
|
|
1189
|
+
const safeEntries = restricted() ? entries.filter(one => ["/work", "/work?view=needs-you", "/", "/board", "/flows", "/done", "/review", "/tasks", "/runs", "/projects", "/ledger"].includes(one.href) || one.href.startsWith("/t/")) : entries;
|
|
1192
1190
|
const json = JSON.stringify(saturated ? [...safeEntries, { label: "… more in the task list", href: "/tasks" }] : safeEntries)
|
|
1193
1191
|
.replace(/</g, "\\u003c");
|
|
1194
1192
|
return `<script type="application/json" id="palette-index">${json}</script>`;
|
|
@@ -1343,7 +1341,7 @@ export function createDecisionServer(options) {
|
|
|
1343
1341
|
// A page with its own view keeps its script; the chat page runs the result script.
|
|
1344
1342
|
const functional = conversation === null || extras.view ? (s.functional?.script ?? '') : RESULT_REVIEW_SCRIPT;
|
|
1345
1343
|
const document = shell(s.title, s.body, { chrome: s.chrome, sensitive });
|
|
1346
|
-
return page(response, status, browserWorkspaceDocument(document, workspace, nonce, functional +
|
|
1344
|
+
return page(response, status, browserWorkspaceDocument(document, workspace, nonce, functional + MOBILE_VIEWPORT_SCRIPT), nonce, true);
|
|
1347
1345
|
}
|
|
1348
1346
|
// A one-time secret on screen (a token, an invite link, a pairing code): the
|
|
1349
1347
|
// page carries no script of any kind, so it leaves the workspace for a
|
|
@@ -1352,13 +1350,9 @@ export function createDecisionServer(options) {
|
|
|
1352
1350
|
return page(response, status, focusDocument(s.title, s.body));
|
|
1353
1351
|
const chromeLayer = !sensitive && s.chrome !== undefined;
|
|
1354
1352
|
const functional = s.functional?.script ?? "";
|
|
1355
|
-
// Sensitive pages strip the palette and keys but keep the
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
const sensitiveChrome = sensitive && s.chrome !== undefined
|
|
1359
|
-
? beatScript(!restricted()) + sidebarScript()
|
|
1360
|
-
: "";
|
|
1361
|
-
const script = functional + (chromeLayer ? chromeScript(!restricted()) + WORKSPACE_MOTION_SCRIPT : sensitiveChrome);
|
|
1353
|
+
// Sensitive pages strip the palette and keys but keep the sidebar toggle.
|
|
1354
|
+
const sensitiveChrome = sensitive && s.chrome !== undefined ? sidebarScript() : "";
|
|
1355
|
+
const script = functional + (chromeLayer ? chromeScript() + WORKSPACE_MOTION_SCRIPT : sensitiveChrome);
|
|
1362
1356
|
const nonce = script === "" ? undefined : randomBytes(16).toString("base64");
|
|
1363
1357
|
const body = chromeLayer
|
|
1364
1358
|
? `${s.body}\n${paletteTagCached(s.chrome?.project ?? null)}\n${KBD_HELP}`
|
|
@@ -1370,8 +1364,8 @@ export function createDecisionServer(options) {
|
|
|
1370
1364
|
...(s.refreshSeconds === undefined ? {} : { refreshSeconds: s.refreshSeconds }),
|
|
1371
1365
|
...(nonce === undefined ? {} : { live: { nonce, script, fallbackRefresh: s.functional?.fetches === true } }),
|
|
1372
1366
|
});
|
|
1373
|
-
//
|
|
1374
|
-
//
|
|
1367
|
+
// Pages that ship the chrome layer keep connect-src (v28 granted it for
|
|
1368
|
+
// the since-removed watched-session beat), beside pages whose own
|
|
1375
1369
|
// functional script polls.
|
|
1376
1370
|
return page(response, status, html, nonce, s.functional?.fetches === true || chromeLayer || sensitiveChrome !== "");
|
|
1377
1371
|
}
|
|
@@ -1462,7 +1456,7 @@ export function createDecisionServer(options) {
|
|
|
1462
1456
|
: {
|
|
1463
1457
|
modeBanner: {
|
|
1464
1458
|
name: liveMode.name,
|
|
1465
|
-
words: `${liveMode.name} mode until ${liveMode.absoluteExpiry.slice(0, 16).replace("T", " ")} UTC${liveModeTerms.autoApproveFiling ? ` — every scope ${liveMode.signedBy} files builds without further ceremony` : ""}${liveModeTerms.permissionDefault === "escalated" ? " — FULL permissions by default" : ""}${liveModeTerms.
|
|
1459
|
+
words: `${liveMode.name} mode until ${liveMode.absoluteExpiry.slice(0, 16).replace("T", " ")} UTC${liveModeTerms.autoApproveFiling ? ` — every scope ${liveMode.signedBy} files builds without further ceremony` : ""}${liveModeTerms.permissionDefault === "escalated" ? " — FULL permissions by default" : ""}${liveModeTerms.publication === "automerge" ? " — merges fire themselves on green" : ""}`,
|
|
1466
1460
|
},
|
|
1467
1461
|
}),
|
|
1468
1462
|
...(!restricted() && !unscopedMode && managedRepos().length > 0 ? { chat: true } : {}),
|
|
@@ -1535,8 +1529,6 @@ export function createDecisionServer(options) {
|
|
|
1535
1529
|
};
|
|
1536
1530
|
async function handlePost(route, url, who, request, response, posted) {
|
|
1537
1531
|
const early = () => dispatchConsole(route, { url, who, request, response, route, now: clock(), project: projectOf(who, request) ?? null, chosenProject: projectOf(who, request) ?? null, posted });
|
|
1538
|
-
if (route.id === 'session.attended-beats')
|
|
1539
|
-
return early();
|
|
1540
1532
|
const denied = authorizeMutation(request, who, readForm(posted, CONSOLE_FORMS.mutationGuard));
|
|
1541
1533
|
if (denied !== null)
|
|
1542
1534
|
return refuse(response, who, denied.status, denied.message);
|
|
@@ -1550,8 +1542,7 @@ export function createDecisionServer(options) {
|
|
|
1550
1542
|
// THE CENTRAL VIEWER GATE (modes chain, D2/E2): consequential POSTs
|
|
1551
1543
|
// require ACTIVE approver standing — cookie and bearer alike — with
|
|
1552
1544
|
// an EXACT allowlist of session-local acts, never a prefix. /login,
|
|
1553
|
-
// /logout and /join answer before this handler
|
|
1554
|
-
// guards itself (approver-only) above.
|
|
1545
|
+
// /logout and /join answer before this handler.
|
|
1555
1546
|
if (who.role === "viewer") {
|
|
1556
1547
|
if (route.viewer !== true) {
|
|
1557
1548
|
return refuse(response, who, 403, "your login can watch, not act — ask an approver to upgrade you");
|
package/dist/server/chat.js
CHANGED
|
@@ -23,7 +23,7 @@ import { MATE_MESSAGE_MAX_CHARS, runMateTurn } from "../mate.js";
|
|
|
23
23
|
import { livePin, modelOptions } from "../model-catalog.js";
|
|
24
24
|
import { verifyApproverByPassword, verifyApproverStanding } from "../principal.js";
|
|
25
25
|
import { projectName } from "../project.js";
|
|
26
|
-
import {
|
|
26
|
+
import { fileTaskProposal } from "../proposal.js";
|
|
27
27
|
import { providerName } from "../provider-auth.js";
|
|
28
28
|
import { validModelId } from "../provider.js";
|
|
29
29
|
import { loadOrCreateVapidKeys, validatePushEndpoint } from "../push.js";
|
|
@@ -763,7 +763,7 @@ export function createChatHandlers(runtime) {
|
|
|
763
763
|
noteMate(who.session.csrf, null, "that proposal was already acted on");
|
|
764
764
|
return redirect(response, chatReturnWithLatest(back));
|
|
765
765
|
}
|
|
766
|
-
const outcome = confirmMateProposal(store, principal, id, now, { chatProvider: teamChatProvider, confirm: body.get("confirm") === "yes", via: "web", evidenceRoot,
|
|
766
|
+
const outcome = confirmMateProposal(store, principal, id, now, { chatProvider: teamChatProvider, confirm: body.get("confirm") === "yes", via: "web", evidenceRoot, ...(body.has("nonce") ? { actionReview: { nonce: body.get("nonce") ?? "", password: body.get("token") ?? "" } } : {}) });
|
|
767
767
|
if (cardJson) {
|
|
768
768
|
if (!outcome.ok)
|
|
769
769
|
return cardAnswer(outcome.reason === "standing" ? 403 : outcome.reason === "not-yours" ? 404 : 409, false, outcome.said);
|
|
@@ -1005,43 +1005,20 @@ export function createChatHandlers(runtime) {
|
|
|
1005
1005
|
// Re-validated at the act: the door runs every field check again, and
|
|
1006
1006
|
// the fields are scanned for secrets before they become durable.
|
|
1007
1007
|
const acceptanceFields = candidate.draft.acceptance.flatMap(c => [c.statement, ...(c.how === null ? [] : [c.how])]);
|
|
1008
|
-
const fields = candidate.draft.
|
|
1009
|
-
? [candidate.draft.title, candidate.draft.goal, candidate.draft.outOfScope ?? "", ...candidate.draft.touches, ...acceptanceFields]
|
|
1010
|
-
: [candidate.draft.name, candidate.draft.goal, candidate.draft.outOfScope ?? "", ...candidate.draft.touches, ...acceptanceFields];
|
|
1008
|
+
const fields = [candidate.draft.title, candidate.draft.goal, candidate.draft.outOfScope ?? "", ...candidate.draft.touches, ...acceptanceFields];
|
|
1011
1009
|
if (scanForSecrets(fields.join("\n")).length > 0) {
|
|
1012
1010
|
chat.candidates.delete(key);
|
|
1013
1011
|
return refuse(response, who, 400, "that draft contains something credential-shaped — discarded", "/chat");
|
|
1014
1012
|
}
|
|
1015
1013
|
const filedVia = `chat:${enabled.config.provider}`;
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
title: candidate.draft.title,
|
|
1019
|
-
repo: candidate.repoPath,
|
|
1020
|
-
goal: candidate.draft.goal,
|
|
1021
|
-
outOfScope: candidate.draft.outOfScope,
|
|
1022
|
-
touches: candidate.draft.touches,
|
|
1023
|
-
acceptance: candidate.draft.acceptance,
|
|
1024
|
-
filedVia, filedBy: { name: who.name, kind: "person" },
|
|
1025
|
-
admittedRepos: managedRepos(),
|
|
1026
|
-
}, now);
|
|
1027
|
-
if (!made.ok) {
|
|
1028
|
-
candidate.state = "pending";
|
|
1029
|
-
return refuse(response, who, 400, `the door refused it: ${made.message}`, "/chat");
|
|
1030
|
-
}
|
|
1031
|
-
chat.candidates.delete(key);
|
|
1032
|
-
return redirect(response, taskHref(made.id));
|
|
1033
|
-
}
|
|
1034
|
-
const made = fileRoutineProposal(store, {
|
|
1035
|
-
name: candidate.draft.name,
|
|
1014
|
+
const made = fileTaskProposal(store, {
|
|
1015
|
+
title: candidate.draft.title,
|
|
1036
1016
|
repo: candidate.repoPath,
|
|
1037
1017
|
goal: candidate.draft.goal,
|
|
1038
1018
|
outOfScope: candidate.draft.outOfScope,
|
|
1039
1019
|
touches: candidate.draft.touches,
|
|
1040
1020
|
acceptance: candidate.draft.acceptance,
|
|
1041
|
-
|
|
1042
|
-
schedule: candidate.draft.schedule,
|
|
1043
|
-
costCeilingUsd: null,
|
|
1044
|
-
filedVia, createdBy: who.name,
|
|
1021
|
+
filedVia, filedBy: { name: who.name, kind: "person" },
|
|
1045
1022
|
admittedRepos: managedRepos(),
|
|
1046
1023
|
}, now);
|
|
1047
1024
|
if (!made.ok) {
|
|
@@ -1049,7 +1026,7 @@ export function createChatHandlers(runtime) {
|
|
|
1049
1026
|
return refuse(response, who, 400, `the door refused it: ${made.message}`, "/chat");
|
|
1050
1027
|
}
|
|
1051
1028
|
chat.candidates.delete(key);
|
|
1052
|
-
return redirect(response,
|
|
1029
|
+
return redirect(response, taskHref(made.id));
|
|
1053
1030
|
}
|
|
1054
1031
|
const chatAckPost = /^\/chat\/ack\/([0-9]{1,15})$/.exec(url.pathname);
|
|
1055
1032
|
if (chatAckPost !== null) {
|
package/dist/server/flows.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import {} from "node:http";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
-
import {
|
|
6
|
+
import { resolveAgentAuthority } from "../agentconfig.js";
|
|
7
7
|
import { checkResponse, CONSOLE_FORMS, readForm } from "../contracts/console-api.js";
|
|
8
8
|
import { stageReferenceProblems } from "../contracts/stage-output.js";
|
|
9
9
|
import { LIMITS } from "../decision.js";
|
|
@@ -17,7 +17,7 @@ import { assignFlowCard, commentOnFlowCard, watchFlowCard } from "../flow-people
|
|
|
17
17
|
import { saveScript } from "../flow-scripts.js";
|
|
18
18
|
import { chooseFlowCard } from "../flow-send.js";
|
|
19
19
|
import { exportFlow, fetchFlowFile, FlowFileError, importFlow, parseFlowFile, planFlowImport } from "../flow-share.js";
|
|
20
|
-
import { addFlowTriggerTo, checkFlowTriggerNow, pressFlowButton, removeFlowTrigger, removeLinearKey, renewFlowHook, saveHooksBase, saveLinearKey, saveLinearSigningSecret, shareFlowButton, stopSharingFlowButton } from "../flow-triggers.js";
|
|
20
|
+
import { addFlowTriggerTo, checkFlowTriggerNow, pressFlowButton, removeFlowTrigger, removeLinearKey, setFlowTriggerOn, renewFlowHook, saveHooksBase, saveLinearKey, saveLinearSigningSecret, shareFlowButton, stopSharingFlowButton } from "../flow-triggers.js";
|
|
21
21
|
import { FLOW_IMPORT_SCRIPT, flowFallbackHtml, flowImportHtml, flowsListHtml, flowView } from "../flows-ui.js";
|
|
22
22
|
import { FLOW_TEMPLATES, FlowContractError, validateFlowDefinition, withZoneNames } from "../flows.js";
|
|
23
23
|
import { keyStatus } from "../keys.js";
|
|
@@ -169,11 +169,12 @@ export function createFlowsHandlers(runtime) {
|
|
|
169
169
|
const revision = who.via === "cookie" ? who.session.projectRevision : 0;
|
|
170
170
|
const render = (html) => sendScreen(response, 200, screen("Workflow recipes", html, { chrome: chromeFor(project, "recipes"), functional: { script: recipeScript() } }));
|
|
171
171
|
if (url.pathname === "/recipes") {
|
|
172
|
-
const recent = project === null ? [] : store.handle.prepare("SELECT document,task_id,
|
|
172
|
+
const recent = project === null ? [] : store.handle.prepare("SELECT document,task_id,flow_id FROM workflow_preview WHERE repo=? AND (task_id IS NOT NULL OR flow_id IS NOT NULL) ORDER BY created_at DESC LIMIT 12").all(project).map(row => {
|
|
173
173
|
const d = importRecipe(String(row["document"]));
|
|
174
174
|
const task = row["task_id"] === null ? null : store.getTask(String(row["task_id"]));
|
|
175
|
-
const
|
|
176
|
-
|
|
175
|
+
const flow = row["flow_id"] === null ? null : store.getFlow(Number(row["flow_id"]));
|
|
176
|
+
const on = flow !== null && flow.state === "active" && store.flowTriggers(flow.id).some(one => one.kind === "schedule" && one.state === "active");
|
|
177
|
+
return { name: d.name, href: task !== null ? taskHref(task.id) : `/flows/${Number(row["flow_id"])}`, state: task?.state ?? (flow === null || flow.state !== "active" ? "Archived" : on ? "Scheduled" : "Paused") };
|
|
177
178
|
});
|
|
178
179
|
return render(recipeLibraryHtml(starterRecipes(), project === null ? [] : savedRecipes(store, who.name, project), project, csrf, revision, recent));
|
|
179
180
|
}
|
|
@@ -215,7 +216,7 @@ export function createFlowsHandlers(runtime) {
|
|
|
215
216
|
if (url.pathname === "/recipes/preview" && preview !== null) {
|
|
216
217
|
if (preview.document.version === 2 || url.searchParams.get("purpose") === "recipe")
|
|
217
218
|
return render(recipeDefinitionPreviewHtml(preview, csrf, revision));
|
|
218
|
-
const agents =
|
|
219
|
+
const agents = resolveAgentAuthority(store, project, preview.document.acceptance, now);
|
|
219
220
|
const workers = store.listRunners().filter(one => one.retiredAt === null && one.repos.includes(project) && runnerAlive(one, now));
|
|
220
221
|
const mode = store.activeMode(project, now);
|
|
221
222
|
const readiness = [
|
|
@@ -498,7 +499,7 @@ export function createFlowsHandlers(runtime) {
|
|
|
498
499
|
return answer(404, { ok: false, said: "That trigger isn't on this flow." });
|
|
499
500
|
const verb = triggerPost[3];
|
|
500
501
|
if (verb === "pause" || verb === "resume") {
|
|
501
|
-
store
|
|
502
|
+
setFlowTriggerOn(store, trigger, verb === "resume", now);
|
|
502
503
|
return settle(verb === "pause" ? "Trigger paused." : "Trigger on again.");
|
|
503
504
|
}
|
|
504
505
|
if (verb === "remove") {
|
|
@@ -722,8 +723,8 @@ export function createFlowsHandlers(runtime) {
|
|
|
722
723
|
const sourceVersion = body.get("sourceRevision") ?? "1";
|
|
723
724
|
if (!/^[1-9][0-9]{0,8}$/.test(sourceVersion))
|
|
724
725
|
throw new RecipeError("Choose a valid recipe version.");
|
|
725
|
-
const original = ["custom", "imported", "task-copy"
|
|
726
|
-
if (original === null && !["custom", "imported", "task-copy"
|
|
726
|
+
const original = ["custom", "imported", "task-copy"].includes(source) ? null : findRecipe(store, who.name, project, source, Number(sourceVersion));
|
|
727
|
+
if (original === null && !["custom", "imported", "task-copy"].includes(source))
|
|
727
728
|
throw new RecipeError("That recipe version is not available in this project.", 404);
|
|
728
729
|
let document;
|
|
729
730
|
try {
|
|
@@ -753,7 +754,7 @@ export function createFlowsHandlers(runtime) {
|
|
|
753
754
|
if (context !== undefined)
|
|
754
755
|
context.createdTask = made.taskId;
|
|
755
756
|
}
|
|
756
|
-
return redirect(response, made.taskId === null ? `/
|
|
757
|
+
return redirect(response, made.taskId === null ? `/flows/${made.flowId}` : taskHref(made.taskId));
|
|
757
758
|
}
|
|
758
759
|
catch (error) {
|
|
759
760
|
if (error instanceof RecipeError)
|
package/dist/server/guards.js
CHANGED
|
@@ -152,7 +152,6 @@ export function createSharedGuards(runtime) {
|
|
|
152
152
|
case 'run': return runRepo(id);
|
|
153
153
|
case 'decision': return runRepo(runtime.store.getDecision(id)?.run);
|
|
154
154
|
case 'incident': return runRepo(runtime.store.openIncidents().find(one => one.id === id)?.run);
|
|
155
|
-
case 'routine': return runtime.store.getRoutine(id)?.repo ?? null;
|
|
156
155
|
case 'flow': return runtime.store.getFlow(id)?.repo ?? null;
|
|
157
156
|
case 'proposal':
|
|
158
157
|
case 'coding': return runtime.actionTarget(url, who, request, body === null ? null : readForm(body, CONSOLE_FORMS.ledgerTarget)).repo;
|
|
@@ -36,7 +36,7 @@ const PWA_MANIFEST = JSON.stringify({
|
|
|
36
36
|
// paths — the payload URL is data, revalidated, never handed raw to the
|
|
37
37
|
// browser (arc 3 finding 5).
|
|
38
38
|
const PWA_WORKER = `// Toolroll — push only; deliberately NO fetch handler (no offline cache of an authenticated console).
|
|
39
|
-
const SHAPES = [/^\\/next$/, /^\\/review$/, /^\\/system$/, /^\\/routines$/, /^\\/routines\\/[0-9]+$/, /^\\/d\\/[0-9]+$/, /^\\/
|
|
39
|
+
const SHAPES = [/^\\/next$/, /^\\/review$/, /^\\/system$/, /^\\/routines$/, /^\\/routines\\/[0-9]+$/, /^\\/d\\/[0-9]+$/, /^\\/r\\/[0-9]+$/];
|
|
40
40
|
self.addEventListener("push", function (event) {
|
|
41
41
|
var data = {};
|
|
42
42
|
try { data = event.data ? event.data.json() : {}; } catch (e) {}
|
package/dist/server/pages.js
CHANGED
|
@@ -320,7 +320,6 @@ export function createPagesHandlers(runtime) {
|
|
|
320
320
|
settings: options.telegramTokenFile !== undefined,
|
|
321
321
|
building: store.liveClaims(project, now),
|
|
322
322
|
runners: store.listRunners(),
|
|
323
|
-
heldSessions: new Map(store.openHeldSessions().reduce((by, one) => by.set(one.runner, (by.get(one.runner) ?? 0) + 1), new Map())),
|
|
324
323
|
worktrees: store
|
|
325
324
|
.listWorktrees()
|
|
326
325
|
.filter(one => project === null || sameRepo(one.repo, project)),
|
|
@@ -346,7 +345,6 @@ export function createPagesHandlers(runtime) {
|
|
|
346
345
|
}),
|
|
347
346
|
building: store.liveClaims(project, now),
|
|
348
347
|
runners: store.listRunners(),
|
|
349
|
-
heldSessions: new Map(store.openHeldSessions().reduce((by, one) => by.set(one.runner, (by.get(one.runner) ?? 0) + 1), new Map())),
|
|
350
348
|
worktrees: store.listWorktrees().filter(one => project === null || sameRepo(one.repo, project)),
|
|
351
349
|
episode: project === null ? null : store.latestWatchEpisode(project),
|
|
352
350
|
outboxPending: store.pendingForAttention().length,
|
|
@@ -73,7 +73,6 @@ export function createPeopleHandlers(runtime) {
|
|
|
73
73
|
`<p class="meta">Operators with all-project access also manage the instance.</p><fieldset><legend>Projects</legend>${projectChoices.length === 0 ? `<p class="meta">Add a project before granting selected access.</p>` : projectChoices.map(repo => `<label class="row"><input type="checkbox" name="projects" value="${escape(repo)}"${selected?.includes(repo) ? " checked" : ""}>${escape(projectName(repo))}</label>`).join("")}</fieldset>`;
|
|
74
74
|
const cards = accounts.map(one => {
|
|
75
75
|
const seen = lastSeenOf(one.name);
|
|
76
|
-
const attended = store.openAttendedOf(one.name);
|
|
77
76
|
const acts = store.recentActsOf(one.name);
|
|
78
77
|
const standing = one.revokedAt !== null
|
|
79
78
|
? `revoked ${escape(one.revokedAt.slice(0, 16).replace("T", " "))} by ${personChip(one.revokedBy ?? "?")}`
|
|
@@ -87,9 +86,6 @@ export function createPeopleHandlers(runtime) {
|
|
|
87
86
|
approverView && one.revokedAt === null ? `<details><summary>Edit project access</summary><form method="post" action="/people/projects">${hiddenFields({ csrf, name: one.name })}${accessFields(one.projects)}<label>Your password<input type="password" name="token" autocomplete="current-password" required></label><p class="meta">Changing access signs this person out and ends their derived sessions and modes. Previously approved work stays recorded.</p><button type="submit">Save project access</button></form></details>` : "",
|
|
88
87
|
`<p class="meta">${seen === null ? "not signed in right now" : `signed in \u2014 active ${escape(new Date(seen).toISOString().slice(11, 16))} UTC`} \u00b7 joined ${escape(one.addedAt.slice(0, 10))}</p>`,
|
|
89
88
|
`<p><a href="${escape(personHref(one.name))}">Tokens and remote activity</a></p>`,
|
|
90
|
-
attended.length === 0
|
|
91
|
-
? ""
|
|
92
|
-
: `<p class="row">watching now: ${attended.map(session => `<a href="/t/${escape(session.taskId)}">${escape(session.taskId)}</a>`).join(", ")}</p>`,
|
|
93
89
|
acts.length === 0
|
|
94
90
|
? `<p class="meta">No recorded acts yet</p>`
|
|
95
91
|
: acts.map(act => `<p class="row meta">${escape(act.kind)} ${escape(act.subject)} \u00b7 ${escape(act.at.slice(0, 16).replace("T", " "))}</p>`).join("\n"),
|
|
@@ -17,7 +17,7 @@ export type Caller = "anonymous" | "service" | "cookie" | "bearer" | "oauth" | "
|
|
|
17
17
|
/** What the caller must hold: nothing, read, act (an approver or an `act` token), or a fresh password/provider check on top. */
|
|
18
18
|
export type RouteScope = "none" | "read" | "act" | "step-up";
|
|
19
19
|
/** Where the request's project comes from. */
|
|
20
|
-
export type ProjectResolver = "none" | "session" | "form" | "form-path" | "task" | "run" | "decision" | "incident" | "
|
|
20
|
+
export type ProjectResolver = "none" | "session" | "form" | "form-path" | "task" | "run" | "decision" | "incident" | "flow" | "proposal" | "conversation" | "coding" | "adapter";
|
|
21
21
|
/**
|
|
22
22
|
* What a project-limited account may do here:
|
|
23
23
|
* - `deny`: instance access only.
|
|
@@ -120,7 +120,6 @@ get("tasks", "runs", "/runs", { limited: "collection" });
|
|
|
120
120
|
get("pages", "menu", "/menu", { limited: "collection", ...free });
|
|
121
121
|
get("tasks", "run.page", `^\\/r\\/${ID}$`, { project: "run", limited: "resource", ...free, sample: "/r/1" });
|
|
122
122
|
get("tasks", "run.evidence", `^\\/r\\/${ID}\\/evidence\\/${ID}$`, { project: "run", limited: "resource", ...free, sample: "/r/1/evidence/2" });
|
|
123
|
-
get("tasks", "contest.page", `^\\/contest\\/${ID}$`, { ...free, sample: "/contest/1" });
|
|
124
123
|
get("pages", "caps", "/caps");
|
|
125
124
|
get("flows", "flows.new", "/flows/new", { limited: "resource", ...free });
|
|
126
125
|
get("flows", "flows.gallery", "^\\/flows\\/new\\/[a-z-]{1,40}$", { limited: "resource", ...free, sample: "/flows/new/triage" });
|
|
@@ -139,13 +138,14 @@ for (const one of ["run", "start", "new", "edit", "from-task", "preview", "expor
|
|
|
139
138
|
get("flows", `recipes.${one}`, `/recipes/${one}`, { limited: "collection" });
|
|
140
139
|
// Any other recipe screen answers the recipe area's own refusal.
|
|
141
140
|
get("flows", "recipes.other", "^\\/recipes\\/.*$", { sample: "/recipes/unknown" });
|
|
141
|
+
// v115: routines became scheduled flows; old links to them land on the flows page.
|
|
142
142
|
get("tasks", "routines", "/routines", { limited: "collection" });
|
|
143
143
|
get("chat", "chat.stream", "/chat/stream", free);
|
|
144
144
|
get("chat", "chat.mate-status", "/chat/mate/status", free);
|
|
145
145
|
get("chat", "chat.demo-live", "/chat/demo/live", free);
|
|
146
146
|
get("chat", "chat.page", "/chat", { project: "conversation", limited: "conversation", ...free });
|
|
147
147
|
get("chat", "chat.ack", `^\\/chat\\/ack\\/${ID}$`, { sample: "/chat/ack/1" });
|
|
148
|
-
get("tasks", "routine.page", `^\\/routines\\/${ID}$`, {
|
|
148
|
+
get("tasks", "routine.page", `^\\/routines\\/${ID}$`, { limited: "resource", sample: "/routines/1" });
|
|
149
149
|
get("settings", "control", "/control");
|
|
150
150
|
get("settings", "control.connection", "/control/connection");
|
|
151
151
|
get("chat", "chat.action", `^\\/chat\\/action\\/${ID}$`, { project: "proposal", limited: "proposal", ...free, sample: "/chat/action/1" });
|
|
@@ -166,8 +166,6 @@ get("chat", "lead.status", "/lead/status", free);
|
|
|
166
166
|
get("chat", "chat.task-status", "/chat/task-status", free);
|
|
167
167
|
get("tasks", "decision.evidence", `^\\/d\\/${ID}\\/evidence\\/${ID}$`, { project: "decision", limited: "resource", ...free, sample: "/d/1/evidence/2" });
|
|
168
168
|
// ---- console actions -------------------------------------------------------------------------------------------
|
|
169
|
-
// The attended beat guards itself (cookie, form type, Sec-Fetch-Site) before the shared mutation guard.
|
|
170
|
-
post("tasks", "session.attended-beats", "/session/attended-beats", { callers: ["cookie"], scopeRefusal: "your login can watch, not keep sessions alive", callerRefusal: { status: 403, type: "text/plain; charset=utf-8", body: "watching is a browser session's act" } });
|
|
171
169
|
// The scripted demo: the handler answers a non-browser caller "no page here".
|
|
172
170
|
post("chat", "chat.demo", "^\\/chat\\/demo\\/(ask|[0-9]{1,9}\\/(approve|change|revise|complete))$", { sample: "/chat/demo/ask" });
|
|
173
171
|
post("settings", "provider.resume", "^\\/providers\\/[a-z]+\\/resume$", { sample: "/providers/codex/resume" });
|
|
@@ -243,12 +241,6 @@ post("people", "people.revoke", "/people/revoke", { scope: "step-up" });
|
|
|
243
241
|
post("pages", "fleet.retire", "/fleet/runner/retire", { scope: "step-up" });
|
|
244
242
|
// Answering a parked decision needs act access and irreversible-choice confirmation, not password step-up.
|
|
245
243
|
post("tasks", "decision.answer", `^\\/d\\/${ID}\\/answer$`, { project: "decision", limited: "resource", scope: "act", sample: "/d/1/answer" });
|
|
246
|
-
post("tasks", "contest.act.arm", `^\\/contest\\/${ID}\\/arm$`, { sample: "/contest/1/arm" });
|
|
247
|
-
post("tasks", "contest.act.pick", `^\\/contest\\/${ID}\\/pick$`, { scope: "step-up", sample: "/contest/1/pick" });
|
|
248
|
-
post("tasks", "contest.act.abandon", `^\\/contest\\/${ID}\\/abandon$`, { scope: "step-up", sample: "/contest/1/abandon" });
|
|
249
|
-
post("tasks", "task.attend.attend-preview", `^\\/t\\/${TASK}\\/attend-preview$`, { callers: ["cookie"], project: "task", sample: "/t/one/attend-preview" });
|
|
250
|
-
post("tasks", "task.attend.attend", `^\\/t\\/${TASK}\\/attend$`, { scope: "step-up", project: "task", sample: "/t/one/attend" });
|
|
251
|
-
post("tasks", "task.attend.attend-revoke", `^\\/t\\/${TASK}\\/attend-revoke$`, { callers: ["cookie"], project: "task", sample: "/t/one/attend-revoke" });
|
|
252
244
|
post("tasks", "task.act.hold", `^\\/t\\/${TASK}\\/hold$`, { project: "task", limited: "resource", sample: "/t/one/hold" });
|
|
253
245
|
post("tasks", "task.act.unhold", `^\\/t\\/${TASK}\\/unhold$`, { project: "task", limited: "resource", sample: "/t/one/unhold" });
|
|
254
246
|
post("tasks", "task.act.requeue", `^\\/t\\/${TASK}\\/requeue$`, { project: "task", limited: "resource", sample: "/t/one/requeue" });
|
|
@@ -291,15 +283,8 @@ post("chat", "chat.ack-send", `^\\/chat\\/ack\\/${ID}$`, { scope: "step-up", sam
|
|
|
291
283
|
for (const one of ["prepare", "preview", "import", "save"])
|
|
292
284
|
post("flows", `recipes.${one}-send`, `/recipes/${one}`, { limited: "collection" });
|
|
293
285
|
post("flows", "recipes.launch-send", "/recipes/launch", { limited: "collection" });
|
|
294
|
-
post("tasks", "routines.add", "/routines/add", { project: "form", limited: "collection" });
|
|
295
|
-
post("tasks", "routine.act.approve", `^\\/routines\\/${ID}\\/approve$`, { scope: "step-up", project: "routine", limited: "resource", sample: "/routines/1/approve" });
|
|
296
|
-
post("tasks", "routine.act.refresh", `^\\/routines\\/${ID}\\/refresh$`, { project: "routine", limited: "resource", sample: "/routines/1/refresh" });
|
|
297
|
-
post("tasks", "routine.act.pause", `^\\/routines\\/${ID}\\/pause$`, { project: "routine", limited: "resource", sample: "/routines/1/pause" });
|
|
298
|
-
post("tasks", "routine.act.resume", `^\\/routines\\/${ID}\\/resume$`, { project: "routine", limited: "resource", sample: "/routines/1/resume" });
|
|
299
|
-
post("tasks", "routine.act.run-now", `^\\/routines\\/${ID}\\/run-now$`, { scope: "step-up", project: "routine", limited: "resource", sample: "/routines/1/run-now" });
|
|
300
286
|
post("tasks", "run.act", `^\\/r\\/${ID}\\/(note|comment|revise|draft-repair|checks|add-tests)$`, { project: "run", limited: "resource", sample: "/r/1/note" });
|
|
301
287
|
post("tasks", "session.editor-links", "/session/editor-links", { viewer: true });
|
|
302
|
-
post("tasks", "run.turn", `^\\/r\\/${ID}\\/turn$`, { project: "run", sample: "/r/1/turn" });
|
|
303
288
|
post("tasks", "incident.resolve", `^\\/i\\/${ID}\\/resolve$`, { project: "incident", sample: "/i/1/resolve" });
|
|
304
289
|
/** The sole route table, in match order. */
|
|
305
290
|
export const ROUTES = Object.freeze(rows.map(one => Object.freeze(one)));
|