@ravefox/space 0.2.1 → 0.4.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 (167) hide show
  1. package/LICENSE +21 -205
  2. package/README.md +104 -31
  3. package/bin/space.js +113 -0
  4. package/lib/agents.js +181 -0
  5. package/lib/boardTools.js +89 -0
  6. package/lib/credentials.js +95 -0
  7. package/lib/daemon.js +294 -0
  8. package/lib/harness.js +166 -0
  9. package/lib/http.js +101 -0
  10. package/lib/login.js +68 -0
  11. package/lib/mcp.js +39 -0
  12. package/lib/runtime.js +253 -0
  13. package/lib/store.js +96 -0
  14. package/lib/tools.js +93 -0
  15. package/lib/workspaceSandbox.js +77 -0
  16. package/lib/workspaceTools.js +160 -0
  17. package/lib/workspaceWorker.js +17 -0
  18. package/package.json +27 -46
  19. package/NOTICE +0 -55
  20. package/assets/webhook-sdk/python/bot.py +0 -63
  21. package/assets/webhook-sdk/python/commonly.py +0 -232
  22. package/node_modules/@commonlyai/seats/README.md +0 -25
  23. package/node_modules/@commonlyai/seats/package.json +0 -22
  24. package/node_modules/@commonlyai/seats/src/adapters/hermes.js +0 -264
  25. package/node_modules/@commonlyai/seats/src/adapters/stub.js +0 -24
  26. package/node_modules/@commonlyai/seats/src/contract.js +0 -48
  27. package/node_modules/@commonlyai/seats/src/failures.js +0 -44
  28. package/node_modules/@commonlyai/seats/src/harness-plan.js +0 -329
  29. package/node_modules/@commonlyai/seats/src/index.js +0 -22
  30. package/node_modules/@commonlyai/seats/src/process-group.js +0 -269
  31. package/node_modules/@commonlyai/seats/src/record/format.js +0 -737
  32. package/node_modules/@commonlyai/seats/src/record/otel.js +0 -238
  33. package/node_modules/@commonlyai/seats/src/runtime-observation.js +0 -29
  34. package/node_modules/@commonlyai/seats/src/seat-model.js +0 -17
  35. package/node_modules/@commonlyai/seats/src/supervisor.js +0 -308
  36. package/node_modules/@commonlyai/seats/src/trace/events.js +0 -46
  37. package/node_modules/@commonlyai/seats/src/trace/store.js +0 -276
  38. package/node_modules/@commonlyai/seats/src/turn-processes.js +0 -226
  39. package/node_modules/@commonlyai/seats/src/upstream-refusal.js +0 -418
  40. package/node_modules/@commonlyai/seats/src/valet/env.js +0 -201
  41. package/node_modules/@commonlyai/seats/src/valet/grant.js +0 -123
  42. package/node_modules/@commonlyai/seats/src/valet/permissions.js +0 -213
  43. package/node_modules/@commonlyai/seats/src/wire/provider-proxy.js +0 -529
  44. package/skills/commonly/SKILL.md +0 -233
  45. package/src/commands/agent.js +0 -6334
  46. package/src/commands/daemon.js +0 -933
  47. package/src/commands/dev.js +0 -153
  48. package/src/commands/doctor.js +0 -141
  49. package/src/commands/inbox.js +0 -330
  50. package/src/commands/login.js +0 -194
  51. package/src/commands/mcp.js +0 -56
  52. package/src/commands/pod.js +0 -120
  53. package/src/commands/task.js +0 -279
  54. package/src/commands/verify.js +0 -48
  55. package/src/index.js +0 -99
  56. package/src/lib/adapter-login.js +0 -76
  57. package/src/lib/adapters/broker-bridge-entry.js +0 -76
  58. package/src/lib/adapters/claude.js +0 -2207
  59. package/src/lib/adapters/codex-broker-bridge.mjs +0 -298
  60. package/src/lib/adapters/codex.js +0 -1898
  61. package/src/lib/adapters/dsh.js +0 -513
  62. package/src/lib/adapters/goose.js +0 -510
  63. package/src/lib/adapters/hermes.js +0 -18
  64. package/src/lib/adapters/index.js +0 -68
  65. package/src/lib/adapters/opencode.js +0 -1681
  66. package/src/lib/adapters/pi-commonly-mcp.mjs +0 -71
  67. package/src/lib/adapters/pi-mcp-client.mjs +0 -617
  68. package/src/lib/adapters/pi-shell-env.mjs +0 -41
  69. package/src/lib/adapters/pi.js +0 -1025
  70. package/src/lib/adapters/qwen.js +0 -523
  71. package/src/lib/adapters/seat-model.js +0 -8
  72. package/src/lib/adapters/stub.js +0 -1
  73. package/src/lib/adapters/tested-version.js +0 -106
  74. package/src/lib/api.js +0 -205
  75. package/src/lib/claim-outcome.js +0 -128
  76. package/src/lib/claimed-work-gate.js +0 -73
  77. package/src/lib/cli-hint.js +0 -33
  78. package/src/lib/commit-attribution.js +0 -391
  79. package/src/lib/config.js +0 -155
  80. package/src/lib/content-refusal.js +0 -94
  81. package/src/lib/credential-file.js +0 -208
  82. package/src/lib/daemon-agent-states.js +0 -95
  83. package/src/lib/daemon-hint.js +0 -18
  84. package/src/lib/daemon-logs.js +0 -43
  85. package/src/lib/daemon-service-windows.js +0 -263
  86. package/src/lib/daemon-service.js +0 -486
  87. package/src/lib/daemon-state.js +0 -84
  88. package/src/lib/daemon-store.js +0 -119
  89. package/src/lib/daemon-supervisor.js +0 -629
  90. package/src/lib/daemon-watch.js +0 -115
  91. package/src/lib/declared-mcp-guard.js +0 -284
  92. package/src/lib/default-environment.js +0 -207
  93. package/src/lib/device-login.js +0 -126
  94. package/src/lib/digest-frame.js +0 -151
  95. package/src/lib/doctor.js +0 -291
  96. package/src/lib/egress-proxy.js +0 -160
  97. package/src/lib/enforcement.js +0 -946
  98. package/src/lib/environment.js +0 -681
  99. package/src/lib/first-output-watchdog.js +0 -191
  100. package/src/lib/git-readonly-server.js +0 -138
  101. package/src/lib/git-readonly.js +0 -463
  102. package/src/lib/git-remote-commonly.mjs +0 -100
  103. package/src/lib/grant-broker-guard.js +0 -345
  104. package/src/lib/hooks-config.js +0 -238
  105. package/src/lib/host-gh-broker.js +0 -152
  106. package/src/lib/host-gh-shim.mjs +0 -98
  107. package/src/lib/inbox.js +0 -483
  108. package/src/lib/instance-proxy.js +0 -76
  109. package/src/lib/machine-host.js +0 -29
  110. package/src/lib/mcp-credential-delivery.js +0 -121
  111. package/src/lib/mcp-first-run.js +0 -50
  112. package/src/lib/mcp-home.js +0 -644
  113. package/src/lib/mcp-ready.js +0 -68
  114. package/src/lib/mcp-server-version.js +0 -123
  115. package/src/lib/mcp-token.js +0 -36
  116. package/src/lib/mcp-warm-child.mjs +0 -412
  117. package/src/lib/memory-bridge.js +0 -191
  118. package/src/lib/memory-import.js +0 -156
  119. package/src/lib/missing-harness.js +0 -31
  120. package/src/lib/model-unavailable.js +0 -154
  121. package/src/lib/native-runtime-inventory.js +0 -40
  122. package/src/lib/outsider-grant-mcp.js +0 -82
  123. package/src/lib/own-key-launch.js +0 -42
  124. package/src/lib/pod-catch-up.js +0 -55
  125. package/src/lib/pod-focus.js +0 -224
  126. package/src/lib/pod-tail.js +0 -68
  127. package/src/lib/poll-liveness.js +0 -103
  128. package/src/lib/poll-retry.js +0 -81
  129. package/src/lib/poller.js +0 -200
  130. package/src/lib/provider-proxy.js +0 -42
  131. package/src/lib/public-seat-mcp-flag.js +0 -36
  132. package/src/lib/public-tool-error.js +0 -60
  133. package/src/lib/public-withheld-tools.js +0 -48
  134. package/src/lib/reply-receipt.js +0 -55
  135. package/src/lib/resolve-on-path.js +0 -22
  136. package/src/lib/runtime-inventory.js +0 -56
  137. package/src/lib/runtime-observation.js +0 -37
  138. package/src/lib/sandbox/bwrap.js +0 -275
  139. package/src/lib/sandbox/codex-perimeter.js +0 -16
  140. package/src/lib/sandbox/host-check.js +0 -268
  141. package/src/lib/sandbox/mode.js +0 -49
  142. package/src/lib/sandbox/opencode-loopback.mjs +0 -68
  143. package/src/lib/sandbox/seatbelt.js +0 -684
  144. package/src/lib/sandbox/turn-dir.js +0 -56
  145. package/src/lib/sandbox/windows-unsandboxed.js +0 -183
  146. package/src/lib/sandbox/workspace-git-config.js +0 -243
  147. package/src/lib/sandbox/workspace-pin.js +0 -158
  148. package/src/lib/seat-runtime-controls.js +0 -61
  149. package/src/lib/seat-stop.js +0 -97
  150. package/src/lib/server-skills.js +0 -32
  151. package/src/lib/session-recycle.js +0 -219
  152. package/src/lib/session-store.js +0 -215
  153. package/src/lib/skill-prompt.js +0 -74
  154. package/src/lib/skills-import.js +0 -113
  155. package/src/lib/spawn-credential.js +0 -402
  156. package/src/lib/spawn-retry.js +0 -311
  157. package/src/lib/stable-install.js +0 -176
  158. package/src/lib/supervised-turn.js +0 -352
  159. package/src/lib/task.js +0 -235
  160. package/src/lib/turn-failure-notice.js +0 -177
  161. package/src/lib/unconfined-seat-guard.js +0 -94
  162. package/src/lib/update-check.js +0 -192
  163. package/src/lib/upstream-refusal.js +0 -1
  164. package/src/lib/webhook-server.js +0 -109
  165. package/src/lib/windows-command.js +0 -393
  166. package/src/lib/workspace-git.js +0 -87
  167. package/src/lib/workspace-trust.js +0 -162
@@ -1,393 +0,0 @@
1
- // Launching an npm-installed CLI (claude, codex) on native Windows, without a
2
- // shell (#2141).
3
- //
4
- // Two things stand between `spawn('codex', args)` and a running Codex on
5
- // Windows. libuv's PATH search tries only the bare name plus `.com` and
6
- // `.exe`, so the `codex.cmd` npm writes is ENOENT. And Node >= 18.20.2 /
7
- // 20.12.2 refuses to spawn a `.cmd`/`.bat` without `shell: true` (EINVAL,
8
- // CVE-2024-27980). `shell: true` is not an option here: the prompt is pod
9
- // content, so it is untrusted, and it travels as an argv element. cmd.exe
10
- // would parse it (a command-injection path for anyone who can post in the
11
- // pod) and would cap the whole command line at 8191 characters.
12
- //
13
- // So the shim is read, never run. npm's cmd-shim writes one of a few fixed
14
- // shapes, each naming the real target relative to the shim's own folder
15
- // (`%dp0%` / `%~dp0`). A JS target runs as `node <script>`, an `.exe` target
16
- // runs as itself; anything else is refused with a reason, never handed to a
17
- // shell. On every other platform this module is a passthrough.
18
-
19
- import { spawnSync as realSpawnSync } from 'child_process';
20
- import { existsSync as realExistsSync, readFileSync as realReadFileSync } from 'fs';
21
- import { win32 } from 'path';
22
-
23
- const DEFAULT_PATHEXT = '.COM;.EXE;.BAT;.CMD';
24
- const BINARY_EXTS = new Set(['.exe', '.com']);
25
- const SHIM_EXTS = new Set(['.cmd', '.bat']);
26
- const SCRIPT_EXTS = new Set(['.js', '.mjs', '.cjs']);
27
-
28
- // `%dp0%\…` (cmd-shim >= 4, where `dp0` is SET from `%~dp0` in a subroutine)
29
- // and `%~dp0\…` (cmd-shim <= 3). `%~dp0` already ends in a backslash, so the
30
- // separator after it is optional.
31
- const DP0_PREFIX = /^%(?:~dp0|dp0%)\\?/i;
32
-
33
- // A flag the shim passes before the script (from the script's shebang line,
34
- // e.g. `#!/usr/bin/env node --no-warnings`). Only plain tokens are accepted:
35
- // nothing a batch file would expand or redirect.
36
- const SAFE_SHIM_ARG = /^[A-Za-z0-9_.=:/+,-]+$/;
37
-
38
- const SEPARATORS = new Set(['&', '&&', '|', '||']);
39
-
40
- const extOf = (file) => win32.extname(file).toLowerCase();
41
-
42
- // Windows environment keys are case-insensitive, but a copied env object is
43
- // not: `{ ...process.env, PATH }` can carry both `Path` and `PATH`. Read
44
- // `Path` first (the casing Windows itself uses), then `PATH`, then any other
45
- // casing, and drop duplicates.
46
- // A relative entry (`.`, `bin`) names a folder relative to the working
47
- // directory the child is spawned in, as Windows reads it there. Given `cwd`,
48
- // it is resolved against that, never against this process's own directory.
49
- const pathEntries = (env = {}, cwd) => {
50
- const keys = Object.keys(env).filter((key) => key.toLowerCase() === 'path');
51
- const rank = (key) => (key === 'Path' ? 0 : key === 'PATH' ? 1 : 2);
52
- keys.sort((a, b) => rank(a) - rank(b));
53
- const seen = new Set();
54
- const entries = [];
55
- for (const key of keys) {
56
- for (const raw of String(env[key] || '').split(';')) {
57
- const entry = raw.trim().replace(/^"(.*)"$/, '$1');
58
- const dir = entry && cwd && !win32.isAbsolute(entry) ? win32.resolve(cwd, entry) : entry;
59
- if (!dir || seen.has(dir.toLowerCase())) continue;
60
- seen.add(dir.toLowerCase());
61
- entries.push(dir);
62
- }
63
- }
64
- return entries;
65
- };
66
-
67
- const pathExtensions = (env = {}) => {
68
- const key = Object.keys(env).find((k) => k.toLowerCase() === 'pathext');
69
- const raw = (key && env[key]) || DEFAULT_PATHEXT;
70
- return String(raw).split(';').map((ext) => ext.trim()).filter((ext) => ext.startsWith('.'));
71
- };
72
-
73
- // Split one batch line into tokens: a "quoted string" or a run of
74
- // non-space characters. Quotes are kept as a flag, not in the value.
75
- const tokenize = (line) => {
76
- const tokens = [];
77
- const re = /"([^"]*)"|([^\s"]+)/g;
78
- let match;
79
- while ((match = re.exec(line)) !== null) {
80
- tokens.push(match[1] !== undefined
81
- ? { value: match[1], quoted: true }
82
- : { value: match[2], quoted: false });
83
- }
84
- return tokens;
85
- };
86
-
87
- const fromShimDir = (value, shimDir) => {
88
- if (!DP0_PREFIX.test(value)) return null;
89
- const rest = value.replace(DP0_PREFIX, '');
90
- return win32.resolve(shimDir, rest);
91
- };
92
-
93
- const isNodeName = (value) => /^node(?:\.exe)?$/i.test(value);
94
-
95
- const isNodeBesideShim = (value, shimDir) => {
96
- const abs = fromShimDir(value, shimDir);
97
- return Boolean(abs) && win32.basename(abs).toLowerCase() === 'node.exe'
98
- && win32.dirname(abs).toLowerCase() === win32.resolve(shimDir).toLowerCase();
99
- };
100
-
101
- // The program a JS shim runs must be node: bare `node`, `node.exe` beside
102
- // the shim, or the `%_prog%` variable the current cmd-shim SETs to one of
103
- // those two. A shim whose program is anything else (cmd-shim writes
104
- // `/bin/sh` for a shell-script bin) is refused.
105
- const programIsNode = (token, shimDir, text) => {
106
- if (!token) return false;
107
- if (!token.quoted && isNodeName(token.value)) return true;
108
- if (token.quoted && isNodeBesideShim(token.value, shimDir)) return true;
109
- if (token.quoted && token.value.toLowerCase() === '%_prog%') {
110
- const values = [...text.matchAll(/SET\s+"_prog=([^"]*)"/gi)].map((m) => m[1]);
111
- return values.length > 0
112
- && values.every((value) => isNodeName(value) || isNodeBesideShim(value, shimDir));
113
- }
114
- return false;
115
- };
116
-
117
- // npm's own `npm.cmd` / `npx.cmd` (npm/bin/*.cmd, which the Node installer
118
- // copies next to node.exe) is hand-written, not a cmd-shim:
119
- //
120
- // SET "NODE_EXE=%~dp0\node.exe" (else `node`)
121
- // SET "NPM_PREFIX_JS=%~dp0\node_modules\npm\bin\npm-prefix.js"
122
- // SET "NPX_CLI_JS=%~dp0\node_modules\npm\bin\npx-cli.js"
123
- // FOR /F … ('CALL "%NODE_EXE%" "%NPM_PREFIX_JS%"') … SET "NPM_PREFIX_NPX_CLI_JS=%%F\node_modules\npm\bin\npx-cli.js"
124
- // IF EXIST "%NPM_PREFIX_NPX_CLI_JS%" ( SET "NPX_CLI_JS=%NPM_PREFIX_NPX_CLI_JS%" )
125
- // "%NODE_EXE%" "%NPX_CLI_JS%" %*
126
- //
127
- // The FOR line asks npm for its global prefix (older npm: `npm-cli.js prefix
128
- // -g`), so an npm the person upgraded with `npm i -g npm` wins over the
129
- // bundled one. That prefix is %APPDATA%\npm unless an npmrc or
130
- // `npm_config_prefix` says otherwise, and npm's config loader is the only
131
- // thing that knows which. So the same script is asked the same way, as
132
- // `node.exe <script>` with no shell, in the child's env, and in its folder
133
- // when the caller passes `cwd` (a project .npmrc counts there). An answer that names no npm, or no answer at all,
134
- // leaves the bundled npm, as npm.cmd does.
135
- const NPM_LAUNCH = /^"%NODE_EXE%"\s+"%(NP[MX]_CLI_JS)%"\s+%\*$/i;
136
- const NPM_PREFIX_FOR = /^FOR\s+\/F\s+"delims="\s+%%F\s+IN\s+\('CALL\s+"%NODE_EXE%"\s+"%([A-Z_]+)%"((?:\s+[A-Za-z0-9-]+)*)'\)\s+DO\s+\($/i;
137
- const PREFIX_TIMEOUT_MS = 10000;
138
-
139
- const declaredBesideShim = (lines, variable, shimDir) => {
140
- const declared = lines
141
- .map((line) => line.match(new RegExp(`^SET\\s+"${variable}=([^"]*)"$`, 'i')))
142
- .find((match) => match && DP0_PREFIX.test(match[1]));
143
- return declared ? fromShimDir(declared[1], shimDir) : null;
144
- };
145
-
146
- // The npm script npm.cmd would switch to after asking for the prefix, or
147
- // null to keep the bundled one.
148
- const npmFromPrefix = (lines, variable, shimDir, node, deps) => {
149
- const forLine = lines.map((line) => line.match(NPM_PREFIX_FOR)).find(Boolean);
150
- if (!forLine) return null;
151
- const script = declaredBesideShim(lines, forLine[1], shimDir);
152
- if (!script || !deps.existsSync(script)) return null;
153
- const target = lines
154
- .map((line) => line.match(new RegExp(`^SET\\s+"NPM_PREFIX_${variable}=%%F\\\\([^"%]+)"$`, 'i')))
155
- .find(Boolean);
156
- if (!target) return null;
157
- let result;
158
- try {
159
- result = deps.spawnSync(node, [script, ...forLine[2].trim().split(/\s+/).filter(Boolean)], {
160
- env: deps.env,
161
- cwd: deps.cwd,
162
- encoding: 'utf8',
163
- stdio: ['ignore', 'pipe', 'ignore'],
164
- timeout: PREFIX_TIMEOUT_MS,
165
- windowsHide: true,
166
- });
167
- } catch {
168
- return null;
169
- }
170
- if (!result || result.status !== 0) return null;
171
- // FOR /F runs its body once per line, so the last line is the one that sticks.
172
- const prefix = String(result.stdout || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean).pop();
173
- if (!prefix || !win32.isAbsolute(prefix)) return null;
174
- const upgraded = win32.join(prefix, target[1]);
175
- return deps.existsSync(upgraded) ? upgraded : null;
176
- };
177
-
178
- const parseNpmLauncher = (lines, shimPath, deps) => {
179
- const launch = lines.map((line) => line.match(NPM_LAUNCH)).find(Boolean);
180
- if (!launch) return null;
181
- const variable = launch[1];
182
- const shimDir = win32.dirname(shimPath);
183
- const bundled = declaredBesideShim(lines, variable, shimDir);
184
- if (!bundled) return { command: null, reason: `${shimPath} does not name npm's script beside itself` };
185
- const nodeBeside = win32.join(shimDir, 'node.exe');
186
- const node = deps.existsSync(nodeBeside) ? nodeBeside : deps.execPath;
187
- const script = npmFromPrefix(lines, variable, shimDir, node, deps) || bundled;
188
- if (!deps.existsSync(script)) return { command: null, reason: `${shimPath} points at ${script}, which does not exist` };
189
- return { command: node, args: [script] };
190
- };
191
-
192
- /**
193
- * Read an npm cmd-shim and return what it would run, as an argv prefix.
194
- * Shapes, from cmd-shim's generator (npm/cmd-shim lib/index.js, read at
195
- * v8.0.0 as bundled with npm, and its pre-4.0 output):
196
- *
197
- * current JS: endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" [flags] "%dp0%\…\cli.js" %*
198
- * current exe: "%dp0%\…\tool.exe" %*
199
- * npm's own: "%NODE_EXE%" "%NPX_CLI_JS%" %* (see parseNpmLauncher)
200
- * legacy JS: @IF EXIST "%~dp0\node.exe" ( "%~dp0\node.exe" "%~dp0\…\cli.js" %* ) ELSE ( … node "%~dp0\…\cli.js" %* )
201
- *
202
- * @returns {{ command: string, args: string[] } | { command: null, reason: string }}
203
- */
204
- export const parseCmdShim = (text, shimPath, {
205
- existsSync = realExistsSync, execPath = process.execPath, env = {}, spawnSync = realSpawnSync, cwd,
206
- } = {}) => {
207
- const shimDir = win32.dirname(shimPath);
208
- const lines = String(text)
209
- .split(/\r?\n/)
210
- .map((line) => line.trim().replace(/^@/, ''))
211
- .filter(Boolean);
212
- const npmLauncher = parseNpmLauncher(lines, shimPath, { existsSync, execPath, env, spawnSync, cwd });
213
- if (npmLauncher) return npmLauncher;
214
- // The launch line is the one that forwards the caller's arguments. The
215
- // legacy shape has two (node beside the shim, else node on PATH) that name
216
- // the same script; the last one is the fallback branch.
217
- const launches = lines
218
- .map(tokenize)
219
- .filter((tokens) => tokens.length > 0 && !tokens[tokens.length - 1].quoted
220
- && tokens[tokens.length - 1].value === '%*');
221
- if (launches.length === 0) {
222
- return { command: null, reason: `${shimPath} is not an npm cmd-shim: no line forwards %*` };
223
- }
224
- let segment = launches[launches.length - 1];
225
- // Drop everything up to the last command separator (the current shape
226
- // prefixes `endLocal & goto … & ` to the launch).
227
- for (let i = segment.length - 1; i >= 0; i -= 1) {
228
- if (!segment[i].quoted && SEPARATORS.has(segment[i].value)) {
229
- segment = segment.slice(i + 1);
230
- break;
231
- }
232
- }
233
- const legacyIfBranch = segment[0] && !segment[0].quoted && /^\($/.test(segment[0].value);
234
- if (legacyIfBranch) segment = segment.slice(1);
235
- const unsafe = segment.slice(0, -1).find((token) => !token.quoted && /[&|<>^()%]/.test(token.value));
236
- if (unsafe) {
237
- return { command: null, reason: `${shimPath} has a launch line this CLI will not interpret (${unsafe.value})` };
238
- }
239
-
240
- const targetToken = segment[segment.length - 2];
241
- const target = targetToken?.quoted ? fromShimDir(targetToken.value, shimDir) : null;
242
- if (!target) {
243
- return { command: null, reason: `${shimPath} does not name a target beside itself` };
244
- }
245
- if (!existsSync(target)) {
246
- return { command: null, reason: `${shimPath} points at ${target}, which does not exist` };
247
- }
248
- const ext = extOf(target);
249
-
250
- if (BINARY_EXTS.has(ext)) {
251
- if (segment.length !== 2) {
252
- return { command: null, reason: `${shimPath} runs ${target} through another program` };
253
- }
254
- return { command: target, args: [] };
255
- }
256
-
257
- if (SCRIPT_EXTS.has(ext)) {
258
- const program = segment[0];
259
- const shimArgs = segment.slice(1, -2);
260
- if (segment.length < 3 || !programIsNode(program, shimDir, String(text))) {
261
- return { command: null, reason: `${shimPath} runs ${target} with a program other than node` };
262
- }
263
- const badArg = shimArgs.find((token) => token.quoted || !SAFE_SHIM_ARG.test(token.value));
264
- if (badArg) {
265
- return { command: null, reason: `${shimPath} passes an argument this CLI will not interpret (${badArg.value})` };
266
- }
267
- const nodeBeside = win32.join(shimDir, 'node.exe');
268
- const node = existsSync(nodeBeside) ? nodeBeside : execPath;
269
- return { command: node, args: [...shimArgs.map((token) => token.value), target] };
270
- }
271
-
272
- return { command: null, reason: `${shimPath} launches ${target}, which is neither a script nor an .exe` };
273
- };
274
-
275
- const classify = (file, deps) => {
276
- const ext = extOf(file);
277
- if (BINARY_EXTS.has(ext)) return { command: file, args: [], source: file };
278
- if (SHIM_EXTS.has(ext)) {
279
- let text;
280
- try {
281
- text = deps.readFileSync(file, 'utf8');
282
- } catch (err) {
283
- return { command: null, reason: `${file} could not be read: ${err.message}`, source: file };
284
- }
285
- return { ...parseCmdShim(text, file, deps), source: file };
286
- }
287
- return null;
288
- };
289
-
290
- /**
291
- * Resolve `name` to something spawnable without a shell, with the reason when
292
- * it cannot be. On anything but win32, `{ command: name, args: [] }`.
293
- * On win32, `source` is the file found on PATH (the shim, or the .exe).
294
- * `cwd` is the child's working directory; relative PATH entries are read
295
- * against it (unset: this process's, as before), and npm's own launcher asks
296
- * npm for its prefix there.
297
- *
298
- * @returns {{ command: string, args: string[], source?: string }
299
- * | { command: null, reason: string, source?: string }}
300
- */
301
- export const resolveWindowsCommandWithReason = (name, {
302
- platform = process.platform,
303
- env = process.env,
304
- existsSync = realExistsSync,
305
- readFileSync = realReadFileSync,
306
- execPath = process.execPath,
307
- spawnSync = realSpawnSync,
308
- cwd,
309
- } = {}) => {
310
- if (platform !== 'win32') return { command: name, args: [] };
311
- const deps = { existsSync, readFileSync, execPath, env, spawnSync, cwd };
312
- const exts = pathExtensions(env);
313
- const hasKnownExt = [...BINARY_EXTS, ...SHIM_EXTS].includes(extOf(name));
314
- // PATHEXT is upper case by convention and NTFS ignores case, so the
315
- // extension is appended in lower case: the same file, a tidier path.
316
- const candidatesFor = (base) => (hasKnownExt ? [base] : exts.map((ext) => base + ext.toLowerCase()));
317
-
318
- // An explicit path is checked where it points, never searched for.
319
- if (/[\\/]/.test(name) || win32.isAbsolute(name)) {
320
- // A path that names an existing file is that file, whatever its extension.
321
- for (const candidate of existsSync(name) ? [name] : candidatesFor(name)) {
322
- if (!existsSync(candidate)) continue;
323
- return classify(candidate, deps) || {
324
- command: null,
325
- reason: `${candidate} is a ${extOf(candidate)} file, which can only run through a shell`,
326
- source: candidate,
327
- };
328
- }
329
- return { command: null, reason: `${name} was not found` };
330
- }
331
-
332
- // Windows' own order: each PATH folder in turn, each PATHEXT extension
333
- // within it. The first file that exists is what a shell would run, so it
334
- // is decisive: when it is something this CLI cannot launch without a shell
335
- // (a `.vbs`, a `.js`, a `.ps1` someone added to PATHEXT), the answer is a
336
- // refusal, never a later match, which would be a different program from the
337
- // one the person runs by typing the name.
338
- for (const dir of pathEntries(env, cwd)) {
339
- for (const candidate of candidatesFor(win32.join(dir, name))) {
340
- if (!existsSync(candidate)) continue;
341
- return classify(candidate, deps) || {
342
- command: null,
343
- reason: `${candidate} is first on PATH, and a ${extOf(candidate)} file can only run through a shell`,
344
- source: candidate,
345
- };
346
- }
347
- }
348
- return { command: null, reason: `${name} was not found on PATH (searched ${exts.join(', ')})` };
349
- };
350
-
351
- /** `resolveWindowsCommandWithReason` without the reason: null when unresolvable. */
352
- export const resolveWindowsCommand = (name, deps) => {
353
- const result = resolveWindowsCommandWithReason(name, deps);
354
- return result.command ? { command: result.command, args: result.args } : null;
355
- };
356
-
357
- /**
358
- * The command and argv an adapter spawns: unchanged off Windows; on Windows,
359
- * the resolved target with its argv prefix ahead of the adapter's own args.
360
- * Throws (code ENOENT) with the reason when the name cannot be launched
361
- * without a shell.
362
- */
363
- export const resolveLaunch = (command, args, deps = {}) => {
364
- const resolved = resolveWindowsCommandWithReason(command, deps);
365
- if (!resolved.command) {
366
- const err = new Error(`cannot launch ${command} on Windows without a shell: ${resolved.reason}`);
367
- err.code = 'ENOENT';
368
- throw err;
369
- }
370
- return { command: resolved.command, args: [...resolved.args, ...args], source: resolved.source };
371
- };
372
-
373
- /**
374
- * A stdio MCP server's argv as the runtime (Claude Code, Codex) should launch
375
- * it. Those runtimes spawn the server themselves, with no shell, so on Windows
376
- * the default `npx -y @commonlyai/mcp@latest` would hit the same `npx.cmd`
377
- * wall. On win32, `argv[0]` is resolved here and its prefix spliced in (an npm
378
- * shim becomes `node.exe …\npx-cli.js -y @commonlyai/mcp@latest`). When it
379
- * cannot be resolved the entry is left as declared and one line says why.
380
- * Off Windows the argv is returned as is (the same array).
381
- */
382
- export const resolveMcpCommand = (argv, deps = {}, { label = 'mcp', name = '', warn } = {}) => {
383
- if ((deps.platform || process.platform) !== 'win32') return argv;
384
- if (!Array.isArray(argv) || argv.length === 0 || typeof argv[0] !== 'string') return argv;
385
- const resolved = resolveWindowsCommandWithReason(argv[0], deps);
386
- if (!resolved.command) {
387
- const line = `[${label}] MCP server ${name || argv[0]}: left as declared, ${resolved.reason}`;
388
- // eslint-disable-next-line no-console
389
- (warn || console.warn)(line);
390
- return argv;
391
- }
392
- return [resolved.command, ...resolved.args, ...argv.slice(1)];
393
- };
@@ -1,87 +0,0 @@
1
- /**
2
- * The read-only `workspace_git` MCP server (git-readonly-server.js), as an
3
- * adapter attaches it to a sandboxed seat. Shared by the claude adapter (for a
4
- * workspace-trust seat) and the codex adapter (for a public seat), so both
5
- * give the model the same one git tool, run against the same real binary.
6
- */
7
- import { spawnSync } from 'child_process';
8
- import { realpathSync } from 'fs';
9
- import { dirname, join } from 'path';
10
- import { fileURLToPath } from 'url';
11
- import { wrapArgvWithSeatbelt } from './sandbox/seatbelt.js';
12
-
13
- // The name git-readonly-server.js serves under. Kept here, not imported: the
14
- // server module pulls in git-readonly.js and its execFile, which an adapter has
15
- // no use for (git-readonly-server.test.mjs checks the two names agree).
16
- export const WORKSPACE_GIT_SERVER_NAME = 'workspace_git';
17
-
18
- // The tool is opt-in: an adapter attaches it only when the environment the
19
- // daemon itself runs in sets COMMONLY_WORKSPACE_GIT=1. Read from the daemon's
20
- // own environment, never from a seat's record, so a seat cannot turn it on.
21
- export const WORKSPACE_GIT_ENV = 'COMMONLY_WORKSPACE_GIT';
22
- export const workspaceGitEnabled = (env = process.env) => env?.[WORKSPACE_GIT_ENV] === '1';
23
-
24
- // The server runs inside the sandbox, so the sandbox must admit its code
25
- // (cli/src/lib, plus package.json for the module type) and the git binary it runs.
26
- export const WORKSPACE_GIT_SERVER = fileURLToPath(new URL('./git-readonly-server.js', import.meta.url));
27
- export const WORKSPACE_GIT_PACKAGE_JSON = fileURLToPath(new URL('../../package.json', import.meta.url));
28
-
29
- // macOS's /usr/bin/git is an xcrun shim, and it fails inside a sandbox ("No
30
- // developer tools were found"). So the sandbox gets the binary git itself runs:
31
- // `git --exec-path`, resolved here on the host, holds a `git` that links to it.
32
- export const resolveRealGit = ({ run = spawnSync } = {}) => {
33
- const result = run('git', ['--exec-path'], { encoding: 'utf8', timeout: 5000 });
34
- const execDir = result?.status === 0 ? String(result.stdout || '').trim() : '';
35
- if (!execDir) return null;
36
- try {
37
- return realpathSync(join(execDir, 'git'));
38
- } catch {
39
- return null;
40
- }
41
- };
42
-
43
- // The MCP server entry and the paths the sandbox has to let it read, for a
44
- // seat whose workspace is `root`. Null when this machine has no git.
45
- export const workspaceGitAttachment = (root, { git = resolveRealGit() } = {}) => {
46
- if (!root || !git) return null;
47
- return {
48
- server: {
49
- name: WORKSPACE_GIT_SERVER_NAME,
50
- transport: 'stdio',
51
- command: [process.execPath, WORKSPACE_GIT_SERVER, '--root', root, '--git', git],
52
- },
53
- executable: git,
54
- readDirectories: [dirname(WORKSPACE_GIT_SERVER)],
55
- readPaths: [WORKSPACE_GIT_PACKAGE_JSON],
56
- };
57
- };
58
-
59
- // For a public Codex seat. Codex starts its MCP servers outside the seat's
60
- // permission profile, so a bare server would run git as the operator, with the
61
- // network on and every file the profile denies in reach. This one runs under a
62
- // Seatbelt of its own: the workspace read-only with its .env, .npmrc and
63
- // .yarnrc* files unreadable, as the profile has them, no network, its code and
64
- // git readable, and nothing writable but `stateDir`, an empty folder of its own (not
65
- // the folder that holds the seat's credential). macOS only: elsewhere there is
66
- // no wrapper here yet, so the seat gets no tool rather than an unconfined one.
67
- export const confinedWorkspaceGitServer = (root, {
68
- stateDir,
69
- platform = process.platform,
70
- git = platform === 'darwin' ? resolveRealGit() : null,
71
- wrap = wrapArgvWithSeatbelt,
72
- } = {}) => {
73
- if (platform !== 'darwin' || !root || !stateDir || !git) return null;
74
- const attachment = workspaceGitAttachment(root, { git });
75
- const command = wrap(attachment.server.command, {
76
- workspacePath: root,
77
- workspaceAccess: 'read',
78
- executablePath: git,
79
- statePath: stateDir,
80
- readOnlyDirectories: attachment.readDirectories,
81
- readOnlyPaths: attachment.readPaths,
82
- network: 'loopback',
83
- processInfo: 'self',
84
- denyRegistryConfig: true,
85
- });
86
- return { ...attachment.server, command };
87
- };
@@ -1,162 +0,0 @@
1
- /**
2
- * The two facts a `workspace`-trust seat depends on (TASK-294), as pure
3
- * predicates so the attach path, the first-run bootstrap and the per-spawn
4
- * check all read them the same way.
5
- *
6
- * 1. Every pod the seat answers in is CONFIRMED private: not publicly
7
- * readable, not listed, and invite-only. Visibility alone is not enough,
8
- * because any agent can add itself to an open pod an agent created, listed
9
- * or not. A pod whose fields the server did not report counts as public:
10
- * the workspace tier lets the room read the start directory, so an unknown
11
- * room fails closed.
12
- * 2. The start directory is a directory worth exposing to that room, and not
13
- * one that contains the operator's whole account: `/`, `$HOME`, an ancestor
14
- * of `$HOME`, or a directory inside one of the credential stores the public
15
- * policy denies by name.
16
- */
17
- import { realpathSync } from 'fs';
18
- import { homedir } from 'os';
19
- import {
20
- isAbsolute, join, relative, resolve, sep,
21
- } from 'path';
22
-
23
- /** A pod is private only when the server says so in all three fields. */
24
- export const podIsConfirmedPrivate = (pod) => (
25
- typeof pod?.publicRead === 'boolean'
26
- && typeof pod?.communityListed === 'boolean'
27
- && !pod.publicRead
28
- && !pod.communityListed
29
- && pod.joinPolicy === 'invite-only'
30
- );
31
-
32
- /** True only for a non-empty list in which every pod is confirmed private. */
33
- export const allPodsConfirmedPrivate = (pods) => (
34
- Array.isArray(pods) && pods.length > 0 && pods.every(podIsConfirmedPrivate)
35
- );
36
-
37
- // The stores claude.js denies by absolute path for a public seat. A start
38
- // directory inside one of them would put the seat's own credentials in reach.
39
- const CREDENTIAL_DIRS = ['.commonly', '.claude', '.codex', '.ssh', '.aws', '.config'];
40
-
41
- const realOrResolved = (p) => {
42
- try {
43
- return realpathSync(p);
44
- } catch {
45
- return resolve(p);
46
- }
47
- };
48
-
49
- const isInsideOrEqual = (child, parent) => {
50
- const rel = relative(parent, child);
51
- return rel === '' || (rel.split(sep)[0] !== '..' && !isAbsolute(rel));
52
- };
53
-
54
- /**
55
- * Why `dir` cannot be a workspace seat's start directory, or `null` when it can.
56
- * Symlinks are resolved first, so a link to `$HOME` is refused as `$HOME`.
57
- */
58
- export const workspaceStartDirRefusal = (dir, { home = homedir() } = {}) => {
59
- if (typeof dir !== 'string' || !isAbsolute(dir)) {
60
- return 'the start directory is not an absolute path';
61
- }
62
- const real = realOrResolved(dir);
63
- const realHome = realOrResolved(home);
64
- if (real === '/') return 'the start directory is /';
65
- if (isInsideOrEqual(realHome, real)) {
66
- return real === realHome
67
- ? 'the start directory is your home directory'
68
- : 'the start directory contains your home directory';
69
- }
70
- const store = CREDENTIAL_DIRS.find((name) => isInsideOrEqual(real, join(realHome, name)));
71
- if (store) return `the start directory is inside ~/${store}`;
72
- return null;
73
- };
74
-
75
- const PUBLIC_POD_REASON = 'a pod it answers in is publicly readable or open to join, or did not report whether it is';
76
- // claude.js runs a workspace seat under Seatbelt on macOS and under bwrap on
77
- // Linux (#2784). Native Windows has no sandbox for it, so there the default
78
- // would be a seat that fails every turn rather than one that reads a project.
79
- const UNSUPPORTED_OS_REASON = 'an agent works in its folder only on macOS and Linux for now';
80
-
81
- /**
82
- * Where a first-run claude seat may read (TASK-294), from three facts the
83
- * bootstrap already holds: the adapter it detected, the rooms its token
84
- * answers in, and the directory the person ran it from. It returns the start
85
- * directory only when all three allow it, and otherwise the reason the seat
86
- * keeps the `public` default it had before, plus a `fix` when the person can
87
- * change the outcome by starting somewhere else. Any other adapter gets `null`:
88
- * `workspace` is claude-only for now, so there is nothing to decide.
89
- */
90
- export const firstRunWorkspace = ({
91
- adapterName, installations, startDir, home, platform = process.platform,
92
- } = {}) => {
93
- if (adapterName !== 'claude') return null;
94
- if (platform !== 'darwin' && platform !== 'linux') {
95
- return { workspacePath: null, reason: UNSUPPORTED_OS_REASON };
96
- }
97
- if (!allPodsConfirmedPrivate(installations)) {
98
- return { workspacePath: null, reason: PUBLIC_POD_REASON };
99
- }
100
- const refusal = workspaceStartDirRefusal(startDir, home === undefined ? {} : { home });
101
- if (refusal) {
102
- return {
103
- workspacePath: null,
104
- reason: refusal,
105
- fix: 'To let it read a project, run this command from that project\'s folder.',
106
- };
107
- }
108
- return { workspacePath: realOrResolved(startDir), reason: null };
109
- };
110
-
111
- /**
112
- * Where one turn of a seat runs, and under which trust. A pod can turn public
113
- * after attach, so a `workspace` seat's rooms are re-read before every turn
114
- * (`installations` is that read, or `null` when it failed). A turn that cannot
115
- * confirm them all private runs as a `public` seat in its scratch folder: the
116
- * confinement it would have had without the workspace. Any other trust runs
117
- * where and as it was started.
118
- */
119
- export const workspaceTurnPlacement = ({
120
- environment, workspacePath, installations, scratchDir,
121
- } = {}) => {
122
- const cwd = workspacePath || scratchDir;
123
- if (environment?.sandbox?.trust !== 'workspace' || allPodsConfirmedPrivate(installations)) {
124
- return { environment, cwd, reason: null };
125
- }
126
- // Its write/web grants widen the workspace tier only, so the public turn
127
- // drops them rather than carry them into a room strangers can read.
128
- const { allow: _grants, ...sandbox } = environment.sandbox;
129
- return {
130
- environment: { ...environment, sandbox: { ...sandbox, trust: 'public' } },
131
- cwd: scratchDir,
132
- reason: installations === null ? 'its pods could not be read' : PUBLIC_POD_REASON,
133
- };
134
- };
135
-
136
- /**
137
- * Where one turn of a Codex seat runs when the person opted in to running it
138
- * unsandboxed (windows-unsandboxed.js; native Windows, or Linux since F387). A
139
- * pod can turn public after the run starts, so its rooms are re-read before
140
- * every turn, as for a `workspace` seat. With every pod confirmed private, the
141
- * turn has no sandbox block and Codex runs unconfined. Anything else, a failed
142
- * read included, is placed `public`, and the run loop refuses that turn on
143
- * every platform, so strangers can never reach an unconfined shell.
144
- */
145
- export const unconfinedTurnPlacement = ({ environment, installations, cwd } = {}) => {
146
- const { sandbox: _sandbox, ...unsandboxed } = environment || {};
147
- if (allPodsConfirmedPrivate(installations)) return { environment: unsandboxed, cwd, reason: null };
148
- return {
149
- environment: { ...unsandboxed, sandbox: { trust: 'public' } },
150
- cwd,
151
- reason: installations === null ? 'its pods could not be read' : PUBLIC_POD_REASON,
152
- };
153
- };
154
-
155
- export default {
156
- podIsConfirmedPrivate,
157
- allPodsConfirmedPrivate,
158
- workspaceStartDirRefusal,
159
- firstRunWorkspace,
160
- workspaceTurnPlacement,
161
- unconfinedTurnPlacement,
162
- };