toolroll 0.9.42 → 0.9.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/dist/action-ledger.d.ts +5 -2
- package/dist/action-ledger.js +13 -3
- package/dist/chat-actions.js +1 -1
- package/dist/chat-approval.d.ts +45 -0
- package/dist/chat-approval.js +81 -0
- package/dist/chat-channel.js +5 -0
- package/dist/chat-decide.d.ts +18 -6
- package/dist/chat-decide.js +54 -20
- package/dist/chat-flow.js +2 -2
- package/dist/cli-http.d.ts +64 -0
- package/dist/cli-http.js +143 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +8 -1
- package/dist/contracts/cli.d.ts +13 -0
- package/dist/contracts/cli.js +7 -0
- package/dist/contracts/console-api.d.ts +1 -0
- package/dist/contracts/console-api.js +1 -0
- package/dist/contracts/gateway-tools.d.ts +82 -0
- package/dist/contracts/gateway-tools.js +33 -0
- package/dist/contracts/lead-tools.d.ts +11 -0
- package/dist/contracts/lead-tools.js +2 -0
- package/dist/desktop-update.d.ts +3 -1
- package/dist/desktop-update.js +7 -6
- package/dist/flow-engine.d.ts +2 -0
- package/dist/flow-engine.js +10 -2
- package/dist/flows-cli.d.ts +28 -0
- package/dist/flows-cli.js +43 -1
- package/dist/guides.js +2 -0
- package/dist/mate-contract.js +1 -1
- package/dist/mate-tools.js +43 -1
- package/dist/mcp-core.d.ts +110 -0
- package/dist/mcp-core.js +311 -0
- package/dist/mcp-http.d.ts +26 -0
- package/dist/mcp-http.js +151 -0
- package/dist/mcp-person.d.ts +31 -0
- package/dist/mcp-person.js +96 -0
- package/dist/mcp.d.ts +1 -8
- package/dist/mcp.js +21 -322
- package/dist/operate-remote-arguments.d.ts +36 -0
- package/dist/operate-remote-arguments.js +185 -0
- package/dist/operate-remote.d.ts +86 -0
- package/dist/operate-remote.js +125 -0
- package/dist/operate.d.ts +21 -1
- package/dist/operate.js +229 -15
- package/dist/process-recovery-eligibility.d.ts +1 -1
- package/dist/remote-command.d.ts +45 -0
- package/dist/remote-command.js +124 -0
- package/dist/remote-exec.d.ts +18 -0
- package/dist/remote-exec.js +241 -0
- package/dist/remote-run.d.ts +16 -0
- package/dist/remote-run.js +13 -0
- package/dist/scope.d.ts +4 -2
- package/dist/scope.js +10 -0
- package/dist/serve.d.ts +7 -0
- package/dist/serve.js +75 -2
- package/dist/store.d.ts +23 -2
- package/dist/store.js +76 -9
- package/dist/surface.d.ts +10 -3
- package/dist/surface.js +71 -3
- package/dist/team-cli.d.ts +19 -0
- package/dist/team-cli.js +20 -3
- package/dist/teammate-work.js +1 -1
- package/dist/telegram-flow.d.ts +41 -1
- package/dist/telegram-flow.js +96 -12
- package/dist/telegram.js +27 -3
- package/dist/toolroll-update.js +4 -1
- package/package.json +1 -1
package/dist/cli-http.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { contractRow, fileArguments, STEP_UP_MESSAGE } from './remote-command.js';
|
|
2
|
+
/** Inlined file contents, summed as UTF-8 bytes. */
|
|
3
|
+
export const CLI_FILES_BYTES = 256 * 1024;
|
|
4
|
+
/** The whole JSON body: the files plus argv and JSON escaping headroom. */
|
|
5
|
+
export const CLI_REQUEST_BYTES = 2 * CLI_FILES_BYTES;
|
|
6
|
+
const ARGV_MAX = 256;
|
|
7
|
+
const ARG_BYTES = 16 * 1024;
|
|
8
|
+
const FILES_MAX = 16;
|
|
9
|
+
function send(response, status, value) {
|
|
10
|
+
if (response.destroyed || response.writableEnded)
|
|
11
|
+
return;
|
|
12
|
+
response.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store', 'x-content-type-options': 'nosniff' });
|
|
13
|
+
response.end(JSON.stringify(value));
|
|
14
|
+
}
|
|
15
|
+
async function body(request) {
|
|
16
|
+
const chunks = [];
|
|
17
|
+
let size = 0;
|
|
18
|
+
for await (const chunk of request) {
|
|
19
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
20
|
+
size += bytes.length;
|
|
21
|
+
if (size > CLI_REQUEST_BYTES)
|
|
22
|
+
throw new Error('too-large');
|
|
23
|
+
chunks.push(bytes);
|
|
24
|
+
}
|
|
25
|
+
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks)));
|
|
26
|
+
}
|
|
27
|
+
/** Decode the request's shape and byte limits. The endpoint checks file references against its command policy. */
|
|
28
|
+
export function cliRequest(value) {
|
|
29
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
30
|
+
return { problem: 'Send {argv, files}.' };
|
|
31
|
+
const row = value;
|
|
32
|
+
if (!Object.keys(row).every(key => key === 'argv' || key === 'files'))
|
|
33
|
+
return { problem: 'Send only argv and files.' };
|
|
34
|
+
const argv = row.argv;
|
|
35
|
+
if (!Array.isArray(argv) || argv.length === 0 || argv.length > ARGV_MAX || !argv.every(one => typeof one === 'string' && Buffer.byteLength(one) <= ARG_BYTES && !one.includes('\0')))
|
|
36
|
+
return { problem: 'argv must be 1–256 strings.' };
|
|
37
|
+
const files = row.files ?? {};
|
|
38
|
+
if (!files || typeof files !== 'object' || Array.isArray(files))
|
|
39
|
+
return { problem: 'files must map an argument to its contents.' };
|
|
40
|
+
const entries = Object.entries(files);
|
|
41
|
+
if (entries.length > FILES_MAX)
|
|
42
|
+
return { problem: 'Too many files.' };
|
|
43
|
+
let size = 0;
|
|
44
|
+
const kept = Object.create(null);
|
|
45
|
+
for (const [key, content] of entries) {
|
|
46
|
+
if (typeof content !== 'string' || key === '' || key.includes('\0'))
|
|
47
|
+
return { problem: 'files must map file paths to text contents.' };
|
|
48
|
+
size += Buffer.byteLength(content);
|
|
49
|
+
if (size > CLI_FILES_BYTES)
|
|
50
|
+
return { problem: 'Files are limited to 256 KiB in total.', tooLarge: true };
|
|
51
|
+
kept[key] = content;
|
|
52
|
+
}
|
|
53
|
+
return { argv: argv, files: kept };
|
|
54
|
+
}
|
|
55
|
+
/** The longest one request may hold the server: the client polls again for longer waits. */
|
|
56
|
+
export const CLI_WAIT_SECONDS = 25;
|
|
57
|
+
function longPollBounded(argv) {
|
|
58
|
+
const words = argv.filter(one => !one.startsWith('-'));
|
|
59
|
+
if (words[0] !== 'task' || words[1] !== 'wait')
|
|
60
|
+
return true;
|
|
61
|
+
const at = argv.indexOf('--timeout'), given = at < 0 ? NaN : Number(argv[at + 1]);
|
|
62
|
+
return argv.filter(one => one === '--timeout').length === 1 && Number.isFinite(given) && given >= 0 && given <= CLI_WAIT_SECONDS;
|
|
63
|
+
}
|
|
64
|
+
/** Handles exactly `/api/cli`; false for any other path. */
|
|
65
|
+
export async function handleCliHttp(request, response, options) {
|
|
66
|
+
const url = new URL(request.url ?? '/', 'http://standing-orders.local');
|
|
67
|
+
if (url.pathname !== '/api/cli')
|
|
68
|
+
return false;
|
|
69
|
+
const reject = (status, code, message) => { send(response, status, { ok: false, code, message }); request.resume(); return true; };
|
|
70
|
+
if (url.search !== '' || url.username !== '' || url.password !== '')
|
|
71
|
+
return reject(400, 'credentials-in-url', 'Send the command in the request body; nothing travels in the URL.');
|
|
72
|
+
if (request.method !== 'POST')
|
|
73
|
+
return reject(405, 'method-not-allowed', 'Use POST.');
|
|
74
|
+
// A browser always names its page. The remote CLI never does, so a page can never drive this endpoint.
|
|
75
|
+
if (request.headers.origin !== undefined)
|
|
76
|
+
return reject(403, 'browser-origin', 'Remote commands come from the CLI, not a browser.');
|
|
77
|
+
const names = request.rawHeaders.filter((_value, index) => index % 2 === 0).map(name => name.toLowerCase());
|
|
78
|
+
const count = names.filter(name => name === 'authorization').length;
|
|
79
|
+
if (count > 1)
|
|
80
|
+
return reject(400, 'ambiguous-credentials', 'Send one authorization header.');
|
|
81
|
+
if (names.includes('cookie'))
|
|
82
|
+
return reject(400, 'cookie-refused', 'Remote commands sign in with an API token only.');
|
|
83
|
+
if (!/^Bearer so_\S+$/.test(request.headers.authorization ?? ''))
|
|
84
|
+
return reject(401, 'unauthenticated', 'Sign in with your API token: toolroll connect <origin> --token-stdin.');
|
|
85
|
+
if (request.headers['content-type']?.split(';')[0]?.trim() !== 'application/json')
|
|
86
|
+
return reject(415, 'invalid-content-type', 'Send application/json.');
|
|
87
|
+
if (Number(request.headers['content-length'] ?? 0) > CLI_REQUEST_BYTES)
|
|
88
|
+
return reject(413, 'request-too-large', 'Files are limited to 256 KiB in total.');
|
|
89
|
+
let principal;
|
|
90
|
+
try {
|
|
91
|
+
principal = options.authenticate(request);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return reject(503, 'authentication-unavailable', 'Sign-in could not be checked.');
|
|
95
|
+
}
|
|
96
|
+
if (!principal)
|
|
97
|
+
return reject(401, 'unauthenticated', 'This API token is not valid (expired, revoked or its person removed).');
|
|
98
|
+
let value;
|
|
99
|
+
try {
|
|
100
|
+
value = await body(request);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
return reject(error instanceof Error && error.message === 'too-large' ? 413 : 400, 'invalid-body', 'Send a valid JSON request; files are limited to 256 KiB in total.');
|
|
104
|
+
}
|
|
105
|
+
const parsed = cliRequest(value);
|
|
106
|
+
if ('problem' in parsed)
|
|
107
|
+
return reject(parsed.tooLarge ? 413 : 400, 'invalid-request', parsed.problem);
|
|
108
|
+
const row = (options.modeOf ?? contractRow)(parsed.argv);
|
|
109
|
+
if (row?.mode === 'step-up')
|
|
110
|
+
return reject(403, 'step-up', STEP_UP_MESSAGE);
|
|
111
|
+
if (row?.mode !== 'yes')
|
|
112
|
+
return reject(403, 'remote-refused', 'This command cannot run remotely.');
|
|
113
|
+
const paths = fileArguments(parsed.argv, row);
|
|
114
|
+
if ('problem' in paths)
|
|
115
|
+
return reject(403, paths.code, paths.problem);
|
|
116
|
+
const missing = paths.find(path => !Object.hasOwn(parsed.files, path));
|
|
117
|
+
if (missing !== undefined)
|
|
118
|
+
return reject(403, 'missing-file', 'Send every input file\'s contents in files; remote commands cannot read a server file.');
|
|
119
|
+
if (Object.keys(parsed.files).some(path => !paths.includes(path)))
|
|
120
|
+
return reject(400, 'invalid-request', 'Each file must name a declared input file argument.');
|
|
121
|
+
if (!longPollBounded(parsed.argv))
|
|
122
|
+
return reject(400, 'wait-too-long', `A remote task wait holds the server for at most ${CLI_WAIT_SECONDS} seconds; give --timeout ${CLI_WAIT_SECONDS} or less.`);
|
|
123
|
+
let run;
|
|
124
|
+
try {
|
|
125
|
+
run = await options.run();
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
run = null;
|
|
129
|
+
}
|
|
130
|
+
if (run === null)
|
|
131
|
+
return reject(501, 'remote-unavailable', 'This server cannot run remote commands yet. Update it, or use --local.');
|
|
132
|
+
// Files stay in memory: runOperateAs reads them by their argument, so nothing is written to disk here.
|
|
133
|
+
const out = [];
|
|
134
|
+
let exitCode;
|
|
135
|
+
try {
|
|
136
|
+
exitCode = await run([...parsed.argv], { principal, store: options.store, write: line => { out.push(`${line}\n`); }, files: parsed.files, source: 'api' });
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return reject(500, 'command-failed', 'The command stopped unexpectedly on the server. Check its state before running it again.');
|
|
140
|
+
}
|
|
141
|
+
send(response, 200, { exitCode, stdout: out.join(''), stderr: '' });
|
|
142
|
+
return true;
|
|
143
|
+
}
|
package/dist/cli.d.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { previewGithubRepo, cloneGithubRepo } from "./onboard.js";
|
|
14
14
|
import { type SessionCliOptions } from "./session-cli.js";
|
|
15
15
|
import { type TeamCliOptions } from "./team-cli.js";
|
|
16
|
+
import { type RemoteExecOptions } from "./remote-exec.js";
|
|
16
17
|
import { type UpdateCliDeps } from "./toolroll-update-cli.js";
|
|
17
18
|
import { type OperateOptions } from "./operate.js";
|
|
18
19
|
export type CliOptions = {
|
|
@@ -54,6 +55,8 @@ export type MainOptions = {
|
|
|
54
55
|
operate?: OperateOptions;
|
|
55
56
|
session?: SessionCliOptions;
|
|
56
57
|
team?: TeamCliOptions;
|
|
58
|
+
/** Injected by tests: the remote CLI's profile, fetch and output streams (defaults to `team`'s profile seams). */
|
|
59
|
+
remote?: RemoteExecOptions;
|
|
57
60
|
/** Injected by tests: the gh-facing halves of `repos add-from-github` —
|
|
58
61
|
* the verb's parsing, gating, and enrollment are what CLI tests prove;
|
|
59
62
|
* gh itself is proved by onboard.test.ts. */
|
package/dist/cli.js
CHANGED
|
@@ -27,6 +27,7 @@ import { run as execRun } from "./exec.js";
|
|
|
27
27
|
import { COMMAND_GUIDE, SURFACE_NOTES, SURFACE_SCHEMA_VERSION } from "./surface.js";
|
|
28
28
|
import { runSessionCommand, SESSION_CLI_ACTIONS } from "./session-cli.js";
|
|
29
29
|
import { maybeRunTeamCommand, TEAM_CLI_ACTIONS } from "./team-cli.js";
|
|
30
|
+
import { maybeRunRemoteCommand } from "./remote-exec.js";
|
|
30
31
|
import { discover, inspectAll } from "./discover.js";
|
|
31
32
|
import { readPulls } from "./pulls.js";
|
|
32
33
|
import { detectGraphs, chooseBackend, setupOptions, LABELS as GRAPH_LABELS, } from "./graph.js";
|
|
@@ -253,7 +254,7 @@ export const OPERATE_COMMANDS = new Set([
|
|
|
253
254
|
"run",
|
|
254
255
|
"routine",
|
|
255
256
|
"flows",
|
|
256
|
-
"config", "chat", "proposals", "mode", "people", "keys",
|
|
257
|
+
"config", "chat", "proposals", "mode", "chat-approval", "people", "keys",
|
|
257
258
|
"setup",
|
|
258
259
|
"verify",
|
|
259
260
|
"intake",
|
|
@@ -375,6 +376,12 @@ export async function main(argv, write = line => console.log(line), mainOptions
|
|
|
375
376
|
return code;
|
|
376
377
|
}
|
|
377
378
|
async function dispatch(argv, write, mainOptions) {
|
|
379
|
+
// A saved API token sends commands marked remote to the central server, before anything local opens.
|
|
380
|
+
const central = await maybeRunRemoteCommand(argv, mainOptions.remote ?? mainOptions.team);
|
|
381
|
+
if (typeof central === "number")
|
|
382
|
+
return central;
|
|
383
|
+
if (central !== null)
|
|
384
|
+
argv = central.local;
|
|
378
385
|
const team = await maybeRunTeamCommand(argv, write, mainOptions.team);
|
|
379
386
|
if (team !== null)
|
|
380
387
|
return team;
|
package/dist/contracts/cli.d.ts
CHANGED
|
@@ -25,6 +25,9 @@ export declare const commandFlagSchema: z.ZodReadonly<z.ZodObject<{
|
|
|
25
25
|
meaning: z.ZodString;
|
|
26
26
|
}, z.core.$loose>>;
|
|
27
27
|
export type CommandFlag = z.infer<typeof commandFlagSchema>;
|
|
28
|
+
/** The remote policy every command row states; see `remote` below. */
|
|
29
|
+
export declare const REMOTE_POLICIES: readonly ["yes", "no", "step-up"];
|
|
30
|
+
export type RemotePolicy = typeof REMOTE_POLICIES[number];
|
|
28
31
|
export declare const commandRowSchema: z.ZodReadonly<z.ZodObject<{
|
|
29
32
|
invocation: z.ZodString;
|
|
30
33
|
envelopeCommand: z.ZodOptional<z.ZodString>;
|
|
@@ -82,6 +85,11 @@ export declare const commandRowSchema: z.ZodReadonly<z.ZodObject<{
|
|
|
82
85
|
const?: number;
|
|
83
86
|
}>;
|
|
84
87
|
}>>;
|
|
88
|
+
remote: z.ZodEnum<{
|
|
89
|
+
no: "no";
|
|
90
|
+
"step-up": "step-up";
|
|
91
|
+
yes: "yes";
|
|
92
|
+
}>;
|
|
85
93
|
}, z.core.$loose>>;
|
|
86
94
|
export type CommandRow = z.infer<typeof commandRowSchema>;
|
|
87
95
|
/**
|
|
@@ -157,6 +165,11 @@ export declare const COMMAND_OUTPUTS: {
|
|
|
157
165
|
const?: number;
|
|
158
166
|
}>;
|
|
159
167
|
}>>;
|
|
168
|
+
remote: z.ZodEnum<{
|
|
169
|
+
no: "no";
|
|
170
|
+
"step-up": "step-up";
|
|
171
|
+
yes: "yes";
|
|
172
|
+
}>;
|
|
160
173
|
}, z.core.$loose>>>;
|
|
161
174
|
}, z.core.$loose>, z.ZodObject<{
|
|
162
175
|
envelopeVersion: z.ZodLiteral<1>;
|
package/dist/contracts/cli.js
CHANGED
|
@@ -35,6 +35,8 @@ function answers(command, shape) {
|
|
|
35
35
|
}
|
|
36
36
|
// ---- the declared command guide, as `contract --commands` dumps it ----
|
|
37
37
|
export const commandFlagSchema = z.looseObject({ name: str, takesValue: z.boolean(), meaning: str }).readonly();
|
|
38
|
+
/** The remote policy every command row states; see `remote` below. */
|
|
39
|
+
export const REMOTE_POLICIES = ["yes", "no", "step-up"];
|
|
38
40
|
/** A session operation's executable input schema, as session-contract.ts states it. */
|
|
39
41
|
const sessionInputSchema = z.custom(value => typeof value === "object" && value !== null && !Array.isArray(value));
|
|
40
42
|
export const commandRowSchema = z.looseObject({
|
|
@@ -61,6 +63,11 @@ export const commandRowSchema = z.looseObject({
|
|
|
61
63
|
notableReasons: z.array(str).readonly().optional(),
|
|
62
64
|
/** Session schemas are executable input contracts, not a grant of authority. */
|
|
63
65
|
inputSchema: sessionInputSchema.optional(),
|
|
66
|
+
/** Whether a person signed in to a central server with their own API token may run it there (`runOperateAs`):
|
|
67
|
+
* yes — runs as that person, within their token's scope and project access.
|
|
68
|
+
* no — this machine's own infrastructure, credentials or files; never run for a remote caller.
|
|
69
|
+
* step-up — approvals, people and policy: a person's act in the console or chat, never a token's. */
|
|
70
|
+
remote: z.enum(REMOTE_POLICIES),
|
|
64
71
|
}).readonly();
|
|
65
72
|
// ---- the commands that describe their fields ----
|
|
66
73
|
const taskSchema = z.looseObject({ id: str, title: str, state: z.enum(LISTED_TASK_STATES), createdAt: str, updatedAt: str, priority: z.number() });
|
|
@@ -102,6 +102,7 @@ export declare const CONSOLE_FORMS: {
|
|
|
102
102
|
readonly learningChange: FormContract<"action" | "identity" | "lesson" | "repo" | "revision" | "sha" | "version">;
|
|
103
103
|
readonly projectSetup: FormContract<"command" | "fingerprint" | "model" | "nonce" | "provider" | "repo" | "seconds" | "token">;
|
|
104
104
|
readonly slack: FormContract<"app-token" | "bot-token" | "password">;
|
|
105
|
+
readonly chatApproval: FormContract<"cap-usd" | "digest" | "full-access" | "nonce" | "scope" | "token">;
|
|
105
106
|
readonly telegramPair: FormContract<"password">;
|
|
106
107
|
readonly teams: FormContract<"app-id" | "password" | "secret" | "tenant">;
|
|
107
108
|
readonly discord: FormContract<"bot-token" | "password">;
|
|
@@ -111,6 +111,7 @@ export const CONSOLE_FORMS = {
|
|
|
111
111
|
learningChange: formContract("POST /settings/learning/change", ["repo", "action", "identity", "revision", "lesson", "version", "sha"]),
|
|
112
112
|
projectSetup: formContract("POST /control/setup-preview, /control/setup-approve, /control/instructions-preview, /control/instructions-approve", ["repo", "nonce", "fingerprint", "token", "provider", "model", "command", "seconds"]),
|
|
113
113
|
slack: formContract("POST /settings/slack/<connect|pair|unpair|disconnect|alerts>", ["password", "app-token", "bot-token"]),
|
|
114
|
+
chatApproval: formContract("POST /settings/chat-approval/confirm, /settings/chat-approval/save, /settings/chat-approval/off", ["scope", "full-access", "cap-usd", "nonce", "digest", "token"]),
|
|
114
115
|
telegramPair: formContract("POST /settings/telegram/pair, /settings/telegram/unpair", ["password"]),
|
|
115
116
|
teams: formContract("POST /settings/teams/<connect|pair|unpair|disconnect|alerts>", ["password", "app-id", "tenant", "secret"]),
|
|
116
117
|
discord: formContract("POST /settings/discord/<connect|pair|unpair|disconnect|alerts>", ["password", "bot-token"]),
|
|
@@ -372,3 +372,85 @@ export declare const GATEWAY_TOOL_OUTPUTS: {
|
|
|
372
372
|
}, z.core.$loose>;
|
|
373
373
|
};
|
|
374
374
|
export type GatewayToolOutput<N extends GatewayToolName> = z.infer<(typeof GATEWAY_TOOL_OUTPUTS)[N]>;
|
|
375
|
+
export declare const PERSON_TOOL_INPUTS: {
|
|
376
|
+
readonly status: z.ZodObject<{}, z.core.$strict>;
|
|
377
|
+
readonly list_tasks: z.ZodObject<{
|
|
378
|
+
state: z.ZodOptional<z.ZodEnum<{
|
|
379
|
+
cancelled: "cancelled";
|
|
380
|
+
done: "done";
|
|
381
|
+
failed: "failed";
|
|
382
|
+
queued: "queued";
|
|
383
|
+
running: "running";
|
|
384
|
+
}>>;
|
|
385
|
+
repo: z.ZodOptional<z.ZodString>;
|
|
386
|
+
cursor: z.ZodOptional<z.ZodInt>;
|
|
387
|
+
limit: z.ZodOptional<z.ZodInt>;
|
|
388
|
+
}, z.core.$strict>;
|
|
389
|
+
readonly task_show: z.ZodObject<{
|
|
390
|
+
ref: z.ZodString;
|
|
391
|
+
}, z.core.$strict>;
|
|
392
|
+
readonly task_review: z.ZodObject<{
|
|
393
|
+
ref: z.ZodString;
|
|
394
|
+
run: z.ZodOptional<z.ZodInt>;
|
|
395
|
+
all: z.ZodOptional<z.ZodBoolean>;
|
|
396
|
+
}, z.core.$strict>;
|
|
397
|
+
readonly review_findings: z.ZodObject<{
|
|
398
|
+
run: z.ZodInt;
|
|
399
|
+
}, z.core.$strict>;
|
|
400
|
+
readonly file_task: z.ZodObject<{
|
|
401
|
+
repo: z.ZodString;
|
|
402
|
+
title: z.ZodString;
|
|
403
|
+
idempotency_key: z.ZodString;
|
|
404
|
+
deliverable: z.ZodOptional<z.ZodEnum<{
|
|
405
|
+
branch: "branch";
|
|
406
|
+
report: "report";
|
|
407
|
+
}>>;
|
|
408
|
+
}, z.core.$strict>;
|
|
409
|
+
};
|
|
410
|
+
export type PersonToolName = keyof typeof PERSON_TOOL_INPUTS;
|
|
411
|
+
export type PersonToolInput<N extends PersonToolName> = z.infer<(typeof PERSON_TOOL_INPUTS)[N]>;
|
|
412
|
+
export declare const PERSON_TOOL_OUTPUTS: {
|
|
413
|
+
readonly status: z.ZodObject<{
|
|
414
|
+
envelopeVersion: z.ZodLiteral<1>;
|
|
415
|
+
ok: z.ZodBoolean;
|
|
416
|
+
command: z.ZodString;
|
|
417
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
418
|
+
message: z.ZodOptional<z.ZodString>;
|
|
419
|
+
}, z.core.$loose>;
|
|
420
|
+
readonly list_tasks: z.ZodObject<{
|
|
421
|
+
envelopeVersion: z.ZodLiteral<1>;
|
|
422
|
+
ok: z.ZodBoolean;
|
|
423
|
+
command: z.ZodString;
|
|
424
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
425
|
+
message: z.ZodOptional<z.ZodString>;
|
|
426
|
+
}, z.core.$loose>;
|
|
427
|
+
readonly task_show: z.ZodObject<{
|
|
428
|
+
envelopeVersion: z.ZodLiteral<1>;
|
|
429
|
+
ok: z.ZodBoolean;
|
|
430
|
+
command: z.ZodString;
|
|
431
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
432
|
+
message: z.ZodOptional<z.ZodString>;
|
|
433
|
+
}, z.core.$loose>;
|
|
434
|
+
readonly task_review: z.ZodObject<{
|
|
435
|
+
envelopeVersion: z.ZodLiteral<1>;
|
|
436
|
+
ok: z.ZodBoolean;
|
|
437
|
+
command: z.ZodString;
|
|
438
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
439
|
+
message: z.ZodOptional<z.ZodString>;
|
|
440
|
+
}, z.core.$loose>;
|
|
441
|
+
readonly review_findings: z.ZodObject<{
|
|
442
|
+
envelopeVersion: z.ZodLiteral<1>;
|
|
443
|
+
ok: z.ZodBoolean;
|
|
444
|
+
command: z.ZodString;
|
|
445
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
446
|
+
message: z.ZodOptional<z.ZodString>;
|
|
447
|
+
}, z.core.$loose>;
|
|
448
|
+
readonly file_task: z.ZodObject<{
|
|
449
|
+
envelopeVersion: z.ZodLiteral<1>;
|
|
450
|
+
ok: z.ZodBoolean;
|
|
451
|
+
command: z.ZodString;
|
|
452
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
453
|
+
message: z.ZodOptional<z.ZodString>;
|
|
454
|
+
id: z.ZodOptional<z.ZodString>;
|
|
455
|
+
}, z.core.$loose>;
|
|
456
|
+
};
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { z } from "zod";
|
|
12
12
|
import { TEXT_LIMITS } from "../text-limits.js";
|
|
13
|
+
import { envelopeSchema } from "./cli.js";
|
|
13
14
|
import { catchUpOutput, decisionOutput, decisionsOutput, LISTED_TASK_STATES, queueColumnsOutput, recapOutput, repositoryContextOutput, scopeText, touches } from "./lead-tools.js";
|
|
14
15
|
const text = (key) => z.string().max(TEXT_LIMITS[key]);
|
|
15
16
|
const ref = z.string().min(1).max(TEXT_LIMITS.taskRef);
|
|
@@ -92,3 +93,35 @@ export const GATEWAY_TOOL_OUTPUTS = {
|
|
|
92
93
|
get_contract: z.looseObject({ contract: str }),
|
|
93
94
|
file_proposal: z.looseObject({ ref: str, replayed: z.boolean(), admission: str }),
|
|
94
95
|
};
|
|
96
|
+
// ------------------------------------------------------------- a person's tools
|
|
97
|
+
/**
|
|
98
|
+
* The tools a PERSON's API token sees over the HTTP gateway (mcp-person.ts). Each call becomes the exact `toolroll`
|
|
99
|
+
* command line the same person could type, run on the server under their own principal (operate.ts `runOperateAs`):
|
|
100
|
+
* the command's own authorization, project grants and attribution apply, and its `--json` envelope is the answer.
|
|
101
|
+
* Two reads keep the coordinator's names and arguments; the rest are new. Every value that lands in a command line
|
|
102
|
+
* may not begin with "-", so an argument can never be read as a flag.
|
|
103
|
+
*/
|
|
104
|
+
const notFlag = (schema) => schema.regex(/^[^-]/, { error: "must not begin with -" });
|
|
105
|
+
const taskRef = notFlag(ref);
|
|
106
|
+
const runId = z.int().min(1);
|
|
107
|
+
export const PERSON_TOOL_INPUTS = {
|
|
108
|
+
status: GATEWAY_TOOL_INPUTS.status,
|
|
109
|
+
list_tasks: GATEWAY_TOOL_INPUTS.list_tasks,
|
|
110
|
+
task_show: z.strictObject({ ref: taskRef }),
|
|
111
|
+
task_review: z.strictObject({ ref: taskRef, run: runId.optional(), all: z.boolean().optional() }),
|
|
112
|
+
review_findings: z.strictObject({ run: runId }),
|
|
113
|
+
file_task: z.strictObject({
|
|
114
|
+
repo: notFlag(gatewayRepo), title: notFlag(text("taskTitle").min(1)), idempotency_key: notFlag(text("idempotencyKey").min(8)),
|
|
115
|
+
deliverable: z.enum(["branch", "report"]).optional(),
|
|
116
|
+
}),
|
|
117
|
+
};
|
|
118
|
+
/** The command's `--json` envelope (contracts/cli.ts); a refusal is `ok: false` with its stable reason. */
|
|
119
|
+
const envelope = envelopeSchema;
|
|
120
|
+
export const PERSON_TOOL_OUTPUTS = {
|
|
121
|
+
status: envelope,
|
|
122
|
+
list_tasks: envelope,
|
|
123
|
+
task_show: envelope,
|
|
124
|
+
task_review: envelope,
|
|
125
|
+
review_findings: envelope,
|
|
126
|
+
file_task: envelope.extend({ id: str.optional() }),
|
|
127
|
+
};
|
|
@@ -88,6 +88,10 @@ export declare const LEAD_TOOL_INPUTS: {
|
|
|
88
88
|
repo: z.ZodOptional<z.ZodString>;
|
|
89
89
|
run: z.ZodOptional<z.ZodInt>;
|
|
90
90
|
}, z.core.$strict>;
|
|
91
|
+
readonly offer_approval: z.ZodObject<{
|
|
92
|
+
task: z.ZodOptional<z.ZodString>;
|
|
93
|
+
card: z.ZodOptional<z.ZodInt>;
|
|
94
|
+
}, z.core.$strict>;
|
|
91
95
|
readonly get_result: z.ZodObject<{
|
|
92
96
|
task: z.ZodString;
|
|
93
97
|
run: z.ZodOptional<z.ZodInt>;
|
|
@@ -777,6 +781,10 @@ export declare const LEAD_TOOL_READ_INPUTS: {
|
|
|
777
781
|
repo: z.ZodOptional<z.ZodString>;
|
|
778
782
|
run: z.ZodOptional<z.ZodInt>;
|
|
779
783
|
}, z.core.$strict>;
|
|
784
|
+
readonly offer_approval: z.ZodObject<{
|
|
785
|
+
task: z.ZodOptional<z.ZodString>;
|
|
786
|
+
card: z.ZodOptional<z.ZodInt>;
|
|
787
|
+
}, z.core.$strict>;
|
|
780
788
|
readonly get_result: z.ZodObject<{
|
|
781
789
|
task: z.ZodString;
|
|
782
790
|
run: z.ZodOptional<z.ZodInt>;
|
|
@@ -1535,6 +1543,9 @@ export declare const LEAD_TOOL_OUTPUTS: {
|
|
|
1535
1543
|
label: z.ZodString;
|
|
1536
1544
|
action: z.ZodString;
|
|
1537
1545
|
}, z.core.$loose>;
|
|
1546
|
+
readonly offer_approval: z.ZodObject<{
|
|
1547
|
+
offered: z.ZodBoolean;
|
|
1548
|
+
}, z.core.$loose>;
|
|
1538
1549
|
readonly get_result: z.ZodObject<{
|
|
1539
1550
|
task: z.ZodString;
|
|
1540
1551
|
root: z.ZodString;
|
|
@@ -65,6 +65,7 @@ export const LEAD_TOOL_INPUTS = {
|
|
|
65
65
|
propose_task_action: z.strictObject({ task: taskRef, operation: enumOf(Object.keys(CHAT_TASK_ACTIONS)), dependency: taskRef.optional(), run: id.optional() }),
|
|
66
66
|
get_controls: z.strictObject({}),
|
|
67
67
|
show_control: z.strictObject({ control: enumOf(Object.keys(CHAT_CONTROLS)), task: taskRef.optional(), repo: repoId.optional(), run: id.optional() }),
|
|
68
|
+
offer_approval: z.strictObject({ task: taskRef.optional(), card: id.optional() }),
|
|
68
69
|
get_result: z.strictObject({ task: taskRef, run: id.optional(), feedback_offset: offset.optional() }),
|
|
69
70
|
get_diff: z.strictObject({ task: taskRef, run: id.optional(), file: text("diffPath").min(1).optional(), offset: offset.optional() }),
|
|
70
71
|
get_check_log: z.strictObject({ task: taskRef, run: id.optional(), search: text("logSearch").min(2).optional(), offset: offset.optional() }),
|
|
@@ -189,6 +190,7 @@ export const LEAD_TOOL_OUTPUTS = {
|
|
|
189
190
|
propose_task_action: z.looseObject({ proposal: int, action: str, awaiting: str }),
|
|
190
191
|
get_controls: z.looseObject({ confirmedInChat: z.array(str), existingControls: rows({ id: str, label: str, needsTask: z.boolean(), needsProject: z.boolean() }), rule: str }),
|
|
191
192
|
show_control: z.looseObject({ card: int, label: str, action: str }),
|
|
193
|
+
offer_approval: z.looseObject({ offered: z.boolean() }),
|
|
192
194
|
get_result: z.looseObject({
|
|
193
195
|
task: str, root: str, currentExecution: str, run: int, title: str, feedback: list, feedbackTotal: int, nextFeedbackOffset: nullable(int),
|
|
194
196
|
changes: str, changesShortened: z.boolean(), verification: str, accepted: z.boolean(), evidenceTool: str, canRevise: z.boolean(),
|
package/dist/desktop-update.d.ts
CHANGED
|
@@ -28,6 +28,8 @@ export declare const updateTerminal: (phase: string) => boolean;
|
|
|
28
28
|
/** Durable receipts are private and independent of the app being replaced. */
|
|
29
29
|
export declare function durableJson(file: string, value: unknown): void;
|
|
30
30
|
export declare function readUpdateJournal(stateDir: string, retainedReceipt?: string): UpdateJournal | null;
|
|
31
|
+
/** Which settled schema versions a caller works on: the desktop update, which never migrates, only this build's own. */
|
|
32
|
+
export type SchemaAccepted = (version: number | null) => boolean;
|
|
31
33
|
/** A finished run whose end Toolroll cannot show yet: in plain words, with the one command that clears it when a
|
|
32
34
|
* person can. */
|
|
33
35
|
export type LingeringRun = {
|
|
@@ -135,7 +137,7 @@ export declare function pauseUpdateRecovery(stateDir: string, id: string, detail
|
|
|
135
137
|
* reservation so no writer slips in between the snapshot and the copy. The
|
|
136
138
|
* copy drops the update's own admission pause. `inside` runs before the
|
|
137
139
|
* reservation is released, `after` once it is; returns the copy's hash. */
|
|
138
|
-
export declare function verifiedDatabaseBackup(databaseFile: string, backupPath: string, gateId: string, inside?: () => Promise<void>, after?: () => void): Promise<string>;
|
|
140
|
+
export declare function verifiedDatabaseBackup(databaseFile: string, backupPath: string, gateId: string, inside?: () => Promise<void>, after?: () => void, accepts?: SchemaAccepted): Promise<string>;
|
|
139
141
|
/** A SQLite OS lock releases on updater death. Each irreversible step has a
|
|
140
142
|
* preceding durable intent; recovery determines the actual side by hashes. */
|
|
141
143
|
export declare function runDesktopUpdate(stateDir: string, hooks?: UpdateHooks): Promise<void>;
|
package/dist/desktop-update.js
CHANGED
|
@@ -116,13 +116,14 @@ export function readUpdateJournal(stateDir, retainedReceipt) {
|
|
|
116
116
|
}
|
|
117
117
|
return j;
|
|
118
118
|
}
|
|
119
|
-
|
|
119
|
+
const currentSchemaOnly = version => version === SCHEMA_VERSION;
|
|
120
|
+
function connect(file, readOnly = false, accepts = currentSchemaOnly) {
|
|
120
121
|
if (!existsSync(file) || lstatSync(file).isSymbolicLink())
|
|
121
122
|
throw Error("The task database is missing or linked. It was not recreated.");
|
|
122
123
|
const db = new (sqlite().DatabaseSync)(file, { readOnly });
|
|
123
124
|
db.exec("PRAGMA busy_timeout=1000");
|
|
124
125
|
const schema = readSchemaVersion(db);
|
|
125
|
-
if (!schema.ok || schema.version
|
|
126
|
+
if (!schema.ok || !accepts(schema.version)) {
|
|
126
127
|
db.close();
|
|
127
128
|
throw Error("This update needs the current database schema. Use the separate verified migration procedure; the installed app is unchanged.");
|
|
128
129
|
}
|
|
@@ -515,13 +516,13 @@ async function verifiedBackup(j) {
|
|
|
515
516
|
* reservation so no writer slips in between the snapshot and the copy. The
|
|
516
517
|
* copy drops the update's own admission pause. `inside` runs before the
|
|
517
518
|
* reservation is released, `after` once it is; returns the copy's hash. */
|
|
518
|
-
export async function verifiedDatabaseBackup(databaseFile, backupPath, gateId, inside = async () => { }, after = () => { }) {
|
|
519
|
-
const db = connect(databaseFile);
|
|
519
|
+
export async function verifiedDatabaseBackup(databaseFile, backupPath, gateId, inside = async () => { }, after = () => { }, accepts = currentSchemaOnly) {
|
|
520
|
+
const db = connect(databaseFile, false, accepts);
|
|
520
521
|
try {
|
|
521
522
|
db.exec("BEGIN IMMEDIATE");
|
|
522
523
|
// The write reservation prevents a concurrent writer; a separate read
|
|
523
524
|
// connection is required because SQLite cannot back up a write transaction.
|
|
524
|
-
const source = connect(databaseFile, true);
|
|
525
|
+
const source = connect(databaseFile, true, accepts);
|
|
525
526
|
let before;
|
|
526
527
|
try {
|
|
527
528
|
before = snapshot(source);
|
|
@@ -531,7 +532,7 @@ export async function verifiedDatabaseBackup(databaseFile, backupPath, gateId, i
|
|
|
531
532
|
source.close();
|
|
532
533
|
}
|
|
533
534
|
chmodSync(backupPath, 0o600);
|
|
534
|
-
const copied = connect(backupPath);
|
|
535
|
+
const copied = connect(backupPath, false, accepts);
|
|
535
536
|
try {
|
|
536
537
|
if (copied.prepare("PRAGMA integrity_check").get()?.integrity_check !== "ok" || copied.prepare("PRAGMA foreign_key_check").all().length !== 0 || snapshot(copied) !== before)
|
|
537
538
|
throw Error("Backup verification failed. The installed app is unchanged.");
|
package/dist/flow-engine.d.ts
CHANGED
|
@@ -74,4 +74,6 @@ export declare function decideFlowCard(store: Store, input: {
|
|
|
74
74
|
entry?: number;
|
|
75
75
|
/** v92: the AI teammate (by handle) deciding a zone it staffs; `actor` is then how it reads in history. */
|
|
76
76
|
teammate?: string;
|
|
77
|
+
/** Where the decision was made, for its ledger line: "the console", "the command line", "Telegram". */
|
|
78
|
+
where?: string;
|
|
77
79
|
}, now: Date): FlowDecision;
|
package/dist/flow-engine.js
CHANGED
|
@@ -485,6 +485,9 @@ export function decideFlowCard(store, input, now) {
|
|
|
485
485
|
// v102: a protected project's decisions are a person's, never an AI teammate's.
|
|
486
486
|
if (input.teammate !== undefined && store.approvalRules(flow.repo).protectProject)
|
|
487
487
|
return { ok: false, message: "This project is protected: a person decides here." };
|
|
488
|
+
// Every decision is one ledger line, the same wherever it was made; only `via` says where.
|
|
489
|
+
const ledger = (outcome) => store.recordAction({ at: now.toISOString(), actor: input.actor, repo: flow.repo, taskId: card.primaryTask, runId: null,
|
|
490
|
+
action: "flow decision", outcome, source: "request", detail: `${flow.name} · card ${card.id} · ${stage.title}${input.teammate === undefined ? "" : ` · teammate ${input.teammate}`} · via ${input.where ?? "Toolroll"}` });
|
|
488
491
|
if (input.decision === "approve") {
|
|
489
492
|
// An edited draft replaces the one Claude wrote, so the steps after this send what the person approved.
|
|
490
493
|
const draft = draftFor(definition, stage);
|
|
@@ -498,9 +501,12 @@ export function decideFlowCard(store, input, now) {
|
|
|
498
501
|
}
|
|
499
502
|
if (stage.next === null) {
|
|
500
503
|
store.updateFlowCard(card.id, { state: "done", waiting: null }, now);
|
|
504
|
+
ledger("approved");
|
|
501
505
|
return { ok: true, said: "Approved. The card is done." };
|
|
502
506
|
}
|
|
503
|
-
store.moveFlowCard(card.id, { to: stage.next, outcome: "approved", actor: input.actor, note: input.note, expectEntry: card.entry }, now)
|
|
507
|
+
if (!store.moveFlowCard(card.id, { to: stage.next, outcome: "approved", actor: input.actor, note: input.note, expectEntry: card.entry }, now))
|
|
508
|
+
return { ok: false, message: "That card has moved on since; nothing was changed." };
|
|
509
|
+
ledger("approved");
|
|
504
510
|
const title = definition.stages.find(one => one.id === stage.next)?.title ?? stage.next;
|
|
505
511
|
if (card.owner !== null)
|
|
506
512
|
notifyPeople(store, card, [card.owner], input.actor, { key: `approved:${card.entry}`, subject: `${input.actor} approved “${card.title}”`, body: `${stage.title}: approved. It moves to ${title}.${input.note === null || input.note.trim() === "" ? "" : `\n\n${input.note.trim()}`}` }, now);
|
|
@@ -512,7 +518,9 @@ export function decideFlowCard(store, input, now) {
|
|
|
512
518
|
return { ok: false, message: "Say what should change." };
|
|
513
519
|
const target = definition.stages.find(one => one.id === stage.onFail);
|
|
514
520
|
const revision = revisionWithNote(store, card, target, input.note.trim(), input.actor, input.repos, input.evidenceRoot, now);
|
|
515
|
-
store.moveFlowCard(card.id, { to: stage.onFail, outcome: "sent-back", actor: input.actor, note: input.note.trim(), ...(revision === null ? {} : { task: revision }), expectEntry: card.entry }, now)
|
|
521
|
+
if (!store.moveFlowCard(card.id, { to: stage.onFail, outcome: "sent-back", actor: input.actor, note: input.note.trim(), ...(revision === null ? {} : { task: revision }), expectEntry: card.entry }, now))
|
|
522
|
+
return { ok: false, message: "That card has moved on since; nothing was changed." };
|
|
523
|
+
ledger("sent-back");
|
|
516
524
|
if (card.owner !== null)
|
|
517
525
|
notifyPeople(store, card, [card.owner], input.actor, { key: `sent-back:${card.entry}`, subject: `${input.actor} sent “${card.title}” back`, body: `Sent back to ${target?.title ?? stage.onFail}:\n\n${input.note.trim()}`, attention: true }, now);
|
|
518
526
|
return { ok: true, said: revision === null ? `Sent back to ${target?.title ?? stage.onFail} with your note.` : `Sent back to ${target?.title ?? stage.onFail}: a revision was made with your note.` };
|
package/dist/flows-cli.d.ts
CHANGED
|
@@ -188,6 +188,34 @@ export declare const FLOWS_DESCRIPTORS: readonly [{
|
|
|
188
188
|
readonly takesValue: true;
|
|
189
189
|
readonly meaning: 'a zone id or name';
|
|
190
190
|
}];
|
|
191
|
+
}, {
|
|
192
|
+
readonly action: 'card approve';
|
|
193
|
+
readonly synopsis: "approve a card waiting at a Person decides step (or pick --option at a Person chooses step), as the console's Approve does";
|
|
194
|
+
readonly mutation: 'identity-idempotent';
|
|
195
|
+
readonly positionals: readonly ['flow', 'card'];
|
|
196
|
+
readonly flags: readonly [...{
|
|
197
|
+
name: string;
|
|
198
|
+
takesValue: boolean;
|
|
199
|
+
meaning: string;
|
|
200
|
+
}[], {
|
|
201
|
+
readonly name: 'option';
|
|
202
|
+
readonly takesValue: true;
|
|
203
|
+
readonly meaning: "at a Person chooses step: the option's words or its number";
|
|
204
|
+
}];
|
|
205
|
+
}, {
|
|
206
|
+
readonly action: 'card send-back';
|
|
207
|
+
readonly synopsis: "send a waiting card back with a note, as the console's Send back (or a reply at a Person chooses step) does";
|
|
208
|
+
readonly mutation: 'identity-idempotent';
|
|
209
|
+
readonly positionals: readonly ['flow', 'card'];
|
|
210
|
+
readonly flags: readonly [...{
|
|
211
|
+
name: string;
|
|
212
|
+
takesValue: boolean;
|
|
213
|
+
meaning: string;
|
|
214
|
+
}[], {
|
|
215
|
+
readonly name: 'note';
|
|
216
|
+
readonly takesValue: true;
|
|
217
|
+
readonly meaning: 'what should change';
|
|
218
|
+
}];
|
|
191
219
|
}, {
|
|
192
220
|
readonly action: 'export';
|
|
193
221
|
readonly synopsis: 'write a flow as a *.toolroll-flow.json file: zones, paths, trigger settings and scripts; never secrets, webhook addresses, names or cards';
|