labgate 0.5.10 → 0.5.12
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 +59 -4
- package/dist/cli.js +425 -27
- package/dist/cli.js.map +1 -1
- package/dist/lib/cluster-mcp.d.ts +33 -0
- package/dist/lib/cluster-mcp.js +313 -0
- package/dist/lib/cluster-mcp.js.map +1 -0
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.js +11 -4
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/container.d.ts +10 -0
- package/dist/lib/container.js +195 -32
- package/dist/lib/container.js.map +1 -1
- package/dist/lib/dataset-mcp.d.ts +20 -0
- package/dist/lib/dataset-mcp.js +809 -0
- package/dist/lib/dataset-mcp.js.map +1 -0
- package/dist/lib/feedback.js +15 -3
- package/dist/lib/feedback.js.map +1 -1
- package/dist/lib/init.js +2 -2
- package/dist/lib/results-mcp.d.ts +9 -0
- package/dist/lib/results-mcp.js +205 -0
- package/dist/lib/results-mcp.js.map +1 -0
- package/dist/lib/results-store.d.ts +61 -0
- package/dist/lib/results-store.js +319 -0
- package/dist/lib/results-store.js.map +1 -0
- package/dist/lib/slurm-cli-passthrough.d.ts +25 -0
- package/dist/lib/slurm-cli-passthrough.js +330 -0
- package/dist/lib/slurm-cli-passthrough.js.map +1 -0
- package/dist/lib/slurm-mcp.js +1 -1
- package/dist/lib/slurm-mcp.js.map +1 -1
- package/dist/lib/test/integration-harness.d.ts +4 -0
- package/dist/lib/test/integration-harness.js +14 -2
- package/dist/lib/test/integration-harness.js.map +1 -1
- package/dist/lib/ui.html +2068 -351
- package/dist/lib/ui.js +701 -0
- package/dist/lib/ui.js.map +1 -1
- package/dist/mcp-bundles/cluster-mcp.bundle.mjs +30235 -0
- package/dist/mcp-bundles/dataset-mcp.bundle.mjs +30971 -0
- package/dist/mcp-bundles/results-mcp.bundle.mjs +30449 -0
- package/dist/mcp-bundles/slurm-mcp.bundle.mjs +30501 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ LabGate runs your AI coding agent inside a sandboxed container with:
|
|
|
33
33
|
|
|
34
34
|
- **Scoped filesystem** — only your working directory and configured paths are visible
|
|
35
35
|
- **Credential blocking** — `.ssh`, `.aws`, `.env`, `.gnupg`, and other sensitive paths are hidden by default
|
|
36
|
-
- **Network
|
|
36
|
+
- **Network policy** — configurable network modes (`host`, `filtered`, `none`)
|
|
37
37
|
- **Command blocking** — `ssh`, `curl`, `wget`, and other commands are blocked by default
|
|
38
38
|
- **Audit logging** — session start/stop and mount configuration logged to `~/.labgate/logs/`
|
|
39
39
|
- **Dashboard instructions editor** — view and update per-session `AGENTS.md` / `CLAUDE.md` from the UI
|
|
@@ -54,16 +54,22 @@ Or start fresh:
|
|
|
54
54
|
labgate init --force
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
Or reset a single setting back to defaults:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
labgate config reset image
|
|
61
|
+
```
|
|
62
|
+
|
|
57
63
|
### Key settings
|
|
58
64
|
|
|
59
65
|
| Setting | Default | What it does |
|
|
60
66
|
|---------|---------|-------------|
|
|
61
67
|
| `runtime` | `auto` | `auto`, `apptainer`, or `podman` |
|
|
62
|
-
| `image` | `node:20-
|
|
68
|
+
| `image` | `docker.io/library/node:20-bookworm` | Container image |
|
|
63
69
|
| `session_timeout_hours` | `8` | Max session length |
|
|
64
70
|
| `filesystem.blocked_patterns` | `.ssh, .aws, .env, ...` | Hidden from sandbox |
|
|
65
71
|
| `filesystem.extra_paths` | `[]` | Additional mounts |
|
|
66
|
-
| `network.mode` | `
|
|
72
|
+
| `network.mode` | `host` | `none`, `filtered`, or `host` |
|
|
67
73
|
| `commands.blacklist` | `ssh, curl, wget, ...` | Blocked commands |
|
|
68
74
|
|
|
69
75
|
## Commands
|
|
@@ -75,6 +81,11 @@ labgate feedback # submit feedback (interactive or piped)
|
|
|
75
81
|
labgate status # list running sessions
|
|
76
82
|
labgate stop <id> # stop a session
|
|
77
83
|
labgate ui # start dashboard server (owner-only Unix socket by default)
|
|
84
|
+
labgate register <activation-key> [--server <url>] # activate + install enterprise license
|
|
85
|
+
labgate license # show enterprise license status
|
|
86
|
+
labgate license install <key-or-file> [--system|--user|--path] # install enterprise license key
|
|
87
|
+
labgate policy init [--institution ... --admin ...] # create policy template
|
|
88
|
+
labgate policy validate [file] # validate policy JSON
|
|
78
89
|
labgate logs [-n 20] # view recent audit events
|
|
79
90
|
labgate logs --follow # stream new audit events
|
|
80
91
|
labgate init [--force] # create/reset config
|
|
@@ -103,7 +114,8 @@ labgate feedback "Short feedback message"
|
|
|
103
114
|
|
|
104
115
|
If `LABGATE_FEEDBACK_URL` is set, LabGate will `POST` feedback JSON to that URL.
|
|
105
116
|
If `LABGATE_FEEDBACK_TOKEN` is set, it will be sent as a Bearer token.
|
|
106
|
-
If no URL is configured
|
|
117
|
+
If no URL is configured, LabGate defaults to `https://labgate.dev/api/feedback`.
|
|
118
|
+
If the request fails (or `LABGATE_FEEDBACK_DISABLE=1`), feedback is saved locally at `~/.labgate/feedback.jsonl`.
|
|
107
119
|
|
|
108
120
|
## Testing
|
|
109
121
|
|
|
@@ -121,6 +133,49 @@ npm test # unit + integration
|
|
|
121
133
|
npm run release:check # verify + npm pack --dry-run
|
|
122
134
|
```
|
|
123
135
|
|
|
136
|
+
### Corporate mode smoke test (local or HPC)
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
# 1) Create a signed enterprise key (issuer side)
|
|
140
|
+
export LABGATE_LICENSE_SECRET='replace-with-your-signing-secret'
|
|
141
|
+
LICENSE_KEY="$(npx tsx scripts/generate-license.ts \
|
|
142
|
+
--institution 'Example University' \
|
|
143
|
+
--tier pro \
|
|
144
|
+
--expires 2099-12-31 2>/dev/null)"
|
|
145
|
+
|
|
146
|
+
# 2) Install key on target host (admin side)
|
|
147
|
+
labgate license install "$LICENSE_KEY" --path /tmp/labgate/license.key --overwrite
|
|
148
|
+
# HPC system-wide install (root):
|
|
149
|
+
# sudo labgate license install "$LICENSE_KEY" --system --overwrite
|
|
150
|
+
|
|
151
|
+
# Optional online activation (instead of direct install):
|
|
152
|
+
# Uses default endpoint: https://labgate.dev/api/license/activate
|
|
153
|
+
# labgate register '<activation-key-from-vendor>' --path /tmp/labgate/license.key --overwrite
|
|
154
|
+
# Optional custom endpoint:
|
|
155
|
+
# export LABGATE_ACTIVATION_URL='https://your-control-plane.example.com/api/license/activate'
|
|
156
|
+
|
|
157
|
+
# 3) Bootstrap and validate policy
|
|
158
|
+
labgate policy init --path /tmp/labgate/policy.json --admin "$(whoami)" --force
|
|
159
|
+
labgate policy validate /tmp/labgate/policy.json
|
|
160
|
+
|
|
161
|
+
# 4) Verify forced settings are locked for users
|
|
162
|
+
LABGATE_LICENSE_PATH=/tmp/labgate/license.key \
|
|
163
|
+
LABGATE_POLICY_PATH=/tmp/labgate/policy.json \
|
|
164
|
+
labgate config set runtime auto
|
|
165
|
+
# expected: error about admin-locked field
|
|
166
|
+
|
|
167
|
+
# 5) Open dashboard and verify UI lock labels ("set by admin")
|
|
168
|
+
LABGATE_LICENSE_PATH=/tmp/labgate/license.key \
|
|
169
|
+
LABGATE_POLICY_PATH=/tmp/labgate/policy.json \
|
|
170
|
+
labgate ui --port 7700
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Automated enterprise coverage:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
npx vitest run -c vitest.integration.config.ts src/lib/cli.enterprise.integration.test.ts src/lib/ui.integration.test.ts
|
|
177
|
+
```
|
|
178
|
+
|
|
124
179
|
CI automation runs `npm run verify` on every push to `main` and on pull requests (`.github/workflows/ci.yml`).
|
|
125
180
|
`npm run test:integration` automatically rebuilds `better-sqlite3` first to avoid Node ABI mismatch errors after Node upgrades.
|
|
126
181
|
|
package/dist/cli.js
CHANGED
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
const commander_1 = require("commander");
|
|
37
37
|
const path_1 = require("path");
|
|
38
38
|
const fs_1 = require("fs");
|
|
39
|
+
const os_1 = require("os");
|
|
39
40
|
const config_js_1 = require("./lib/config.js");
|
|
40
41
|
const init_js_1 = require("./lib/init.js");
|
|
41
42
|
const container_js_1 = require("./lib/container.js");
|
|
@@ -43,6 +44,7 @@ const runtime_js_1 = require("./lib/runtime.js");
|
|
|
43
44
|
const feedback_js_1 = require("./lib/feedback.js");
|
|
44
45
|
const log = __importStar(require("./lib/log.js"));
|
|
45
46
|
const AGENTS = ['claude', 'codex'];
|
|
47
|
+
const DEFAULT_ACTIVATION_URL = 'https://labgate.dev/api/license/activate';
|
|
46
48
|
// Read version from package.json so it stays in sync
|
|
47
49
|
const pkgPath = (0, path_1.resolve)(__dirname, '..', 'package.json');
|
|
48
50
|
const PKG_VERSION = JSON.parse((0, fs_1.readFileSync)(pkgPath, 'utf-8')).version;
|
|
@@ -181,6 +183,39 @@ configCmd
|
|
|
181
183
|
process.exit(1);
|
|
182
184
|
}
|
|
183
185
|
});
|
|
186
|
+
configCmd
|
|
187
|
+
.command('reset')
|
|
188
|
+
.description('Reset (unset) a config value so defaults apply')
|
|
189
|
+
.argument('<key>', 'Config key (e.g. image, network.mode, slurm.enabled)')
|
|
190
|
+
.action((key) => {
|
|
191
|
+
const defaultValue = getNestedValue(config_js_1.DEFAULT_CONFIG, key);
|
|
192
|
+
if (defaultValue === undefined) {
|
|
193
|
+
console.error(`Unknown config key: ${key}`);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
const configPath = (0, config_js_1.getConfigPath)();
|
|
197
|
+
if (!(0, fs_1.existsSync)(configPath)) {
|
|
198
|
+
console.error('No config file found. Run "labgate init" first.');
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
const rawText = (0, fs_1.readFileSync)(configPath, 'utf-8');
|
|
203
|
+
const stripped = stripJsonComments(rawText).trim();
|
|
204
|
+
const obj = stripped ? JSON.parse(stripped) : {};
|
|
205
|
+
const deleted = deleteNestedValue(obj, key);
|
|
206
|
+
if (!deleted) {
|
|
207
|
+
console.log(`No user override for ${key} (already using defaults / policy).`);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
(0, fs_1.writeFileSync)(configPath, JSON.stringify(obj, null, 2) + '\n', { encoding: 'utf-8', mode: config_js_1.PRIVATE_FILE_MODE });
|
|
211
|
+
(0, config_js_1.ensurePrivateFile)(configPath);
|
|
212
|
+
console.log(`Reset ${key} (unset from user config).`);
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
console.error(`Error: ${err.message}`);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
184
219
|
configCmd
|
|
185
220
|
.command('show')
|
|
186
221
|
.description('Show the full resolved config')
|
|
@@ -528,39 +563,178 @@ slurmCmd
|
|
|
528
563
|
await main();
|
|
529
564
|
});
|
|
530
565
|
// ── labgate license ──────────────────────────────────────
|
|
531
|
-
program
|
|
566
|
+
const licenseCmd = program
|
|
532
567
|
.command('license')
|
|
533
|
-
.description('
|
|
568
|
+
.description('Manage enterprise license');
|
|
569
|
+
licenseCmd
|
|
534
570
|
.action(async () => {
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
571
|
+
await printLicenseStatus();
|
|
572
|
+
});
|
|
573
|
+
licenseCmd
|
|
574
|
+
.command('install')
|
|
575
|
+
.description('Install an enterprise license key (raw key or file path)')
|
|
576
|
+
.argument('<keyOrFile>', 'License key string or path to a file containing the key')
|
|
577
|
+
.option('--system', 'Install to /etc/labgate/license.key')
|
|
578
|
+
.option('--user', `Install to ${(0, path_1.join)((0, os_1.homedir)(), '.labgate', 'license.key')}`)
|
|
579
|
+
.option('--path <path>', 'Install to a custom path')
|
|
580
|
+
.option('--overwrite', 'Overwrite existing target file')
|
|
581
|
+
.action(async (keyOrFile, opts) => {
|
|
582
|
+
try {
|
|
583
|
+
const sourcePath = (0, path_1.resolve)(keyOrFile);
|
|
584
|
+
const raw = (0, fs_1.existsSync)(sourcePath) ? (0, fs_1.readFileSync)(sourcePath, 'utf-8') : keyOrFile;
|
|
585
|
+
const targetPath = resolveLicenseTargetPath(opts);
|
|
586
|
+
await installLicenseKeyAtPath(raw, targetPath, !!opts.overwrite);
|
|
587
|
+
console.log(`License installed: ${targetPath}`);
|
|
588
|
+
await printLicenseStatus(targetPath);
|
|
544
589
|
}
|
|
545
|
-
|
|
546
|
-
console.
|
|
590
|
+
catch (err) {
|
|
591
|
+
console.error(`Error: ${err.message}`);
|
|
592
|
+
process.exit(1);
|
|
547
593
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
594
|
+
});
|
|
595
|
+
// ── labgate register ─────────────────────────────────────
|
|
596
|
+
program
|
|
597
|
+
.command('register')
|
|
598
|
+
.description('Activate enterprise license with an activation key and install it locally')
|
|
599
|
+
.argument('<activationKey>', 'Activation key issued by LabGate')
|
|
600
|
+
.option('--server <url>', `Activation endpoint URL override (default: ${DEFAULT_ACTIVATION_URL})`)
|
|
601
|
+
.option('--token <token>', 'Optional bearer token for activation API (or LABGATE_ACTIVATION_TOKEN)')
|
|
602
|
+
.option('--timeout <ms>', 'Activation request timeout in milliseconds', '15000')
|
|
603
|
+
.option('--system', 'Install to /etc/labgate/license.key')
|
|
604
|
+
.option('--user', `Install to ${(0, path_1.join)((0, os_1.homedir)(), '.labgate', 'license.key')}`)
|
|
605
|
+
.option('--path <path>', 'Install to a custom path')
|
|
606
|
+
.option('--overwrite', 'Overwrite existing target file')
|
|
607
|
+
.action(async (activationKey, opts) => {
|
|
608
|
+
try {
|
|
609
|
+
const serverUrl = (opts.server || process.env.LABGATE_ACTIVATION_URL || DEFAULT_ACTIVATION_URL).trim();
|
|
610
|
+
const key = (activationKey || '').trim();
|
|
611
|
+
if (!key) {
|
|
612
|
+
console.error('Error: activation key is empty.');
|
|
613
|
+
process.exit(1);
|
|
553
614
|
}
|
|
554
|
-
|
|
615
|
+
const timeoutMs = parseInt(opts.timeout, 10);
|
|
616
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 1000 || timeoutMs > 300000) {
|
|
617
|
+
console.error('Error: --timeout must be an integer between 1000 and 300000.');
|
|
618
|
+
process.exit(1);
|
|
619
|
+
}
|
|
620
|
+
const token = (opts.token || process.env.LABGATE_ACTIVATION_TOKEN || '').trim() || undefined;
|
|
621
|
+
const targetPath = resolveLicenseTargetPath(opts);
|
|
622
|
+
console.log(`Activating enterprise license via ${serverUrl} ...`);
|
|
623
|
+
const activation = await activateEnterpriseLicense({
|
|
624
|
+
serverUrl,
|
|
625
|
+
activationKey: key,
|
|
626
|
+
timeoutMs,
|
|
627
|
+
token,
|
|
628
|
+
});
|
|
629
|
+
await installLicenseKeyAtPath(activation.licenseKey, targetPath, !!opts.overwrite);
|
|
630
|
+
console.log(`License installed: ${targetPath}`);
|
|
631
|
+
await printLicenseStatus(targetPath);
|
|
632
|
+
if (activation.message) {
|
|
633
|
+
console.log(`Activation: ${activation.message}`);
|
|
634
|
+
}
|
|
635
|
+
console.log('Next: run "labgate ui" and open Admin > Policy to enforce institution settings.');
|
|
555
636
|
}
|
|
556
|
-
|
|
557
|
-
console.
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
637
|
+
catch (err) {
|
|
638
|
+
console.error(`Error: ${err.message}`);
|
|
639
|
+
process.exit(1);
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
// ── labgate policy ───────────────────────────────────────
|
|
643
|
+
const policyCmd = program
|
|
644
|
+
.command('policy')
|
|
645
|
+
.description('Manage enterprise admin policy');
|
|
646
|
+
policyCmd
|
|
647
|
+
.command('validate')
|
|
648
|
+
.description('Validate a policy JSON file')
|
|
649
|
+
.argument('[file]', 'Path to policy JSON (defaults to system policy path)')
|
|
650
|
+
.action(async (file) => {
|
|
651
|
+
try {
|
|
652
|
+
const { getSystemPolicyPath, validatePolicy } = await import('./lib/policy.js');
|
|
653
|
+
const policyPath = file ? (0, path_1.resolve)(file) : getSystemPolicyPath();
|
|
654
|
+
if (!(0, fs_1.existsSync)(policyPath)) {
|
|
655
|
+
console.error(`Error: policy file not found: ${policyPath}`);
|
|
656
|
+
process.exit(1);
|
|
657
|
+
}
|
|
658
|
+
const rawText = (0, fs_1.readFileSync)(policyPath, 'utf-8');
|
|
659
|
+
const stripped = stripJsonComments(rawText);
|
|
660
|
+
const parsed = JSON.parse(stripped);
|
|
661
|
+
const errors = validatePolicy(parsed);
|
|
662
|
+
if (errors.length > 0) {
|
|
663
|
+
console.error(`Policy INVALID: ${policyPath}`);
|
|
664
|
+
for (const e of errors) {
|
|
665
|
+
console.error(` - ${e}`);
|
|
666
|
+
}
|
|
667
|
+
process.exit(1);
|
|
668
|
+
}
|
|
669
|
+
console.log(`Policy VALID: ${policyPath}`);
|
|
670
|
+
}
|
|
671
|
+
catch (err) {
|
|
672
|
+
console.error(`Error: ${err.message}`);
|
|
673
|
+
process.exit(1);
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
policyCmd
|
|
677
|
+
.command('init')
|
|
678
|
+
.description('Create an enterprise policy template')
|
|
679
|
+
.option('--path <path>', 'Write policy to a custom path')
|
|
680
|
+
.option('--institution <name>', 'Institution name shown in UI')
|
|
681
|
+
.option('--admin <username>', 'Admin username (repeatable)', collectStringOption, [])
|
|
682
|
+
.option('--runtime <runtime>', 'Forced runtime (auto|apptainer|podman)', 'apptainer')
|
|
683
|
+
.option('--force', 'Overwrite existing policy file')
|
|
684
|
+
.action(async (opts) => {
|
|
685
|
+
try {
|
|
686
|
+
const policyMod = await import('./lib/policy.js');
|
|
687
|
+
const { getSystemPolicyPath, savePolicy, validatePolicy } = policyMod;
|
|
688
|
+
const targetPath = opts.path ? (0, path_1.resolve)(opts.path) : getSystemPolicyPath();
|
|
689
|
+
if ((0, fs_1.existsSync)(targetPath) && !opts.force) {
|
|
690
|
+
console.error(`Error: policy already exists: ${targetPath}`);
|
|
691
|
+
console.error('Use --force to overwrite.');
|
|
692
|
+
process.exit(1);
|
|
693
|
+
}
|
|
694
|
+
const runtime = (opts.runtime || '').trim().toLowerCase();
|
|
695
|
+
if (!['auto', 'apptainer', 'podman'].includes(runtime)) {
|
|
696
|
+
console.error('Error: --runtime must be one of: auto, apptainer, podman');
|
|
697
|
+
process.exit(1);
|
|
698
|
+
}
|
|
699
|
+
const licenseStatus = await (async () => {
|
|
700
|
+
const { validateLicense } = await import('./lib/license.js');
|
|
701
|
+
return validateLicense();
|
|
702
|
+
})();
|
|
703
|
+
const institution = (opts.institution || '').trim()
|
|
704
|
+
|| licenseStatus.payload?.institution
|
|
705
|
+
|| 'Your Institution';
|
|
706
|
+
const admins = (opts.admin || []).map(a => String(a).trim()).filter(Boolean);
|
|
707
|
+
const bootstrapAdmin = (0, os_1.userInfo)().username;
|
|
708
|
+
if (!admins.includes(bootstrapAdmin))
|
|
709
|
+
admins.unshift(bootstrapAdmin);
|
|
710
|
+
const policy = {
|
|
711
|
+
version: 1,
|
|
712
|
+
institution,
|
|
713
|
+
admins: { usernames: [...new Set(admins)] },
|
|
714
|
+
force: {
|
|
715
|
+
runtime: runtime,
|
|
716
|
+
},
|
|
717
|
+
constraints: {},
|
|
718
|
+
};
|
|
719
|
+
const errors = validatePolicy(policy);
|
|
720
|
+
if (errors.length > 0) {
|
|
721
|
+
console.error('Generated policy is invalid:');
|
|
722
|
+
for (const e of errors) {
|
|
723
|
+
console.error(` - ${e}`);
|
|
724
|
+
}
|
|
725
|
+
process.exit(1);
|
|
726
|
+
}
|
|
727
|
+
const dirMode = targetPath.startsWith('/etc/') ? 0o755 : 0o700;
|
|
728
|
+
(0, fs_1.mkdirSync)((0, path_1.dirname)(targetPath), { recursive: true, mode: dirMode });
|
|
729
|
+
savePolicy(policy, targetPath);
|
|
730
|
+
console.log(`Policy written: ${targetPath}`);
|
|
731
|
+
console.log(`Institution: ${institution}`);
|
|
732
|
+
console.log(`Admins: ${policy.admins.usernames.join(', ')}`);
|
|
733
|
+
console.log(`Forced runtime: ${runtime}`);
|
|
734
|
+
}
|
|
735
|
+
catch (err) {
|
|
736
|
+
console.error(`Error: ${err.message}`);
|
|
737
|
+
process.exit(1);
|
|
564
738
|
}
|
|
565
739
|
});
|
|
566
740
|
// ── Dot-notation helpers ──────────────────────────────────
|
|
@@ -585,6 +759,181 @@ function setNestedValue(obj, key, value) {
|
|
|
585
759
|
}
|
|
586
760
|
current[parts[parts.length - 1]] = value;
|
|
587
761
|
}
|
|
762
|
+
function deleteNestedValue(obj, key) {
|
|
763
|
+
const parts = key.split('.').filter(Boolean);
|
|
764
|
+
if (parts.length === 0)
|
|
765
|
+
return false;
|
|
766
|
+
const stack = [obj];
|
|
767
|
+
let current = obj;
|
|
768
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
769
|
+
if (current == null || typeof current !== 'object')
|
|
770
|
+
return false;
|
|
771
|
+
current = current[parts[i]];
|
|
772
|
+
stack.push(current);
|
|
773
|
+
}
|
|
774
|
+
const parent = stack[stack.length - 1];
|
|
775
|
+
const last = parts[parts.length - 1];
|
|
776
|
+
if (parent == null || typeof parent !== 'object')
|
|
777
|
+
return false;
|
|
778
|
+
if (!Object.prototype.hasOwnProperty.call(parent, last))
|
|
779
|
+
return false;
|
|
780
|
+
delete parent[last];
|
|
781
|
+
// Prune empty parent objects (e.g. resetting "network.mode" should remove "network" if empty)
|
|
782
|
+
for (let i = stack.length - 1; i > 0; i--) {
|
|
783
|
+
const node = stack[i];
|
|
784
|
+
if (node == null || typeof node !== 'object')
|
|
785
|
+
break;
|
|
786
|
+
if (Array.isArray(node))
|
|
787
|
+
break;
|
|
788
|
+
if (Object.keys(node).length > 0)
|
|
789
|
+
break;
|
|
790
|
+
const parentNode = stack[i - 1];
|
|
791
|
+
const prop = parts[i - 1];
|
|
792
|
+
if (parentNode && typeof parentNode === 'object') {
|
|
793
|
+
delete parentNode[prop];
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return true;
|
|
797
|
+
}
|
|
798
|
+
function stripJsonComments(rawText) {
|
|
799
|
+
return rawText
|
|
800
|
+
.split('\n')
|
|
801
|
+
.filter(line => !line.trimStart().startsWith('//'))
|
|
802
|
+
.join('\n');
|
|
803
|
+
}
|
|
804
|
+
function collectStringOption(value, previous) {
|
|
805
|
+
previous.push(value);
|
|
806
|
+
return previous;
|
|
807
|
+
}
|
|
808
|
+
function resolveLicenseTargetPath(opts) {
|
|
809
|
+
const explicitTargets = [opts.system, opts.user, !!opts.path].filter(Boolean).length;
|
|
810
|
+
if (explicitTargets > 1) {
|
|
811
|
+
throw new Error('use only one of --system, --user, or --path.');
|
|
812
|
+
}
|
|
813
|
+
const isRoot = typeof process.getuid === 'function' && process.getuid() === 0;
|
|
814
|
+
return opts.path
|
|
815
|
+
? (0, path_1.resolve)(opts.path)
|
|
816
|
+
: opts.system
|
|
817
|
+
? '/etc/labgate/license.key'
|
|
818
|
+
: opts.user
|
|
819
|
+
? (0, path_1.join)((0, os_1.homedir)(), '.labgate', 'license.key')
|
|
820
|
+
: (isRoot ? '/etc/labgate/license.key' : (0, path_1.join)((0, os_1.homedir)(), '.labgate', 'license.key'));
|
|
821
|
+
}
|
|
822
|
+
async function installLicenseKeyAtPath(rawLicenseKey, targetPath, overwrite) {
|
|
823
|
+
const key = (rawLicenseKey || '').trim();
|
|
824
|
+
if (!key) {
|
|
825
|
+
throw new Error('license key is empty.');
|
|
826
|
+
}
|
|
827
|
+
if ((0, fs_1.existsSync)(targetPath) && !overwrite) {
|
|
828
|
+
throw new Error(`target already exists: ${targetPath}. Use --overwrite to replace it.`);
|
|
829
|
+
}
|
|
830
|
+
const targetDir = (0, path_1.dirname)(targetPath);
|
|
831
|
+
const dirMode = targetPath.startsWith('/etc/') ? 0o755 : 0o700;
|
|
832
|
+
(0, fs_1.mkdirSync)(targetDir, { recursive: true, mode: dirMode });
|
|
833
|
+
(0, fs_1.writeFileSync)(targetPath, key + '\n', { encoding: 'utf-8', mode: config_js_1.PRIVATE_FILE_MODE });
|
|
834
|
+
(0, config_js_1.ensurePrivateFile)(targetPath);
|
|
835
|
+
const previousLicensePath = process.env.LABGATE_LICENSE_PATH;
|
|
836
|
+
process.env.LABGATE_LICENSE_PATH = targetPath;
|
|
837
|
+
const { validateLicense } = await import('./lib/license.js');
|
|
838
|
+
const status = validateLicense();
|
|
839
|
+
if (previousLicensePath === undefined) {
|
|
840
|
+
delete process.env.LABGATE_LICENSE_PATH;
|
|
841
|
+
}
|
|
842
|
+
else {
|
|
843
|
+
process.env.LABGATE_LICENSE_PATH = previousLicensePath;
|
|
844
|
+
}
|
|
845
|
+
if (!status.valid) {
|
|
846
|
+
try {
|
|
847
|
+
(0, fs_1.unlinkSync)(targetPath);
|
|
848
|
+
}
|
|
849
|
+
catch { /* best effort cleanup */ }
|
|
850
|
+
throw new Error(`installed key is invalid: ${status.error ?? 'unknown error'}`);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
async function activateEnterpriseLicense(input) {
|
|
854
|
+
let parsedUrl;
|
|
855
|
+
try {
|
|
856
|
+
parsedUrl = new URL(input.serverUrl);
|
|
857
|
+
}
|
|
858
|
+
catch {
|
|
859
|
+
throw new Error(`invalid activation URL: ${input.serverUrl}`);
|
|
860
|
+
}
|
|
861
|
+
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
|
|
862
|
+
throw new Error(`invalid activation URL protocol: ${parsedUrl.protocol}`);
|
|
863
|
+
}
|
|
864
|
+
const headers = {
|
|
865
|
+
'Content-Type': 'application/json',
|
|
866
|
+
};
|
|
867
|
+
if (input.token) {
|
|
868
|
+
headers.authorization = `Bearer ${input.token}`;
|
|
869
|
+
}
|
|
870
|
+
const payload = {
|
|
871
|
+
activation_key: input.activationKey,
|
|
872
|
+
client: {
|
|
873
|
+
product: 'labgate',
|
|
874
|
+
version: PKG_VERSION,
|
|
875
|
+
hostname: (0, os_1.hostname)(),
|
|
876
|
+
username: (0, os_1.userInfo)().username,
|
|
877
|
+
platform: process.platform,
|
|
878
|
+
arch: process.arch,
|
|
879
|
+
},
|
|
880
|
+
};
|
|
881
|
+
const controller = new AbortController();
|
|
882
|
+
const timer = setTimeout(() => controller.abort(), input.timeoutMs);
|
|
883
|
+
let response;
|
|
884
|
+
try {
|
|
885
|
+
response = await fetch(parsedUrl.toString(), {
|
|
886
|
+
method: 'POST',
|
|
887
|
+
headers,
|
|
888
|
+
body: JSON.stringify(payload),
|
|
889
|
+
signal: controller.signal,
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
catch (err) {
|
|
893
|
+
if (err?.name === 'AbortError') {
|
|
894
|
+
throw new Error(`activation request timed out after ${input.timeoutMs}ms`);
|
|
895
|
+
}
|
|
896
|
+
throw new Error(`activation request failed: ${err?.message ?? String(err)}`);
|
|
897
|
+
}
|
|
898
|
+
finally {
|
|
899
|
+
clearTimeout(timer);
|
|
900
|
+
}
|
|
901
|
+
const raw = await response.text();
|
|
902
|
+
let body = {};
|
|
903
|
+
if (raw.trim()) {
|
|
904
|
+
try {
|
|
905
|
+
body = JSON.parse(raw);
|
|
906
|
+
}
|
|
907
|
+
catch {
|
|
908
|
+
if (!response.ok) {
|
|
909
|
+
throw new Error(`activation failed: HTTP ${response.status} ${response.statusText}`);
|
|
910
|
+
}
|
|
911
|
+
throw new Error('activation server returned invalid JSON');
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
if (!response.ok) {
|
|
915
|
+
const detail = pickString(body.error, body.message, body.detail) || `HTTP ${response.status} ${response.statusText}`;
|
|
916
|
+
throw new Error(`activation failed: ${detail}`);
|
|
917
|
+
}
|
|
918
|
+
const licenseKey = pickString(body.license_key, body.licenseKey, body.key);
|
|
919
|
+
if (!licenseKey) {
|
|
920
|
+
throw new Error('activation response missing license key');
|
|
921
|
+
}
|
|
922
|
+
return {
|
|
923
|
+
licenseKey,
|
|
924
|
+
message: pickString(body.message),
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
function pickString(...values) {
|
|
928
|
+
for (const value of values) {
|
|
929
|
+
if (typeof value === 'string') {
|
|
930
|
+
const trimmed = value.trim();
|
|
931
|
+
if (trimmed)
|
|
932
|
+
return trimmed;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
return undefined;
|
|
936
|
+
}
|
|
588
937
|
async function runAgent(agent, workdir, opts) {
|
|
589
938
|
// Resolve workdir
|
|
590
939
|
const resolved = (0, path_1.resolve)(workdir);
|
|
@@ -601,6 +950,11 @@ async function runAgent(agent, workdir, opts) {
|
|
|
601
950
|
// Use effective config (merges user config with admin policy if enterprise)
|
|
602
951
|
const effective = (0, config_js_1.loadEffectiveConfig)();
|
|
603
952
|
const config = effective.config;
|
|
953
|
+
if (config.network.mode === 'none' && (agent === 'claude' || agent === 'codex')) {
|
|
954
|
+
log.error(`network.mode=none is not supported for ${agent}. ` +
|
|
955
|
+
'Set network.mode to "host" or "filtered".');
|
|
956
|
+
process.exit(1);
|
|
957
|
+
}
|
|
604
958
|
if (verbose) {
|
|
605
959
|
log.header('Config');
|
|
606
960
|
const rows = [
|
|
@@ -646,6 +1000,50 @@ async function runAgent(agent, workdir, opts) {
|
|
|
646
1000
|
sharedAuditDir: effective.sharedAuditDir,
|
|
647
1001
|
});
|
|
648
1002
|
}
|
|
1003
|
+
async function printLicenseStatus(licensePathOverride) {
|
|
1004
|
+
const previousLicensePath = process.env.LABGATE_LICENSE_PATH;
|
|
1005
|
+
if (licensePathOverride) {
|
|
1006
|
+
process.env.LABGATE_LICENSE_PATH = licensePathOverride;
|
|
1007
|
+
}
|
|
1008
|
+
const { validateLicense } = await import('./lib/license.js');
|
|
1009
|
+
const status = validateLicense();
|
|
1010
|
+
if (licensePathOverride) {
|
|
1011
|
+
if (previousLicensePath === undefined) {
|
|
1012
|
+
delete process.env.LABGATE_LICENSE_PATH;
|
|
1013
|
+
}
|
|
1014
|
+
else {
|
|
1015
|
+
process.env.LABGATE_LICENSE_PATH = previousLicensePath;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
if (!status.filePath && !status.error) {
|
|
1019
|
+
console.log('No enterprise license found.');
|
|
1020
|
+
console.log('License file locations (checked in order):');
|
|
1021
|
+
console.log(' 1. $LABGATE_LICENSE_PATH (env var)');
|
|
1022
|
+
console.log(' 2. /etc/labgate/license.key');
|
|
1023
|
+
console.log(' 3. ~/.labgate/license.key');
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
if (status.filePath) {
|
|
1027
|
+
console.log(`License file: ${status.filePath}`);
|
|
1028
|
+
}
|
|
1029
|
+
if (status.error) {
|
|
1030
|
+
console.log(`Status: INVALID — ${status.error}`);
|
|
1031
|
+
if (status.payload) {
|
|
1032
|
+
console.log(`Institution: ${status.payload.institution}`);
|
|
1033
|
+
console.log(`Expires: ${status.payload.expires}`);
|
|
1034
|
+
}
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
if (status.valid && status.payload) {
|
|
1038
|
+
console.log(`Status: VALID`);
|
|
1039
|
+
console.log(`Institution: ${status.payload.institution}`);
|
|
1040
|
+
console.log(`Tier: ${status.payload.tier}`);
|
|
1041
|
+
console.log(`License ID: ${status.payload.id}`);
|
|
1042
|
+
console.log(`Issued: ${status.payload.issued}`);
|
|
1043
|
+
console.log(`Expires: ${status.payload.expires}`);
|
|
1044
|
+
console.log(`Days remaining: ${status.daysRemaining}`);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
649
1047
|
// Commander uses -V for version by default. Keep backward compatibility and
|
|
650
1048
|
// provide lowercase -v as a user-friendly alias.
|
|
651
1049
|
if (process.argv.length === 3 && process.argv[2] === '-v') {
|