toolroll 0.9.57 → 0.9.59
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 +15 -0
- package/dist/action-ledger.js +10 -5
- package/dist/actor.d.ts +0 -10
- package/dist/actor.js +1 -18
- package/dist/api-tokens.d.ts +33 -2
- package/dist/api-tokens.js +39 -11
- package/dist/assignment-delivery.js +1 -1
- package/dist/assignment-status.js +1 -1
- package/dist/assignment.js +3 -2
- package/dist/browser/workspace.js +1 -1
- package/dist/builder.js +4 -2
- package/dist/chat-channel.d.ts +1 -0
- package/dist/chat-channel.js +3 -0
- package/dist/chat-delivery.js +2 -0
- package/dist/cli-http.d.ts +7 -0
- package/dist/cli-http.js +30 -4
- package/dist/cli.js +2 -1
- package/dist/contract-changes.d.ts +52 -0
- package/dist/contract-changes.js +61 -0
- package/dist/coordinator.d.ts +2 -1
- package/dist/coordinator.js +22 -31
- package/dist/decision.d.ts +4 -0
- package/dist/decision.js +8 -0
- package/dist/deploy-lock.d.ts +8 -0
- package/dist/deploy-lock.js +45 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/dispatch.js +1 -1
- package/dist/export.js +2 -2
- package/dist/lead-cli.js +3 -0
- package/dist/lead-doors.js +3 -0
- package/dist/lead-tools.js +2 -1
- package/dist/mcp-http.js +5 -5
- package/dist/needs-you.js +3 -1
- package/dist/operate.js +23 -11
- package/dist/password-bearer.d.ts +27 -0
- package/dist/password-bearer.js +59 -0
- package/dist/planner-source.d.ts +23 -53
- package/dist/planner-source.js +43 -68
- package/dist/release-gate.d.ts +2 -3
- package/dist/release-gate.js +44 -16
- package/dist/remote-command.js +0 -2
- package/dist/remote-exec.d.ts +19 -0
- package/dist/remote-exec.js +9 -8
- package/dist/request-budget.d.ts +1 -1
- package/dist/scope.js +16 -1
- package/dist/serve.js +15 -0
- package/dist/server/edge-refusal.d.ts +2 -0
- package/dist/server/edge-refusal.js +8 -6
- package/dist/server/guards.js +5 -4
- package/dist/server/http.d.ts +2 -0
- package/dist/server/remote.js +6 -13
- package/dist/server/render-chat.js +10 -0
- package/dist/server/render-tasks.js +19 -9
- package/dist/server/route-table.js +0 -3
- package/dist/server/tasks.js +3 -1
- package/dist/session-cli.d.ts +38 -9
- package/dist/session-cli.js +141 -154
- package/dist/session-contract.js +2 -2
- package/dist/session-remote.d.ts +19 -0
- package/dist/session-remote.js +86 -0
- package/dist/store.d.ts +72 -12
- package/dist/store.js +367 -105
- package/dist/task-status.js +3 -1
- package/dist/team-cli.d.ts +3 -0
- package/dist/team-cli.js +23 -3
- package/dist/telegram-lead.js +6 -0
- package/dist/telegram.js +9 -7
- package/dist/toolroll-update.js +10 -1
- package/package.json +1 -1
- package/dist/session-http.d.ts +0 -24
- package/dist/session-http.js +0 -170
- package/dist/session-server.d.ts +0 -17
- package/dist/session-server.js +0 -39
package/dist/remote-exec.js
CHANGED
|
@@ -14,11 +14,12 @@ import { envelopeJson } from './envelope.js';
|
|
|
14
14
|
import { COMMAND_GUIDE } from './surface.js';
|
|
15
15
|
import { centralProfile, UsageError } from './team-cli.js';
|
|
16
16
|
import { CLI_FILES_BYTES, CLI_WAIT_SECONDS } from './cli-http.js';
|
|
17
|
+
import { TOKEN_REPLACEMENT } from './password-bearer.js';
|
|
17
18
|
import { contractRow, fileArguments, STEP_UP_MESSAGE } from './remote-command.js';
|
|
18
19
|
export { contractRow, STEP_UP_MESSAGE } from './remote-command.js';
|
|
19
20
|
const EXIT = { ok: 0, failed: 1, usage: 2, refused: 3 };
|
|
20
|
-
/** Team commands own their own central path (team-cli.ts). */
|
|
21
|
-
const TEAM_COMMANDS = new Set(['connect', 'lead', 'conversation', 'chat', 'brief']);
|
|
21
|
+
/** Team commands own their own central path (team-cli.ts); `session` sends its own checked request (session-cli.ts). */
|
|
22
|
+
const TEAM_COMMANDS = new Set(['connect', 'lead', 'conversation', 'chat', 'brief', 'session']);
|
|
22
23
|
const RESPONSE_BYTES = 16 * 1024 * 1024;
|
|
23
24
|
const REQUEST_TIMEOUT_MS = (CLI_WAIT_SECONDS + 35) * 1_000;
|
|
24
25
|
async function bounded(response) {
|
|
@@ -45,7 +46,7 @@ async function bounded(response) {
|
|
|
45
46
|
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks)));
|
|
46
47
|
}
|
|
47
48
|
/** One request, never retried. */
|
|
48
|
-
async function
|
|
49
|
+
export async function postCli(origin, token, request, options) {
|
|
49
50
|
try {
|
|
50
51
|
const timeout = AbortSignal.timeout(REQUEST_TIMEOUT_MS);
|
|
51
52
|
const response = await (options.fetch ?? fetch)(`${origin}/api/cli`, {
|
|
@@ -152,7 +153,7 @@ export async function maybeRunRemoteCommand(argv, options = {}) {
|
|
|
152
153
|
if (profile === null || !profile.apiToken) {
|
|
153
154
|
if (!explicit)
|
|
154
155
|
return null;
|
|
155
|
-
return refuse('usage',
|
|
156
|
+
return refuse('usage', `This profile has no API token. ${TOKEN_REPLACEMENT}, then connect again with it (toolroll connect <origin> --token-stdin).`, EXIT.usage);
|
|
156
157
|
}
|
|
157
158
|
if (row === null || mode === 'no') {
|
|
158
159
|
return refuse('usage', `${command} runs only on this computer. Drop --profile to run it here.`, EXIT.usage);
|
|
@@ -192,7 +193,7 @@ export async function maybeRunRemoteCommand(argv, options = {}) {
|
|
|
192
193
|
};
|
|
193
194
|
if (row?.invocation === 'task wait')
|
|
194
195
|
return waitRemotely(sent, files, profile, options, deliver);
|
|
195
|
-
return deliver(await
|
|
196
|
+
return deliver(await postCli(profile.origin, profile.token, { argv: sent, files }, options));
|
|
196
197
|
}
|
|
197
198
|
/**
|
|
198
199
|
* `task wait` holds the server for at most CLI_WAIT_SECONDS per request; the client keeps asking within the caller's
|
|
@@ -203,7 +204,7 @@ async function waitRemotely(argv, files, profile, options, deliver) {
|
|
|
203
204
|
const given = at < 0 ? null : Number(argv[at + 1]);
|
|
204
205
|
const base = at < 0 ? argv : argv.filter((_one, index) => index !== at && index !== at + 1);
|
|
205
206
|
if (given !== null && (!Number.isFinite(given) || given < 0))
|
|
206
|
-
return deliver(await
|
|
207
|
+
return deliver(await postCli(profile.origin, profile.token, { argv, files }, options));
|
|
207
208
|
const json = base.includes('--json');
|
|
208
209
|
const probe = json ? base : [...base, '--json'];
|
|
209
210
|
const started = Date.now();
|
|
@@ -212,7 +213,7 @@ async function waitRemotely(argv, files, profile, options, deliver) {
|
|
|
212
213
|
while (true) {
|
|
213
214
|
const remaining = given === null ? CLI_WAIT_SECONDS : Math.max(0, given - (Date.now() - started) / 1_000);
|
|
214
215
|
const slice = Math.min(CLI_WAIT_SECONDS, Math.floor(remaining * 1_000) / 1_000);
|
|
215
|
-
const outcome = await
|
|
216
|
+
const outcome = await postCli(profile.origin, profile.token, { argv: [...probe, '--timeout', String(slice)], files }, options);
|
|
216
217
|
if (outcome.kind !== 'answer')
|
|
217
218
|
return deliver(outcome);
|
|
218
219
|
let reason;
|
|
@@ -227,7 +228,7 @@ async function waitRemotely(argv, files, profile, options, deliver) {
|
|
|
227
228
|
if (json)
|
|
228
229
|
return deliver(outcome);
|
|
229
230
|
// Settled (or out of time): the server renders the final answer in the person's own format.
|
|
230
|
-
return deliver(await
|
|
231
|
+
return deliver(await postCli(profile.origin, profile.token, { argv: [...base, '--timeout', '0'], files }, options));
|
|
231
232
|
}
|
|
232
233
|
if (signal.aborted)
|
|
233
234
|
return deliver(outcome);
|
package/dist/request-budget.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Request budgets for person API tokens (v112): how many requests one `so_` token may make across all accepting routes.
|
|
3
|
-
* One budget per token, keyed by its
|
|
3
|
+
* One budget per token, keyed by its credential row id (a person's API token, v118) (never the secret, its name or an address), shared by those
|
|
4
4
|
* routes. A read token gets 120 requests in any sliding minute, an act token 30, and either 10,000 in a rolling day;
|
|
5
5
|
* an instance operator may override any of these for the installation or for one token in the console (step-up).
|
|
6
6
|
*
|
package/dist/scope.js
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
export { ACCEPTANCE_LIMITS, EVIDENCE_KINDS } from "./contracts/acceptance-terms.js";
|
|
3
3
|
import { currentPasswordSource, passwordGuardOf, provenPasswordAccount } from "./sign-in-guard.js";
|
|
4
4
|
import { validateScopeText } from "./task-text.js";
|
|
5
|
+
import { contractChangesOf } from "./contract-changes.js";
|
|
5
6
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
7
|
import { projectAuthority } from "./project-access.js";
|
|
7
8
|
import { activeRemote } from "./remote-run.js";
|
|
8
|
-
import { claimActor, currentActor
|
|
9
|
+
import { claimActor, currentActor } from "./actor.js";
|
|
10
|
+
import { parseLeadToken } from "./api-tokens.js";
|
|
9
11
|
/**
|
|
10
12
|
* What a task is allowed to become, agreed before anything builds it.
|
|
11
13
|
*
|
|
@@ -557,6 +559,19 @@ route) {
|
|
|
557
559
|
.slice(0, 32);
|
|
558
560
|
}
|
|
559
561
|
export function propose(store, input) {
|
|
562
|
+
return store.transact(() => {
|
|
563
|
+
const previous = store.getScope(input.taskId);
|
|
564
|
+
const saved = proposeLocked(store, input);
|
|
565
|
+
// A rewritten goal, exclusions, touches or rubric on a task that is planning starts its planning again (v119), in
|
|
566
|
+
// the same transaction: the next planner plans for the new terms, never from the old plan or its answers. An
|
|
567
|
+
// identical refile changes nothing; the planner's own handoff writes its draft without coming through here.
|
|
568
|
+
const ref = store.lookupRef(input.taskId);
|
|
569
|
+
if (ref !== null && ref.plan !== null && (previous === null || contractChangesOf(previous, saved).length > 0))
|
|
570
|
+
store.restartPlanning(ref.id, input.now);
|
|
571
|
+
return saved;
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
function proposeLocked(store, input) {
|
|
560
575
|
const { taskId, goal, outOfScope = null, touches = [], budgetMicrousd = null, acceptance = [], qualityMode = "default", now, mutation = {}, profile, permissionMode, posture, proposedVia = null } = input;
|
|
561
576
|
const draft = { goal, outOfScope, touches: [...touches], budgetMicrousd, acceptance: [...acceptance], qualityMode, candidate: input.candidate ?? null };
|
|
562
577
|
const previous = store.getScope(taskId);
|
package/dist/serve.js
CHANGED
|
@@ -16,6 +16,7 @@ import { MOBILE_VIEWPORT_SCRIPT } from "./mobile-viewport.js";
|
|
|
16
16
|
import { projectAuthority } from "./project-access.js";
|
|
17
17
|
import { discoverTools, projectToolsOf, secretsSetFor, TOOL_CATALOG } from "./project-tools.js";
|
|
18
18
|
import { isEdgeAddress, refuseEdge } from "./server/edge-refusal.js";
|
|
19
|
+
import { announcePasswordBearer, isPasswordBearer, PASSWORD_BEARER_REFUSED, passwordBearerAccepted } from "./password-bearer.js";
|
|
19
20
|
import { createSharedGuards } from "./server/guards.js";
|
|
20
21
|
import { createHandlerRegistry } from "./server/handler-registry.js";
|
|
21
22
|
import {} from "./server/remote-hooks.js";
|
|
@@ -606,6 +607,17 @@ export function createDecisionServer(options) {
|
|
|
606
607
|
const matchedRoute = matchRoute(method, hook.pathname);
|
|
607
608
|
const edgeRoute = matchedRoute?.stage === 'edge' ? matchedRoute : null;
|
|
608
609
|
const wrongMethodEdge = edgeRoute === null ? ROUTES.find(row => row.stage === 'edge' && new RegExp(row.pattern).test(hook.pathname)) ?? null : null;
|
|
610
|
+
// D7: a password on a request still signs in this release, announced on every answer and named in every refusal;
|
|
611
|
+
// from the release that removes it, it is refused here before anything checks it.
|
|
612
|
+
if (isPasswordBearer(request.headers.authorization)) {
|
|
613
|
+
if (!(options.passwordBearerAccepted ?? passwordBearerAccepted())) {
|
|
614
|
+
request.resume();
|
|
615
|
+
return hook.pathname.startsWith('/api/')
|
|
616
|
+
? respond(response, 401, 'application/json; charset=utf-8', JSON.stringify({ ...(hook.pathname.startsWith('/api/team') ? { version: 1 } : {}), ok: false, code: 'password-bearer-refused', message: PASSWORD_BEARER_REFUSED }))
|
|
617
|
+
: respond(response, 401, 'text/plain; charset=utf-8', PASSWORD_BEARER_REFUSED);
|
|
618
|
+
}
|
|
619
|
+
announcePasswordBearer(response);
|
|
620
|
+
}
|
|
609
621
|
if (edgeRoute?.host === 'before')
|
|
610
622
|
return dispatchEdge(edgeRoute, { url: hook, who: null, request, response, method });
|
|
611
623
|
if (wrongMethodEdge?.host === 'before')
|
|
@@ -667,6 +679,9 @@ export function createDecisionServer(options) {
|
|
|
667
679
|
if (edgeRoute?.domain === 'people')
|
|
668
680
|
return provenPasswordAccount.run(passwordProof, () => dispatchEdge(edgeRoute, { url, who, request, response, method }));
|
|
669
681
|
if (who === null) {
|
|
682
|
+
// A password on a request that did not sign in is a machine's refusal, never a login page.
|
|
683
|
+
if (isPasswordBearer(request.headers.authorization))
|
|
684
|
+
return respond(response, 401, "text/plain; charset=utf-8", "authenticate first");
|
|
670
685
|
// A GET to an exact task or result (a phone's deep link) signs in and
|
|
671
686
|
// comes back to it — the destination is a same-site path only.
|
|
672
687
|
return method === "GET"
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
2
|
import { type RouteDeclaration } from './route-table.js';
|
|
3
|
+
/** What a request to the retired /api/sessions transport hears (D7). */
|
|
4
|
+
export declare const SESSIONS_MOVED: string;
|
|
3
5
|
export declare function isEdgeAddress(path: string): boolean;
|
|
4
6
|
/** Preserve each transport's refusal envelope without invoking an undeclared adapter. */
|
|
5
7
|
export declare function refuseEdge(request: IncomingMessage, response: ServerResponse, path: string, declared: RouteDeclaration | null): void;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { TOKEN_REPLACEMENT } from '../password-bearer.js';
|
|
2
2
|
import { ROUTES } from './route-table.js';
|
|
3
|
+
/** What a request to the retired /api/sessions transport hears (D7). */
|
|
4
|
+
export const SESSIONS_MOVED = `Session operations moved to /api/cli. Update toolroll, ${TOKEN_REPLACEMENT.charAt(0).toLowerCase()}${TOKEN_REPLACEMENT.slice(1)}, connect with it, and run toolroll session <operation>.`;
|
|
3
5
|
export function isEdgeAddress(path) {
|
|
4
6
|
return ROUTES.some(row => row.stage === 'edge' && new RegExp(row.pattern).test(path))
|
|
5
7
|
|| /^\/(?:oauth|hooks|api\/(?:sessions|team|cli))(?:\/|$)/.test(path)
|
|
@@ -15,13 +17,13 @@ export function refuseEdge(request, response, path, declared) {
|
|
|
15
17
|
type = 'application/json';
|
|
16
18
|
body = JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32000, message: 'the MCP gateway takes POST only — one JSON-RPC message per request' } });
|
|
17
19
|
}
|
|
18
|
-
else if (path.startsWith('/api/sessions/')) {
|
|
19
|
-
|
|
20
|
+
else if (path === '/api/sessions' || path.startsWith('/api/sessions/')) {
|
|
21
|
+
// D7: session operations moved to /api/cli. An older client hears that nothing was sent, and what to use instead.
|
|
22
|
+
response.statusCode = 404;
|
|
20
23
|
type = 'application/json';
|
|
21
24
|
response.setHeader('X-Toolroll-Delivery', 'not-sent');
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
body = JSON.stringify({ version: 1, ...(spec ? { operation: spec.operation } : {}), ok: false, status: 'rejected', delivery: 'not-sent', retry: spec?.mutation === false ? 'safe-read' : 'never', reason: spec ? 'method-not-allowed' : 'unsupported-operation', message: spec ? 'Session operations require POST.' : 'This session operation is not supported.', nextActions: [] });
|
|
25
|
+
response.setHeader('X-Standing-Orders-Session-Delivery', 'not-sent');
|
|
26
|
+
body = JSON.stringify({ version: 1, ok: false, status: 'rejected', delivery: 'not-sent', retry: 'never', reason: 'moved', message: SESSIONS_MOVED, nextActions: [] });
|
|
25
27
|
}
|
|
26
28
|
else if (path === '/api/cli' || path.startsWith('/api/team')) {
|
|
27
29
|
type = 'application/json';
|
package/dist/server/guards.js
CHANGED
|
@@ -2,7 +2,7 @@ import { OWNER_ACTIONS, sharedActionPayload } from '../chat-actions.js';
|
|
|
2
2
|
import { CONSOLE_FORMS, readForm } from "../contracts/console-api.js";
|
|
3
3
|
import { timingSafeEqual } from "node:crypto";
|
|
4
4
|
import {} from "node:http";
|
|
5
|
-
import {
|
|
5
|
+
import { tokenLive, tokenProjects } from "../api-tokens.js";
|
|
6
6
|
import { oauthProjects, oauthTokenAllowed } from "../mcp-oauth.js";
|
|
7
7
|
import { REMOTE_MESSAGES, reproveRemote } from "../operate-remote.js";
|
|
8
8
|
import { canonicalProject, rowVisible } from "../project.js";
|
|
@@ -264,9 +264,10 @@ export function createSharedGuards(runtime) {
|
|
|
264
264
|
const source = runtime.joinSourceOf(request), at = Date.now();
|
|
265
265
|
if (runtime.signInBudget.waitFor(source, at) > 0)
|
|
266
266
|
return null;
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
267
|
+
// The one credential check (api-tokens.ts): only a person's API token signs in here.
|
|
268
|
+
const found = runtime.store.presentedCredential(presented);
|
|
269
|
+
const kept = found?.kind === "person" ? runtime.store.apiTokenSecret(found.id) : null;
|
|
270
|
+
if (kept === null) {
|
|
270
271
|
runtime.signInBudget.failed(source, at);
|
|
271
272
|
return null;
|
|
272
273
|
}
|
package/dist/server/http.d.ts
CHANGED
|
@@ -43,6 +43,8 @@ export type ServeOptions = {
|
|
|
43
43
|
cliRunner?: RunOperateAs;
|
|
44
44
|
/** Tests: remote command metadata until the shared contract declares it. */
|
|
45
45
|
cliModeOf?: CliHttpOptions['modeOf'];
|
|
46
|
+
/** Tests: whether a password on a request is still accepted (default: this release's version, password-bearer.ts). */
|
|
47
|
+
passwordBearerAccepted?: boolean;
|
|
46
48
|
/** Tests: the clock request budgets for API tokens count by (request-budget.ts). */
|
|
47
49
|
requestBudgetClock?: () => number;
|
|
48
50
|
/** Where repos.json lives — every enrollment locks exactly this file. */
|
package/dist/server/remote.js
CHANGED
|
@@ -9,7 +9,7 @@ import { reproveRemote } from "../operate-remote.js";
|
|
|
9
9
|
import { rowVisible } from "../project.js";
|
|
10
10
|
import { RequestBudget, SourceAdmission } from "../request-budget.js";
|
|
11
11
|
import { authenticateAccount } from "../scope.js";
|
|
12
|
-
import {
|
|
12
|
+
import { createSessionRunner } from '../session-remote.js';
|
|
13
13
|
import {} from "../store.js";
|
|
14
14
|
import { handleTeamHttp, TEAM_PASSWORD_REVERIFY_MS } from '../team-http.js';
|
|
15
15
|
import { handleTeamsHttp } from "../teams.js";
|
|
@@ -97,16 +97,17 @@ export function createRemoteHandlers(runtime) {
|
|
|
97
97
|
return who?.via === 'cookie' ? teamBrowserReply(reply, actor, who.session.csrf) : reply;
|
|
98
98
|
},
|
|
99
99
|
});
|
|
100
|
+
// D7: native session operations are remote commands too (`session …` over /api/cli), run by the session owner.
|
|
101
|
+
const sessionRunner = createSessionRunner({ store, workspace: coding, projects: codingProjects, projectAllowed: repo => rowVisible(liveCeiling(), repo), clock });
|
|
100
102
|
// Remote CLI: one live API token names the person; the shared command boundary decides everything else.
|
|
101
103
|
const cliEndpoint = (request, response) => handleCliHttp(request, response, {
|
|
102
104
|
admit: principal => requestBudget.admit(principal.tokenId, "api"),
|
|
103
105
|
authenticate: tokenPrincipalOf,
|
|
104
106
|
run: async () => options.cliRunner ?? (await import("../operate.js")).runOperateAs ?? null,
|
|
105
107
|
modeOf: options.cliModeOf,
|
|
108
|
+
session: sessionRunner,
|
|
106
109
|
store,
|
|
107
110
|
});
|
|
108
|
-
const sessionEndpoint = createSessionEndpoint({ store, workspace: coding, projects: codingProjects, projectAllowed: repo => rowVisible(liveCeiling(), repo),
|
|
109
|
-
admit: admitPasswordSource, admitAuthenticated: admitBearer });
|
|
110
111
|
const mcpHttp = createMcpHttp({ store, clock, evidenceRoot, ...(options.requestBudgetClock === undefined ? {} : { requestBudgetClock: options.requestBudgetClock }), signedIn: request => identify(request, true, true) !== null, admit: person => requestBudget.admit(person.principal.tokenId, "mcp"),
|
|
111
112
|
resourceMetadata: request => { const origin = consoleOrigin(request.headers.host); return origin === null ? null : resourceMetadataUrl(origin); },
|
|
112
113
|
enrolled: () => [...new Set([...managedRepos(), ...store.listProjects().map(project => project.path)])], ...(options.runOperateAs === undefined ? {} : { runAs: options.runOperateAs }) });
|
|
@@ -138,8 +139,8 @@ export function createRemoteHandlers(runtime) {
|
|
|
138
139
|
return;
|
|
139
140
|
return respond(response, 404, 'text/plain; charset=utf-8', 'No such address.');
|
|
140
141
|
};
|
|
141
|
-
// D5:
|
|
142
|
-
//
|
|
142
|
+
// D5: the central team service is deprecated (still served this release); each answer says so in a Deprecation
|
|
143
|
+
// header (RFC 9745).
|
|
143
144
|
// Each protocol adapter keeps its own operation boundary; every row still names its own entry.
|
|
144
145
|
const registrations = handlersOf("remote", {}, {
|
|
145
146
|
"edge.telegram-hook": async ({ request, response }) => { await telegramHook(hookContext, request, response); },
|
|
@@ -152,14 +153,6 @@ export function createRemoteHandlers(runtime) {
|
|
|
152
153
|
"edge.oauth-token": async ({ request, response, url }) => { await oauthHttp(request, response, url); },
|
|
153
154
|
"edge.oauth-authorize": async ({ request, response, url }) => { await oauthHttp(request, response, url); },
|
|
154
155
|
"edge.oauth-consent": async ({ request, response, url }) => { await oauthHttp(request, response, url); },
|
|
155
|
-
"edge.sessions-list": async ({ request, response }) => { response.setHeader('Deprecation', 'true'); await sessionEndpoint(request, response); },
|
|
156
|
-
"edge.sessions-show": async ({ request, response }) => { response.setHeader('Deprecation', 'true'); await sessionEndpoint(request, response); },
|
|
157
|
-
"edge.sessions-changes": async ({ request, response }) => { response.setHeader('Deprecation', 'true'); await sessionEndpoint(request, response); },
|
|
158
|
-
"edge.sessions-start": async ({ request, response }) => { response.setHeader('Deprecation', 'true'); await sessionEndpoint(request, response); },
|
|
159
|
-
"edge.sessions-send": async ({ request, response }) => { response.setHeader('Deprecation', 'true'); await sessionEndpoint(request, response); },
|
|
160
|
-
"edge.sessions-stop": async ({ request, response }) => { response.setHeader('Deprecation', 'true'); await sessionEndpoint(request, response); },
|
|
161
|
-
"edge.sessions-resume": async ({ request, response }) => { response.setHeader('Deprecation', 'true'); await sessionEndpoint(request, response); },
|
|
162
|
-
"edge.sessions-recover": async ({ request, response }) => { response.setHeader('Deprecation', 'true'); await sessionEndpoint(request, response); },
|
|
163
156
|
"edge.team-read": async ({ request, response }) => { response.setHeader('Deprecation', 'true'); await teamEndpoint(request, response); },
|
|
164
157
|
"edge.team-send": async ({ request, response }) => { response.setHeader('Deprecation', 'true'); await teamEndpoint(request, response); },
|
|
165
158
|
"edge.cli": async ({ request, response }) => { await cliEndpoint(request, response); },
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { assignmentSummaryHtml } from "../assignment-ui.js";
|
|
3
3
|
import {} from "../assignment.js";
|
|
4
4
|
import {} from "../browser-workspace.js";
|
|
5
|
+
import { closedQuestionReason } from "../decision.js";
|
|
5
6
|
import { CHAT_ACTIONS, sharedActionNeedsReview, sharedActionPayload, sharedActionReviewPath } from "../chat-actions.js";
|
|
6
7
|
import { CHAT_CONTINUITY_SCRIPT } from "../chat-continuity.js";
|
|
7
8
|
import { CHAT_CONTROLS, chatControlHref, isChatControl } from "../chat-controls.js";
|
|
@@ -478,6 +479,15 @@ export function proposalCardParts(view, inert, decision, returnTo = null) {
|
|
|
478
479
|
const text = (key) => (typeof payload[key] === "string" ? payload[key] : "");
|
|
479
480
|
const task = text("task");
|
|
480
481
|
const repoId = text("repoId");
|
|
482
|
+
if (view.kind === "answer" && decision?.supersededReason != null) {
|
|
483
|
+
// The proposal remains in history, but its question no longer needs an answer.
|
|
484
|
+
// Both the server page and browser card use the same closed view, without stale consent controls.
|
|
485
|
+
const body = html `<p><a href="/d/${decision.id}">${decision.question}</a></p><p class="meta">${closedQuestionReason(decision.supersededReason)}</p>`;
|
|
486
|
+
return {
|
|
487
|
+
html: html `<article class="card proposal proposal-answer" data-card-kind="answer"><header class="proposal-head"><strong>Question closed</strong></header><div class="proposal-body">${body}</div></article>`,
|
|
488
|
+
card: { id: view.id, kind: view.kind, label: "Question closed", state: view.state, body: htmlString(body), said: null, links: [], primary: null, dismissable: false, note: null },
|
|
489
|
+
};
|
|
490
|
+
}
|
|
481
491
|
const presentations = {
|
|
482
492
|
task: { label: payload["report"] === true ? "Scout investigation" : "New task", action: "file task", icon: html `<path d="M12 5v14"/><path d="M5 12h14"/>` },
|
|
483
493
|
next: { label: "Queue priority", action: "move to front", icon: html `<path d="M12 19V5"/><path d="m5 12 7-7 7 7"/>` },
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Task pages: approval, agents, plans, controls, decisions and the task body. */
|
|
2
2
|
import { html, htmlString, joinHtml, postForm, replaceMarkup } from "../html.js";
|
|
3
|
+
import { closedQuestionReason, closedQuestionWords } from "../decision.js";
|
|
3
4
|
import {} from "../activity-line.js";
|
|
4
5
|
import {} from "../agentconfig.js";
|
|
5
6
|
import { gateWords } from "../approval-policy.js";
|
|
@@ -1469,9 +1470,12 @@ export function taskBodyParts(data) {
|
|
|
1469
1470
|
!(data.revision != null && "problem" in data.revision) && scope.profileState !== "unresolved" && consentDoorOf(scope, data.route).open;
|
|
1470
1471
|
const planCard = data.planDocument === null
|
|
1471
1472
|
? data.plan === "requested"
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1473
|
+
// A request the planner cannot be given waits on a person; the status card above already says what to shorten.
|
|
1474
|
+
? data.dispatch?.code === "planner-source"
|
|
1475
|
+
? ""
|
|
1476
|
+
: data.revision != null && !("problem" in data.revision)
|
|
1477
|
+
? html `<div class="card planner-status"><span class="planner-orb" aria-hidden="true"></span><p><strong>Updating the plan</strong><span class="meta">Adding your notes to the plan. You approve any change before it builds.</span></p></div>`
|
|
1478
|
+
: html `<div class="card planner-status"><span class="planner-orb" aria-hidden="true"></span><p><strong>Planning requested</strong><span class="meta">${data.planAuto ? "Automatic approval is enabled for a verified plan that preserves your filed contract. Amendments and unanswered questions still pause." : "The agent is inspecting the repository and drafting the goal, acceptance criteria, and approach. It will ask only if a missing answer changes the work."}</span></p></div>`
|
|
1475
1479
|
: ""
|
|
1476
1480
|
: joinHtml([
|
|
1477
1481
|
approval.approved ? html `<details class="card planner-plan planner-plan-collapsed"><summary class="execution-plan-head">` : html `<section class="card planner-plan"><div class="execution-plan-head">`,
|
|
@@ -1788,7 +1792,7 @@ export function taskBodyParts(data) {
|
|
|
1788
1792
|
})();
|
|
1789
1793
|
const decisions = data.decisions.length === 0
|
|
1790
1794
|
? ""
|
|
1791
|
-
: html `<h2>Decisions</h2>${joinHtml(data.decisions.map(decision => html `<p class="row"><a href="/d/${decision.id}">${decision.question}</a> <span class="meta">${decision.state}${isOverdue(decision, data.now) ? " · overdue" : ""}</span></p>`), "\n")}`;
|
|
1795
|
+
: html `<h2>Decisions</h2>${joinHtml(data.decisions.map(decision => html `<p class="row"><a href="/d/${decision.id}">${decision.question}</a> <span class="meta">${decision.supersededReason !== null ? closedQuestionWords(decision.supersededReason) : decision.state}${isOverdue(decision, data.now) ? " · overdue" : ""}</span></p>`), "\n")}`;
|
|
1792
1796
|
const incidents = data.incidents.length === 0
|
|
1793
1797
|
? ""
|
|
1794
1798
|
: html `<h2>Incidents</h2>${joinHtml(data.incidents.map(one => one.resolvedAt === null
|
|
@@ -2068,7 +2072,11 @@ export function taskBodyParts(data) {
|
|
|
2068
2072
|
continue;
|
|
2069
2073
|
thread.push({ key: `question-${decision.id}`, at: decision.createdAt, kind: "question", who: "agent", author: "", title: "Asked",
|
|
2070
2074
|
text: decision.question, link: { label: "Question", href: `/d/${decision.id}` }, html: "", more: null });
|
|
2071
|
-
if (decision.
|
|
2075
|
+
if (decision.supersededReason !== null) {
|
|
2076
|
+
thread.push({ key: `answer-${decision.id}`, at: decision.answeredAt ?? decision.createdAt, kind: "progress", who: "agent", author: "", title: "Closed without an answer",
|
|
2077
|
+
text: closedQuestionReason(decision.supersededReason), link: null, html: "", more: null });
|
|
2078
|
+
}
|
|
2079
|
+
else if (decision.answeredAt !== null) {
|
|
2072
2080
|
const chosen = decision.options.find(one => one.id === decision.choice)?.label ?? decision.choice;
|
|
2073
2081
|
thread.push({ key: `answer-${decision.id}`, at: decision.answeredAt, kind: "reply", who: "person", author: decision.answeredBy ?? "You", title: "Answered",
|
|
2074
2082
|
text: [chosen, decision.note].filter((one) => one !== null && one !== "").join(" — ") || null, link: null, html: "", more: null });
|
|
@@ -2320,9 +2328,11 @@ export function decisionOptionForms(decision, returnTo) {
|
|
|
2320
2328
|
}
|
|
2321
2329
|
export function decisionPage(chrome, decision, taskId, artifacts, who, now, returnTo = null) {
|
|
2322
2330
|
const options = decisionOptionForms(decision, returnTo);
|
|
2323
|
-
const answered = decision.
|
|
2324
|
-
? html `<
|
|
2325
|
-
: ""
|
|
2331
|
+
const answered = decision.supersededReason !== null
|
|
2332
|
+
? html `<p class="meta">${closedQuestionReason(decision.supersededReason)}</p>`
|
|
2333
|
+
: decision.state === "answered"
|
|
2334
|
+
? html `<div class="answered">Answered: <strong>${decision.choice ?? ""}</strong> by ${decision.answeredBy ?? ""}${decision.note === null ? "" : ` — ${decision.note}`}</div>`
|
|
2335
|
+
: "";
|
|
2326
2336
|
const evidence = artifacts.length === 0
|
|
2327
2337
|
? ""
|
|
2328
2338
|
: joinHtml([
|
|
@@ -2331,7 +2341,7 @@ export function decisionPage(chrome, decision, taskId, artifacts, who, now, retu
|
|
|
2331
2341
|
html `</div>`,
|
|
2332
2342
|
]);
|
|
2333
2343
|
return screen(`decide · ${taskId}`, joinHtml([
|
|
2334
|
-
html `<h1>${taskId} <span class="badge badge-${decision.state}">${decision.state}</span>${isOverdue(decision, now) ? html ` <span class="badge badge-overdue">Overdue</span>` : ""}${decision.deadline === null ? "" : html ` <span class="meta">deadline ${decision.deadline}</span>`}</h1>`,
|
|
2344
|
+
html `<h1>${taskId} <span class="badge badge-${decision.state}">${decision.supersededReason !== null ? "closed" : decision.state}</span>${isOverdue(decision, now) ? html ` <span class="badge badge-overdue">Overdue</span>` : ""}${decision.deadline === null ? "" : html ` <span class="meta">deadline ${decision.deadline}</span>`}</h1>`,
|
|
2335
2345
|
html `<div class="recap">${decision.recap}</div>`,
|
|
2336
2346
|
html `<div class="question">${decision.question}</div>`,
|
|
2337
2347
|
decision.state === "answered" ? answered : options,
|
|
@@ -79,9 +79,6 @@ edge("remote", "edge.oauth-authorize", "GET,HEAD", "/oauth/authorize");
|
|
|
79
79
|
edge("remote", "edge.oauth-consent", "POST", "/oauth/authorize", { callers: ["cookie"], scope: "step-up", viewer: true, proof: "adapter" });
|
|
80
80
|
edge("settings", "edge.google-callback", "GET", "/settings/google/callback", { callers: ["service"], proof: "adapter" });
|
|
81
81
|
edge("settings", "edge.connect-callback", "GET", "/settings/tools/connected", { callers: ["service"], proof: "adapter" });
|
|
82
|
-
for (const operation of ["list", "show", "changes", "start", "send", "stop", "resume", "recover"]) {
|
|
83
|
-
edge("remote", `edge.sessions-${operation}`, "POST", `/api/sessions/${operation}`, { callers: ["bearer"], scope: ["list", "show", "changes"].includes(operation) ? "read" : "act", project: "adapter", proof: "adapter" });
|
|
84
|
-
}
|
|
85
82
|
edge("remote", "edge.team-read", "GET", "/api/team", { callers: ["cookie", "bearer"], scope: "read", project: "adapter", proof: "adapter" });
|
|
86
83
|
edge("remote", "edge.team-send", "POST", "/api/team", { callers: ["cookie", "bearer"], scope: "read", project: "adapter", proof: "adapter" });
|
|
87
84
|
edge("remote", "edge.cli", "POST", "/api/cli", { callers: ["bearer"], scope: "read", project: "adapter", proof: "adapter" });
|
package/dist/server/tasks.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { handlersOf } from './handler-registry.js';
|
|
2
|
+
import { closedQuestionWords } from "../decision.js";
|
|
2
3
|
import { DEPRECATED_PAGE } from "../deprecations.js";
|
|
3
4
|
/** tasks handlers, moved without changing their route bodies. */
|
|
4
5
|
import { html, htmlString, joinHtml, postForm, textHtml } from "../html.js";
|
|
@@ -1118,7 +1119,8 @@ export function createTasksHandlers(runtime) {
|
|
|
1118
1119
|
}, now);
|
|
1119
1120
|
if (!answered.ok) {
|
|
1120
1121
|
const status = answered.reason === "bad-option" || answered.reason === "bad-note" ? 400 : 409;
|
|
1121
|
-
const
|
|
1122
|
+
const closed = answered.reason === "closed" ? store.getDecision(id)?.supersededReason ?? null : null;
|
|
1123
|
+
const why = closed !== null ? closedQuestionWords(closed) : answered.reason === "already-answered" ? "already answered — somebody got there first" : answered.reason;
|
|
1122
1124
|
return refuse(response, who, status, why, requestedReturn === null ? `/d/${id}` : decisionBack);
|
|
1123
1125
|
}
|
|
1124
1126
|
return redirect(response, decisionBack);
|
package/dist/session-cli.d.ts
CHANGED
|
@@ -1,13 +1,42 @@
|
|
|
1
|
-
import { type SessionDescriptor, type
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { type SessionDescriptor, type SessionRequest, type SessionResponse } from './session-contract.js';
|
|
2
|
+
import { type TeamCliOptions } from './team-cli.js';
|
|
3
|
+
/**
|
|
4
|
+
* `toolroll session …` (D7): native session operations run on the central server over `POST /api/cli`, signed in with
|
|
5
|
+
* the API token `toolroll connect` saved, like every other remote command. The client checks and sends the exact
|
|
6
|
+
* request; the server (session-remote.ts) runs it through the existing session owner and answers one session envelope.
|
|
7
|
+
*/
|
|
8
|
+
export type SessionCliOptions = TeamCliOptions & {
|
|
5
9
|
readStdin?: () => Promise<string>;
|
|
6
10
|
stderr?: (line: string) => void;
|
|
7
11
|
};
|
|
8
|
-
|
|
9
|
-
export declare
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
/** The files key the prompt travels under: the server reads no paths, only what the request carries. */
|
|
13
|
+
export declare const SESSION_PROMPT_FILE = "prompt";
|
|
14
|
+
export declare const SESSION_CLI_ACTIONS: readonly ["capabilities", ...import("./session-contract.js").SessionOperation[]];
|
|
15
|
+
export declare const SESSION_TOKEN_REQUIRED = "Session commands use your saved API token. Create an API token with `toolroll tokens create`, then save it with `toolroll connect <origin> --as <account> --token-stdin`.";
|
|
16
|
+
/** The flags an operation takes: on this computer (where the prompt may come from a file or stdin, and a saved
|
|
17
|
+
* profile is chosen), or on the server (where the prompt is only ever the request's own file). */
|
|
18
|
+
export declare function sessionCliFlags(spec: SessionDescriptor, side?: 'client' | 'server'): Record<string, 'value' | 'flag'>;
|
|
19
|
+
export type SessionArguments = {
|
|
20
|
+
spec: SessionDescriptor;
|
|
21
|
+
flags: Record<string, string | true>;
|
|
22
|
+
positionals: string[];
|
|
23
|
+
};
|
|
24
|
+
/** One parser for both sides of /api/cli: `<operation> [<session-id>] --flag value …`. */
|
|
25
|
+
export declare function parseSessionArguments(argv: readonly string[], side: 'client' | 'server'): SessionArguments | {
|
|
26
|
+
problem: string;
|
|
27
|
+
};
|
|
28
|
+
/** The validated request the parsed arguments and prompt spell. */
|
|
29
|
+
export declare function sessionRequestOf({ spec, flags, positionals }: SessionArguments, prompt: string | undefined): {
|
|
30
|
+
ok: true;
|
|
31
|
+
request: SessionRequest;
|
|
32
|
+
} | {
|
|
33
|
+
ok: false;
|
|
34
|
+
message: string;
|
|
35
|
+
};
|
|
36
|
+
/** The exact argv the server runs for a validated request: always --json, the prompt as the request's one file. */
|
|
37
|
+
export declare function sessionArgv(spec: SessionDescriptor, request: SessionRequest): string[];
|
|
38
|
+
/** The plain-text answer, the same on either side of the transport. */
|
|
39
|
+
export declare function renderSessionResponse(response: SessionResponse): string;
|
|
40
|
+
/** 0 delivered · 1 unknown (inspect first) · 3 refused. */
|
|
41
|
+
export declare const sessionExitCode: (response: SessionResponse) => number;
|
|
13
42
|
export declare function runSessionCommand(argv: readonly string[], write: (line: string) => void, options?: SessionCliOptions): Promise<number>;
|