@pellux/goodvibes-tui 1.16.0 → 1.17.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 +95 -9
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +1091 -271
- package/package.json +7 -7
- package/src/cli/completions/generate.ts +5 -4
- package/src/cli/config-overrides.ts +51 -11
- package/src/cli/doctor.ts +4 -5
- package/src/cli/entrypoint.ts +13 -8
- package/src/cli/help.ts +7 -6
- package/src/cli/launch-auto-update.ts +214 -0
- package/src/cli/parser.ts +5 -0
- package/src/cli/status.ts +13 -0
- package/src/cli/surface-command.ts +5 -5
- package/src/cli/tui-startup.ts +60 -97
- package/src/cli/types.ts +2 -0
- package/src/config/index.ts +1 -1
- package/src/config/tui-extension-settings.ts +33 -0
- package/src/core/composer-state.ts +1 -1
- package/src/core/conversation-line-cache.ts +3 -3
- package/src/core/conversation-rendering.ts +4 -4
- package/src/core/conversation.ts +2 -2
- package/src/core/long-task-notifier.ts +7 -7
- package/src/core/stream-event-wiring.ts +8 -6
- package/src/core/system-message-noise.ts +1 -1
- package/src/core/system-message-router.ts +6 -6
- package/src/core/turn-cancellation.ts +1 -1
- package/src/core/turn-event-wiring.ts +8 -8
- package/src/daemon/cli.ts +20 -5
- package/src/daemon/lifecycle.ts +41 -0
- package/src/daemon/service-commands.ts +5 -5
- package/src/export/cost-utils.ts +128 -6
- package/src/input/autocomplete.ts +2 -2
- package/src/input/command-registry.ts +34 -9
- package/src/input/commands/ci-runtime.ts +5 -1
- package/src/input/commands/codebase-runtime.ts +6 -6
- package/src/input/commands/cost-runtime.ts +1 -1
- package/src/input/commands/health-runtime.ts +2 -2
- package/src/input/commands/intelligence-runtime.ts +1 -1
- package/src/input/commands/local-auth-runtime.ts +1 -1
- package/src/input/commands/local-provider-runtime.ts +49 -10
- package/src/input/commands/local-runtime.ts +53 -5
- package/src/input/commands/memory.ts +2 -2
- package/src/input/commands/operator-panel-runtime.ts +5 -5
- package/src/input/commands/permissions-provenance.ts +1 -1
- package/src/input/commands/permissions-runtime.ts +99 -8
- package/src/input/commands/planning-runtime.ts +3 -3
- package/src/input/commands/platform-sandbox-runtime.ts +1 -1
- package/src/input/commands/provider-accounts-runtime.ts +71 -6
- package/src/input/commands/provider.ts +7 -5
- package/src/input/commands/recall-files-git.ts +13 -51
- package/src/input/commands/recall-files.ts +8 -11
- package/src/input/commands/recall-review.ts +2 -2
- package/src/input/commands/relay-runtime.ts +1 -1
- package/src/input/commands/remote-runtime.ts +1 -1
- package/src/input/commands/runtime-services.ts +4 -4
- package/src/input/commands/schedule-runtime.ts +14 -7
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-workflow.ts +2 -2
- package/src/input/commands/settings-sync-runtime.ts +2 -2
- package/src/input/commands/share-runtime.ts +1 -1
- package/src/input/commands/shell-core.ts +46 -68
- package/src/input/commands/subscription-runtime.ts +1 -1
- package/src/input/commands/update-runtime.ts +153 -96
- package/src/input/commands/work-plan-runtime.ts +1 -1
- package/src/input/commands/workstream-runtime.ts +2 -2
- package/src/input/commands.ts +0 -2
- package/src/input/config-modal-types.ts +3 -3
- package/src/input/config-modal.ts +10 -11
- package/src/input/feature-unit-layout.ts +93 -82
- package/src/input/feed-context-factory.ts +2 -2
- package/src/input/handler-command-route.ts +2 -2
- package/src/input/handler-content-actions.ts +1 -1
- package/src/input/handler-feed-routes.ts +7 -7
- package/src/input/handler-feed.ts +4 -4
- package/src/input/handler-interactions.ts +3 -2
- package/src/input/handler-modal-routes.ts +12 -4
- package/src/input/handler-modal-token-routes.ts +1 -1
- package/src/input/handler-onboarding-daemon-adopt.ts +2 -2
- package/src/input/handler-onboarding.ts +3 -2
- package/src/input/handler-picker-routes.ts +26 -13
- package/src/input/handler-shortcuts.ts +6 -6
- package/src/input/handler.ts +2 -2
- package/src/input/keybindings.ts +2 -2
- package/src/input/onboarding/onboarding-feature-units.ts +34 -27
- package/src/input/onboarding/onboarding-provider-key-fields.ts +112 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +22 -21
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +17 -0
- package/src/input/onboarding/onboarding-wizard-network-adopt.ts +2 -2
- package/src/input/onboarding/onboarding-wizard-steps.ts +30 -42
- package/src/input/panel-integration-actions.ts +2 -2
- package/src/input/panel-mouse-geometry.ts +1 -1
- package/src/input/panel-paste-flood-guard.ts +1 -1
- package/src/input/provider-key-intake.ts +167 -0
- package/src/input/sandbox-exec-config.ts +2 -2
- package/src/input/selection-modal.ts +1 -1
- package/src/input/settings-modal-activation.ts +5 -0
- package/src/input/settings-modal-adjustment.ts +4 -31
- package/src/input/settings-modal-data.ts +22 -21
- package/src/input/settings-modal-mutations.ts +12 -68
- package/src/input/settings-modal-types.ts +23 -16
- package/src/input/settings-modal.ts +74 -55
- package/src/main.ts +48 -48
- package/src/panels/agent-inspector-shared.ts +4 -4
- package/src/panels/base-panel.ts +2 -2
- package/src/panels/builtin/agent.ts +4 -4
- package/src/panels/builtin/development.ts +4 -4
- package/src/panels/builtin/knowledge.ts +1 -1
- package/src/panels/builtin/operations.ts +29 -10
- package/src/panels/builtin/session.ts +5 -5
- package/src/panels/builtin/shared.ts +4 -4
- package/src/panels/builtin-modals.ts +10 -10
- package/src/panels/builtin-panels.ts +1 -1
- package/src/panels/cost-tracker-panel.ts +124 -7
- package/src/panels/diff-panel.ts +5 -5
- package/src/panels/eval-registry.ts +2 -2
- package/src/panels/fleet-deep-link.ts +1 -1
- package/src/panels/fleet-panel-format.ts +44 -8
- package/src/panels/fleet-panel.ts +16 -16
- package/src/panels/fleet-read-model.ts +52 -13
- package/src/panels/fleet-steer.ts +2 -2
- package/src/panels/fleet-stop.ts +3 -3
- package/src/panels/fleet-tabs.ts +13 -14
- package/src/panels/fleet-transcript.ts +5 -5
- package/src/panels/git-panel.ts +2 -2
- package/src/panels/index.ts +4 -1
- package/src/panels/local-auth-panel.ts +1 -1
- package/src/panels/modals/hooks-modal.ts +1 -1
- package/src/panels/modals/keybindings-modal.ts +2 -2
- package/src/panels/modals/knowledge-modal.ts +1 -1
- package/src/panels/modals/marketplace-modal.ts +4 -4
- package/src/panels/modals/memory-modal.ts +4 -4
- package/src/panels/modals/modal-surface-helpers.ts +1 -1
- package/src/panels/modals/modal-theme.ts +1 -1
- package/src/panels/modals/pairing-modal.ts +2 -2
- package/src/panels/modals/planning-modal.ts +2 -2
- package/src/panels/modals/plugins-modal.ts +1 -1
- package/src/panels/modals/policy-modal.ts +1 -1
- package/src/panels/modals/security-modal.ts +1 -1
- package/src/panels/modals/skills-modal.ts +1 -1
- package/src/panels/modals/work-plan-modal.ts +3 -3
- package/src/panels/notifications-feed.ts +127 -0
- package/src/panels/notifications-panel.ts +160 -0
- package/src/panels/panel-manager.ts +9 -9
- package/src/panels/plugins-panel.ts +4 -4
- package/src/panels/project-planning-answer-actions.ts +1 -1
- package/src/panels/provider-health-routes.ts +2 -2
- package/src/panels/skills-panel.ts +4 -5
- package/src/panels/token-budget-panel.ts +11 -3
- package/src/panels/types.ts +5 -5
- package/src/permissions/broker-approval-card.ts +201 -0
- package/src/permissions/hunk-selection.ts +28 -4
- package/src/permissions/prompt.ts +244 -29
- package/src/renderer/autocomplete-overlay.ts +107 -54
- package/src/renderer/code-block.ts +2 -2
- package/src/renderer/compaction-history-modal.ts +1 -1
- package/src/renderer/compaction-preview.ts +1 -1
- package/src/renderer/compaction-quality.ts +1 -1
- package/src/renderer/compositor.ts +2 -2
- package/src/renderer/config-modal.ts +1 -1
- package/src/renderer/conversation-overlays.ts +1 -1
- package/src/renderer/footer-tips.ts +3 -3
- package/src/renderer/hint-grammar.ts +1 -1
- package/src/renderer/modal-factory.ts +1 -1
- package/src/renderer/model-picker-overlay.ts +1 -1
- package/src/renderer/model-workspace.ts +7 -2
- package/src/renderer/onboarding/onboarding-wizard.ts +22 -8
- package/src/renderer/panel-composite.ts +2 -2
- package/src/renderer/panel-workspace-bar.ts +1 -1
- package/src/renderer/process-indicator.ts +1 -1
- package/src/renderer/selection-modal-overlay.ts +102 -24
- package/src/renderer/session-picker-modal.ts +2 -2
- package/src/renderer/settings-modal-helpers.ts +14 -5
- package/src/renderer/settings-modal.ts +127 -39
- package/src/renderer/shell-surface.ts +3 -0
- package/src/renderer/terminal-bg-probe.ts +1 -1
- package/src/renderer/theme.ts +3 -3
- package/src/renderer/tool-call.ts +1 -1
- package/src/renderer/tool-result-summary.ts +1 -1
- package/src/renderer/turn-injection.ts +7 -6
- package/src/renderer/ui-factory.ts +6 -1
- package/src/runtime/bootstrap-command-context.ts +4 -1
- package/src/runtime/bootstrap-command-parts.ts +6 -4
- package/src/runtime/bootstrap-core.ts +35 -35
- package/src/runtime/bootstrap-hook-bridge.ts +1 -1
- package/src/runtime/bootstrap-shell.ts +6 -5
- package/src/runtime/bootstrap.ts +34 -32
- package/src/runtime/channel-composition.ts +46 -0
- package/src/runtime/code-index-services.ts +3 -4
- package/src/runtime/daemon-attach-notices.ts +120 -0
- package/src/runtime/durability-services.ts +55 -0
- package/src/runtime/embedded-service-factories.ts +40 -0
- package/src/runtime/feature-settings.ts +147 -0
- package/src/runtime/index.ts +4 -21
- package/src/runtime/legacy-daemon-migration.ts +29 -19
- package/src/runtime/memory-fold.ts +1 -1
- package/src/runtime/notification-dispatch.ts +110 -0
- package/src/runtime/onboarding/apply.ts +3 -29
- package/src/runtime/onboarding/fast-path.ts +39 -6
- package/src/runtime/onboarding/provider-key-capture.ts +126 -0
- package/src/runtime/onboarding/snapshot.ts +0 -1
- package/src/runtime/onboarding/types.ts +9 -3
- package/src/runtime/provider-fallback.ts +55 -0
- package/src/runtime/provider-optimizer-wiring.ts +55 -0
- package/src/runtime/release-artifacts.ts +19 -103
- package/src/runtime/resume-notice.ts +1 -1
- package/src/runtime/services.ts +81 -82
- package/src/runtime/session-continuity-hints.ts +38 -0
- package/src/runtime/surface-feature-flags.ts +40 -9
- package/src/runtime/terminal-output-guard.ts +2 -2
- package/src/runtime/trust/workspace-registration.ts +10 -2
- package/src/runtime/trust/workspace-trust.ts +58 -31
- package/src/runtime/ui-services.ts +3 -1
- package/src/runtime/update-check.ts +15 -69
- package/src/runtime/workstream-services.ts +2 -2
- package/src/shell/blocking-input.ts +102 -6
- package/src/shell/ui-openers.ts +38 -6
- package/src/utils/format-duration.ts +1 -1
- package/src/verification/verification-ledger.ts +21 -4
- package/src/version.ts +1 -1
- package/src/input/commands/flags-runtime.ts +0 -328
package/src/runtime/services.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { join } from 'node:path';
|
|
|
2
2
|
import { FocusTracker } from '../core/focus-tracker.ts';
|
|
3
3
|
import { ConfigManager, ServiceRegistry, SubscriptionManager, ToolLLM } from '@pellux/goodvibes-sdk/platform/config';
|
|
4
4
|
import { SecretsManager } from '../config/secrets.ts';
|
|
5
|
-
import { AutomationDeliveryManager, AutomationManager
|
|
6
|
-
import { ChannelDeliveryRouter,
|
|
5
|
+
import { AutomationDeliveryManager, AutomationManager } from '@pellux/goodvibes-sdk/platform/automation';
|
|
6
|
+
import { ChannelDeliveryRouter, ChannelPolicyManager, type ChannelPluginRegistry, type RouteBindingManager, type SurfaceRegistry } from '@pellux/goodvibes-sdk/platform/channels';
|
|
7
7
|
import { ApprovalBroker, GatewayMethodCatalog, SharedSessionBroker } from '@pellux/goodvibes-sdk/platform/control-plane';
|
|
8
8
|
import { StepUpService } from '@pellux/goodvibes-sdk/daemon';
|
|
9
9
|
import { attachWsOnlyGatewayVerbHandlers, createArchivableFleetRegistry } from '@pellux/goodvibes-terminal-shell';
|
|
@@ -26,6 +26,12 @@ import { MultimodalService } from '@pellux/goodvibes-sdk/platform/multimodal';
|
|
|
26
26
|
import { AgentMessageBus, AgentOrchestrator, ArchetypeLoader, WrfcController } from '@pellux/goodvibes-sdk/platform/agents';
|
|
27
27
|
import { AgentManager, ContextAccountingHolder, OverflowHandler, ProcessManager, createWorkflowServices, type WorkflowServices } from '@pellux/goodvibes-sdk/platform/tools';
|
|
28
28
|
import { FileStateCache, FileUndoManager, MemoryEmbeddingProviderRegistry, MemoryRegistry, MemoryStore, ModeManager, ProjectIndex, resolveCanonicalMemoryDbPath, type CodeIndexStore, type CodeIndexReindexScheduler } from '@pellux/goodvibes-sdk/platform/state';
|
|
29
|
+
import type { StoreSnapshotScheduler } from '@pellux/goodvibes-sdk/platform/state/store-snapshots';
|
|
30
|
+
import type { UserPermissionRuleStore } from '@pellux/goodvibes-sdk/platform/permissions';
|
|
31
|
+
import { buildExecPromptAnswerHandler } from '@pellux/goodvibes-sdk/platform/runtime/permissions/exec-prompt-wiring';
|
|
32
|
+
import { buildLocalhostFetchApproval } from '@pellux/goodvibes-sdk/platform/runtime/permissions/localhost-fetch-approval';
|
|
33
|
+
import { createNotificationDispatcher, wireRuntimeNotificationBridge, type NotificationDispatcher } from './notification-dispatch.ts';
|
|
34
|
+
import { createDurabilityServices } from './durability-services.ts';
|
|
29
35
|
import { MemorySpineClient, createLocalMemoryAccess } from '@pellux/goodvibes-sdk/platform/runtime/memory-spine';
|
|
30
36
|
import { WorkspaceCheckpointManager } from '@pellux/goodvibes-sdk/platform/workspace';
|
|
31
37
|
import { createWorkspaceCheckpointing } from './workspace-checkpointing.ts';
|
|
@@ -49,20 +55,18 @@ import { SessionManager, CrossSessionTaskRegistry, SessionChangeTracker } from '
|
|
|
49
55
|
import { ApiTokenAuditor, UserAuthManager } from '@pellux/goodvibes-sdk/platform/security';
|
|
50
56
|
import { WebhookNotifier } from '@pellux/goodvibes-sdk/platform/integrations';
|
|
51
57
|
import { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp';
|
|
52
|
-
import { BenchmarkStore, CacheHitTracker,
|
|
58
|
+
import { BenchmarkStore, CacheHitTracker, computeUsageCostUsd, FavoritesStore, ModelLimitsService, ProviderCapabilityRegistry, ProviderOptimizer, ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
|
|
53
59
|
import { KeybindingsManager } from '../input/keybindings.ts';
|
|
54
60
|
import { AdaptivePlanner, DeterministicReplayEngine, ExecutionPlanManager, SessionLineageTracker, SessionMemoryStore } from '@pellux/goodvibes-sdk/platform/core';
|
|
55
|
-
import {
|
|
61
|
+
import { deriveFeatureStates, bindFeatureSettingsBridge } from '@pellux/goodvibes-sdk/platform/runtime/state';
|
|
62
|
+
import { createChannelComposition } from './channel-composition.ts';
|
|
63
|
+
import { applyProviderOptimizerConfigMode, bindProviderOptimizerFeatureFlag } from './provider-optimizer-wiring.ts';
|
|
56
64
|
import { type ArchivableProcessRegistry } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
|
|
57
|
-
import { calcSessionCost, isModelPriced } from '../export/cost-utils.ts';
|
|
58
65
|
import { createWorkstreamServices, type OrchestrationEngine, type WorkstreamCommandService } from './workstream-services.ts';
|
|
59
66
|
import { wireFleetNeedsInputPush } from './fleet-needs-input-push.ts';
|
|
60
67
|
import { codeIndexDbPath, createCodeIndexServices, isCodeInjectionSettingEnabled } from './code-index-services.ts';
|
|
61
68
|
import { WorkPlanStore } from '../work-plans/work-plan-store.ts';
|
|
62
|
-
import {
|
|
63
|
-
registerDaemonHandlers,
|
|
64
|
-
type DaemonHandlerSurfaces,
|
|
65
|
-
} from '../daemon/handlers/index.ts';
|
|
69
|
+
import { registerDaemonHandlers, type DaemonHandlerSurfaces } from '../daemon/handlers/index.ts';
|
|
66
70
|
import type { HandlerContext, HandlerLogger } from '../daemon/handlers/context.ts';
|
|
67
71
|
import { createDaemonCredentialStore } from '../daemon/handlers/credentials.ts';
|
|
68
72
|
import { registerRouting } from '../daemon/handlers/routing/index.ts';
|
|
@@ -73,58 +77,10 @@ import { registerCalendar } from '../daemon/handlers/calendar/index.ts';
|
|
|
73
77
|
import { registerEmailMethods } from '../daemon/handlers/email/index.ts';
|
|
74
78
|
import { registerRemoteSurface } from '../daemon/handlers/remote/index.ts';
|
|
75
79
|
import { WorkspaceTrustManager } from './trust/workspace-trust.ts';
|
|
80
|
+
import { ensureConfiguredModelIsRoutable } from './provider-fallback.ts';
|
|
76
81
|
|
|
77
82
|
const REGULAR_KNOWLEDGE_DB_FILE = 'knowledge-wiki.sqlite';
|
|
78
83
|
const HOME_GRAPH_KNOWLEDGE_DB_FILE = 'knowledge-home-graph.sqlite';
|
|
79
|
-
|
|
80
|
-
function buildFallbackModelDefinition(provider: string, modelId: string): ModelDefinition {
|
|
81
|
-
const providerLower = provider.toLowerCase();
|
|
82
|
-
const isReasoningProvider = providerLower.includes('openai')
|
|
83
|
-
|| providerLower.includes('anthropic')
|
|
84
|
-
|| providerLower.includes('gemini')
|
|
85
|
-
|| providerLower.includes('google');
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
id: modelId,
|
|
89
|
-
provider,
|
|
90
|
-
registryKey: `${provider}:${modelId}`,
|
|
91
|
-
displayName: modelId,
|
|
92
|
-
description: 'Configured model available before the model catalog cache has loaded.',
|
|
93
|
-
capabilities: {
|
|
94
|
-
toolCalling: true,
|
|
95
|
-
codeEditing: true,
|
|
96
|
-
reasoning: isReasoningProvider,
|
|
97
|
-
multimodal: isReasoningProvider,
|
|
98
|
-
},
|
|
99
|
-
// Pre-catalog fallback uses the SDK's family-aware inference (SDK 0.35.0+),
|
|
100
|
-
// matching the post-catalog window so the meter/compaction denominator agrees.
|
|
101
|
-
contextWindow: inferFallbackContextWindow(provider, modelId),
|
|
102
|
-
contextWindowProvenance: 'fallback',
|
|
103
|
-
selectable: true,
|
|
104
|
-
tier: 'standard',
|
|
105
|
-
...(isReasoningProvider ? { reasoningEffort: ['instant', 'low', 'medium', 'high'] } : {}),
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function ensureConfiguredModelIsRoutable(providerRegistry: ProviderRegistry, configManager: ConfigManager): void {
|
|
110
|
-
const configuredModel = String(configManager.get('provider.model') ?? '').trim();
|
|
111
|
-
if (!configuredModel.includes(':')) return;
|
|
112
|
-
if (providerRegistry.listModels().some((model) => model.registryKey === configuredModel)) return;
|
|
113
|
-
|
|
114
|
-
const [providerId, ...modelParts] = configuredModel.split(':');
|
|
115
|
-
const modelId = modelParts.join(':').trim();
|
|
116
|
-
if (!providerId || !modelId) return;
|
|
117
|
-
|
|
118
|
-
const provider = providerRegistry.tryGet(providerId);
|
|
119
|
-
if (!provider) return;
|
|
120
|
-
|
|
121
|
-
providerRegistry.registerRuntimeProvider({
|
|
122
|
-
provider,
|
|
123
|
-
replace: true,
|
|
124
|
-
models: [buildFallbackModelDefinition(providerId, modelId)],
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
|
|
128
84
|
export interface RuntimeServicesOptions {
|
|
129
85
|
readonly runtimeBus: RuntimeEventBus;
|
|
130
86
|
readonly runtimeStore: RuntimeStore;
|
|
@@ -134,8 +90,6 @@ export interface RuntimeServicesOptions {
|
|
|
134
90
|
readonly getConversationTitle?: () => string | undefined;
|
|
135
91
|
readonly workingDir: string;
|
|
136
92
|
readonly homeDirectory: string;
|
|
137
|
-
/** Prior GoodVibes runtime state existed at startup — sampled before any write; seeds the trust gate's grandfathering. */
|
|
138
|
-
readonly workspaceHadPriorState?: boolean;
|
|
139
93
|
}
|
|
140
94
|
|
|
141
95
|
export interface RuntimeServices {
|
|
@@ -155,6 +109,14 @@ export interface RuntimeServices {
|
|
|
155
109
|
readonly channelDeliveryRouter: ChannelDeliveryRouter;
|
|
156
110
|
readonly watcherRegistry: WatcherRegistry;
|
|
157
111
|
readonly approvalBroker: ApprovalBroker;
|
|
112
|
+
/** Loopback-fetch approval that rides the approval broker; shared by the tool registry and orchestrator so every surface asks the same way. */
|
|
113
|
+
readonly localhostFetchApproval: ReturnType<typeof buildLocalhostFetchApproval>;
|
|
114
|
+
/** Terminal prompt-answer handler that rides the approval broker; shared by the tool registry and orchestrator so an interactive command's prompt gets an ask/card on every surface. */
|
|
115
|
+
readonly execPromptAnswerHandler: ReturnType<typeof buildExecPromptAnswerHandler>;
|
|
116
|
+
/** Routes curated runtime-domain events into the panel_only notification feed (the panel's live producer). */
|
|
117
|
+
readonly notificationDispatcher: NotificationDispatcher;
|
|
118
|
+
/** Durable user-origin permission rules (remembered approvals); permissions.rules.* surface. Mirrors the SDK composition. */
|
|
119
|
+
readonly userPermissionRuleStore: UserPermissionRuleStore;
|
|
158
120
|
readonly sessionBroker: SharedSessionBroker;
|
|
159
121
|
readonly deliveryManager: AutomationDeliveryManager;
|
|
160
122
|
readonly automationManager: AutomationManager;
|
|
@@ -198,7 +160,7 @@ export interface RuntimeServices {
|
|
|
198
160
|
readonly worktreeRegistry: WorktreeRegistry;
|
|
199
161
|
readonly sandboxSessionRegistry: SandboxSessionRegistry;
|
|
200
162
|
readonly webhookNotifier: WebhookNotifier;
|
|
201
|
-
/** Terminal focus tracker
|
|
163
|
+
/** Terminal focus tracker — fed by input/handler-feed.ts, read by the alert notifiers in core/. */
|
|
202
164
|
readonly focusTracker: FocusTracker;
|
|
203
165
|
readonly replayEngine: DeterministicReplayEngine;
|
|
204
166
|
readonly providerOptimizer: ProviderOptimizer;
|
|
@@ -234,6 +196,8 @@ export interface RuntimeServices {
|
|
|
234
196
|
/** The repo source-tree code index — see runtime/code-index-services.ts. */
|
|
235
197
|
readonly codeIndexStore: CodeIndexStore;
|
|
236
198
|
readonly codeIndexReindexScheduler: CodeIndexReindexScheduler; // tool-site reindex
|
|
199
|
+
/** Daily snapshots of every SQLite store this runtime writes, with bounded retention; unref'd timers (mirrors the SDK composition — hosts that tear down a runtime stop() it themselves). */
|
|
200
|
+
readonly storeSnapshotScheduler: StoreSnapshotScheduler;
|
|
237
201
|
/** Unified live process registry (agents, WRFC chains, workflows, watchers, background processes) backing the Fleet panel; archive-aware — finished subtrees can be moved to the session archive view. */
|
|
238
202
|
readonly processRegistry: ArchivableProcessRegistry;
|
|
239
203
|
readonly modeManager: ModeManager;
|
|
@@ -253,26 +217,31 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
253
217
|
workingDirectory,
|
|
254
218
|
homeDirectory,
|
|
255
219
|
});
|
|
256
|
-
const workspaceTrustManager = new WorkspaceTrustManager({ shellPaths
|
|
220
|
+
const workspaceTrustManager = new WorkspaceTrustManager({ shellPaths });
|
|
257
221
|
const configManager = options.configManager;
|
|
258
222
|
const featureFlags = options.featureFlags ?? createFeatureFlagManager();
|
|
223
|
+
if (options.featureFlags === undefined) {
|
|
224
|
+
// Owned manager: gate states derive from domain settings keys + live
|
|
225
|
+
// bridge (mirrors the SDK composition root; a passed manager is the caller's to wire).
|
|
226
|
+
featureFlags.loadFromConfig({ flags: deriveFeatureStates(configManager) });
|
|
227
|
+
bindFeatureSettingsBridge(configManager, featureFlags);
|
|
228
|
+
}
|
|
259
229
|
const runtimeDispatch = createDomainDispatch(options.runtimeStore);
|
|
260
230
|
const gatewayMethods = new GatewayMethodCatalog();
|
|
261
231
|
const panelManager = new PanelManager();
|
|
262
|
-
//
|
|
232
|
+
// (the purge): MIGRATE-TO-MODAL surface + redirect registration moved to
|
|
263
233
|
// registerBuiltinPanels (builtin-panels.ts), where the panels' resolved deps
|
|
264
234
|
// are available for the surfaces to close over.
|
|
265
235
|
const keybindingsManager = new KeybindingsManager({
|
|
266
236
|
configPath: shellPaths.resolveUserPath('tui', 'keybindings.json'),
|
|
267
237
|
});
|
|
268
|
-
|
|
269
|
-
|
|
238
|
+
// Channel/surface wiring: see channel-composition.ts (incl. the recorded surface-gating divergence note).
|
|
239
|
+
const { routeBindings, surfaceRegistry, channelPlugins } = createChannelComposition({
|
|
240
|
+
configManager,
|
|
270
241
|
runtimeStore: options.runtimeStore,
|
|
271
242
|
runtimeBus: options.runtimeBus,
|
|
243
|
+
featureFlags,
|
|
272
244
|
});
|
|
273
|
-
const surfaceRegistry = new SurfaceRegistry(configManager, options.runtimeStore);
|
|
274
|
-
const channelPlugins = new ChannelPluginRegistry();
|
|
275
|
-
surfaceRegistry.attachPluginRegistry(channelPlugins);
|
|
276
245
|
const secretsManager = new SecretsManager({
|
|
277
246
|
projectRoot: workingDirectory,
|
|
278
247
|
globalHome: homeDirectory,
|
|
@@ -307,6 +276,10 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
307
276
|
});
|
|
308
277
|
ensureConfiguredModelIsRoutable(providerRegistry, configManager);
|
|
309
278
|
providerRegistry.initCustomProviders();
|
|
279
|
+
// Kick off the background, TTL-respecting live model discovery sweep so
|
|
280
|
+
// provider model lists refresh from their own listing APIs — matching the
|
|
281
|
+
// SDK and agent compositions (this call was the one omitted here).
|
|
282
|
+
providerRegistry.initProviderModelDiscovery();
|
|
310
283
|
const toolLLM = new ToolLLM({
|
|
311
284
|
configManager,
|
|
312
285
|
providerRegistry,
|
|
@@ -341,6 +314,9 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
341
314
|
messageBus: agentMessageBus,
|
|
342
315
|
executor: agentOrchestrator,
|
|
343
316
|
configManager,
|
|
317
|
+
// The live registry lets a bare model id in a spawn() override resolve
|
|
318
|
+
// through the shared resolver instead of being rejected as unqualified.
|
|
319
|
+
providerRegistry,
|
|
344
320
|
});
|
|
345
321
|
const contextAccountingHolder = new ContextAccountingHolder();
|
|
346
322
|
agentOrchestrator.setConversationSink({ // Conversation-snapshot bridge (mirrors the SDK's own createRuntimeServices)
|
|
@@ -418,6 +394,9 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
418
394
|
runtimeStore: options.runtimeStore,
|
|
419
395
|
runtimeBus: options.runtimeBus,
|
|
420
396
|
deliveryManager,
|
|
397
|
+
// Same live registry: a bare model id on an automation job resolves through
|
|
398
|
+
// the shared resolver instead of a format-only rejection.
|
|
399
|
+
providerRegistry,
|
|
421
400
|
spawnTask: (input) => {
|
|
422
401
|
const record = agentManager.spawn({
|
|
423
402
|
mode: 'spawn',
|
|
@@ -578,11 +557,9 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
578
557
|
const webhookNotifier = new WebhookNotifier();
|
|
579
558
|
const focusTracker = new FocusTracker();
|
|
580
559
|
const replayEngine = new DeterministicReplayEngine(workingDirectory);
|
|
581
|
-
const providerOptimizer = new ProviderOptimizer(
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
isFeatureFlagEnabled(configManager, 'provider-optimizer'),
|
|
585
|
-
);
|
|
560
|
+
const providerOptimizer = new ProviderOptimizer(providerRegistry, providerCapabilityRegistry, false); // dark until its gate flips it (see provider-optimizer-wiring.ts)
|
|
561
|
+
bindProviderOptimizerFeatureFlag(featureFlags, providerOptimizer);
|
|
562
|
+
applyProviderOptimizerConfigMode(configManager, providerOptimizer);
|
|
586
563
|
const sessionMemoryStore = new SessionMemoryStore();
|
|
587
564
|
const sessionLineageTracker = new SessionLineageTracker();
|
|
588
565
|
const sessionChangeTracker = new SessionChangeTracker();
|
|
@@ -611,6 +588,11 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
611
588
|
// with MemoryStore above. Auto-build is config-gated (default off) — see
|
|
612
589
|
// code-index-services.ts's header doc.
|
|
613
590
|
const { codeIndexStore, codeIndexReindexScheduler } = createCodeIndexServices({ workingDirectory, configManager, memoryEmbeddingRegistry });
|
|
591
|
+
// Store snapshots + durable remembered-approval rules + the live credential
|
|
592
|
+
// chain, mirroring the SDK composition — see durability-services.ts.
|
|
593
|
+
const { storeSnapshotScheduler, userPermissionRuleStore } = createDurabilityServices({
|
|
594
|
+
configManager, secretsManager, providerRegistry, memoryDbPath, codeIndexDbPath: codeIndexDbPath(workingDirectory),
|
|
595
|
+
});
|
|
614
596
|
const codeInjectionOrchestratorDeps = { codeIndex: codeIndexStore, isCodeInjectionSettingEnabled: () => isCodeInjectionSettingEnabled(configManager), codeIndexReindexScheduler }; // Code-injection seam (agent here; main via orchestrator-core-services.ts)
|
|
615
597
|
// Shared, archive-aware fleet registry — see gateway-verbs.ts's factory doc.
|
|
616
598
|
const processRegistry = createArchivableFleetRegistry({
|
|
@@ -626,12 +608,9 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
626
608
|
messageBus: agentMessageBus, // Backs steer()/`steerable` (the Fleet steer composer builds on top)
|
|
627
609
|
automationManager, // Folds /schedule AutomationJobs into the fleet as 'schedule' nodes
|
|
628
610
|
runtimeBus: options.runtimeBus,
|
|
629
|
-
// Honest
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
if (!isModelPriced(modelId)) return null;
|
|
633
|
-
return calcSessionCost(usage.inputTokens, usage.outputTokens, usage.cacheReadTokens, usage.cacheWriteTokens, modelId);
|
|
634
|
-
},
|
|
611
|
+
// Honest-unpriced through the ONE pricing resolver (manual -> registration -> provider-served
|
|
612
|
+
// -> catalog -> unknown); unknown/subscription yields null, never $0. Mirrors the SDK composition.
|
|
613
|
+
priceUsage: (model, usage) => (model ? computeUsageCostUsd(providerRegistry.resolveModelPricing(model), usage) : null),
|
|
635
614
|
});
|
|
636
615
|
const modeManager = new ModeManager();
|
|
637
616
|
const fileUndoManager = new FileUndoManager();
|
|
@@ -641,7 +620,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
641
620
|
// family without it. principals.*/channels.profiles.*/ci.* register unconditionally; checkin.* only with
|
|
642
621
|
// channelDeliveryRouter/providerRegistry/automationManager/sessionLister ALL present; fleet needs-input push gated by
|
|
643
622
|
// runtimeBus/sessionPresence (fleet-needs-input-push.ts). conversationRewindPort serves conversation-scope rewind live.
|
|
644
|
-
attachWsOnlyGatewayVerbHandlers(gatewayMethods, { processRegistry, workspaceCheckpointManager, conversationRewindPort: createSessionConversationRewindPort(), sessionBroker, secretsManager, stepUpService, approvalBroker, shellPaths, configManager, runtimeStore: options.runtimeStore, channelDeliveryRouter, providerRegistry, automationManager, sessionLister: sessionBroker, ...wireFleetNeedsInputPush({ registry: processRegistry, runtimeBus: options.runtimeBus, sessionBroker }) });
|
|
623
|
+
attachWsOnlyGatewayVerbHandlers(gatewayMethods, { processRegistry, workspaceCheckpointManager, conversationRewindPort: createSessionConversationRewindPort(), sessionBroker, secretsManager, stepUpService, approvalBroker, requestApproval: (input) => approvalBroker.requestApproval(input), watcherRegistry, userPermissionRuleStore, shellPaths, configManager, runtimeStore: options.runtimeStore, channelDeliveryRouter, providerRegistry, automationManager, sessionLister: sessionBroker, sessionIntake: sessionBroker, workingDirectory, memoryRegistry, ...wireFleetNeedsInputPush({ registry: processRegistry, runtimeBus: options.runtimeBus, sessionBroker }) });
|
|
645
624
|
const integrationHelpers = new IntegrationHelperService({
|
|
646
625
|
workingDirectory,
|
|
647
626
|
homeDirectory,
|
|
@@ -662,8 +641,16 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
662
641
|
subscriptionManager,
|
|
663
642
|
secretsManager,
|
|
664
643
|
});
|
|
644
|
+
// A loopback fetch that isn't allow-listed asks once through the approval
|
|
645
|
+
// broker; "allow for this project" persists and later fetches never ask. Built
|
|
646
|
+
// once and shared with the tool registry (bootstrap-core) so both ask alike.
|
|
647
|
+
const localhostFetchApproval = buildLocalhostFetchApproval({ requestApproval: (input) => approvalBroker.requestApproval(input), configManager });
|
|
648
|
+
// Exec stuck on a terminal prompt rides the approval broker; the typed answer feeds the continuing run. Built once and shared (like localhostFetchApproval) so the tool registry AND every setDependencies site — incl. the post-clobber rewire in bootstrap-core — install the SAME handler; otherwise a wholesale replace drops it and interactive prompts hang.
|
|
649
|
+
const execPromptAnswerHandler = buildExecPromptAnswerHandler({ requestApproval: (input) => approvalBroker.requestApproval(input) });
|
|
665
650
|
agentOrchestrator.setDependencies({
|
|
666
651
|
surfaceRoot: 'tui',
|
|
652
|
+
execPromptAnswerHandler,
|
|
653
|
+
localhostFetchApproval,
|
|
667
654
|
fileCache,
|
|
668
655
|
projectIndex,
|
|
669
656
|
workingDirectory,
|
|
@@ -691,6 +678,12 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
691
678
|
contextAccountingHolder,
|
|
692
679
|
});
|
|
693
680
|
|
|
681
|
+
// Give the panel_only notification target a live producer: curated
|
|
682
|
+
// runtime-domain events route through the notification router into the shared
|
|
683
|
+
// panel feed (panel_only and burst-collapsed decisions land there).
|
|
684
|
+
const notificationDispatcher = createNotificationDispatcher(configManager);
|
|
685
|
+
wireRuntimeNotificationBridge(options.runtimeBus, notificationDispatcher);
|
|
686
|
+
|
|
694
687
|
return {
|
|
695
688
|
workingDirectory,
|
|
696
689
|
homeDirectory,
|
|
@@ -709,6 +702,10 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
709
702
|
channelDeliveryRouter,
|
|
710
703
|
watcherRegistry,
|
|
711
704
|
approvalBroker,
|
|
705
|
+
localhostFetchApproval,
|
|
706
|
+
execPromptAnswerHandler,
|
|
707
|
+
notificationDispatcher,
|
|
708
|
+
userPermissionRuleStore,
|
|
712
709
|
sessionBroker,
|
|
713
710
|
deliveryManager,
|
|
714
711
|
automationManager,
|
|
@@ -784,6 +781,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
784
781
|
workstreamCommands,
|
|
785
782
|
codeIndexStore,
|
|
786
783
|
codeIndexReindexScheduler,
|
|
784
|
+
storeSnapshotScheduler,
|
|
787
785
|
processRegistry,
|
|
788
786
|
modeManager,
|
|
789
787
|
fileUndoManager,
|
|
@@ -791,8 +789,9 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
791
789
|
integrationHelpers,
|
|
792
790
|
async rerootStores(newWorkingDir: string): Promise<void> {
|
|
793
791
|
// The memory store is the home-scoped canonical cross-surface store and
|
|
794
|
-
// deliberately does NOT reroot per-project (that would re-silo memory
|
|
795
|
-
//
|
|
792
|
+
// deliberately does NOT reroot per-project (that would re-silo memory back
|
|
793
|
+
// into per-project stores). Only working-tree-bound stores (code index,
|
|
794
|
+
// project index) reroot.
|
|
796
795
|
await codeIndexStore.reroot(newWorkingDir, codeIndexDbPath(newWorkingDir));
|
|
797
796
|
await projectIndex.reroot(newWorkingDir);
|
|
798
797
|
},
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-continuity-hints — the recovery snapshot's continuity summary.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from main.ts (which sits at the architecture line cap) so the
|
|
5
|
+
* startup wiring there stays within budget. Builds the small counts/paths
|
|
6
|
+
* bundle written into the recovery file so a resumed session can describe what
|
|
7
|
+
* was in flight (pending approvals, active/blocked tasks, remotes, worktrees,
|
|
8
|
+
* open panels) without re-reading live state.
|
|
9
|
+
*/
|
|
10
|
+
import type { UiReadModels } from './ui-read-models.ts';
|
|
11
|
+
import type { PanelManager } from '../panels/panel-manager.ts';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The continuity summary written into the recovery file. A loose serializable
|
|
15
|
+
* bag (Record) because it is JSON-persisted and read back by the recovery
|
|
16
|
+
* continuity-hints consumer, which types it structurally.
|
|
17
|
+
*/
|
|
18
|
+
export function createSessionContinuityHintsBuilder(deps: {
|
|
19
|
+
readonly readModels: UiReadModels;
|
|
20
|
+
readonly panelManager: Pick<PanelManager, 'getAllOpen'>;
|
|
21
|
+
}): () => Record<string, unknown> {
|
|
22
|
+
return () => {
|
|
23
|
+
const sessionSnapshot = deps.readModels.session.getSnapshot();
|
|
24
|
+
const tasksSnapshot = deps.readModels.tasks.getSnapshot();
|
|
25
|
+
const remoteSnapshot = deps.readModels.remote.getSnapshot();
|
|
26
|
+
const worktreeSnapshot = deps.readModels.worktrees.getSnapshot();
|
|
27
|
+
return {
|
|
28
|
+
pendingApprovals: sessionSnapshot.pendingApproval ? 1 : 0,
|
|
29
|
+
activeTasks: tasksSnapshot.tasks.filter((task) => task.status === 'running' || task.status === 'queued').length,
|
|
30
|
+
blockedTasks: tasksSnapshot.tasks.filter((task) => task.status === 'blocked').length,
|
|
31
|
+
remoteContracts: remoteSnapshot.contracts.length,
|
|
32
|
+
remoteRunners: remoteSnapshot.contracts.slice(0, 4).map((contract) => contract.runnerId),
|
|
33
|
+
worktreeCount: worktreeSnapshot.records.length,
|
|
34
|
+
worktreePaths: worktreeSnapshot.records.slice(0, 3).map((record) => record.path),
|
|
35
|
+
openPanels: deps.panelManager.getAllOpen().map((panel) => panel.id),
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -1,5 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* surface-feature-flags — which platform capabilities a surface/server setup
|
|
3
|
+
* needs, and how to switch them on through their real domain settings keys.
|
|
4
|
+
*
|
|
5
|
+
* Capabilities are configured through first-class domain settings (see
|
|
6
|
+
* feature-settings.ts); enabling a surface writes keys like
|
|
7
|
+
* `controlPlane.gateway` or `surfaces.slack.enabled`, never a separate
|
|
8
|
+
* enablement namespace.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ConfigManager } from '../config/index.ts';
|
|
2
12
|
import { surfaceFeatureGateId } from '@/runtime/index.ts';
|
|
13
|
+
import { featureEnablementWrite, getFeatureSetting, isFeatureConfigEnabled } from './feature-settings.ts';
|
|
3
14
|
|
|
4
15
|
export const CONTROL_PLANE_FEATURE_FLAG = 'control-plane-gateway';
|
|
5
16
|
export const ROUTE_BINDING_FEATURE_FLAG = 'route-binding';
|
|
@@ -12,10 +23,15 @@ const CORE_CHANNEL_FEATURE_FLAGS = [
|
|
|
12
23
|
DELIVERY_ENGINE_FEATURE_FLAG,
|
|
13
24
|
] as const;
|
|
14
25
|
|
|
15
|
-
export type FeatureFlagConfigKey = 'featureFlags' | `featureFlags.${string}`;
|
|
16
|
-
|
|
17
26
|
export function getSurfaceFeatureFlag(surfaceId: string): string | null {
|
|
18
|
-
|
|
27
|
+
// Onboarding surface ids may be camelCase ('googleChat'); the SDK's gate
|
|
28
|
+
// map is keyed by the kebab-case ChannelSurface id ('google-chat'). Try the
|
|
29
|
+
// literal id first, then the kebab-case form, so choosing Google Chat in
|
|
30
|
+
// onboarding really flips google-chat-surface.
|
|
31
|
+
const literal = surfaceFeatureGateId(surfaceId);
|
|
32
|
+
if (literal) return literal;
|
|
33
|
+
const kebab = surfaceId.replace(/[A-Z]/g, (ch) => `-${ch.toLowerCase()}`);
|
|
34
|
+
return kebab === surfaceId ? null : surfaceFeatureGateId(kebab);
|
|
19
35
|
}
|
|
20
36
|
|
|
21
37
|
export function getServerSurfaceFeatureFlags(options: {
|
|
@@ -46,20 +62,35 @@ export function getServerSurfaceFeatureFlags(options: {
|
|
|
46
62
|
return [...flags].sort((left, right) => left.localeCompare(right));
|
|
47
63
|
}
|
|
48
64
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return
|
|
65
|
+
/** Whether the live config has the capability on, derived from its domain settings key. */
|
|
66
|
+
export function isFeatureFlagEnabled(config: Pick<ConfigManager, 'get'>, flagId: string): boolean {
|
|
67
|
+
return isFeatureConfigEnabled(config, flagId);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Translate internal feature-gate ids (e.g. 'control-plane-gateway') to the
|
|
72
|
+
* canonical domain settings keys the user actually sets (e.g.
|
|
73
|
+
* 'controlPlane.gateway') — matching the SDK's own gate errors, which name the
|
|
74
|
+
* settings key. A gate with no mapped setting falls back to its id. Deduped and
|
|
75
|
+
* sorted for stable output.
|
|
76
|
+
*/
|
|
77
|
+
export function surfaceFeatureGateSettingsKeys(flagIds: readonly string[]): string[] {
|
|
78
|
+
const keys = new Set<string>();
|
|
79
|
+
for (const flagId of flagIds) keys.add(getFeatureSetting(flagId)?.enablement.key ?? flagId);
|
|
80
|
+
return [...keys].sort((left, right) => left.localeCompare(right));
|
|
52
81
|
}
|
|
53
82
|
|
|
54
|
-
export function getMissingSurfaceFeatureFlags(config: Pick<ConfigManager, '
|
|
83
|
+
export function getMissingSurfaceFeatureFlags(config: Pick<ConfigManager, 'get'>, surfaceId: string): readonly string[] {
|
|
55
84
|
const required = surfaceId === 'web'
|
|
56
85
|
? getServerSurfaceFeatureFlags({ web: true })
|
|
57
86
|
: getServerSurfaceFeatureFlags({ externalSurfaces: [surfaceId] });
|
|
58
87
|
return required.filter((flagId) => !isFeatureFlagEnabled(config, flagId));
|
|
59
88
|
}
|
|
60
89
|
|
|
90
|
+
/** Switch each capability on by writing its real enablement settings key. */
|
|
61
91
|
export function enableFeatureFlags(config: Pick<ConfigManager, 'setDynamic'>, flagIds: readonly string[]): void {
|
|
62
92
|
for (const flagId of flagIds) {
|
|
63
|
-
|
|
93
|
+
const write = featureEnablementWrite(flagId, true);
|
|
94
|
+
if (write) config.setDynamic(write.key, write.value);
|
|
64
95
|
}
|
|
65
96
|
}
|
|
@@ -45,7 +45,7 @@ export type TuiTerminalOutputGuardOptions = {
|
|
|
45
45
|
* writes captured this session, so an honest quiet counter (e.g. /debug) can
|
|
46
46
|
* refresh. The per-write detail is already recorded to the activity log via
|
|
47
47
|
* logger.warn — this callback intentionally does NOT push transcript lines.
|
|
48
|
-
* (
|
|
48
|
+
* (item 1a.)
|
|
49
49
|
*/
|
|
50
50
|
readonly onCapture?: (total: number) => void;
|
|
51
51
|
};
|
|
@@ -224,7 +224,7 @@ export function installTuiTerminalOutputGuard(options: TuiTerminalOutputGuardOpt
|
|
|
224
224
|
onIntercept: () => {
|
|
225
225
|
// Each intercept is already logged (logger.warn in record()). Here we only
|
|
226
226
|
// maintain a cumulative counter and, rate-limited, refresh a quiet counter
|
|
227
|
-
// surface — no repeated transcript lines. (
|
|
227
|
+
// surface — no repeated transcript lines. (item 1a.)
|
|
228
228
|
totalInterceptedWrites++;
|
|
229
229
|
const now = Date.now();
|
|
230
230
|
if (now - lastNoticeAt < 5_000) return;
|
|
@@ -137,13 +137,21 @@ export class WorkspaceRegistrationManager {
|
|
|
137
137
|
* Register the current working directory at its resolved project root. The
|
|
138
138
|
* store refuses broad roots (WorkspaceRegistrationError) — we surface that
|
|
139
139
|
* honestly rather than record a phantom registration.
|
|
140
|
+
*
|
|
141
|
+
* `origin` stamps which surface wrote the record (honest provenance). This
|
|
142
|
+
* NEVER passes `checkpointEligible`: absent means false, so a TUI self-record
|
|
143
|
+
* can never widen the checkpoint-owning consumer's boundary — only that
|
|
144
|
+
* consumer stamps its own roots eligible on boot.
|
|
140
145
|
*/
|
|
141
|
-
async register(label?: string): Promise<
|
|
146
|
+
async register(label?: string, origin?: string): Promise<
|
|
142
147
|
| { readonly registered: true; readonly result: RegisterWorkspaceResult }
|
|
143
148
|
| { readonly registered: false; readonly refusedReason: string }
|
|
144
149
|
> {
|
|
150
|
+
const opts = label || origin
|
|
151
|
+
? { ...(label ? { label } : {}), ...(origin ? { origin } : {}) }
|
|
152
|
+
: undefined;
|
|
145
153
|
try {
|
|
146
|
-
const result = await this.store.add(this.workingDirectory,
|
|
154
|
+
const result = await this.store.add(this.workingDirectory, opts);
|
|
147
155
|
return { registered: true, result };
|
|
148
156
|
} catch (error) {
|
|
149
157
|
if (error instanceof WorkspaceRegistrationError) {
|
|
@@ -2,18 +2,24 @@
|
|
|
2
2
|
* workspace-trust.ts — per-workspace trust gate.
|
|
3
3
|
*
|
|
4
4
|
* The first time GoodVibes opens a workspace (a cwd / project root it has no
|
|
5
|
-
* prior
|
|
6
|
-
* run; write, execute,
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* prior decision for), that workspace is "undecided": only read-category
|
|
6
|
+
* tools run; the first write, execute, or delegate tool request raises the
|
|
7
|
+
* trust question AT THAT MOMENT — the exact point it has a real consequence —
|
|
8
|
+
* instead of silently failing or being decided by a side effect of the
|
|
9
|
+
* product's own droppings. This is NOT a parallel permission checker — the
|
|
10
|
+
* decision is consulted by the real permission machinery at its final ask
|
|
11
|
+
* layer (the requestPermission callback wired in bootstrap-core.ts), so it
|
|
12
|
+
* composes with, and cannot drift from, the existing PermissionManager layer
|
|
13
|
+
* chain.
|
|
11
14
|
*
|
|
12
|
-
* Trust is persisted per-workspace in <cwd>/.goodvibes/tui/trust.json.
|
|
13
|
-
* workspace that already carries
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* Trust is persisted per-workspace in <cwd>/.goodvibes/tui/trust.json. There
|
|
16
|
+
* is no more grandfathering: a workspace that already carries prior GoodVibes
|
|
17
|
+
* runtime state gets no special treatment (that side-effect-based shortcut
|
|
18
|
+
* used to paper over the fact that the first session's write attempts were
|
|
19
|
+
* silently denied without ever recording a real decision — now that the first
|
|
20
|
+
* attempt properly raises the question and persists the answer, the shortcut
|
|
21
|
+
* is unnecessary and would only hide a decision the user should actually see
|
|
22
|
+
* once, even on an upgrade).
|
|
17
23
|
*/
|
|
18
24
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
19
25
|
import { join } from 'node:path';
|
|
@@ -25,13 +31,41 @@ type AskCallback = (request: PermissionPromptRequest) => Promise<PermissionPromp
|
|
|
25
31
|
|
|
26
32
|
/**
|
|
27
33
|
* Wrap the permission machinery's final ask callback with the workspace trust
|
|
28
|
-
* gate
|
|
29
|
-
* asking. This runs inside PermissionManager's own layer chain (it IS the ask
|
|
34
|
+
* gate. This runs inside PermissionManager's own layer chain (it IS the ask
|
|
30
35
|
* layer), so it composes with — and cannot drift from — the real machinery.
|
|
36
|
+
*
|
|
37
|
+
* - Trusted workspace: every category passes through to `ask` unchanged.
|
|
38
|
+
* - Restricted workspace with an EXPLICIT prior decision: non-read categories
|
|
39
|
+
* are denied outright, same as before — that IS what "restricted" means
|
|
40
|
+
* once the user has actually chosen it.
|
|
41
|
+
* - Undecided workspace (no decision yet, ever): the first non-read request
|
|
42
|
+
* raises `requestTrustDecision()` — one modal, persisted via `setLevel` —
|
|
43
|
+
* and, if the answer is 'trusted', the ORIGINAL request is forwarded to
|
|
44
|
+
* `ask` so the very thing the user just approved actually happens instead
|
|
45
|
+
* of failing anyway. Concurrent requests that arrive before the first
|
|
46
|
+
* decision resolves await the same in-flight prompt rather than opening a
|
|
47
|
+
* second one.
|
|
31
48
|
*/
|
|
32
|
-
export function trustGatedAsk(
|
|
33
|
-
|
|
34
|
-
|
|
49
|
+
export function trustGatedAsk(
|
|
50
|
+
manager: Pick<WorkspaceTrustManager, 'isCategoryAllowed' | 'isDecided' | 'setLevel'>,
|
|
51
|
+
ask: AskCallback,
|
|
52
|
+
requestTrustDecision: () => Promise<WorkspaceTrustLevel>,
|
|
53
|
+
): AskCallback {
|
|
54
|
+
let pendingDecision: Promise<WorkspaceTrustLevel> | null = null;
|
|
55
|
+
return async (request) => {
|
|
56
|
+
if (manager.isCategoryAllowed(request.category)) return ask(request);
|
|
57
|
+
if (!manager.isDecided()) {
|
|
58
|
+
if (!pendingDecision) {
|
|
59
|
+
pendingDecision = requestTrustDecision().finally(() => {
|
|
60
|
+
pendingDecision = null;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const level = await pendingDecision;
|
|
64
|
+
await manager.setLevel(level);
|
|
65
|
+
if (manager.isCategoryAllowed(request.category)) return ask(request);
|
|
66
|
+
}
|
|
67
|
+
return { approved: false };
|
|
68
|
+
};
|
|
35
69
|
}
|
|
36
70
|
|
|
37
71
|
export type WorkspaceTrustLevel = 'trusted' | 'restricted';
|
|
@@ -115,8 +149,6 @@ export function readPersistedWorkspaceTrust(paths: WorkspaceTrustPaths): Persist
|
|
|
115
149
|
|
|
116
150
|
export interface WorkspaceTrustManagerOptions {
|
|
117
151
|
readonly shellPaths: WorkspaceTrustPaths;
|
|
118
|
-
/** Whether the workspace already had prior GoodVibes runtime state at startup. */
|
|
119
|
-
readonly hadPriorState: boolean;
|
|
120
152
|
}
|
|
121
153
|
|
|
122
154
|
export class WorkspaceTrustManager {
|
|
@@ -124,20 +156,22 @@ export class WorkspaceTrustManager {
|
|
|
124
156
|
private grandfathered = false;
|
|
125
157
|
private loaded = false;
|
|
126
158
|
private readonly store: JsonFileStore<PersistedWorkspaceTrust>;
|
|
127
|
-
private readonly hadPriorState: boolean;
|
|
128
159
|
|
|
129
160
|
constructor(options: WorkspaceTrustManagerOptions) {
|
|
130
161
|
this.store = new JsonFileStore<PersistedWorkspaceTrust>(
|
|
131
162
|
options.shellPaths.resolveProjectPath('tui', TRUST_FILE),
|
|
132
163
|
);
|
|
133
|
-
this.hadPriorState = options.hadPriorState;
|
|
134
164
|
}
|
|
135
165
|
|
|
136
166
|
/**
|
|
137
|
-
* Load the persisted decision
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
167
|
+
* Load the persisted decision, if one exists. No grandfathering: a
|
|
168
|
+
* workspace with no persisted decision stays undecided (the gate treats it
|
|
169
|
+
* as restricted, and the first non-read tool request raises the trust
|
|
170
|
+
* question via `trustGatedAsk`'s `requestTrustDecision` callback) — even
|
|
171
|
+
* one that already carries prior GoodVibes runtime state. `grandfathered`
|
|
172
|
+
* on an already-persisted decision is read-only history from before this
|
|
173
|
+
* fix (status/doctor still report it honestly); nothing new is ever
|
|
174
|
+
* grandfathered.
|
|
141
175
|
*/
|
|
142
176
|
async load(): Promise<void> {
|
|
143
177
|
if (this.loaded) return;
|
|
@@ -145,13 +179,6 @@ export class WorkspaceTrustManager {
|
|
|
145
179
|
if (persisted && (persisted.level === 'trusted' || persisted.level === 'restricted')) {
|
|
146
180
|
this.level = persisted.level;
|
|
147
181
|
this.grandfathered = persisted.grandfathered ?? false;
|
|
148
|
-
this.loaded = true;
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
if (this.hadPriorState) {
|
|
152
|
-
this.level = 'trusted';
|
|
153
|
-
this.grandfathered = true;
|
|
154
|
-
await this.persist();
|
|
155
182
|
}
|
|
156
183
|
this.loaded = true;
|
|
157
184
|
}
|