@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.
- package/LICENSE +201 -0
- package/README.md +52 -0
- package/bin/vibe80.js +176 -0
- package/client/dist/assets/DiffPanel-C_IGzKI5.js +1 -0
- package/client/dist/assets/ExplorerPanel-BtlyAT00.js +11 -0
- package/client/dist/assets/LogsPanel-BW79JWzR.js +1 -0
- package/client/dist/assets/SettingsPanel-b9B7ygP_.js +1 -0
- package/client/dist/assets/TerminalPanel-C3fc1HbK.js +1 -0
- package/client/dist/assets/browser-e3WgtMs-.js +8 -0
- package/client/dist/assets/index-CgqGyssr.css +32 -0
- package/client/dist/assets/index-DnwKjoj7.js +706 -0
- package/client/dist/assets/vibe80_dark-D7OVPKcU.svg +51 -0
- package/client/dist/assets/vibe80_light-BJK37ybI.svg +50 -0
- package/client/dist/favicon.ico +0 -0
- package/client/dist/favicon.png +0 -0
- package/client/dist/favicon.svg +35 -0
- package/client/dist/index.html +14 -0
- package/client/index.html +16 -0
- package/client/package.json +34 -0
- package/client/public/favicon.ico +0 -0
- package/client/public/favicon.png +0 -0
- package/client/public/favicon.svg +35 -0
- package/client/public/pwa-192x192.png +0 -0
- package/client/public/pwa-512x512.png +0 -0
- package/client/src/App.jsx +3131 -0
- package/client/src/assets/logo_small.png +0 -0
- package/client/src/assets/vibe80_dark.svg +51 -0
- package/client/src/assets/vibe80_light.svg +50 -0
- package/client/src/components/Chat/ChatComposer.jsx +228 -0
- package/client/src/components/Chat/ChatMessages.jsx +811 -0
- package/client/src/components/Chat/ChatToolbar.jsx +109 -0
- package/client/src/components/Chat/useChatComposer.js +462 -0
- package/client/src/components/Diff/DiffPanel.jsx +129 -0
- package/client/src/components/Explorer/ExplorerPanel.jsx +449 -0
- package/client/src/components/Logs/LogsPanel.jsx +80 -0
- package/client/src/components/SessionGate/SessionGate.jsx +874 -0
- package/client/src/components/Settings/SettingsPanel.jsx +212 -0
- package/client/src/components/Terminal/TerminalPanel.jsx +39 -0
- package/client/src/components/Topbar/Topbar.jsx +101 -0
- package/client/src/components/WorktreeTabs.css +419 -0
- package/client/src/components/WorktreeTabs.jsx +604 -0
- package/client/src/hooks/useAttachments.jsx +125 -0
- package/client/src/hooks/useBacklog.js +254 -0
- package/client/src/hooks/useChatClear.js +90 -0
- package/client/src/hooks/useChatCollapse.js +42 -0
- package/client/src/hooks/useChatCommands.js +294 -0
- package/client/src/hooks/useChatExport.js +144 -0
- package/client/src/hooks/useChatMessagesState.js +69 -0
- package/client/src/hooks/useChatSend.js +158 -0
- package/client/src/hooks/useChatSocket.js +1239 -0
- package/client/src/hooks/useDiffNavigation.js +19 -0
- package/client/src/hooks/useExplorerActions.js +1184 -0
- package/client/src/hooks/useGitIdentity.js +114 -0
- package/client/src/hooks/useLayoutMode.js +31 -0
- package/client/src/hooks/useLocalPreferences.js +131 -0
- package/client/src/hooks/useMessageSync.js +30 -0
- package/client/src/hooks/useNotifications.js +132 -0
- package/client/src/hooks/usePaneNavigation.js +67 -0
- package/client/src/hooks/usePanelState.js +13 -0
- package/client/src/hooks/useProviderSelection.js +70 -0
- package/client/src/hooks/useRepoBranchesModels.js +218 -0
- package/client/src/hooks/useRepoStatus.js +350 -0
- package/client/src/hooks/useRpcLogActions.js +19 -0
- package/client/src/hooks/useRpcLogView.js +58 -0
- package/client/src/hooks/useSessionHandoff.js +97 -0
- package/client/src/hooks/useSessionLifecycle.js +287 -0
- package/client/src/hooks/useSessionReset.js +63 -0
- package/client/src/hooks/useSessionResync.js +77 -0
- package/client/src/hooks/useTerminalSession.js +328 -0
- package/client/src/hooks/useToolbarExport.js +27 -0
- package/client/src/hooks/useTurnInterrupt.js +43 -0
- package/client/src/hooks/useVibe80Forms.js +128 -0
- package/client/src/hooks/useWorkspaceAuth.js +932 -0
- package/client/src/hooks/useWorktreeCloseConfirm.js +46 -0
- package/client/src/hooks/useWorktrees.js +396 -0
- package/client/src/i18n.jsx +87 -0
- package/client/src/index.css +5147 -0
- package/client/src/locales/en.json +37 -0
- package/client/src/locales/fr.json +321 -0
- package/client/src/main.jsx +16 -0
- package/client/vite.config.js +62 -0
- package/docs/api/asyncapi.json +1511 -0
- package/docs/api/openapi.json +3242 -0
- package/git_hooks/prepare-commit-msg +35 -0
- package/package.json +36 -0
- package/server/package.json +29 -0
- package/server/scripts/rotate-workspace-secret.js +101 -0
- package/server/src/claudeClient.js +454 -0
- package/server/src/clientEvents.js +594 -0
- package/server/src/clientFactory.js +164 -0
- package/server/src/codexClient.js +468 -0
- package/server/src/config.js +27 -0
- package/server/src/helpers.js +138 -0
- package/server/src/index.js +1641 -0
- package/server/src/middleware/auth.js +93 -0
- package/server/src/middleware/debug.js +89 -0
- package/server/src/middleware/errorTypes.js +60 -0
- package/server/src/providerLogger.js +60 -0
- package/server/src/routes/files.js +114 -0
- package/server/src/routes/git.js +183 -0
- package/server/src/routes/health.js +13 -0
- package/server/src/routes/sessions.js +407 -0
- package/server/src/routes/workspaces.js +296 -0
- package/server/src/routes/worktrees.js +993 -0
- package/server/src/runAs.js +458 -0
- package/server/src/runtimeStore.js +32 -0
- package/server/src/services/auth.js +157 -0
- package/server/src/services/claudeThreadDirectory.js +33 -0
- package/server/src/services/session.js +918 -0
- package/server/src/services/workspace.js +858 -0
- package/server/src/storage/index.js +17 -0
- package/server/src/storage/redis.js +412 -0
- package/server/src/storage/sqlite.js +649 -0
- package/server/src/worktreeManager.js +717 -0
- package/server/tests/README.md +13 -0
- package/server/tests/factories/workspaceFactory.js +13 -0
- package/server/tests/fixtures/workspaceCredentials.json +4 -0
- package/server/tests/integration/routes/workspaces-routes.test.js +626 -0
- package/server/tests/setup/env.js +9 -0
- package/server/tests/unit/helpers.test.js +95 -0
- package/server/tests/unit/services/auth.test.js +181 -0
- package/server/tests/unit/services/workspace.test.js +115 -0
- 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
|
+
};
|