@pellux/goodvibes-tui 1.13.1 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -0
- package/README.md +6 -6
- package/bin/goodvibes +6 -0
- package/bin/goodvibes-daemon +6 -0
- package/bin/launcher-support.js +34 -1
- package/docs/foundation-artifacts/operator-contract.json +8076 -2047
- package/package.json +15 -11
- package/scripts/postinstall.js +73 -48
- package/src/cli/bundle-command.ts +5 -5
- package/src/cli/completion.ts +1 -1
- package/src/cli/completions/generate.ts +15 -3
- package/src/cli/doctor.ts +385 -0
- package/src/cli/entrypoint.ts +72 -1
- package/src/cli/help.ts +29 -9
- package/src/cli/hooks-command.ts +74 -0
- package/src/cli/hooks-report.ts +167 -0
- package/src/cli/index.ts +3 -0
- package/src/cli/management.ts +13 -1
- package/src/cli/parser.ts +10 -2
- package/src/cli/plugin-bundles-command.ts +228 -0
- package/src/cli/plugin-command.ts +179 -0
- package/src/cli/service-posture.ts +10 -0
- package/src/cli/status.ts +275 -2
- package/src/cli/tui-startup.ts +119 -1
- package/src/cli/types.ts +3 -1
- package/src/config/index.ts +16 -0
- package/src/config/tui-extension-settings.ts +169 -0
- package/src/core/approval-alert.ts +15 -2
- package/src/core/compaction-receipt.ts +127 -0
- package/src/core/model-routing-chip.ts +82 -0
- package/src/core/permission-mode.ts +89 -0
- package/src/core/rewind-receipt.ts +83 -0
- package/src/core/rewind-turn-anchors.ts +160 -0
- package/src/core/scriptable-statusline.ts +141 -0
- package/src/core/session-auto-titler.ts +110 -0
- package/src/core/stream-event-wiring.ts +20 -0
- package/src/core/system-message-router.ts +1 -1
- package/src/core/terminal-notifier.ts +102 -0
- package/src/core/turn-event-wiring.ts +80 -1
- package/src/input/command-reference.ts +96 -0
- package/src/input/command-registry.ts +25 -0
- package/src/input/commands/channel-pairing.ts +245 -0
- package/src/input/commands/channel-runtime.ts +152 -6
- package/src/input/commands/checkin-runtime.ts +89 -0
- package/src/input/commands/checkpoint-runtime.ts +7 -179
- package/src/input/commands/ci-runtime.ts +207 -0
- package/src/input/commands/cost-attribution-format.ts +52 -0
- package/src/input/commands/cost-runtime.ts +44 -4
- package/src/input/commands/editor-runtime.ts +22 -0
- package/src/input/commands/flags-runtime.ts +328 -0
- package/src/input/commands/health-metrics-format.ts +67 -0
- package/src/input/commands/health-runtime.ts +55 -1
- package/src/input/commands/memory.ts +23 -0
- package/src/input/commands/operator-rpc.ts +95 -0
- package/src/input/commands/palette-runtime.ts +22 -0
- package/src/input/commands/permissions-provenance.ts +264 -0
- package/src/input/commands/permissions-runtime.ts +21 -0
- package/src/input/commands/planning-runtime.ts +28 -1
- package/src/input/commands/platform-access-runtime.ts +2 -97
- package/src/input/commands/principals-runtime.ts +212 -0
- package/src/input/commands/product-runtime.ts +43 -3
- package/src/input/commands/recall-files-config.ts +54 -0
- package/src/input/commands/recall-files-git.ts +57 -0
- package/src/input/commands/recall-files.ts +185 -0
- package/src/input/commands/recall-query.ts +8 -3
- package/src/input/commands/recall-review.ts +2 -2
- package/src/input/commands/recall-shared.ts +13 -0
- package/src/input/commands/relay-runtime.ts +86 -0
- package/src/input/commands/review-runtime.ts +160 -0
- package/src/input/commands/rewind-runtime.ts +362 -0
- package/src/input/commands/schedule-nl.ts +147 -0
- package/src/input/commands/schedule-runtime.ts +29 -10
- package/src/input/commands/secret-runtime.ts +55 -0
- package/src/input/commands/session-content.ts +19 -8
- package/src/input/commands/session-workflow.ts +10 -0
- package/src/input/commands/update-runtime.ts +363 -0
- package/src/input/commands/workstream-attempt-dependents.ts +103 -0
- package/src/input/commands/workstream-attempts.ts +244 -0
- package/src/input/commands/workstream-runtime.ts +164 -26
- package/src/input/commands/worktree-runtime.ts +56 -2
- package/src/input/commands.ts +135 -80
- package/src/input/composer-capture.ts +67 -0
- package/src/input/composer-editor.ts +134 -0
- package/src/input/concealed-input.ts +94 -0
- package/src/input/exec-env-scrub-config.ts +84 -0
- package/src/input/feature-unit-layout.ts +142 -0
- package/src/input/feed-context-factory.ts +2 -0
- package/src/input/handler-feed-routes.ts +17 -0
- package/src/input/handler-feed.ts +3 -0
- package/src/input/handler-modal-routes.ts +5 -2
- package/src/input/handler-shortcuts.ts +37 -0
- package/src/input/handler.ts +24 -2
- package/src/input/keybindings.ts +11 -2
- package/src/input/onboarding/onboarding-feature-units.ts +536 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +40 -6
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +324 -629
- package/src/input/onboarding/onboarding-wizard-steps.ts +5 -5
- package/src/input/onboarding/onboarding-wizard-types.ts +6 -0
- package/src/input/relay-settings-descriptions.ts +35 -0
- package/src/input/sandbox-exec-config.ts +107 -0
- package/src/input/settings-modal-adjustment.ts +4 -7
- package/src/input/settings-modal-data.ts +178 -8
- package/src/input/settings-modal-mutations.ts +41 -13
- package/src/input/settings-modal-types.ts +42 -5
- package/src/input/settings-modal.ts +67 -15
- package/src/input/submission-router.ts +4 -1
- package/src/input/worktree-setup-config.ts +93 -0
- package/src/main.ts +43 -43
- package/src/panels/builtin/development.ts +14 -0
- package/src/panels/diff-review-model.ts +189 -0
- package/src/panels/diff-review-panel.ts +395 -0
- package/src/panels/fleet-panel-format.ts +114 -2
- package/src/panels/fleet-panel.ts +65 -78
- package/src/panels/fleet-read-model.ts +91 -11
- package/src/panels/fleet-stop.ts +19 -4
- package/src/panels/modals/memory-modal.ts +11 -2
- package/src/permissions/hunk-selection.ts +86 -5
- package/src/permissions/prompt.ts +108 -7
- package/src/permissions/sandbox-exec-gate.ts +162 -0
- package/src/renderer/settings-modal-helpers.ts +21 -1
- package/src/renderer/settings-modal.ts +70 -44
- package/src/renderer/shell-surface.ts +23 -1
- package/src/renderer/system-message.ts +4 -0
- package/src/renderer/tool-result-summary.ts +95 -5
- package/src/renderer/ui-factory.ts +22 -0
- package/src/runtime/bootstrap-command-context.ts +15 -1
- package/src/runtime/bootstrap-command-parts.ts +14 -2
- package/src/runtime/bootstrap-core.ts +38 -38
- package/src/runtime/bootstrap-notifier-sync.ts +41 -0
- package/src/runtime/bootstrap-shell.ts +8 -0
- package/src/runtime/bootstrap.ts +21 -23
- package/src/runtime/context-accounting-source.ts +103 -0
- package/src/runtime/conversation-rewind-port.ts +130 -0
- package/src/runtime/fleet-needs-input-push.ts +70 -0
- package/src/runtime/install-self-check-startup.ts +36 -0
- package/src/runtime/install-self-check.ts +149 -0
- package/src/runtime/onboarding/fast-path.ts +93 -0
- package/src/runtime/process-lifecycle.ts +32 -24
- package/src/runtime/relay-reachability-bridge.ts +53 -0
- package/src/runtime/release-artifacts.ts +79 -0
- package/src/runtime/services.ts +33 -33
- package/src/runtime/trust/workspace-registration.ts +184 -0
- package/src/runtime/trust/workspace-trust.ts +202 -0
- package/src/runtime/ui-services.ts +2 -1
- package/src/runtime/update-check.ts +113 -0
- package/src/runtime/workspace-checkpointing.ts +32 -0
- package/src/runtime/workstream-attempts-validation.ts +77 -0
- package/src/runtime/workstream-draft-edits.ts +95 -0
- package/src/runtime/workstream-draft-store.ts +101 -0
- package/src/runtime/workstream-draft-types.ts +42 -0
- package/src/runtime/workstream-services.ts +70 -39
- package/src/shell/blocking-input.ts +29 -0
- package/src/shell/ui-openers.ts +65 -0
- package/src/verification/verification-ledger.ts +56 -2
- package/src/version.ts +1 -1
- package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +0 -130
- package/src/runtime/gateway-verbs.ts +0 -47
- package/src/runtime/render-scheduler.ts +0 -90
package/src/cli/status.ts
CHANGED
|
@@ -2,9 +2,11 @@ import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
|
2
2
|
import { resolveDaemonEnabled } from '@pellux/goodvibes-sdk/platform/config';
|
|
3
3
|
import type { OnboardingCheckMarkersState } from '../runtime/onboarding/index.ts';
|
|
4
4
|
import { resolveRuntimeEndpointBinding } from './endpoints.ts';
|
|
5
|
-
import {
|
|
5
|
+
import type { RuntimeEndpointId } from './endpoints.ts';
|
|
6
|
+
import { classifyBindPosture, isNetworkFacing } from './network-posture.ts';
|
|
6
7
|
import type { GoodVibesCliOutputFormat } from './types.ts';
|
|
7
8
|
import type { CliServicePosture } from './service-posture.ts';
|
|
9
|
+
import type { InstallSelfCheckFinding } from '../runtime/install-self-check.ts';
|
|
8
10
|
import { getProviderIdFromModel } from '../config/provider-model.ts';
|
|
9
11
|
|
|
10
12
|
export interface CliStatusOptions {
|
|
@@ -14,8 +16,57 @@ export interface CliStatusOptions {
|
|
|
14
16
|
readonly onboardingMarkers?: OnboardingCheckMarkersState;
|
|
15
17
|
readonly auth?: CliAuthStatus;
|
|
16
18
|
readonly service?: CliServicePosture;
|
|
19
|
+
/** Findings from the install self-check (missing vendor binaries, broken daemon path), computed by the caller. */
|
|
20
|
+
readonly install?: readonly InstallSelfCheckFinding[];
|
|
17
21
|
readonly doctor?: boolean;
|
|
18
22
|
readonly outputFormat?: GoodVibesCliOutputFormat;
|
|
23
|
+
/** Per-workspace trust + registration posture, computed read-only by the caller. */
|
|
24
|
+
readonly workspace?: CliWorkspaceStatus;
|
|
25
|
+
/** Per-command exec sandbox availability, host-probed by the caller. */
|
|
26
|
+
readonly sandbox?: CliSandboxStatus;
|
|
27
|
+
/** Outbound relay reachability posture (config + feature-flag gate only — a one-shot CLI process has no live connection to inspect). */
|
|
28
|
+
readonly relay?: CliRelayStatus;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Config-derived relay posture. No live socket check — see doctor's other surfaces for the same honesty bar. */
|
|
32
|
+
export interface CliRelayStatus {
|
|
33
|
+
readonly configEnabled: boolean;
|
|
34
|
+
readonly featureEnabled: boolean;
|
|
35
|
+
readonly url: string;
|
|
36
|
+
readonly rendezvousId: string;
|
|
37
|
+
readonly requireStepUpForMutations: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Honest per-command exec sandbox posture for the report. */
|
|
41
|
+
export interface CliSandboxStatus {
|
|
42
|
+
/** `sandbox.enabled` config switch. */
|
|
43
|
+
readonly configEnabled: boolean;
|
|
44
|
+
/** The graduation-gated `exec-sandbox` feature flag. */
|
|
45
|
+
readonly featureEnabled: boolean;
|
|
46
|
+
/** Host can actually provide a boundary (bubblewrap present and working). */
|
|
47
|
+
readonly available: boolean;
|
|
48
|
+
readonly backend: 'bubblewrap' | 'none';
|
|
49
|
+
/** Host-probe diagnosis (why unavailable) or one-line availability summary. */
|
|
50
|
+
readonly reason: string;
|
|
51
|
+
/** Whether `--unshare-net` isolation is confirmed on this host, or unknown. */
|
|
52
|
+
readonly networkIsolationGuaranteed: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Read-only snapshot of this workspace's trust gate and registration coverage. */
|
|
56
|
+
export interface CliWorkspaceStatus {
|
|
57
|
+
/** TUI-local trust decision; 'undecided' when no choice is persisted yet. */
|
|
58
|
+
readonly trustLevel: 'trusted' | 'restricted' | 'undecided';
|
|
59
|
+
readonly trustGrandfathered: boolean;
|
|
60
|
+
/** Coverage verdict from the shared workspace registry. */
|
|
61
|
+
readonly registrationStatus: 'covered' | 'declined' | 'unknown';
|
|
62
|
+
/** Normalized working directory resolved against the registry. */
|
|
63
|
+
readonly registrationRoot: string;
|
|
64
|
+
/** Nearest registered root covering this path, or null. */
|
|
65
|
+
readonly registeredBy: string | null;
|
|
66
|
+
/** Coverage inherited through the git worktree→main-repo link. */
|
|
67
|
+
readonly viaWorktreeLink: boolean;
|
|
68
|
+
/** True when the root is too broad to register ($HOME, filesystem root, ~/.goodvibes). */
|
|
69
|
+
readonly registrationBroad: boolean;
|
|
19
70
|
}
|
|
20
71
|
|
|
21
72
|
export interface CliAuthStatus {
|
|
@@ -29,7 +80,7 @@ export interface CliAuthStatus {
|
|
|
29
80
|
|
|
30
81
|
export interface CliDoctorFinding {
|
|
31
82
|
readonly id: string;
|
|
32
|
-
readonly area: 'auth' | 'network' | 'onboarding' | 'security' | 'service' | 'secrets';
|
|
83
|
+
readonly area: 'auth' | 'network' | 'onboarding' | 'security' | 'service' | 'secrets' | 'install' | 'sandbox';
|
|
33
84
|
readonly severity: 'warning' | 'risk';
|
|
34
85
|
readonly summary: string;
|
|
35
86
|
readonly cause: string;
|
|
@@ -69,9 +120,33 @@ export interface CliStatusSnapshot {
|
|
|
69
120
|
readonly scope: string;
|
|
70
121
|
readonly updatedAt: number | null;
|
|
71
122
|
};
|
|
123
|
+
readonly workspace: CliWorkspaceStatus | null;
|
|
124
|
+
readonly sandbox: CliSandboxStatus | null;
|
|
125
|
+
readonly relay: CliRelayStatus | null;
|
|
126
|
+
readonly exposure: readonly CliExposureSurface[];
|
|
72
127
|
readonly findings: readonly CliDoctorFinding[];
|
|
73
128
|
}
|
|
74
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Per-surface exposure report row: what a network surface binds to, how it
|
|
132
|
+
* authenticates callers, and what cross-origin allowlist (if any) applies.
|
|
133
|
+
* A plain report — it changes no behavior and writes no config.
|
|
134
|
+
*/
|
|
135
|
+
export interface CliExposureSurface {
|
|
136
|
+
readonly id: RuntimeEndpointId;
|
|
137
|
+
readonly label: string;
|
|
138
|
+
readonly enabled: boolean;
|
|
139
|
+
/** Bind envelope, e.g. "network 0.0.0.0:3421". */
|
|
140
|
+
readonly bind: string;
|
|
141
|
+
/** Reachability posture label, e.g. "Local only" / "Local Network" / "Custom network". */
|
|
142
|
+
readonly reach: string;
|
|
143
|
+
readonly networkFacing: boolean;
|
|
144
|
+
/** How callers authenticate against this surface when it is network-facing. */
|
|
145
|
+
readonly authMode: string;
|
|
146
|
+
/** Browser-origin allowlist state. Only the control plane has one; others report n/a. */
|
|
147
|
+
readonly originAllowlist: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
75
150
|
function yesNo(value: unknown): string {
|
|
76
151
|
return value === true ? 'yes' : 'no';
|
|
77
152
|
}
|
|
@@ -94,6 +169,146 @@ function bindLine(label: string, enabled: unknown, binding: { readonly hostMode:
|
|
|
94
169
|
return ` ${label}: ${yesNo(enabled)} (${binding.hostMode} ${binding.host}:${binding.port})`;
|
|
95
170
|
}
|
|
96
171
|
|
|
172
|
+
/** Describe registration coverage for the Workspace status section. */
|
|
173
|
+
function registrationLine(workspace: CliWorkspaceStatus): string {
|
|
174
|
+
switch (workspace.registrationStatus) {
|
|
175
|
+
case 'covered':
|
|
176
|
+
return workspace.viaWorktreeLink
|
|
177
|
+
? `registered (via worktree link to ${workspace.registeredBy})`
|
|
178
|
+
: `registered (covered by ${workspace.registeredBy})`;
|
|
179
|
+
case 'declined':
|
|
180
|
+
return 'not registered (declined for this directory)';
|
|
181
|
+
case 'unknown':
|
|
182
|
+
return workspace.registrationBroad
|
|
183
|
+
? 'not registered (root too broad to register)'
|
|
184
|
+
: 'not registered (never asked)';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The `Workspace:` block showing the TUI-local trust gate and shared-registry coverage. */
|
|
189
|
+
function renderWorkspaceSection(workspace: CliWorkspaceStatus | null): string[] {
|
|
190
|
+
if (!workspace) return [];
|
|
191
|
+
return [
|
|
192
|
+
'Workspace:',
|
|
193
|
+
` trust: ${workspace.trustLevel}${workspace.trustGrandfathered ? ' (grandfathered)' : ''}`,
|
|
194
|
+
` registration: ${registrationLine(workspace)}`,
|
|
195
|
+
` root: ${workspace.registrationRoot}`,
|
|
196
|
+
'',
|
|
197
|
+
];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** The `Exec sandbox:` block — honest host-probed availability of the per-command boundary. */
|
|
201
|
+
function renderSandboxSection(sandbox: CliSandboxStatus | null): string[] {
|
|
202
|
+
if (!sandbox) return [];
|
|
203
|
+
const active = sandbox.configEnabled && sandbox.featureEnabled && sandbox.available;
|
|
204
|
+
const network = !sandbox.available
|
|
205
|
+
? 'n/a'
|
|
206
|
+
: sandbox.networkIsolationGuaranteed
|
|
207
|
+
? 'isolation confirmed'
|
|
208
|
+
: 'isolation unknown';
|
|
209
|
+
return [
|
|
210
|
+
'Exec sandbox:',
|
|
211
|
+
` active: ${active ? 'yes' : 'no'}`,
|
|
212
|
+
` configEnabled: ${yesNo(sandbox.configEnabled)}`,
|
|
213
|
+
` featureEnabled: ${yesNo(sandbox.featureEnabled)}`,
|
|
214
|
+
` backend: ${sandbox.backend}`,
|
|
215
|
+
` available: ${yesNo(sandbox.available)}`,
|
|
216
|
+
` network: ${network}`,
|
|
217
|
+
` detail: ${sandbox.reason}`,
|
|
218
|
+
'',
|
|
219
|
+
];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The `Relay:` block — config + feature-flag gate only. This CLI invocation is
|
|
224
|
+
* a separate one-shot process from any running daemon, so it cannot read the
|
|
225
|
+
* daemon's live in-memory connection state (see /relay status in the TUI for
|
|
226
|
+
* that). "active" here means the gate that lets a running daemon register.
|
|
227
|
+
*/
|
|
228
|
+
function renderRelaySection(relay: CliRelayStatus | null): string[] {
|
|
229
|
+
if (!relay) return [];
|
|
230
|
+
const active = relay.configEnabled && relay.featureEnabled;
|
|
231
|
+
return [
|
|
232
|
+
'Relay:',
|
|
233
|
+
` active: ${active ? 'yes (a running daemon will register)' : 'no'}`,
|
|
234
|
+
` configEnabled (relay.enabled): ${yesNo(relay.configEnabled)}`,
|
|
235
|
+
` featureEnabled (relay-connect flag): ${yesNo(relay.featureEnabled)}`,
|
|
236
|
+
` url: ${relay.url || '(not set)'}`,
|
|
237
|
+
` rendezvousId: ${relay.rendezvousId || '(not yet minted)'}`,
|
|
238
|
+
` requireStepUpForMutations: ${yesNo(relay.requireStepUpForMutations)}`,
|
|
239
|
+
' Threat model: the relay operator sees only ciphertext and connection metadata — self-host your own relay for full control.',
|
|
240
|
+
'',
|
|
241
|
+
];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* How callers authenticate against a surface, derived from the auth material
|
|
246
|
+
* the CLI already inspects. A loopback surface trusts the local host; a
|
|
247
|
+
* network-facing surface is authenticated by a local user store and/or an
|
|
248
|
+
* operator token, and reports "none configured" when neither exists.
|
|
249
|
+
*/
|
|
250
|
+
function surfaceAuthMode(networkFacing: boolean, auth: CliAuthStatus | undefined): string {
|
|
251
|
+
if (!networkFacing) return 'loopback (host-local trust)';
|
|
252
|
+
if (!auth) return 'unknown';
|
|
253
|
+
const parts: string[] = [];
|
|
254
|
+
if (auth.userStorePresent) parts.push('local users');
|
|
255
|
+
if (auth.operatorTokenPresent) parts.push('operator token');
|
|
256
|
+
if (parts.length === 0) return 'none configured';
|
|
257
|
+
return parts.join(' + ');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Comma-separated CORS origins, trimmed and emptied of blanks. */
|
|
261
|
+
function parseAllowedOrigins(raw: unknown): string[] {
|
|
262
|
+
return String(raw ?? '')
|
|
263
|
+
.split(',')
|
|
264
|
+
.map((origin) => origin.trim())
|
|
265
|
+
.filter((origin) => origin.length > 0);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* The only browser-origin allowlist in this codebase belongs to the control
|
|
270
|
+
* plane (controlPlane.cors.*). httpListener and web have no origin allowlist
|
|
271
|
+
* concept, so this returns an honest n/a for them.
|
|
272
|
+
*/
|
|
273
|
+
function surfaceOriginAllowlist(id: RuntimeEndpointId, config: Pick<ConfigManager, 'get'>): string {
|
|
274
|
+
if (id !== 'controlPlane') return 'n/a (no origin allowlist for this surface)';
|
|
275
|
+
if (config.get('controlPlane.cors.enabled') !== true) return 'CORS off (same-origin only)';
|
|
276
|
+
const origins = parseAllowedOrigins(config.get('controlPlane.cors.allowedOrigins'));
|
|
277
|
+
if (origins.length === 0) return 'CORS on, empty allowlist (refuses all cross-origin)';
|
|
278
|
+
if (origins.includes('*')) return `CORS on, WILDCARD any-origin (${origins.join(', ')})`;
|
|
279
|
+
return `CORS on, allowlist: ${origins.join(', ')}`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const EXPOSURE_SURFACES: readonly { readonly id: RuntimeEndpointId; readonly label: string; readonly enabledKey: string }[] = [
|
|
283
|
+
{ id: 'controlPlane', label: 'controlPlane', enabledKey: 'controlPlane.enabled' },
|
|
284
|
+
{ id: 'httpListener', label: 'httpListener', enabledKey: 'danger.httpListener' },
|
|
285
|
+
{ id: 'web', label: 'web', enabledKey: 'web.enabled' },
|
|
286
|
+
];
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Build the per-surface exposure report (bind address, auth mode, origin
|
|
290
|
+
* allowlist) for every network surface. Pure over the status options — a
|
|
291
|
+
* report only, no behavior change.
|
|
292
|
+
*/
|
|
293
|
+
export function buildCliExposureReport(options: CliStatusOptions): readonly CliExposureSurface[] {
|
|
294
|
+
const config = options.configManager;
|
|
295
|
+
return EXPOSURE_SURFACES.map((surface): CliExposureSurface => {
|
|
296
|
+
const enabled = config.get(surface.enabledKey as never) === true;
|
|
297
|
+
const binding = resolveRuntimeEndpointBinding(config, surface.id);
|
|
298
|
+
const networkFacing = isNetworkFacing(enabled, binding);
|
|
299
|
+
return {
|
|
300
|
+
id: surface.id,
|
|
301
|
+
label: surface.label,
|
|
302
|
+
enabled,
|
|
303
|
+
bind: `${binding.hostMode} ${binding.host}:${binding.port}`,
|
|
304
|
+
reach: classifyBindPosture(binding).label,
|
|
305
|
+
networkFacing,
|
|
306
|
+
authMode: surfaceAuthMode(networkFacing, options.auth),
|
|
307
|
+
originAllowlist: surfaceOriginAllowlist(surface.id, config),
|
|
308
|
+
};
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
97
312
|
export function buildCliDoctorFindings(options: CliStatusOptions): readonly CliDoctorFinding[] {
|
|
98
313
|
const config = options.configManager;
|
|
99
314
|
const serviceEnabled = config.get('service.enabled') === true;
|
|
@@ -241,6 +456,49 @@ export function buildCliDoctorFindings(options: CliStatusOptions): readonly CliD
|
|
|
241
456
|
});
|
|
242
457
|
}
|
|
243
458
|
|
|
459
|
+
if (isNetworkFacing(controlPlaneEnabled, controlPlaneBinding)
|
|
460
|
+
&& config.get('controlPlane.cors.enabled') === true
|
|
461
|
+
&& parseAllowedOrigins(config.get('controlPlane.cors.allowedOrigins')).includes('*')) {
|
|
462
|
+
findings.push({
|
|
463
|
+
id: 'control-plane-cors-wildcard-origin',
|
|
464
|
+
area: 'network',
|
|
465
|
+
severity: 'risk',
|
|
466
|
+
summary: 'The control plane accepts cross-origin requests from any origin.',
|
|
467
|
+
cause: `controlPlane is ${controlPlaneBinding.hostMode}-bound on ${controlPlaneBinding.host}:${controlPlaneBinding.port}, controlPlane.cors.enabled is true, and controlPlane.cors.allowedOrigins contains a wildcard "*".`,
|
|
468
|
+
impact: 'Any website a signed-in operator visits could make credentialed cross-origin calls to the control plane.',
|
|
469
|
+
action: 'Replace the wildcard with an explicit list of trusted browser origins in controlPlane.cors.allowedOrigins.',
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (options.install) {
|
|
474
|
+
for (const finding of options.install) {
|
|
475
|
+
findings.push({
|
|
476
|
+
id: `install-${finding.id}`,
|
|
477
|
+
area: 'install',
|
|
478
|
+
severity: 'warning',
|
|
479
|
+
summary: finding.summary,
|
|
480
|
+
cause: finding.detail,
|
|
481
|
+
impact: 'The daemon and any background surfaces (control plane, listener, web) may fail to start until the install is repaired.',
|
|
482
|
+
action: `Repair this install by running: ${finding.repairCommand}`,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Exec sandbox requested but the host cannot provide a boundary: the exec tool
|
|
488
|
+
// silently runs unsandboxed and the approval flow keeps prompting. Flag it so
|
|
489
|
+
// the operator knows the boundary they turned on is not in force.
|
|
490
|
+
if (options.sandbox && options.sandbox.configEnabled && options.sandbox.featureEnabled && !options.sandbox.available) {
|
|
491
|
+
findings.push({
|
|
492
|
+
id: 'exec-sandbox-unavailable',
|
|
493
|
+
area: 'sandbox',
|
|
494
|
+
severity: 'warning',
|
|
495
|
+
summary: 'The per-command exec sandbox is enabled but unavailable on this host.',
|
|
496
|
+
cause: options.sandbox.reason,
|
|
497
|
+
impact: 'Exec commands run unsandboxed and boundary-safe commands still prompt instead of auto-allowing.',
|
|
498
|
+
action: 'Install bubblewrap (bwrap) on Linux, or disable sandbox.enabled to stop requesting a boundary this host cannot provide.',
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
244
502
|
return findings;
|
|
245
503
|
}
|
|
246
504
|
|
|
@@ -283,6 +541,10 @@ export function buildCliStatusSnapshot(options: CliStatusOptions): CliStatusSnap
|
|
|
283
541
|
scope: marker?.scope ?? 'none',
|
|
284
542
|
updatedAt: marker?.payload?.updatedAt ?? null,
|
|
285
543
|
},
|
|
544
|
+
workspace: options.workspace ?? null,
|
|
545
|
+
sandbox: options.sandbox ?? null,
|
|
546
|
+
relay: options.relay ?? null,
|
|
547
|
+
exposure: buildCliExposureReport(options),
|
|
286
548
|
findings,
|
|
287
549
|
};
|
|
288
550
|
}
|
|
@@ -349,6 +611,17 @@ export function renderCliStatus(options: CliStatusOptions): string {
|
|
|
349
611
|
` checked: ${marker?.exists ? 'yes' : 'no'}`,
|
|
350
612
|
` scope: ${marker?.scope ?? 'none'}`,
|
|
351
613
|
` updatedAt: ${marker?.payload ? new Date(marker.payload.updatedAt).toISOString() : 'n/a'}`,
|
|
614
|
+
'',
|
|
615
|
+
...renderWorkspaceSection(snapshot.workspace),
|
|
616
|
+
...renderSandboxSection(snapshot.sandbox),
|
|
617
|
+
...renderRelaySection(snapshot.relay),
|
|
618
|
+
'Exposure (report only — no changes made):',
|
|
619
|
+
...snapshot.exposure.flatMap((surface) => [
|
|
620
|
+
` ${surface.label}: ${yesNo(surface.enabled)} · ${surface.reach}${surface.networkFacing ? ' · network-facing' : ''}`,
|
|
621
|
+
` bind: ${surface.bind}`,
|
|
622
|
+
` auth: ${surface.authMode}`,
|
|
623
|
+
` originAllowlist: ${surface.originAllowlist}`,
|
|
624
|
+
]),
|
|
352
625
|
];
|
|
353
626
|
|
|
354
627
|
if (options.doctor) {
|
package/src/cli/tui-startup.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { CommandContext, CommandRegistry } from '../input/command-registry.ts';
|
|
2
2
|
import type { InputHandler } from '../input/handler.ts';
|
|
3
|
+
import type { SelectionItem } from '../input/selection-modal.ts';
|
|
3
4
|
import { hasResumableWizardProgress, readOnboardingCheckMarker, readWizardProgress } from '../runtime/onboarding/index.ts';
|
|
5
|
+
import { startOnboardingFastPath } from '../runtime/onboarding/fast-path.ts';
|
|
4
6
|
import { readLastSessionPointer } from '@/runtime/index.ts';
|
|
5
7
|
import type { GoodVibesCliParseResult } from './types.ts';
|
|
6
8
|
|
|
@@ -9,6 +11,115 @@ export type TuiStartupShellPaths = Parameters<typeof readOnboardingCheckMarker>[
|
|
|
9
11
|
readonly homeDirectory: string;
|
|
10
12
|
};
|
|
11
13
|
|
|
14
|
+
export type FirstOpenTrustLevel = 'trusted' | 'restricted';
|
|
15
|
+
|
|
16
|
+
/** The decisions a first-open prompt choice resolves to. Absent halves were not asked. */
|
|
17
|
+
export interface FirstOpenDecision {
|
|
18
|
+
readonly trust?: FirstOpenTrustLevel;
|
|
19
|
+
readonly register?: 'yes' | 'no';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* decodeFirstOpenChoice — pure mapping from a chosen selection-item id (or null
|
|
24
|
+
* on Escape/enter-through) to the trust + registration decisions, given which
|
|
25
|
+
* halves the prompt actually offered. Kept pure so the combined-prompt semantics
|
|
26
|
+
* are unit-testable without a live modal.
|
|
27
|
+
*
|
|
28
|
+
* Escape/enter-through (id === null) is the safe default: an offered trust half
|
|
29
|
+
* settles to 'restricted' (matching the standalone trust prompt), and an offered
|
|
30
|
+
* register half defaults to NO — recorded as a subtree-scoped decline so the
|
|
31
|
+
* directory is not re-asked every startup.
|
|
32
|
+
*/
|
|
33
|
+
export function decodeFirstOpenChoice(
|
|
34
|
+
id: string | null,
|
|
35
|
+
offered: { readonly trustNeeded: boolean; readonly registerNeeded: boolean },
|
|
36
|
+
): FirstOpenDecision {
|
|
37
|
+
const decision: { trust?: FirstOpenTrustLevel; register?: 'yes' | 'no' } = {};
|
|
38
|
+
if (offered.trustNeeded && offered.registerNeeded) {
|
|
39
|
+
// Combined 2x2 choice.
|
|
40
|
+
const trust: FirstOpenTrustLevel = id === 'trust-register' || id === 'trust-only' ? 'trusted' : 'restricted';
|
|
41
|
+
const register: 'yes' | 'no' = id === 'trust-register' || id === 'restrict-register' ? 'yes' : 'no';
|
|
42
|
+
decision.trust = trust;
|
|
43
|
+
decision.register = register;
|
|
44
|
+
} else if (offered.trustNeeded) {
|
|
45
|
+
decision.trust = id === 'trusted' ? 'trusted' : 'restricted';
|
|
46
|
+
} else if (offered.registerNeeded) {
|
|
47
|
+
decision.register = id === 'register' ? 'yes' : 'no';
|
|
48
|
+
}
|
|
49
|
+
return decision;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Build the selection rows for a first-open prompt given which halves apply. */
|
|
53
|
+
export function buildFirstOpenItems(offered: {
|
|
54
|
+
readonly trustNeeded: boolean;
|
|
55
|
+
readonly registerNeeded: boolean;
|
|
56
|
+
}): { readonly title: string; readonly items: SelectionItem[] } {
|
|
57
|
+
if (offered.trustNeeded && offered.registerNeeded) {
|
|
58
|
+
return {
|
|
59
|
+
title: 'New workspace — trust level and registration',
|
|
60
|
+
items: [
|
|
61
|
+
{ id: 'trust-register', label: 'Trust & register this workspace', detail: 'Full capability, and track it in your workspace registry', primaryAction: 'select' },
|
|
62
|
+
{ id: 'trust-only', label: 'Trust, don\'t register', detail: 'Full capability; leave it out of the registry', primaryAction: 'select' },
|
|
63
|
+
{ id: 'restrict-register', label: 'Keep restricted, register', detail: 'Read-only for now; still track it in the registry', primaryAction: 'select' },
|
|
64
|
+
{ id: 'restrict-only', label: 'Keep restricted, don\'t register', detail: 'Read-only exploration; nothing recorded (default)', primaryAction: 'select' },
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (offered.trustNeeded) {
|
|
69
|
+
return {
|
|
70
|
+
title: 'New workspace — choose a trust level',
|
|
71
|
+
items: [
|
|
72
|
+
{ id: 'trusted', label: 'Trust this workspace', detail: 'Full capability — all tools may run', primaryAction: 'select' },
|
|
73
|
+
{ id: 'restricted', label: 'Keep restricted (read-only)', detail: 'Explore safely; writes and commands are denied until trusted', primaryAction: 'select' },
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
title: 'Register this directory as a workspace?',
|
|
79
|
+
items: [
|
|
80
|
+
{ id: 'register', label: 'Register this workspace', detail: 'Track this project root in your workspace registry', primaryAction: 'select' },
|
|
81
|
+
{ id: 'skip', label: 'Don\'t register', detail: 'Leave it out; not asked again for this directory (default)', primaryAction: 'select' },
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* First-open workspace prompt — one surface folding the TUI-local trust gate and
|
|
88
|
+
* the platform-wide registration half. When GoodVibes opens a workspace it has
|
|
89
|
+
* no trust decision for, and/or one whose registration resolves UNKNOWN, ask
|
|
90
|
+
* once (never two stacked modals): trust level AND "register this directory?".
|
|
91
|
+
*
|
|
92
|
+
* The two records are independent: a grandfathered-trusted workspace skips the
|
|
93
|
+
* trust half but may still see the register half once; a covered/declined/broad
|
|
94
|
+
* directory skips the register half. Escape/enter-through takes the safe default
|
|
95
|
+
* (restricted, and a recorded decline for the register half).
|
|
96
|
+
*/
|
|
97
|
+
async function promptWorkspaceFirstOpen(commandContext: CommandContext, render: () => void): Promise<void> {
|
|
98
|
+
const trustManager = commandContext.workspace?.workspaceTrustManager;
|
|
99
|
+
const registrationManager = commandContext.workspace?.workspaceRegistrationManager;
|
|
100
|
+
const trustNeeded = Boolean(trustManager && !trustManager.isDecided());
|
|
101
|
+
const evaluation = registrationManager ? await registrationManager.evaluate() : null;
|
|
102
|
+
const registerNeeded = Boolean(evaluation?.offerRegister);
|
|
103
|
+
if (!trustNeeded && !registerNeeded) return;
|
|
104
|
+
|
|
105
|
+
const offered = { trustNeeded, registerNeeded };
|
|
106
|
+
const { title, items } = buildFirstOpenItems(offered);
|
|
107
|
+
commandContext.openSelection?.(
|
|
108
|
+
title,
|
|
109
|
+
items,
|
|
110
|
+
{ allowSearch: false, primaryVerbLabel: 'Choose' },
|
|
111
|
+
(result) => {
|
|
112
|
+
const decision = decodeFirstOpenChoice(result?.item.id ?? null, offered);
|
|
113
|
+
const apply = async () => {
|
|
114
|
+
if (decision.trust && trustManager) await trustManager.setLevel(decision.trust);
|
|
115
|
+
if (decision.register === 'yes' && registrationManager) await registrationManager.register();
|
|
116
|
+
else if (decision.register === 'no' && registrationManager) await registrationManager.decline();
|
|
117
|
+
};
|
|
118
|
+
void apply().finally(render);
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
12
123
|
export function applyInitialTuiCliState(options: {
|
|
13
124
|
readonly cli: GoodVibesCliParseResult;
|
|
14
125
|
readonly input: InputHandler;
|
|
@@ -70,7 +181,9 @@ export function applyInitialTuiCliState(options: {
|
|
|
70
181
|
.then(() => render());
|
|
71
182
|
}
|
|
72
183
|
} else if (!globalOnboardingMarker.exists) {
|
|
73
|
-
|
|
184
|
+
// Fast path: get a brand-new user to a working session in the fewest steps.
|
|
185
|
+
// Falls back to the full wizard when the surface can't detect providers.
|
|
186
|
+
startOnboardingFastPath({ input, commandContext, shellPaths, render });
|
|
74
187
|
} else {
|
|
75
188
|
// User has completed onboarding before but left a wizard session in progress.
|
|
76
189
|
// Reopen the wizard at the last saved step so they can continue or dismiss.
|
|
@@ -89,6 +202,11 @@ export function applyInitialTuiCliState(options: {
|
|
|
89
202
|
},
|
|
90
203
|
});
|
|
91
204
|
}
|
|
205
|
+
} else {
|
|
206
|
+
// Returning user, no wizard to resume: this is the moment to ask, in one
|
|
207
|
+
// surface, about a genuinely new workspace's trust level and whether to
|
|
208
|
+
// register it — before they run anything here.
|
|
209
|
+
void promptWorkspaceFirstOpen(commandContext, render);
|
|
92
210
|
}
|
|
93
211
|
}
|
|
94
212
|
}
|
package/src/cli/types.ts
CHANGED
package/src/config/index.ts
CHANGED
|
@@ -39,6 +39,22 @@ export function isAutoApproveEnabled(configManager: Pick<ConfigManager, 'get'>):
|
|
|
39
39
|
return configManager.get('behavior.autoApprove');
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* True when the current permission posture effectively auto-approves everything:
|
|
44
|
+
* auto-approve on, permission mode `allow-all`, or a `custom` mode whose every
|
|
45
|
+
* tool action is `allow`. Drives the footer's danger indicator.
|
|
46
|
+
*/
|
|
47
|
+
export function isEffectiveDangerMode(configManager: Pick<ConfigManager, 'get' | 'getCategory'>): boolean {
|
|
48
|
+
if (configManager.get('behavior.autoApprove')) return true;
|
|
49
|
+
const permMode = configManager.get('permissions.mode');
|
|
50
|
+
if (permMode === 'allow-all') return true;
|
|
51
|
+
if (permMode === 'custom') {
|
|
52
|
+
const tools = configManager.getCategory('permissions').tools;
|
|
53
|
+
if (Object.values(tools).every((action) => action === 'allow')) return true;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
42
58
|
export function getWorkingDirectory(configManager: Pick<ConfigManager, 'getWorkingDirectory'>): string | null {
|
|
43
59
|
return configManager.getWorkingDirectory();
|
|
44
60
|
}
|