toolroll 0.9.61 → 0.9.63

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 (117) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +51 -3
  3. package/dist/agent-fence.d.ts +155 -10
  4. package/dist/agent-fence.js +473 -38
  5. package/dist/browser/workspace.css +1 -1
  6. package/dist/browser/workspace.js +21 -21
  7. package/dist/browser-workspace.d.ts +28 -0
  8. package/dist/chat-actions.js +2 -2
  9. package/dist/check-levels.js +3 -0
  10. package/dist/claim.js +1 -1
  11. package/dist/cli-help.d.ts +1 -1
  12. package/dist/cli-help.js +1 -0
  13. package/dist/cli.js +31 -11
  14. package/dist/contracts/cli.d.ts +8 -0
  15. package/dist/contracts/cli.js +3 -1
  16. package/dist/contracts/console-api.d.ts +19 -1
  17. package/dist/contracts/console-api.js +2 -1
  18. package/dist/contracts/integration-metadata.d.ts +2 -1
  19. package/dist/contracts/integration-metadata.js +2 -1
  20. package/dist/desktop-update.d.ts +1 -1
  21. package/dist/dispatch.js +7 -4
  22. package/dist/doctor.d.ts +53 -0
  23. package/dist/doctor.js +427 -0
  24. package/dist/evidence-pack.d.ts +1 -1
  25. package/dist/evidence-pack.js +2 -2
  26. package/dist/exec.d.ts +3 -0
  27. package/dist/exec.js +5 -4
  28. package/dist/flow-code.d.ts +2 -2
  29. package/dist/flow-code.js +4 -15
  30. package/dist/flow-engine.js +11 -6
  31. package/dist/flow-triggers.d.ts +8 -4
  32. package/dist/flow-triggers.js +45 -25
  33. package/dist/flow-unattended-words.d.ts +28 -0
  34. package/dist/flow-unattended-words.js +50 -0
  35. package/dist/flow-unattended.d.ts +227 -0
  36. package/dist/flow-unattended.js +776 -0
  37. package/dist/flows-cli.d.ts +28 -1
  38. package/dist/flows-cli.js +55 -10
  39. package/dist/flows-ui.d.ts +2 -1
  40. package/dist/flows-ui.js +4 -1
  41. package/dist/google-mail.d.ts +1 -0
  42. package/dist/google-mail.js +61 -18
  43. package/dist/integrations-ui.d.ts +2 -1
  44. package/dist/integrations-ui.js +6 -3
  45. package/dist/integrations.d.ts +17 -3
  46. package/dist/integrations.js +38 -18
  47. package/dist/invoke.js +300 -168
  48. package/dist/landed-cleanup.d.ts +26 -12
  49. package/dist/landed-cleanup.js +199 -85
  50. package/dist/lead-contract.d.ts +1 -1
  51. package/dist/lead-contract.js +3 -3
  52. package/dist/lead-tools.js +11 -3
  53. package/dist/mcp-connect.d.ts +8 -7
  54. package/dist/mcp-connect.js +49 -51
  55. package/dist/oauth-run-token.d.ts +51 -0
  56. package/dist/oauth-run-token.js +127 -0
  57. package/dist/operate.js +74 -17
  58. package/dist/plan-auto.d.ts +11 -0
  59. package/dist/plan-auto.js +62 -11
  60. package/dist/process-recovery-eligibility.d.ts +1 -1
  61. package/dist/project-access.d.ts +3 -0
  62. package/dist/project-access.js +3 -0
  63. package/dist/project-admission.d.ts +10 -6
  64. package/dist/project-admission.js +33 -11
  65. package/dist/project-delete-ui.d.ts +1 -0
  66. package/dist/project-delete-ui.js +4 -1
  67. package/dist/project-delete.d.ts +3 -1
  68. package/dist/project-delete.js +10 -4
  69. package/dist/project-file.d.ts +3 -0
  70. package/dist/project-file.js +16 -0
  71. package/dist/project-tools.d.ts +99 -25
  72. package/dist/project-tools.js +394 -98
  73. package/dist/provider-home.d.ts +22 -0
  74. package/dist/provider-home.js +99 -0
  75. package/dist/provider.d.ts +17 -3
  76. package/dist/provider.js +34 -10
  77. package/dist/remote-main.d.ts +61 -0
  78. package/dist/remote-main.js +179 -0
  79. package/dist/scope.d.ts +1 -1
  80. package/dist/serve.js +11 -3
  81. package/dist/server/flows.js +27 -5
  82. package/dist/server/render-tasks.d.ts +3 -0
  83. package/dist/server/render-tasks.js +3 -0
  84. package/dist/server/route-table.js +3 -0
  85. package/dist/server/runtime.d.ts +1 -1
  86. package/dist/server/settings.js +14 -5
  87. package/dist/server/task-views.js +2 -0
  88. package/dist/storage-sweep-child.js +3 -1
  89. package/dist/storage-sweep-process.d.ts +14 -0
  90. package/dist/storage-sweep-process.js +37 -3
  91. package/dist/storage-sweep-schedule.d.ts +30 -0
  92. package/dist/storage-sweep-schedule.js +164 -0
  93. package/dist/storage-sweep.d.ts +6 -5
  94. package/dist/storage-sweep.js +10 -70
  95. package/dist/storage-ui.d.ts +2 -0
  96. package/dist/storage-ui.js +7 -4
  97. package/dist/store.d.ts +75 -7
  98. package/dist/store.js +241 -17
  99. package/dist/subagent-tools.d.ts +1 -1
  100. package/dist/surface.js +10 -6
  101. package/dist/task-base.d.ts +35 -0
  102. package/dist/task-base.js +51 -0
  103. package/dist/teardown-git.d.ts +14 -0
  104. package/dist/teardown-git.js +119 -0
  105. package/dist/tool-broker.d.ts +37 -0
  106. package/dist/tool-broker.js +369 -0
  107. package/dist/tool-launcher.js +5 -4
  108. package/dist/tool-secret-store.d.ts +170 -0
  109. package/dist/tool-secret-store.js +834 -0
  110. package/dist/toolroll-update.js +4 -2
  111. package/dist/tools-ui.d.ts +7 -1
  112. package/dist/tools-ui.js +17 -8
  113. package/dist/work-index.js +5 -3
  114. package/dist/worktree.d.ts +64 -6
  115. package/dist/worktree.js +171 -35
  116. package/package.json +3 -1
  117. package/scripts/provider-canary.mjs +210 -31
@@ -14,7 +14,7 @@
14
14
  */
15
15
  import { existsSync, lstatSync, rmSync } from "node:fs";
16
16
  import { isAbsolute, relative, resolve } from "node:path";
17
- import { run } from "./exec.js";
17
+ import { teardownGit, refusal } from "./teardown-git.js";
18
18
  import { BUILT_IN } from "./store.js";
19
19
  import { BRANCH_PREFIX, BRANCH_PREFIXES, isOwnBranch } from "./names.js";
20
20
  /** The tables whose history is the audit record itself: never touched. */
@@ -100,7 +100,6 @@ export function projectRunning(store, repo, now) {
100
100
  ...(checkouts > 0 && building === 0 ? [`${plural(checkouts, "checkout")} ${checkouts === 1 ? "is" : "are"} in use`] : []),
101
101
  ];
102
102
  }
103
- const defaultGit = (args, cwd) => run("git", ["--no-optional-locks", ...args], { cwd, timeoutMs: 60_000 });
104
103
  /** Whether `path` is inside `root` (not the root itself). */
105
104
  const inside = (root, path) => { const rel = relative(resolve(root), resolve(path)); return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel); };
106
105
  /**
@@ -121,7 +120,10 @@ async function removeCheckoutsAndBranches(store, repo, d, git, poolRoot) {
121
120
  const ours = new Set(named);
122
121
  const left = [];
123
122
  let checkouts = 0, branches = 0;
124
- const isRepo = existsSync(repo) && (await git(["rev-parse", "--git-dir"], repo)).code === 0;
123
+ const probe = existsSync(repo) ? await git(["rev-parse", "--git-dir"], repo) : null;
124
+ if (probe !== null && refusal(probe) !== null)
125
+ return { checkouts, branches, left: [refusal(probe)] };
126
+ const isRepo = probe?.code === 0;
125
127
  if (!isRepo) {
126
128
  // No repository to ask: only checkouts in Toolroll's own folder go.
127
129
  for (const row of recorded) {
@@ -291,6 +293,8 @@ function deleteRows(store, repo, d, now) {
291
293
  // Flows, cards and subagents.
292
294
  del("flow_trigger_event", `${IN("card")} OR trigger IN (SELECT id FROM flow_trigger WHERE ${IN("flow")})`, C, F);
293
295
  del("flow_trigger", IN("flow"), F);
296
+ del("flow_unattended_run", `grant_id IN (SELECT id FROM flow_unattended_grant WHERE ${IN("flow")})`, F);
297
+ del("flow_unattended_grant", IN("flow"), F);
294
298
  for (const table of ["flow_card_watcher", "flow_comment", "flow_event", "flow_mail", "flow_step_run"])
295
299
  del(table, IN("card"), C);
296
300
  del("subagent_question", IN("id"), questions);
@@ -323,7 +327,9 @@ function deleteRows(store, repo, d, now) {
323
327
  * starts while the checkouts and branches go.
324
328
  */
325
329
  export async function deleteProject(store, repo, options) {
326
- const git = options.git ?? defaultGit;
330
+ const hardened = teardownGit(options.runner ?? (options.git === undefined ? undefined :
331
+ (_file, args, execOptions) => options.git(args, execOptions.cwd)));
332
+ const git = (args, cwd) => hardened(cwd, args);
327
333
  const running = projectRunning(store, repo, options.now);
328
334
  if (running.length > 0)
329
335
  return { ok: false, reason: "running", said: `Nothing was deleted: ${running.join(", ")}. Stop it, then try again.`, running };
@@ -0,0 +1,3 @@
1
+ /** A project file a script names, if it is one: inside the project, and there. Kept free of the runner's imports so a
2
+ * flow's "runs on its own" grant (flow-unattended.ts) resolves the very file a script would run. */
3
+ export declare function projectFile(root: string, path: string): string | null;
@@ -0,0 +1,16 @@
1
+ import { realpathSync, statSync } from "node:fs";
2
+ import { isAbsolute, join, relative } from "node:path";
3
+ /** A project file a script names, if it is one: inside the project, and there. Kept free of the runner's imports so a
4
+ * flow's "runs on its own" grant (flow-unattended.ts) resolves the very file a script would run. */
5
+ export function projectFile(root, path) {
6
+ if (path === "" || isAbsolute(path) || path.split(/[\\/]/).includes(".."))
7
+ return null;
8
+ try {
9
+ const base = realpathSync(root), full = realpathSync(join(root, path));
10
+ const inside = relative(base, full);
11
+ return inside !== "" && !inside.startsWith("..") && !isAbsolute(inside) && statSync(full).isFile() ? full : null;
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
@@ -1,4 +1,7 @@
1
1
  import type { Store, ToolActionInfo } from "./store.js";
2
+ import { type BrokerOptions } from "./tool-broker.js";
3
+ import type { FenceMethod } from "./agent-fence.js";
4
+ import { type SecretStoreKind } from "./tool-secret-store.js";
2
5
  import { type ToolSecret, type ToolSpec } from "./contracts/integration-metadata.js";
3
6
  export type { ToolSecret, ToolSpec };
4
7
  export type ToolTest = {
@@ -55,12 +58,36 @@ export declare function toolDigest(spec: ToolSpec): string;
55
58
  export declare function validateToolSpec(input: Record<string, unknown>): ToolSpec;
56
59
  /** How the tool starts, in one line a person can check. Never a secret value. */
57
60
  export declare function toolCommandLine(spec: ToolSpec): string;
58
- export declare function toolSecretsDir(home?: string): string;
59
- export declare function readToolSecrets(repo: string, tool: string, home?: string): Record<string, string>;
61
+ /**
62
+ * Where tool secrets were kept before (an existing ~/.toolroll/tool-secrets, or an older name's), read once when
63
+ * Toolroll starts so their files move in on first use. Never where new secrets go.
64
+ */
65
+ export declare function legacyToolSecretsDir(home?: string): string | null;
66
+ /** Commands that can start builds or serve the console: Toolroll's folder for secrets is made before any build runs. */
67
+ export declare const SECRETS_FIRST: ReadonlySet<string>;
68
+ /**
69
+ * Toolroll's folder for the secrets it holds, beside `databaseFile`, checked against the sealing key the database
70
+ * recorded: made now when `eager` (a command that can start builds), else the first time a secret is used.
71
+ * A folder that can't be used is said in Settings and `toolroll integrations`; every secret is refused until then.
72
+ */
73
+ export declare function startToolSecrets(store: Pick<Store, "toolSecretKeyDigest" | "swapToolSecretKeyDigest" | "toolSecretLegacyDir">, databaseFile: string, clock: () => Date, options: {
74
+ eager: boolean;
75
+ }): void;
76
+ /** Toolroll's folder for secrets (tool-secret-store.ts): beside the database Toolroll started with, or a test's `state`. */
77
+ export declare function toolSecretsDir(state?: string): string;
78
+ /** A tool's secret values. Read only to test it, change it or start a run: a page reads which are set (secretsSetFor). */
79
+ export declare function readToolSecrets(repo: string, tool: string, state?: string): Record<string, string>;
60
80
  /** Set (or, with an empty value, clear) one secret of one tool. */
61
- export declare function setToolSecret(repo: string, tool: string, name: string, value: string, home?: string): void;
62
- export declare function setToolSecrets(repo: string, tool: string, values: Record<string, string>, home?: string): void;
63
- export declare function clearToolSecrets(repo: string, tool: string, home?: string): void;
81
+ export declare function setToolSecret(repo: string, tool: string, name: string, value: string, state?: string): void;
82
+ /** Set (or, with empty values, clear) some secrets of one tool, in one change. */
83
+ export declare function setToolSecrets(repo: string, tool: string, values: Record<string, string>, state?: string): void;
84
+ export declare function clearToolSecrets(repo: string, tool: string, state?: string): void;
85
+ /**
86
+ * One renewal of a sign-in at a time, across Toolroll's processes (a lock in its folder) and within one: a service
87
+ * that rotates its refresh token on every use would otherwise see the same one twice, and whichever answer was saved
88
+ * last would be the newest only by luck.
89
+ */
90
+ export declare function renewingSignIn<T>(repo: string, tool: string, state: string | undefined, work: () => Promise<T>): Promise<T>;
64
91
  /** Required secrets with no value yet: the tool is left out of builds until they are set. */
65
92
  export declare function missingSecrets(spec: ToolSpec, values: Record<string, string>): string[];
66
93
  /** A server already configured on this computer, ready to add to a project (values moved, never shown). */
@@ -123,42 +150,53 @@ export declare function callProjectTool(spec: ToolSpec, values: Record<string, s
123
150
  env?: NodeJS.ProcessEnv;
124
151
  omitEnv?: readonly string[];
125
152
  }): Promise<ToolCall>;
126
- /** The exact servers one launch gets, and those left out with why. */
153
+ /** The exact servers one launch gets, and those left out with why (`repo`: the project, from the run's record). */
127
154
  export type ToolLaunch = {
128
155
  tools: ResolvedTool[];
129
156
  skipped: {
130
157
  name: string;
131
158
  reason: string;
132
159
  }[];
160
+ repo?: string;
133
161
  };
134
162
  /** Everything a provider spawn needs to use exactly these tools, and the
135
- * cleanup. `privateDir` holds the run's secret launch files: Codex starts
136
- * tool servers outside its command sandbox, so its agent can be fenced out
137
- * of that folder while the tools still read it. */
163
+ * cleanup. `privateDir` holds the run's launch files, which name the broker
164
+ * and its token, never a tool's secret (tool-broker.ts). */
138
165
  export type ToolLaunchArgs = {
139
166
  argv: string[];
140
167
  env: Record<string, string>;
141
168
  omitEnv: string[];
142
169
  cleanup: () => void;
143
170
  privateDir?: string;
171
+ serverPids?: () => number[];
172
+ };
173
+ /** How a launch's tools are offered: research steps' servers only their read-only actions. */
174
+ export type ToolLaunchOptions = {
175
+ broker?: BrokerOptions;
176
+ enabledTools?: Readonly<Record<string, readonly string[]>>;
144
177
  };
145
- /** Claude: only these servers, written to a private 0600 file removed after the run (the provider adds `--strict-mcp-config`). */
146
- export declare function claudeToolArgs(launch: ToolLaunch): ToolLaunchArgs;
178
+ /**
179
+ * Claude: only these servers, in a private 0600 file removed after the run
180
+ * (the provider adds `--strict-mcp-config`). Each is the broker's address
181
+ * with this launch's token: Claude and its shell share one sandbox, so the
182
+ * file holds nothing a tool signs in with.
183
+ */
184
+ export declare function claudeToolArgs(launch: ToolLaunch, options?: ToolLaunchOptions): Promise<ToolLaunchArgs>;
147
185
  /** Where the tool launcher lives beside this module, built. */
148
186
  export declare function toolLauncherPath(): string;
149
187
  /**
150
- * Codex: each tool is named by `-c` and started through the launcher. The
151
- * operator's config.toml, rules, apps, plugins and hooks are already off
152
- * (the provider's CODEX_BUILD_ISOLATION_ARGV), and nothing of config.toml
153
- * is carried. Codex gives its own environment to the agent's shell whatever
154
- * its policy says, so no secret rides it: each tool's values sit in the
155
- * run's private 0600 file and reach only that tool's process.
188
+ * Codex: each tool is named by `-c` and started through the launcher, which
189
+ * relays it to the launch's broker. The operator's config.toml, rules, apps,
190
+ * plugins and hooks are already off (the provider's
191
+ * CODEX_BUILD_ISOLATION_ARGV), and nothing of config.toml is carried. Codex
192
+ * gives its own environment to the agent's shell whatever its policy says,
193
+ * so nothing rides it: the launch files name the broker and its token, and
194
+ * the folder holding them is fenced from the agent.
156
195
  */
157
- export declare function codexToolArgs(launch: ToolLaunch, options?: {
196
+ export declare function codexToolArgs(launch: ToolLaunch, options?: ToolLaunchOptions & {
158
197
  launcher?: string;
159
198
  node?: string;
160
- enabledTools?: Readonly<Record<string, readonly string[]>>;
161
- }): ToolLaunchArgs;
199
+ }): Promise<ToolLaunchArgs>;
162
200
  /** Gemini: no way yet to hand it exactly these servers, so it gets none. */
163
201
  export declare const GEMINI_NO_TOOLS_ARGV: readonly string[];
164
202
  type ToolRows = Pick<Store, "projectTools">;
@@ -171,6 +209,20 @@ export declare function projectToolsOf(store: ToolRows, repo: string): ProjectTo
171
209
  * left out says why, in words the task page can show.
172
210
  */
173
211
  export declare function toolLaunchFor(store: Pick<Store, "projectTools" | "getRun" | "refById" | "getScope" | "toolSealFor" | "orgPolicy">, runId: number, home?: string): ToolLaunch;
212
+ /** What stops a build reading a local tool server's environment: this computer's platform and the OS fence its run
213
+ * gets, both fixed by the gateway before any tool is prepared (never from a launch's own files or the checkout). */
214
+ export type ToolContainment = {
215
+ platform: NodeJS.Platform;
216
+ fence: FenceMethod;
217
+ };
218
+ /**
219
+ * Why a local tool that carries secrets can't start for this launch, or null when it can. Its server's environment
220
+ * holds the values, and a process of the same user can read another's (KERN_PROCARGS2, /proc/<pid>/environ) unless
221
+ * an OS fence stops it: only the macOS sandbox around a Claude build does. Codex's filesystem policy can't deny that
222
+ * read (OpenRouter uses the same harness); Claude's own file rules and Linux's bubblewrap don't stop it, and on Linux
223
+ * hidepid hides nothing from the same user, so Linux never starts one.
224
+ */
225
+ export declare function secretToolRefusal(provider: "claude" | "codex" | "openrouter" | "gemini", containment: ToolContainment): string | null;
174
226
  /**
175
227
  * One attempt's tools, ready for its provider, and recorded on the run:
176
228
  * what it launched with and what was left out and why. The caller spawns
@@ -179,16 +231,22 @@ export declare function toolLaunchFor(store: Pick<Store, "projectTools" | "getRu
179
231
  export declare function prepareRunTools(store: Pick<Store, "projectTools" | "getRun" | "refById" | "getScope" | "toolSealFor" | "recordRunTools" | "orgPolicy">, runId: number, provider: "claude" | "codex" | "openrouter" | "gemini", options: {
180
232
  home?: string;
181
233
  now: Date;
234
+ containment: ToolContainment;
182
235
  readOnly?: Readonly<Record<string, readonly string[]>>;
183
236
  withheld?: Readonly<Record<string, string>>;
184
- }): ToolLaunchArgs;
237
+ broker?: BrokerOptions;
238
+ fetcher?: typeof fetch;
239
+ }): Promise<ToolLaunchArgs>;
240
+ /** A run whose sign-ins couldn't all be made and kept: it doesn't start, and every token made for it is revoked. */
241
+ export declare class RunSignInError extends Error {
242
+ }
185
243
  /** When a launch's tools cannot be prepared, it still gets none of anything else's: the same isolation with an empty list. */
186
244
  export declare function noToolsArgs(provider: "claude" | "codex" | "openrouter" | "gemini"): ToolLaunchArgs;
187
245
  type ToolWrites = Pick<Store, "projectTools" | "addProjectTool" | "removeProjectTool" | "recordProjectToolTest">;
188
246
  /** Add one tool (and, for an import, the values it brings) to a project's builds. */
189
247
  export declare function addToolTo(store: ToolWrites, repo: string, input: ToolSpec, source: string, by: string, now: Date, options?: {
190
248
  values?: Record<string, string>;
191
- home?: string;
249
+ home?: string | undefined;
192
250
  }): {
193
251
  ok: true;
194
252
  spec: ToolSpec;
@@ -200,7 +258,7 @@ export declare function addToolTo(store: ToolWrites, repo: string, input: ToolSp
200
258
  export declare function removeToolFrom(store: ToolWrites, repo: string, name: string, by: string, now: Date, home?: string): boolean;
201
259
  /** Test one tool of a project and keep the answer on it. */
202
260
  export declare function testToolOf(store: ToolWrites, repo: string, name: string, now: Date, options?: {
203
- home?: string;
261
+ home?: string | undefined;
204
262
  timeoutMs?: number;
205
263
  omitEnv?: readonly string[];
206
264
  }): Promise<ToolTest | null>;
@@ -209,7 +267,23 @@ export declare function toolStanding(tool: ProjectTool, secretsSet: readonly str
209
267
  ready: boolean;
210
268
  words: string;
211
269
  };
212
- /** Names of a tool's secrets that have a value (never the values). */
213
- export declare function secretsSetFor(repo: string, tool: ToolSpec, home?: string): string[];
270
+ /** How a tool's runs get its credentials, in the words Settings and `toolroll integrations` share. */
271
+ export type CredentialMode = {
272
+ mode: "run-token" | "held-key" | "no-key";
273
+ words: string;
274
+ detail: string | null;
275
+ };
276
+ export declare const CREDENTIAL_MODES: Readonly<Record<CredentialMode["mode"], Omit<CredentialMode, "mode">>>;
277
+ /** A tool's credential mode from the names of its secrets that are set (never their values); null while a key it needs is missing. */
278
+ export declare function credentialModeOf(spec: ToolSpec, secretsSet: readonly string[]): CredentialMode | null;
279
+ /** Names of a tool's secrets that have a value, never the values: in the keychain, from the names kept beside them. */
280
+ export declare function secretsSetFor(repo: string, tool: ToolSpec, state?: string): string[];
281
+ /** Where this computer keeps tool secrets, in the words Settings and `toolroll integrations` share; `problem` when they can't be used. */
282
+ export declare function toolSecretStorage(): {
283
+ kind: SecretStoreKind;
284
+ words: string;
285
+ filesOnly: boolean;
286
+ problem?: string;
287
+ };
214
288
  /** A command line split into program and arguments, honouring "double" and 'single' quotes. */
215
289
  export declare function splitCommandLine(line: string): string[];