toolroll 0.9.43 → 0.9.45

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.
Files changed (50) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/action-ledger.d.ts +5 -2
  3. package/dist/action-ledger.js +13 -3
  4. package/dist/cli-http.d.ts +53 -0
  5. package/dist/cli-http.js +143 -0
  6. package/dist/cli.d.ts +3 -0
  7. package/dist/cli.js +7 -0
  8. package/dist/contracts/cli.d.ts +13 -0
  9. package/dist/contracts/cli.js +7 -0
  10. package/dist/contracts/gateway-tools.d.ts +82 -0
  11. package/dist/contracts/gateway-tools.js +33 -0
  12. package/dist/desktop-update.d.ts +4 -2
  13. package/dist/desktop-update.js +7 -6
  14. package/dist/dispatch.d.ts +2 -2
  15. package/dist/dispatch.js +8 -4
  16. package/dist/lead-status.d.ts +7 -3
  17. package/dist/lead-status.js +21 -12
  18. package/dist/mcp-core.d.ts +110 -0
  19. package/dist/mcp-core.js +311 -0
  20. package/dist/mcp-http.d.ts +26 -0
  21. package/dist/mcp-http.js +151 -0
  22. package/dist/mcp-person.d.ts +31 -0
  23. package/dist/mcp-person.js +96 -0
  24. package/dist/mcp.d.ts +1 -8
  25. package/dist/mcp.js +21 -322
  26. package/dist/operate-remote-arguments.d.ts +36 -0
  27. package/dist/operate-remote-arguments.js +185 -0
  28. package/dist/operate-remote.d.ts +98 -0
  29. package/dist/operate-remote.js +145 -0
  30. package/dist/operate.d.ts +21 -1
  31. package/dist/operate.js +206 -19
  32. package/dist/remote-command.d.ts +45 -0
  33. package/dist/remote-command.js +124 -0
  34. package/dist/remote-exec.d.ts +18 -0
  35. package/dist/remote-exec.js +241 -0
  36. package/dist/remote-run.d.ts +20 -0
  37. package/dist/remote-run.js +13 -0
  38. package/dist/scope.js +10 -0
  39. package/dist/serve.d.ts +7 -0
  40. package/dist/serve.js +24 -0
  41. package/dist/store.d.ts +15 -1
  42. package/dist/store.js +34 -7
  43. package/dist/surface.d.ts +10 -3
  44. package/dist/surface.js +67 -2
  45. package/dist/team-cli.d.ts +19 -0
  46. package/dist/team-cli.js +20 -3
  47. package/dist/toolroll-update.js +4 -1
  48. package/dist/work-summary.d.ts +4 -1
  49. package/dist/work-summary.js +1 -1
  50. package/package.json +1 -1
@@ -0,0 +1,98 @@
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
+ * listing — lists across projects: someone with access to all of them sees everything, as locally; anyone else
37
+ * sees only their own projects (the run's `lens`), with every count taken over those projects alone.
38
+ * repo — needs --repo, naming a project the server knows and the person may use.
39
+ * references — the audited arguments resolve every resource to its stored project before dispatch.
40
+ * self — the command already narrows to the person's own projects (their access is passed in).
41
+ * global — touches no project's data.
42
+ */
43
+ export type RemoteScope = {
44
+ kind: "installation";
45
+ } | {
46
+ kind: "listing";
47
+ } | {
48
+ kind: "repo";
49
+ } | {
50
+ kind: "references";
51
+ } | {
52
+ kind: "self";
53
+ } | {
54
+ kind: "global";
55
+ };
56
+ /** Every "yes" row's scope. The remote test holds this to the guide's "yes" rows exactly. */
57
+ export declare const REMOTE_SCOPES: ReadonlyMap<string, RemoteScope>;
58
+ /**
59
+ * Why each "installation" row still needs access to every project rather than listing a person's own. The remote
60
+ * test holds this to the table's "installation" rows exactly, so a newly allowed cross-project command has to be
61
+ * scoped as a listing or stated here; it cannot skip project scoping by omission.
62
+ */
63
+ export declare const REMOTE_ALL_PROJECTS_ONLY: ReadonlyMap<string, "installation-wide" | "not yet scoped">;
64
+ /** The projects a listing or task reference may disclose: null for someone with access to all of them. */
65
+ export declare function remoteLensOf(store: Store, scope: RemoteScope, allows: (repo: string | null) => boolean): readonly string[] | null;
66
+ /** Flags a remote caller never passes: who they are comes from the token, and the server reads and writes no paths for them. */
67
+ export declare const REMOTE_REFUSED_FLAGS: ReadonlySet<string>;
68
+ /**
69
+ * The guide row an argv is: the longest invocation its command and leading positionals spell. A command with no
70
+ * row of its own runs nowhere remotely (backup, restore, storage, ledger, policy and the rest).
71
+ */
72
+ export declare function remoteRowOf(command: string, positional: readonly string[]): CommandRow | null;
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 declare function reproveRemote(store: Store, principal: Principal, now: Date): {
78
+ ok: true;
79
+ scope: "read" | "act";
80
+ tokenName: string;
81
+ } | {
82
+ ok: false;
83
+ };
84
+ /** Whether the person may use `repo`: inside their token's projects and their account's current access. Null is "every project". */
85
+ export declare function remoteAllows(store: Store, principal: Principal, repo: string | null): boolean;
86
+ /** The project a command touches, or why it may not run: --repo must be a project the server knows, by its exact path. */
87
+ export declare function remoteProjectOf(store: Store, run: {
88
+ allows: (repo: string | null) => boolean;
89
+ }, scope: RemoteScope, positional: readonly string[], repos: readonly string[], invocation: string, flags: ReadonlyMap<string, string | true>, files: Readonly<Record<string, string>>): {
90
+ ok: true;
91
+ repo: string | null;
92
+ taskId: string | null;
93
+ } | {
94
+ ok: false;
95
+ reason: string;
96
+ message: string;
97
+ };
98
+ export declare function remoteSecret(): string;
@@ -0,0 +1,145 @@
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" }, listing = { kind: "listing" };
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", listing], ["integrations", installation], ["ready", listing], ["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", listing],
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
+ /**
57
+ * Why each "installation" row still needs access to every project rather than listing a person's own. The remote
58
+ * test holds this to the table's "installation" rows exactly, so a newly allowed cross-project command has to be
59
+ * scoped as a listing or stated here; it cannot skip project scoping by omission.
60
+ */
61
+ export const REMOTE_ALL_PROJECTS_ONLY = new Map([
62
+ // Settings, connections and acts that belong to the installation as a whole, not to any one project.
63
+ ...["integrations", "grants", "sync", "incident resolve", "routine add", "routine refresh", "routine pause", "routine resume", "routine run-now",
64
+ "config show", "intake run", "contest exclude", "webhook status", "webhook test"].map(row => [row, "installation-wide"]),
65
+ // Cross-project lists and reads a limited person is still refused; scoping them is later work.
66
+ ...["runner list", "coordinator list", "outbox list", "incident list", "routine list", "routine show", "intake show", "intake preview",
67
+ "intake pr-comments", "template list", "template show", "contest show", "review show"].map(row => [row, "not yet scoped"]),
68
+ ]);
69
+ /** The projects a listing or task reference may disclose: null for someone with access to all of them. */
70
+ export function remoteLensOf(store, scope, allows) {
71
+ if ((scope.kind !== "listing" && scope.kind !== "references") || allows(null))
72
+ return null;
73
+ return Object.freeze(store.knownRepos().filter(repo => allows(repo)));
74
+ }
75
+ /** Flags a remote caller never passes: who they are comes from the token, and the server reads and writes no paths for them. */
76
+ export const REMOTE_REFUSED_FLAGS = new Set([
77
+ "db", "as", "token", "token-file", "token-env", "password", "key-file", "credentials", "containment", "out", "project-root", "bin", "port", "host",
78
+ ]);
79
+ /**
80
+ * The guide row an argv is: the longest invocation its command and leading positionals spell. A command with no
81
+ * row of its own runs nowhere remotely (backup, restore, storage, ledger, policy and the rest).
82
+ */
83
+ export function remoteRowOf(command, positional) {
84
+ for (let words = Math.min(positional.length, 2); words >= 0; words -= 1) {
85
+ const invocation = [command, ...positional.slice(0, words)].join(" ");
86
+ const row = COMMAND_GUIDE.find(one => one.invocation === invocation && one.invocation !== "");
87
+ if (row !== undefined)
88
+ return row;
89
+ }
90
+ return null;
91
+ }
92
+ /**
93
+ * Re-prove the principal now, against the store: the account still stands at the same generation, the token is
94
+ * the person's, unrevoked and unexpired, and the scope is no wider than the token's or the person's role.
95
+ */
96
+ export function reproveRemote(store, principal, now) {
97
+ if (principal === null || typeof principal !== "object" || principal.kind !== "person" || typeof principal.account !== "string" || typeof principal.tokenId !== "string")
98
+ return { ok: false };
99
+ const account = store.accountOf(principal.account);
100
+ if (account === null || account.revokedAt !== null || account.generation !== principal.generation)
101
+ return { ok: false };
102
+ const token = store.apiTokenSecret(principal.tokenId)?.row ?? null;
103
+ if (token === null || token.account !== principal.account || token.revokedAt !== null || Date.parse(token.expiresAt) <= now.getTime())
104
+ return { ok: false };
105
+ const scope = principal.scope === "act" && token.access === "act" && account.role === "approver" ? "act" : "read";
106
+ return { ok: true, scope, tokenName: token.name };
107
+ }
108
+ /** Whether the person may use `repo`: inside their token's projects and their account's current access. Null is "every project". */
109
+ export function remoteAllows(store, principal, repo) {
110
+ if (principal.projects !== null && (repo === null || !principal.projects.includes(repo)))
111
+ return false;
112
+ return store.accountCanAccess(principal.account, repo);
113
+ }
114
+ /** The project a command touches, or why it may not run: --repo must be a project the server knows, by its exact path. */
115
+ export function remoteProjectOf(store, run, scope, positional, repos, invocation, flags, files) {
116
+ const known = store.knownRepos();
117
+ // The local parser keeps the last raw --repo as well as a split repoList. Require those identities
118
+ // to agree: comma lists and repeated project flags cannot authorize one path and dispatch another.
119
+ const rawRepo = flags.get("repo");
120
+ if (rawRepo !== undefined && (repos.length !== 1 || rawRepo !== repos[0]))
121
+ return { ok: false, reason: "unknown-project", message: REMOTE_MESSAGES.project };
122
+ for (const given of repos) {
123
+ if (!isAbsolute(given) || !known.includes(given) || !run.allows(given))
124
+ return { ok: false, reason: "unknown-project", message: REMOTE_MESSAGES.project };
125
+ }
126
+ const named = repos[0] ?? null;
127
+ const checked = remoteArgumentsOf(store, run.allows, invocation, positional, flags, named, files);
128
+ if (!checked.ok)
129
+ return checked;
130
+ switch (scope.kind) {
131
+ case "global":
132
+ case "self":
133
+ case "listing":
134
+ return checked;
135
+ case "installation":
136
+ return run.allows(null) ? { ok: true, repo: named, taskId: null } : { ok: false, reason: "all-projects", message: REMOTE_MESSAGES.installation };
137
+ case "repo":
138
+ return named === null ? { ok: false, reason: "unknown-project", message: REMOTE_MESSAGES.project } : checked;
139
+ case "references":
140
+ return checked.repo === null ? REMOTE_NOT_FOUND : checked;
141
+ }
142
+ }
143
+ export function remoteSecret() {
144
+ return `remote:${randomBytes(24).toString("base64url")}`;
145
+ }
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 {};