@vibe80/vibe80 0.1.1

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 (123) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +52 -0
  3. package/bin/vibe80.js +176 -0
  4. package/client/dist/assets/DiffPanel-C_IGzKI5.js +1 -0
  5. package/client/dist/assets/ExplorerPanel-BtlyAT00.js +11 -0
  6. package/client/dist/assets/LogsPanel-BW79JWzR.js +1 -0
  7. package/client/dist/assets/SettingsPanel-b9B7ygP_.js +1 -0
  8. package/client/dist/assets/TerminalPanel-C3fc1HbK.js +1 -0
  9. package/client/dist/assets/browser-e3WgtMs-.js +8 -0
  10. package/client/dist/assets/index-CgqGyssr.css +32 -0
  11. package/client/dist/assets/index-DnwKjoj7.js +706 -0
  12. package/client/dist/assets/vibe80_dark-D7OVPKcU.svg +51 -0
  13. package/client/dist/assets/vibe80_light-BJK37ybI.svg +50 -0
  14. package/client/dist/favicon.ico +0 -0
  15. package/client/dist/favicon.png +0 -0
  16. package/client/dist/favicon.svg +35 -0
  17. package/client/dist/index.html +14 -0
  18. package/client/index.html +16 -0
  19. package/client/package.json +34 -0
  20. package/client/public/favicon.ico +0 -0
  21. package/client/public/favicon.png +0 -0
  22. package/client/public/favicon.svg +35 -0
  23. package/client/public/pwa-192x192.png +0 -0
  24. package/client/public/pwa-512x512.png +0 -0
  25. package/client/src/App.jsx +3131 -0
  26. package/client/src/assets/logo_small.png +0 -0
  27. package/client/src/assets/vibe80_dark.svg +51 -0
  28. package/client/src/assets/vibe80_light.svg +50 -0
  29. package/client/src/components/Chat/ChatComposer.jsx +228 -0
  30. package/client/src/components/Chat/ChatMessages.jsx +811 -0
  31. package/client/src/components/Chat/ChatToolbar.jsx +109 -0
  32. package/client/src/components/Chat/useChatComposer.js +462 -0
  33. package/client/src/components/Diff/DiffPanel.jsx +129 -0
  34. package/client/src/components/Explorer/ExplorerPanel.jsx +449 -0
  35. package/client/src/components/Logs/LogsPanel.jsx +80 -0
  36. package/client/src/components/SessionGate/SessionGate.jsx +874 -0
  37. package/client/src/components/Settings/SettingsPanel.jsx +212 -0
  38. package/client/src/components/Terminal/TerminalPanel.jsx +39 -0
  39. package/client/src/components/Topbar/Topbar.jsx +101 -0
  40. package/client/src/components/WorktreeTabs.css +419 -0
  41. package/client/src/components/WorktreeTabs.jsx +604 -0
  42. package/client/src/hooks/useAttachments.jsx +125 -0
  43. package/client/src/hooks/useBacklog.js +254 -0
  44. package/client/src/hooks/useChatClear.js +90 -0
  45. package/client/src/hooks/useChatCollapse.js +42 -0
  46. package/client/src/hooks/useChatCommands.js +294 -0
  47. package/client/src/hooks/useChatExport.js +144 -0
  48. package/client/src/hooks/useChatMessagesState.js +69 -0
  49. package/client/src/hooks/useChatSend.js +158 -0
  50. package/client/src/hooks/useChatSocket.js +1239 -0
  51. package/client/src/hooks/useDiffNavigation.js +19 -0
  52. package/client/src/hooks/useExplorerActions.js +1184 -0
  53. package/client/src/hooks/useGitIdentity.js +114 -0
  54. package/client/src/hooks/useLayoutMode.js +31 -0
  55. package/client/src/hooks/useLocalPreferences.js +131 -0
  56. package/client/src/hooks/useMessageSync.js +30 -0
  57. package/client/src/hooks/useNotifications.js +132 -0
  58. package/client/src/hooks/usePaneNavigation.js +67 -0
  59. package/client/src/hooks/usePanelState.js +13 -0
  60. package/client/src/hooks/useProviderSelection.js +70 -0
  61. package/client/src/hooks/useRepoBranchesModels.js +218 -0
  62. package/client/src/hooks/useRepoStatus.js +350 -0
  63. package/client/src/hooks/useRpcLogActions.js +19 -0
  64. package/client/src/hooks/useRpcLogView.js +58 -0
  65. package/client/src/hooks/useSessionHandoff.js +97 -0
  66. package/client/src/hooks/useSessionLifecycle.js +287 -0
  67. package/client/src/hooks/useSessionReset.js +63 -0
  68. package/client/src/hooks/useSessionResync.js +77 -0
  69. package/client/src/hooks/useTerminalSession.js +328 -0
  70. package/client/src/hooks/useToolbarExport.js +27 -0
  71. package/client/src/hooks/useTurnInterrupt.js +43 -0
  72. package/client/src/hooks/useVibe80Forms.js +128 -0
  73. package/client/src/hooks/useWorkspaceAuth.js +932 -0
  74. package/client/src/hooks/useWorktreeCloseConfirm.js +46 -0
  75. package/client/src/hooks/useWorktrees.js +396 -0
  76. package/client/src/i18n.jsx +87 -0
  77. package/client/src/index.css +5147 -0
  78. package/client/src/locales/en.json +37 -0
  79. package/client/src/locales/fr.json +321 -0
  80. package/client/src/main.jsx +16 -0
  81. package/client/vite.config.js +62 -0
  82. package/docs/api/asyncapi.json +1511 -0
  83. package/docs/api/openapi.json +3242 -0
  84. package/git_hooks/prepare-commit-msg +35 -0
  85. package/package.json +36 -0
  86. package/server/package.json +29 -0
  87. package/server/scripts/rotate-workspace-secret.js +101 -0
  88. package/server/src/claudeClient.js +454 -0
  89. package/server/src/clientEvents.js +594 -0
  90. package/server/src/clientFactory.js +164 -0
  91. package/server/src/codexClient.js +468 -0
  92. package/server/src/config.js +27 -0
  93. package/server/src/helpers.js +138 -0
  94. package/server/src/index.js +1641 -0
  95. package/server/src/middleware/auth.js +93 -0
  96. package/server/src/middleware/debug.js +89 -0
  97. package/server/src/middleware/errorTypes.js +60 -0
  98. package/server/src/providerLogger.js +60 -0
  99. package/server/src/routes/files.js +114 -0
  100. package/server/src/routes/git.js +183 -0
  101. package/server/src/routes/health.js +13 -0
  102. package/server/src/routes/sessions.js +407 -0
  103. package/server/src/routes/workspaces.js +296 -0
  104. package/server/src/routes/worktrees.js +993 -0
  105. package/server/src/runAs.js +458 -0
  106. package/server/src/runtimeStore.js +32 -0
  107. package/server/src/services/auth.js +157 -0
  108. package/server/src/services/claudeThreadDirectory.js +33 -0
  109. package/server/src/services/session.js +918 -0
  110. package/server/src/services/workspace.js +858 -0
  111. package/server/src/storage/index.js +17 -0
  112. package/server/src/storage/redis.js +412 -0
  113. package/server/src/storage/sqlite.js +649 -0
  114. package/server/src/worktreeManager.js +717 -0
  115. package/server/tests/README.md +13 -0
  116. package/server/tests/factories/workspaceFactory.js +13 -0
  117. package/server/tests/fixtures/workspaceCredentials.json +4 -0
  118. package/server/tests/integration/routes/workspaces-routes.test.js +626 -0
  119. package/server/tests/setup/env.js +9 -0
  120. package/server/tests/unit/helpers.test.js +95 -0
  121. package/server/tests/unit/services/auth.test.js +181 -0
  122. package/server/tests/unit/services/workspace.test.js +115 -0
  123. package/server/vitest.config.js +23 -0
@@ -0,0 +1,458 @@
1
+ import { spawn } from "child_process";
2
+ import os from "os";
3
+ import path from "path";
4
+ import { GIT_HOOKS_DIR } from "./config.js";
5
+
6
+ const RUN_AS_HELPER = process.env.VIBE80_RUN_AS_HELPER || "/usr/local/bin/vibe80-run-as";
7
+ const SUDO_PATH = process.env.VIBE80_SUDO_PATH || "sudo";
8
+ const DEPLOYMENT_MODE = process.env.DEPLOYMENT_MODE;
9
+ const IS_MONO_USER = DEPLOYMENT_MODE === "mono_user";
10
+ const WORKSPACE_ROOT_DIRECTORY = process.env.WORKSPACE_ROOT_DIRECTORY || "/workspaces";
11
+ const ALLOWED_ENV_KEYS = new Set([
12
+ "GIT_SSH_COMMAND",
13
+ "GIT_CONFIG_GLOBAL",
14
+ "GIT_TERMINAL_PROMPT",
15
+ "TERM",
16
+ "TMPDIR",
17
+ "CLAUDE_CODE_TMPDIR",
18
+ ]);
19
+ export const DEFAULT_ALLOW_RO = [
20
+ "/bin",
21
+ "/etc",
22
+ "/lib",
23
+ "/lib64",
24
+ "/usr",
25
+ "/proc",
26
+ GIT_HOOKS_DIR,
27
+ ];
28
+ export const DEFAULT_ALLOW_RW = [
29
+ "/dev",
30
+ "/tmp",
31
+ ];
32
+
33
+ let ensureWorkspaceUserExistsRef = null;
34
+
35
+ const ensureWorkspaceUserExistsCached = async (workspaceId) => {
36
+ if (IS_MONO_USER) {
37
+ return;
38
+ }
39
+ if (!ensureWorkspaceUserExistsRef) {
40
+ const mod = await import("./services/workspace.js");
41
+ ensureWorkspaceUserExistsRef = mod.ensureWorkspaceUserExists;
42
+ }
43
+ await ensureWorkspaceUserExistsRef(workspaceId);
44
+ };
45
+
46
+ const normalizePaths = (paths = []) => {
47
+ const seen = new Set();
48
+ const result = [];
49
+ for (const entry of paths) {
50
+ if (!entry) {
51
+ continue;
52
+ }
53
+ const resolved = path.resolve(entry);
54
+ if (seen.has(resolved)) {
55
+ continue;
56
+ }
57
+ seen.add(resolved);
58
+ result.push(resolved);
59
+ }
60
+ return result;
61
+ };
62
+
63
+ export const buildSandboxArgs = (options = {}) => {
64
+ const homeDir = options.homeDir || (options.workspaceId
65
+ ? getWorkspaceHome(options.workspaceId)
66
+ : null);
67
+ const workspaceRootDir = options.workspaceRootDir || (options.workspaceId
68
+ ? getWorkspaceRoot(options.workspaceId)
69
+ : null);
70
+ const allowRo = normalizePaths([
71
+ ...(options.allowRo || DEFAULT_ALLOW_RO),
72
+ ...(options.extraAllowRo || []),
73
+ ]);
74
+ const allowRw = normalizePaths([
75
+ ...(options.allowRw || DEFAULT_ALLOW_RW),
76
+ ...(options.extraAllowRw || []),
77
+ homeDir,
78
+ options.repoDir,
79
+ options.cwd,
80
+ options.attachmentsDir,
81
+ options.tmpDir,
82
+ ]);
83
+ const allowRoFiles = normalizePaths([
84
+ ...(options.allowRoFiles || []),
85
+ ...(options.extraAllowRoFiles || []),
86
+ ]);
87
+ const allowRwFiles = normalizePaths([
88
+ ...(options.allowRwFiles || []),
89
+ ...(options.extraAllowRwFiles || []),
90
+ ]);
91
+ const args = [];
92
+ if (allowRo.length) {
93
+ args.push("--allow-ro", allowRo.join(","));
94
+ }
95
+ if (allowRw.length) {
96
+ args.push("--allow-rw", allowRw.join(","));
97
+ }
98
+ if (allowRoFiles.length) {
99
+ args.push("--allow-ro-file", allowRoFiles.join(","));
100
+ }
101
+ if (allowRwFiles.length) {
102
+ args.push("--allow-rw-file", allowRwFiles.join(","));
103
+ }
104
+ const netMode = options.netMode
105
+ ?? (options.internetAccess === false ? "none" : "tcp:22,53,443");
106
+ args.push("--net", netMode);
107
+ args.push("--seccomp", options.seccomp || "default");
108
+ return args;
109
+ };
110
+
111
+ const collectEnvPairs = (env = {}) =>
112
+ Object.entries(env)
113
+ .filter(([key]) => ALLOWED_ENV_KEYS.has(key))
114
+ .map(([key, value]) => `${key}=${value}`);
115
+
116
+ const buildRunAsArgs = (workspaceId, command, args, options = {}) => {
117
+ const result = ["--workspace-id", workspaceId];
118
+ const envPairs = collectEnvPairs(options.env || {});
119
+ if (options.cwd) {
120
+ result.push("--cwd", options.cwd);
121
+ }
122
+ if (envPairs.length) {
123
+ envPairs.forEach((pair) => {
124
+ result.push("--env", pair);
125
+ });
126
+ }
127
+ if (options.sandbox) {
128
+ result.push(...buildSandboxArgs(options));
129
+ }
130
+ result.push("--", command, ...args);
131
+ return { args: result, envPairs };
132
+ };
133
+
134
+ const buildRunEnv = (options = {}) => {
135
+ const env = { ...process.env };
136
+ if (options.env) {
137
+ for (const [key, value] of Object.entries(options.env)) {
138
+ if (!ALLOWED_ENV_KEYS.has(key)) {
139
+ continue;
140
+ }
141
+ env[key] = value;
142
+ }
143
+ }
144
+ return env;
145
+ };
146
+
147
+ export const getWorkspaceHome = (workspaceId) => {
148
+ const homeBase = process.env.WORKSPACE_HOME_BASE || "/home";
149
+ return IS_MONO_USER ? os.homedir() : path.join(homeBase, workspaceId);
150
+ };
151
+
152
+ export const getWorkspaceRoot = (workspaceId) =>
153
+ (IS_MONO_USER
154
+ ? path.join(os.homedir(), "vibe80_workspace")
155
+ : path.join(WORKSPACE_ROOT_DIRECTORY, workspaceId));
156
+
157
+ const validateCwd = (workspaceId, cwd) => {
158
+ const resolved = path.resolve(cwd);
159
+ const homeDir = getWorkspaceHome(workspaceId);
160
+ if (
161
+ resolved !== homeDir &&
162
+ !resolved.startsWith(homeDir + path.sep)
163
+ ) {
164
+ throw new Error("cwd outside workspace");
165
+ }
166
+ };
167
+
168
+ export const runCommand = (command, args, options = {}) =>
169
+ new Promise((resolve, reject) => {
170
+ const proc = spawn(command, args, { stdio: ["pipe", "ignore", "pipe"], ...options });
171
+ let stderr = "";
172
+
173
+ proc.stderr.on("data", (chunk) => {
174
+ stderr += chunk.toString();
175
+ });
176
+
177
+ if (options.input) {
178
+ if (typeof options.input.pipe === "function") {
179
+ options.input.pipe(proc.stdin);
180
+ } else {
181
+ proc.stdin.write(options.input);
182
+ proc.stdin.end();
183
+ }
184
+ } else {
185
+ proc.stdin.end();
186
+ }
187
+
188
+ proc.on("error", reject);
189
+ proc.on("close", (code) => {
190
+ if (code === 0) {
191
+ resolve();
192
+ return;
193
+ }
194
+ reject(new Error(stderr.trim() || `${command} exited with ${code}`));
195
+ });
196
+ });
197
+
198
+ export const runCommandOutput = (command, args, options = {}) =>
199
+ new Promise((resolve, reject) => {
200
+ const proc = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"], ...options });
201
+ const stdoutChunks = [];
202
+ let stderr = "";
203
+
204
+ proc.stdout.on("data", (chunk) => {
205
+ stdoutChunks.push(chunk);
206
+ });
207
+
208
+ proc.stderr.on("data", (chunk) => {
209
+ stderr += chunk.toString();
210
+ });
211
+
212
+ if (options.input) {
213
+ if (typeof options.input.pipe === "function") {
214
+ options.input.pipe(proc.stdin);
215
+ } else {
216
+ proc.stdin.write(options.input);
217
+ proc.stdin.end();
218
+ }
219
+ } else {
220
+ proc.stdin.end();
221
+ }
222
+
223
+ proc.on("error", reject);
224
+ proc.on("close", (code) => {
225
+ if (code === 0) {
226
+ const output = Buffer.concat(stdoutChunks);
227
+ resolve(options.binary ? output : output.toString("utf8"));
228
+ return;
229
+ }
230
+ reject(new Error(stderr.trim() || `${command} exited with ${code}`));
231
+ });
232
+ });
233
+
234
+ export const runCommandOutputWithStatus = (command, args, options = {}) =>
235
+ new Promise((resolve, reject) => {
236
+ const proc = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"], ...options });
237
+ const stdoutChunks = [];
238
+ const stderrChunks = [];
239
+
240
+ proc.stdout.on("data", (chunk) => {
241
+ stdoutChunks.push(chunk);
242
+ });
243
+
244
+ proc.stderr.on("data", (chunk) => {
245
+ stderrChunks.push(chunk);
246
+ });
247
+
248
+ if (options.input) {
249
+ if (typeof options.input.pipe === "function") {
250
+ options.input.pipe(proc.stdin);
251
+ } else {
252
+ proc.stdin.write(options.input);
253
+ proc.stdin.end();
254
+ }
255
+ } else {
256
+ proc.stdin.end();
257
+ }
258
+
259
+ proc.on("error", reject);
260
+ proc.on("close", (code) => {
261
+ const stdout = Buffer.concat(stdoutChunks);
262
+ const stderr = Buffer.concat(stderrChunks);
263
+ if (options.binary) {
264
+ resolve({ output: Buffer.concat([stdout, stderr]), code });
265
+ return;
266
+ }
267
+ let output = stdout.toString("utf8");
268
+ const stderrText = stderr.toString("utf8");
269
+ if (stderrText) {
270
+ if (output && !output.endsWith("\n")) {
271
+ output += "\n";
272
+ }
273
+ output += stderrText;
274
+ }
275
+ resolve({ output, code });
276
+ });
277
+ });
278
+
279
+ export const runAsCommand = (workspaceId, command, args, options = {}) =>
280
+ (IS_MONO_USER
281
+ ? (validateCwd(workspaceId, options.cwd || getWorkspaceHome(workspaceId)),
282
+ runCommand(command, args, {
283
+ cwd: options.cwd || getWorkspaceHome(workspaceId),
284
+ env: buildRunEnv(options),
285
+ input: options.input,
286
+ }))
287
+ : (() => {
288
+ const { args: runArgs, envPairs } = buildRunAsArgs(
289
+ workspaceId,
290
+ command,
291
+ args,
292
+ options
293
+ );
294
+ return ensureWorkspaceUserExistsCached(workspaceId).then(() =>
295
+ runCommand(
296
+ SUDO_PATH,
297
+ ["-n", RUN_AS_HELPER, ...runArgs],
298
+ {
299
+ env: process.env,
300
+ input: options.input,
301
+ }
302
+ )
303
+ ).catch((error) => {
304
+ const details = [
305
+ "run-as failed",
306
+ `mode=${DEPLOYMENT_MODE || "unknown"}`,
307
+ `sudo=${SUDO_PATH}`,
308
+ `helper=${RUN_AS_HELPER}`,
309
+ `workspace=${workspaceId}`,
310
+ `command=${command}`,
311
+ `args=${JSON.stringify(args || [])}`,
312
+ envPairs?.length ? `env=${JSON.stringify(envPairs)}` : null,
313
+ `error=${error?.message || error}`,
314
+ ]
315
+ .filter(Boolean)
316
+ .join(" ");
317
+ throw new Error(details);
318
+ });
319
+ })()
320
+ ).catch((error) => {
321
+ const envPairs = collectEnvPairs(options.env || {});
322
+ const details = [
323
+ "run-as failed",
324
+ `mode=${DEPLOYMENT_MODE || "unknown"}`,
325
+ IS_MONO_USER ? null : `sudo=${SUDO_PATH}`,
326
+ IS_MONO_USER ? null : `helper=${RUN_AS_HELPER}`,
327
+ `workspace=${workspaceId}`,
328
+ `command=${command}`,
329
+ `args=${JSON.stringify(args || [])}`,
330
+ envPairs.length ? `env=${JSON.stringify(envPairs)}` : null,
331
+ `error=${error?.message || error}`,
332
+ ]
333
+ .filter(Boolean)
334
+ .join(" ");
335
+ throw new Error(details);
336
+ });
337
+
338
+ export const runAsCommandOutput = (workspaceId, command, args, options = {}) =>
339
+ (IS_MONO_USER
340
+ ? (validateCwd(workspaceId, options.cwd || getWorkspaceHome(workspaceId)),
341
+ runCommandOutput(command, args, {
342
+ cwd: options.cwd || getWorkspaceHome(workspaceId),
343
+ env: buildRunEnv(options),
344
+ input: options.input,
345
+ binary: options.binary,
346
+ }))
347
+ : (() => {
348
+ const { args: runArgs, envPairs } = buildRunAsArgs(
349
+ workspaceId,
350
+ command,
351
+ args,
352
+ options
353
+ );
354
+ return ensureWorkspaceUserExistsCached(workspaceId).then(() =>
355
+ runCommandOutput(
356
+ SUDO_PATH,
357
+ ["-n", RUN_AS_HELPER, ...runArgs],
358
+ {
359
+ env: process.env,
360
+ input: options.input,
361
+ binary: options.binary,
362
+ }
363
+ )
364
+ ).catch((error) => {
365
+ const details = [
366
+ "run-as output failed",
367
+ `mode=${DEPLOYMENT_MODE || "unknown"}`,
368
+ `sudo=${SUDO_PATH}`,
369
+ `helper=${RUN_AS_HELPER}`,
370
+ `workspace=${workspaceId}`,
371
+ `command=${command}`,
372
+ `args=${JSON.stringify(args || [])}`,
373
+ envPairs?.length ? `env=${JSON.stringify(envPairs)}` : null,
374
+ `error=${error?.message || error}`,
375
+ ]
376
+ .filter(Boolean)
377
+ .join(" ");
378
+ throw new Error(details);
379
+ });
380
+ })()
381
+ ).catch((error) => {
382
+ const envPairs = collectEnvPairs(options.env || {});
383
+ const details = [
384
+ "run-as output failed",
385
+ `mode=${DEPLOYMENT_MODE || "unknown"}`,
386
+ IS_MONO_USER ? null : `sudo=${SUDO_PATH}`,
387
+ IS_MONO_USER ? null : `helper=${RUN_AS_HELPER}`,
388
+ `workspace=${workspaceId}`,
389
+ `command=${command}`,
390
+ `args=${JSON.stringify(args || [])}`,
391
+ envPairs.length ? `env=${JSON.stringify(envPairs)}` : null,
392
+ `error=${error?.message || error}`,
393
+ ]
394
+ .filter(Boolean)
395
+ .join(" ");
396
+ throw new Error(details);
397
+ });
398
+
399
+ export const runAsCommandOutputWithStatus = (workspaceId, command, args, options = {}) =>
400
+ (IS_MONO_USER
401
+ ? (validateCwd(workspaceId, options.cwd || getWorkspaceHome(workspaceId)),
402
+ runCommandOutputWithStatus(command, args, {
403
+ cwd: options.cwd || getWorkspaceHome(workspaceId),
404
+ env: buildRunEnv(options),
405
+ input: options.input,
406
+ binary: options.binary,
407
+ }))
408
+ : (() => {
409
+ const { args: runArgs, envPairs } = buildRunAsArgs(
410
+ workspaceId,
411
+ command,
412
+ args,
413
+ options
414
+ );
415
+ return ensureWorkspaceUserExistsCached(workspaceId).then(() =>
416
+ runCommandOutputWithStatus(
417
+ SUDO_PATH,
418
+ ["-n", RUN_AS_HELPER, ...runArgs],
419
+ {
420
+ env: process.env,
421
+ input: options.input,
422
+ binary: options.binary,
423
+ }
424
+ )
425
+ ).catch((error) => {
426
+ const details = [
427
+ "run-as output failed",
428
+ `mode=${DEPLOYMENT_MODE || "unknown"}`,
429
+ `sudo=${SUDO_PATH}`,
430
+ `helper=${RUN_AS_HELPER}`,
431
+ `workspace=${workspaceId}`,
432
+ `command=${command}`,
433
+ `args=${JSON.stringify(args || [])}`,
434
+ envPairs?.length ? `env=${JSON.stringify(envPairs)}` : null,
435
+ `error=${error?.message || error}`,
436
+ ]
437
+ .filter(Boolean)
438
+ .join(" ");
439
+ throw new Error(details);
440
+ });
441
+ })()
442
+ ).catch((error) => {
443
+ const envPairs = collectEnvPairs(options.env || {});
444
+ const details = [
445
+ "run-as output failed",
446
+ `mode=${DEPLOYMENT_MODE || "unknown"}`,
447
+ IS_MONO_USER ? null : `sudo=${SUDO_PATH}`,
448
+ IS_MONO_USER ? null : `helper=${RUN_AS_HELPER}`,
449
+ `workspace=${workspaceId}`,
450
+ `command=${command}`,
451
+ `args=${JSON.stringify(args || [])}`,
452
+ envPairs.length ? `env=${JSON.stringify(envPairs)}` : null,
453
+ `error=${error?.message || error}`,
454
+ ]
455
+ .filter(Boolean)
456
+ .join(" ");
457
+ throw new Error(details);
458
+ });
@@ -0,0 +1,32 @@
1
+ const runtimeSessions = new Map();
2
+
3
+ export const getSessionRuntime = (sessionId) => {
4
+ if (!sessionId) {
5
+ return null;
6
+ }
7
+ let runtime = runtimeSessions.get(sessionId);
8
+ if (!runtime) {
9
+ runtime = {
10
+ sockets: new Set(),
11
+ clients: {},
12
+ worktreeClients: new Map(),
13
+ };
14
+ runtimeSessions.set(sessionId, runtime);
15
+ }
16
+ return runtime;
17
+ };
18
+
19
+ export const getExistingSessionRuntime = (sessionId) => {
20
+ if (!sessionId) {
21
+ return null;
22
+ }
23
+ return runtimeSessions.get(sessionId) || null;
24
+ };
25
+
26
+ export const listSessionRuntimes = () => Array.from(runtimeSessions.values());
27
+
28
+ export const listSessionRuntimeEntries = () => Array.from(runtimeSessions.entries());
29
+
30
+ export const deleteSessionRuntime = (sessionId) => {
31
+ runtimeSessions.delete(sessionId);
32
+ };
@@ -0,0 +1,157 @@
1
+ import storage from "../storage/index.js";
2
+ import { createWorkspaceToken, accessTokenTtlSeconds } from "../middleware/auth.js";
3
+ import { generateId, hashRefreshToken, generateRefreshToken } from "../helpers.js";
4
+
5
+ const refreshTokenTtlSeconds =
6
+ Number(process.env.REFRESH_TOKEN_TTL_SECONDS) || 30 * 24 * 60 * 60;
7
+ const refreshTokenTtlMs = refreshTokenTtlSeconds * 1000;
8
+ const handoffTokenTtlMs =
9
+ Number(process.env.HANDOFF_TOKEN_TTL_MS) || 120 * 1000;
10
+ const monoAuthTokenTtlMs =
11
+ Number(process.env.MONO_AUTH_TOKEN_TTL_MS) || 5 * 60 * 1000;
12
+
13
+ export const handoffTokens = new Map();
14
+ const monoAuthTokens = new Map();
15
+
16
+ export const issueWorkspaceTokens = async (workspaceId) => {
17
+ const workspaceToken = createWorkspaceToken(workspaceId);
18
+ const refreshToken = generateRefreshToken();
19
+ const tokenHash = hashRefreshToken(refreshToken);
20
+ const expiresAt = Date.now() + refreshTokenTtlMs;
21
+ await storage.saveWorkspaceRefreshToken(
22
+ workspaceId,
23
+ tokenHash,
24
+ expiresAt,
25
+ refreshTokenTtlMs
26
+ );
27
+ return {
28
+ workspaceToken,
29
+ refreshToken,
30
+ expiresIn: accessTokenTtlSeconds,
31
+ refreshExpiresIn: refreshTokenTtlSeconds,
32
+ };
33
+ };
34
+
35
+ const buildRefreshError = (code) => {
36
+ if (code === "refresh_token_expired") {
37
+ return {
38
+ status: 401,
39
+ payload: { error: "Refresh token expired.", code },
40
+ };
41
+ }
42
+ if (code === "refresh_token_reused") {
43
+ return {
44
+ status: 401,
45
+ payload: { error: "Refresh token reused.", code },
46
+ };
47
+ }
48
+ return {
49
+ status: 401,
50
+ payload: { error: "Invalid refresh token.", code: "invalid_refresh_token" },
51
+ };
52
+ };
53
+
54
+ export const rotateWorkspaceRefreshToken = async (refreshToken) => {
55
+ const currentTokenHash = hashRefreshToken(refreshToken);
56
+ const nextRefreshToken = generateRefreshToken();
57
+ const nextTokenHash = hashRefreshToken(nextRefreshToken);
58
+ const nextExpiresAt = Date.now() + refreshTokenTtlMs;
59
+ const result = await storage.rotateWorkspaceRefreshToken(
60
+ currentTokenHash,
61
+ nextTokenHash,
62
+ nextExpiresAt,
63
+ refreshTokenTtlMs
64
+ );
65
+ if (!result?.ok || !result.workspaceId) {
66
+ const error = buildRefreshError(result?.code || "invalid_refresh_token");
67
+ return {
68
+ ok: false,
69
+ ...error,
70
+ };
71
+ }
72
+ return {
73
+ ok: true,
74
+ payload: {
75
+ workspaceToken: createWorkspaceToken(result.workspaceId),
76
+ refreshToken: nextRefreshToken,
77
+ expiresIn: accessTokenTtlSeconds,
78
+ refreshExpiresIn: refreshTokenTtlSeconds,
79
+ },
80
+ };
81
+ };
82
+
83
+ export const createHandoffToken = (session) => {
84
+ const now = Date.now();
85
+ const token = generateId("h");
86
+ const record = {
87
+ token,
88
+ sessionId: session.sessionId,
89
+ workspaceId: session.workspaceId,
90
+ createdAt: now,
91
+ expiresAt: now + handoffTokenTtlMs,
92
+ usedAt: null,
93
+ };
94
+ handoffTokens.set(token, record);
95
+ return record;
96
+ };
97
+
98
+ export const createMonoAuthToken = (workspaceId = "default") => {
99
+ const now = Date.now();
100
+ const token = generateId("m");
101
+ const record = {
102
+ token,
103
+ workspaceId,
104
+ createdAt: now,
105
+ expiresAt: now + monoAuthTokenTtlMs,
106
+ usedAt: null,
107
+ };
108
+ monoAuthTokens.set(token, record);
109
+ return record;
110
+ };
111
+
112
+ export const consumeMonoAuthToken = (token) => {
113
+ const record = monoAuthTokens.get(token);
114
+ if (!record) {
115
+ return { ok: false, code: "MONO_AUTH_TOKEN_INVALID" };
116
+ }
117
+ if (record.usedAt) {
118
+ monoAuthTokens.delete(token);
119
+ return { ok: false, code: "MONO_AUTH_TOKEN_USED" };
120
+ }
121
+ if (record.expiresAt && record.expiresAt <= Date.now()) {
122
+ monoAuthTokens.delete(token);
123
+ return { ok: false, code: "MONO_AUTH_TOKEN_EXPIRED" };
124
+ }
125
+ record.usedAt = Date.now();
126
+ monoAuthTokens.delete(token);
127
+ return {
128
+ ok: true,
129
+ workspaceId: record.workspaceId,
130
+ };
131
+ };
132
+
133
+ export const cleanupHandoffTokens = () => {
134
+ if (handoffTokens.size === 0) return;
135
+ const now = Date.now();
136
+ for (const [token, record] of handoffTokens.entries()) {
137
+ if (record.usedAt || (record.expiresAt && record.expiresAt <= now)) {
138
+ handoffTokens.delete(token);
139
+ }
140
+ }
141
+ };
142
+
143
+ export const cleanupMonoAuthTokens = () => {
144
+ if (monoAuthTokens.size === 0) return;
145
+ const now = Date.now();
146
+ for (const [token, record] of monoAuthTokens.entries()) {
147
+ if (record.usedAt || (record.expiresAt && record.expiresAt <= now)) {
148
+ monoAuthTokens.delete(token);
149
+ }
150
+ }
151
+ };
152
+
153
+ export {
154
+ hashRefreshToken,
155
+ refreshTokenTtlMs,
156
+ refreshTokenTtlSeconds,
157
+ };
@@ -0,0 +1,33 @@
1
+ import path from "path";
2
+ import { getWorkspaceHome, runAsCommand } from "../runAs.js";
3
+
4
+ export const buildClaudeThreadRelativeDirectory = (cwd) =>
5
+ String(cwd || "")
6
+ .trim()
7
+ .replaceAll("/", "-");
8
+
9
+ export const resolveClaudeThreadDirectory = (workspaceId, cwd) => {
10
+ const workspaceHome = getWorkspaceHome(workspaceId);
11
+ const threadRelativeDirectory = buildClaudeThreadRelativeDirectory(cwd);
12
+ return path.join(workspaceHome, ".claude", "projects", threadRelativeDirectory);
13
+ };
14
+
15
+ export const copyClaudeThreadDirectory = async (workspaceId, sourceCwd, targetCwd) => {
16
+ const sourceThreadDirectory = resolveClaudeThreadDirectory(workspaceId, sourceCwd);
17
+ const targetThreadDirectory = resolveClaudeThreadDirectory(workspaceId, targetCwd);
18
+ const targetParent = path.dirname(targetThreadDirectory);
19
+
20
+ await runAsCommand(workspaceId, "/bin/mkdir", ["-p", targetParent]);
21
+ await runAsCommand(workspaceId, "/bin/rm", ["-rf", targetThreadDirectory]);
22
+ await runAsCommand(workspaceId, "/bin/mkdir", ["-p", targetThreadDirectory]);
23
+ await runAsCommand(
24
+ workspaceId,
25
+ "/bin/cp",
26
+ ["-a", `${sourceThreadDirectory}/.`, targetThreadDirectory]
27
+ );
28
+
29
+ return {
30
+ sourceThreadDirectory,
31
+ targetThreadDirectory,
32
+ };
33
+ };