labgate 0.5.31 → 0.5.33
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 +50 -2
- package/dist/cli.js +533 -0
- package/dist/cli.js.map +1 -1
- package/dist/lib/config.d.ts +11 -0
- package/dist/lib/config.js +45 -4
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/container.d.ts +3 -3
- package/dist/lib/container.js +144 -12
- package/dist/lib/container.js.map +1 -1
- package/dist/lib/display-mcp.d.ts +10 -0
- package/dist/lib/display-mcp.js +160 -0
- package/dist/lib/display-mcp.js.map +1 -0
- package/dist/lib/display-store.d.ts +24 -0
- package/dist/lib/display-store.js +150 -0
- package/dist/lib/display-store.js.map +1 -0
- package/dist/lib/explorer-autopilot.d.ts +16 -0
- package/dist/lib/explorer-autopilot.js +573 -0
- package/dist/lib/explorer-autopilot.js.map +1 -0
- package/dist/lib/explorer-claude.d.ts +16 -0
- package/dist/lib/explorer-claude.js +361 -0
- package/dist/lib/explorer-claude.js.map +1 -0
- package/dist/lib/explorer-compare.d.ts +9 -0
- package/dist/lib/explorer-compare.js +190 -0
- package/dist/lib/explorer-compare.js.map +1 -0
- package/dist/lib/explorer-eval.d.ts +23 -0
- package/dist/lib/explorer-eval.js +161 -0
- package/dist/lib/explorer-eval.js.map +1 -0
- package/dist/lib/explorer-gc.d.ts +11 -0
- package/dist/lib/explorer-gc.js +304 -0
- package/dist/lib/explorer-gc.js.map +1 -0
- package/dist/lib/explorer-git.d.ts +14 -0
- package/dist/lib/explorer-git.js +136 -0
- package/dist/lib/explorer-git.js.map +1 -0
- package/dist/lib/explorer-lock.d.ts +5 -0
- package/dist/lib/explorer-lock.js +100 -0
- package/dist/lib/explorer-lock.js.map +1 -0
- package/dist/lib/explorer-mcp.d.ts +11 -0
- package/dist/lib/explorer-mcp.js +611 -0
- package/dist/lib/explorer-mcp.js.map +1 -0
- package/dist/lib/explorer-retention.d.ts +4 -0
- package/dist/lib/explorer-retention.js +58 -0
- package/dist/lib/explorer-retention.js.map +1 -0
- package/dist/lib/explorer-store.d.ts +77 -0
- package/dist/lib/explorer-store.js +950 -0
- package/dist/lib/explorer-store.js.map +1 -0
- package/dist/lib/explorer-types.d.ts +161 -0
- package/dist/lib/explorer-types.js +3 -0
- package/dist/lib/explorer-types.js.map +1 -0
- package/dist/lib/explorer.d.ts +31 -0
- package/dist/lib/explorer.js +247 -0
- package/dist/lib/explorer.js.map +1 -0
- package/dist/lib/results-store.js +37 -3
- package/dist/lib/results-store.js.map +1 -1
- package/dist/lib/test/integration-harness.js +1 -1
- package/dist/lib/test/integration-harness.js.map +1 -1
- package/dist/lib/ui.html +5115 -2052
- package/dist/lib/ui.js +906 -39
- package/dist/lib/ui.js.map +1 -1
- package/dist/lib/web-terminal.js +4 -3
- package/dist/lib/web-terminal.js.map +1 -1
- package/dist/mcp-bundles/dataset-mcp.bundle.mjs +0 -8
- package/dist/mcp-bundles/display-mcp.bundle.mjs +30209 -0
- package/dist/mcp-bundles/explorer-mcp.bundle.mjs +40036 -0
- package/dist/mcp-bundles/results-mcp.bundle.mjs +30 -4
- package/package.json +3 -2
- package/templates/tsp-lab/API_CONTRACT.md +20 -0
- package/templates/tsp-lab/EVAL.md +20 -0
- package/templates/tsp-lab/PROBLEM.md +18 -0
- package/templates/tsp-lab/data/generate_instances.py +51 -0
- package/templates/tsp-lab/data/instances.jsonl +12 -0
- package/templates/tsp-lab/eval.py +148 -0
- package/templates/tsp-lab/solver.py +88 -0
- package/templates/tsp-lab/stub-patches/enable_two_opt.patch +14 -0
package/dist/lib/container.js
CHANGED
|
@@ -203,6 +203,8 @@ function cleanupLabgateInstructionFile(workdir, filename, deleteWhenEmpty) {
|
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
205
|
function buildLabgateInstructionBlock(session) {
|
|
206
|
+
const agentLower = session.agent.toLowerCase();
|
|
207
|
+
const hasMcp = agentLower === 'claude' || agentLower === 'codex';
|
|
206
208
|
const lines = [
|
|
207
209
|
LABGATE_INSTRUCTION_START,
|
|
208
210
|
'## LabGate Sandbox Context (Auto-Managed)',
|
|
@@ -233,7 +235,7 @@ function buildLabgateInstructionBlock(session) {
|
|
|
233
235
|
lines.push('');
|
|
234
236
|
lines.push('Use these dataset paths directly when the user references data by name.');
|
|
235
237
|
}
|
|
236
|
-
if (
|
|
238
|
+
if (hasMcp) {
|
|
237
239
|
lines.push('');
|
|
238
240
|
lines.push('### Dataset Management');
|
|
239
241
|
lines.push('- Use the `labgate-datasets` MCP tools to manage and explore datasets.');
|
|
@@ -244,13 +246,30 @@ function buildLabgateInstructionBlock(session) {
|
|
|
244
246
|
lines.push('- Use the `labgate-results` MCP tools to record and revisit important outcomes.');
|
|
245
247
|
lines.push('- Available tools: `list_results`, `register_result`, `get_result`, `update_result`, `delete_result`');
|
|
246
248
|
lines.push('- When you finish meaningful work, call `register_result` with a concise title, summary, and relevant tags/artifacts.');
|
|
249
|
+
lines.push('');
|
|
250
|
+
lines.push('### Display Widgets');
|
|
251
|
+
lines.push('- Use the `labgate-display` MCP tool `display_widget` to render rich interactive content in the LabGate web UI.');
|
|
252
|
+
lines.push('- Supported widgets: `markdown`, `table`, `plot`, `image`, `pdf`, `molecule`, `sequence`, `alignment`, `file_preview`, `tree`, `heatmap`, `tracks`, `domains`, `network`');
|
|
253
|
+
lines.push('- Use `molecule` with `pdb_id` to show 3D protein structures (e.g. `{ pdb_id: "2HIU" }`).');
|
|
254
|
+
lines.push('- Use `sequence` to display colored DNA/RNA/protein sequences.');
|
|
255
|
+
lines.push('- Use `plot` with a Plotly.js spec (`{ plotly: { data: [...], layout: {...} } }`) for interactive charts.');
|
|
256
|
+
lines.push('- Use `table` for structured data (`{ columns: [...], rows: [...] }`).');
|
|
257
|
+
lines.push('- Use `image` with `path` to display images from the filesystem.');
|
|
258
|
+
lines.push('- Use `pdf` with `path` to display PDF documents inline (`{ path: "/work/paper.pdf" }`).');
|
|
259
|
+
lines.push('- Use `alignment` for multiple sequence alignments with conservation highlighting.');
|
|
260
|
+
lines.push('- Use `tree` with Newick format string for phylogenetic trees (e.g. `{ newick: "((A,B),(C,D));" }`).');
|
|
261
|
+
lines.push('- Use `heatmap` for clustered heatmaps with optional dendrograms (`{ matrix: [[...]], row_labels: [...], cluster_rows: true }`).');
|
|
262
|
+
lines.push('- Use `tracks` for genome feature viewing via igv.js (`{ tracks: [{ path: "/work/file.gff3", format: "gff3" }] }`).');
|
|
263
|
+
lines.push('- Use `domains` for protein domain architecture (`{ length: 300, features: [{ type: "domain", name: "Kinase", start: 10, end: 120 }] }`).');
|
|
264
|
+
lines.push('- Use `network` for interactive graph visualization (`{ nodes: [{id:"A"}], edges: [{source:"A", target:"B"}] }`).');
|
|
265
|
+
lines.push('- Prefer `display_widget` over plain text when showing structured data, sequences, or molecular information.');
|
|
247
266
|
}
|
|
248
267
|
if (session.config.slurm.enabled) {
|
|
249
268
|
lines.push('');
|
|
250
269
|
lines.push('### SLURM Integration');
|
|
251
270
|
lines.push('- SLURM commands (srun, sbatch, squeue, scancel) are available.');
|
|
252
271
|
lines.push('- LabGate tracks your SLURM jobs automatically via squeue polling.');
|
|
253
|
-
if (session.config.slurm.mcp_server &&
|
|
272
|
+
if (session.config.slurm.mcp_server && hasMcp) {
|
|
254
273
|
lines.push('- Use the `labgate-cluster` MCP tools to inspect partitions, queue pressure, and available modules.');
|
|
255
274
|
lines.push('- Available tools: `get_cluster_partitions`, `get_partition_limits`, `get_queue_pressure`, `find_module`');
|
|
256
275
|
lines.push('- Use the `labgate-slurm` MCP tools to check job status and read output files.');
|
|
@@ -266,14 +285,64 @@ function buildLabgateInstructionBlock(session) {
|
|
|
266
285
|
lines.push(LABGATE_INSTRUCTION_END);
|
|
267
286
|
return lines.join('\n');
|
|
268
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* Remove all [mcp_servers.*] sections (and their key/value lines) from a TOML string,
|
|
290
|
+
* preserving all other sections (model, projects, features, etc.).
|
|
291
|
+
*/
|
|
292
|
+
function stripTomlMcpSections(toml) {
|
|
293
|
+
const lines = toml.split('\n');
|
|
294
|
+
const result = [];
|
|
295
|
+
let inMcpSection = false;
|
|
296
|
+
for (const line of lines) {
|
|
297
|
+
const trimmed = line.trim();
|
|
298
|
+
if (/^\[mcp_servers[\].]/i.test(trimmed)) {
|
|
299
|
+
inMcpSection = true;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (inMcpSection && /^\[/.test(trimmed) && !/^\[mcp_servers[\].]/i.test(trimmed)) {
|
|
303
|
+
inMcpSection = false;
|
|
304
|
+
}
|
|
305
|
+
if (!inMcpSection) {
|
|
306
|
+
result.push(line);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// Trim trailing blank lines
|
|
310
|
+
while (result.length > 0 && result[result.length - 1].trim() === '') {
|
|
311
|
+
result.pop();
|
|
312
|
+
}
|
|
313
|
+
return result.join('\n');
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Serialize MCP server entries to Codex-compatible TOML format.
|
|
317
|
+
* Produces [mcp_servers."<name>"] sections with command, args, and optional env.
|
|
318
|
+
*/
|
|
319
|
+
function serializeCodexMcpToml(servers) {
|
|
320
|
+
const lines = [];
|
|
321
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
322
|
+
lines.push(`[mcp_servers.${JSON.stringify(name)}]`);
|
|
323
|
+
lines.push(`command = ${JSON.stringify(server.command)}`);
|
|
324
|
+
lines.push(`args = [${server.args.map((a) => JSON.stringify(a)).join(', ')}]`);
|
|
325
|
+
if (server.env && Object.keys(server.env).length > 0) {
|
|
326
|
+
lines.push('');
|
|
327
|
+
lines.push(`[mcp_servers.${JSON.stringify(name)}.env]`);
|
|
328
|
+
for (const [k, v] of Object.entries(server.env)) {
|
|
329
|
+
lines.push(`${k} = ${JSON.stringify(v)}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
lines.push('');
|
|
333
|
+
}
|
|
334
|
+
return lines.join('\n');
|
|
335
|
+
}
|
|
269
336
|
function prepareMcpServers(session, options = {}) {
|
|
270
|
-
|
|
337
|
+
const agentLower = session.agent.toLowerCase();
|
|
338
|
+
if (agentLower !== 'claude' && agentLower !== 'codex')
|
|
271
339
|
return;
|
|
272
340
|
try {
|
|
273
341
|
const sandboxHome = (0, config_js_1.getSandboxHome)();
|
|
274
342
|
const mcpDir = (0, path_1.join)(sandboxHome, '.mcp-servers');
|
|
275
343
|
(0, fs_1.mkdirSync)(mcpDir, { recursive: true });
|
|
276
|
-
// Claude Code reads MCP config from ~/.claude.json (in HOME root)
|
|
344
|
+
// Claude Code reads MCP config from ~/.claude.json (in HOME root).
|
|
345
|
+
// For Codex we build the same mcpConfig object, then serialize to TOML below.
|
|
277
346
|
const mcpConfigPath = (0, path_1.join)(sandboxHome, '.claude.json');
|
|
278
347
|
let mcpConfig = {};
|
|
279
348
|
try {
|
|
@@ -303,7 +372,7 @@ function prepareMcpServers(session, options = {}) {
|
|
|
303
372
|
copyMcpNodePackage('bindings') &&
|
|
304
373
|
copyMcpNodePackage('file-uri-to-path'));
|
|
305
374
|
};
|
|
306
|
-
// Dataset MCP server (always
|
|
375
|
+
// Dataset MCP server (always)
|
|
307
376
|
const datasetBundleSrc = (0, path_1.resolve)(__dirname, '../mcp-bundles/dataset-mcp.bundle.mjs');
|
|
308
377
|
if ((0, fs_1.existsSync)(datasetBundleSrc)) {
|
|
309
378
|
(0, fs_1.copyFileSync)(datasetBundleSrc, (0, path_1.join)(mcpDir, 'dataset-mcp.bundle.mjs'));
|
|
@@ -321,7 +390,7 @@ function prepareMcpServers(session, options = {}) {
|
|
|
321
390
|
args: [datasetMcpPath],
|
|
322
391
|
};
|
|
323
392
|
}
|
|
324
|
-
// Results MCP server (always
|
|
393
|
+
// Results MCP server (always)
|
|
325
394
|
const resultsBundleSrc = (0, path_1.resolve)(__dirname, '../mcp-bundles/results-mcp.bundle.mjs');
|
|
326
395
|
if ((0, fs_1.existsSync)(resultsBundleSrc)) {
|
|
327
396
|
(0, fs_1.copyFileSync)(resultsBundleSrc, (0, path_1.join)(mcpDir, 'results-mcp.bundle.mjs'));
|
|
@@ -338,6 +407,23 @@ function prepareMcpServers(session, options = {}) {
|
|
|
338
407
|
args: [resultsMcpPath, '--db', (0, config_js_1.getResultsDbPath)()],
|
|
339
408
|
};
|
|
340
409
|
}
|
|
410
|
+
// Display MCP server (always — renders rich widgets in LabGate UI)
|
|
411
|
+
const displayBundleSrc = (0, path_1.resolve)(__dirname, '../mcp-bundles/display-mcp.bundle.mjs');
|
|
412
|
+
if ((0, fs_1.existsSync)(displayBundleSrc)) {
|
|
413
|
+
(0, fs_1.copyFileSync)(displayBundleSrc, (0, path_1.join)(mcpDir, 'display-mcp.bundle.mjs'));
|
|
414
|
+
mcpConfig.mcpServers['labgate-display'] = {
|
|
415
|
+
command: 'node',
|
|
416
|
+
args: ['/home/sandbox/.mcp-servers/display-mcp.bundle.mjs', '--db', '/labgate-config/display.json'],
|
|
417
|
+
env,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
const displayMcpPath = (0, path_1.resolve)(__dirname, 'display-mcp.js');
|
|
422
|
+
mcpConfig.mcpServers['labgate-display'] = {
|
|
423
|
+
command: 'node',
|
|
424
|
+
args: [displayMcpPath, '--db', (0, config_js_1.getDisplayDbPath)()],
|
|
425
|
+
};
|
|
426
|
+
}
|
|
341
427
|
// Cluster MCP server (when SLURM integration is enabled)
|
|
342
428
|
if (session.config.slurm.enabled && session.config.slurm.mcp_server) {
|
|
343
429
|
const clusterBundleSrc = (0, path_1.resolve)(__dirname, '../mcp-bundles/cluster-mcp.bundle.mjs');
|
|
@@ -389,8 +475,29 @@ function prepareMcpServers(session, options = {}) {
|
|
|
389
475
|
else {
|
|
390
476
|
delete mcpConfig.mcpServers['labgate-slurm'];
|
|
391
477
|
}
|
|
392
|
-
|
|
393
|
-
|
|
478
|
+
if (agentLower === 'claude') {
|
|
479
|
+
(0, fs_1.writeFileSync)(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), 'utf-8');
|
|
480
|
+
(0, config_js_1.ensurePrivateFile)(mcpConfigPath);
|
|
481
|
+
}
|
|
482
|
+
if (agentLower === 'codex') {
|
|
483
|
+
// Codex reads MCP config from ~/.codex/config.toml
|
|
484
|
+
const codexDir = (0, path_1.join)(sandboxHome, '.codex');
|
|
485
|
+
(0, fs_1.mkdirSync)(codexDir, { recursive: true });
|
|
486
|
+
const codexConfigPath = (0, path_1.join)(codexDir, 'config.toml');
|
|
487
|
+
let existingToml = '';
|
|
488
|
+
try {
|
|
489
|
+
if ((0, fs_1.existsSync)(codexConfigPath)) {
|
|
490
|
+
existingToml = (0, fs_1.readFileSync)(codexConfigPath, 'utf-8');
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
catch { /* start fresh */ }
|
|
494
|
+
// Strip old [mcp_servers.*] sections and append fresh ones
|
|
495
|
+
const cleaned = stripTomlMcpSections(existingToml);
|
|
496
|
+
const mcpToml = serializeCodexMcpToml(mcpConfig.mcpServers);
|
|
497
|
+
const separator = cleaned.length > 0 && !cleaned.endsWith('\n') ? '\n\n' : cleaned.length > 0 ? '\n' : '';
|
|
498
|
+
(0, fs_1.writeFileSync)(codexConfigPath, cleaned + separator + mcpToml, 'utf-8');
|
|
499
|
+
(0, config_js_1.ensurePrivateFile)(codexConfigPath);
|
|
500
|
+
}
|
|
394
501
|
}
|
|
395
502
|
catch {
|
|
396
503
|
// Best effort — MCP registration failure is non-fatal
|
|
@@ -984,6 +1091,7 @@ function buildApptainerArgs(session, sifPath, sessionId, tokenEnv = []) {
|
|
|
984
1091
|
// ── LabGate config for in-container MCP servers ──
|
|
985
1092
|
...((0, fs_1.existsSync)((0, config_js_1.getConfigPath)()) ? ['--bind', `${(0, config_js_1.getConfigPath)()}:/labgate-config/config.json`] : []),
|
|
986
1093
|
...(resultsDbPath ? ['--bind', `${resultsDbPath}:/labgate-config/results.json`] : []),
|
|
1094
|
+
...((0, fs_1.existsSync)((0, config_js_1.getDisplayDbPath)()) ? ['--bind', `${(0, config_js_1.getDisplayDbPath)()}:/labgate-config/display.json`] : []),
|
|
987
1095
|
...(config.slurm.enabled && (0, fs_1.existsSync)((0, config_js_1.getSlurmDbPath)())
|
|
988
1096
|
? ['--bind', `${(0, config_js_1.getSlurmDbPath)()}:/labgate-config/slurm.db`] : []),
|
|
989
1097
|
...(config.slurm.enabled && (0, fs_1.existsSync)(`${(0, config_js_1.getSlurmDbPath)()}.json`)
|
|
@@ -1047,6 +1155,7 @@ function buildPodmanArgs(session, image, sessionId, tokenEnv = [], options = { t
|
|
|
1047
1155
|
// ── LabGate config for in-container MCP servers ──
|
|
1048
1156
|
...((0, fs_1.existsSync)((0, config_js_1.getConfigPath)()) ? ['--volume', `${(0, config_js_1.getConfigPath)()}:/labgate-config/config.json`] : []),
|
|
1049
1157
|
...(resultsDbPath ? ['--volume', `${resultsDbPath}:/labgate-config/results.json`] : []),
|
|
1158
|
+
...((0, fs_1.existsSync)((0, config_js_1.getDisplayDbPath)()) ? ['--volume', `${(0, config_js_1.getDisplayDbPath)()}:/labgate-config/display.json`] : []),
|
|
1050
1159
|
...(config.slurm.enabled && (0, fs_1.existsSync)((0, config_js_1.getSlurmDbPath)())
|
|
1051
1160
|
? ['--volume', `${(0, config_js_1.getSlurmDbPath)()}:/labgate-config/slurm.db`] : []),
|
|
1052
1161
|
...(config.slurm.enabled && (0, fs_1.existsSync)(`${(0, config_js_1.getSlurmDbPath)()}.json`)
|
|
@@ -1176,7 +1285,7 @@ function buildEntrypoint(agent) {
|
|
|
1176
1285
|
}
|
|
1177
1286
|
const preLaunchLines = [];
|
|
1178
1287
|
if (agent === 'codex') {
|
|
1179
|
-
preLaunchLines.push('labgate_ensure_ca_certificates() {', ' if [ -s /etc/ssl/certs/ca-certificates.crt ] || [ -s /etc/ssl/cert.pem ]; then', ' return 0', ' fi', ' if [ "$(id -u)" != "0" ]; then', ' echo "[labgate] Warning: missing CA certificates and no root access to install them; Codex login may fail." >&2', ' return 0', ' fi', ' echo "[labgate] Installing ca-certificates for Codex TLS..."', ' if command -v apt-get >/dev/null 2>&1; then', ' export DEBIAN_FRONTEND=noninteractive', ' apt-get update >/dev/null 2>&1 || return 0', ' apt-get install -y ca-certificates >/dev/null 2>&1 || return 0', ' update-ca-certificates >/dev/null 2>&1 || true', ' return 0', ' fi', ' if command -v apk >/dev/null 2>&1; then', ' apk add --no-cache ca-certificates >/dev/null 2>&1 || return 0', ' update-ca-certificates >/dev/null 2>&1 || true', ' return 0', ' fi', ' if command -v dnf >/dev/null 2>&1; then', ' dnf install -y ca-certificates >/dev/null 2>&1 || return 0', ' return 0', ' fi', ' if command -v yum >/dev/null 2>&1; then', ' yum install -y ca-certificates >/dev/null 2>&1 || return 0', ' return 0', ' fi', ' if command -v microdnf >/dev/null 2>&1; then', ' microdnf install -y ca-certificates >/dev/null 2>&1 || return 0', ' fi', '}', 'labgate_ensure_ca_certificates', 'if [ "${LABGATE_CODEX_PODMAN_DARWIN_WORKAROUND:-}" = "1" ]; then', ' if ! codex login status >/dev/null 2>&1; then', ' echo "[labgate] Starting Codex device-code login (Podman macOS callback workaround)..."', ' codex login --device-auth', ' fi', 'fi');
|
|
1288
|
+
preLaunchLines.push('labgate_ensure_ca_certificates() {', ' if [ -s /etc/ssl/certs/ca-certificates.crt ] || [ -s /etc/ssl/cert.pem ]; then', ' return 0', ' fi', ' if [ "$(id -u)" != "0" ]; then', ' echo "[labgate] Warning: missing CA certificates and no root access to install them; Codex login may fail." >&2', ' return 0', ' fi', ' echo "[labgate] Installing ca-certificates for Codex TLS..."', ' if command -v apt-get >/dev/null 2>&1; then', ' export DEBIAN_FRONTEND=noninteractive', ' apt-get update >/dev/null 2>&1 || return 0', ' apt-get install -y ca-certificates >/dev/null 2>&1 || return 0', ' update-ca-certificates >/dev/null 2>&1 || true', ' return 0', ' fi', ' if command -v apk >/dev/null 2>&1; then', ' apk add --no-cache ca-certificates >/dev/null 2>&1 || return 0', ' update-ca-certificates >/dev/null 2>&1 || true', ' return 0', ' fi', ' if command -v dnf >/dev/null 2>&1; then', ' dnf install -y ca-certificates >/dev/null 2>&1 || return 0', ' return 0', ' fi', ' if command -v yum >/dev/null 2>&1; then', ' yum install -y ca-certificates >/dev/null 2>&1 || return 0', ' return 0', ' fi', ' if command -v microdnf >/dev/null 2>&1; then', ' microdnf install -y ca-certificates >/dev/null 2>&1 || return 0', ' fi', '}', 'labgate_ensure_ca_certificates', 'if [ "${LABGATE_CODEX_PODMAN_DARWIN_WORKAROUND:-}" = "1" ] || [ "${LABGATE_CODEX_APPTAINER_REMOTE_DEVICE_AUTH:-}" = "1" ]; then', ' if ! codex login status >/dev/null 2>&1; then', ' if [ "${LABGATE_CODEX_PODMAN_DARWIN_WORKAROUND:-}" = "1" ]; then', ' echo "[labgate] Starting Codex device-code login (Podman macOS callback workaround)..."', ' else', ' echo "[labgate] Starting Codex device-code login (Apptainer remote login flow)..."', ' fi', ' codex login --device-auth', ' fi', 'fi');
|
|
1180
1289
|
}
|
|
1181
1290
|
lines.push(`if ! command -v ${setup.bin} >/dev/null 2>&1; then`, ` ${setup.installer}`, 'fi', ...preLaunchLines, `echo "[labgate] Starting ${setup.bin} in /work"`, `exec ${setup.bin}`);
|
|
1182
1291
|
return lines.join('\n');
|
|
@@ -1206,6 +1315,21 @@ function osc52Copy(text) {
|
|
|
1206
1315
|
const tty = process.stderr.isTTY ? process.stderr : process.stdout;
|
|
1207
1316
|
tty.write(`\x1b]52;c;${encoded}\x07`);
|
|
1208
1317
|
}
|
|
1318
|
+
function supportsTerminalHyperlinks() {
|
|
1319
|
+
if (!(process.stderr.isTTY ?? false))
|
|
1320
|
+
return false;
|
|
1321
|
+
if (process.env.LABGATE_HYPERLINKS === '0')
|
|
1322
|
+
return false;
|
|
1323
|
+
if ((process.env.TERM || '').toLowerCase() === 'dumb')
|
|
1324
|
+
return false;
|
|
1325
|
+
return true;
|
|
1326
|
+
}
|
|
1327
|
+
function formatTerminalHyperlink(url) {
|
|
1328
|
+
if (!supportsTerminalHyperlinks())
|
|
1329
|
+
return url;
|
|
1330
|
+
const safe = String(url || '').replace(/[\u001b\u0007]/g, '');
|
|
1331
|
+
return `\u001b]8;;${safe}\u0007${url}\u001b]8;;\u0007`;
|
|
1332
|
+
}
|
|
1209
1333
|
const OAUTH_URL_DEDUPE_WINDOW_MS = 20_000;
|
|
1210
1334
|
let lastHandledOAuthUrl = '';
|
|
1211
1335
|
let lastHandledOAuthAt = 0;
|
|
@@ -1278,6 +1402,7 @@ function handleOAuthUrl(url, options) {
|
|
|
1278
1402
|
const normalizedUrl = normalizeClaudeOAuthUrl(url);
|
|
1279
1403
|
if (!normalizedUrl)
|
|
1280
1404
|
return;
|
|
1405
|
+
const displayUrl = formatTerminalHyperlink(normalizedUrl);
|
|
1281
1406
|
const now = Date.now();
|
|
1282
1407
|
if (normalizedUrl === lastHandledOAuthUrl && (now - lastHandledOAuthAt) < OAUTH_URL_DEDUPE_WINDOW_MS) {
|
|
1283
1408
|
return;
|
|
@@ -1287,7 +1412,7 @@ function handleOAuthUrl(url, options) {
|
|
|
1287
1412
|
if (options.isRemote) {
|
|
1288
1413
|
// SSH / headless: push URL to local clipboard via OSC 52 if supported.
|
|
1289
1414
|
osc52Copy(normalizedUrl);
|
|
1290
|
-
log.info(`Login URL:\n${
|
|
1415
|
+
log.info(`Login URL:\n${displayUrl}`);
|
|
1291
1416
|
log.info('Tried to copy URL via terminal clipboard (OSC 52).');
|
|
1292
1417
|
log.info('Open it in your local browser, then paste the code back here.');
|
|
1293
1418
|
return;
|
|
@@ -1303,7 +1428,7 @@ function handleOAuthUrl(url, options) {
|
|
|
1303
1428
|
}
|
|
1304
1429
|
catch {
|
|
1305
1430
|
log.warn('Could not auto-open browser. URL copied to clipboard (if available).');
|
|
1306
|
-
log.info(`Login URL:\n${
|
|
1431
|
+
log.info(`Login URL:\n${displayUrl}`);
|
|
1307
1432
|
}
|
|
1308
1433
|
return;
|
|
1309
1434
|
}
|
|
@@ -1315,7 +1440,7 @@ function handleOAuthUrl(url, options) {
|
|
|
1315
1440
|
catch {
|
|
1316
1441
|
osc52Copy(normalizedUrl);
|
|
1317
1442
|
log.warn('Could not auto-open browser. Tried clipboard copy via terminal.');
|
|
1318
|
-
log.info(`Login URL:\n${
|
|
1443
|
+
log.info(`Login URL:\n${displayUrl}`);
|
|
1319
1444
|
}
|
|
1320
1445
|
}
|
|
1321
1446
|
/**
|
|
@@ -1647,9 +1772,16 @@ async function startSession(session) {
|
|
|
1647
1772
|
(0, os_1.platform)() === 'darwin')
|
|
1648
1773
|
? ['--env', 'LABGATE_CODEX_PODMAN_DARWIN_WORKAROUND=1']
|
|
1649
1774
|
: [];
|
|
1775
|
+
const codexApptainerRemoteDeviceAuth = (runtime === 'apptainer' &&
|
|
1776
|
+
session.agent === 'codex' &&
|
|
1777
|
+
tokenEnv.length === 0 &&
|
|
1778
|
+
!canOpenBrowser())
|
|
1779
|
+
? ['--env', 'LABGATE_CODEX_APPTAINER_REMOTE_DEVICE_AUTH=1']
|
|
1780
|
+
: [];
|
|
1650
1781
|
const runtimeEnvArgs = [
|
|
1651
1782
|
...claudeEnvGuard,
|
|
1652
1783
|
...codexPodmanDarwinLoginWorkaround,
|
|
1784
|
+
...codexApptainerRemoteDeviceAuth,
|
|
1653
1785
|
...tokenEnv,
|
|
1654
1786
|
...(browserHook?.env ?? []),
|
|
1655
1787
|
];
|