@pellux/goodvibes-tui 0.20.1 → 0.20.3
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/CHANGELOG.md +13 -0
- package/README.md +7 -7
- package/package.json +1 -1
- package/src/input/commands/local-setup-review.ts +4 -6
- package/src/input/commands/local-setup.ts +1 -1
- package/src/input/commands/platform-sandbox-qemu.ts +187 -19
- package/src/input/commands/platform-sandbox-runtime.ts +4 -4
- package/src/runtime/sandbox-qemu-templates.ts +15 -0
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ All notable changes to GoodVibes TUI.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [0.20.3] — 2026-05-13
|
|
8
|
+
|
|
9
|
+
### Fixes
|
|
10
|
+
- Changed `/sandbox qemu bootstrap` to run image build and guest provisioning asynchronously so the TUI can clear the prompt, render progress, and remain responsive while long QEMU setup steps run.
|
|
11
|
+
- Updated generated QEMU wrappers to fail fast when the QEMU process exits before SSH is available, including a tail of the QEMU log for port conflicts and startup failures.
|
|
12
|
+
|
|
13
|
+
## [0.20.2] — 2026-05-12
|
|
14
|
+
|
|
15
|
+
### Changes
|
|
16
|
+
- Changed QEMU sandbox setup/bootstrap defaults to use `~/.goodvibes/tui/sandbox` instead of writing setup bundles into the current project.
|
|
17
|
+
- Made `/sandbox qemu bootstrap` perform the image build and guest runtime provisioning by default, with `--scaffold-only`, `--no-build`, and `--no-provision` escape hatches for manual workflows.
|
|
18
|
+
- Updated sandbox review guidance and QEMU documentation so first-run setup no longer points users at project-local sandbox paths.
|
|
19
|
+
|
|
7
20
|
## [0.20.1] — 2026-05-12
|
|
8
21
|
|
|
9
22
|
### Changes
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](https://github.com/mgd34msu/goodvibes-tui)
|
|
6
6
|
|
|
7
7
|
A terminal-native AI coding, operations, automation, knowledge, and integration console with a typed runtime, omnichannel surfaces, structured memory/knowledge, and a raw ANSI renderer.
|
|
8
8
|
|
|
@@ -624,7 +624,7 @@ Isolation controls:
|
|
|
624
624
|
|
|
625
625
|
The QEMU path includes:
|
|
626
626
|
|
|
627
|
-
- setup bundle generation
|
|
627
|
+
- setup bundle generation under the user GoodVibes data directory by default
|
|
628
628
|
- first-run bootstrap scaffolding
|
|
629
629
|
- Debian cloud-image download, mutable qcow2 clone, resize, and NoCloud ISO creation through the generated `create-image.sh`
|
|
630
630
|
- host-side wrapper generation
|
|
@@ -644,8 +644,8 @@ Key commands:
|
|
|
644
644
|
- `/sandbox recommend`
|
|
645
645
|
- `/sandbox doctor`
|
|
646
646
|
- `/sandbox probe`
|
|
647
|
-
- `/sandbox qemu setup
|
|
648
|
-
- `/sandbox qemu bootstrap
|
|
647
|
+
- `/sandbox qemu setup [dir]`
|
|
648
|
+
- `/sandbox qemu bootstrap [dir] [size-gb] [--scaffold-only|--no-build|--no-provision]`
|
|
649
649
|
- `/sandbox qemu inspect-setup <manifest>`
|
|
650
650
|
- `/sandbox qemu apply-setup <manifest>`
|
|
651
651
|
- `/sandbox session ...`
|
|
@@ -655,13 +655,13 @@ Key commands:
|
|
|
655
655
|
Typical first-run path:
|
|
656
656
|
|
|
657
657
|
```sh
|
|
658
|
-
/sandbox qemu bootstrap
|
|
659
|
-
.goodvibes/tui/sandbox/create-image.sh .goodvibes/tui/sandbox/goodvibes-sandbox.qcow2 20G
|
|
660
|
-
GV_SANDBOX_SYNC_WORKSPACE=0 GV_SANDBOX_WRAPPER_MODE=launch-qemu-ssh .goodvibes/tui/sandbox/qemu-wrapper.sh bash -s < .goodvibes/tui/sandbox/guest-bootstrap.sh
|
|
658
|
+
/sandbox qemu bootstrap
|
|
661
659
|
/sandbox doctor
|
|
662
660
|
/sandbox guest-test eval-py
|
|
663
661
|
```
|
|
664
662
|
|
|
663
|
+
By default, QEMU setup files, the mutable qcow2 image, SSH keys, logs, and runtime state live in `~/.goodvibes/tui/sandbox`, not in the current project. Pass an explicit directory only when you intentionally want a non-default bundle location. Use `/sandbox qemu setup` or `/sandbox qemu bootstrap --scaffold-only` if you want to generate/review the bundle without building the image or provisioning guest runtimes.
|
|
664
|
+
|
|
665
665
|
See [QEMU sandbox bootstrapping](docs/qemu-sandbox.md) for host prerequisites, generated files, first-boot behavior, guest runtime packages, and troubleshooting logs.
|
|
666
666
|
|
|
667
667
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-tui",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.3",
|
|
4
4
|
"description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/main.ts",
|
|
@@ -154,14 +154,12 @@ export function renderSetupSandboxReview(ctx: CommandContext, snapshot: SetupRev
|
|
|
154
154
|
' next:',
|
|
155
155
|
];
|
|
156
156
|
if (backend === 'local') {
|
|
157
|
-
lines.push(' /sandbox qemu bootstrap
|
|
158
|
-
lines.push('
|
|
159
|
-
lines.push(' GV_SANDBOX_SYNC_WORKSPACE=0 GV_SANDBOX_WRAPPER_MODE=launch-qemu-ssh .goodvibes/tui/sandbox/qemu-wrapper.sh bash -s < .goodvibes/tui/sandbox/guest-bootstrap.sh');
|
|
157
|
+
lines.push(' /sandbox qemu bootstrap');
|
|
158
|
+
lines.push(' default bundle: ~/.goodvibes/tui/sandbox');
|
|
160
159
|
lines.push(' /sandbox doctor');
|
|
161
160
|
} else if (!image || !wrapper) {
|
|
162
|
-
lines.push(' /sandbox qemu
|
|
163
|
-
lines.push('
|
|
164
|
-
lines.push(' GV_SANDBOX_SYNC_WORKSPACE=0 GV_SANDBOX_WRAPPER_MODE=launch-qemu-ssh .goodvibes/tui/sandbox/qemu-wrapper.sh bash -s < .goodvibes/tui/sandbox/guest-bootstrap.sh');
|
|
161
|
+
lines.push(' /sandbox qemu bootstrap');
|
|
162
|
+
lines.push(' default bundle: ~/.goodvibes/tui/sandbox');
|
|
165
163
|
lines.push(' /sandbox doctor');
|
|
166
164
|
} else {
|
|
167
165
|
lines.push(' /sandbox guest-test eval-js');
|
|
@@ -73,7 +73,7 @@ export function registerLocalSetupCommands(registry: CommandRegistry): void {
|
|
|
73
73
|
...(`${ctx.platform.configManager.get('sandbox.vmBackend')}` === 'qemu' && !String(ctx.platform.configManager.get('sandbox.qemuImagePath')).trim()
|
|
74
74
|
? [' [WARN] sandbox: qemu backend selected without qemuImagePath'] : []),
|
|
75
75
|
...(`${ctx.platform.configManager.get('sandbox.vmBackend')}` === 'qemu' && !String(ctx.platform.configManager.get('sandbox.qemuExecWrapper')).trim()
|
|
76
|
-
? [' [WARN] sandbox: qemu backend selected without qemuExecWrapper', ' next: /sandbox
|
|
76
|
+
? [' [WARN] sandbox: qemu backend selected without qemuExecWrapper', ' next: /sandbox qemu setup'] : []),
|
|
77
77
|
...(`${ctx.platform.configManager.get('sandbox.vmBackend')}` === 'qemu' && String(ctx.platform.configManager.get('sandbox.qemuExecWrapper')).trim()
|
|
78
78
|
? [' [INFO] sandbox: wrapper bridge can be validated with GV_SANDBOX_WRAPPER_MODE=host-exec before wiring a real guest transport'] : []),
|
|
79
79
|
...(snapshot.serviceIssues.length > 0
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
3
|
import type { CommandContext } from '../command-registry.ts';
|
|
4
4
|
import {
|
|
5
5
|
applySandboxQemuSetupManifest,
|
|
@@ -11,6 +11,141 @@ import {
|
|
|
11
11
|
import { requireShellPaths } from './runtime-services.ts';
|
|
12
12
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
13
13
|
|
|
14
|
+
const DEFAULT_QEMU_SIZE_GB = 20;
|
|
15
|
+
|
|
16
|
+
interface ParsedQemuSetupArgs {
|
|
17
|
+
readonly directory: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ParsedQemuBootstrapArgs extends ParsedQemuSetupArgs {
|
|
21
|
+
readonly sizeGb: number;
|
|
22
|
+
readonly buildImage: boolean;
|
|
23
|
+
readonly provisionGuest: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function defaultQemuSandboxDirectory(shellPaths: ReturnType<typeof requireShellPaths>): string {
|
|
27
|
+
return shellPaths.resolveUserPath('tui', 'sandbox');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parseSetupArgs(args: string[], shellPaths: ReturnType<typeof requireShellPaths>): ParsedQemuSetupArgs {
|
|
31
|
+
const directory = args[2] ?? defaultQemuSandboxDirectory(shellPaths);
|
|
32
|
+
return {
|
|
33
|
+
directory,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseBootstrapArgs(args: string[], shellPaths: ReturnType<typeof requireShellPaths>): ParsedQemuBootstrapArgs {
|
|
38
|
+
let directory = defaultQemuSandboxDirectory(shellPaths);
|
|
39
|
+
let sizeGb = DEFAULT_QEMU_SIZE_GB;
|
|
40
|
+
let buildImage = true;
|
|
41
|
+
let provisionGuest = true;
|
|
42
|
+
for (const arg of args.slice(2)) {
|
|
43
|
+
if (arg === '--scaffold-only') {
|
|
44
|
+
buildImage = false;
|
|
45
|
+
provisionGuest = false;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (arg === '--no-build') {
|
|
49
|
+
buildImage = false;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (arg === '--no-provision') {
|
|
53
|
+
provisionGuest = false;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (/^\d+$/.test(arg)) {
|
|
57
|
+
sizeGb = Number.parseInt(arg, 10);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
directory = arg;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
directory,
|
|
64
|
+
sizeGb,
|
|
65
|
+
buildImage,
|
|
66
|
+
provisionGuest,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function tailText(value: string, maxLines = 40): string {
|
|
71
|
+
const lines = value.trim().split(/\r?\n/).filter(Boolean);
|
|
72
|
+
return lines.slice(Math.max(0, lines.length - maxLines)).join('\n');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function appendBounded(current: string, chunk: Buffer | string, maxLength = 40_000): string {
|
|
76
|
+
const next = current + chunk.toString();
|
|
77
|
+
return next.length > maxLength ? next.slice(next.length - maxLength) : next;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function printProgress(ctx: CommandContext, message: string): void {
|
|
81
|
+
ctx.print(message);
|
|
82
|
+
ctx.renderRequest();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function runQemuBootstrapStep(
|
|
86
|
+
label: string,
|
|
87
|
+
command: string,
|
|
88
|
+
args: readonly string[],
|
|
89
|
+
options: {
|
|
90
|
+
readonly cwd: string;
|
|
91
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
92
|
+
readonly input?: string;
|
|
93
|
+
readonly timeoutMs: number;
|
|
94
|
+
},
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
let stdout = '';
|
|
97
|
+
let stderr = '';
|
|
98
|
+
let timedOut = false;
|
|
99
|
+
const child = spawn(command, [...args], {
|
|
100
|
+
cwd: options.cwd,
|
|
101
|
+
env: { ...process.env, ...options.env },
|
|
102
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
103
|
+
windowsHide: true,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
child.stdout?.on('data', (chunk) => {
|
|
107
|
+
stdout = appendBounded(stdout, chunk);
|
|
108
|
+
});
|
|
109
|
+
child.stderr?.on('data', (chunk) => {
|
|
110
|
+
stderr = appendBounded(stderr, chunk);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (options.input !== undefined) {
|
|
114
|
+
child.stdin?.end(options.input);
|
|
115
|
+
} else {
|
|
116
|
+
child.stdin?.end();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let killTimer: NodeJS.Timeout | undefined;
|
|
120
|
+
const timeout = setTimeout(() => {
|
|
121
|
+
timedOut = true;
|
|
122
|
+
child.kill('SIGTERM');
|
|
123
|
+
killTimer = setTimeout(() => child.kill('SIGKILL'), 5_000);
|
|
124
|
+
}, options.timeoutMs);
|
|
125
|
+
|
|
126
|
+
const result = await new Promise<{ status: number | null; signal: NodeJS.Signals | null }>((resolve, reject) => {
|
|
127
|
+
child.once('error', reject);
|
|
128
|
+
child.once('close', (status, signal) => resolve({ status, signal }));
|
|
129
|
+
}).finally(() => {
|
|
130
|
+
clearTimeout(timeout);
|
|
131
|
+
if (killTimer) clearTimeout(killTimer);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (result.status === 0) return;
|
|
135
|
+
const stderrTail = tailText(stderr);
|
|
136
|
+
const stdoutTail = tailText(stdout);
|
|
137
|
+
const reason = timedOut
|
|
138
|
+
? `timed out after ${Math.round(options.timeoutMs / 1000)}s`
|
|
139
|
+
: result.signal
|
|
140
|
+
? `signal ${result.signal}`
|
|
141
|
+
: `exit ${result.status ?? 'unknown'}`;
|
|
142
|
+
throw new Error([
|
|
143
|
+
`${label} failed (${reason}).`,
|
|
144
|
+
stderrTail ? `stderr:\n${stderrTail}` : '',
|
|
145
|
+
stdoutTail ? `stdout:\n${stdoutTail}` : '',
|
|
146
|
+
].filter(Boolean).join('\n'));
|
|
147
|
+
}
|
|
148
|
+
|
|
14
149
|
export async function handleSandboxQemuCommand(args: string[], ctx: CommandContext): Promise<boolean> {
|
|
15
150
|
const shellPaths = requireShellPaths(ctx);
|
|
16
151
|
const sub = (args[1] ?? '').toLowerCase();
|
|
@@ -20,12 +155,8 @@ export async function handleSandboxQemuCommand(args: string[], ctx: CommandConte
|
|
|
20
155
|
return true;
|
|
21
156
|
}
|
|
22
157
|
if (sub === 'setup') {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
ctx.print('Usage: /sandbox qemu setup <directory>');
|
|
26
|
-
return true;
|
|
27
|
-
}
|
|
28
|
-
const bundle = scaffoldSandboxQemuSetupBundle(ctx.platform.configManager, shellPaths.workingDirectory, dirArg, { surfaceRoot: 'tui' });
|
|
158
|
+
const parsed = parseSetupArgs(args, shellPaths);
|
|
159
|
+
const bundle = scaffoldSandboxQemuSetupBundle(ctx.platform.configManager, shellPaths.workingDirectory, parsed.directory, { surfaceRoot: 'tui' });
|
|
29
160
|
const manifest = loadSandboxQemuSetupManifest(shellPaths.workingDirectory, bundle.manifestPath);
|
|
30
161
|
applySandboxQemuSetupManifest(ctx.platform.configManager, manifest);
|
|
31
162
|
ctx.platform.configManager.setDynamic('sandbox.replIsolation', 'shared-vm');
|
|
@@ -45,19 +176,21 @@ export async function handleSandboxQemuCommand(args: string[], ctx: CommandConte
|
|
|
45
176
|
' applied: backend=qemu, qemu binary, wrapper path, image path, launch-per-command guest settings, shared VM isolation',
|
|
46
177
|
` next: ${bundle.imageCreateScriptPath} ${bundle.imagePath} 20G`,
|
|
47
178
|
` then provision runtimes: GV_SANDBOX_SYNC_WORKSPACE=0 GV_SANDBOX_WRAPPER_MODE=launch-qemu-ssh ${bundle.wrapperPath} bash -s < ${bundle.guestBootstrapScriptPath}`,
|
|
179
|
+
'',
|
|
180
|
+
`Default setup location: ${defaultQemuSandboxDirectory(shellPaths)}`,
|
|
181
|
+
'Pass an explicit directory only when you intentionally want a non-default bundle location.',
|
|
48
182
|
].join('\n'));
|
|
49
183
|
return true;
|
|
50
184
|
}
|
|
51
185
|
if (sub === 'bootstrap') {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
ctx.print('Usage: /sandbox qemu bootstrap <directory> [size-gb]');
|
|
186
|
+
const parsed = parseBootstrapArgs(args, shellPaths);
|
|
187
|
+
if (!Number.isInteger(parsed.sizeGb) || parsed.sizeGb < 1) {
|
|
188
|
+
ctx.print('Usage: /sandbox qemu bootstrap [directory] [size-gb] [--scaffold-only|--no-build|--no-provision]');
|
|
56
189
|
return true;
|
|
57
190
|
}
|
|
58
191
|
try {
|
|
59
|
-
const bundle = bootstrapSandboxQemuSetupBundle(ctx.platform.configManager, shellPaths.workingDirectory,
|
|
60
|
-
|
|
192
|
+
const bundle = bootstrapSandboxQemuSetupBundle(ctx.platform.configManager, shellPaths.workingDirectory, parsed.directory, parsed.sizeGb, { surfaceRoot: 'tui' });
|
|
193
|
+
const lines = [
|
|
61
194
|
`Bootstrapped QEMU sandbox in ${bundle.directory}`,
|
|
62
195
|
` wrapper: ${bundle.wrapperPath}`,
|
|
63
196
|
` image path: ${bundle.imagePath}`,
|
|
@@ -67,10 +200,45 @@ export async function handleSandboxQemuCommand(args: string[], ctx: CommandConte
|
|
|
67
200
|
` projection policy: ${bundle.projectionPolicyPath}`,
|
|
68
201
|
` manifest: ${bundle.manifestPath}`,
|
|
69
202
|
' applied: backend=qemu, qemu binary, wrapper path, image path, launch-per-command guest settings, shared VM isolation',
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
203
|
+
];
|
|
204
|
+
if (parsed.buildImage) {
|
|
205
|
+
printProgress(ctx, `Building QEMU image at ${bundle.imagePath} (${parsed.sizeGb}G). This can download the Debian cloud image on first run.`);
|
|
206
|
+
await runQemuBootstrapStep('QEMU image build', bundle.imageCreateScriptPath, [bundle.imagePath, `${parsed.sizeGb}G`], {
|
|
207
|
+
cwd: bundle.directory,
|
|
208
|
+
timeoutMs: 30 * 60 * 1000,
|
|
209
|
+
});
|
|
210
|
+
lines.push(' image build: complete');
|
|
211
|
+
} else {
|
|
212
|
+
lines.push(` image build: skipped`);
|
|
213
|
+
lines.push(` next: ${bundle.imageCreateScriptPath} ${bundle.imagePath} ${parsed.sizeGb}G`);
|
|
214
|
+
}
|
|
215
|
+
if (parsed.provisionGuest) {
|
|
216
|
+
if (!existsSync(bundle.imagePath)) {
|
|
217
|
+
lines.push(' guest provisioning: skipped because image does not exist');
|
|
218
|
+
lines.push(` next: ${bundle.imageCreateScriptPath} ${bundle.imagePath} ${parsed.sizeGb}G`);
|
|
219
|
+
} else {
|
|
220
|
+
printProgress(ctx, `Provisioning guest runtimes through ${bundle.wrapperPath}. First boot can take several minutes.`);
|
|
221
|
+
await runQemuBootstrapStep('QEMU guest runtime provisioning', bundle.wrapperPath, ['bash', '-s'], {
|
|
222
|
+
cwd: bundle.directory,
|
|
223
|
+
env: {
|
|
224
|
+
GV_SANDBOX_SYNC_WORKSPACE: '0',
|
|
225
|
+
GV_SANDBOX_WRAPPER_MODE: 'launch-qemu-ssh',
|
|
226
|
+
},
|
|
227
|
+
input: readFileSync(bundle.guestBootstrapScriptPath, 'utf8'),
|
|
228
|
+
timeoutMs: 45 * 60 * 1000,
|
|
229
|
+
});
|
|
230
|
+
lines.push(' guest provisioning: complete');
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
lines.push(` guest provisioning: skipped`);
|
|
234
|
+
lines.push(` next: GV_SANDBOX_SYNC_WORKSPACE=0 GV_SANDBOX_WRAPPER_MODE=launch-qemu-ssh ${bundle.wrapperPath} bash -s < ${bundle.guestBootstrapScriptPath}`);
|
|
235
|
+
}
|
|
236
|
+
lines.push(' verify: /sandbox doctor');
|
|
237
|
+
lines.push(' verify: /sandbox guest-test eval-py');
|
|
238
|
+
lines.push('');
|
|
239
|
+
lines.push(`Default setup location: ${defaultQemuSandboxDirectory(shellPaths)}`);
|
|
240
|
+
lines.push('Pass an explicit directory only when you intentionally want a non-default bundle location.');
|
|
241
|
+
ctx.print(lines.join('\n'));
|
|
74
242
|
} catch (error) {
|
|
75
243
|
ctx.print(summarizeError(error));
|
|
76
244
|
}
|
|
@@ -114,6 +282,6 @@ export async function handleSandboxQemuCommand(args: string[], ctx: CommandConte
|
|
|
114
282
|
ctx.print(`Applied QEMU sandbox setup from ${shellPaths.resolveWorkspacePath(pathArg)}.`);
|
|
115
283
|
return true;
|
|
116
284
|
}
|
|
117
|
-
ctx.print('Usage: /sandbox qemu <setup
|
|
285
|
+
ctx.print('Usage: /sandbox qemu <setup [directory]|bootstrap [directory] [size-gb] [--scaffold-only|--no-build|--no-provision]|recover <session-id>|inspect-setup <setup-manifest.json>|apply-setup <setup-manifest.json>>');
|
|
118
286
|
return true;
|
|
119
287
|
}
|
|
@@ -93,7 +93,7 @@ export function registerPlatformSandboxRuntimeCommands(registry: CommandRegistry
|
|
|
93
93
|
registry.register({
|
|
94
94
|
name: 'sandbox',
|
|
95
95
|
description: 'Review and configure VM isolation policy for MCP and evaluation runtimes',
|
|
96
|
-
usage: '[open|review|recommend|profiles|presets|preset <id>|apply-preset <id>|probe|doctor|wrapper-test <profile>|guest-test <profile>|init-qemu <dir>|qemu <setup|bootstrap|recover|inspect-setup|apply-setup> ...|session ...|bundle ...|guest-bundle <export|inspect> <path>|scaffold-qemu-wrapper <path>|set-mcp <mode>|set-repl <mode>|set-windows <mode>|set-backend <mode>|set-qemu-binary <path>|set-qemu-image <path>|set-qemu-wrapper <path>|set-qemu-guest-host <host>|set-qemu-guest-port <port>|set-qemu-guest-user <user>|set-qemu-workspace <path>|set-qemu-session-mode <attach|launch-per-command>]',
|
|
96
|
+
usage: '[open|review|recommend|profiles|presets|preset <id>|apply-preset <id>|probe|doctor|wrapper-test <profile>|guest-test <profile>|init-qemu <dir>|qemu <setup [dir]|bootstrap [dir] [size-gb]|recover|inspect-setup|apply-setup> ...|session ...|bundle ...|guest-bundle <export|inspect> <path>|scaffold-qemu-wrapper <path>|set-mcp <mode>|set-repl <mode>|set-windows <mode>|set-backend <mode>|set-qemu-binary <path>|set-qemu-image <path>|set-qemu-wrapper <path>|set-qemu-guest-host <host>|set-qemu-guest-port <port>|set-qemu-guest-user <user>|set-qemu-workspace <path>|set-qemu-session-mode <attach|launch-per-command>]',
|
|
97
97
|
async handler(args, ctx) {
|
|
98
98
|
const shellPaths = requireShellPaths(ctx);
|
|
99
99
|
const sub = args[0] ?? 'open';
|
|
@@ -144,7 +144,7 @@ export function registerPlatformSandboxRuntimeCommands(registry: CommandRegistry
|
|
|
144
144
|
windowsMode: `${ctx.platform.configManager.get('sandbox.windowsMode')}`,
|
|
145
145
|
secureSandboxReady: renderSandboxReview(ctx.platform.configManager).includes('available'),
|
|
146
146
|
recommendedCommand: `${ctx.platform.configManager.get('sandbox.vmBackend')}` === 'local'
|
|
147
|
-
? '/sandbox qemu bootstrap
|
|
147
|
+
? '/sandbox qemu bootstrap'
|
|
148
148
|
: '/sandbox doctor',
|
|
149
149
|
};
|
|
150
150
|
ctx.print([inspectSandboxProbe(probe), ...backendProbe.warnings.map((warning: string) => ` warning: ${warning}`)].join('\n'));
|
|
@@ -207,7 +207,7 @@ export function registerPlatformSandboxRuntimeCommands(registry: CommandRegistry
|
|
|
207
207
|
` wrapper: ${bundle.wrapperPath}`,
|
|
208
208
|
` guest bundle: ${bundle.guestBundlePath}`,
|
|
209
209
|
` readme: ${bundle.readmePath}`,
|
|
210
|
-
' next: /sandbox qemu setup
|
|
210
|
+
' next: /sandbox qemu setup for the full default setup path',
|
|
211
211
|
].join('\n'));
|
|
212
212
|
return;
|
|
213
213
|
}
|
|
@@ -402,7 +402,7 @@ export function registerPlatformSandboxRuntimeCommands(registry: CommandRegistry
|
|
|
402
402
|
ctx.print([`Scaffolded QEMU wrapper to ${targetPath}`, ' bridge test mode: GV_SANDBOX_WRAPPER_MODE=host-exec', ' next: /sandbox set-qemu-wrapper ' + targetPath].join('\n'));
|
|
403
403
|
return;
|
|
404
404
|
}
|
|
405
|
-
ctx.print('Usage: /sandbox [open|review|recommend|profiles|presets|preset <id>|apply-preset <id>|probe|doctor|wrapper-test <profile>|guest-test <profile>|init-qemu <dir>|qemu <setup|bootstrap|recover|inspect-setup|apply-setup> ...|session ...|bundle export <path>|bundle inspect <path>|guest-bundle <export|inspect> <path>|scaffold-qemu-wrapper <path>|set-mcp <mode>|set-repl <mode>|set-windows <mode>|set-backend <mode>|set-qemu-binary <path>|set-qemu-image <path>|set-qemu-wrapper <path>|set-qemu-guest-host <host>|set-qemu-guest-port <port>|set-qemu-guest-user <user>|set-qemu-workspace <path>|set-qemu-session-mode <attach|launch-per-command>]');
|
|
405
|
+
ctx.print('Usage: /sandbox [open|review|recommend|profiles|presets|preset <id>|apply-preset <id>|probe|doctor|wrapper-test <profile>|guest-test <profile>|init-qemu <dir>|qemu <setup [dir]|bootstrap [dir] [size-gb]|recover|inspect-setup|apply-setup> ...|session ...|bundle export <path>|bundle inspect <path>|guest-bundle <export|inspect> <path>|scaffold-qemu-wrapper <path>|set-mcp <mode>|set-repl <mode>|set-windows <mode>|set-backend <mode>|set-qemu-binary <path>|set-qemu-image <path>|set-qemu-wrapper <path>|set-qemu-guest-host <host>|set-qemu-guest-port <port>|set-qemu-guest-user <user>|set-qemu-workspace <path>|set-qemu-session-mode <attach|launch-per-command>]');
|
|
406
406
|
},
|
|
407
407
|
});
|
|
408
408
|
}
|
|
@@ -49,6 +49,18 @@ wait_for_ssh() {
|
|
|
49
49
|
local start now
|
|
50
50
|
start="$(date +%s)"
|
|
51
51
|
while true; do
|
|
52
|
+
if [[ -n "\${GV_SANDBOX_ACTIVE_QEMU_PIDFILE:-}" && -f "\${GV_SANDBOX_ACTIVE_QEMU_PIDFILE:-}" ]]; then
|
|
53
|
+
local qemu_pid
|
|
54
|
+
qemu_pid="$(<"\${GV_SANDBOX_ACTIVE_QEMU_PIDFILE:-}")"
|
|
55
|
+
if [[ -n "$qemu_pid" ]] && ! kill -0 "$qemu_pid" 2>/dev/null; then
|
|
56
|
+
echo "QEMU process exited before SSH became available." >&2
|
|
57
|
+
if [[ -n "\${GV_SANDBOX_ACTIVE_QEMU_LOG:-}" && -f "\${GV_SANDBOX_ACTIVE_QEMU_LOG:-}" ]]; then
|
|
58
|
+
echo "QEMU log tail:" >&2
|
|
59
|
+
tail -80 "\${GV_SANDBOX_ACTIVE_QEMU_LOG:-}" >&2 || true
|
|
60
|
+
fi
|
|
61
|
+
return 1
|
|
62
|
+
fi
|
|
63
|
+
fi
|
|
52
64
|
if ssh "\${ssh_opts[@]}" "$user@$host" "true" >/dev/null 2>&1; then
|
|
53
65
|
return 0
|
|
54
66
|
fi
|
|
@@ -85,6 +97,9 @@ start_qemu() {
|
|
|
85
97
|
local serial_log="$logs_dir/serial-$port.log"
|
|
86
98
|
local qemu_log="$logs_dir/qemu-$port.log"
|
|
87
99
|
local monitor_sock="$run_dir/monitor-$port.sock"
|
|
100
|
+
GV_SANDBOX_ACTIVE_QEMU_PIDFILE="$pidfile"
|
|
101
|
+
GV_SANDBOX_ACTIVE_QEMU_LOG="$qemu_log"
|
|
102
|
+
export GV_SANDBOX_ACTIVE_QEMU_PIDFILE GV_SANDBOX_ACTIVE_QEMU_LOG
|
|
88
103
|
rm -f "$monitor_sock"
|
|
89
104
|
if [[ -f "$pidfile" ]]; then
|
|
90
105
|
local old_pid
|
package/src/version.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { join } from 'node:path';
|
|
|
6
6
|
// The prebuild script updates the fallback value before compilation.
|
|
7
7
|
// Uses import.meta.dir (Bun) to locate package.json relative to this file,
|
|
8
8
|
// which is correct regardless of the process working directory.
|
|
9
|
-
let _version = '0.20.
|
|
9
|
+
let _version = '0.20.3';
|
|
10
10
|
try {
|
|
11
11
|
const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
|
|
12
12
|
_version = pkg.version ?? _version;
|