@pellux/goodvibes-agent 1.9.1 → 1.10.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +12 -1
  3. package/dist/package/main.js +36712 -28803
  4. package/dist/package/{web-tree-sitter-jbz042ba.wasm → web-tree-sitter-e011xaqr.wasm} +0 -0
  5. package/docs/README.md +1 -1
  6. package/docs/connected-host.md +1 -1
  7. package/docs/getting-started.md +1 -1
  8. package/docs/tools-and-commands.md +1 -0
  9. package/package.json +3 -3
  10. package/release/live-verification/live-verification.json +11 -11
  11. package/release/live-verification/live-verification.md +12 -12
  12. package/src/agent/email/email-service.ts +1 -1
  13. package/src/agent/email/imap-client.ts +4 -4
  14. package/src/agent/email/smtp-client.ts +5 -5
  15. package/src/cli/config-overrides.ts +29 -14
  16. package/src/cli/entrypoint.ts +12 -4
  17. package/src/cli/launch-auto-update.ts +218 -0
  18. package/src/cli/relay-command.ts +4 -4
  19. package/src/cli/service-posture.ts +2 -2
  20. package/src/cli/tui-startup.ts +31 -0
  21. package/src/cli/workspaces-command.ts +5 -1
  22. package/src/cli-flags.ts +1 -1
  23. package/src/config/index.ts +1 -1
  24. package/src/config/update-settings.ts +45 -0
  25. package/src/config/workspace-registration.ts +214 -15
  26. package/src/input/commands/runtime-services.ts +0 -5
  27. package/src/input/commands/update-runtime.ts +313 -0
  28. package/src/input/commands.ts +2 -0
  29. package/src/input/feed-context-factory.ts +2 -2
  30. package/src/input/handler-feed.ts +8 -8
  31. package/src/input/handler.ts +1 -1
  32. package/src/input/mcp-workspace.ts +5 -1
  33. package/src/input/panel-paste-flood-guard.ts +1 -1
  34. package/src/input/settings-modal-types.ts +11 -5
  35. package/src/input/settings-modal.ts +56 -61
  36. package/src/main.ts +19 -20
  37. package/src/renderer/activity-sidebar.ts +53 -4
  38. package/src/renderer/settings-modal-helpers.ts +2 -0
  39. package/src/renderer/settings-modal.ts +37 -25
  40. package/src/runtime/bootstrap-core.ts +16 -5
  41. package/src/runtime/bootstrap-external-services.ts +107 -11
  42. package/src/runtime/bootstrap-hook-bridge.ts +2 -2
  43. package/src/runtime/bootstrap-shell.ts +4 -4
  44. package/src/runtime/bootstrap.ts +34 -12
  45. package/src/runtime/connected-host-autostart.ts +269 -0
  46. package/src/runtime/daemon-receipts.ts +66 -0
  47. package/src/runtime/diagnostics/panels/index.ts +0 -2
  48. package/src/runtime/feature-enablement.ts +176 -0
  49. package/src/runtime/index.ts +12 -29
  50. package/src/runtime/memory-spine-adoption.ts +18 -0
  51. package/src/runtime/onboarding/apply.ts +50 -37
  52. package/src/runtime/onboarding/types.ts +2 -2
  53. package/src/runtime/onboarding/verify.ts +22 -4
  54. package/src/runtime/release-artifacts.ts +113 -0
  55. package/src/runtime/services.ts +228 -19
  56. package/src/runtime/session-spine-rest-transport.ts +84 -4
  57. package/src/runtime/ui-services.ts +1 -1
  58. package/src/runtime/update-check.ts +64 -0
  59. package/src/shell/ui-openers.ts +8 -0
  60. package/src/tools/agent-harness-metadata.ts +8 -0
  61. package/src/tools/agent-harness-setup-connected-host.ts +1 -1
  62. package/src/tools/agent-harness-setup-posture.ts +1 -1
  63. package/src/version.ts +1 -1
  64. package/src/runtime/diagnostics/panels/ops.ts +0 -156
  65. package/src/runtime/surface-feature-flags.ts +0 -100
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Names and shapes of this repo's release assets, exactly as the Release
3
+ * workflow publishes them (.github/workflows/release.yml):
4
+ *
5
+ * - compiled binaries `goodvibes-agent-<os>-<arch>` for linux/macos ×
6
+ * x64/arm64 (the release tag maps darwin → "macos");
7
+ * - per-platform sqlite-vec native addon ARCHIVES named
8
+ * `sqlite-vec-<platform>-<arch>.tar.gz` (Node-style platform tag), each
9
+ * carrying the exact layout the runtime resolves relative to the binary —
10
+ * `lib/sqlite-vec-<platform>-<arch>/vec0.<suffix>`;
11
+ * - one `SHA256SUMS.txt` manifest covering every asset above.
12
+ *
13
+ * The checksum manifest name and parser are the SDK's canonical ones
14
+ * (platform/runtime/self-update) so the agent verifies with the same
15
+ * mechanism every other surface uses. Asset NAMING stays local because it
16
+ * encodes THIS repo's release layout — the SDK's resolveArtifactNames names
17
+ * the TUI's `goodvibes`/`goodvibes-daemon` pair, which this repo does not
18
+ * ship.
19
+ */
20
+ import { gunzipSync } from 'node:zlib';
21
+
22
+ export { CHECKSUM_MANIFEST_NAME, parseChecksumFile } from '@pellux/goodvibes-sdk/platform/runtime/self-update';
23
+
24
+ const SUPPORTED_ARCHES = new Set(['x64', 'arm64']);
25
+
26
+ /**
27
+ * Release asset name of the compiled agent binary for a platform/arch, or
28
+ * null when no prebuilt binary is published for it (the release ships no
29
+ * Windows binary asset).
30
+ */
31
+ export function resolveAgentBinaryAssetName(platform: string, arch: string): string | null {
32
+ if (!SUPPORTED_ARCHES.has(arch)) return null;
33
+ const os = platform === 'linux' ? 'linux' : platform === 'darwin' ? 'macos' : null;
34
+ if (!os) return null;
35
+ return `goodvibes-agent-${os}-${arch}`;
36
+ }
37
+
38
+ export interface SqliteVecArchiveAsset {
39
+ /** Release asset filename, e.g. `sqlite-vec-linux-x64.tar.gz`. */
40
+ readonly assetName: string;
41
+ /** Directory name the loader resolves, e.g. `sqlite-vec-linux-x64`. */
42
+ readonly dirName: string;
43
+ /** File the loader opens inside that directory, e.g. `vec0.so`. */
44
+ readonly fileName: string;
45
+ /** The addon file's path inside the archive (and relative to the binary): `lib/<dirName>/<fileName>`. */
46
+ readonly entryPath: string;
47
+ }
48
+
49
+ /**
50
+ * Names the sqlite-vec addon ARCHIVE for a platform/arch. Unlike the binary
51
+ * (whose release name maps darwin to "macos"), the archive keeps the
52
+ * Node-style platform tag because that is exactly what the extension loader
53
+ * resolves at `<execDir>/lib/sqlite-vec-<platform>-<arch>/vec0.<suffix>`.
54
+ */
55
+ export function resolveSqliteVecArchive(platform: string, arch: string): SqliteVecArchiveAsset | null {
56
+ if (!SUPPORTED_ARCHES.has(arch)) return null;
57
+ const suffix = platform === 'linux' ? 'so' : platform === 'darwin' ? 'dylib' : null;
58
+ if (!suffix) return null;
59
+ const dirName = `sqlite-vec-${platform}-${arch}`;
60
+ const fileName = `vec0.${suffix}`;
61
+ return {
62
+ assetName: `${dirName}.tar.gz`,
63
+ dirName,
64
+ fileName,
65
+ entryPath: `lib/${dirName}/${fileName}`,
66
+ };
67
+ }
68
+
69
+ const TAR_BLOCK = 512;
70
+
71
+ function readTarString(block: Buffer, offset: number, length: number): string {
72
+ const slice = block.subarray(offset, offset + length);
73
+ const nul = slice.indexOf(0);
74
+ return slice.toString('utf-8', 0, nul === -1 ? length : nul);
75
+ }
76
+
77
+ /**
78
+ * Extract one regular-file entry from a gzipped tar archive, by its exact
79
+ * path (a leading `./` on the archived name is tolerated). Returns the file
80
+ * bytes, or null when the archive holds no such entry. Throws on a gzip
81
+ * stream that does not decompress — a corrupted download must fail loudly,
82
+ * never read as "entry absent".
83
+ *
84
+ * Local on purpose: the released addon archives are flat ustar archives with
85
+ * short paths, so this reads the standard header fields (name, size octal,
86
+ * typeflag, ustar prefix) and skips everything that is not the requested
87
+ * regular file — no external `tar` spawn in the update path.
88
+ */
89
+ export function extractTarGzEntry(archive: Buffer | Uint8Array, entryPath: string): Buffer | null {
90
+ const tar = gunzipSync(archive);
91
+ let offset = 0;
92
+ while (offset + TAR_BLOCK <= tar.length) {
93
+ const header = tar.subarray(offset, offset + TAR_BLOCK);
94
+ // Two consecutive zero blocks end the archive; a zero name block is enough here.
95
+ if (header[0] === 0) break;
96
+ const name = readTarString(header, 0, 100);
97
+ const prefix = readTarString(header, 345, 155);
98
+ const fullName = prefix ? `${prefix}/${name}` : name;
99
+ const sizeOctal = readTarString(header, 124, 12).trim();
100
+ const size = Number.parseInt(sizeOctal || '0', 8);
101
+ if (!Number.isFinite(size) || size < 0) return null;
102
+ const typeflag = header[156];
103
+ const isRegular = typeflag === 0 || typeflag === 0x30; // NUL or '0'
104
+ const normalized = fullName.startsWith('./') ? fullName.slice(2) : fullName;
105
+ const dataStart = offset + TAR_BLOCK;
106
+ if (isRegular && normalized === entryPath) {
107
+ if (dataStart + size > tar.length) return null;
108
+ return Buffer.from(tar.subarray(dataStart, dataStart + size));
109
+ }
110
+ offset = dataStart + Math.ceil(size / TAR_BLOCK) * TAR_BLOCK;
111
+ }
112
+ return null;
113
+ }
@@ -5,7 +5,7 @@ import { shell as runtimeShell } from '@pellux/goodvibes-sdk/platform/runtime';
5
5
  import type { shell as RuntimeShell } from '@pellux/goodvibes-sdk/platform/runtime';
6
6
  import { SecretsManager } from '../config/secrets.ts';
7
7
  import { readCheckpointGuardSettings, readCheckpointRegistrationSetting } from '../config/checkpoint-settings.ts';
8
- import { createWorkspaceRegistrationLiveChecker, migrateLegacyWorkspaceRegistryIfNeeded } from '../config/workspace-registration.ts';
8
+ import { backfillCheckpointEligibilityIfNeeded, createWorkspaceRegistrationLiveChecker, migrateLegacyWorkspaceRegistryIfNeeded } from '../config/workspace-registration.ts';
9
9
  import { FocusTracker } from '../core/focus-tracker.ts';
10
10
  import { ServiceRegistry } from '@pellux/goodvibes-sdk/platform/config';
11
11
  import { SubscriptionManager } from '@pellux/goodvibes-sdk/platform/config';
@@ -21,10 +21,13 @@ import { createSessionConversationRewindPort } from './conversation-rewind-port.
21
21
  // terminal-shell does not already wrap, rather than hand-rolling the bridge.
22
22
  import { attachFleetEmitBridge } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
23
23
  import type { SharedSessionRoutingIntent } from '@pellux/goodvibes-sdk/platform/control-plane';
24
- import { logger } from '@pellux/goodvibes-sdk/platform/utils';
24
+ import { computeUsageCostUsd, resolveModelReference, type ModelIdCandidate } from '@pellux/goodvibes-sdk/platform/providers';
25
+ import { logger, summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
26
+ import { UserPermissionRuleStore } from '@pellux/goodvibes-sdk/platform/permissions';
25
27
  import { AGENT_SPINE_PARTICIPANT, SessionSpineClient } from '@pellux/goodvibes-sdk/platform/runtime/session-spine';
26
28
  import {
27
29
  createSpineConnectionResolver,
30
+ createSpineReceiptConsumer,
28
31
  createSpineRestProbe,
29
32
  createSpineRestTransport,
30
33
  } from './session-spine-rest-transport.ts';
@@ -53,9 +56,12 @@ import { AgentMessageBus } from '@pellux/goodvibes-sdk/platform/agents';
53
56
  import { WrfcController } from '@pellux/goodvibes-sdk/platform/agents';
54
57
  import { AgentOrchestrator } from '@pellux/goodvibes-sdk/platform/agents';
55
58
  import { ArchetypeLoader } from '@pellux/goodvibes-sdk/platform/agents';
56
- import { CodeIndexStore } from '@pellux/goodvibes-sdk/platform/state';
59
+ import { CodeIndexStore, resolveMemoryVectorDbPath } from '@pellux/goodvibes-sdk/platform/state';
57
60
  import { CodeIndexReindexScheduler } from '@pellux/goodvibes-sdk/platform/state';
58
- import { createOrchestrationEngine } from '@pellux/goodvibes-sdk/platform/orchestration';
61
+ import { createOrchestrationEngine, createProviderBackedAttemptJudge } from '@pellux/goodvibes-sdk/platform/orchestration';
62
+ import { StoreSnapshotScheduler } from '@pellux/goodvibes-sdk/platform/state/store-snapshots';
63
+ import { buildExecPromptAnswerHandler } from '@pellux/goodvibes-sdk/platform/runtime/permissions/exec-prompt-wiring';
64
+ import { AgentDaemonReceiptFeed } from './daemon-receipts.ts';
59
65
  import { WorkspaceCheckpointManager } from '@pellux/goodvibes-sdk/platform/workspace';
60
66
 
61
67
  /**
@@ -120,7 +126,16 @@ import { SandboxSessionRegistry } from '@/runtime/index.ts';
120
126
  import { createShellPathService, type ShellPathService } from '@/runtime/index.ts';
121
127
  import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
122
128
  import type { FeatureFlagManager, RuntimeFoundationClientsOptions } from '@/runtime/index.ts';
123
- import { createFeatureFlagManager } from '@/runtime/index.ts';
129
+ import { createFeatureFlagManager, deriveFeatureStates, bindFeatureSettingsBridge } from '@/runtime/index.ts';
130
+ import {
131
+ FeatureAnnouncementStore,
132
+ createSandboxContainmentAnnouncer,
133
+ featureAnnouncementsPath,
134
+ } from '@pellux/goodvibes-sdk/platform/runtime/feature-announcements';
135
+ import {
136
+ buildLocalhostFetchApproval,
137
+ type LocalhostFetchApproval,
138
+ } from '@pellux/goodvibes-sdk/platform/runtime/permissions/localhost-fetch-approval';
124
139
  import { PolicyRuntimeState } from '@/runtime/index.ts';
125
140
  import {
126
141
  createWorkflowServices,
@@ -169,40 +184,79 @@ function buildFallbackModelDefinition(provider: string, modelId: string): ModelD
169
184
  };
170
185
  }
171
186
 
172
- function normalizeSharedSessionModelId(modelId: string | undefined, providerId: string | undefined): string | undefined {
187
+ /**
188
+ * Fork-mirror of the SDK's buildSharedSessionAgentSpawnRoutingInput
189
+ * (platform/control-plane/session-intents.ts — the builder itself has no
190
+ * public export path; only its intent types do). Bare model ids resolve
191
+ * through the SDK's PUBLIC shared resolver (resolveModelReference from
192
+ * platform/providers): unique across the registry auto-qualifies, an
193
+ * ambiguous id throws the real candidate registryKeys, an unknown id throws
194
+ * closest-match suggestions plus a concrete valid example. Keep the body
195
+ * faithful to the SDK's; any deliberate divergence gets its own comment.
196
+ */
197
+ function normalizeSharedSessionModelId(
198
+ modelId: string | undefined,
199
+ providerId: string | undefined,
200
+ modelCandidates?: readonly ModelIdCandidate[],
201
+ ): string | undefined {
173
202
  const trimmedModelId = modelId?.trim();
174
203
  if (!trimmedModelId) return undefined;
175
204
  const trimmedProviderId = providerId?.trim();
176
205
  const separatorIndex = trimmedModelId.indexOf(':');
177
- if (separatorIndex > 0) return trimmedModelId;
206
+ if (separatorIndex > 0) {
207
+ const modelProviderId = trimmedModelId.slice(0, separatorIndex);
208
+ if (trimmedProviderId && trimmedProviderId !== modelProviderId) {
209
+ throw new Error(`Shared-session routing model '${trimmedModelId}' conflicts with provider '${trimmedProviderId}'.`);
210
+ }
211
+ return trimmedModelId;
212
+ }
178
213
  if (trimmedProviderId) return `${trimmedProviderId}:${trimmedModelId}`;
179
- return trimmedModelId;
214
+ if (modelCandidates) {
215
+ try {
216
+ return resolveModelReference(trimmedModelId, modelCandidates);
217
+ } catch (err) {
218
+ throw new Error(`Shared-session routing model: ${err instanceof Error ? err.message : String(err)}`);
219
+ }
220
+ }
221
+ throw new Error(`Shared-session routing model '${trimmedModelId}' must be provider-qualified.`);
180
222
  }
181
223
 
182
- function normalizeSharedSessionFallbackModels(models: readonly string[] | undefined): string[] {
224
+ function normalizeSharedSessionFallbackModels(
225
+ models: readonly string[] | undefined,
226
+ modelCandidates?: readonly ModelIdCandidate[],
227
+ ): string[] {
183
228
  return (models ?? [])
184
229
  .filter((model): model is string => typeof model === 'string' && model.trim().length > 0)
185
230
  .map((model) => {
186
231
  const trimmed = model.trim();
187
- const separatorIndex = trimmed.indexOf(':');
188
- if (separatorIndex <= 0 || separatorIndex === trimmed.length - 1) {
189
- throw new Error(`Shared-session fallback model '${model}' must be provider-qualified.`);
232
+ if (trimmed.includes(':')) return trimmed;
233
+ if (modelCandidates) {
234
+ try {
235
+ return resolveModelReference(trimmed, modelCandidates);
236
+ } catch (err) {
237
+ throw new Error(`Shared-session fallback model: ${err instanceof Error ? err.message : String(err)}`);
238
+ }
190
239
  }
191
- return trimmed;
240
+ throw new Error(`Shared-session fallback model '${model}' must be provider-qualified.`);
192
241
  });
193
242
  }
194
243
 
195
- function buildAgentSpawnRoutingFromSharedSession(
244
+ /** Exported for the spawn-routing contract tests; the composition root below is the only live caller. */
245
+ export function buildAgentSpawnRoutingFromSharedSession(
196
246
  routing: SharedSessionRoutingIntent | undefined,
197
- options: { readonly restrictTools?: boolean | undefined } = {},
247
+ options: {
248
+ readonly restrictTools?: boolean | undefined;
249
+ /** The live registry's model candidates — enables bare model id resolution when supplied. */
250
+ readonly modelCandidates?: readonly ModelIdCandidate[] | undefined;
251
+ } = {},
198
252
  ): Partial<Parameters<AgentManager['spawn']>[0]> {
199
253
  if (!routing) return options.restrictTools ? { restrictTools: true } : {};
200
254
  const provider = routing.providerId?.trim();
201
- const model = normalizeSharedSessionModelId(routing.modelId, provider);
255
+ const model = normalizeSharedSessionModelId(routing.modelId, provider, options.modelCandidates);
202
256
  if (provider && !model) {
203
257
  throw new Error('Shared-session provider routing requires a provider-qualified model when provider is supplied.');
204
258
  }
205
- const fallbackModels = normalizeSharedSessionFallbackModels(routing.fallbackModels);
259
+ const fallbackModels = normalizeSharedSessionFallbackModels(routing.fallbackModels, options.modelCandidates);
206
260
  const providerFailurePolicy = routing.providerFailurePolicy ?? (
207
261
  fallbackModels.length ? 'ordered-fallbacks' : 'fail'
208
262
  );
@@ -446,8 +500,39 @@ export interface RuntimeServices extends SdkRuntimeServices {
446
500
  readonly channelDeliveryRouter: ChannelDeliveryRouter;
447
501
  readonly watcherRegistry: WatcherRegistry;
448
502
  readonly approvalBroker: ApprovalBroker;
503
+ /**
504
+ * Localhost dev-server fetch approval (ask once, persist
505
+ * fetch.allowLocalhost for the project) — same instance wired into the
506
+ * orchestrator's tool deps; bootstrap-core must replay it there.
507
+ */
508
+ readonly localhostFetchApproval: LocalhostFetchApproval;
509
+ /**
510
+ * Announce-once store for default-on feature receipts (shared per-install
511
+ * file under the control-plane config dir).
512
+ */
513
+ readonly featureAnnouncementStore: FeatureAnnouncementStore;
514
+ /**
515
+ * First-contained-exec-run announcer — same instance wired into the
516
+ * orchestrator's tool deps; bootstrap-core must replay it there.
517
+ */
518
+ readonly onSandboxedRun: () => void;
449
519
  readonly sessionBroker: SharedSessionBroker;
450
520
  readonly sessionSpineClient: SessionSpineClient;
521
+ /**
522
+ * Connected-host honesty receipts ("updated from X to Y", "restarted after
523
+ * a crash") captured off the spine probe's /status reads; the renderer
524
+ * attaches at bootstrap and every receipt is delivered exactly once.
525
+ */
526
+ readonly daemonReceiptFeed: AgentDaemonReceiptFeed;
527
+ /**
528
+ * Performs ONE receipt-consuming /status read (`?receipts=consume`) and pushes
529
+ * whatever the daemon delivered into {@link daemonReceiptFeed}. A current
530
+ * daemon delivers its one-shot honesty receipts only to a consuming read (a
531
+ * plain liveness /status read is receipt-neutral), so bootstrap invokes this
532
+ * exactly once per attach — see the memory-spine adoption reconciler's
533
+ * `onAttach`. Best-effort: it never throws and yields nothing on failure.
534
+ */
535
+ readonly consumeDaemonReceipts: () => Promise<void>;
451
536
  readonly deliveryManager: AutomationDeliveryManager;
452
537
  readonly automationManager: AutomationManager;
453
538
  readonly gatewayMethods: GatewayMethodCatalog;
@@ -502,7 +587,7 @@ export interface RuntimeServices extends SdkRuntimeServices {
502
587
  readonly worktreeRegistry: WorktreeRegistry;
503
588
  readonly sandboxSessionRegistry: SandboxSessionRegistry;
504
589
  readonly webhookNotifier: WebhookNotifier;
505
- /** OS-level terminal focus tracker, ported from goodvibes-tui's W2.3 (core/focus-tracker.ts). */
590
+ /** OS-level terminal focus tracker, ported from goodvibes-tui's core/focus-tracker.ts. */
506
591
  readonly focusTracker: FocusTracker;
507
592
  readonly replayEngine: DeterministicReplayEngine;
508
593
  readonly providerOptimizer: ProviderOptimizer;
@@ -553,6 +638,13 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
553
638
  });
554
639
  const configManager = options.configManager;
555
640
  const featureFlags = options.featureFlags ?? createFeatureFlagManager();
641
+ if (options.featureFlags === undefined) {
642
+ // Gate states derive from domain settings keys; the bridge keeps live
643
+ // config.set changes flowing. Wired only for a manager this call owns —
644
+ // an injected manager (bootstrap-core's) is the caller's to seed/bridge.
645
+ featureFlags.loadFromConfig({ flags: deriveFeatureStates(configManager) });
646
+ bindFeatureSettingsBridge(configManager, featureFlags);
647
+ }
556
648
  const runtimeDispatch = createDomainDispatch(options.runtimeStore);
557
649
  const gatewayMethods = new GatewayMethodCatalog();
558
650
  const keybindingsManager = new KeybindingsManager({
@@ -609,9 +701,16 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
609
701
  });
610
702
  ensureConfiguredModelIsRoutable(providerRegistry, configManager);
611
703
  providerRegistry.initCustomProviders();
704
+ providerRegistry.initProviderModelDiscovery();
705
+ // ONE credential chain (env -> secrets -> subscription), mirroring the SDK
706
+ // composition root: boot applies secrets-backed keys; every secrets
707
+ // write/delete re-registers builtin providers LIVE (no restart needed).
708
+ secretsManager.onDidChange(() => void providerRegistry.refreshProviderCredentials().catch((error) => logger.warn('live credential refresh failed', { error: summarizeError(error) })));
709
+ void providerRegistry.refreshProviderCredentials().catch((error) => logger.warn('boot credential refresh failed', { error: summarizeError(error) }));
612
710
  const toolLLM = new ToolLLM({
613
711
  configManager,
614
712
  providerRegistry,
713
+ runtimeBus: options.runtimeBus,
615
714
  });
616
715
  const localUserAuthManager = options.localUserAuthManager ?? new UserAuthManager({
617
716
  bootstrapFilePath: shellPaths.resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'auth-users.json'),
@@ -643,6 +742,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
643
742
  messageBus: agentMessageBus,
644
743
  executor: agentOrchestrator,
645
744
  configManager,
745
+ providerRegistry,
646
746
  });
647
747
  agentManager.setRuntimeBus(options.runtimeBus);
648
748
  const wrfcController = Reflect.construct(WrfcController, [options.runtimeBus, agentMessageBus, {
@@ -662,6 +762,12 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
662
762
  const approvalBroker = new ApprovalBroker({
663
763
  storePath: shellPaths.resolveProjectPath(GOODVIBES_AGENT_SURFACE_ROOT, 'control-plane', 'approvals.json'),
664
764
  });
765
+ // Durable user-origin permission rules (remembered approvals), mirroring the
766
+ // SDK composition root: one store per project, consumed by the permission
767
+ // manager (bootstrap-core) and the permissions.rules.* gateway verbs below.
768
+ // Background init is fail-safe: a broken store means asks keep prompting.
769
+ const userPermissionRuleStore = new UserPermissionRuleStore(join(configManager.getControlPlaneConfigDir(), 'permission-rules.json'));
770
+ void userPermissionRuleStore.init().catch((error) => logger.warn('user permission rule store init failed; asks will prompt', { error: summarizeError(error) }));
665
771
  const sessionBroker = new SharedSessionBroker({
666
772
  storePath: shellPaths.resolveProjectPath(GOODVIBES_AGENT_SURFACE_ROOT, 'control-plane', 'sessions.json'),
667
773
  routeBindings,
@@ -677,9 +783,21 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
677
783
  // starts the daemon. All calls are fire-and-forget; a down daemon degrades to
678
784
  // an honest offline queue while the local broker keeps rendering.
679
785
  const spineResolveConnection = createSpineConnectionResolver(configManager, homeDirectory);
786
+ const daemonReceiptFeed = new AgentDaemonReceiptFeed();
787
+ // The daemon's one-shot honesty receipts are delivered ONLY to an explicit
788
+ // `?receipts=consume` read (a plain /status read is receipt-neutral), so the
789
+ // liveness probe below stays plain and a SEPARATE consuming read runs once
790
+ // per attach (bootstrap wires this to the memory-spine reconciler's onAttach).
791
+ const spineReceiptConsumer = createSpineReceiptConsumer({ resolveConnection: spineResolveConnection });
792
+ const consumeDaemonReceipts = async (): Promise<void> => {
793
+ const receipts = await spineReceiptConsumer();
794
+ if (receipts.length > 0) daemonReceiptFeed.push(receipts);
795
+ };
680
796
  const sessionSpineClient = new SessionSpineClient({
681
797
  participant: AGENT_SPINE_PARTICIPANT,
682
798
  transport: createSpineRestTransport({ resolveConnection: spineResolveConnection }),
799
+ // Liveness only: a plain /status read that never consumes receipts. The
800
+ // once-per-attach consuming read above is the agent's receipt reader.
683
801
  probe: createSpineRestProbe({ resolveConnection: spineResolveConnection }),
684
802
  log: logger,
685
803
  });
@@ -687,7 +805,11 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
687
805
  const record = agentManager.spawn({
688
806
  mode: 'spawn',
689
807
  task,
690
- ...buildAgentSpawnRoutingFromSharedSession(input.routing, { restrictTools: true }),
808
+ // Spawn routing resolves through the SDK's shared model-reference
809
+ // resolver contract (unique-across-registry auto-qualifies; ambiguous
810
+ // and unknown ids throw errors naming real candidates) — the live
811
+ // registry's models are the candidate list.
812
+ ...buildAgentSpawnRoutingFromSharedSession(input.routing, { restrictTools: true, modelCandidates: providerRegistry.listModels() }),
691
813
  context: `shared-session:${input.sessionId}`,
692
814
  });
693
815
  return { agentId: record.id };
@@ -733,6 +855,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
733
855
  runtimeStore: options.runtimeStore,
734
856
  runtimeBus: options.runtimeBus,
735
857
  deliveryManager,
858
+ providerRegistry,
736
859
  spawnTask: (input) => {
737
860
  const record = agentManager.spawn({
738
861
  mode: 'spawn',
@@ -908,8 +1031,32 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
908
1031
  subscriptionManager,
909
1032
  secretsManager,
910
1033
  });
1034
+ // Localhost dev-server fetches ride the same broker: ask once, one-tap
1035
+ // "allow for this project", persisted as fetch.allowLocalhost.
1036
+ // An exec command blocked on a terminal prompt (host-key confirmation,
1037
+ // credential ask) rides the same broker: the pending prompt surfaces through
1038
+ // every surface's approval machinery and the typed answer feeds the same
1039
+ // continuing run. The SDK's own wiring (public since this SDK round): the
1040
+ // former agent-local mirror existed only because the builder had no public
1041
+ // export path.
1042
+ const execPromptAnswerHandler = buildExecPromptAnswerHandler({
1043
+ requestApproval: (input) => approvalBroker.requestApproval(input),
1044
+ });
1045
+ const localhostFetchApproval = buildLocalhostFetchApproval({
1046
+ requestApproval: (input) => approvalBroker.requestApproval(input),
1047
+ configManager,
1048
+ });
1049
+ // Announce-once receipts for default-on features: the first contained exec
1050
+ // run yields the one-time containment line (persisted, once per install).
1051
+ const announcementStore = new FeatureAnnouncementStore(featureAnnouncementsPath(configManager));
1052
+ const onSandboxedRun = createSandboxContainmentAnnouncer(announcementStore, (announcement) => {
1053
+ logger.info(announcement.text, { announcement: announcement.id });
1054
+ });
911
1055
  agentOrchestrator.setDependencies({
912
1056
  surfaceRoot: GOODVIBES_AGENT_SURFACE_ROOT,
1057
+ execPromptAnswerHandler,
1058
+ localhostFetchApproval,
1059
+ onSandboxedRun,
913
1060
  fileCache,
914
1061
  projectIndex,
915
1062
  workingDirectory,
@@ -959,17 +1106,43 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
959
1106
  // explicitly invoked. See the construction site
960
1107
  // below for the full registered-workspaces-only
961
1108
  // rule (owner ruling, 2026-07-10).
1109
+ // Honest-unpriced: usage prices through the ONE model pricing resolver
1110
+ // (manual -> registration -> provider-served -> catalog -> unknown; any
1111
+ // resolvable model). Unknown/subscription yields null (costState
1112
+ // 'unpriced'), never $0. SHARED by fleet + orchestration so totals never
1113
+ // double-count — mirrors the SDK composition root.
1114
+ const priceUsage = (model: string | undefined, usage: { inputTokens: number; outputTokens: number }): number | null => (model ? computeUsageCostUsd(providerRegistry.resolveModelPricing(model), usage) : null);
1115
+
962
1116
  const orchestrationEngine = createOrchestrationEngine({
963
1117
  agentManager,
964
1118
  configManager,
965
1119
  runtimeBus: options.runtimeBus,
966
1120
  projectRoot: workingDirectory,
1121
+ priceUsage,
1122
+ judgeAttempts: createProviderBackedAttemptJudge(providerRegistry),
967
1123
  });
968
1124
  const codeIndexStore = new CodeIndexStore(
969
1125
  workingDirectory,
970
1126
  join(workingDirectory, '.goodvibes', 'agent', 'code-index.sqlite'),
971
1127
  memoryEmbeddingRegistry,
972
1128
  );
1129
+ // Data safety with no discipline: a daily snapshot of every SQLite store
1130
+ // this runtime writes, bounded retention, unref'd timers — the SDK's own
1131
+ // StoreSnapshotScheduler (public since this SDK round; the former agent-local
1132
+ // mirror and its local pruning engine existed only because the class and
1133
+ // RetentionPolicy/SnapshotPruner had no public export path). The canonical
1134
+ // memory db is shared with the daemon/TUI; the shared snapshot layout means
1135
+ // whichever process sweeps first writes that day's copy. The agent's code
1136
+ // index is deliberately inert (see the block comment above) — its entry is a
1137
+ // no-op until a file actually exists.
1138
+ const storeSnapshotScheduler = new StoreSnapshotScheduler({
1139
+ stores: [
1140
+ { name: 'memory store', dbPath: memoryDbPath },
1141
+ { name: 'memory vector index', dbPath: resolveMemoryVectorDbPath(memoryDbPath) },
1142
+ { name: 'code index store', dbPath: join(workingDirectory, '.goodvibes', 'agent', 'code-index.sqlite') },
1143
+ ],
1144
+ });
1145
+ storeSnapshotScheduler.start();
973
1146
  const codeIndexReindexScheduler = new CodeIndexReindexScheduler({
974
1147
  target: codeIndexStore,
975
1148
  workingDirectory,
@@ -993,6 +1166,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
993
1166
  messageBus: agentMessageBus,
994
1167
  automationManager,
995
1168
  runtimeBus: options.runtimeBus,
1169
+ priceUsage,
996
1170
  });
997
1171
  // Root/retention guard options come from the user's `checkpoints.*` settings
998
1172
  // (see config/checkpoint-settings.ts); any key the user did not set is omitted
@@ -1032,6 +1206,17 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
1032
1206
  if (registrationMigration) {
1033
1207
  logger.info('Migrated the local workspace registry into the shared registration store', { ...registrationMigration });
1034
1208
  }
1209
+ // After the import migration (records now in the shared store), stamp the ones
1210
+ // that came from the agent's OWN explicit list as checkpoint-eligible, so the
1211
+ // eligibility boundary below does not retroactively drop workspaces the owner
1212
+ // had already opted into checkpoints. Runs once (receipt-gated); derives the
1213
+ // explicit set from the still-present legacy registry file. Records lacking the
1214
+ // flag (a TUI first-open self-record) stay ineligible — the boundary the owner
1215
+ // ruled stays explicit.
1216
+ const eligibilityBackfill = backfillCheckpointEligibilityIfNeeded(shellPaths);
1217
+ if (eligibilityBackfill && eligibilityBackfill.recordsStamped > 0) {
1218
+ logger.info('Marked the agent\'s explicitly-registered workspaces checkpoint-eligible', { ...eligibilityBackfill });
1219
+ }
1035
1220
  const checkpointsRegistrationStatus = createWorkspaceRegistrationLiveChecker(shellPaths, workingDirectory);
1036
1221
  const checkpointsCurrentlyAllowed = (): boolean =>
1037
1222
  checkpointsRegistrationStatus() === 'covered' || readCheckpointRegistrationSetting(configManager) === 'guarded';
@@ -1142,6 +1327,23 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
1142
1327
  sessionBroker,
1143
1328
  secretsManager,
1144
1329
  approvalBroker,
1330
+ // The generic broker ask seam (rounds 4-6): CI-watch red runs raise a
1331
+ // "fix this?" offer through the same approval machinery as every ask.
1332
+ requestApproval: (input) => approvalBroker.requestApproval(input),
1333
+ // Recurring CI polling over the same watcher framework the fleet rows
1334
+ // already observe; degrades honestly to the manual verb when watchers
1335
+ // are disabled (the SDK registrar checks watchers.enabled itself).
1336
+ watcherRegistry,
1337
+ // permissions.rules.list/.delete over the durable remembered-approval
1338
+ // rules constructed above.
1339
+ userPermissionRuleStore,
1340
+ // Completion push source (rounds 4-6) plus the needs-input source ride
1341
+ // the runtime bus's fleet domain. DELIBERATE DIVERGENCE from the SDK
1342
+ // composition root: no sessionPresence is passed — the SDK builds its
1343
+ // isAttached check from hasFreshSurfaceParticipant, which has no public
1344
+ // export path. Absent presence means every needs-input block pushes
1345
+ // (the SDK-documented fallback), never a missed notification.
1346
+ runtimeBus: options.runtimeBus,
1145
1347
  shellPaths,
1146
1348
  configManager,
1147
1349
  runtimeStore: options.runtimeStore,
@@ -1212,6 +1414,8 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
1212
1414
  orchestrationEngine,
1213
1415
  codeIndexStore,
1214
1416
  codeIndexReindexScheduler,
1417
+ storeSnapshotScheduler,
1418
+ userPermissionRuleStore,
1215
1419
  processRegistry,
1216
1420
  workspaceCheckpointManager,
1217
1421
  runtimeBus: options.runtimeBus,
@@ -1225,8 +1429,13 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
1225
1429
  channelDeliveryRouter,
1226
1430
  watcherRegistry,
1227
1431
  approvalBroker,
1432
+ localhostFetchApproval,
1433
+ featureAnnouncementStore: announcementStore,
1434
+ onSandboxedRun,
1228
1435
  sessionBroker,
1229
1436
  sessionSpineClient,
1437
+ daemonReceiptFeed,
1438
+ consumeDaemonReceipts,
1230
1439
  deliveryManager,
1231
1440
  automationManager,
1232
1441
  gatewayMethods,