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
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { Store } from "./store.js";
|
|
2
|
+
import type { LedgerSource } from "./action-ledger.js";
|
|
3
|
+
import { type CommandRow } from "./surface.js";
|
|
4
|
+
/** Who a remote call is, as the transport proved it from an `so_` token. */
|
|
5
|
+
export type Principal = {
|
|
6
|
+
kind: "person";
|
|
7
|
+
account: string;
|
|
8
|
+
/** The account's credential generation when the token was checked. */
|
|
9
|
+
generation: number;
|
|
10
|
+
scope: "read" | "act";
|
|
11
|
+
tokenId: string;
|
|
12
|
+
/** The projects the person may use (null: all of them). */
|
|
13
|
+
projects: string[] | null;
|
|
14
|
+
};
|
|
15
|
+
export type RemoteSource = Extract<LedgerSource, "api" | "mcp">;
|
|
16
|
+
export { activeRemote, withRemote, type RemoteRun } from "./remote-run.js";
|
|
17
|
+
/** Thrown where a remote run reaches something only this machine's owner may use; the command ends refused. */
|
|
18
|
+
export declare class RemoteRefusal extends Error {
|
|
19
|
+
readonly reason: string;
|
|
20
|
+
constructor(reason: string, message: string);
|
|
21
|
+
}
|
|
22
|
+
export declare const REMOTE_MESSAGES: {
|
|
23
|
+
readonly "step-up": "Approvals, people and policy changes aren't taken from a token: approve in the console or chat.";
|
|
24
|
+
readonly no: "That command only runs on the server's own machine, not for a remote caller.";
|
|
25
|
+
readonly unknown: "That isn't a command a remote caller can run.";
|
|
26
|
+
readonly read: "Your token reads only. Use an act token for this command.";
|
|
27
|
+
readonly stale: "Your sign-in changed since this token was checked. Sign in again.";
|
|
28
|
+
readonly credential: "Remote commands run as your token's person: leave out --as, --token, --db and other credential or file flags.";
|
|
29
|
+
readonly project: "Name a project you have access to with --repo (its path on the server).";
|
|
30
|
+
readonly installation: "This command covers every project; it needs access to all projects.";
|
|
31
|
+
readonly files: "Send that file with the request; the server reads no paths of its own for you.";
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* How a "yes" command finds the project it touches, before it runs. Any --repo given is checked too.
|
|
35
|
+
* installation — reads or acts across every project: only for someone with access to all of them.
|
|
36
|
+
* repo — needs --repo, naming a project the server knows and the person may use.
|
|
37
|
+
* references — the audited arguments resolve every resource to its stored project before dispatch.
|
|
38
|
+
* self — the command already narrows to the person's own projects (their access is passed in).
|
|
39
|
+
* global — touches no project's data.
|
|
40
|
+
*/
|
|
41
|
+
export type RemoteScope = {
|
|
42
|
+
kind: "installation";
|
|
43
|
+
} | {
|
|
44
|
+
kind: "repo";
|
|
45
|
+
} | {
|
|
46
|
+
kind: "references";
|
|
47
|
+
} | {
|
|
48
|
+
kind: "self";
|
|
49
|
+
} | {
|
|
50
|
+
kind: "global";
|
|
51
|
+
};
|
|
52
|
+
/** Every "yes" row's scope. The remote test holds this to the guide's "yes" rows exactly. */
|
|
53
|
+
export declare const REMOTE_SCOPES: ReadonlyMap<string, RemoteScope>;
|
|
54
|
+
/** Flags a remote caller never passes: who they are comes from the token, and the server reads and writes no paths for them. */
|
|
55
|
+
export declare const REMOTE_REFUSED_FLAGS: ReadonlySet<string>;
|
|
56
|
+
/**
|
|
57
|
+
* The guide row an argv is: the longest invocation its command and leading positionals spell. A command with no
|
|
58
|
+
* row of its own runs nowhere remotely (backup, restore, storage, ledger, policy and the rest).
|
|
59
|
+
*/
|
|
60
|
+
export declare function remoteRowOf(command: string, positional: readonly string[]): CommandRow | null;
|
|
61
|
+
/**
|
|
62
|
+
* Re-prove the principal now, against the store: the account still stands at the same generation, the token is
|
|
63
|
+
* the person's, unrevoked and unexpired, and the scope is no wider than the token's or the person's role.
|
|
64
|
+
*/
|
|
65
|
+
export declare function reproveRemote(store: Store, principal: Principal, now: Date): {
|
|
66
|
+
ok: true;
|
|
67
|
+
scope: "read" | "act";
|
|
68
|
+
tokenName: string;
|
|
69
|
+
} | {
|
|
70
|
+
ok: false;
|
|
71
|
+
};
|
|
72
|
+
/** Whether the person may use `repo`: inside their token's projects and their account's current access. Null is "every project". */
|
|
73
|
+
export declare function remoteAllows(store: Store, principal: Principal, repo: string | null): boolean;
|
|
74
|
+
/** The project a command touches, or why it may not run: --repo must be a project the server knows, by its exact path. */
|
|
75
|
+
export declare function remoteProjectOf(store: Store, run: {
|
|
76
|
+
allows: (repo: string | null) => boolean;
|
|
77
|
+
}, scope: RemoteScope, positional: readonly string[], repos: readonly string[], invocation: string, flags: ReadonlyMap<string, string | true>, files: Readonly<Record<string, string>>): {
|
|
78
|
+
ok: true;
|
|
79
|
+
repo: string | null;
|
|
80
|
+
taskId: string | null;
|
|
81
|
+
} | {
|
|
82
|
+
ok: false;
|
|
83
|
+
reason: string;
|
|
84
|
+
message: string;
|
|
85
|
+
};
|
|
86
|
+
export declare function remoteSecret(): string;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Running a command on a central server for a person who signed in with their own API token
|
|
3
|
+
* (`runOperateAs` in operate.ts). The machine contract is the allowlist: every row states `remote`
|
|
4
|
+
* (src/surface.ts), and only "yes" rows run. This module holds what that boundary decides before any
|
|
5
|
+
* command code runs — which row an argv is, whether the person and token still stand, and which
|
|
6
|
+
* project the command touches — and the per-run state the credential paths in operate.ts consult so
|
|
7
|
+
* a remote run is always the person and never this machine's owner.
|
|
8
|
+
*/
|
|
9
|
+
import { randomBytes } from "node:crypto";
|
|
10
|
+
import { isAbsolute } from "node:path";
|
|
11
|
+
import { COMMAND_GUIDE } from "./surface.js";
|
|
12
|
+
import { remoteArgumentsOf, REMOTE_NOT_FOUND } from "./operate-remote-arguments.js";
|
|
13
|
+
export { activeRemote, withRemote } from "./remote-run.js";
|
|
14
|
+
/** Thrown where a remote run reaches something only this machine's owner may use; the command ends refused. */
|
|
15
|
+
export class RemoteRefusal extends Error {
|
|
16
|
+
reason;
|
|
17
|
+
constructor(reason, message) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.reason = reason;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export const REMOTE_MESSAGES = {
|
|
23
|
+
"step-up": "Approvals, people and policy changes aren't taken from a token: approve in the console or chat.",
|
|
24
|
+
no: "That command only runs on the server's own machine, not for a remote caller.",
|
|
25
|
+
unknown: "That isn't a command a remote caller can run.",
|
|
26
|
+
read: "Your token reads only. Use an act token for this command.",
|
|
27
|
+
stale: "Your sign-in changed since this token was checked. Sign in again.",
|
|
28
|
+
credential: "Remote commands run as your token's person: leave out --as, --token, --db and other credential or file flags.",
|
|
29
|
+
project: "Name a project you have access to with --repo (its path on the server).",
|
|
30
|
+
installation: "This command covers every project; it needs access to all projects.",
|
|
31
|
+
files: "Send that file with the request; the server reads no paths of its own for you.",
|
|
32
|
+
};
|
|
33
|
+
const installation = { kind: "installation" }, repo = { kind: "repo" }, self = { kind: "self" }, global = { kind: "global" };
|
|
34
|
+
const references = { kind: "references" };
|
|
35
|
+
/** Every "yes" row's scope. The remote test holds this to the guide's "yes" rows exactly. */
|
|
36
|
+
export const REMOTE_SCOPES = new Map([
|
|
37
|
+
["status", installation], ["integrations", installation], ["ready", installation], ["grants", installation], ["sync", installation],
|
|
38
|
+
["gaps", repo], ["task add", repo],
|
|
39
|
+
...["ask", "checks", "add-tests", "show", "wait", "complete", "revise", "state", "block", "unblock", "next", "steer", "assign", "scope", "plan", "hold",
|
|
40
|
+
"unhold", "require", "requeue", "review", "repair", "route", "reopen", "stop", "resume"].map(action => [`task ${action}`, references]),
|
|
41
|
+
["check-progress", references],
|
|
42
|
+
["task list", installation],
|
|
43
|
+
["runner list", installation], ["coordinator list", installation], ["outbox list", installation], ["incident list", installation], ["incident resolve", installation],
|
|
44
|
+
["cap list", repo], ["cap add", repo],
|
|
45
|
+
...["list", "show", "add", "refresh", "pause", "resume", "run-now"].map(action => [`routine ${action}`, installation]),
|
|
46
|
+
["config show", installation], ["verify show", repo], ["intake show", installation], ["intake run", installation], ["intake preview", installation],
|
|
47
|
+
["intake pr-comments", installation], ["template list", installation], ["template show", installation], ["contest show", installation],
|
|
48
|
+
["contest exclude", installation], ["webhook status", installation], ["webhook test", installation], ["review show", installation],
|
|
49
|
+
["chat-approval show", self], ["chat-approval off", self],
|
|
50
|
+
["knowledge search", repo], ["knowledge impact", repo], ["knowledge refresh", repo],
|
|
51
|
+
...["search", "decisions", "show", "decide", "retire", "review", "status"].map(action => [`memory ${action}`, self]),
|
|
52
|
+
...["list", "show", "export", "create", "edit", "trigger add", "trigger pause", "trigger resume", "trigger remove", "card add", "archive"]
|
|
53
|
+
.map(action => [`flows ${action}`, self]),
|
|
54
|
+
["models status", global], ["models list", global],
|
|
55
|
+
]);
|
|
56
|
+
/** Flags a remote caller never passes: who they are comes from the token, and the server reads and writes no paths for them. */
|
|
57
|
+
export const REMOTE_REFUSED_FLAGS = new Set([
|
|
58
|
+
"db", "as", "token", "token-file", "token-env", "password", "key-file", "credentials", "containment", "out", "project-root", "bin", "port", "host",
|
|
59
|
+
]);
|
|
60
|
+
/**
|
|
61
|
+
* The guide row an argv is: the longest invocation its command and leading positionals spell. A command with no
|
|
62
|
+
* row of its own runs nowhere remotely (backup, restore, storage, ledger, policy and the rest).
|
|
63
|
+
*/
|
|
64
|
+
export function remoteRowOf(command, positional) {
|
|
65
|
+
for (let words = Math.min(positional.length, 2); words >= 0; words -= 1) {
|
|
66
|
+
const invocation = [command, ...positional.slice(0, words)].join(" ");
|
|
67
|
+
const row = COMMAND_GUIDE.find(one => one.invocation === invocation && one.invocation !== "");
|
|
68
|
+
if (row !== undefined)
|
|
69
|
+
return row;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Re-prove the principal now, against the store: the account still stands at the same generation, the token is
|
|
75
|
+
* the person's, unrevoked and unexpired, and the scope is no wider than the token's or the person's role.
|
|
76
|
+
*/
|
|
77
|
+
export function reproveRemote(store, principal, now) {
|
|
78
|
+
if (principal === null || typeof principal !== "object" || principal.kind !== "person" || typeof principal.account !== "string" || typeof principal.tokenId !== "string")
|
|
79
|
+
return { ok: false };
|
|
80
|
+
const account = store.accountOf(principal.account);
|
|
81
|
+
if (account === null || account.revokedAt !== null || account.generation !== principal.generation)
|
|
82
|
+
return { ok: false };
|
|
83
|
+
const token = store.apiTokenSecret(principal.tokenId)?.row ?? null;
|
|
84
|
+
if (token === null || token.account !== principal.account || token.revokedAt !== null || Date.parse(token.expiresAt) <= now.getTime())
|
|
85
|
+
return { ok: false };
|
|
86
|
+
const scope = principal.scope === "act" && token.access === "act" && account.role === "approver" ? "act" : "read";
|
|
87
|
+
return { ok: true, scope, tokenName: token.name };
|
|
88
|
+
}
|
|
89
|
+
/** Whether the person may use `repo`: inside their token's projects and their account's current access. Null is "every project". */
|
|
90
|
+
export function remoteAllows(store, principal, repo) {
|
|
91
|
+
if (principal.projects !== null && (repo === null || !principal.projects.includes(repo)))
|
|
92
|
+
return false;
|
|
93
|
+
return store.accountCanAccess(principal.account, repo);
|
|
94
|
+
}
|
|
95
|
+
/** The project a command touches, or why it may not run: --repo must be a project the server knows, by its exact path. */
|
|
96
|
+
export function remoteProjectOf(store, run, scope, positional, repos, invocation, flags, files) {
|
|
97
|
+
const known = store.knownRepos();
|
|
98
|
+
// The local parser keeps the last raw --repo as well as a split repoList. Require those identities
|
|
99
|
+
// to agree: comma lists and repeated project flags cannot authorize one path and dispatch another.
|
|
100
|
+
const rawRepo = flags.get("repo");
|
|
101
|
+
if (rawRepo !== undefined && (repos.length !== 1 || rawRepo !== repos[0]))
|
|
102
|
+
return { ok: false, reason: "unknown-project", message: REMOTE_MESSAGES.project };
|
|
103
|
+
for (const given of repos) {
|
|
104
|
+
if (!isAbsolute(given) || !known.includes(given) || !run.allows(given))
|
|
105
|
+
return { ok: false, reason: "unknown-project", message: REMOTE_MESSAGES.project };
|
|
106
|
+
}
|
|
107
|
+
const named = repos[0] ?? null;
|
|
108
|
+
const checked = remoteArgumentsOf(store, run.allows, invocation, positional, flags, named, files);
|
|
109
|
+
if (!checked.ok)
|
|
110
|
+
return checked;
|
|
111
|
+
switch (scope.kind) {
|
|
112
|
+
case "global":
|
|
113
|
+
case "self":
|
|
114
|
+
return checked;
|
|
115
|
+
case "installation":
|
|
116
|
+
return run.allows(null) ? { ok: true, repo: named, taskId: null } : { ok: false, reason: "all-projects", message: REMOTE_MESSAGES.installation };
|
|
117
|
+
case "repo":
|
|
118
|
+
return named === null ? { ok: false, reason: "unknown-project", message: REMOTE_MESSAGES.project } : checked;
|
|
119
|
+
case "references":
|
|
120
|
+
return checked.repo === null ? REMOTE_NOT_FOUND : checked;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export function remoteSecret() {
|
|
124
|
+
return `remote:${randomBytes(24).toString("base64url")}`;
|
|
125
|
+
}
|
package/dist/operate.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { type Store } from "./store.js";
|
|
|
8
8
|
import { type DispatchAdapter } from "./sync.js";
|
|
9
9
|
import { type TelegramTransport } from "./telegram.js";
|
|
10
10
|
import { type MateCliSeams } from "./mate-cli.js";
|
|
11
|
+
import { type Principal, type RemoteSource } from "./operate-remote.js";
|
|
11
12
|
import { type PublishExec } from "./publish.js";
|
|
12
13
|
import { type Runner as CommandRunner } from "./builder.js";
|
|
13
14
|
import { type ReleaseIo } from "./releases.js";
|
|
@@ -123,6 +124,26 @@ export declare function parseOperateArgs(argv: readonly string[], ownValues?: Re
|
|
|
123
124
|
};
|
|
124
125
|
/** Route an `operate` command. Returns the process exit code. */
|
|
125
126
|
export declare function runOperate(command: string, argv: readonly string[], write: Write, options?: OperateOptions): Promise<number>;
|
|
127
|
+
export type { Principal };
|
|
128
|
+
/**
|
|
129
|
+
* Run one command on this server for a person who signed in with their own API token (the transport and MCP
|
|
130
|
+
* gateway prove the `so_` token and refuse browser origins; this is everything after). The command's row in the
|
|
131
|
+
* machine contract decides first: "no" and "step-up" rows, and commands with no row, are refused before any
|
|
132
|
+
* command code runs. The principal is re-proved against the store, a read token runs only reads, and the project
|
|
133
|
+
* the command touches must be one the person may use, named by --repo where a local run would use the working
|
|
134
|
+
* directory. The command then runs as that person: every credential check resolves to them, nothing prompts, and
|
|
135
|
+
* this machine's saved login and lead token are never read. Files are only those sent in `files`, keyed by the
|
|
136
|
+
* argument that names them. Every call is in the action history as the person, from source `api` (or `mcp`),
|
|
137
|
+
* with the token's name. The store is the caller's and stays open.
|
|
138
|
+
*/
|
|
139
|
+
export declare function runOperateAs(argv: string[], opts: {
|
|
140
|
+
principal: Principal;
|
|
141
|
+
store: Store;
|
|
142
|
+
write: (s: string) => void;
|
|
143
|
+
files?: Record<string, string>;
|
|
144
|
+
source?: RemoteSource;
|
|
145
|
+
options?: OperateOptions;
|
|
146
|
+
}): Promise<number>;
|
|
126
147
|
/** Durably create the login file BEFORE the bootstrap commits (arc 2
|
|
127
148
|
* finding 27): exclusive, 0600, fsynced — file and directory both — so the
|
|
128
149
|
* row only ever follows a durable secret. Throws on any failure. */
|
|
@@ -142,4 +163,3 @@ export declare function readUpConsoleUrl(dir: string): string | null;
|
|
|
142
163
|
* missing credentials stay a usage refusal, exactly as before.
|
|
143
164
|
*/
|
|
144
165
|
export declare const PROPOSALS_ACTIONS: readonly ["list", "confirm", "dismiss"];
|
|
145
|
-
export {};
|