labgate 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -0
- package/bin/labgate.js +3 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +97 -0
- package/dist/cli.js.map +1 -0
- package/dist/lib/audit.d.ts +8 -0
- package/dist/lib/audit.js +25 -0
- package/dist/lib/audit.js.map +1 -0
- package/dist/lib/config.d.ts +47 -0
- package/dist/lib/config.js +128 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/container.d.ts +19 -0
- package/dist/lib/container.js +794 -0
- package/dist/lib/container.js.map +1 -0
- package/dist/lib/init.d.ts +3 -0
- package/dist/lib/init.js +81 -0
- package/dist/lib/init.js.map +1 -0
- package/dist/lib/runtime.d.ts +23 -0
- package/dist/lib/runtime.js +189 -0
- package/dist/lib/runtime.js.map +1 -0
- package/package.json +33 -0
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.imageToSifName = imageToSifName;
|
|
7
|
+
exports.buildEntrypoint = buildEntrypoint;
|
|
8
|
+
exports.startSession = startSession;
|
|
9
|
+
exports.listSessions = listSessions;
|
|
10
|
+
exports.stopSession = stopSession;
|
|
11
|
+
const child_process_1 = require("child_process");
|
|
12
|
+
const fs_1 = require("fs");
|
|
13
|
+
const path_1 = require("path");
|
|
14
|
+
const os_1 = require("os");
|
|
15
|
+
const child_process_2 = require("child_process");
|
|
16
|
+
const crypto_1 = require("crypto");
|
|
17
|
+
const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
18
|
+
const config_js_1 = require("./config.js");
|
|
19
|
+
const runtime_js_1 = require("./runtime.js");
|
|
20
|
+
const audit_js_1 = require("./audit.js");
|
|
21
|
+
// ── SIF image management (Apptainer/Singularity) ─────────
|
|
22
|
+
/**
|
|
23
|
+
* Convert a container image URI to a local SIF filename.
|
|
24
|
+
* e.g. "docker.io/library/ubuntu:22.04" → "docker.io_library_ubuntu_22.04.sif"
|
|
25
|
+
*/
|
|
26
|
+
function imageToSifName(image) {
|
|
27
|
+
return image.replace(/[/:]/g, '_') + '.sif';
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Ensure a SIF image exists in the cache directory.
|
|
31
|
+
* Pulls from docker:// URI if not already cached.
|
|
32
|
+
*/
|
|
33
|
+
function ensureSifImage(runtime, image) {
|
|
34
|
+
const imagesDir = (0, config_js_1.getImagesDir)();
|
|
35
|
+
(0, fs_1.mkdirSync)(imagesDir, { recursive: true });
|
|
36
|
+
const sifPath = (0, path_1.join)(imagesDir, imageToSifName(image));
|
|
37
|
+
if ((0, fs_1.existsSync)(sifPath)) {
|
|
38
|
+
return sifPath;
|
|
39
|
+
}
|
|
40
|
+
console.log(`[labgate] Pulling image to SIF (first run): ${image}`);
|
|
41
|
+
(0, child_process_1.execFileSync)(runtime, ['pull', sifPath, `docker://${image}`], {
|
|
42
|
+
stdio: 'inherit',
|
|
43
|
+
});
|
|
44
|
+
return sifPath;
|
|
45
|
+
}
|
|
46
|
+
// ── OCI image management (Podman/Docker) ─────────────────
|
|
47
|
+
function ensureOciImage(runtime, image) {
|
|
48
|
+
try {
|
|
49
|
+
if (runtime === 'docker') {
|
|
50
|
+
(0, child_process_1.execFileSync)(runtime, ['image', 'inspect', image], { stdio: 'ignore' });
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
(0, child_process_1.execFileSync)(runtime, ['image', 'exists', image], { stdio: 'ignore' });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
console.log(`[labgate] Pulling image (first run): ${image}`);
|
|
58
|
+
(0, child_process_1.execFileSync)(runtime, ['pull', image], { stdio: 'inherit' });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function ensureEmptyDir() {
|
|
62
|
+
const emptyDir = (0, config_js_1.getEmptyDir)();
|
|
63
|
+
(0, fs_1.mkdirSync)(emptyDir, { recursive: true });
|
|
64
|
+
return emptyDir;
|
|
65
|
+
}
|
|
66
|
+
function resolveBlockedMounts(patterns, mounts) {
|
|
67
|
+
const blocked = [];
|
|
68
|
+
const seen = new Set();
|
|
69
|
+
for (const mount of mounts) {
|
|
70
|
+
if (!(0, fs_1.existsSync)(mount.host))
|
|
71
|
+
continue;
|
|
72
|
+
const matches = fast_glob_1.default.sync(patterns, {
|
|
73
|
+
cwd: mount.host,
|
|
74
|
+
dot: true,
|
|
75
|
+
onlyFiles: false,
|
|
76
|
+
followSymbolicLinks: false,
|
|
77
|
+
unique: true,
|
|
78
|
+
suppressErrors: true,
|
|
79
|
+
absolute: true,
|
|
80
|
+
});
|
|
81
|
+
for (const abs of matches) {
|
|
82
|
+
if (!(0, fs_1.existsSync)(abs))
|
|
83
|
+
continue;
|
|
84
|
+
const rel = (0, path_1.relative)(mount.host, abs);
|
|
85
|
+
if (!rel || rel.startsWith('..'))
|
|
86
|
+
continue;
|
|
87
|
+
const containerPath = (0, path_1.join)(mount.container, rel);
|
|
88
|
+
if (seen.has(containerPath))
|
|
89
|
+
continue;
|
|
90
|
+
try {
|
|
91
|
+
const stat = (0, fs_1.statSync)(abs);
|
|
92
|
+
blocked.push({
|
|
93
|
+
hostPath: abs,
|
|
94
|
+
containerPath,
|
|
95
|
+
kind: stat.isDirectory() ? 'dir' : 'file',
|
|
96
|
+
});
|
|
97
|
+
seen.add(containerPath);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Skip paths that disappeared between glob and stat
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return blocked;
|
|
105
|
+
}
|
|
106
|
+
function getMountRoots(session) {
|
|
107
|
+
const { workdir, config } = session;
|
|
108
|
+
const sandboxHome = (0, config_js_1.getSandboxHome)();
|
|
109
|
+
return [
|
|
110
|
+
{ host: sandboxHome, container: '/home/sandbox' },
|
|
111
|
+
{ host: workdir, container: '/work' },
|
|
112
|
+
...config.filesystem.extra_paths.map(({ path: p }) => {
|
|
113
|
+
const resolved = p.replace(/^~/, (0, os_1.homedir)());
|
|
114
|
+
const target = `/mnt/${(0, path_1.basename)(resolved)}`;
|
|
115
|
+
return { host: resolved, container: target };
|
|
116
|
+
}),
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
// ── Dry-run runtime fallback ──────────────────────────────
|
|
120
|
+
function getDryRunRuntime(preferred) {
|
|
121
|
+
const check = (0, runtime_js_1.checkRuntime)(preferred);
|
|
122
|
+
if (check.ok)
|
|
123
|
+
return check.runtime;
|
|
124
|
+
// No runtime installed — use the preference for preview, or default to apptainer
|
|
125
|
+
if (preferred && preferred !== 'auto')
|
|
126
|
+
return preferred;
|
|
127
|
+
return 'apptainer';
|
|
128
|
+
}
|
|
129
|
+
// ── Network + policy helpers ──────────────────────────────
|
|
130
|
+
function resolveCommandBlacklist(config) {
|
|
131
|
+
const cmds = config.commands.blacklist.map(c => c.trim()).filter(Boolean);
|
|
132
|
+
return cmds.length ? cmds.join(':') : null;
|
|
133
|
+
}
|
|
134
|
+
function buildFilteredProxyEnv(config) {
|
|
135
|
+
const proxy = process.env.LABGATE_PROXY ??
|
|
136
|
+
process.env.HTTP_PROXY ??
|
|
137
|
+
process.env.HTTPS_PROXY ??
|
|
138
|
+
process.env.http_proxy ??
|
|
139
|
+
process.env.https_proxy;
|
|
140
|
+
const httpProxy = process.env.LABGATE_HTTP_PROXY ??
|
|
141
|
+
process.env.HTTP_PROXY ??
|
|
142
|
+
process.env.http_proxy ??
|
|
143
|
+
proxy ??
|
|
144
|
+
'';
|
|
145
|
+
const httpsProxy = process.env.LABGATE_HTTPS_PROXY ??
|
|
146
|
+
process.env.HTTPS_PROXY ??
|
|
147
|
+
process.env.https_proxy ??
|
|
148
|
+
proxy ??
|
|
149
|
+
'';
|
|
150
|
+
if (!httpProxy && !httpsProxy) {
|
|
151
|
+
throw new Error('[labgate] network.mode=filtered requires a proxy. ' +
|
|
152
|
+
'Set LABGATE_PROXY, LABGATE_HTTP_PROXY/LABGATE_HTTPS_PROXY, or HTTP_PROXY/HTTPS_PROXY.');
|
|
153
|
+
}
|
|
154
|
+
const env = [];
|
|
155
|
+
if (httpProxy) {
|
|
156
|
+
env.push('--env', `HTTP_PROXY=${httpProxy}`, '--env', `http_proxy=${httpProxy}`);
|
|
157
|
+
}
|
|
158
|
+
if (httpsProxy) {
|
|
159
|
+
env.push('--env', `HTTPS_PROXY=${httpsProxy}`, '--env', `https_proxy=${httpsProxy}`);
|
|
160
|
+
}
|
|
161
|
+
env.push('--env', 'NO_PROXY=localhost,127.0.0.1', '--env', 'no_proxy=localhost,127.0.0.1');
|
|
162
|
+
if (config.network.allowed_domains.length > 0) {
|
|
163
|
+
env.push('--env', `LABGATE_ALLOWED_DOMAINS=${config.network.allowed_domains.join(',')}`);
|
|
164
|
+
}
|
|
165
|
+
return env;
|
|
166
|
+
}
|
|
167
|
+
function buildNetworkArgs(config, runtime) {
|
|
168
|
+
if (config.network.mode === 'none')
|
|
169
|
+
return ['--network=none'];
|
|
170
|
+
if (config.network.mode === 'host')
|
|
171
|
+
return ['--network=host'];
|
|
172
|
+
// filtered
|
|
173
|
+
return runtime === 'podman' ? ['--network=slirp4netns'] : ['--network=bridge'];
|
|
174
|
+
}
|
|
175
|
+
// ── Build Podman/Docker arguments ─────────────────────────
|
|
176
|
+
function buildPodmanArgs(session, runtime, sessionId, tokenEnv = []) {
|
|
177
|
+
const { agent, workdir, config, imageOverride } = session;
|
|
178
|
+
const image = imageOverride ?? config.image;
|
|
179
|
+
const sandboxHome = (0, config_js_1.getSandboxHome)();
|
|
180
|
+
(0, fs_1.mkdirSync)(sandboxHome, { recursive: true });
|
|
181
|
+
const blockedMounts = resolveBlockedMounts(config.filesystem.blocked_patterns, getMountRoots(session));
|
|
182
|
+
const emptyDir = ensureEmptyDir();
|
|
183
|
+
const commandBlacklist = resolveCommandBlacklist(config);
|
|
184
|
+
const proxyEnv = config.network.mode === 'filtered' && !session.dryRun ? buildFilteredProxyEnv(config) : [];
|
|
185
|
+
const args = [
|
|
186
|
+
'run',
|
|
187
|
+
'--rm',
|
|
188
|
+
'--interactive',
|
|
189
|
+
'--tty',
|
|
190
|
+
'--name', `labgate-${sessionId}`,
|
|
191
|
+
// ── Isolation ──
|
|
192
|
+
'--cap-drop=ALL',
|
|
193
|
+
'--security-opt=no-new-privileges',
|
|
194
|
+
'--pids-limit=512',
|
|
195
|
+
// ── Network ──
|
|
196
|
+
...buildNetworkArgs(config, runtime),
|
|
197
|
+
// ── Persistent sandbox HOME ──
|
|
198
|
+
'--volume', `${sandboxHome}:/home/sandbox:rw`,
|
|
199
|
+
// ── Working directory ──
|
|
200
|
+
'--volume', `${workdir}:/work:rw`,
|
|
201
|
+
'--workdir', '/work',
|
|
202
|
+
// ── Extra mounts from config ──
|
|
203
|
+
...config.filesystem.extra_paths.map(({ path: p, mode }) => {
|
|
204
|
+
const resolved = p.replace(/^~/, (0, os_1.homedir)());
|
|
205
|
+
const target = `/mnt/${(0, path_1.basename)(resolved)}`;
|
|
206
|
+
return `--volume=${resolved}:${target}:${mode}`;
|
|
207
|
+
}),
|
|
208
|
+
// ── Block sensitive paths ──
|
|
209
|
+
...blockedMounts.flatMap(({ containerPath, kind }) => {
|
|
210
|
+
const source = kind === 'dir' ? emptyDir : '/dev/null';
|
|
211
|
+
return ['--volume', `${source}:${containerPath}:ro`];
|
|
212
|
+
}),
|
|
213
|
+
// ── Environment ──
|
|
214
|
+
'--env', `LABGATE_AGENT=${agent}`,
|
|
215
|
+
'--env', `LABGATE_SESSION=${sessionId}`,
|
|
216
|
+
'--env', `LABGATE_NETWORK_MODE=${config.network.mode}`,
|
|
217
|
+
'--env', 'HOME=/home/sandbox',
|
|
218
|
+
...(commandBlacklist ? ['--env', `LABGATE_CMD_BLACKLIST=${commandBlacklist}`] : []),
|
|
219
|
+
...tokenEnv,
|
|
220
|
+
...proxyEnv,
|
|
221
|
+
// ── Image ──
|
|
222
|
+
image,
|
|
223
|
+
// ── Entrypoint ──
|
|
224
|
+
'bash', '-lc', buildEntrypoint(agent, session.statusline ?? true),
|
|
225
|
+
];
|
|
226
|
+
return args;
|
|
227
|
+
}
|
|
228
|
+
// ── Build Apptainer/Singularity arguments ─────────────────
|
|
229
|
+
function buildApptainerArgs(session, runtime, sifPath, sessionId, tokenEnv = []) {
|
|
230
|
+
const { agent, workdir, config } = session;
|
|
231
|
+
const sandboxHome = (0, config_js_1.getSandboxHome)();
|
|
232
|
+
(0, fs_1.mkdirSync)(sandboxHome, { recursive: true });
|
|
233
|
+
const blockedMounts = resolveBlockedMounts(config.filesystem.blocked_patterns, getMountRoots(session));
|
|
234
|
+
const emptyDir = ensureEmptyDir();
|
|
235
|
+
const commandBlacklist = resolveCommandBlacklist(config);
|
|
236
|
+
const proxyEnv = config.network.mode === 'filtered' && !session.dryRun ? buildFilteredProxyEnv(config) : [];
|
|
237
|
+
const args = [
|
|
238
|
+
'exec',
|
|
239
|
+
'--containall',
|
|
240
|
+
'--cleanenv',
|
|
241
|
+
// ── Persistent sandbox HOME ──
|
|
242
|
+
'--home', `${sandboxHome}:/home/sandbox`,
|
|
243
|
+
// ── Working directory ──
|
|
244
|
+
'--bind', `${workdir}:/work`,
|
|
245
|
+
'--pwd', '/work',
|
|
246
|
+
// ── Extra mounts from config ──
|
|
247
|
+
...config.filesystem.extra_paths.flatMap(({ path: p, mode }) => {
|
|
248
|
+
const resolved = p.replace(/^~/, (0, os_1.homedir)());
|
|
249
|
+
const target = `/mnt/${(0, path_1.basename)(resolved)}`;
|
|
250
|
+
const bindSpec = mode === 'ro' ? `${resolved}:${target}:ro` : `${resolved}:${target}`;
|
|
251
|
+
return ['--bind', bindSpec];
|
|
252
|
+
}),
|
|
253
|
+
// ── Block sensitive paths ──
|
|
254
|
+
...blockedMounts.flatMap(({ containerPath, kind }) => {
|
|
255
|
+
const source = kind === 'dir' ? emptyDir : '/dev/null';
|
|
256
|
+
return ['--bind', `${source}:${containerPath}`];
|
|
257
|
+
}),
|
|
258
|
+
// ── Environment ──
|
|
259
|
+
'--env', `LABGATE_AGENT=${agent}`,
|
|
260
|
+
'--env', `LABGATE_SESSION=${sessionId}`,
|
|
261
|
+
'--env', `LABGATE_NETWORK_MODE=${config.network.mode}`,
|
|
262
|
+
'--env', `HOME=/home/sandbox`,
|
|
263
|
+
...(commandBlacklist ? ['--env', `LABGATE_CMD_BLACKLIST=${commandBlacklist}`] : []),
|
|
264
|
+
...tokenEnv,
|
|
265
|
+
...proxyEnv,
|
|
266
|
+
// ── SIF image ──
|
|
267
|
+
sifPath,
|
|
268
|
+
// ── Entrypoint ──
|
|
269
|
+
'bash', '-lc', buildEntrypoint(agent, session.statusline ?? true),
|
|
270
|
+
];
|
|
271
|
+
return args;
|
|
272
|
+
}
|
|
273
|
+
// ── Entrypoint script (inline, same logic for both backends) ──
|
|
274
|
+
function buildEntrypoint(agent, statuslineEnabled = true) {
|
|
275
|
+
const agentSetup = {
|
|
276
|
+
claude: {
|
|
277
|
+
pkg: '@anthropic-ai/claude-code',
|
|
278
|
+
installer: 'npm i -g @anthropic-ai/claude-code 2>&1 | tail -1',
|
|
279
|
+
bin: 'claude',
|
|
280
|
+
},
|
|
281
|
+
codex: {
|
|
282
|
+
pkg: '@openai/codex',
|
|
283
|
+
installer: 'npm i -g @openai/codex 2>&1 | tail -1',
|
|
284
|
+
bin: 'codex',
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
const setup = agentSetup[agent];
|
|
288
|
+
if (!setup) {
|
|
289
|
+
throw new Error(`Unknown agent: ${agent}`);
|
|
290
|
+
}
|
|
291
|
+
const lines = [
|
|
292
|
+
'set -euo pipefail',
|
|
293
|
+
'export HOME=/home/sandbox',
|
|
294
|
+
'export PATH="$HOME/.npm-global/bin:$PATH"',
|
|
295
|
+
'npm config set prefix "$HOME/.npm-global" 2>/dev/null || true',
|
|
296
|
+
'if [ -n "${LABGATE_CMD_BLACKLIST:-}" ]; then',
|
|
297
|
+
' mkdir -p "$HOME/.labgate-bin"',
|
|
298
|
+
' IFS=":" read -r -a _labgate_cmds <<< "$LABGATE_CMD_BLACKLIST"',
|
|
299
|
+
' for _cmd in "${_labgate_cmds[@]}"; do',
|
|
300
|
+
' [ -z "$_cmd" ] && continue',
|
|
301
|
+
' printf \'#!/usr/bin/env bash\\necho "[labgate] Command blocked: %s" >&2\\nexit 126\\n\' "$_cmd" > "$HOME/.labgate-bin/$_cmd"',
|
|
302
|
+
' chmod +x "$HOME/.labgate-bin/$_cmd"',
|
|
303
|
+
' done',
|
|
304
|
+
' export PATH="$HOME/.labgate-bin:$PATH"',
|
|
305
|
+
'fi',
|
|
306
|
+
'',
|
|
307
|
+
];
|
|
308
|
+
if (agent === 'claude' && statuslineEnabled) {
|
|
309
|
+
lines.push('mkdir -p "$HOME/.claude"', 'STATUSLINE_PATH="$HOME/.claude/statusline.js"', 'SETTINGS_PATH="$HOME/.claude/settings.json"', 'if [ ! -f "$STATUSLINE_PATH" ] || grep -q "LABGATE_STATUSLINE" "$STATUSLINE_PATH" || grep -q "statusline.js" "$SETTINGS_PATH" 2>/dev/null; then', ' cat > "$STATUSLINE_PATH" <<\'LABGATE_STATUSLINE\'', '#!/usr/bin/env node', '// LABGATE_STATUSLINE', 'const fs = require("fs");', 'const path = require("path");', 'let input = "";', 'try { input = fs.readFileSync(0, "utf8"); } catch {}', 'let data = {};', 'try { data = JSON.parse(input || "{}"); } catch {}', 'const model = (data.model && data.model.display_name) || "unknown";', 'const currentDir = (data.workspace && data.workspace.current_dir) || "";', 'const dirBase = currentDir ? path.basename(currentDir) : "";', 'let branch = "";', 'try {', ' const headPath = path.join(currentDir || process.cwd(), ".git", "HEAD");', ' const head = fs.readFileSync(headPath, "utf8").trim();', ' if (head.startsWith("ref: ")) {', ' branch = head.replace("ref: refs/heads/", "");', ' }', '} catch {}', 'let percentUsed = null;', 'const cw = data.context_window || {};', 'const usage = cw.current_usage || null;', 'if (usage && typeof cw.context_window_size === "number" && cw.context_window_size > 0) {', ' const currentTokens = (usage.input_tokens || 0) + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);', ' percentUsed = Math.round((currentTokens * 100) / cw.context_window_size);', '}', 'const parts = ["Gated by LabGate"];', 'const agent = process.env.LABGATE_AGENT || "";', 'if (agent) parts.push(agent);', 'const session = process.env.LABGATE_SESSION || "";', 'if (session) parts.push(`id=${session.slice(0, 8)}`);', 'if (process.env.LABGATE_NETWORK_MODE) parts.push(`net=${process.env.LABGATE_NETWORK_MODE}`);', 'parts.push(`model=${model}`);', 'if (dirBase) parts.push(`dir=${dirBase}`);', 'if (branch) parts.push(`git=${branch}`);', 'if (typeof percentUsed === "number") parts.push(`ctx=${percentUsed}%`);', 'process.stdout.write(parts.join(" | "));', 'LABGATE_STATUSLINE', ' chmod +x "$STATUSLINE_PATH"', 'fi', 'node - <<\'LABGATE_STATUSLINE_SETTINGS\'', 'const fs = require("fs");', 'const path = require("path");', 'const home = process.env.HOME || "/home/sandbox";', 'const settingsPath = path.join(home, ".claude", "settings.json");', 'let settings = {};', 'try { settings = JSON.parse(fs.readFileSync(settingsPath, "utf8")); } catch {}', 'const desired = { type: "command", command: path.join(home, ".claude", "statusline.js"), padding: 0 };', 'const existingCmd = settings.statusLine && settings.statusLine.command;', 'if (!settings.statusLine || (typeof existingCmd === "string" && existingCmd.includes("statusline.js"))) {', ' settings.statusLine = desired;', ' fs.mkdirSync(path.dirname(settingsPath), { recursive: true });', ' fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));', '}', 'LABGATE_STATUSLINE_SETTINGS', '');
|
|
310
|
+
}
|
|
311
|
+
lines.push(`if ! command -v ${setup.bin} >/dev/null 2>&1; then`, ` echo "[labgate] Installing ${setup.pkg}..."`, ` ${setup.installer}`, 'fi', `echo "[labgate] Starting ${setup.bin} in /work"`, `exec ${setup.bin}`);
|
|
312
|
+
return lines.join('\n');
|
|
313
|
+
}
|
|
314
|
+
// ── Browser-open hook for OAuth (via sandbox home) ────────
|
|
315
|
+
/**
|
|
316
|
+
* Detect whether we can open a browser on this machine.
|
|
317
|
+
* Returns false for SSH sessions, headless servers, etc.
|
|
318
|
+
*/
|
|
319
|
+
function canOpenBrowser() {
|
|
320
|
+
if (process.env.SSH_CONNECTION || process.env.SSH_TTY)
|
|
321
|
+
return false;
|
|
322
|
+
if ((0, os_1.platform)() === 'darwin')
|
|
323
|
+
return true;
|
|
324
|
+
if (process.env.DISPLAY || process.env.WAYLAND_DISPLAY)
|
|
325
|
+
return true;
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Sets up OAuth browser handling. Three scenarios:
|
|
330
|
+
*
|
|
331
|
+
* 1. Local macOS + podman: Set BROWSER hook, forward callback port via
|
|
332
|
+
* `podman machine ssh`, open URL with `open`.
|
|
333
|
+
* 2. Local Linux with display: Set BROWSER hook, open with xdg-open.
|
|
334
|
+
* No port forwarding needed (podman runs natively).
|
|
335
|
+
* 3. SSH / headless: Do NOT set BROWSER — Claude Code uses the code-paste
|
|
336
|
+
* flow (redirect to platform.claude.com). Pass COLUMNS=1000 so the URL
|
|
337
|
+
* doesn't get line-wrapped in the terminal.
|
|
338
|
+
*/
|
|
339
|
+
function setupBrowserHook() {
|
|
340
|
+
if (!canOpenBrowser()) {
|
|
341
|
+
// SSH or headless: code-paste flow, wide columns to avoid URL wrapping
|
|
342
|
+
console.log('[labgate] No local browser detected (SSH/headless). OAuth will use code-paste flow.');
|
|
343
|
+
return { env: ['--env', 'COLUMNS=1000'], cleanup: () => { } };
|
|
344
|
+
}
|
|
345
|
+
const sandboxHome = (0, config_js_1.getSandboxHome)();
|
|
346
|
+
const labgateDir = (0, path_1.join)(sandboxHome, '.labgate');
|
|
347
|
+
(0, fs_1.mkdirSync)(labgateDir, { recursive: true });
|
|
348
|
+
// Write the browser-open script (runs inside the container)
|
|
349
|
+
const scriptPath = (0, path_1.join)(labgateDir, 'browser-open.sh');
|
|
350
|
+
(0, fs_1.writeFileSync)(scriptPath, '#!/bin/sh\necho "$1" > /home/sandbox/.labgate/browser-url\n', { mode: 0o755 });
|
|
351
|
+
// Remove stale URL file
|
|
352
|
+
const urlFilePath = (0, path_1.join)(labgateDir, 'browser-url');
|
|
353
|
+
try {
|
|
354
|
+
(0, fs_1.unlinkSync)(urlFilePath);
|
|
355
|
+
}
|
|
356
|
+
catch { /* doesn't exist */ }
|
|
357
|
+
let handled = false;
|
|
358
|
+
// Watch for the URL file on the host side
|
|
359
|
+
const watcher = (0, fs_1.watch)(labgateDir, (_eventType, filename) => {
|
|
360
|
+
if (handled || filename !== 'browser-url')
|
|
361
|
+
return;
|
|
362
|
+
handled = true;
|
|
363
|
+
try {
|
|
364
|
+
const { readFileSync } = require('fs');
|
|
365
|
+
const url = readFileSync(urlFilePath, 'utf-8').trim();
|
|
366
|
+
if (!url)
|
|
367
|
+
return;
|
|
368
|
+
// Forward callback port from host → container VM (macOS podman only)
|
|
369
|
+
if ((0, os_1.platform)() === 'darwin') {
|
|
370
|
+
const portMatch = url.match(/localhost%3A(\d+)/);
|
|
371
|
+
if (portMatch) {
|
|
372
|
+
const port = parseInt(portMatch[1], 10);
|
|
373
|
+
if (port > 0 && port < 65536) {
|
|
374
|
+
try {
|
|
375
|
+
(0, child_process_2.execSync)(`podman machine ssh -- -f -N -L ${port}:localhost:${port}`, {
|
|
376
|
+
timeout: 5000,
|
|
377
|
+
stdio: 'ignore',
|
|
378
|
+
});
|
|
379
|
+
console.log(`[labgate] Forwarding callback port ${port} from host to container`);
|
|
380
|
+
}
|
|
381
|
+
catch { /* best effort — might not be podman, or already forwarded */ }
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
try {
|
|
385
|
+
(0, child_process_2.execSync)(`printf '%s' ${JSON.stringify(url)} | pbcopy`);
|
|
386
|
+
}
|
|
387
|
+
catch { /* best effort */ }
|
|
388
|
+
try {
|
|
389
|
+
(0, child_process_2.execSync)(`open ${JSON.stringify(url)}`);
|
|
390
|
+
}
|
|
391
|
+
catch { /* best effort */ }
|
|
392
|
+
console.log('\n[labgate] Login URL opened in browser and copied to clipboard.');
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
// Linux with display
|
|
396
|
+
try {
|
|
397
|
+
(0, child_process_2.execSync)(`xdg-open ${JSON.stringify(url)}`);
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
console.log(`\n[labgate] Login URL:\n${url}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
catch { /* best effort */ }
|
|
405
|
+
});
|
|
406
|
+
const cleanup = () => {
|
|
407
|
+
watcher.close();
|
|
408
|
+
try {
|
|
409
|
+
(0, fs_1.unlinkSync)(urlFilePath);
|
|
410
|
+
}
|
|
411
|
+
catch { /* best effort */ }
|
|
412
|
+
};
|
|
413
|
+
return {
|
|
414
|
+
env: ['--env', 'BROWSER=/home/sandbox/.labgate/browser-open.sh'],
|
|
415
|
+
cleanup,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
// ── Pretty-print a command for dry-run ────────────────────
|
|
419
|
+
function prettyPrintCommand(runtime, args) {
|
|
420
|
+
const parts = [];
|
|
421
|
+
for (let i = 0; i < args.length; i++) {
|
|
422
|
+
const a = args[i];
|
|
423
|
+
if (a.startsWith('--') && !a.includes('=') && i + 1 < args.length && !args[i + 1].startsWith('--')) {
|
|
424
|
+
const val = args[i + 1];
|
|
425
|
+
const quotedVal = val.includes(' ') || val.includes('\n') ? `'${val}'` : val;
|
|
426
|
+
parts.push(`${a} ${quotedVal}`);
|
|
427
|
+
i++;
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
parts.push(a.includes(' ') || a.includes('\n') ? `'${a}'` : a);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
console.log(`${runtime} ${parts.join(' \\\n ')}`);
|
|
434
|
+
}
|
|
435
|
+
async function loadPty() {
|
|
436
|
+
try {
|
|
437
|
+
const mod = await import('node-pty');
|
|
438
|
+
return mod?.default ?? mod;
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function renderStickyFooter(line) {
|
|
445
|
+
if (!process.stdout.isTTY)
|
|
446
|
+
return;
|
|
447
|
+
const cols = process.stdout.columns || 80;
|
|
448
|
+
const rows = process.stdout.rows || 24;
|
|
449
|
+
const trimmed = line.length > cols ? line.slice(0, Math.max(0, cols - 1)) : line;
|
|
450
|
+
const padded = trimmed.padEnd(cols, ' ');
|
|
451
|
+
process.stdout.write(`\x1b7\x1b[${rows};1H\x1b[2K${padded}\x1b8`);
|
|
452
|
+
}
|
|
453
|
+
// ── Agent credential extraction ───────────────────────────
|
|
454
|
+
/**
|
|
455
|
+
* On macOS, Claude Code stores OAuth tokens in the system keychain.
|
|
456
|
+
* We sync the full credential JSON into the sandbox home so Claude Code
|
|
457
|
+
* finds valid OAuth credentials at ~/.claude/.credentials.json inside
|
|
458
|
+
* the container. We also pass the access token as ANTHROPIC_API_KEY
|
|
459
|
+
* as a fallback.
|
|
460
|
+
*/
|
|
461
|
+
function getAgentTokenEnv(agent) {
|
|
462
|
+
if ((0, os_1.platform)() !== 'darwin')
|
|
463
|
+
return [];
|
|
464
|
+
if (agent !== 'claude')
|
|
465
|
+
return [];
|
|
466
|
+
try {
|
|
467
|
+
const raw = (0, child_process_1.execFileSync)('security', [
|
|
468
|
+
'find-generic-password',
|
|
469
|
+
'-s', 'Claude Code-credentials',
|
|
470
|
+
'-w',
|
|
471
|
+
], { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
472
|
+
const creds = JSON.parse(raw);
|
|
473
|
+
const token = creds.claudeAiOauth?.accessToken;
|
|
474
|
+
if (!token)
|
|
475
|
+
return [];
|
|
476
|
+
// Sync the full credential file into the sandbox home so
|
|
477
|
+
// Claude Code picks it up natively (no OAuth flow needed)
|
|
478
|
+
const sandboxHome = (0, config_js_1.getSandboxHome)();
|
|
479
|
+
const claudeDir = (0, path_1.join)(sandboxHome, '.claude');
|
|
480
|
+
(0, fs_1.mkdirSync)(claudeDir, { recursive: true });
|
|
481
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(claudeDir, '.credentials.json'), raw, { mode: 0o600 });
|
|
482
|
+
console.log('[labgate] Synced Claude credentials from macOS keychain');
|
|
483
|
+
return ['--env', `ANTHROPIC_API_KEY=${token}`];
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
return [];
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
function formatStatusFooter(session, runtime, sessionId, image) {
|
|
490
|
+
const timeout = session.config.session_timeout_hours;
|
|
491
|
+
const timeoutLabel = Number.isFinite(timeout) && timeout > 0 ? `${timeout}h` : 'off';
|
|
492
|
+
const audit = session.config.audit.enabled ? 'on' : 'off';
|
|
493
|
+
const net = session.config.network.mode;
|
|
494
|
+
return `[labgate] Status: session=${sessionId} | runtime=${runtime} | net=${net} | timeout=${timeoutLabel} | audit=${audit} | image=${image}`;
|
|
495
|
+
}
|
|
496
|
+
// ── Session lifecycle ─────────────────────────────────────
|
|
497
|
+
async function startSession(session) {
|
|
498
|
+
const preferred = session.config.runtime;
|
|
499
|
+
const runtime = session.dryRun ? getDryRunRuntime(preferred) : (0, runtime_js_1.getRuntime)(preferred);
|
|
500
|
+
const image = session.imageOverride ?? session.config.image;
|
|
501
|
+
const sessionId = (0, crypto_1.randomBytes)(4).toString('hex');
|
|
502
|
+
const footerMode = session.footerMode ?? 'sticky';
|
|
503
|
+
const footerLine = formatStatusFooter(session, runtime, sessionId, image);
|
|
504
|
+
// Extract agent auth token from host keychain (macOS)
|
|
505
|
+
const tokenEnv = session.dryRun ? [] : getAgentTokenEnv(session.agent);
|
|
506
|
+
// Set up browser hook so OAuth URLs get opened on the host
|
|
507
|
+
const browserHook = session.dryRun ? undefined : setupBrowserHook();
|
|
508
|
+
if ((0, runtime_js_1.isApptainerFamily)(runtime) && session.config.network.mode !== 'host') {
|
|
509
|
+
console.warn(`[labgate] Warning: network mode "${session.config.network.mode}" is not enforced for ${runtime}. ` +
|
|
510
|
+
'Configure network restrictions externally or use podman/docker.');
|
|
511
|
+
}
|
|
512
|
+
let args;
|
|
513
|
+
if ((0, runtime_js_1.isApptainerFamily)(runtime)) {
|
|
514
|
+
// Apptainer/Singularity path
|
|
515
|
+
let sifPath;
|
|
516
|
+
if (session.dryRun) {
|
|
517
|
+
sifPath = (0, path_1.join)((0, config_js_1.getImagesDir)(), imageToSifName(image));
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
sifPath = ensureSifImage(runtime, image);
|
|
521
|
+
}
|
|
522
|
+
args = buildApptainerArgs(session, runtime, sifPath, sessionId, [...tokenEnv, ...(browserHook?.env ?? [])]);
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
// Podman/Docker path
|
|
526
|
+
if (!session.dryRun) {
|
|
527
|
+
ensureOciImage(runtime, image);
|
|
528
|
+
}
|
|
529
|
+
args = buildPodmanArgs(session, runtime, sessionId, [...tokenEnv, ...(browserHook?.env ?? [])]);
|
|
530
|
+
}
|
|
531
|
+
if (session.dryRun) {
|
|
532
|
+
prettyPrintCommand(runtime, args);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
// Log session start
|
|
536
|
+
if (session.config.audit.enabled) {
|
|
537
|
+
const event = {
|
|
538
|
+
timestamp: new Date().toISOString(),
|
|
539
|
+
session: sessionId,
|
|
540
|
+
event: 'session_start',
|
|
541
|
+
agent: session.agent,
|
|
542
|
+
workdir: session.workdir,
|
|
543
|
+
network_mode: session.config.network.mode,
|
|
544
|
+
mounts: [
|
|
545
|
+
{ path: session.workdir, target: '/work', mode: 'rw' },
|
|
546
|
+
...session.config.filesystem.extra_paths.map(p => ({
|
|
547
|
+
path: p.path.replace(/^~/, (0, os_1.homedir)()),
|
|
548
|
+
target: `/mnt/${(0, path_1.basename)(p.path)}`,
|
|
549
|
+
mode: p.mode,
|
|
550
|
+
})),
|
|
551
|
+
],
|
|
552
|
+
};
|
|
553
|
+
(0, audit_js_1.writeAuditEvent)(session.config, event);
|
|
554
|
+
}
|
|
555
|
+
// Print session info
|
|
556
|
+
const mode = session.config.network.mode;
|
|
557
|
+
console.log(`[labgate] Session ${sessionId}`);
|
|
558
|
+
console.log(`[labgate] Runtime: ${runtime}`);
|
|
559
|
+
console.log(`[labgate] Agent: ${session.agent}`);
|
|
560
|
+
console.log(`[labgate] Workdir: ${session.workdir} → /work`);
|
|
561
|
+
console.log(`[labgate] Network: ${mode}`);
|
|
562
|
+
console.log(`[labgate] Blocked: ${session.config.filesystem.blocked_patterns.length} patterns`);
|
|
563
|
+
if (session.config.audit.enabled) {
|
|
564
|
+
console.log(`[labgate] Audit log: ${(0, config_js_1.getLogDir)(session.config)}`);
|
|
565
|
+
}
|
|
566
|
+
if (footerMode === 'once') {
|
|
567
|
+
console.log(footerLine);
|
|
568
|
+
}
|
|
569
|
+
const wantsSticky = footerMode === 'sticky';
|
|
570
|
+
if (wantsSticky) {
|
|
571
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
572
|
+
console.warn('[labgate] Sticky footer needs a TTY; falling back to one-time footer.');
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
const pty = await loadPty();
|
|
576
|
+
if (!pty) {
|
|
577
|
+
console.warn('[labgate] Sticky footer requires node-pty. Install it or use --no-footer.');
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
let runtimePath;
|
|
581
|
+
try {
|
|
582
|
+
runtimePath = (0, child_process_1.execFileSync)('which', [runtime], { encoding: 'utf-8' }).trim();
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
runtimePath = runtime;
|
|
586
|
+
}
|
|
587
|
+
const cleanEnv = {};
|
|
588
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
589
|
+
if (v !== undefined)
|
|
590
|
+
cleanEnv[k] = v;
|
|
591
|
+
}
|
|
592
|
+
const cols = process.stdout.columns || 80;
|
|
593
|
+
const rows = process.stdout.rows || 24;
|
|
594
|
+
const child = pty.spawn(runtimePath, args, {
|
|
595
|
+
name: 'xterm-256color',
|
|
596
|
+
cols,
|
|
597
|
+
rows,
|
|
598
|
+
cwd: process.cwd(),
|
|
599
|
+
env: cleanEnv,
|
|
600
|
+
});
|
|
601
|
+
let exited = false;
|
|
602
|
+
const resizeHandler = () => {
|
|
603
|
+
child.resize(process.stdout.columns || 80, process.stdout.rows || 24);
|
|
604
|
+
renderStickyFooter(footerLine);
|
|
605
|
+
};
|
|
606
|
+
process.stdout.on('resize', resizeHandler);
|
|
607
|
+
renderStickyFooter(footerLine);
|
|
608
|
+
child.onData((data) => {
|
|
609
|
+
process.stdout.write(data);
|
|
610
|
+
renderStickyFooter(footerLine);
|
|
611
|
+
});
|
|
612
|
+
if (process.stdin.isTTY) {
|
|
613
|
+
process.stdin.setRawMode(true);
|
|
614
|
+
}
|
|
615
|
+
process.stdin.resume();
|
|
616
|
+
process.stdin.on('data', (data) => {
|
|
617
|
+
child.write(data.toString());
|
|
618
|
+
});
|
|
619
|
+
// Session timeout
|
|
620
|
+
let timeoutHandle;
|
|
621
|
+
const timeoutHours = session.config.session_timeout_hours;
|
|
622
|
+
if (Number.isFinite(timeoutHours) && timeoutHours > 0) {
|
|
623
|
+
const timeoutMs = timeoutHours * 60 * 60 * 1000;
|
|
624
|
+
timeoutHandle = setTimeout(() => {
|
|
625
|
+
if (exited)
|
|
626
|
+
return;
|
|
627
|
+
console.error(`\n[labgate] Session ${sessionId} reached timeout (${timeoutHours}h). Stopping...`);
|
|
628
|
+
if (session.config.audit.enabled) {
|
|
629
|
+
(0, audit_js_1.writeAuditEvent)(session.config, {
|
|
630
|
+
timestamp: new Date().toISOString(),
|
|
631
|
+
session: sessionId,
|
|
632
|
+
event: 'session_timeout',
|
|
633
|
+
timeout_hours: timeoutHours,
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
if (!(0, runtime_js_1.isApptainerFamily)(runtime)) {
|
|
637
|
+
try {
|
|
638
|
+
(0, child_process_1.execFileSync)(runtime, ['stop', `labgate-${sessionId}`], { stdio: 'ignore' });
|
|
639
|
+
}
|
|
640
|
+
catch { /* best effort */ }
|
|
641
|
+
}
|
|
642
|
+
child.kill('SIGTERM');
|
|
643
|
+
}, timeoutMs);
|
|
644
|
+
timeoutHandle.unref();
|
|
645
|
+
}
|
|
646
|
+
child.onExit((event) => {
|
|
647
|
+
exited = true;
|
|
648
|
+
if (timeoutHandle)
|
|
649
|
+
clearTimeout(timeoutHandle);
|
|
650
|
+
browserHook?.cleanup();
|
|
651
|
+
if (process.stdin.isTTY) {
|
|
652
|
+
process.stdin.setRawMode(false);
|
|
653
|
+
}
|
|
654
|
+
process.stdout.off('resize', resizeHandler);
|
|
655
|
+
if (session.config.audit.enabled) {
|
|
656
|
+
(0, audit_js_1.writeAuditEvent)(session.config, {
|
|
657
|
+
timestamp: new Date().toISOString(),
|
|
658
|
+
session: sessionId,
|
|
659
|
+
event: 'session_end',
|
|
660
|
+
exit_code: event.exitCode ?? 0,
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
process.exit(event.exitCode ?? 0);
|
|
664
|
+
});
|
|
665
|
+
for (const sig of ['SIGINT', 'SIGTERM']) {
|
|
666
|
+
process.on(sig, () => {
|
|
667
|
+
child.kill(sig);
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (footerMode === 'sticky') {
|
|
675
|
+
console.log(footerLine);
|
|
676
|
+
}
|
|
677
|
+
console.log('');
|
|
678
|
+
// Spawn container (inherit stdio so it's fully interactive with TTY)
|
|
679
|
+
const child = (0, child_process_1.spawn)(runtime, args, {
|
|
680
|
+
stdio: 'inherit',
|
|
681
|
+
});
|
|
682
|
+
// Session timeout
|
|
683
|
+
let timeoutHandle;
|
|
684
|
+
const timeoutHours = session.config.session_timeout_hours;
|
|
685
|
+
if (Number.isFinite(timeoutHours) && timeoutHours > 0) {
|
|
686
|
+
const timeoutMs = timeoutHours * 60 * 60 * 1000;
|
|
687
|
+
timeoutHandle = setTimeout(() => {
|
|
688
|
+
if (child.exitCode !== null)
|
|
689
|
+
return;
|
|
690
|
+
console.error(`\n[labgate] Session ${sessionId} reached timeout (${timeoutHours}h). Stopping...`);
|
|
691
|
+
if (session.config.audit.enabled) {
|
|
692
|
+
(0, audit_js_1.writeAuditEvent)(session.config, {
|
|
693
|
+
timestamp: new Date().toISOString(),
|
|
694
|
+
session: sessionId,
|
|
695
|
+
event: 'session_timeout',
|
|
696
|
+
timeout_hours: timeoutHours,
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
if (!(0, runtime_js_1.isApptainerFamily)(runtime)) {
|
|
700
|
+
try {
|
|
701
|
+
(0, child_process_1.execFileSync)(runtime, ['stop', `labgate-${sessionId}`], { stdio: 'ignore' });
|
|
702
|
+
}
|
|
703
|
+
catch { /* best effort */ }
|
|
704
|
+
}
|
|
705
|
+
child.kill('SIGTERM');
|
|
706
|
+
}, timeoutMs);
|
|
707
|
+
timeoutHandle.unref();
|
|
708
|
+
}
|
|
709
|
+
// Handle exit
|
|
710
|
+
child.on('close', (code) => {
|
|
711
|
+
if (timeoutHandle)
|
|
712
|
+
clearTimeout(timeoutHandle);
|
|
713
|
+
browserHook?.cleanup();
|
|
714
|
+
if (session.config.audit.enabled) {
|
|
715
|
+
(0, audit_js_1.writeAuditEvent)(session.config, {
|
|
716
|
+
timestamp: new Date().toISOString(),
|
|
717
|
+
session: sessionId,
|
|
718
|
+
event: 'session_end',
|
|
719
|
+
exit_code: code ?? 0,
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
process.exit(code ?? 0);
|
|
723
|
+
});
|
|
724
|
+
// Forward signals
|
|
725
|
+
for (const sig of ['SIGINT', 'SIGTERM']) {
|
|
726
|
+
process.on(sig, () => {
|
|
727
|
+
child.kill(sig);
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
// ── List running sessions ─────────────────────────────────
|
|
732
|
+
async function listSessions() {
|
|
733
|
+
const config = (0, config_js_1.loadConfig)();
|
|
734
|
+
let runtime;
|
|
735
|
+
try {
|
|
736
|
+
runtime = (0, runtime_js_1.getRuntime)(config.runtime);
|
|
737
|
+
}
|
|
738
|
+
catch (err) {
|
|
739
|
+
console.error(err.message ?? String(err));
|
|
740
|
+
process.exit(1);
|
|
741
|
+
}
|
|
742
|
+
if ((0, runtime_js_1.isApptainerFamily)(runtime)) {
|
|
743
|
+
console.log('Apptainer sessions run as regular processes (not daemons).');
|
|
744
|
+
console.log('To find running labgate sessions:');
|
|
745
|
+
console.log(' ps aux | grep apptainer');
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
try {
|
|
749
|
+
const output = (0, child_process_1.execFileSync)(runtime, [
|
|
750
|
+
'ps',
|
|
751
|
+
'--filter', 'name=labgate-',
|
|
752
|
+
'--format', '{{.Names}}\t{{.Status}}\t{{.RunningFor}}',
|
|
753
|
+
], { encoding: 'utf-8' }).trim();
|
|
754
|
+
if (!output) {
|
|
755
|
+
console.log('No active labgate sessions.');
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
console.log('Active sessions:');
|
|
759
|
+
console.log('NAME\t\t\tSTATUS\t\tRUNNING');
|
|
760
|
+
console.log(output);
|
|
761
|
+
}
|
|
762
|
+
catch {
|
|
763
|
+
console.log('No active labgate sessions.');
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
// ── Stop a session ────────────────────────────────────────
|
|
767
|
+
async function stopSession(id) {
|
|
768
|
+
const config = (0, config_js_1.loadConfig)();
|
|
769
|
+
let runtime;
|
|
770
|
+
try {
|
|
771
|
+
runtime = (0, runtime_js_1.getRuntime)(config.runtime);
|
|
772
|
+
}
|
|
773
|
+
catch (err) {
|
|
774
|
+
console.error(err.message ?? String(err));
|
|
775
|
+
process.exit(1);
|
|
776
|
+
}
|
|
777
|
+
if ((0, runtime_js_1.isApptainerFamily)(runtime)) {
|
|
778
|
+
console.log('Apptainer sessions run as regular processes.');
|
|
779
|
+
console.log('To stop a session, find and kill the process:');
|
|
780
|
+
console.log(' ps aux | grep apptainer');
|
|
781
|
+
console.log(' kill <PID>');
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
const name = id.startsWith('labgate-') ? id : `labgate-${id}`;
|
|
785
|
+
try {
|
|
786
|
+
(0, child_process_1.execFileSync)(runtime, ['stop', name], { stdio: 'inherit' });
|
|
787
|
+
console.log(`Stopped ${name}`);
|
|
788
|
+
}
|
|
789
|
+
catch {
|
|
790
|
+
console.error(`Could not stop ${name}. Is it running?`);
|
|
791
|
+
process.exit(1);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
//# sourceMappingURL=container.js.map
|