getbotons 0.0.0-stage → 0.1.0

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 (191) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +505 -2
  3. package/installer/admin.mjs +6 -0
  4. package/installer/botons.mjs +430 -0
  5. package/installer/brain-clis.mjs +28 -0
  6. package/installer/linger.mjs +39 -0
  7. package/installer/output.mjs +27 -0
  8. package/installer/revision.mjs +30 -0
  9. package/installer/shell-path.mjs +66 -0
  10. package/installer/source-lock.json +2891 -0
  11. package/package.json +26 -4
  12. package/payload/LICENSE +21 -0
  13. package/payload/SECURITY.md +33 -0
  14. package/payload/installer/admin.mjs +6 -0
  15. package/payload/installer/botons.mjs +430 -0
  16. package/payload/installer/brain-clis.mjs +28 -0
  17. package/payload/installer/linger.mjs +39 -0
  18. package/payload/installer/output.mjs +27 -0
  19. package/payload/installer/revision.mjs +30 -0
  20. package/payload/installer/shell-path.mjs +66 -0
  21. package/payload/installer/source-lock.json +2891 -0
  22. package/payload/npm-shrinkwrap.json +51 -0
  23. package/payload/package.json +13 -0
  24. package/payload/scripts/backup.ts +47 -0
  25. package/payload/scripts/botons-backup.service +15 -0
  26. package/payload/scripts/botons-backup.timer +11 -0
  27. package/payload/scripts/botons.service +36 -0
  28. package/payload/scripts/check-browser.ts +90 -0
  29. package/payload/scripts/doctor.ts +790 -0
  30. package/payload/scripts/install-backup-timer.ts +52 -0
  31. package/payload/scripts/install-service.ts +206 -0
  32. package/payload/scripts/login.ts +17 -0
  33. package/payload/scripts/migrate-names.ts +47 -0
  34. package/payload/scripts/pair.ts +163 -0
  35. package/payload/scripts/restore.ts +108 -0
  36. package/payload/server/bin/botons +4 -0
  37. package/payload/server/bin/legacy/minion +4 -0
  38. package/payload/server/bin/legacy/tots +4 -0
  39. package/payload/server/src/acting.ts +16 -0
  40. package/payload/server/src/api-keys.ts +544 -0
  41. package/payload/server/src/api.ts +1484 -0
  42. package/payload/server/src/app.ts +546 -0
  43. package/payload/server/src/approvals.ts +873 -0
  44. package/payload/server/src/ask-rules.ts +181 -0
  45. package/payload/server/src/audit.ts +158 -0
  46. package/payload/server/src/auth.ts +497 -0
  47. package/payload/server/src/backup.ts +382 -0
  48. package/payload/server/src/brain-login.ts +75 -0
  49. package/payload/server/src/browser-cert.ts +67 -0
  50. package/payload/server/src/browser-handoff.ts +119 -0
  51. package/payload/server/src/browser-handover.ts +175 -0
  52. package/payload/server/src/browser-policy.ts +210 -0
  53. package/payload/server/src/browser-proxy.ts +580 -0
  54. package/payload/server/src/browser-request.ts +335 -0
  55. package/payload/server/src/browser-screenshot.ts +56 -0
  56. package/payload/server/src/browser-send.ts +155 -0
  57. package/payload/server/src/browser.ts +2164 -0
  58. package/payload/server/src/callback.ts +218 -0
  59. package/payload/server/src/capabilities.ts +90 -0
  60. package/payload/server/src/cards.ts +243 -0
  61. package/payload/server/src/cli/approve-mcp.ts +119 -0
  62. package/payload/server/src/cli/botons.ts +629 -0
  63. package/payload/server/src/cli/browser-mcp.ts +168 -0
  64. package/payload/server/src/cli/workspace-file.ts +78 -0
  65. package/payload/server/src/cli-homes.ts +38 -0
  66. package/payload/server/src/cli-install.ts +104 -0
  67. package/payload/server/src/cli-updates.ts +112 -0
  68. package/payload/server/src/codex-config-repair.ts +80 -0
  69. package/payload/server/src/config.ts +473 -0
  70. package/payload/server/src/connector-access.ts +158 -0
  71. package/payload/server/src/connector-launch.ts +203 -0
  72. package/payload/server/src/connectors.ts +947 -0
  73. package/payload/server/src/disk-usage.ts +62 -0
  74. package/payload/server/src/engine.ts +2328 -0
  75. package/payload/server/src/env-init.ts +5 -0
  76. package/payload/server/src/events.ts +46 -0
  77. package/payload/server/src/files.ts +528 -0
  78. package/payload/server/src/github-access.ts +52 -0
  79. package/payload/server/src/health.ts +117 -0
  80. package/payload/server/src/held-dir.ts +96 -0
  81. package/payload/server/src/hooks.ts +141 -0
  82. package/payload/server/src/http.ts +207 -0
  83. package/payload/server/src/index.ts +139 -0
  84. package/payload/server/src/install-type.ts +11 -0
  85. package/payload/server/src/instructions.ts +204 -0
  86. package/payload/server/src/isolation.ts +234 -0
  87. package/payload/server/src/legacy-upgrade.ts +668 -0
  88. package/payload/server/src/limits.ts +44 -0
  89. package/payload/server/src/lock.ts +75 -0
  90. package/payload/server/src/log.ts +137 -0
  91. package/payload/server/src/managed-clis.ts +65 -0
  92. package/payload/server/src/mcp-client.ts +229 -0
  93. package/payload/server/src/notify.ts +193 -0
  94. package/payload/server/src/oauth-connections.ts +650 -0
  95. package/payload/server/src/oauth.ts +311 -0
  96. package/payload/server/src/outbound.ts +142 -0
  97. package/payload/server/src/portable.ts +1669 -0
  98. package/payload/server/src/prompt.ts +676 -0
  99. package/payload/server/src/providers/api-brain.ts +612 -0
  100. package/payload/server/src/providers/api-history.ts +131 -0
  101. package/payload/server/src/providers/api-tools.ts +481 -0
  102. package/payload/server/src/providers/api-wire.ts +690 -0
  103. package/payload/server/src/providers/claude-cli.ts +620 -0
  104. package/payload/server/src/providers/cli-check.ts +265 -0
  105. package/payload/server/src/providers/cli-turn.ts +20 -0
  106. package/payload/server/src/providers/codex-cli.ts +552 -0
  107. package/payload/server/src/providers/codex-workspace.ts +61 -0
  108. package/payload/server/src/providers/failures.ts +143 -0
  109. package/payload/server/src/providers/index.ts +131 -0
  110. package/payload/server/src/providers/mcp-stdio.ts +147 -0
  111. package/payload/server/src/providers/mock.ts +94 -0
  112. package/payload/server/src/providers/process.ts +268 -0
  113. package/payload/server/src/providers/readiness.ts +43 -0
  114. package/payload/server/src/providers/sse.ts +73 -0
  115. package/payload/server/src/providers/types.ts +173 -0
  116. package/payload/server/src/rates.ts +128 -0
  117. package/payload/server/src/reactions.ts +266 -0
  118. package/payload/server/src/recent-logs.ts +489 -0
  119. package/payload/server/src/rehearsal.ts +593 -0
  120. package/payload/server/src/rehearsals.ts +303 -0
  121. package/payload/server/src/replay.ts +165 -0
  122. package/payload/server/src/sandbox.ts +767 -0
  123. package/payload/server/src/schedule.ts +214 -0
  124. package/payload/server/src/scheduler.ts +24 -0
  125. package/payload/server/src/schema.ts +882 -0
  126. package/payload/server/src/secrets.ts +46 -0
  127. package/payload/server/src/setup.ts +356 -0
  128. package/payload/server/src/skill-packages.ts +363 -0
  129. package/payload/server/src/skills.ts +527 -0
  130. package/payload/server/src/spend.ts +255 -0
  131. package/payload/server/src/ssrf.ts +126 -0
  132. package/payload/server/src/starter-gate.ts +168 -0
  133. package/payload/server/src/starters.ts +610 -0
  134. package/payload/server/src/storage.ts +32 -0
  135. package/payload/server/src/store.ts +3087 -0
  136. package/payload/server/src/tailnet.ts +132 -0
  137. package/payload/server/src/templates.ts +454 -0
  138. package/payload/server/src/trash.ts +772 -0
  139. package/payload/server/src/util.ts +97 -0
  140. package/payload/server/src/webhooks.ts +216 -0
  141. package/payload/server/src/work-feed.ts +473 -0
  142. package/payload/shared/app-version.d.mts +1 -0
  143. package/payload/shared/app-version.mjs +3 -0
  144. package/payload/shared/boton.ts +323 -0
  145. package/payload/shared/brand.ts +107 -0
  146. package/payload/shared/cards.ts +427 -0
  147. package/payload/shared/effort.ts +91 -0
  148. package/payload/shared/files.ts +104 -0
  149. package/payload/shared/handoff.ts +97 -0
  150. package/payload/shared/install-commands.ts +7 -0
  151. package/payload/shared/legacy-names.d.mts +35 -0
  152. package/payload/shared/legacy-names.mjs +244 -0
  153. package/payload/shared/live-view.ts +94 -0
  154. package/payload/shared/look-critter.ts +189 -0
  155. package/payload/shared/look-tater.ts +127 -0
  156. package/payload/shared/look.ts +200 -0
  157. package/payload/shared/reactions.ts +43 -0
  158. package/payload/shared/seeded.ts +34 -0
  159. package/payload/shared/services.ts +90 -0
  160. package/payload/shared/types.ts +3016 -0
  161. package/payload/shared/util.ts +68 -0
  162. package/payload/shared/versions.ts +44 -0
  163. package/payload/shared/wake.ts +164 -0
  164. package/payload/web/dist/apple-touch-icon.png +0 -0
  165. package/payload/web/dist/assets/Icons-nytVAa9g.js +1 -0
  166. package/payload/web/dist/assets/NewBotDialog-DXr2XhDP.js +1 -0
  167. package/payload/web/dist/assets/PairScreen-CMcJO7cQ.js +1 -0
  168. package/payload/web/dist/assets/Panel-Bvqz3I3m.js +4 -0
  169. package/payload/web/dist/assets/ReactionPicker-B0SKsjiy.js +1 -0
  170. package/payload/web/dist/assets/SettingRows-CvLomtuo.js +4 -0
  171. package/payload/web/dist/assets/SettingsDialog-CQ41LV-P.js +8 -0
  172. package/payload/web/dist/assets/Setup-C-3P7Mtn.js +1 -0
  173. package/payload/web/dist/assets/Skills-DXMH69kH.js +1 -0
  174. package/payload/web/dist/assets/StarterSetup-Ce2V1_Cs.js +5 -0
  175. package/payload/web/dist/assets/Steps-CKtB3GAM.js +1 -0
  176. package/payload/web/dist/assets/emoji-catalog-DfpPz8qw.js +540 -0
  177. package/payload/web/dist/assets/index-BEU7FDxV.css +1 -0
  178. package/payload/web/dist/assets/index-Dhwn-rf0.js +3 -0
  179. package/payload/web/dist/assets/react-C1iE8mnY.js +9 -0
  180. package/payload/web/dist/assets/reactions-rw0YeCmA.js +1 -0
  181. package/payload/web/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  182. package/payload/web/dist/assets/util-Cx5feSJu.js +1 -0
  183. package/payload/web/dist/assets/vendor-B3qdcjGo.js +14 -0
  184. package/payload/web/dist/favicon.svg +1 -0
  185. package/payload/web/dist/icon-192.png +0 -0
  186. package/payload/web/dist/icon-512.png +0 -0
  187. package/payload/web/dist/icon.svg +1 -0
  188. package/payload/web/dist/index.html +67 -0
  189. package/payload/web/dist/manifest.webmanifest +12 -0
  190. package/shared/legacy-names.d.mts +35 -0
  191. package/shared/legacy-names.mjs +244 -0
@@ -0,0 +1,203 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { closeSync, existsSync, openSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
5
+ import { agentEnv } from './providers/process.ts';
6
+ import { bwrapArgs, mountTable, type SandboxPlan, sandboxBinary, seccompFilter, viewOf } from './sandbox.ts';
7
+
8
+ /*
9
+ * A library connector's command (a local, stdio MCP server) is started by the agent CLI, or by Botons for an API key
10
+ * brain, as the server's own user. The owner chose that command, not code a Workspace Boton wrote, and a Workspace
11
+ * Boton can write its workspace, the shared folder and (on some CLIs) the temporary folders. No look at the command's
12
+ * text can tell what it will load from there (an interpreter's options, a module loader, a PATH entry, a setting the
13
+ * program reads), and which Boton's turn starts it doesn't change what it runs: a Full computer Boton's connector
14
+ * running a script another Boton can rewrite runs that Boton's code with the server's access.
15
+ *
16
+ * So every one, in every turn, on every brain, starts in the Workspace sandbox's own view of this computer (sandbox.ts):
17
+ * an empty read-only root with only the system's programs and libraries (SYSTEM_TREES), the folders on its PATH and
18
+ * Node's own install shown, each read-only, plus the folder of its command and of each argument that is an absolute
19
+ * path. No workspace, no shared folder, no data folder, no home folder but what those name, the private paths hidden
20
+ * where they fall inside what it sees, an empty /tmp of its own (its HOME and where it starts). What a Boton can write
21
+ * is never shown by any path: a folder it would see that is, or lies in, a workspace, the shared folder, the data folder
22
+ * or a temporary folder through another mount of the same filesystem (a bind mount) is left out, and where one holds
23
+ * them that way, that place is hidden (SandboxPlan.apart). A command or argument folder that would be left out or
24
+ * hidden is refused, with why, rather than started without it.
25
+ *
26
+ * Unlike a Workspace command it keeps this computer's network (a connector talks to its service; the sandbox's socket
27
+ * filter still refuses Unix sockets, so no program's local socket is reachable), and the environment it gets is the
28
+ * agent CLIs' inherited variables (HOME and TMPDIR are its /tmp) and the connector's own. Its process list, IPC and
29
+ * capabilities are the sandbox's. Descriptors the CLI that starts it leaves open are the CLI's to close.
30
+ *
31
+ * Its sandbox, environment included, goes to bubblewrap in a private file (--args), never on a command line or in the
32
+ * environment of what starts bubblewrap (a loader variable there would act outside the sandbox): the CLI is given
33
+ * `/bin/sh` opening that file and the socket filter, then running the bubblewrap whose check passed with the command
34
+ * and arguments as saved. The file is removed when the turn ends (release()), and at start.
35
+ * Where bubblewrap can't run (sandboxProblem()), a turn goes without its local connectors and says why: nothing starts
36
+ * one outside the sandbox. Botons' own connectors (the managed browser, the approval server) are the server's own code
37
+ * and start as they are.
38
+ */
39
+
40
+ /** What a turn's local connectors never see, and where their launch files go. */
41
+ export interface LaunchPolicy {
42
+ /** Folders a Boton can write, and the data folder: never shown by any path. The temporary folders are added. */
43
+ apart: string[];
44
+ /** Hidden where they fall inside what it sees (isolation.ts privatePaths). */
45
+ hidden: string[];
46
+ /** Private (BOTONS_HOME/run/connectors): the launch files and the socket filter. */
47
+ dir: string;
48
+ }
49
+
50
+ export const TEMP_DIRS = ['/tmp', '/var/tmp', '/dev/shm'];
51
+
52
+ /** Each one starts as `sh -c SCRIPT sh <bubblewrap> <filter> <launch file> <command> <args...>`. */
53
+ export const LAUNCH_SCRIPT = 'exec 3<"$2" 4<"$3" || exit 126; b=$1; shift 3; exec "$b" --args 4 -- "$@"';
54
+ const SHELL = '/bin/sh';
55
+ const LAUNCH_FILE = /^launch-[0-9a-f]{18}\.args$/;
56
+
57
+ export interface Launch {
58
+ command: string;
59
+ args: string[];
60
+ /** Nothing of the connector's: its environment is in the launch file. */
61
+ env: Record<string, string>;
62
+ /** The launch file, removed with release(). */
63
+ file: string;
64
+ /** The values of the environment it was saved with: never to come back out in text. */
65
+ secrets: string[];
66
+ }
67
+
68
+ export type Jail = { wrap: (s: { command: string; args: string[]; env: Record<string, string> }) => Launch | { problem: string } } | { problem: string };
69
+
70
+ const within = (p: string, root: string) => p === root || p.startsWith(root.endsWith(sep) ? root : root + sep);
71
+
72
+ const real = (p: string) => {
73
+ try {
74
+ return realpathSync(p);
75
+ } catch {
76
+ return resolve(p);
77
+ }
78
+ };
79
+
80
+ /**
81
+ * The folders its command and arguments name: each absolute path that exists, a folder as itself, a file by its folder,
82
+ * by its real path: a file's real folder and, for a link, the folder the link is in, so the path as saved is there inside.
83
+ */
84
+ export function namedFolders(command: string, args: string[]): string[] {
85
+ const out = new Set<string>();
86
+ for (const a of [command, ...args]) {
87
+ if (!isAbsolute(a)) continue;
88
+ try {
89
+ if (statSync(a).isDirectory()) out.add(real(a));
90
+ else for (const p of [dirname(real(a)), real(dirname(resolve(a)))]) out.add(p);
91
+ } catch {}
92
+ }
93
+ return [...out];
94
+ }
95
+
96
+ /** The environment inside: the agent CLIs' inherited variables (not their homes), its /tmp, then the connector's own. */
97
+ export function launchEnv(own: Record<string, string>): Record<string, string> {
98
+ const out: Record<string, string> = {};
99
+ for (const [k, v] of Object.entries(agentEnv({}))) if (typeof v === 'string' && k !== 'CLAUDE_CONFIG_DIR' && k !== 'CODEX_HOME') out[k] = v;
100
+ out.HOME = '/tmp';
101
+ out.TMPDIR = '/tmp';
102
+ // The server's own Node first: a version manager's shims need files the sandbox doesn't show.
103
+ out.PATH = [dirname(process.execPath), ...(out.PATH ?? '/usr/bin:/bin').split(':').filter((d) => d && d !== dirname(process.execPath))].join(':');
104
+ return { ...out, ...own };
105
+ }
106
+
107
+ /** The sandbox plan for one connector under `policy`. */
108
+ export function launchPlan(policy: LaunchPolicy, s: { command: string; args: string[]; env: Record<string, string> }): SandboxPlan {
109
+ const apart = [...new Set([...policy.apart, ...TEMP_DIRS, tmpdir()].map((p) => resolve(p)))];
110
+ return { cwd: '/tmp', writable: [], hidden: [...policy.hidden, ...apart], readOnly: [], env: launchEnv(s.env), network: true, trees: namedFolders(s.command, s.args), apart };
111
+ }
112
+
113
+ /** Why `plan` can't show each folder its command and arguments name, or null when it shows them all. */
114
+ export function namedProblem(plan: SandboxPlan): string | null {
115
+ const { trees, masked } = viewOf(plan);
116
+ const shown = trees.filter((t) => !t.link).map((t) => t.path);
117
+ const covered = [...plan.hidden.map((p) => resolve(p)), ...masked];
118
+ for (const n of plan.trees ?? []) {
119
+ if (shown.some((t) => within(n, t)) && !covered.some((h) => within(n, h) || within(h, n))) continue;
120
+ return `${n} (named by its command) can't be shown to it: it is, holds or lies in a workspace, the shared folder, the data folder, a temporary folder or a private folder (by its path or another mount of it), or another filesystem is mounted inside it. Keep its program outside those`;
121
+ }
122
+ return null;
123
+ }
124
+
125
+ /** How a turn starts its local connectors under `policy`, or why it can't. */
126
+ export function connectorJail(policy: LaunchPolicy): Jail {
127
+ const b = sandboxBinary();
128
+ if ('problem' in b) return { problem: `a local connector starts only in the Workspace sandbox, which can't run here: ${b.problem}` };
129
+ const filter = seccompFilter();
130
+ if (!filter) return { problem: `a local connector starts only in the Workspace sandbox, which has no socket filter for ${process.arch}` };
131
+ if (!mountTable()) return { problem: "a local connector starts only in the Workspace sandbox, and Botons can't read this computer's mount table to keep every workspace out of its view" };
132
+ if (!existsSync(SHELL)) return { problem: `a local connector starts through ${SHELL}, which isn't on this computer` };
133
+ let filterFile: string;
134
+ try {
135
+ filterFile = writeFilter(policy.dir, filter);
136
+ } catch (err) {
137
+ return { problem: `Botons could not write the sandbox's socket filter for a local connector (${(err as Error).message})` };
138
+ }
139
+ return {
140
+ wrap: (s) => {
141
+ const plan = launchPlan(policy, s);
142
+ const named = namedProblem(plan);
143
+ if (named) return { problem: named };
144
+ const argv = bwrapArgs(plan);
145
+ if ([...argv, s.command, ...s.args].some((a) => a.includes('\0'))) return { problem: 'its command, arguments or environment hold a NUL character' };
146
+ const file = join(policy.dir, `launch-${randomBytes(9).toString('hex')}.args`);
147
+ writeLaunchFile(file, argv.map((a) => `${a}\0`).join(''));
148
+ return { command: SHELL, args: ['-c', LAUNCH_SCRIPT, 'sh', b.bin, filterFile, file, s.command, ...s.args], env: {}, file, secrets: Object.values(s.env).filter((v) => v.length >= 8) };
149
+ },
150
+ };
151
+ }
152
+
153
+ /** The socket filter, in `dir`, rewritten whole (a new file renamed over it) only when it differs. */
154
+ function writeFilter(dir: string, filter: Buffer): string {
155
+ const file = join(dir, 'seccomp.bpf');
156
+ try {
157
+ if (readFileSync(file).equals(filter)) return file;
158
+ } catch {}
159
+ const tmp = join(dir, `.seccomp-${randomBytes(6).toString('hex')}`);
160
+ writeFileSync(tmp, filter, { mode: 0o600, flag: 'wx' });
161
+ renameSync(tmp, file);
162
+ return file;
163
+ }
164
+
165
+ /**
166
+ * A new launch file (never one already there), private from the start. One that can't be written whole is removed
167
+ * before the error goes on: a part of one is never left behind, nor listed for a turn.
168
+ */
169
+ function writeLaunchFile(file: string, text: string): void {
170
+ const fd = openSync(file, 'wx', 0o600);
171
+ try {
172
+ writeFileSync(fd, text);
173
+ } catch (err) {
174
+ try {
175
+ closeSync(fd);
176
+ } catch {}
177
+ try {
178
+ rmSync(file, { force: true });
179
+ } catch {}
180
+ throw err;
181
+ }
182
+ closeSync(fd);
183
+ }
184
+
185
+ /** Removes the launch files given (a turn's, as it ends): tries every one, then throws the first that failed. */
186
+ export function release(files: string[]): void {
187
+ let first: unknown;
188
+ let failed = false;
189
+ for (const f of files) {
190
+ try {
191
+ rmSync(f, { force: true });
192
+ } catch (err) {
193
+ if (!failed) first = err;
194
+ failed = true;
195
+ }
196
+ }
197
+ if (failed) throw first;
198
+ }
199
+
200
+ /** Removes every launch file in `dir` (at start: those a crashed turn left). */
201
+ export function releaseAll(dir: string): void {
202
+ for (const f of readdirSync(dir)) if (LAUNCH_FILE.test(f)) rmSync(join(dir, f), { force: true });
203
+ }