@pellux/goodvibes-daemon 1.28.19 → 1.28.20
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 +85 -63
- package/README.md +13 -13
- package/bin/launcher-support.js +5 -5
- package/package.json +6 -4
- package/scripts/postinstall.js +8 -8
- package/src/cli/command-catalog.ts +22 -22
- package/src/cli/completion.ts +4 -4
- package/src/cli/help.ts +5 -5
- package/src/cli/index.ts +3 -3
- package/src/cli/parser.ts +2 -2
- package/src/cli/surface-catalog.ts +1 -1
- package/src/cli/types.ts +2 -2
- package/src/cluster/daemon-ws-call.ts +5 -5
- package/src/cluster/raw-reply-route.ts +5 -5
- package/src/config/checkpoint-settings.ts +7 -7
- package/src/config/config-key-guard.ts +22 -0
- package/src/config/run-daemon-config-migration.ts +3 -3
- package/src/config/secret-config.ts +7 -7
- package/src/config/surface.ts +3 -3
- package/src/core/pairing-banner.ts +5 -5
- package/src/daemon/cli.ts +45 -43
- package/src/daemon/config-command.ts +15 -15
- package/src/daemon/handlers/context.ts +1 -1
- package/src/daemon/handlers/contracts.ts +4 -4
- package/src/daemon/handlers/credentials.ts +1 -1
- package/src/daemon/handlers/drafts/draft-store.ts +3 -3
- package/src/daemon/handlers/drafts/register.ts +4 -4
- package/src/daemon/handlers/inbox/aggregator.ts +8 -8
- package/src/daemon/handlers/inbox/cursor-store.ts +10 -10
- package/src/daemon/handlers/inbox/index.ts +7 -7
- package/src/daemon/handlers/inbox/mapping.ts +2 -2
- package/src/daemon/handlers/inbox/poller.ts +5 -5
- package/src/daemon/handlers/inbox/provider-adapter.ts +8 -8
- package/src/daemon/handlers/inbox/providers/discord.ts +6 -6
- package/src/daemon/handlers/inbox/providers/email.ts +3 -3
- package/src/daemon/handlers/inbox/providers/imap-client.ts +1 -1
- package/src/daemon/handlers/inbox/providers/slack.ts +4 -4
- package/src/daemon/handlers/index.ts +18 -8
- package/src/daemon/handlers/payments/card-store.ts +486 -0
- package/src/daemon/handlers/payments/index.ts +32 -0
- package/src/daemon/handlers/payments/purchase-ledger.ts +108 -0
- package/src/daemon/handlers/payments/register.ts +391 -0
- package/src/daemon/handlers/register.ts +3 -3
- package/src/daemon/handlers/remote/backends/cloud-terminal.ts +9 -1
- package/src/daemon/handlers/remote/backends/process-runner.ts +1 -1
- package/src/daemon/handlers/remote/backends/ssh.ts +9 -1
- package/src/daemon/handlers/remote/backends/types.ts +2 -2
- package/src/daemon/handlers/remote/dispatcher.ts +3 -3
- package/src/daemon/handlers/remote/index.ts +1 -1
- package/src/daemon/handlers/remote/peer-registry.ts +62 -13
- package/src/daemon/handlers/routing/inbox-bridge.ts +5 -5
- package/src/daemon/handlers/routing/index.ts +1 -1
- package/src/daemon/handlers/routing/route-store.ts +1 -1
- package/src/daemon/handlers/routing/routing-resolver.ts +3 -3
- package/src/daemon/handlers/sqlite-store.ts +9 -9
- package/src/daemon/handlers/triage/index.ts +1 -1
- package/src/daemon/handlers/triage/integration.ts +3 -3
- package/src/daemon/handlers/triage/pipeline.ts +2 -2
- package/src/daemon/handlers/triage/scorer.ts +2 -2
- package/src/daemon/handlers/triage/tagger/discord.ts +3 -3
- package/src/daemon/handlers/triage/tagger/imap.ts +7 -7
- package/src/daemon/handlers/triage/tagger/index.ts +1 -1
- package/src/daemon/handlers/triage/tagger/shared.ts +3 -3
- package/src/daemon/handlers/triage/tagger/slack.ts +1 -1
- package/src/daemon/handlers/triage/types.ts +2 -2
- package/src/daemon/lifecycle.ts +5 -5
- package/src/daemon/local-daemon-state.ts +7 -7
- package/src/daemon/pair-command.ts +14 -14
- package/src/daemon/provision-wake-model.ts +5 -5
- package/src/daemon/send/channels.ts +7 -7
- package/src/daemon/send/command.ts +11 -11
- package/src/daemon/send/composition.ts +5 -5
- package/src/daemon/send/failure-text.ts +6 -6
- package/src/daemon/send/inert-text.ts +18 -18
- package/src/daemon/send/stdin.ts +3 -3
- package/src/daemon/service-commands.ts +32 -32
- package/src/daemon/sessions-command.ts +7 -7
- package/src/daemon/status-command.ts +22 -22
- package/src/daemon/webui-command.ts +14 -14
- package/src/runtime/boot-tasks.ts +1 -1
- package/src/runtime/cluster-composition.ts +9 -9
- package/src/runtime/cluster-group-composition.ts +7 -7
- package/src/runtime/conversation-rewind-port.ts +8 -8
- package/src/runtime/credential-composition.ts +2 -2
- package/src/runtime/daemon-handler-composition.ts +44 -4
- package/src/runtime/device-posture-composition.ts +10 -10
- package/src/runtime/disposal-wiring.ts +8 -8
- package/src/runtime/fleet-needs-input-push.ts +4 -4
- package/src/runtime/fleet-services.ts +1 -1
- package/src/runtime/hosted-session-composition.ts +13 -13
- package/src/runtime/index.ts +1 -1
- package/src/runtime/knowledge-services.ts +2 -2
- package/src/runtime/legacy-daemon-migration.ts +43 -43
- package/src/runtime/legacy-daemon-reconcile.ts +30 -30
- package/src/runtime/mail-composition.ts +6 -6
- package/src/runtime/notification-dispatch.ts +7 -7
- package/src/runtime/payments-composition.ts +143 -0
- package/src/runtime/plugin-composition.ts +7 -7
- package/src/runtime/runtime-services-types.ts +9 -9
- package/src/runtime/services.ts +29 -27
- package/src/runtime/trigger-services.ts +1 -1
- package/src/runtime/trust/checkpoint-eligibility.ts +5 -5
- package/src/runtime/trust/trust-gated-approvals.ts +9 -9
- package/src/runtime/update-check.ts +4 -4
- package/src/runtime/workspace-checkpointing.ts +6 -6
- package/src/testing/daemon-fixture.ts +11 -11
- package/src/testing/hosted-session-failures.ts +4 -4
- package/src/version.ts +2 -2
package/src/runtime/services.ts
CHANGED
|
@@ -60,7 +60,7 @@ import { createDevicePostureServices } from './device-posture-composition.ts';
|
|
|
60
60
|
// Re-exported so the daemon entrypoint reaches the housekeeping sweep through
|
|
61
61
|
// the same module it already imports the runtime graph from. `installDevicePosture`
|
|
62
62
|
// is deliberately NOT re-exported: it registers the phone TOOL into a tool
|
|
63
|
-
// registry, and the daemon registers no tools
|
|
63
|
+
// registry, and the daemon registers no tools, the sweep is the half it needs.
|
|
64
64
|
export { startDeviceHousekeeping } from './device-posture-composition.ts';
|
|
65
65
|
import { createClusterServices, startClusterServices } from './cluster-group-composition.ts';
|
|
66
66
|
import { createWorkspaceTrustDecisionAsk, trustGatedApprovalRaiser } from './trust/trust-gated-approvals.ts';
|
|
@@ -69,7 +69,7 @@ import type { RuntimeServicesOptions, RuntimeServices } from './runtime-services
|
|
|
69
69
|
export type { RuntimeServicesOptions, RuntimeServices } from './runtime-services-types.ts';
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
|
-
* createRuntimeServices
|
|
72
|
+
* createRuntimeServices, the daemon's service graph.
|
|
73
73
|
*
|
|
74
74
|
* This is the one composition root the daemon has. Capabilities a client and
|
|
75
75
|
* the daemon both need live in the SDK and are composed from there (memory
|
|
@@ -81,7 +81,7 @@ export type { RuntimeServicesOptions, RuntimeServices } from './runtime-services
|
|
|
81
81
|
*/
|
|
82
82
|
export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeServices {
|
|
83
83
|
// The SDK's disposal scope and its all-required poller list, plus the four
|
|
84
|
-
// pollers only the daemon has
|
|
84
|
+
// pollers only the daemon has, see disposal-wiring.ts.
|
|
85
85
|
const disposalScope = createDisposalScope('RuntimeServices');
|
|
86
86
|
const workingDirectory = options.workingDir;
|
|
87
87
|
const homeDirectory = options.homeDirectory;
|
|
@@ -105,7 +105,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
105
105
|
// so the scheduler gates and the knowledge background jobs can consult the pause
|
|
106
106
|
// controller before the MemoryGovernor (constructed at the composition tail)
|
|
107
107
|
// drives it. The admission gate is late-bound: expensive entry points capture
|
|
108
|
-
// this closure now and the governor binds into it at the tail
|
|
108
|
+
// this closure now and the governor binds into it at the tail, until then
|
|
109
109
|
// everything is admitted (the daemon is still booting).
|
|
110
110
|
const cacheRegistry = new CacheRegistry();
|
|
111
111
|
const pauseController = new PauseController();
|
|
@@ -149,7 +149,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
149
149
|
// Launch-tolerant: a provider whose API key is absent from the environment is
|
|
150
150
|
// constructed with a placeholder that is stripped immediately afterwards, so
|
|
151
151
|
// it lands unconfigured instead of throwing during construction. The daemon
|
|
152
|
-
// has the same must-boot property the agent has
|
|
152
|
+
// has the same must-boot property the agent has, it is a supervised service,
|
|
153
153
|
// and a constructor that throws on a missing key turns one unset variable into
|
|
154
154
|
// a crash loop with no screen to explain it.
|
|
155
155
|
const providerRegistry = createLaunchTolerantProviderRegistry({
|
|
@@ -186,8 +186,8 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
186
186
|
);
|
|
187
187
|
const hookActivityTracker = new HookActivityTracker();
|
|
188
188
|
// featureFlags is REQUIRED here in practice, even though the SDK types it
|
|
189
|
-
// optional. isFeatureGateEnabled(null, ...) is permissive by design
|
|
190
|
-
// embed with no manager wired gets the capability rather than a silent off
|
|
189
|
+
// optional. isFeatureGateEnabled(null, ...) is permissive by design, a narrow
|
|
190
|
+
// embed with no manager wired gets the capability rather than a silent off,
|
|
191
191
|
// so omitting it did not disable the watcher framework when watchers.enabled
|
|
192
192
|
// is turned off; it made the setting configure nothing.
|
|
193
193
|
const watcherRegistry = new WatcherRegistry({
|
|
@@ -235,9 +235,9 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
235
235
|
task,
|
|
236
236
|
// Conversation first: a follow-up message in a session gets an answer, not
|
|
237
237
|
// a write-review-fix-confirm chain with a reviewer, quality gates and a
|
|
238
|
-
// second agent. A chain opens only for an explicit authorization marker
|
|
238
|
+
// second agent. A chain opens only for an explicit authorization marker,
|
|
239
239
|
// the channel confirmation the owner gave, or the schedule/trigger that
|
|
240
|
-
// was confirmed when it was created
|
|
240
|
+
// was confirmed when it was created, or for a follow-up typed on a local
|
|
241
241
|
// surface. Both `conversationGate.mode` and the gated-surfaces list are
|
|
242
242
|
// read live.
|
|
243
243
|
...continuationChainOptions(input, {
|
|
@@ -256,7 +256,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
256
256
|
// It also supplies the run's context, which is why the bare
|
|
257
257
|
// `context: shared-session:<id>` line that used to sit below is gone.
|
|
258
258
|
// Spread BEFORE the routing builder so an explicit tool list coming from
|
|
259
|
-
// a routing intent still wins
|
|
259
|
+
// a routing intent still wins, that builder emits `tools` only when it
|
|
260
260
|
// actually has one.
|
|
261
261
|
...conversationalTurnSpawnOptions(input, { configReader: configManager }),
|
|
262
262
|
// Spawn routing through the SDK's shared model-reference resolver
|
|
@@ -286,7 +286,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
286
286
|
// through. Without the secrets manager it cannot resolve a
|
|
287
287
|
// goodvibes://secrets/... credential, so Telegram accepted every inbound
|
|
288
288
|
// message and dropped every reply with "Missing Telegram bot token" while
|
|
289
|
-
// ntfy
|
|
289
|
+
// ntfy, which needs no secret, worked.
|
|
290
290
|
secretsManager,
|
|
291
291
|
serviceRegistry,
|
|
292
292
|
runtimeBus: options.runtimeBus,
|
|
@@ -326,7 +326,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
326
326
|
return record.id;
|
|
327
327
|
},
|
|
328
328
|
});
|
|
329
|
-
// Knowledge/wiki + home-graph stack (governor backpressure wired in)
|
|
329
|
+
// Knowledge/wiki + home-graph stack (governor backpressure wired in), see knowledge-services.ts.
|
|
330
330
|
const {
|
|
331
331
|
knowledgeStore, agentKnowledgeStore, homeGraphKnowledgeStore,
|
|
332
332
|
knowledgeSemanticService, homeGraphSemanticService, agentKnowledgeSemanticService,
|
|
@@ -382,19 +382,21 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
382
382
|
});
|
|
383
383
|
|
|
384
384
|
// Which machines on this network are "us", and which of them reads the shared
|
|
385
|
-
// inbox. Both inert until startCluster()
|
|
385
|
+
// inbox. Both inert until startCluster(), no socket, no key material read;
|
|
386
386
|
// see cluster-group-composition.ts for why they are built together.
|
|
387
387
|
const { clusterGroup, clusterCoordinator } = createClusterServices({
|
|
388
388
|
configManager, shellPaths, secretsManager,
|
|
389
389
|
});
|
|
390
390
|
// Daemon handler surfaces (see daemon-handler-composition.ts); the inbox
|
|
391
|
-
// poller registers itself with the coordinator rather than starting eagerly
|
|
391
|
+
// poller registers itself with the coordinator rather than starting eagerly,
|
|
392
|
+
// and the payments family stops being a cataloged 501 facade there.
|
|
392
393
|
const daemonHandlers = createDaemonHandlerComposition({
|
|
393
394
|
gatewayMethods,
|
|
394
395
|
secretsManager,
|
|
395
396
|
configManager,
|
|
396
397
|
workingDirectory,
|
|
397
398
|
homeDirectory,
|
|
399
|
+
shellPaths,
|
|
398
400
|
distributedRuntime,
|
|
399
401
|
clusterCoordinator,
|
|
400
402
|
});
|
|
@@ -427,7 +429,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
427
429
|
// ONE router, not two. This was a second ChannelDeliveryRouter built from the
|
|
428
430
|
// same four arguments AutomationDeliveryManager builds its own from, so the
|
|
429
431
|
// router the gateway verbs held and the router replies actually leave through
|
|
430
|
-
// were different objects
|
|
432
|
+
// were different objects, and a delivery strategy registered on one was
|
|
431
433
|
// invisible to the other. The manager's is the one that replies; it is the one.
|
|
432
434
|
const channelDeliveryRouter = deliveryManager.getDeliveryRouter();
|
|
433
435
|
const processManager = new ProcessManager();
|
|
@@ -447,7 +449,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
447
449
|
});
|
|
448
450
|
const codeInjectionOrchestratorDeps = { codeIndex: codeIndexStore, isCodeInjectionSettingEnabled: () => isCodeInjectionSettingEnabled(configManager), codeIndexReindexScheduler };
|
|
449
451
|
// The trigger family: stream watchers, on-exit process triggers, condition
|
|
450
|
-
// checks
|
|
452
|
+
// checks, fed to the fleet below as its trigger supervisor, so a trigger
|
|
451
453
|
// is visible and steerable like every other running thing.
|
|
452
454
|
const triggerManager = createTriggerServices({
|
|
453
455
|
configManager, shellPaths, surfaceRoot: GOODVIBES_DAEMON_SURFACE_ROOT,
|
|
@@ -477,7 +479,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
477
479
|
acpHost, // Folds live hosted-agent sessions into the fleet as 'acp' rows
|
|
478
480
|
});
|
|
479
481
|
const modeManager = new ModeManager({ featureFlags }); const fileUndoManager = new FileUndoManager();
|
|
480
|
-
// Checkpoints, gated on live workspace registration
|
|
482
|
+
// Checkpoints, gated on live workspace registration, see workspace-checkpointing.ts.
|
|
481
483
|
const checkpointing = createWorkspaceCheckpointing({
|
|
482
484
|
workspaceRoot: workingDirectory, surface, runtimeBus: options.runtimeBus, configManager, shellPaths,
|
|
483
485
|
});
|
|
@@ -487,7 +489,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
487
489
|
// admitted (mirrors the SDK's own createRuntimeServices idle gate).
|
|
488
490
|
const { memoryConsolidationScheduler, powerManager, sessionLiveTurnControls } = wireIdlePowerAndLiveTurn({ configManager, memoryRegistry, runtimeBus: options.runtimeBus, isIdle: () => sessionBroker.countBusySessions() === 0 && !pauseController.isPaused('memory-consolidation') && admitExpensiveWork('memory consolidation').allowed, snapshotTick: () => storeSnapshotScheduler.tick(), heartbeat: async () => { await automationManager.triggerHeartbeat({ source: 'wake-catchup' }); }, powerSeam: options.powerSeam });
|
|
489
491
|
|
|
490
|
-
// Construct + start the MemoryGovernor (default ON
|
|
492
|
+
// Construct + start the MemoryGovernor (default ON, a safety feature) with the
|
|
491
493
|
// standard KNOWN cache adapters (knowledge stores + shared session broker),
|
|
492
494
|
// then late-bind the admission gate the expensive entry points captured
|
|
493
495
|
// earlier. The SDK owns this wiring.
|
|
@@ -514,7 +516,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
514
516
|
});
|
|
515
517
|
admitExpensiveWorkRef.current = (label) => memoryGovernor.admitExpensiveWork(label);
|
|
516
518
|
|
|
517
|
-
// Managed local-voice provisioning (voice.local.status/install)
|
|
519
|
+
// Managed local-voice provisioning (voice.local.status/install), single-flight
|
|
518
520
|
// one-act install + no-network status.
|
|
519
521
|
const { voiceSetup, stopWakeHousekeeping } = wireVoiceSetup({ configManager, shellPaths, voiceProviders, admitExpensiveWork,
|
|
520
522
|
// Boot provisioning of the wake-word model + its recovery sweep, opted into
|
|
@@ -543,11 +545,11 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
543
545
|
// to the owner principal instead of unknown.
|
|
544
546
|
channelPolicy,
|
|
545
547
|
approvalBroker, requestApproval: (input) => approvalBroker.requestApproval(input),
|
|
546
|
-
// approvals.raise
|
|
548
|
+
// approvals.raise, a surface CREATING an ask in this broker. Without it the
|
|
547
549
|
// verb is cataloged and unhandled, and a client whose prompt runs outside
|
|
548
550
|
// this process has no way to raise one.
|
|
549
551
|
approvalRaise: approvalBroker,
|
|
550
|
-
// credentials.set / credentials.delete
|
|
552
|
+
// credentials.set / credentials.delete, a credential written THROUGH the
|
|
551
553
|
// control plane, so a client with no access to the daemon's settings file can
|
|
552
554
|
// configure one. The value lands in the daemon's secret tier and the verb
|
|
553
555
|
// never echoes it back.
|
|
@@ -573,15 +575,15 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
573
575
|
const execPromptAnswerHandler = buildExecPromptAnswerHandler({ requestApproval: (input) => approvalBroker.requestApproval(input) });
|
|
574
576
|
// Tool asks from the runs this daemon HOSTS. Without a manager here, the
|
|
575
577
|
// background permission gate short-circuits to approved and every hosted
|
|
576
|
-
// write, command and delegation ran ungated
|
|
578
|
+
// write, command and delegation ran ungated, the workspace trust decision
|
|
577
579
|
// was read by nobody in this process.
|
|
578
580
|
//
|
|
579
581
|
// The ask seam is the trust gate wrapping the approval broker: a workspace
|
|
580
582
|
// with no decision yet has the question raised as an approval record and
|
|
581
|
-
// answered by whichever surface is attached (trust-gated-approvals.ts)
|
|
583
|
+
// answered by whichever surface is attached (trust-gated-approvals.ts),
|
|
582
584
|
// there is no screen here to show a modal on, so the raise replaces it. The
|
|
583
|
-
// manager's own layers
|
|
584
|
-
// user rules
|
|
585
|
+
// manager's own layers, permission mode, policy, session cache, durable
|
|
586
|
+
// user rules, still run first and are unchanged.
|
|
585
587
|
const permissionManager = createBrokeredPermissionManager({
|
|
586
588
|
requestApproval: trustGatedApprovalRaiser(
|
|
587
589
|
workspaceTrustManager,
|
|
@@ -648,7 +650,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
648
650
|
// whole high-churn 'ops' domain, and sent over the SAME WebhookNotifier the
|
|
649
651
|
// notification verbs keep live and boot-tasks attaches to the bus. There is
|
|
650
652
|
// no panel notification router here: its targets are all screen targets and
|
|
651
|
-
// this product has no screen
|
|
653
|
+
// this product has no screen, see notification-dispatch.ts.
|
|
652
654
|
wireMemoryPressureChannelNotice(options.runtimeBus, webhookNotifier);
|
|
653
655
|
|
|
654
656
|
// In-process config changes become key-level events on the `config` domain, so
|
|
@@ -780,7 +782,7 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
780
782
|
// Cancels the agent runs this graph was hosting. By dispose() time the fleet
|
|
781
783
|
// registry, orchestration engine, process registry and bus these runs report
|
|
782
784
|
// through are already down, so a run still described as "running" is orphaned
|
|
783
|
-
// rather than preserved
|
|
785
|
+
// rather than preserved, and this is the only shutdown-reachable way to
|
|
784
786
|
// abort its in-flight provider call instead of letting it sleep out a retry
|
|
785
787
|
// backoff nobody is waiting on.
|
|
786
788
|
cancelHostedAgentRuns: () => cancelAllAgentRuns(agentManager),
|
|
@@ -11,7 +11,7 @@ import type { ShellPathService } from '@/runtime/index.ts';
|
|
|
11
11
|
*
|
|
12
12
|
* This daemon composes the full family and feeds the manager to the fleet as
|
|
13
13
|
* its trigger supervisor, so a trigger defined against the daemon fires
|
|
14
|
-
* reliably. The daemon is the right process to own it
|
|
14
|
+
* reliably. The daemon is the right process to own it, it is the one that
|
|
15
15
|
* stays running.
|
|
16
16
|
*
|
|
17
17
|
* Two things about the shape are load-bearing:
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
*
|
|
24
24
|
* This read is synchronous by design. The decision is made per lifecycle event,
|
|
25
25
|
* inside a subscription callback that cannot await, and it has to reflect the
|
|
26
|
-
* store as it is on disk RIGHT NOW
|
|
26
|
+
* store as it is on disk RIGHT NOW, registering a workspace while the daemon is
|
|
27
27
|
* running has to take effect on the next eligible event, not on the next
|
|
28
28
|
* restart. The SDK's resolver is pure, so the only I/O is one small JSON read
|
|
29
29
|
* plus a single git worktree probe amortized at construction.
|
|
@@ -32,13 +32,13 @@ import {
|
|
|
32
32
|
export type StoreShellPaths = Pick<ShellPathService, 'resolveUserPath' | 'homeDirectory'>;
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* Path of the shared store's JSON document
|
|
35
|
+
* Path of the shared store's JSON document, the same path the SDK's gateway
|
|
36
36
|
* verb group constructs its own store over.
|
|
37
37
|
*
|
|
38
38
|
* "The same path" is the whole contract, and it is why this goes through the
|
|
39
39
|
* SDK's resolver rather than spelling any location out. goodvibes-agent reads
|
|
40
40
|
* and writes this same file directly and the SDK's gateway writes it, so the
|
|
41
|
-
* register is cross-product state rather than the daemon's own
|
|
41
|
+
* register is cross-product state rather than the daemon's own, it lives in
|
|
42
42
|
* the shared tier (~/.goodvibes/shared/), which takes no surface root, so all
|
|
43
43
|
* three resolve one identical path. Until the boot fold has moved it, the
|
|
44
44
|
* resolver falls back to the pre-split location read-only, so this reader never
|
|
@@ -92,7 +92,7 @@ interface SharedRegistrationSnapshot {
|
|
|
92
92
|
/**
|
|
93
93
|
* Synchronous read of the shared store's on-disk JSON, mirroring the store's own
|
|
94
94
|
* validation exactly (version 1, workspaces[], declines[]). A missing or
|
|
95
|
-
* unparsable file reads as empty
|
|
95
|
+
* unparsable file reads as empty, never throws.
|
|
96
96
|
*/
|
|
97
97
|
export function readSharedWorkspaceRegistrationSnapshotSync(shellPaths: StoreShellPaths): SharedRegistrationSnapshot {
|
|
98
98
|
const path = sharedWorkspaceRegistrationStorePath(shellPaths);
|
|
@@ -116,7 +116,7 @@ export function readSharedWorkspaceRegistrationSnapshotSync(shellPaths: StoreShe
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
/**
|
|
119
|
-
* Resolve `path` against ONLY the checkpoint-eligible registrations
|
|
119
|
+
* Resolve `path` against ONLY the checkpoint-eligible registrations, the
|
|
120
120
|
* boundary the automatic and explicit checkpoint gates consume. Worktree-link
|
|
121
121
|
* inheritance still applies: a linked worktree of a checkpoint-eligible main
|
|
122
122
|
* repository resolves as covered.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* trust-gated-approvals.ts
|
|
2
|
+
* trust-gated-approvals.ts, how a headless daemon asks the workspace trust
|
|
3
3
|
* question, and how the answer reaches the runs it hosts.
|
|
4
4
|
*
|
|
5
5
|
* The terminal app composes the trust gate at the permission machinery's final
|
|
6
6
|
* ask layer and raises the question as a modal on its own screen. The daemon
|
|
7
|
-
* has the same gate
|
|
8
|
-
* `<cwd>/.goodvibes/<surface>/trust.json` the terminal app writes
|
|
7
|
+
* has the same gate, `trustGatedAsk` next door, reading the same
|
|
8
|
+
* `<cwd>/.goodvibes/<surface>/trust.json` the terminal app writes, and no
|
|
9
9
|
* screen to raise anything on. Before this module it therefore
|
|
10
10
|
* did neither: the gate was constructed, never loaded, never consulted, and no
|
|
11
11
|
* hosted run passed through it.
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* ordinary approval record. That is the whole point of the approval-raise
|
|
17
17
|
* path: a process with no screen states the question, publishes it on
|
|
18
18
|
* `approval-update`, and whichever surface is attached answers it. Approved
|
|
19
|
-
* means "trusted", denied means "restricted"
|
|
19
|
+
* means "trusted", denied means "restricted", a real decision either way,
|
|
20
20
|
* persisted by the gate, and never asked again for this workspace.
|
|
21
21
|
*
|
|
22
22
|
* 2. `trustGatedApprovalRaiser` puts the gate in front of the raiser the
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
*
|
|
31
31
|
* What the daemon does NOT do here is decide by default. An untrusted
|
|
32
32
|
* workspace's hosted run neither runs as if trusted nor fails as if refused: it
|
|
33
|
-
* asks. If the ask cannot be answered
|
|
34
|
-
* failed
|
|
33
|
+
* asks. If the ask cannot be answered, nothing attached, or the broker itself
|
|
34
|
+
* failed, the workspace stays undecided and this run is refused, with the
|
|
35
35
|
* reason in the log rather than in a silence. A refusal that records nothing is
|
|
36
36
|
* the failure mode this whole seam exists to remove.
|
|
37
37
|
*/
|
|
@@ -44,7 +44,7 @@ type WorkspaceTrustLevel = operations.WorkspaceTrustLevel;
|
|
|
44
44
|
type WorkspaceTrustManager = operations.WorkspaceTrustManager;
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
* The extra fields a raise carries beside the request itself
|
|
47
|
+
* The extra fields a raise carries beside the request itself, the attribution
|
|
48
48
|
* routing/metadata a background-agent ask is stamped with before it reaches the
|
|
49
49
|
* broker. The gate only reads `request.category`, so these ride around it.
|
|
50
50
|
*/
|
|
@@ -73,7 +73,7 @@ export const WORKSPACE_TRUST_ASK_TIMEOUT_MS = 10 * 60 * 1000;
|
|
|
73
73
|
|
|
74
74
|
export interface WorkspaceTrustDecisionAskDeps {
|
|
75
75
|
readonly requestApproval: ApprovalRaise;
|
|
76
|
-
/** The workspace the question is about
|
|
76
|
+
/** The workspace the question is about, it names the directory being trusted. */
|
|
77
77
|
readonly workingDirectory: string;
|
|
78
78
|
readonly timeoutMs?: number | undefined;
|
|
79
79
|
}
|
|
@@ -118,7 +118,7 @@ export function createWorkspaceTrustDecisionAsk(
|
|
|
118
118
|
return decision.approved ? 'trusted' : 'restricted';
|
|
119
119
|
} catch (error) {
|
|
120
120
|
// Nothing attached, or the broker failed. Say so and leave the workspace
|
|
121
|
-
// undecided
|
|
121
|
+
// undecided, the next run asks again rather than inheriting a decision
|
|
122
122
|
// nobody made.
|
|
123
123
|
logger.warn('Workspace trust question could not be answered; this run is refused and the workspace stays undecided', {
|
|
124
124
|
workspace: deps.workingDirectory,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Version comparison and the release-tag lookup are re-exported from the
|
|
6
6
|
* SDK's canonical update policy module (platform/runtime/self-update), which
|
|
7
|
-
* was hoisted from this file's semantics
|
|
7
|
+
* was hoisted from this file's semantics, one mechanism everywhere.
|
|
8
8
|
* Install-kind detection stays local: it encodes how THIS package is
|
|
9
9
|
* installed (compiled binary vs bun/npm package vs source run) and what
|
|
10
10
|
* command replaces a swap for each kind.
|
|
@@ -27,14 +27,14 @@ export {
|
|
|
27
27
|
* How this running process was installed, detected honestly from
|
|
28
28
|
* process.execPath rather than assumed:
|
|
29
29
|
* - "binary": a standalone `bun build --compile` executable with no
|
|
30
|
-
* package-manager ancestry
|
|
30
|
+
* package-manager ancestry, the scripts/install.sh install path.
|
|
31
31
|
* Swappable in place.
|
|
32
32
|
* - "bun-global-package": running the vendored binary shipped inside an
|
|
33
33
|
* npm/bun-managed package install (execPath contains a "node_modules"
|
|
34
|
-
* path segment
|
|
34
|
+
* path segment, true for both `bun add -g` and a local project
|
|
35
35
|
* dependency). Managed by the package manager; swapping the vendored
|
|
36
36
|
* file in place would fight the next `bun add -g` upgrade, so this is
|
|
37
|
-
* never swapped
|
|
37
|
+
* never swapped, the user re-runs their package manager instead.
|
|
38
38
|
* - "source": running directly via the `bun` interpreter (`bun run
|
|
39
39
|
* src/main.ts`), not a compiled binary at all.
|
|
40
40
|
*/
|
|
@@ -17,19 +17,19 @@ import { createWorkspaceRegistrationLiveChecker, type StoreShellPaths } from './
|
|
|
17
17
|
* runtime bus, so building it without one for an unregistered workspace would
|
|
18
18
|
* mean registering that workspace mid-run had no effect until a restart. It is
|
|
19
19
|
* always built WITH the bus and each individual automatic snapshot attempt is
|
|
20
|
-
* refused instead
|
|
20
|
+
* refused instead, by overriding this one instance's own `create`, since the
|
|
21
21
|
* manager has no predicate hook and `create` is the single seam both the
|
|
22
22
|
* automatic subscription and every explicit caller pass through.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
export interface WorkspaceCheckpointing {
|
|
26
|
-
/** The manager itself
|
|
26
|
+
/** The manager itself, automatic snapshots gated, explicit creates unrestricted. */
|
|
27
27
|
readonly manager: WorkspaceCheckpointManager;
|
|
28
28
|
/**
|
|
29
29
|
* The narrower surface handed to the `checkpoints.*` gateway verbs: identical
|
|
30
30
|
* except that an explicit create refuses, with an actionable message, when the
|
|
31
31
|
* workspace is not checkpoint-eligible. Reads (list/diff/sessionChanges) and
|
|
32
|
-
* restore stay unrestricted
|
|
32
|
+
* restore stay unrestricted, they operate over checkpoints that may already
|
|
33
33
|
* exist, including from a since-unregistered workspace.
|
|
34
34
|
*/
|
|
35
35
|
readonly gatewayManager: Pick<WorkspaceCheckpointManager, 'list' | 'create' | 'diff' | 'restore' | 'sessionChanges' | 'workspaceRoot'>;
|
|
@@ -69,7 +69,7 @@ export function createWorkspaceCheckpointing(opts: {
|
|
|
69
69
|
});
|
|
70
70
|
|
|
71
71
|
// Automatic snapshots ('turn' | 'agent-run', fired by the manager's own bus
|
|
72
|
-
// subscription) resolve to null quietly when the workspace is not eligible
|
|
72
|
+
// subscription) resolve to null quietly when the workspace is not eligible,
|
|
73
73
|
// there is no caller to throw to, and the manager already documents a null
|
|
74
74
|
// return as the cheap no-op for an unchanged tree. Explicit ('manual') creates
|
|
75
75
|
// are NOT re-gated here: they go through the gateway surface below, which
|
|
@@ -82,7 +82,7 @@ export function createWorkspaceCheckpointing(opts: {
|
|
|
82
82
|
|
|
83
83
|
// Eagerly initialize so the automatic-snapshot subscription is live before the
|
|
84
84
|
// first turn completes. If init() rejects, the manager caches that rejection
|
|
85
|
-
// forever and every later call re-throws it
|
|
85
|
+
// forever and every later call re-throws it, the catch here only prevents an
|
|
86
86
|
// unhandled rejection at startup; the checkpoint verbs report the failure to
|
|
87
87
|
// whoever calls them.
|
|
88
88
|
void manager.init().catch((error: unknown) => {
|
|
@@ -104,7 +104,7 @@ export function createWorkspaceCheckpointing(opts: {
|
|
|
104
104
|
);
|
|
105
105
|
}
|
|
106
106
|
// Default the session stamp from the live resolver when the caller omits
|
|
107
|
-
// it
|
|
107
|
+
// it, the resolveSessionId hook only auto-stamps automatic snapshots, so
|
|
108
108
|
// without this an explicit checkpoint made this launch would be written
|
|
109
109
|
// unstamped and excluded by the session-scoped restore lookup.
|
|
110
110
|
const sessionId = createOpts.sessionId ?? opts.resolveSessionId?.({});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* daemon-fixture.ts
|
|
2
|
+
* daemon-fixture.ts, a real, running daemon, composed the way this product
|
|
3
3
|
* composes one, for a test to drive.
|
|
4
4
|
*
|
|
5
5
|
* ── Why this is a shipped module and not a test helper ────────────────────
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* and each of them hand-builds a composition to test against: a stub session
|
|
9
9
|
* broker here, a fake approval broker there, a catalog with the handlers
|
|
10
10
|
* somebody remembered to attach. They are large (several hundred lines each),
|
|
11
|
-
* they duplicate each other, and
|
|
11
|
+
* they duplicate each other, and, the part that matters, they are each a
|
|
12
12
|
* SECOND idea of what a daemon is. A client contract test passing against a
|
|
13
13
|
* hand-rolled stand-in tells you the stand-in agrees with the client, which is
|
|
14
14
|
* the one agreement that was never in doubt.
|
|
@@ -43,19 +43,19 @@
|
|
|
43
43
|
* can do for itself:
|
|
44
44
|
*
|
|
45
45
|
* 1. A NAMED ENTRY POINT. `goodvibes-daemon/testing` instead of the deep
|
|
46
|
-
* path. That means adding an `exports` map to package.json
|
|
46
|
+
* path. That means adding an `exports` map to package.json, and adding
|
|
47
47
|
* one is not additive: an `exports` map REPLACES path-based resolution, so
|
|
48
48
|
* every existing deep import into this package (its own `bin` shim
|
|
49
49
|
* included) stops resolving unless the map enumerates them. That is a
|
|
50
50
|
* packaging change with a blast radius, made once, deliberately, by
|
|
51
|
-
* whoever owns distribution
|
|
51
|
+
* whoever owns distribution, not a side effect of adding a test helper.
|
|
52
52
|
*
|
|
53
53
|
* 2. THE CONSUMER DEPENDENCY. A consumer repo has to depend on
|
|
54
54
|
* `goodvibes-daemon` to import this at all. Today the agent does (it is
|
|
55
55
|
* what makes its own install fail against an unpublished version); the
|
|
56
56
|
* terminal app and the webui do not. For the terminal app that is a
|
|
57
57
|
* devDependency and a version pin. For the webui, whose suites run in a
|
|
58
|
-
* browser context, the fixture cannot run in-process at all
|
|
58
|
+
* browser context, the fixture cannot run in-process at all, it needs a
|
|
59
59
|
* launcher script that starts the fixture in a node process and hands the
|
|
60
60
|
* Playwright suite `baseUrl` and `token`. That launcher does not exist and
|
|
61
61
|
* should be written on the webui side, where its runner lives.
|
|
@@ -65,12 +65,12 @@
|
|
|
65
65
|
* per test file pays about a second each and must `stop()` every one; the
|
|
66
66
|
* three hand-built stand-ins it replaces cost nothing and leak nothing.
|
|
67
67
|
* The honest guidance is one fixture per FILE (`beforeAll`/`afterAll`),
|
|
68
|
-
* which is how this repository's own suites use it
|
|
68
|
+
* which is how this repository's own suites use it, not one per test.
|
|
69
69
|
*
|
|
70
70
|
* 4. WHAT IT DOES NOT REPLACE. A stand-in is still the right tool for
|
|
71
71
|
* driving a client through a daemon state that is hard to reach for real:
|
|
72
72
|
* a wedged session, a specific 500, a torn connection. This fixture
|
|
73
|
-
* replaces the stand-ins that exist only to answer normally
|
|
73
|
+
* replaces the stand-ins that exist only to answer normally, which is
|
|
74
74
|
* most of the several hundred lines in each consumer, and all of the part
|
|
75
75
|
* that silently drifts. The refusal shapes it cannot easily produce live
|
|
76
76
|
* are exported separately and pinned against the real engine; see
|
|
@@ -145,7 +145,7 @@ export interface DaemonFixtureOptions {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
export interface DaemonFixture {
|
|
148
|
-
/** The composed runtime graph
|
|
148
|
+
/** The composed runtime graph, the same object `DaemonServer` was handed. */
|
|
149
149
|
readonly services: RuntimeServices;
|
|
150
150
|
/** The running server. */
|
|
151
151
|
readonly daemon: DaemonServer;
|
|
@@ -159,7 +159,7 @@ export interface DaemonFixture {
|
|
|
159
159
|
fetch(path: string, init?: RequestInit): Promise<Response>;
|
|
160
160
|
/**
|
|
161
161
|
* Fetch a path with NO credential. A route that exists answers 401; a path
|
|
162
|
-
* nothing serves answers 404
|
|
162
|
+
* nothing serves answers 404, which is what makes this the side-effect-free
|
|
163
163
|
* way to ask whether a route exists, even for a write verb.
|
|
164
164
|
*/
|
|
165
165
|
fetchAnonymous(path: string, init?: RequestInit): Promise<Response>;
|
|
@@ -170,7 +170,7 @@ export interface DaemonFixture {
|
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
/**
|
|
173
|
-
* Compose and start a daemon. Always `await fixture.stop()
|
|
173
|
+
* Compose and start a daemon. Always `await fixture.stop()`, the runtime graph
|
|
174
174
|
* starts pollers while it builds, and abandoning it leaves every one of them
|
|
175
175
|
* firing for the rest of the process.
|
|
176
176
|
*/
|
|
@@ -234,7 +234,7 @@ export async function startDaemonFixture(options: DaemonFixtureOptions = {}): Pr
|
|
|
234
234
|
// What the real entrypoint states, for the same reason it states it: the
|
|
235
235
|
// hosted-session verbs are registered by this composition and by nothing
|
|
236
236
|
// else, so a fixture that leaves it out is a daemon a client cannot start a
|
|
237
|
-
// session on
|
|
237
|
+
// session on, and every contract test written against it would agree.
|
|
238
238
|
...(options.hostSessions === false ? {} : { hostedSessions: createHostedSessionOptions(services) }),
|
|
239
239
|
});
|
|
240
240
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* hosted-session-failures.ts
|
|
2
|
+
* hosted-session-failures.ts, what `sessions.hosted.*` answers when it refuses.
|
|
3
3
|
*
|
|
4
4
|
* ── Why this is a shipped module ─────────────────────────────────────────
|
|
5
5
|
*
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* These are the three refusals a client has to handle and could not previously
|
|
20
20
|
* mock. They are declared here rather than hand-written in each consumer, and
|
|
21
21
|
* `src/test/daemon/gateway-hosted-session-failures.test.ts` drives the REAL
|
|
22
|
-
* engine to produce each one and asserts it still matches
|
|
22
|
+
* engine to produce each one and asserts it still matches, so a fixture that
|
|
23
23
|
* drifts from the daemon fails in this repository, not in a consumer's CI six
|
|
24
24
|
* weeks later.
|
|
25
25
|
*/
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
export interface HostedSessionFailureFixture {
|
|
29
29
|
/** The verb whose refusal this is. */
|
|
30
30
|
readonly methodId: string;
|
|
31
|
-
/** Machine-readable code
|
|
31
|
+
/** Machine-readable code, the field a client branches on. */
|
|
32
32
|
readonly code: string;
|
|
33
33
|
/** HTTP status the control plane maps this refusal to. */
|
|
34
34
|
readonly status: number;
|
|
@@ -65,7 +65,7 @@ export const HOSTED_SESSION_UNAVAILABLE: HostedSessionFailureFixture = {
|
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
67
|
* At the configured cap. 429: the request is well-formed and the daemon is not
|
|
68
|
-
* broken
|
|
68
|
+
* broken, there is no room. Retrying after a kill succeeds.
|
|
69
69
|
*/
|
|
70
70
|
export const HOSTED_SESSION_LIMIT_REACHED: HostedSessionFailureFixture = {
|
|
71
71
|
methodId: 'sessions.hosted.create',
|
package/src/version.ts
CHANGED
|
@@ -13,14 +13,14 @@ try {
|
|
|
13
13
|
// compiled binary resolves import.meta.dir to a virtual root where the
|
|
14
14
|
// `../package.json` path can land on a DIFFERENT package.json (a bundled
|
|
15
15
|
// dependency's, or the embedded runtime's) that reports a placeholder like
|
|
16
|
-
// "0.0.0"
|
|
16
|
+
// "0.0.0", exactly the wrong-version banner a bare daemon launch showed in
|
|
17
17
|
// the field. Guarding on the package name means the prebuild-baked fallback
|
|
18
18
|
// above wins in that case instead of a stray version.
|
|
19
19
|
if (pkg?.name === 'goodvibes-daemon' && typeof pkg.version === 'string' && pkg.version.length > 0) {
|
|
20
20
|
_version = pkg.version;
|
|
21
21
|
}
|
|
22
22
|
} catch {
|
|
23
|
-
// Compiled binary or missing package.json
|
|
23
|
+
// Compiled binary or missing package.json, use fallback
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export const VERSION = _version;
|