@pellux/goodvibes-tui 0.19.59 → 0.19.61
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 +52 -0
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +2398 -2086
- package/package.json +2 -2
- package/src/audio/player.ts +1 -1
- package/src/audio/spoken-turn-controller.ts +5 -5
- package/src/audio/spoken-turn-model-routing.ts +3 -3
- package/src/audio/spoken-turn-wiring.ts +3 -3
- package/src/cli/bundle-command.ts +3 -3
- package/src/cli/entrypoint.ts +9 -8
- package/src/cli/management-commands.ts +6 -6
- package/src/cli/management.ts +17 -16
- package/src/cli/provider-auth-routes.ts +1 -1
- package/src/cli/service-posture.ts +2 -2
- package/src/cli/status.ts +4 -3
- package/src/cli/surface-command.ts +1 -1
- package/src/config/index.ts +12 -11
- package/src/config/provider-model.ts +23 -0
- package/src/config/secret-config.ts +1 -1
- package/src/config/secrets.ts +3 -3
- package/src/core/composer-state.ts +1 -1
- package/src/core/conversation-rendering.ts +3 -3
- package/src/core/conversation.ts +10 -10
- package/src/core/orchestrator.ts +1 -1
- package/src/core/system-message-router.ts +3 -3
- package/src/daemon/cli.ts +19 -19
- package/src/daemon/safe-serve.ts +2 -2
- package/src/input/bookmark-modal.ts +1 -1
- package/src/input/command-registry.ts +31 -31
- package/src/input/commands/control-room-runtime.ts +3 -3
- package/src/input/commands/conversation-runtime.ts +1 -1
- package/src/input/commands/diff-runtime.ts +1 -1
- package/src/input/commands/discovery-runtime.ts +2 -2
- package/src/input/commands/eval.ts +6 -6
- package/src/input/commands/git-runtime.ts +2 -2
- package/src/input/commands/guidance-runtime.ts +3 -3
- package/src/input/commands/health-runtime.ts +10 -10
- package/src/input/commands/incident-runtime.ts +1 -1
- package/src/input/commands/integration-runtime.ts +2 -2
- package/src/input/commands/intelligence-runtime.ts +3 -3
- package/src/input/commands/knowledge.ts +1 -1
- package/src/input/commands/local-auth-runtime.ts +1 -1
- package/src/input/commands/local-provider-runtime.ts +3 -4
- package/src/input/commands/local-runtime.ts +6 -6
- package/src/input/commands/local-setup-review.ts +3 -3
- package/src/input/commands/local-setup.ts +3 -3
- package/src/input/commands/managed-runtime.ts +9 -8
- package/src/input/commands/marketplace-runtime.ts +1 -1
- package/src/input/commands/mcp-runtime.ts +1 -1
- package/src/input/commands/operator-panel-runtime.ts +1 -1
- package/src/input/commands/operator-runtime.ts +5 -5
- package/src/input/commands/platform-access-runtime.ts +2 -2
- package/src/input/commands/platform-sandbox-qemu.ts +2 -2
- package/src/input/commands/platform-sandbox-runtime.ts +6 -6
- package/src/input/commands/platform-sandbox-session.ts +2 -2
- package/src/input/commands/policy-dispatch.ts +6 -6
- package/src/input/commands/product-runtime.ts +2 -2
- package/src/input/commands/profile-sync-runtime.ts +2 -2
- package/src/input/commands/provider-accounts-runtime.ts +1 -1
- package/src/input/commands/provider.ts +3 -3
- package/src/input/commands/quit-shared.ts +2 -2
- package/src/input/commands/recall-bundle.ts +2 -2
- package/src/input/commands/recall-capture.ts +2 -2
- package/src/input/commands/recall-query.ts +2 -2
- package/src/input/commands/recall-shared.ts +2 -2
- package/src/input/commands/remote-runtime-setup.ts +2 -2
- package/src/input/commands/remote-runtime.ts +1 -1
- package/src/input/commands/replay-runtime.ts +1 -1
- package/src/input/commands/runtime-services.ts +11 -11
- package/src/input/commands/schedule-runtime.ts +6 -6
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-content.ts +5 -5
- package/src/input/commands/session-workflow.ts +5 -5
- package/src/input/commands/session.ts +2 -2
- package/src/input/commands/settings-sync-runtime.ts +5 -4
- package/src/input/commands/share-runtime.ts +3 -3
- package/src/input/commands/shell-core.ts +4 -5
- package/src/input/commands/skills-runtime.ts +1 -1
- package/src/input/commands/subscription-runtime.ts +7 -7
- package/src/input/commands/tasks-runtime.ts +5 -5
- package/src/input/commands/teamwork-runtime.ts +3 -3
- package/src/input/commands/teleport-runtime.ts +1 -1
- package/src/input/commands/worktree-runtime.ts +3 -3
- package/src/input/feed-context-factory.ts +1 -1
- package/src/input/file-picker.ts +1 -1
- package/src/input/handler-command-route.ts +2 -2
- package/src/input/handler-content-actions.ts +7 -7
- package/src/input/handler-feed-routes.ts +3 -3
- package/src/input/handler-feed.ts +2 -2
- package/src/input/handler-interactions.ts +1 -1
- package/src/input/handler-modal-routes.ts +1 -2
- package/src/input/handler-modal-token-routes.ts +1 -1
- package/src/input/handler-onboarding.ts +9 -6
- package/src/input/handler-picker-routes.ts +3 -3
- package/src/input/handler-shortcuts.ts +1 -1
- package/src/input/handler-ui-state.ts +1 -1
- package/src/input/handler.ts +5 -5
- package/src/input/input-history.ts +2 -2
- package/src/input/keybindings.ts +3 -3
- package/src/input/model-picker-types.ts +1 -1
- package/src/input/model-picker.ts +6 -6
- package/src/input/onboarding/handler-onboarding-routes.ts +1 -1
- package/src/input/onboarding/onboarding-runtime-status.ts +1 -1
- package/src/input/onboarding/onboarding-wizard-apply.ts +2 -2
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +1 -1
- package/src/input/panel-integration-actions.ts +1 -1
- package/src/input/profile-picker-modal.ts +5 -5
- package/src/input/session-picker-modal.ts +2 -2
- package/src/input/settings-modal-behavior.ts +3 -5
- package/src/input/settings-modal-secrets.ts +4 -4
- package/src/input/settings-modal-subscriptions.ts +3 -3
- package/src/input/settings-modal-types.ts +4 -4
- package/src/input/settings-modal.ts +9 -9
- package/src/input/tts-settings-actions.ts +1 -1
- package/src/main.ts +16 -17
- package/src/panels/agent-inspector-panel.ts +4 -4
- package/src/panels/agent-logs-panel.ts +2 -2
- package/src/panels/approval-panel.ts +2 -2
- package/src/panels/builtin/operations.ts +1 -1
- package/src/panels/builtin/session.ts +2 -2
- package/src/panels/builtin/shared.ts +26 -26
- package/src/panels/context-visualizer-panel.ts +3 -3
- package/src/panels/cost-tracker-panel.ts +1 -1
- package/src/panels/debug-panel.ts +1 -1
- package/src/panels/eval-panel.ts +1 -1
- package/src/panels/forensics-panel.ts +3 -3
- package/src/panels/git-panel.ts +3 -3
- package/src/panels/hooks-panel.ts +7 -7
- package/src/panels/incident-review-panel.ts +2 -2
- package/src/panels/knowledge-panel.ts +1 -1
- package/src/panels/local-auth-panel.ts +1 -1
- package/src/panels/marketplace-panel.ts +2 -2
- package/src/panels/mcp-panel.ts +2 -2
- package/src/panels/memory-panel.ts +2 -2
- package/src/panels/ops-control-panel.ts +1 -1
- package/src/panels/ops-strategy-panel.ts +2 -2
- package/src/panels/orchestration-panel.ts +1 -1
- package/src/panels/panel-list-panel.ts +1 -1
- package/src/panels/plan-dashboard-panel.ts +1 -1
- package/src/panels/plugins-panel.ts +1 -1
- package/src/panels/policy-panel.ts +1 -1
- package/src/panels/project-planning-panel.ts +1 -1
- package/src/panels/provider-account-snapshot.ts +1 -1
- package/src/panels/provider-health-domains.ts +4 -4
- package/src/panels/provider-health-panel.ts +4 -4
- package/src/panels/provider-stats-panel.ts +1 -1
- package/src/panels/qr-panel.ts +1 -1
- package/src/panels/sandbox-panel.ts +3 -3
- package/src/panels/schedule-panel.ts +4 -4
- package/src/panels/security-panel.ts +1 -1
- package/src/panels/services-panel.ts +1 -1
- package/src/panels/session-browser-panel.ts +3 -3
- package/src/panels/settings-sync-panel.ts +1 -1
- package/src/panels/skills-panel.ts +1 -1
- package/src/panels/subscription-panel.ts +2 -2
- package/src/panels/system-messages-panel.ts +1 -1
- package/src/panels/tasks-panel.ts +5 -5
- package/src/panels/thinking-panel.ts +1 -1
- package/src/panels/token-budget-panel.ts +2 -2
- package/src/panels/tool-inspector-panel.ts +1 -1
- package/src/panels/worktree-panel.ts +6 -6
- package/src/panels/wrfc-panel.ts +4 -4
- package/src/permissions/prompt.ts +3 -3
- package/src/planning/project-planning-coordinator.ts +1 -1
- package/src/plugins/loader.ts +2 -2
- package/src/renderer/agent-detail-modal.ts +5 -5
- package/src/renderer/bookmark-modal.ts +1 -1
- package/src/renderer/git-status.ts +3 -3
- package/src/renderer/help-overlay.ts +1 -1
- package/src/renderer/live-tail-modal.ts +2 -2
- package/src/renderer/model-picker-overlay.ts +2 -2
- package/src/renderer/model-workspace.ts +1 -1
- package/src/renderer/process-modal.ts +3 -3
- package/src/renderer/qr-renderer.ts +1 -1
- package/src/renderer/semantic-diff.ts +6 -6
- package/src/renderer/syntax-highlighter.ts +3 -3
- package/src/renderer/tool-call.ts +1 -1
- package/src/runtime/bootstrap-command-context.ts +46 -46
- package/src/runtime/bootstrap-command-parts.ts +46 -47
- package/src/runtime/bootstrap-core.ts +24 -23
- package/src/runtime/bootstrap-hook-bridge.ts +14 -14
- package/src/runtime/bootstrap-shell.ts +15 -15
- package/src/runtime/bootstrap.ts +27 -27
- package/src/runtime/cloudflare-control-plane.ts +1 -1
- package/src/runtime/context.ts +13 -13
- package/src/runtime/diagnostics/panels/index.ts +14 -14
- package/src/runtime/diagnostics/panels/ops.ts +7 -7
- package/src/runtime/diagnostics/panels/panel-resources.ts +1 -1
- package/src/runtime/diagnostics/panels/policy.ts +11 -11
- package/src/runtime/index.ts +625 -163
- package/src/runtime/onboarding/apply.ts +1 -1
- package/src/runtime/onboarding/derivation.ts +3 -2
- package/src/runtime/onboarding/markers.ts +1 -1
- package/src/runtime/onboarding/snapshot.ts +3 -2
- package/src/runtime/onboarding/types.ts +2 -2
- package/src/runtime/onboarding/verify.ts +1 -1
- package/src/runtime/perf/panel-contracts.ts +2 -2
- package/src/runtime/perf/panel-health-monitor.ts +2 -2
- package/src/runtime/sandbox-public-gaps.ts +486 -0
- package/src/runtime/services.ts +114 -67
- package/src/runtime/store/domains/index.ts +50 -50
- package/src/runtime/store/index.ts +21 -20
- package/src/runtime/store/selectors/index.ts +17 -17
- package/src/runtime/store/state.ts +50 -50
- package/src/runtime/surface-feature-flags.ts +1 -3
- package/src/runtime/terminal-output-guard.ts +1 -1
- package/src/runtime/ui/index.ts +7 -7
- package/src/runtime/ui/model-picker/data-provider.ts +7 -7
- package/src/runtime/ui/model-picker/health-enrichment.ts +7 -7
- package/src/runtime/ui/model-picker/index.ts +11 -11
- package/src/runtime/ui/model-picker/types.ts +1 -1
- package/src/runtime/ui/provider-health/data-provider.ts +4 -4
- package/src/runtime/ui/provider-health/fallback-visualizer.ts +5 -3
- package/src/runtime/ui/provider-health/index.ts +7 -7
- package/src/runtime/ui/provider-health/types.ts +1 -1
- package/src/runtime/ui-events.ts +1 -1
- package/src/runtime/ui-read-model-helpers.ts +1 -1
- package/src/runtime/ui-read-models-observability-maintenance.ts +1 -1
- package/src/runtime/ui-read-models-observability-options.ts +1 -1
- package/src/runtime/ui-read-models-observability-remote.ts +1 -1
- package/src/runtime/ui-read-models-observability-security.ts +1 -1
- package/src/runtime/ui-read-models-observability-system.ts +1 -1
- package/src/runtime/ui-read-models-observability.ts +1 -1
- package/src/runtime/ui-read-models.ts +8 -8
- package/src/runtime/ui-service-queries.ts +1 -1
- package/src/runtime/ui-services.ts +8 -8
- package/src/scripts/process-messages.ts +1 -1
- package/src/shell/blocking-input.ts +2 -2
- package/src/shell/ui-openers.ts +8 -7
- package/src/tools/index.ts +1 -1
- package/src/utils/clipboard.ts +3 -3
- package/src/version.ts +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
|
-
import { isSecretRefInput } from '@pellux/goodvibes-sdk/platform/config
|
|
3
|
+
import { isSecretRefInput } from '@pellux/goodvibes-sdk/platform/config';
|
|
4
4
|
import { CONFIG_SCHEMA, DEFAULT_CONFIG } from '../../config/index.ts';
|
|
5
5
|
import type { FeatureFlagConfigKey } from '../surface-feature-flags.ts';
|
|
6
6
|
import {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_CONFIG } from '../../config/index.ts';
|
|
2
|
+
import { getProviderIdFromModel } from '../../config/provider-model.ts';
|
|
2
3
|
import type {
|
|
3
4
|
OnboardingAcknowledgementState,
|
|
4
5
|
OnboardingAcknowledgementTarget,
|
|
@@ -97,7 +98,7 @@ function countPermissionToolOverrides(snapshot: OnboardingSnapshotState): number
|
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
function hasCustomizedProviderRouting(snapshot: OnboardingSnapshotState): boolean {
|
|
100
|
-
return snapshot.providerRouting.primaryProviderId !== DEFAULT_CONFIG.provider.
|
|
101
|
+
return snapshot.providerRouting.primaryProviderId !== getProviderIdFromModel(DEFAULT_CONFIG.provider.model)
|
|
101
102
|
|| snapshot.providerRouting.primaryModelId !== DEFAULT_CONFIG.provider.model
|
|
102
103
|
|| snapshot.providerRouting.primaryReasoningEffort !== DEFAULT_CONFIG.provider.reasoningEffort
|
|
103
104
|
|| snapshot.providerRouting.embeddingProviderId !== DEFAULT_CONFIG.provider.embeddingProvider
|
|
@@ -236,7 +237,7 @@ function getProviderIdentityIds(snapshot: OnboardingSnapshotState): Set<string>
|
|
|
236
237
|
snapshot.providerRouting.embeddingProviderId,
|
|
237
238
|
snapshot.providerRouting.helperProviderId,
|
|
238
239
|
snapshot.providerRouting.toolProviderId,
|
|
239
|
-
].filter((value) => value.trim().length > 0));
|
|
240
|
+
].filter((value): value is string => typeof value === 'string' && value.trim().length > 0));
|
|
240
241
|
}
|
|
241
242
|
|
|
242
243
|
function getExternalIntegrationServiceIds(snapshot: OnboardingSnapshotState): string[] {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
|
-
import type { ShellPathService } from '
|
|
3
|
+
import type { ShellPathService } from '@/runtime/index.ts';
|
|
4
4
|
import type {
|
|
5
5
|
OnboardingCheckMarkerPayload,
|
|
6
6
|
OnboardingCheckMarkerState,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SecretStorageReview } from '../../config/secrets.ts';
|
|
2
|
-
import
|
|
2
|
+
import { getProviderIdFromModel } from '../../config/provider-model.ts';
|
|
3
|
+
import type { LocalAuthSnapshot } from '@pellux/goodvibes-sdk/platform/security';
|
|
3
4
|
import { readOnboardingRuntimeState } from './state.ts';
|
|
4
5
|
import type {
|
|
5
6
|
OnboardingAcknowledgementSnapshot,
|
|
@@ -50,7 +51,7 @@ function buildProviderRoutingSnapshot(
|
|
|
50
51
|
config: OnboardingConfigSnapshot,
|
|
51
52
|
): OnboardingProviderRoutingSnapshot {
|
|
52
53
|
return {
|
|
53
|
-
primaryProviderId: config.provider.
|
|
54
|
+
primaryProviderId: getProviderIdFromModel(config.provider.model),
|
|
54
55
|
primaryModelId: config.provider.model,
|
|
55
56
|
primaryReasoningEffort: config.provider.reasoningEffort,
|
|
56
57
|
embeddingProviderId: config.provider.embeddingProvider,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { ConfigManager, ConfigKey, GoodVibesConfig } from '../../config/index.ts';
|
|
2
2
|
import type { SecretsManager, SecretRecord, SecretStorageReview } from '../../config/secrets.ts';
|
|
3
3
|
import type { FeatureFlagConfigKey } from '../surface-feature-flags.ts';
|
|
4
|
-
import type { LocalAuthSnapshot, UserAuthManager } from '@pellux/goodvibes-sdk/platform/security
|
|
5
|
-
import type { ShellPathService } from '
|
|
4
|
+
import type { LocalAuthSnapshot, UserAuthManager } from '@pellux/goodvibes-sdk/platform/security';
|
|
5
|
+
import type { ShellPathService } from '@/runtime/index.ts';
|
|
6
6
|
import type {
|
|
7
7
|
LocalAuthInspectionQuery,
|
|
8
8
|
ServiceInspectionQuery,
|
|
@@ -22,11 +22,11 @@ export type {
|
|
|
22
22
|
PanelHealthStatus,
|
|
23
23
|
PanelResourceContract,
|
|
24
24
|
PanelHealthState,
|
|
25
|
-
} from '
|
|
25
|
+
} from '@/runtime/index.ts';
|
|
26
26
|
|
|
27
27
|
export {
|
|
28
28
|
CATEGORY_CONTRACTS,
|
|
29
29
|
buildContract,
|
|
30
30
|
createInitialComponentHealthState,
|
|
31
31
|
createInitialPanelHealthState,
|
|
32
|
-
} from '
|
|
32
|
+
} from '@/runtime/index.ts';
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import {
|
|
5
|
+
detectSandboxHostStatus,
|
|
6
|
+
getSandboxConfigSnapshot,
|
|
7
|
+
type ConfigManagerLike,
|
|
8
|
+
type SandboxBackendProbe,
|
|
9
|
+
type SandboxLaunchPlan,
|
|
10
|
+
type SandboxProfile,
|
|
11
|
+
} from '@pellux/goodvibes-sdk/platform/runtime/sandbox';
|
|
12
|
+
|
|
13
|
+
export interface SandboxGuestBundle {
|
|
14
|
+
readonly version: 1;
|
|
15
|
+
readonly exportedAt: number;
|
|
16
|
+
readonly guest: {
|
|
17
|
+
readonly qemuBinary: string;
|
|
18
|
+
readonly imagePath: string;
|
|
19
|
+
readonly wrapperPath: string;
|
|
20
|
+
readonly host: string;
|
|
21
|
+
readonly port: number;
|
|
22
|
+
readonly user: string;
|
|
23
|
+
readonly workspacePath: string;
|
|
24
|
+
readonly sessionMode: string;
|
|
25
|
+
};
|
|
26
|
+
readonly nextSteps: readonly string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SandboxQemuInitBundle {
|
|
30
|
+
readonly directory: string;
|
|
31
|
+
readonly wrapperPath: string;
|
|
32
|
+
readonly guestBundlePath: string;
|
|
33
|
+
readonly readmePath: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SandboxQemuSetupBundle extends SandboxQemuInitBundle {
|
|
37
|
+
readonly imagePath: string;
|
|
38
|
+
readonly imageCreateScriptPath: string;
|
|
39
|
+
readonly guestBootstrapScriptPath: string;
|
|
40
|
+
readonly projectionPolicyPath: string;
|
|
41
|
+
readonly sshConfigPath: string;
|
|
42
|
+
readonly manifestPath: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface SandboxQemuSetupManifest {
|
|
46
|
+
readonly version: 1;
|
|
47
|
+
readonly createdAt: number;
|
|
48
|
+
readonly wrapperPath: string;
|
|
49
|
+
readonly imagePath: string;
|
|
50
|
+
readonly imageCreateScriptPath: string;
|
|
51
|
+
readonly guestBootstrapScriptPath: string;
|
|
52
|
+
readonly projectionPolicyPath: string;
|
|
53
|
+
readonly sshConfigPath: string;
|
|
54
|
+
readonly recommendedSettings: {
|
|
55
|
+
readonly backend: 'qemu';
|
|
56
|
+
readonly wrapperPath: string;
|
|
57
|
+
readonly imagePath: string;
|
|
58
|
+
readonly guestHost: string;
|
|
59
|
+
readonly guestPort: number;
|
|
60
|
+
readonly guestUser: string;
|
|
61
|
+
readonly guestWorkspacePath: string;
|
|
62
|
+
readonly sessionMode: string;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface SandboxProvisioningOptions {
|
|
67
|
+
readonly surfaceRoot: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface WritableConfigManagerLike extends ConfigManagerLike {
|
|
71
|
+
setDynamic(key: string, value: unknown): void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function renderQemuWrapperTemplate(): string {
|
|
75
|
+
return `#!/usr/bin/env bash
|
|
76
|
+
set -euo pipefail
|
|
77
|
+
mode="\${GV_SANDBOX_WRAPPER_MODE:-ssh-guest}"
|
|
78
|
+
if [[ "$mode" == "host-exec" ]]; then
|
|
79
|
+
exec "$@"
|
|
80
|
+
fi
|
|
81
|
+
host="\${GOODVIBES_QEMU_GUEST_HOST:-127.0.0.1}"
|
|
82
|
+
port="\${GOODVIBES_QEMU_GUEST_PORT:-2222}"
|
|
83
|
+
user="\${GOODVIBES_QEMU_GUEST_USER:-goodvibes}"
|
|
84
|
+
workspace="\${GOODVIBES_QEMU_WORKSPACE:-/workspace}"
|
|
85
|
+
exec ssh -o StrictHostKeyChecking=accept-new -p "$port" "$user@$host" "cd '$workspace' && exec \\"$@\\""
|
|
86
|
+
`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function probeSandboxBackends(manager: ConfigManagerLike): SandboxBackendProbe {
|
|
90
|
+
const host = detectSandboxHostStatus(manager);
|
|
91
|
+
const config = getSandboxConfigSnapshot(manager);
|
|
92
|
+
const qemuBinary = config.qemuBinary || 'qemu-system-x86_64';
|
|
93
|
+
const qemuImage = config.qemuImagePath || '';
|
|
94
|
+
const qemuExecWrapper = config.qemuExecWrapper || '';
|
|
95
|
+
const qemuGuestHost = config.qemuGuestHost || '';
|
|
96
|
+
const qemuProbe = spawnSync(qemuBinary, ['--version'], {
|
|
97
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
98
|
+
encoding: 'buffer',
|
|
99
|
+
timeout: 1500,
|
|
100
|
+
windowsHide: true,
|
|
101
|
+
});
|
|
102
|
+
const qemuAvailable = qemuProbe.status === 0 || qemuProbe.status === 1;
|
|
103
|
+
const qemuBlockedReason = host.windows && !host.runningInWsl
|
|
104
|
+
? 'QEMU sandboxing on Windows requires running GoodVibes inside WSL.'
|
|
105
|
+
: '';
|
|
106
|
+
const qemuDetail = qemuBlockedReason || (
|
|
107
|
+
qemuAvailable
|
|
108
|
+
? `requires ${qemuBinary} on PATH`
|
|
109
|
+
: `requires ${qemuBinary} on PATH (${qemuProbe.error instanceof Error ? qemuProbe.error.message : `exit ${qemuProbe.status ?? 'unknown'}`})`
|
|
110
|
+
);
|
|
111
|
+
const warnings: string[] = [];
|
|
112
|
+
if (config.vmBackend === 'qemu' && !qemuAvailable) {
|
|
113
|
+
warnings.push(`Requested sandbox backend "${config.vmBackend}" is unavailable; local process isolation will not be used unless sandbox.vmBackend is set to "local".`);
|
|
114
|
+
}
|
|
115
|
+
if (config.vmBackend === 'qemu' && !qemuImage) {
|
|
116
|
+
warnings.push('QEMU backend selected without sandbox.qemuImagePath; sessions can be planned and reviewed, but guest execution remains disabled.');
|
|
117
|
+
}
|
|
118
|
+
if (config.vmBackend === 'qemu' && qemuImage && !qemuExecWrapper) {
|
|
119
|
+
warnings.push('QEMU image is configured without sandbox.qemuExecWrapper; guest launch planning is wired, but command execution remains disabled until a host bridge is configured.');
|
|
120
|
+
}
|
|
121
|
+
if (config.vmBackend === 'qemu' && qemuExecWrapper && !qemuGuestHost) {
|
|
122
|
+
warnings.push('QEMU wrapper is configured without sandbox.qemuGuestHost; host bridge mode is available, but real guest SSH transport remains disabled until the guest host is configured.');
|
|
123
|
+
}
|
|
124
|
+
if (config.vmBackend === 'qemu' && qemuExecWrapper && !existsSync(qemuExecWrapper)) {
|
|
125
|
+
warnings.push(`Configured sandbox.qemuExecWrapper does not exist: ${qemuExecWrapper}`);
|
|
126
|
+
}
|
|
127
|
+
if (config.vmBackend === 'qemu' && qemuExecWrapper && existsSync(qemuExecWrapper) && !isExecutableFile(qemuExecWrapper)) {
|
|
128
|
+
warnings.push(`Configured sandbox.qemuExecWrapper is not executable: ${qemuExecWrapper}`);
|
|
129
|
+
}
|
|
130
|
+
if (qemuBlockedReason) warnings.push(qemuBlockedReason);
|
|
131
|
+
return {
|
|
132
|
+
requestedBackend: config.vmBackend,
|
|
133
|
+
resolvedBackend: config.vmBackend,
|
|
134
|
+
backends: [
|
|
135
|
+
{ id: 'local', available: true, detail: 'host-local process isolation is available when explicitly selected' },
|
|
136
|
+
{ id: 'qemu', available: qemuAvailable && !qemuBlockedReason, detail: qemuDetail },
|
|
137
|
+
],
|
|
138
|
+
warnings,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function buildSandboxLaunchPlan(
|
|
143
|
+
profile: SandboxProfile,
|
|
144
|
+
label: string,
|
|
145
|
+
manager: ConfigManagerLike,
|
|
146
|
+
workspaceRoot: string,
|
|
147
|
+
): SandboxLaunchPlan {
|
|
148
|
+
const config = getSandboxConfigSnapshot(manager);
|
|
149
|
+
const backendProbe = probeSandboxBackends(manager);
|
|
150
|
+
const backend = backendProbe.resolvedBackend === 'qemu' ? 'qemu' : 'local';
|
|
151
|
+
const safeWorkspaceRoot = resolve(workspaceRoot);
|
|
152
|
+
if (backend === 'qemu') {
|
|
153
|
+
const qemuAvailability = backendProbe.backends.find((entry) => entry.id === 'qemu');
|
|
154
|
+
if (!qemuAvailability?.available) {
|
|
155
|
+
throw new Error(`Requested QEMU sandbox backend is unavailable (${qemuAvailability?.detail ?? 'probe failed'}); refusing to downgrade to local process isolation. Set sandbox.vmBackend to "local" to use host-local isolation explicitly.`);
|
|
156
|
+
}
|
|
157
|
+
const guestPort = config.qemuGuestPort || 2222;
|
|
158
|
+
const args = [
|
|
159
|
+
'-display', 'none',
|
|
160
|
+
'-nodefaults',
|
|
161
|
+
'-name', `gv-${profile.id}`,
|
|
162
|
+
'-snapshot',
|
|
163
|
+
'-m', '512',
|
|
164
|
+
'-nic', `user,hostfwd=tcp::${guestPort}-:22`,
|
|
165
|
+
];
|
|
166
|
+
if (config.qemuImagePath) args.push('-drive', `file=${config.qemuImagePath},if=virtio,format=qcow2`);
|
|
167
|
+
return {
|
|
168
|
+
backend,
|
|
169
|
+
command: config.qemuBinary || 'qemu-system-x86_64',
|
|
170
|
+
args,
|
|
171
|
+
workspaceRoot: safeWorkspaceRoot,
|
|
172
|
+
summary: buildCommandSummary(config.qemuBinary || 'qemu-system-x86_64', args),
|
|
173
|
+
imagePath: config.qemuImagePath || undefined,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
backend: 'local',
|
|
178
|
+
command: process.env.SHELL || 'bash',
|
|
179
|
+
args: ['-lc', `echo "goodvibes sandbox ${profile.id}: ${label}"`],
|
|
180
|
+
workspaceRoot: safeWorkspaceRoot,
|
|
181
|
+
summary: buildCommandSummary(process.env.SHELL || 'bash', ['-lc', `echo "goodvibes sandbox ${profile.id}: ${label}"`]),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface SandboxCommandPlan {
|
|
186
|
+
readonly command: string;
|
|
187
|
+
readonly args: readonly string[];
|
|
188
|
+
readonly summary: string;
|
|
189
|
+
readonly env?: NodeJS.ProcessEnv | undefined;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface SandboxCommandResult {
|
|
193
|
+
readonly status: number | null;
|
|
194
|
+
readonly stdout: string;
|
|
195
|
+
readonly stderr: string;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function buildCommandSummary(command: string, args: readonly string[]): string {
|
|
199
|
+
return [command, ...args].join(' ').trim();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function isExecutableFile(path: string): boolean {
|
|
203
|
+
try {
|
|
204
|
+
const stat = statSync(path);
|
|
205
|
+
return stat.isFile() && (stat.mode & 0o111) !== 0;
|
|
206
|
+
} catch {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function resolveSandboxCommandPlan(
|
|
212
|
+
launchPlan: SandboxLaunchPlan,
|
|
213
|
+
command: string,
|
|
214
|
+
args: readonly string[],
|
|
215
|
+
manager?: ConfigManagerLike,
|
|
216
|
+
): SandboxCommandPlan {
|
|
217
|
+
if (launchPlan.backend === 'qemu') {
|
|
218
|
+
if (!launchPlan.imagePath) {
|
|
219
|
+
throw new Error('QEMU-backed sandbox execution requires sandbox.qemuImagePath; guest launch planning is available, but command execution stays disabled until an image is configured.');
|
|
220
|
+
}
|
|
221
|
+
const config = manager ? getSandboxConfigSnapshot(manager) : undefined;
|
|
222
|
+
const wrapper = config?.qemuExecWrapper || '';
|
|
223
|
+
if (!wrapper) {
|
|
224
|
+
throw new Error('QEMU-backed sandbox execution requires sandbox.qemuExecWrapper; image-backed launch planning is wired, but guest command execution stays disabled until a wrapper is configured.');
|
|
225
|
+
}
|
|
226
|
+
if (!existsSync(wrapper)) {
|
|
227
|
+
throw new Error(`QEMU-backed sandbox execution requires an existing sandbox.qemuExecWrapper; missing: ${wrapper}`);
|
|
228
|
+
}
|
|
229
|
+
if (!isExecutableFile(wrapper)) {
|
|
230
|
+
throw new Error(`QEMU-backed sandbox execution requires an executable sandbox.qemuExecWrapper; not executable: ${wrapper}`);
|
|
231
|
+
}
|
|
232
|
+
const guestHost = config?.qemuGuestHost || '';
|
|
233
|
+
const sessionMode = config?.qemuSessionMode === 'launch-per-command' ? 'launch-per-command' : 'attach';
|
|
234
|
+
return {
|
|
235
|
+
command: wrapper,
|
|
236
|
+
args: [command, ...args],
|
|
237
|
+
env: {
|
|
238
|
+
GV_SANDBOX_QEMU_BINARY: launchPlan.command,
|
|
239
|
+
GV_SANDBOX_QEMU_ARGS: JSON.stringify(launchPlan.args),
|
|
240
|
+
GV_SANDBOX_QEMU_IMAGE: launchPlan.imagePath,
|
|
241
|
+
GV_SANDBOX_WORKSPACE_ROOT: launchPlan.workspaceRoot,
|
|
242
|
+
GV_SANDBOX_GUEST_COMMAND: command,
|
|
243
|
+
GV_SANDBOX_GUEST_ARGS: JSON.stringify(args),
|
|
244
|
+
GV_SANDBOX_GUEST_HOST: guestHost,
|
|
245
|
+
GV_SANDBOX_GUEST_PORT: `${config?.qemuGuestPort || 2222}`,
|
|
246
|
+
GV_SANDBOX_GUEST_USER: config?.qemuGuestUser || '',
|
|
247
|
+
GV_SANDBOX_GUEST_WORKSPACE: config?.qemuWorkspacePath || '',
|
|
248
|
+
GV_SANDBOX_WRAPPER_MODE: guestHost ? (sessionMode === 'launch-per-command' ? 'launch-qemu-ssh' : 'ssh-guest') : '',
|
|
249
|
+
GV_SANDBOX_EXEC_MODE: 'wrapper',
|
|
250
|
+
},
|
|
251
|
+
summary: buildCommandSummary(wrapper, [command, ...args]),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
command,
|
|
256
|
+
args,
|
|
257
|
+
summary: buildCommandSummary(command, args),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function executeSandboxCommand(
|
|
262
|
+
launchPlan: SandboxLaunchPlan,
|
|
263
|
+
command: string,
|
|
264
|
+
args: readonly string[],
|
|
265
|
+
options: {
|
|
266
|
+
readonly cwd?: string;
|
|
267
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
268
|
+
readonly inheritHostEnv?: boolean;
|
|
269
|
+
readonly timeoutMs?: number;
|
|
270
|
+
readonly input?: string;
|
|
271
|
+
} = {},
|
|
272
|
+
): SandboxCommandResult {
|
|
273
|
+
return runSandboxCommand(launchPlan, command, args, undefined, options);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function executeSandboxManagedCommand(
|
|
277
|
+
launchPlan: SandboxLaunchPlan,
|
|
278
|
+
command: string,
|
|
279
|
+
args: readonly string[],
|
|
280
|
+
manager: ConfigManagerLike,
|
|
281
|
+
options: {
|
|
282
|
+
readonly cwd?: string;
|
|
283
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
284
|
+
readonly inheritHostEnv?: boolean;
|
|
285
|
+
readonly timeoutMs?: number;
|
|
286
|
+
readonly input?: string;
|
|
287
|
+
} = {},
|
|
288
|
+
): SandboxCommandResult {
|
|
289
|
+
return runSandboxCommand(launchPlan, command, args, manager, options);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function runSandboxCommand(
|
|
293
|
+
launchPlan: SandboxLaunchPlan,
|
|
294
|
+
command: string,
|
|
295
|
+
args: readonly string[],
|
|
296
|
+
manager: ConfigManagerLike | undefined,
|
|
297
|
+
options: {
|
|
298
|
+
readonly cwd?: string;
|
|
299
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
300
|
+
readonly inheritHostEnv?: boolean;
|
|
301
|
+
readonly timeoutMs?: number;
|
|
302
|
+
readonly input?: string;
|
|
303
|
+
},
|
|
304
|
+
): SandboxCommandResult {
|
|
305
|
+
const resolved = resolveSandboxCommandPlan(launchPlan, command, args, manager);
|
|
306
|
+
const result = spawnSync(resolved.command, [...resolved.args], {
|
|
307
|
+
cwd: options.cwd ?? launchPlan.workspaceRoot,
|
|
308
|
+
env: options.inheritHostEnv === false ? { ...resolved.env, ...options.env } : { ...process.env, ...resolved.env, ...options.env },
|
|
309
|
+
input: options.input,
|
|
310
|
+
timeout: options.timeoutMs ?? 5000,
|
|
311
|
+
encoding: 'utf8',
|
|
312
|
+
windowsHide: true,
|
|
313
|
+
});
|
|
314
|
+
return {
|
|
315
|
+
status: result.status,
|
|
316
|
+
stdout: result.stdout || '',
|
|
317
|
+
stderr: result.stderr || (result.error ? String(result.error) : ''),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function resolveWorkspacePath(workspaceRoot: string, pathArg: string): string {
|
|
322
|
+
return resolve(workspaceRoot, pathArg);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function ensureDir(path: string): void {
|
|
326
|
+
mkdirSync(path, { recursive: true });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function scaffoldSandboxQemuInitBundle(
|
|
330
|
+
manager: ConfigManagerLike,
|
|
331
|
+
workspaceRoot: string,
|
|
332
|
+
pathArg: string,
|
|
333
|
+
_options: SandboxProvisioningOptions,
|
|
334
|
+
): SandboxQemuInitBundle {
|
|
335
|
+
const directory = resolveWorkspacePath(workspaceRoot, pathArg);
|
|
336
|
+
ensureDir(directory);
|
|
337
|
+
const wrapperPath = resolve(directory, 'qemu-wrapper.sh');
|
|
338
|
+
const guestBundlePath = resolve(directory, 'guest-bundle.json');
|
|
339
|
+
const readmePath = resolve(directory, 'README.txt');
|
|
340
|
+
writeFileSync(wrapperPath, renderQemuWrapperTemplate(), { mode: 0o755 });
|
|
341
|
+
const guestBundle = buildGuestBundle(manager, workspaceRoot, guestBundlePath);
|
|
342
|
+
writeFileSync(guestBundlePath, `${JSON.stringify(guestBundle, null, 2)}\n`);
|
|
343
|
+
writeFileSync(readmePath, 'GoodVibes QEMU sandbox bootstrap files.\n');
|
|
344
|
+
return { directory, wrapperPath, guestBundlePath, readmePath };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function buildGuestBundle(manager: ConfigManagerLike, _workspaceRoot: string, wrapperPath: string): SandboxGuestBundle {
|
|
348
|
+
const config = getSandboxConfigSnapshot(manager);
|
|
349
|
+
return {
|
|
350
|
+
version: 1,
|
|
351
|
+
exportedAt: Date.now(),
|
|
352
|
+
guest: {
|
|
353
|
+
qemuBinary: config.qemuBinary,
|
|
354
|
+
imagePath: config.qemuImagePath,
|
|
355
|
+
wrapperPath,
|
|
356
|
+
host: config.qemuGuestHost,
|
|
357
|
+
port: config.qemuGuestPort,
|
|
358
|
+
user: config.qemuGuestUser,
|
|
359
|
+
workspacePath: config.qemuWorkspacePath,
|
|
360
|
+
sessionMode: config.qemuSessionMode,
|
|
361
|
+
},
|
|
362
|
+
nextSteps: ['Create or attach a QEMU guest, run the bootstrap script, then run /sandbox guest-test eval-js.'],
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function scaffoldSandboxQemuSetupBundle(
|
|
367
|
+
manager: ConfigManagerLike,
|
|
368
|
+
workspaceRoot: string,
|
|
369
|
+
pathArg: string,
|
|
370
|
+
options: SandboxProvisioningOptions,
|
|
371
|
+
): SandboxQemuSetupBundle {
|
|
372
|
+
const init = scaffoldSandboxQemuInitBundle(manager, workspaceRoot, pathArg, options);
|
|
373
|
+
const imagePath = resolve(init.directory, 'goodvibes-sandbox.qcow2');
|
|
374
|
+
const imageCreateScriptPath = resolve(init.directory, 'create-image.sh');
|
|
375
|
+
const guestBootstrapScriptPath = resolve(init.directory, 'guest-bootstrap.sh');
|
|
376
|
+
const projectionPolicyPath = resolve(init.directory, 'projection-policy.json');
|
|
377
|
+
const sshConfigPath = resolve(init.directory, 'ssh-config');
|
|
378
|
+
const manifestPath = resolve(init.directory, 'setup-manifest.json');
|
|
379
|
+
const config = getSandboxConfigSnapshot(manager);
|
|
380
|
+
const manifest: SandboxQemuSetupManifest = {
|
|
381
|
+
version: 1,
|
|
382
|
+
createdAt: Date.now(),
|
|
383
|
+
wrapperPath: init.wrapperPath,
|
|
384
|
+
imagePath,
|
|
385
|
+
imageCreateScriptPath,
|
|
386
|
+
guestBootstrapScriptPath,
|
|
387
|
+
projectionPolicyPath,
|
|
388
|
+
sshConfigPath,
|
|
389
|
+
recommendedSettings: {
|
|
390
|
+
backend: 'qemu',
|
|
391
|
+
wrapperPath: init.wrapperPath,
|
|
392
|
+
imagePath,
|
|
393
|
+
guestHost: config.qemuGuestHost || '127.0.0.1',
|
|
394
|
+
guestPort: config.qemuGuestPort || 2222,
|
|
395
|
+
guestUser: config.qemuGuestUser || 'goodvibes',
|
|
396
|
+
guestWorkspacePath: config.qemuWorkspacePath || '/workspace',
|
|
397
|
+
sessionMode: config.qemuSessionMode || 'attach',
|
|
398
|
+
},
|
|
399
|
+
};
|
|
400
|
+
writeFileSync(imageCreateScriptPath, '#!/usr/bin/env bash\nset -euo pipefail\nqemu-img create -f qcow2 "$1" "${2:-20G}"\n', { mode: 0o755 });
|
|
401
|
+
writeFileSync(guestBootstrapScriptPath, '#!/usr/bin/env bash\nset -euo pipefail\nmkdir -p /workspace\n');
|
|
402
|
+
writeFileSync(projectionPolicyPath, `${JSON.stringify({ version: 1, workspace: '/workspace' }, null, 2)}\n`);
|
|
403
|
+
writeFileSync(sshConfigPath, 'Host goodvibes-qemu\n HostName 127.0.0.1\n Port 2222\n User goodvibes\n');
|
|
404
|
+
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
405
|
+
return { ...init, imagePath, imageCreateScriptPath, guestBootstrapScriptPath, projectionPolicyPath, sshConfigPath, manifestPath };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export function bootstrapSandboxQemuSetupBundle(
|
|
409
|
+
manager: WritableConfigManagerLike,
|
|
410
|
+
workspaceRoot: string,
|
|
411
|
+
pathArg: string,
|
|
412
|
+
_sizeGb: number,
|
|
413
|
+
options: SandboxProvisioningOptions,
|
|
414
|
+
): SandboxQemuSetupBundle {
|
|
415
|
+
const bundle = scaffoldSandboxQemuSetupBundle(manager, workspaceRoot, pathArg, options);
|
|
416
|
+
manager.setDynamic('sandbox.vmBackend', 'qemu');
|
|
417
|
+
manager.setDynamic('sandbox.qemuExecWrapper', bundle.wrapperPath);
|
|
418
|
+
manager.setDynamic('sandbox.qemuImagePath', bundle.imagePath);
|
|
419
|
+
return bundle;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export function createSandboxQemuImage(workspaceRoot: string, imagePathArg: string, sizeGb: number): { readonly path: string; readonly sizeGb: number } {
|
|
423
|
+
const path = resolveWorkspacePath(workspaceRoot, imagePathArg);
|
|
424
|
+
ensureDir(dirname(path));
|
|
425
|
+
if (!existsSync(path)) writeFileSync(path, '');
|
|
426
|
+
return { path, sizeGb };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export function inspectSandboxQemuSetupManifest(manifest: SandboxQemuSetupManifest): string {
|
|
430
|
+
return [
|
|
431
|
+
'QEMU sandbox setup manifest',
|
|
432
|
+
` wrapper: ${manifest.wrapperPath}`,
|
|
433
|
+
` image: ${manifest.imagePath}`,
|
|
434
|
+
` guest: ${manifest.recommendedSettings.guestUser}@${manifest.recommendedSettings.guestHost}:${manifest.recommendedSettings.guestPort}`,
|
|
435
|
+
` workspace: ${manifest.recommendedSettings.guestWorkspacePath}`,
|
|
436
|
+
].join('\n');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export function loadSandboxQemuSetupManifest(workspaceRoot: string, pathArg: string): SandboxQemuSetupManifest {
|
|
440
|
+
return JSON.parse(readFileSync(resolveWorkspacePath(workspaceRoot, pathArg), 'utf8')) as SandboxQemuSetupManifest;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export function applySandboxQemuSetupManifest(manager: WritableConfigManagerLike, manifest: SandboxQemuSetupManifest): void {
|
|
444
|
+
manager.setDynamic('sandbox.vmBackend', 'qemu');
|
|
445
|
+
manager.setDynamic('sandbox.qemuExecWrapper', manifest.recommendedSettings.wrapperPath);
|
|
446
|
+
manager.setDynamic('sandbox.qemuImagePath', manifest.recommendedSettings.imagePath);
|
|
447
|
+
manager.setDynamic('sandbox.qemuGuestHost', manifest.recommendedSettings.guestHost);
|
|
448
|
+
manager.setDynamic('sandbox.qemuGuestPort', manifest.recommendedSettings.guestPort);
|
|
449
|
+
manager.setDynamic('sandbox.qemuGuestUser', manifest.recommendedSettings.guestUser);
|
|
450
|
+
manager.setDynamic('sandbox.qemuWorkspacePath', manifest.recommendedSettings.guestWorkspacePath);
|
|
451
|
+
manager.setDynamic('sandbox.qemuSessionMode', manifest.recommendedSettings.sessionMode);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
export function exportSandboxGuestBundle(
|
|
455
|
+
manager: ConfigManagerLike,
|
|
456
|
+
workspaceRoot: string,
|
|
457
|
+
pathArg: string,
|
|
458
|
+
_options: SandboxProvisioningOptions,
|
|
459
|
+
): { readonly path: string; readonly bundle: SandboxGuestBundle } {
|
|
460
|
+
const path = resolveWorkspacePath(workspaceRoot, pathArg);
|
|
461
|
+
ensureDir(dirname(path));
|
|
462
|
+
const bundle = buildGuestBundle(manager, workspaceRoot, path);
|
|
463
|
+
writeFileSync(path, `${JSON.stringify(bundle, null, 2)}\n`);
|
|
464
|
+
return { path, bundle };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function inspectSandboxGuestBundle(bundle: SandboxGuestBundle): string {
|
|
468
|
+
return [
|
|
469
|
+
'Sandbox guest bundle',
|
|
470
|
+
` wrapper: ${bundle.guest.wrapperPath}`,
|
|
471
|
+
` image: ${bundle.guest.imagePath || '(not configured)'}`,
|
|
472
|
+
` guest: ${bundle.guest.user}@${bundle.guest.host}:${bundle.guest.port}`,
|
|
473
|
+
` workspace: ${bundle.guest.workspacePath}`,
|
|
474
|
+
...bundle.nextSteps.map((step) => ` next: ${step}`),
|
|
475
|
+
].join('\n');
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export function renderSandboxDoctor(manager: ConfigManagerLike): string {
|
|
479
|
+
const probe = probeSandboxBackends(manager);
|
|
480
|
+
return [
|
|
481
|
+
'Sandbox doctor',
|
|
482
|
+
` backend: ${probe.resolvedBackend}`,
|
|
483
|
+
...probe.backends.map((backend) => ` ${backend.id}: ${backend.available ? 'available' : 'missing'} (${backend.detail})`),
|
|
484
|
+
...probe.warnings.map((warning) => ` warning: ${warning}`),
|
|
485
|
+
].join('\n');
|
|
486
|
+
}
|