@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.
- package/CHANGELOG.md +10 -0
- package/README.md +12 -1
- package/dist/package/main.js +36712 -28803
- package/dist/package/{web-tree-sitter-jbz042ba.wasm → web-tree-sitter-e011xaqr.wasm} +0 -0
- package/docs/README.md +1 -1
- package/docs/connected-host.md +1 -1
- package/docs/getting-started.md +1 -1
- package/docs/tools-and-commands.md +1 -0
- package/package.json +3 -3
- package/release/live-verification/live-verification.json +11 -11
- package/release/live-verification/live-verification.md +12 -12
- package/src/agent/email/email-service.ts +1 -1
- package/src/agent/email/imap-client.ts +4 -4
- package/src/agent/email/smtp-client.ts +5 -5
- package/src/cli/config-overrides.ts +29 -14
- package/src/cli/entrypoint.ts +12 -4
- package/src/cli/launch-auto-update.ts +218 -0
- package/src/cli/relay-command.ts +4 -4
- package/src/cli/service-posture.ts +2 -2
- package/src/cli/tui-startup.ts +31 -0
- package/src/cli/workspaces-command.ts +5 -1
- package/src/cli-flags.ts +1 -1
- package/src/config/index.ts +1 -1
- package/src/config/update-settings.ts +45 -0
- package/src/config/workspace-registration.ts +214 -15
- package/src/input/commands/runtime-services.ts +0 -5
- package/src/input/commands/update-runtime.ts +313 -0
- package/src/input/commands.ts +2 -0
- package/src/input/feed-context-factory.ts +2 -2
- package/src/input/handler-feed.ts +8 -8
- package/src/input/handler.ts +1 -1
- package/src/input/mcp-workspace.ts +5 -1
- package/src/input/panel-paste-flood-guard.ts +1 -1
- package/src/input/settings-modal-types.ts +11 -5
- package/src/input/settings-modal.ts +56 -61
- package/src/main.ts +19 -20
- package/src/renderer/activity-sidebar.ts +53 -4
- package/src/renderer/settings-modal-helpers.ts +2 -0
- package/src/renderer/settings-modal.ts +37 -25
- package/src/runtime/bootstrap-core.ts +16 -5
- package/src/runtime/bootstrap-external-services.ts +107 -11
- package/src/runtime/bootstrap-hook-bridge.ts +2 -2
- package/src/runtime/bootstrap-shell.ts +4 -4
- package/src/runtime/bootstrap.ts +34 -12
- package/src/runtime/connected-host-autostart.ts +269 -0
- package/src/runtime/daemon-receipts.ts +66 -0
- package/src/runtime/diagnostics/panels/index.ts +0 -2
- package/src/runtime/feature-enablement.ts +176 -0
- package/src/runtime/index.ts +12 -29
- package/src/runtime/memory-spine-adoption.ts +18 -0
- package/src/runtime/onboarding/apply.ts +50 -37
- package/src/runtime/onboarding/types.ts +2 -2
- package/src/runtime/onboarding/verify.ts +22 -4
- package/src/runtime/release-artifacts.ts +113 -0
- package/src/runtime/services.ts +228 -19
- package/src/runtime/session-spine-rest-transport.ts +84 -4
- package/src/runtime/ui-services.ts +1 -1
- package/src/runtime/update-check.ts +64 -0
- package/src/shell/ui-openers.ts +8 -0
- package/src/tools/agent-harness-metadata.ts +8 -0
- package/src/tools/agent-harness-setup-connected-host.ts +1 -1
- package/src/tools/agent-harness-setup-posture.ts +1 -1
- package/src/version.ts +1 -1
- package/src/runtime/diagnostics/panels/ops.ts +0 -156
- 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
|
+
}
|
package/src/runtime/services.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
188
|
-
if (
|
|
189
|
-
|
|
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
|
-
|
|
240
|
+
throw new Error(`Shared-session fallback model '${model}' must be provider-qualified.`);
|
|
192
241
|
});
|
|
193
242
|
}
|
|
194
243
|
|
|
195
|
-
|
|
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: {
|
|
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
|
|
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
|
-
|
|
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,
|