@cat-factory/node-server 0.107.25 → 0.108.1

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/dist/container.js CHANGED
@@ -340,6 +340,458 @@ function applyMothershipRemoteRepos(dependencies, remoteRepos) {
340
340
  remoteRepos.skillSourceRepository;
341
341
  }
342
342
  }
343
+ /**
344
+ * Assemble the engine {@link CoreDependencies} from the {@link NodeCoreDepsBundle} the composition
345
+ * root built. Extracted verbatim from {@link buildNodeContainer} so the runtime object is identical
346
+ * (later spreads still override earlier ones in the same order) — purely the function-size ratchet
347
+ * split, not a behaviour change.
348
+ */
349
+ function assembleNodeCoreDependencies(bundle) {
350
+ const { config, options, env, db, repos, sourced, idGenerator, clock, gateways, runnerUrlPolicy, githubInstallationRepository, environmentBackendRegistry, runnerBackendRegistry, customManifestTypeRegistry, agentKindRegistry, gateRegistry, stepResolverRegistry, initiativePresetRegistry, providerRegistry, apiKeys, subscriptions, personalSubscriptions, localModelEndpoints, openRouterCatalog, modelProviderResolver, cloudflareModelsEnabled, deployDeps, runnerPoolConnectionRepository, agentContextObservability, searchQueryObservability, resolveTestSecretRefs, githubClient, tasks, fileGitHubIssue, issueWritebackProvider, githubGateDeps, githubModuleDeps, bootstrapJobRepository, repoBootstrapper, slackDeps, executionEventPublisher, agentExecutor, notificationChannel, releaseHealthDeps, packageRegistryDeps, incidentEnrichmentDeps, accountSettings, resolveBinaryArtifactStore, } = bundle;
351
+ return {
352
+ ...releaseHealthDeps,
353
+ ...incidentEnrichmentDeps,
354
+ ...packageRegistryDeps,
355
+ // Fold the service frame's SENSITIVE test-credential refs (key + description, never values)
356
+ // into the tester prompt. Present when ENCRYPTION_KEY is set; absent ⇒ no advertised secrets.
357
+ ...(resolveTestSecretRefs ? { resolveTestSecretRefs } : {}),
358
+ // App-owned backend registries (kind → provider) the connection services resolve through.
359
+ environmentBackendRegistry,
360
+ runnerBackendRegistry,
361
+ // The app-owned agent-kind registry (built-ins + any deployment-registered kinds); the
362
+ // engine reads it (traits / inline-surface / pre-post-op hooks) and re-exposes it on Core.
363
+ agentKindRegistry,
364
+ // The app-owned gate + step-resolver registries; the engine's gate machine + completion hub
365
+ // read them, and the gate registry is re-exposed on Core for the boot-time validation.
366
+ gateRegistry,
367
+ stepResolverRegistry,
368
+ // The app-owned provider registry the gate providers were wired onto above; the engine's gate
369
+ // machine reads the SAME instance through its GateContext.
370
+ providerRegistry,
371
+ // The app-owned pipeline registry (deployment-registered extra pipelines); createCore threads
372
+ // it into the workspace + pipeline services and re-exposes it on Core for boot-time validation.
373
+ pipelineRegistry: options.pipelineRegistry,
374
+ // The app-owned custom task-type registry (deployment-registered namespaced task types);
375
+ // createCore threads it into the board service (default-pipeline resolution) and re-exposes it
376
+ // on Core for the snapshot projection (`customTaskTypes`) + boot-time validation.
377
+ taskTypeRegistry: options.taskTypeRegistry,
378
+ // The app-owned initiative-preset registry; the initiative services read it and it is
379
+ // re-exposed on Core for the snapshot descriptors + preset probe.
380
+ initiativePresetRegistry,
381
+ // The code-defined custom provision-type catalog, merged with the workspace rows by
382
+ // `listCustomTypes` so a programmatically-registered type surfaces in the infra editor + the
383
+ // per-service provisioning picker.
384
+ customManifestTypeRegistry,
385
+ ...(accountSettings ? { accountSettings } : {}),
386
+ // Resolves the per-account binary-artifact store (screenshots) for the visual-confirmation
387
+ // gate; resolving to null (no storage configured) ⇒ the gate passes through.
388
+ resolveBinaryArtifactStore,
389
+ workspaceRepository: repos.workspaceRepository,
390
+ workspaceMemberRepository: repos.workspaceMemberRepository,
391
+ accountRepository: repos.accountRepository,
392
+ membershipRepository: repos.membershipRepository,
393
+ userRepository: repos.userRepository,
394
+ passwordHasher: new WebCryptoPasswordHasher(),
395
+ blockRepository: repos.blockRepository,
396
+ pipelineRepository: repos.pipelineRepository,
397
+ executionRepository: repos.executionRepository,
398
+ // Clear a finished run's personal-credential activation promptly (TTL sweep is the backstop).
399
+ // In mothership mode its home is the LOCAL `node:sqlite` credential bucket (the activation
400
+ // re-seals the token for the run, and the LOCAL container executor decrypts it), injected via
401
+ // `options.subscriptionActivationRepository` — the SAME instance the personal-subscription
402
+ // service above mints into, so mint + clear agree. Absent (plain Node / siloed-Postgres local)
403
+ // → the Drizzle repo over `db`. This is NEVER routed through `sourced` (the remote registry):
404
+ // every no-db (mothership) caller injects the override — `buildLocalContainer` in production
405
+ // and `makeMothershipConformanceApp` in tests — so `db` here is always a real Postgres handle,
406
+ // and routing an activation clear to the mothership (where `deleteByExecution` isn't
407
+ // allow-listed) is a path no caller takes.
408
+ subscriptionActivationRepository: options.subscriptionActivationRepository ?? new DrizzleSubscriptionActivationRepository(db),
409
+ // In-org shared services. When a realtime hub is wired (start()), the engine's
410
+ // event publisher (composed above) is a `FanOutEventPublisher` over these two repos,
411
+ // so a shared service's live events reach every board that mounts it — parity with
412
+ // the Cloudflare facade. Without a hub (createServer/tests) the engine uses its
413
+ // NoopEventPublisher and nothing is pushed.
414
+ serviceRepository: repos.serviceRepository,
415
+ workspaceMountRepository: repos.workspaceMountRepository,
416
+ tokenUsageRepository: repos.tokenUsageRepository,
417
+ llmCallMetricRepository: repos.llmCallMetricRepository,
418
+ // Deployment-level rollups over `agent_runs` for the operator dashboard.
419
+ platformMetricsRepository: repos.platformMetricsRepository,
420
+ // Unified provisioning event log (its own Postgres schema). Threads the recorder
421
+ // into the env services and exposes the read service for the logs controller.
422
+ provisioningLogRepository: repos.provisioningLogRepository,
423
+ recordLlmPrompts: config.observability.recordPrompts,
424
+ // Re-exposed on the core for the agent-context read endpoint; the same instance
425
+ // is injected into the container executor above for the write path.
426
+ agentContextObservability,
427
+ // Re-exposed on the core for the search-query read endpoint AND the search proxy's
428
+ // write path (it reads it off the request container).
429
+ searchQueryObservability,
430
+ // Opt-in external trace sink(s) — Langfuse and/or OpenTelemetry — fanning every
431
+ // recorded LLM call out as a generation. Built only when configured; otherwise
432
+ // undefined and there is no external emission.
433
+ llmTraceSink: buildTraceSink(config),
434
+ modelPresetRepository: repos.modelPresetRepository,
435
+ // A fresh workspace's model-preset library is seeded with this built-in as the default
436
+ // (Node deploy → Kimi K2.7, the Cloudflare-runnable baseline; the local facade injects
437
+ // Claude). Applied only at first seed, so a user's later manual default choice wins.
438
+ defaultModelPresetId: options.defaultModelPresetId ?? DEFAULT_MODEL_PRESET_ID,
439
+ serviceFragmentDefaultsRepository: repos.serviceFragmentDefaultsRepository,
440
+ // Requirements-review feature (stateless reviewer + the requirements-rework
441
+ // step). Wired identically to the Cloudflare facade's `selectRequirementsDeps`
442
+ // so both runtimes serve the review/rework API AND substitute a block's reworked
443
+ // requirements into the agent context (the cross-runtime conformance suite asserts
444
+ // the substitution against both stores). The reviewer's model resolves exactly
445
+ // like a pipeline step: block-pin > workspace per-kind default > routing default
446
+ // (which falls back to Cloudflare Workers AI unless a direct key is set).
447
+ requirementReviewRepository: repos.requirementReviewRepository,
448
+ // Interactive document-interview sessions (WS5). Wired unconditionally; the interviewer
449
+ // reuses the requirements reviewer's model config resolved just below.
450
+ docInterviewRepository: repos.docInterviewRepository,
451
+ // Kaizen agent (post-run grading). Wired unconditionally, mirroring the Cloudflare
452
+ // facade, so the engine schedules gradings at run completion and the background sweep
453
+ // runs them. The grader resolves its model for the `kaizen` kind exactly like a step.
454
+ kaizenGradingRepository: repos.kaizenGradingRepository,
455
+ kaizenVerifiedComboRepository: repos.kaizenVerifiedComboRepository,
456
+ clarityReviewRepository: repos.clarityReviewRepository,
457
+ brainstormSessionRepository: repos.brainstormSessionRepository,
458
+ // Initiatives (the long-running multi-task work container). Wired unconditionally,
459
+ // mirroring the Worker's `selectMergeLifecycleDeps`, so the create/read API + the
460
+ // planning pipeline's ingest/committer steps work identically on both runtimes.
461
+ initiativeRepository: repos.initiativeRepository,
462
+ // Merge threshold presets: the per-workspace auto-merge ceiling library a task's
463
+ // merge gate resolves (block-pinned preset > workspace default). Wired
464
+ // unconditionally, exactly like the Worker's `selectMergeLifecycleDeps`, so the
465
+ // preset CRUD API + the merger step's threshold resolution work identically.
466
+ riskPolicyRepository: repos.riskPolicyRepository,
467
+ // Shared stacks (long-lived compose infra a consumer environment attaches to). Wired
468
+ // unconditionally like the merge presets so the CRUD API works identically on both
469
+ // runtimes; the bring-up (`ensureUp`) needs a host daemon, so plain Node has no
470
+ // `composeRuntime` — the local facade injects one via `overrides.composeRuntime`.
471
+ sharedStackRepository: repos.sharedStackRepository,
472
+ // Sandbox (parallel prompt/model testing) — contributed as one sandbox-owned mixin,
473
+ // symmetric with the Worker's `...selectSandboxDeps(db)`; the run-driver reuses the
474
+ // reviewer model config below. The container body never enumerates the five repos.
475
+ ...createDrizzleSandboxDeps(db),
476
+ // Per-workspace runtime settings (human-wait escalation threshold + per-service task
477
+ // limit). Wired unconditionally so the settings API + the limit enforcement + the
478
+ // escalation sweep work identically to the Worker.
479
+ workspaceSettingsRepository: repos.workspaceSettingsRepository,
480
+ userSettingsRepository: repos.userSettingsRepository,
481
+ modelProviderResolver,
482
+ requirementReviewModel: config.agents.routing.default.ref,
483
+ requirementReviewResolveModel: config.agents.resolveBlockModel,
484
+ // Local mode runs the inline reviewers/brainstorm/estimator on the ambient Claude Code /
485
+ // Codex CLI when the pinned model is a subscription harness (undefined on stock Node, so
486
+ // such refs degrade to the routing default). Also drives the preset satisfiability guard.
487
+ ...(config.agents.inlineHarnessRef ? { inlineHarnessRef: config.agents.inlineHarnessRef } : {}),
488
+ // Notifications subsystem (parity with the Worker, which wires it unconditionally):
489
+ // the inbox + the human-action surfaces. Node has no real-time push, so the rows
490
+ // persist (inbox + snapshot) and any channel composed below — e.g. Slack — delivers.
491
+ notificationRepository: sourced('notificationRepository', (d) => new DrizzleNotificationRepository(d)),
492
+ ...tasks.deps,
493
+ // Recurring pipelines + the workspace tracker selection. The tracker provider
494
+ // files the tech-debt pipeline's issue by resolving the *workspace's* connected
495
+ // integration: GitHub issues through the workspace's GitHub App installation,
496
+ // Jira tickets from the per-workspace encrypted connection store — both per-tenant.
497
+ pipelineScheduleRepository: repos.pipelineScheduleRepository,
498
+ trackerSettingsRepository: repos.trackerSettingsRepository,
499
+ ticketTrackerProvider: new TicketTrackerService({
500
+ trackerSettingsRepository: repos.trackerSettingsRepository,
501
+ fetchImpl: fetch,
502
+ ...(fileGitHubIssue ? { fileGitHubIssue } : {}),
503
+ ...(tasks.taskConnectionRepository
504
+ ? {
505
+ resolveJiraConnection: async (workspaceId) => {
506
+ const connection = await tasks.taskConnectionRepository.getByWorkspace(workspaceId, 'jira');
507
+ const { baseUrl, accountEmail, apiToken } = connection?.credentials ?? {};
508
+ if (!baseUrl || !accountEmail || !apiToken)
509
+ return null;
510
+ return { baseUrl, accountEmail, apiToken };
511
+ },
512
+ resolveLinearConnection: async (workspaceId) => {
513
+ const connection = await tasks.taskConnectionRepository.getByWorkspace(workspaceId, 'linear');
514
+ const { apiKey, token } = connection?.credentials ?? {};
515
+ return apiKey || token ? { apiKey, token } : null;
516
+ },
517
+ }
518
+ : {}),
519
+ }),
520
+ issueWritebackProvider,
521
+ idGenerator,
522
+ clock,
523
+ agentExecutor,
524
+ spendPricing: config.spend,
525
+ // Price metered dynamic OpenRouter models at their real per-model rate (not the
526
+ // bare-`openrouter` fallback) using this workspace's enabled catalog.
527
+ dynamicModelPricesFor: openRouterCatalog
528
+ ? (ws) => openRouterCatalog.capabilitiesFor(ws)
529
+ : undefined,
530
+ // The runner-pool integration assembles when enabled, so a workspace can
531
+ // register the self-hosted pool its container agents dispatch to.
532
+ ...(config.runners.enabled && config.runners.encryptionKey
533
+ ? {
534
+ runnerPoolConnectionRepository,
535
+ runnerSecretCipher: new WebCryptoSecretCipher({
536
+ masterKeyBase64: config.runners.encryptionKey,
537
+ info: RUNNERS_CIPHER_INFO,
538
+ }),
539
+ // The pool provider instance backs the connection service's describeProvider +
540
+ // testConnection (the manifest editor's secret-key form + a pre-save probe). An
541
+ // injected native adapter wins here too (same instance that drives dispatch), so
542
+ // its describeConfig/testConnection render — else the generic manifest provider
543
+ // (same SSRF policy as the dispatch transport).
544
+ runnerPoolProvider: options.runnerPoolProvider ??
545
+ new HttpRunnerPoolProvider(runnerUrlPolicy ? { urlPolicy: runnerUrlPolicy } : {}),
546
+ // Node (and local) has undici, so it can verify a private CA / skip TLS for a
547
+ // Kubernetes apiserver — accept such a config at registration.
548
+ runnerCustomTlsSupported: true,
549
+ ...(runnerUrlPolicy ? { runnerUrlSafetyPolicy: runnerUrlPolicy } : {}),
550
+ }
551
+ : {}),
552
+ ...(options.boss
553
+ ? {
554
+ workRunner: new PgBossWorkRunner(options.boss, executionRuntime(config, env).queue),
555
+ // The durable bootstrap driver (analogue of the Worker's BootstrapWorkflow):
556
+ // BootstrapService.startRun enqueues a drive job that polls the run to terminal.
557
+ bootstrapRunner: new PgBossBootstrapRunner(options.boss, executionRuntime(config, env).queue),
558
+ // The durable env-config-repair driver (analogue of the Worker's
559
+ // EnvConfigRepairWorkflow): start enqueues a drive job that polls the run to terminal.
560
+ envConfigRepairRunner: new PgBossEnvConfigRepairRunner(options.boss, executionRuntime(config, env).queue),
561
+ // The durable ephemeral-environment self-test driver (analogue of the Worker's
562
+ // EnvironmentTestWorkflow): startRun enqueues a drive job that advances the run.
563
+ environmentTestRunner: new PgBossEnvironmentTestRunner(options.boss, executionRuntime(config, env).queue),
564
+ }
565
+ : {}),
566
+ ...githubGateDeps,
567
+ // GitHub installation + repo/branch/PR/issue/commit/check-run projections + the
568
+ // sync/webhook module (inline ingest persists to these repos on Node).
569
+ ...githubModuleDeps,
570
+ // Repo-bootstrap: the reference-architecture library + bootstrap-run store make the
571
+ // module + API available; `repoBootstrapper` (when wired) dispatches the bootstrap
572
+ // container through the shared runner seam, and `bootstrapRunner` (pg-boss, below)
573
+ // durably drives its poll loop — parity with the Worker's BootstrapWorkflow.
574
+ referenceArchitectureRepository: sourced('referenceArchitectureRepository', (d) => new DrizzleReferenceArchitectureRepository(d)),
575
+ bootstrapJobRepository,
576
+ ...(repoBootstrapper ? { repoBootstrapper } : {}),
577
+ // Env-config-repair runs share the unified agent_runs table (kind-scoped). The job
578
+ // repository is wired unconditionally; the repairer (agent fallback) is wired
579
+ // post-overrides below over the FINAL provider, and the durable runner in the
580
+ // `options.boss` block above — parity with the Worker's EnvConfigRepairWorkflow.
581
+ envConfigRepairJobRepository: sourced('envConfigRepairJobRepository', (d) => new DrizzleEnvConfigRepairJobRepository(d)),
582
+ // Ephemeral-environment self-test runs (their own table). The store is wired
583
+ // unconditionally; the environments module builds the service when it + a git provider
584
+ // are present, and the durable runner is wired in the `options.boss` block above.
585
+ environmentTestRunRepository: sourced('environmentTestRunRepository', (d) => new DrizzleEnvironmentTestRunRepository(d)),
586
+ // Document sources (Confluence / Notion / GitHub docs): wired from the shared
587
+ // integration providers exactly like the Worker, so a workspace can connect a
588
+ // source and import requirement/PRD/RFC pages as agent context.
589
+ ...selectNodeDocumentsDeps(config, db, githubClient, githubInstallationRepository),
590
+ // Ephemeral environments (opt-in): a workspace registers its own environment
591
+ // management API; the tester provisions/destroys per-run environments from it. A
592
+ // trusted in-house adapter can replace the default HTTP provider via the seam.
593
+ // The environment integration scopes its own URL/host policy from
594
+ // `config.environments` inside this selector (separate from the runner pool's).
595
+ ...selectNodeEnvironmentsDeps(config, db),
596
+ // The async container-backed Kubernetes deploy lifecycle (deployJobClient +
597
+ // resolveDeployCloneTarget) — pool-backed by default, overridable by the local facade.
598
+ ...deployDeps,
599
+ // Prompt-fragment library (ADR 0006; opt-in): the managed tenant-scoped catalog
600
+ // of best-practice fragments feeding every agent run, wired exactly like the
601
+ // Worker's selectFragmentLibraryDeps (repos + installation resolver + selector).
602
+ ...selectNodeFragmentLibraryDeps(config, env, db, githubClient, githubInstallationRepository, modelProviderResolver),
603
+ // Repo-sourced Claude Skills library (docs/initiatives/repo-skills.md; opt-in): the
604
+ // account's catalog of repo-authored skills, wired exactly like the Worker's
605
+ // selectSkillLibraryDeps (account repos + installation resolver).
606
+ ...selectNodeSkillLibraryDeps(config, db, githubClient, githubInstallationRepository),
607
+ // Push-webhook skill-source freshness fan-out (slice 4): resync affected sources via the
608
+ // pg-boss GitHub-sync queue. No boss (pure-logic test) ⇒ no proactive resync; the
609
+ // dispatch-time probe is the freshness backstop.
610
+ enqueueSkillResync: async ({ accountId, sourceId }) => {
611
+ await gateways.githubWebhook.queueSkillResync(accountId, sourceId);
612
+ },
613
+ // Slack: an extra notification transport (the channel) + its management module.
614
+ // Default-off; when enabled its channel is composed into `notificationChannel` below
615
+ // alongside the in-app push, identically to the Worker.
616
+ ...slackDeps,
617
+ // Account invitations + per-account email senders (UI-onboarded, DB-stored).
618
+ ...selectNodeEmailInvitationDeps(config, repos),
619
+ // The pipeline-start guard resolves what's configured for a workspace + initiator.
620
+ resolveProviderCapabilities: (workspaceId, initiatedBy) => resolveWorkspaceCapabilities({
621
+ apiKeys,
622
+ subscriptions,
623
+ personalSubscriptions,
624
+ cloudflareModelsEnabled,
625
+ baseUrlFor: (provider) => baseUrlForNode(provider, env),
626
+ localModelEndpoints,
627
+ openRouterCatalog,
628
+ accountSettings,
629
+ workspaceAccountOf: (workspaceId) => repos.workspaceRepository.accountOf(workspaceId),
630
+ modelPolicySupported: config.infrastructure?.modelPolicy?.supported ?? false,
631
+ ...(options.caches ? { caches: options.caches } : {}),
632
+ }, workspaceId, initiatedBy),
633
+ // Real-time push (when a hub is wired) + the composed notification channel (in-app
634
+ // push + Slack). These come AFTER the spreads so the composite replaces the bare
635
+ // Slack channel `slackDeps` set; both are absent (no override) when nothing is wired.
636
+ ...(executionEventPublisher ? { executionEventPublisher } : {}),
637
+ ...(notificationChannel ? { notificationChannel } : {}),
638
+ // Run the engine's gate-probe / merge GitHub reads under the run initiator's ambient
639
+ // context, so a per-user PAT (when set) is preferred over the App/env token.
640
+ runInitiatorScope: runWithInitiator,
641
+ // The process-wide cache bag from start() (Redis-notified invalidation when REDIS_URL
642
+ // is set). Absent ⇒ createCore builds bare in-memory defaults.
643
+ ...(options.caches ? { caches: options.caches } : {}),
644
+ ...options.overrides,
645
+ };
646
+ }
647
+ /**
648
+ * Project the assembled engine core + the Node-facade extras onto the {@link ServerContainer}
649
+ * the HTTP layer resolves. Extracted verbatim from {@link buildNodeContainer} (a function-size
650
+ * ratchet split — behaviour is identical); the mothership persistence-registry surface, the
651
+ * per-user credential stores, and the VCS/gateway wiring are surfaced exactly as before.
652
+ */
653
+ function projectNodeServerContainer(bundle) {
654
+ const { dependencies, config, defaultWebSearchUpstream, resolveRepoTarget, repos, appRegistry, options, repoProjectionRepository, githubInstallationRepository, environmentBackendRegistry, runnerBackendRegistry, resolveBinaryArtifactStore, gateways, vcsRegistry, testSecretsService, subscriptions, personalSubscriptions, apiKeys, publicApiKeys, cloudflareModelsEnabled, env, localModelEndpoints, userSecrets, db, openRouterCatalog, traceSink, } = bundle;
655
+ return {
656
+ ...createCore(dependencies),
657
+ config,
658
+ // The deployment-wide trusted web-search upstream (built from `WEB_SEARCH_*` env above),
659
+ // read by `WebSearchProxyController` as the fallback when a run's account has no keys.
660
+ ...(defaultWebSearchUpstream ? { defaultWebSearchUpstream } : {}),
661
+ // The same checkout-free repo resolver the engine binds pre/post-ops with, surfaced so
662
+ // the shared service-spec read controller can read the `spec/` artifact off main.
663
+ resolveRunRepoContext: dependencies.resolveRunRepoContext,
664
+ // The block→service→repo resolver, surfaced so the task-search controller can scope a
665
+ // GitHub-issue search to the originating service's repo (and refuse it when unlinked).
666
+ resolveRepoTarget,
667
+ agentRunRepository: repos.agentRunRepository,
668
+ // Execution-scoped repo, surfaced for the conformance suite's compareAndSwap parity check.
669
+ executionRepository: repos.executionRepository,
670
+ // The repository registry the mothership-mode machine API (`/internal/persistence`) reflects
671
+ // over, so a Node deployment can act as a mothership for mothership-mode local nodes. The
672
+ // controller gates which repo+method is callable (allow-list) and account-scopes each call;
673
+ // exposing the whole `dependencies` (which carries every repo under its canonical name) is
674
+ // safe. `agentRunRepository` is the one repo NOT part of `CoreDependencies` (the engine's
675
+ // Core never reads it — it's surfaced separately above for `AgentRunController`), so fold it
676
+ // in explicitly, else the board's retry/stop `getRef` call comes back `... is not wired`.
677
+ // Sourced identically on both facades so they attach the same registry surface.
678
+ // Mothership-side GitHub token delegation (`POST /internal/github/installation-token`):
679
+ // when this deployment's GitHub App is configured, a machine-authed mothership-mode node
680
+ // can mint the short-lived installation tokens its agent containers/gates need — the App
681
+ // private key never leaves this service. The registry satisfies the seam structurally.
682
+ // Wired symmetrically on the Cloudflare facade.
683
+ ...(appRegistry ? { githubTokenDelegation: appRegistry } : {}),
684
+ // Mothership-side real-time UPSTREAM delivery (`POST /internal/events/publish`): when this
685
+ // deployment is a mothership (its realtime transport is wired), a machine-authed mothership-mode
686
+ // node's relayed engine events land in this deployment's OWN fan-out (`options.realtimeSink` —
687
+ // the hub, or the layered propagator on a multi-node deployment), so hosted teammates on the
688
+ // shared board see the local node's activity live. Wired symmetrically on the Cloudflare facade
689
+ // (the per-workspace WorkspaceEventsHub Durable Object). Absent realtime ⇒ the endpoint 503s.
690
+ ...(options.realtimeSink
691
+ ? { machineEventRelay: new LocalMachineEventRelay(options.realtimeSink) }
692
+ : {}),
693
+ repositories: {
694
+ ...dependencies,
695
+ agentRunRepository: repos.agentRunRepository,
696
+ // The binary-artifact METADATA store (visual-confirmation gate screenshots/references) is
697
+ // not part of `CoreDependencies` (it's composed into `resolveBinaryArtifactStore`, not the
698
+ // engine's Core), so fold it into the reflected registry explicitly — else a mothership-mode
699
+ // node's artifact reads/writes come back `... is not wired`. The blob BYTES stay per-account
700
+ // local; only the metadata is proxied.
701
+ binaryArtifactMetadataStore: repos.binaryArtifactMetadataStore,
702
+ // The sensitive per-service test-credential store is org/durable state the engine reads via
703
+ // the `resolveTestSecretRefs` FUNCTION (never the repo directly), so it isn't in
704
+ // `CoreDependencies` either — fold it in explicitly, else a mothership-mode node's tester
705
+ // run-path read + the inspector CRUD come back `... is not wired`. Only the SEALED blob is
706
+ // proxied (decrypted service-side under the LOCAL key), like the observability/runner-pool
707
+ // connections.
708
+ testSecretsRepository: repos.testSecretsRepository,
709
+ // GitHub projection + installation reads the mothership serves over the persistence RPC even
710
+ // when its OWN github service is off. A mothership-mode local node reaches GitHub by token
711
+ // DELEGATION (no local App), which enables `container.github`, so its board snapshot
712
+ // (`github.service.listRepos` → `repoProjectionRepository.list`) and run-path repo resolution
713
+ // (`githubInstallationRepository.getByWorkspace` + `repoProjectionRepository.list`) read the
714
+ // projection over RPC. Both are plain org tables the mothership owns, constructed
715
+ // unconditionally above — so reflect them regardless of `config.github.enabled` (they land in
716
+ // `dependencies` only when the github MODULE is wired), else a mothership without its own App
717
+ // configured 500s that board load with `... is not wired`. Allow-listed in
718
+ // `REMOTE_PERSISTENCE_METHODS`; folded in explicitly like the stores above.
719
+ repoProjectionRepository,
720
+ githubInstallationRepository,
721
+ },
722
+ // App-owned backend registries, surfaced so the workspace snapshot's backend-kind
723
+ // selectors (`environmentBackendKinds` / `runnerBackendKinds`) read the registered kinds.
724
+ environmentBackendRegistry,
725
+ runnerBackendRegistry,
726
+ // The consensus transcript store, for the read endpoint (window load / reload).
727
+ consensusSessionRepository: repos.consensusSessionRepository,
728
+ // Resolves the per-account binary-artifact store (screenshots) for the artifact
729
+ // controllers + the visual-confirmation gate (configured per-account in the UI).
730
+ resolveBinaryArtifactStore,
731
+ // Stock/remote Node has NO built-in container runtime, so container agents run ONLY on a
732
+ // self-hosted runner pool — an unregistered pool means no agent can run, which the infra-setup
733
+ // banner should surface. Local mode injects its own per-run-host-container `resolveTransport`
734
+ // (so the pool is optional there); detect that by the absence of the default pool transport.
735
+ agentExecutorRequiresRunnerPool: options.resolveTransport === undefined,
736
+ // A missing ephemeral-environment provider is a real setup gap ONLY when no zero-config
737
+ // in-container test-env default exists. Stock Node's sole test-env backend is the
738
+ // `environment-provider`, so it's required here; local mode on a Docker-family runtime
739
+ // advertises `local-compose` (docker-compose in the run's container, no connection), which
740
+ // flips this false so the "test environment not configured" banner stays quiet. Derived from
741
+ // the capability descriptor local already populated, so the two can't drift.
742
+ ephemeralEnvironmentsRequireProvider: !testEnvHasZeroConfigDefault(config.infrastructure),
743
+ // pg-boss-backed async GitHub ingest when the durable engine is wired (the real
744
+ // server drains the queue via `startGitHubSyncWorker`); inline fallback with no boss.
745
+ // Built once above so the skill-freshness fan-out shares this same instance.
746
+ gateways,
747
+ // Source-control PAT login: lets a user sign in with their own GitHub/GitLab PAT via
748
+ // `/auth/pat`, held to the server's login/org/domain allowlist. Local mode overrides this
749
+ // (via its container spread) with a configured-token, allowlist-exempt registry.
750
+ vcsIdentity: buildNodeVcsIdentityRegistry(config),
751
+ // The app-owned VCS provider registry the neutral webhook route resolves a provider from.
752
+ vcsRegistry,
753
+ // The sensitive per-service test-credential store the shared test-secrets controller reads;
754
+ // present when the shared ENCRYPTION_KEY is configured.
755
+ ...(testSecretsService ? { testSecrets: testSecretsService } : {}),
756
+ // The vendor-credential (subscription token pool) service the shared controller
757
+ // reads; present when the shared ENCRYPTION_KEY is configured.
758
+ subscriptions,
759
+ // The per-user individual-usage subscription store (Claude); present when the
760
+ // shared ENCRYPTION_KEY is configured.
761
+ personalSubscriptions,
762
+ // The direct-provider API-key pool (account/workspace/user); present when the
763
+ // shared ENCRYPTION_KEY is configured.
764
+ apiKeys,
765
+ // The inbound public-API key store; present when the shared ENCRYPTION_KEY is configured.
766
+ publicApiKeys,
767
+ // Whether the opt-in Cloudflare Workers AI lib is enabled (REST creds present).
768
+ cloudflareModelsEnabled,
769
+ // The direct-provider base-URL resolver the catalog uses to gate selectability on a
770
+ // resolvable endpoint (e.g. LiteLLM stays unselectable until LITELLM_BASE_URL is set).
771
+ baseUrlFor: (provider) => baseUrlForNode(provider, env),
772
+ // The per-user locally-run model endpoints store; present when ENCRYPTION_KEY is set.
773
+ localModelEndpoints,
774
+ // The per-user generic secret store (GitHub PAT, …); present when ENCRYPTION_KEY is set.
775
+ userSecrets,
776
+ // The per-user "repos my PAT can reach" projection (board redaction + picker expansion);
777
+ // Postgres-backed, so absent in the no-DB mothership node (redaction degrades to visible).
778
+ userRepoAccess: db ? new DrizzleUserRepoAccessRepository(db) : undefined,
779
+ // The sealed-secret inventory the key-drift sweep + drop remediation use (ADR 0026 D6.2/D6.3);
780
+ // Postgres-backed and gated on ENCRYPTION_KEY (no key ⇒ nothing is sealed to scan).
781
+ ...(db && env.ENCRYPTION_KEY?.trim()
782
+ ? { sealedSecretInventory: new DrizzleSealedSecretInventory(db) }
783
+ : {}),
784
+ // The per-workspace OpenRouter dynamic-catalog store; present when the API-key pool is.
785
+ openRouterCatalog,
786
+ // Flush + release the external trace sink on graceful shutdown so the OpenTelemetry SDK
787
+ // exporter's final batch of spans/metrics isn't dropped and its background timers are
788
+ // cleared. Best-effort; a no-op for the fetch-based Langfuse sink and when nothing is
789
+ // wired. (The local facade composes this into its own `onShutdown` — see its container.)
790
+ onShutdown: async () => {
791
+ await traceSink?.shutdown?.();
792
+ },
793
+ };
794
+ }
343
795
  export function buildNodeContainer(options) {
344
796
  const env = options.env ?? process.env;
345
797
  const config = options.config ?? loadNodeConfig(env);
@@ -602,320 +1054,79 @@ export function buildNodeContainer(options) {
602
1054
  env,
603
1055
  config,
604
1056
  db,
605
- repos,
606
- idGenerator,
607
- clock,
608
- providerRegistry,
609
- packageRegistrySecretCipher,
610
- contentStorageDefaultBackend: options.contentStorageDefaultBackend,
611
- caches: options.caches,
612
- });
613
- // Runner-pool URL/host guard, scoped to its own config (independent of the environment
614
- // allow-list); absent => strict public-https.
615
- const runnerUrlPolicy = resolveUrlSafetyPolicy(config.runners);
616
- // Apply any test-injected gate providers LAST, so they override the config wiring above (the
617
- // cross-runtime conformance suite drives the externalized CI gate over a faked verdict; in
618
- // local mode a PAT-backed CI provider is wired above and would otherwise win). Production
619
- // leaves `gateProviders` undefined, so this is a no-op outside tests.
620
- applyGateProviders(providerRegistry, options.gateProviders);
621
- // Surface any gate left as a silent pass-through (no provider wired) so a misconfigured
622
- // deployment is visible in the logs instead of quietly auto-merging without checking CI.
623
- warnUnwiredGates(providerRegistry, logger);
624
- // pg-boss-backed async GitHub ingest (webhook/resync/backfill) when the durable engine is
625
- // wired; inline fallback with no boss. Built once so the engine's skill-freshness fan-out
626
- // (slice 4) enqueues through the SAME `githubWebhook` seam rather than re-deriving the queue.
627
- const gateways = createNodeGateways(env, options.boss);
628
- const dependencies = {
629
- ...releaseHealthDeps,
630
- ...incidentEnrichmentDeps,
631
- ...packageRegistryDeps,
632
- // Fold the service frame's SENSITIVE test-credential refs (key + description, never values)
633
- // into the tester prompt. Present when ENCRYPTION_KEY is set; absent ⇒ no advertised secrets.
634
- ...(resolveTestSecretRefs ? { resolveTestSecretRefs } : {}),
635
- // App-owned backend registries (kind → provider) the connection services resolve through.
636
- environmentBackendRegistry,
637
- runnerBackendRegistry,
638
- // The app-owned agent-kind registry (built-ins + any deployment-registered kinds); the
639
- // engine reads it (traits / inline-surface / pre-post-op hooks) and re-exposes it on Core.
640
- agentKindRegistry,
641
- // The app-owned gate + step-resolver registries; the engine's gate machine + completion hub
642
- // read them, and the gate registry is re-exposed on Core for the boot-time validation.
643
- gateRegistry,
644
- stepResolverRegistry,
645
- // The app-owned provider registry the gate providers were wired onto above; the engine's gate
646
- // machine reads the SAME instance through its GateContext.
647
- providerRegistry,
648
- // The app-owned pipeline registry (deployment-registered extra pipelines); createCore threads
649
- // it into the workspace + pipeline services and re-exposes it on Core for boot-time validation.
650
- pipelineRegistry: options.pipelineRegistry,
651
- // The app-owned initiative-preset registry; the initiative services read it and it is
652
- // re-exposed on Core for the snapshot descriptors + preset probe.
653
- initiativePresetRegistry,
654
- // The code-defined custom provision-type catalog, merged with the workspace rows by
655
- // `listCustomTypes` so a programmatically-registered type surfaces in the infra editor + the
656
- // per-service provisioning picker.
657
- customManifestTypeRegistry,
658
- ...(accountSettings ? { accountSettings } : {}),
659
- // Resolves the per-account binary-artifact store (screenshots) for the visual-confirmation
660
- // gate; resolving to null (no storage configured) ⇒ the gate passes through.
661
- resolveBinaryArtifactStore,
662
- workspaceRepository: repos.workspaceRepository,
663
- workspaceMemberRepository: repos.workspaceMemberRepository,
664
- accountRepository: repos.accountRepository,
665
- membershipRepository: repos.membershipRepository,
666
- userRepository: repos.userRepository,
667
- passwordHasher: new WebCryptoPasswordHasher(),
668
- blockRepository: repos.blockRepository,
669
- pipelineRepository: repos.pipelineRepository,
670
- executionRepository: repos.executionRepository,
671
- // Clear a finished run's personal-credential activation promptly (TTL sweep is the backstop).
672
- // In mothership mode its home is the LOCAL `node:sqlite` credential bucket (the activation
673
- // re-seals the token for the run, and the LOCAL container executor decrypts it), injected via
674
- // `options.subscriptionActivationRepository` — the SAME instance the personal-subscription
675
- // service above mints into, so mint + clear agree. Absent (plain Node / siloed-Postgres local)
676
- // → the Drizzle repo over `db`. This is NEVER routed through `sourced` (the remote registry):
677
- // every no-db (mothership) caller injects the override — `buildLocalContainer` in production
678
- // and `makeMothershipConformanceApp` in tests — so `db` here is always a real Postgres handle,
679
- // and routing an activation clear to the mothership (where `deleteByExecution` isn't
680
- // allow-listed) is a path no caller takes.
681
- subscriptionActivationRepository: options.subscriptionActivationRepository ?? new DrizzleSubscriptionActivationRepository(db),
682
- // In-org shared services. When a realtime hub is wired (start()), the engine's
683
- // event publisher (composed above) is a `FanOutEventPublisher` over these two repos,
684
- // so a shared service's live events reach every board that mounts it — parity with
685
- // the Cloudflare facade. Without a hub (createServer/tests) the engine uses its
686
- // NoopEventPublisher and nothing is pushed.
687
- serviceRepository: repos.serviceRepository,
688
- workspaceMountRepository: repos.workspaceMountRepository,
689
- tokenUsageRepository: repos.tokenUsageRepository,
690
- llmCallMetricRepository: repos.llmCallMetricRepository,
691
- // Deployment-level rollups over `agent_runs` for the operator dashboard.
692
- platformMetricsRepository: repos.platformMetricsRepository,
693
- // Unified provisioning event log (its own Postgres schema). Threads the recorder
694
- // into the env services and exposes the read service for the logs controller.
695
- provisioningLogRepository: repos.provisioningLogRepository,
696
- recordLlmPrompts: config.observability.recordPrompts,
697
- // Re-exposed on the core for the agent-context read endpoint; the same instance
698
- // is injected into the container executor above for the write path.
699
- agentContextObservability,
700
- // Re-exposed on the core for the search-query read endpoint AND the search proxy's
701
- // write path (it reads it off the request container).
702
- searchQueryObservability,
703
- // Opt-in external trace sink(s) — Langfuse and/or OpenTelemetry — fanning every
704
- // recorded LLM call out as a generation. Built only when configured; otherwise
705
- // undefined and there is no external emission.
706
- llmTraceSink: buildTraceSink(config),
707
- modelPresetRepository: repos.modelPresetRepository,
708
- // A fresh workspace's model-preset library is seeded with this built-in as the default
709
- // (Node deploy → Kimi K2.7, the Cloudflare-runnable baseline; the local facade injects
710
- // Claude). Applied only at first seed, so a user's later manual default choice wins.
711
- defaultModelPresetId: options.defaultModelPresetId ?? DEFAULT_MODEL_PRESET_ID,
712
- serviceFragmentDefaultsRepository: repos.serviceFragmentDefaultsRepository,
713
- // Requirements-review feature (stateless reviewer + the requirements-rework
714
- // step). Wired identically to the Cloudflare facade's `selectRequirementsDeps`
715
- // so both runtimes serve the review/rework API AND substitute a block's reworked
716
- // requirements into the agent context (the cross-runtime conformance suite asserts
717
- // the substitution against both stores). The reviewer's model resolves exactly
718
- // like a pipeline step: block-pin > workspace per-kind default > routing default
719
- // (which falls back to Cloudflare Workers AI unless a direct key is set).
720
- requirementReviewRepository: repos.requirementReviewRepository,
721
- // Interactive document-interview sessions (WS5). Wired unconditionally; the interviewer
722
- // reuses the requirements reviewer's model config resolved just below.
723
- docInterviewRepository: repos.docInterviewRepository,
724
- // Kaizen agent (post-run grading). Wired unconditionally, mirroring the Cloudflare
725
- // facade, so the engine schedules gradings at run completion and the background sweep
726
- // runs them. The grader resolves its model for the `kaizen` kind exactly like a step.
727
- kaizenGradingRepository: repos.kaizenGradingRepository,
728
- kaizenVerifiedComboRepository: repos.kaizenVerifiedComboRepository,
729
- clarityReviewRepository: repos.clarityReviewRepository,
730
- brainstormSessionRepository: repos.brainstormSessionRepository,
731
- // Initiatives (the long-running multi-task work container). Wired unconditionally,
732
- // mirroring the Worker's `selectMergeLifecycleDeps`, so the create/read API + the
733
- // planning pipeline's ingest/committer steps work identically on both runtimes.
734
- initiativeRepository: repos.initiativeRepository,
735
- // Merge threshold presets: the per-workspace auto-merge ceiling library a task's
736
- // merge gate resolves (block-pinned preset > workspace default). Wired
737
- // unconditionally, exactly like the Worker's `selectMergeLifecycleDeps`, so the
738
- // preset CRUD API + the merger step's threshold resolution work identically.
739
- riskPolicyRepository: repos.riskPolicyRepository,
740
- // Shared stacks (long-lived compose infra a consumer environment attaches to). Wired
741
- // unconditionally like the merge presets so the CRUD API works identically on both
742
- // runtimes; the bring-up (`ensureUp`) needs a host daemon, so plain Node has no
743
- // `composeRuntime` — the local facade injects one via `overrides.composeRuntime`.
744
- sharedStackRepository: repos.sharedStackRepository,
745
- // Sandbox (parallel prompt/model testing) — contributed as one sandbox-owned mixin,
746
- // symmetric with the Worker's `...selectSandboxDeps(db)`; the run-driver reuses the
747
- // reviewer model config below. The container body never enumerates the five repos.
748
- ...createDrizzleSandboxDeps(db),
749
- // Per-workspace runtime settings (human-wait escalation threshold + per-service task
750
- // limit). Wired unconditionally so the settings API + the limit enforcement + the
751
- // escalation sweep work identically to the Worker.
752
- workspaceSettingsRepository: repos.workspaceSettingsRepository,
753
- userSettingsRepository: repos.userSettingsRepository,
754
- modelProviderResolver,
755
- requirementReviewModel: config.agents.routing.default.ref,
756
- requirementReviewResolveModel: config.agents.resolveBlockModel,
757
- // Local mode runs the inline reviewers/brainstorm/estimator on the ambient Claude Code /
758
- // Codex CLI when the pinned model is a subscription harness (undefined on stock Node, so
759
- // such refs degrade to the routing default). Also drives the preset satisfiability guard.
760
- ...(config.agents.inlineHarnessRef ? { inlineHarnessRef: config.agents.inlineHarnessRef } : {}),
761
- // Notifications subsystem (parity with the Worker, which wires it unconditionally):
762
- // the inbox + the human-action surfaces. Node has no real-time push, so the rows
763
- // persist (inbox + snapshot) and any channel composed below — e.g. Slack — delivers.
764
- notificationRepository: sourced('notificationRepository', (d) => new DrizzleNotificationRepository(d)),
765
- ...tasks.deps,
766
- // Recurring pipelines + the workspace tracker selection. The tracker provider
767
- // files the tech-debt pipeline's issue by resolving the *workspace's* connected
768
- // integration: GitHub issues through the workspace's GitHub App installation,
769
- // Jira tickets from the per-workspace encrypted connection store — both per-tenant.
770
- pipelineScheduleRepository: repos.pipelineScheduleRepository,
771
- trackerSettingsRepository: repos.trackerSettingsRepository,
772
- ticketTrackerProvider: new TicketTrackerService({
773
- trackerSettingsRepository: repos.trackerSettingsRepository,
774
- fetchImpl: fetch,
775
- ...(fileGitHubIssue ? { fileGitHubIssue } : {}),
776
- ...(tasks.taskConnectionRepository
777
- ? {
778
- resolveJiraConnection: async (workspaceId) => {
779
- const connection = await tasks.taskConnectionRepository.getByWorkspace(workspaceId, 'jira');
780
- const { baseUrl, accountEmail, apiToken } = connection?.credentials ?? {};
781
- if (!baseUrl || !accountEmail || !apiToken)
782
- return null;
783
- return { baseUrl, accountEmail, apiToken };
784
- },
785
- resolveLinearConnection: async (workspaceId) => {
786
- const connection = await tasks.taskConnectionRepository.getByWorkspace(workspaceId, 'linear');
787
- const { apiKey, token } = connection?.credentials ?? {};
788
- return apiKey || token ? { apiKey, token } : null;
789
- },
790
- }
791
- : {}),
792
- }),
793
- issueWritebackProvider,
794
- idGenerator,
795
- clock,
796
- agentExecutor,
797
- spendPricing: config.spend,
798
- // Price metered dynamic OpenRouter models at their real per-model rate (not the
799
- // bare-`openrouter` fallback) using this workspace's enabled catalog.
800
- dynamicModelPricesFor: openRouterCatalog
801
- ? (ws) => openRouterCatalog.capabilitiesFor(ws)
802
- : undefined,
803
- // The runner-pool integration assembles when enabled, so a workspace can
804
- // register the self-hosted pool its container agents dispatch to.
805
- ...(config.runners.enabled && config.runners.encryptionKey
806
- ? {
807
- runnerPoolConnectionRepository,
808
- runnerSecretCipher: new WebCryptoSecretCipher({
809
- masterKeyBase64: config.runners.encryptionKey,
810
- info: RUNNERS_CIPHER_INFO,
811
- }),
812
- // The pool provider instance backs the connection service's describeProvider +
813
- // testConnection (the manifest editor's secret-key form + a pre-save probe). An
814
- // injected native adapter wins here too (same instance that drives dispatch), so
815
- // its describeConfig/testConnection render — else the generic manifest provider
816
- // (same SSRF policy as the dispatch transport).
817
- runnerPoolProvider: options.runnerPoolProvider ??
818
- new HttpRunnerPoolProvider(runnerUrlPolicy ? { urlPolicy: runnerUrlPolicy } : {}),
819
- // Node (and local) has undici, so it can verify a private CA / skip TLS for a
820
- // Kubernetes apiserver — accept such a config at registration.
821
- runnerCustomTlsSupported: true,
822
- ...(runnerUrlPolicy ? { runnerUrlSafetyPolicy: runnerUrlPolicy } : {}),
823
- }
824
- : {}),
825
- ...(options.boss
826
- ? {
827
- workRunner: new PgBossWorkRunner(options.boss, executionRuntime(config, env).queue),
828
- // The durable bootstrap driver (analogue of the Worker's BootstrapWorkflow):
829
- // BootstrapService.startRun enqueues a drive job that polls the run to terminal.
830
- bootstrapRunner: new PgBossBootstrapRunner(options.boss, executionRuntime(config, env).queue),
831
- // The durable env-config-repair driver (analogue of the Worker's
832
- // EnvConfigRepairWorkflow): start enqueues a drive job that polls the run to terminal.
833
- envConfigRepairRunner: new PgBossEnvConfigRepairRunner(options.boss, executionRuntime(config, env).queue),
834
- // The durable ephemeral-environment self-test driver (analogue of the Worker's
835
- // EnvironmentTestWorkflow): startRun enqueues a drive job that advances the run.
836
- environmentTestRunner: new PgBossEnvironmentTestRunner(options.boss, executionRuntime(config, env).queue),
837
- }
838
- : {}),
839
- ...githubGateDeps,
840
- // GitHub installation + repo/branch/PR/issue/commit/check-run projections + the
841
- // sync/webhook module (inline ingest persists to these repos on Node).
842
- ...githubModuleDeps,
843
- // Repo-bootstrap: the reference-architecture library + bootstrap-run store make the
844
- // module + API available; `repoBootstrapper` (when wired) dispatches the bootstrap
845
- // container through the shared runner seam, and `bootstrapRunner` (pg-boss, below)
846
- // durably drives its poll loop — parity with the Worker's BootstrapWorkflow.
847
- referenceArchitectureRepository: sourced('referenceArchitectureRepository', (d) => new DrizzleReferenceArchitectureRepository(d)),
1057
+ repos,
1058
+ idGenerator,
1059
+ clock,
1060
+ providerRegistry,
1061
+ packageRegistrySecretCipher,
1062
+ contentStorageDefaultBackend: options.contentStorageDefaultBackend,
1063
+ caches: options.caches,
1064
+ });
1065
+ // Runner-pool URL/host guard, scoped to its own config (independent of the environment
1066
+ // allow-list); absent => strict public-https.
1067
+ const runnerUrlPolicy = resolveUrlSafetyPolicy(config.runners);
1068
+ // Apply any test-injected gate providers LAST, so they override the config wiring above (the
1069
+ // cross-runtime conformance suite drives the externalized CI gate over a faked verdict; in
1070
+ // local mode a PAT-backed CI provider is wired above and would otherwise win). Production
1071
+ // leaves `gateProviders` undefined, so this is a no-op outside tests.
1072
+ applyGateProviders(providerRegistry, options.gateProviders);
1073
+ // Surface any gate left as a silent pass-through (no provider wired) so a misconfigured
1074
+ // deployment is visible in the logs instead of quietly auto-merging without checking CI.
1075
+ warnUnwiredGates(providerRegistry, logger);
1076
+ // pg-boss-backed async GitHub ingest (webhook/resync/backfill) when the durable engine is
1077
+ // wired; inline fallback with no boss. Built once so the engine's skill-freshness fan-out
1078
+ // (slice 4) enqueues through the SAME `githubWebhook` seam rather than re-deriving the queue.
1079
+ const gateways = createNodeGateways(env, options.boss);
1080
+ const dependencies = assembleNodeCoreDependencies({
1081
+ config,
1082
+ options,
1083
+ env,
1084
+ db,
1085
+ repos,
1086
+ sourced,
1087
+ idGenerator,
1088
+ clock,
1089
+ gateways,
1090
+ runnerUrlPolicy,
1091
+ githubInstallationRepository,
1092
+ environmentBackendRegistry,
1093
+ runnerBackendRegistry,
1094
+ customManifestTypeRegistry,
1095
+ agentKindRegistry,
1096
+ gateRegistry,
1097
+ stepResolverRegistry,
1098
+ initiativePresetRegistry,
1099
+ providerRegistry,
1100
+ apiKeys,
1101
+ subscriptions,
1102
+ personalSubscriptions,
1103
+ localModelEndpoints,
1104
+ openRouterCatalog,
1105
+ modelProviderResolver,
1106
+ cloudflareModelsEnabled,
1107
+ deployDeps,
1108
+ runnerPoolConnectionRepository,
1109
+ agentContextObservability,
1110
+ searchQueryObservability,
1111
+ resolveTestSecretRefs,
1112
+ githubClient,
1113
+ tasks,
1114
+ fileGitHubIssue,
1115
+ issueWritebackProvider,
1116
+ githubGateDeps,
1117
+ githubModuleDeps,
848
1118
  bootstrapJobRepository,
849
- ...(repoBootstrapper ? { repoBootstrapper } : {}),
850
- // Env-config-repair runs share the unified agent_runs table (kind-scoped). The job
851
- // repository is wired unconditionally; the repairer (agent fallback) is wired
852
- // post-overrides below over the FINAL provider, and the durable runner in the
853
- // `options.boss` block above — parity with the Worker's EnvConfigRepairWorkflow.
854
- envConfigRepairJobRepository: sourced('envConfigRepairJobRepository', (d) => new DrizzleEnvConfigRepairJobRepository(d)),
855
- // Ephemeral-environment self-test runs (their own table). The store is wired
856
- // unconditionally; the environments module builds the service when it + a git provider
857
- // are present, and the durable runner is wired in the `options.boss` block above.
858
- environmentTestRunRepository: sourced('environmentTestRunRepository', (d) => new DrizzleEnvironmentTestRunRepository(d)),
859
- // Document sources (Confluence / Notion / GitHub docs): wired from the shared
860
- // integration providers exactly like the Worker, so a workspace can connect a
861
- // source and import requirement/PRD/RFC pages as agent context.
862
- ...selectNodeDocumentsDeps(config, db, githubClient, githubInstallationRepository),
863
- // Ephemeral environments (opt-in): a workspace registers its own environment
864
- // management API; the tester provisions/destroys per-run environments from it. A
865
- // trusted in-house adapter can replace the default HTTP provider via the seam.
866
- // The environment integration scopes its own URL/host policy from
867
- // `config.environments` inside this selector (separate from the runner pool's).
868
- ...selectNodeEnvironmentsDeps(config, db),
869
- // The async container-backed Kubernetes deploy lifecycle (deployJobClient +
870
- // resolveDeployCloneTarget) — pool-backed by default, overridable by the local facade.
871
- ...deployDeps,
872
- // Prompt-fragment library (ADR 0006; opt-in): the managed tenant-scoped catalog
873
- // of best-practice fragments feeding every agent run, wired exactly like the
874
- // Worker's selectFragmentLibraryDeps (repos + installation resolver + selector).
875
- ...selectNodeFragmentLibraryDeps(config, env, db, githubClient, githubInstallationRepository, modelProviderResolver),
876
- // Repo-sourced Claude Skills library (docs/initiatives/repo-skills.md; opt-in): the
877
- // account's catalog of repo-authored skills, wired exactly like the Worker's
878
- // selectSkillLibraryDeps (account repos + installation resolver).
879
- ...selectNodeSkillLibraryDeps(config, db, githubClient, githubInstallationRepository),
880
- // Push-webhook skill-source freshness fan-out (slice 4): resync affected sources via the
881
- // pg-boss GitHub-sync queue. No boss (pure-logic test) ⇒ no proactive resync; the
882
- // dispatch-time probe is the freshness backstop.
883
- enqueueSkillResync: async ({ accountId, sourceId }) => {
884
- await gateways.githubWebhook.queueSkillResync(accountId, sourceId);
885
- },
886
- // Slack: an extra notification transport (the channel) + its management module.
887
- // Default-off; when enabled its channel is composed into `notificationChannel` below
888
- // alongside the in-app push, identically to the Worker.
889
- ...slackDeps,
890
- // Account invitations + per-account email senders (UI-onboarded, DB-stored).
891
- ...selectNodeEmailInvitationDeps(config, repos),
892
- // The pipeline-start guard resolves what's configured for a workspace + initiator.
893
- resolveProviderCapabilities: (workspaceId, initiatedBy) => resolveWorkspaceCapabilities({
894
- apiKeys,
895
- subscriptions,
896
- personalSubscriptions,
897
- cloudflareModelsEnabled,
898
- baseUrlFor: (provider) => baseUrlForNode(provider, env),
899
- localModelEndpoints,
900
- openRouterCatalog,
901
- accountSettings,
902
- workspaceAccountOf: (workspaceId) => repos.workspaceRepository.accountOf(workspaceId),
903
- modelPolicySupported: config.infrastructure?.modelPolicy?.supported ?? false,
904
- ...(options.caches ? { caches: options.caches } : {}),
905
- }, workspaceId, initiatedBy),
906
- // Real-time push (when a hub is wired) + the composed notification channel (in-app
907
- // push + Slack). These come AFTER the spreads so the composite replaces the bare
908
- // Slack channel `slackDeps` set; both are absent (no override) when nothing is wired.
909
- ...(executionEventPublisher ? { executionEventPublisher } : {}),
910
- ...(notificationChannel ? { notificationChannel } : {}),
911
- // Run the engine's gate-probe / merge GitHub reads under the run initiator's ambient
912
- // context, so a per-user PAT (when set) is preferred over the App/env token.
913
- runInitiatorScope: runWithInitiator,
914
- // The process-wide cache bag from start() (Redis-notified invalidation when REDIS_URL
915
- // is set). Absent ⇒ createCore builds bare in-memory defaults.
916
- ...(options.caches ? { caches: options.caches } : {}),
917
- ...options.overrides,
918
- };
1119
+ repoBootstrapper,
1120
+ slackDeps,
1121
+ executionEventPublisher,
1122
+ agentExecutor,
1123
+ notificationChannel,
1124
+ releaseHealthDeps,
1125
+ packageRegistryDeps,
1126
+ incidentEnrichmentDeps,
1127
+ accountSettings,
1128
+ resolveBinaryArtifactStore,
1129
+ });
919
1130
  // Browsable frontend preview (slice 5c): wire the preview module when a per-runtime preview
920
1131
  // transport is available (real in local mode / a fake pair in the conformance suite).
921
1132
  wirePreviewModule(dependencies, options, {
@@ -947,145 +1158,34 @@ export function buildNodeContainer(options) {
947
1158
  // Mothership mode (`db` undefined): re-source the run-path org/durable repos the sub-helpers
948
1159
  // built directly over the absent `db` from the remote registry (a no-op outside mothership mode).
949
1160
  applyMothershipRemoteRepos(dependencies, remoteRepos);
950
- return {
951
- ...createCore(dependencies),
1161
+ return projectNodeServerContainer({
1162
+ dependencies,
952
1163
  config,
953
- // The deployment-wide trusted web-search upstream (built from `WEB_SEARCH_*` env above),
954
- // read by `WebSearchProxyController` as the fallback when a run's account has no keys.
955
- ...(defaultWebSearchUpstream ? { defaultWebSearchUpstream } : {}),
956
- // The same checkout-free repo resolver the engine binds pre/post-ops with, surfaced so
957
- // the shared service-spec read controller can read the `spec/` artifact off main.
958
- resolveRunRepoContext: dependencies.resolveRunRepoContext,
959
- // The block→service→repo resolver, surfaced so the task-search controller can scope a
960
- // GitHub-issue search to the originating service's repo (and refuse it when unlinked).
1164
+ defaultWebSearchUpstream,
961
1165
  resolveRepoTarget,
962
- agentRunRepository: repos.agentRunRepository,
963
- // Execution-scoped repo, surfaced for the conformance suite's compareAndSwap parity check.
964
- executionRepository: repos.executionRepository,
965
- // The repository registry the mothership-mode machine API (`/internal/persistence`) reflects
966
- // over, so a Node deployment can act as a mothership for mothership-mode local nodes. The
967
- // controller gates which repo+method is callable (allow-list) and account-scopes each call;
968
- // exposing the whole `dependencies` (which carries every repo under its canonical name) is
969
- // safe. `agentRunRepository` is the one repo NOT part of `CoreDependencies` (the engine's
970
- // Core never reads it — it's surfaced separately above for `AgentRunController`), so fold it
971
- // in explicitly, else the board's retry/stop `getRef` call comes back `... is not wired`.
972
- // Sourced identically on both facades so they attach the same registry surface.
973
- // Mothership-side GitHub token delegation (`POST /internal/github/installation-token`):
974
- // when this deployment's GitHub App is configured, a machine-authed mothership-mode node
975
- // can mint the short-lived installation tokens its agent containers/gates need — the App
976
- // private key never leaves this service. The registry satisfies the seam structurally.
977
- // Wired symmetrically on the Cloudflare facade.
978
- ...(appRegistry ? { githubTokenDelegation: appRegistry } : {}),
979
- // Mothership-side real-time UPSTREAM delivery (`POST /internal/events/publish`): when this
980
- // deployment is a mothership (its realtime transport is wired), a machine-authed mothership-mode
981
- // node's relayed engine events land in this deployment's OWN fan-out (`options.realtimeSink` —
982
- // the hub, or the layered propagator on a multi-node deployment), so hosted teammates on the
983
- // shared board see the local node's activity live. Wired symmetrically on the Cloudflare facade
984
- // (the per-workspace WorkspaceEventsHub Durable Object). Absent realtime ⇒ the endpoint 503s.
985
- ...(options.realtimeSink
986
- ? { machineEventRelay: new LocalMachineEventRelay(options.realtimeSink) }
987
- : {}),
988
- repositories: {
989
- ...dependencies,
990
- agentRunRepository: repos.agentRunRepository,
991
- // The binary-artifact METADATA store (visual-confirmation gate screenshots/references) is
992
- // not part of `CoreDependencies` (it's composed into `resolveBinaryArtifactStore`, not the
993
- // engine's Core), so fold it into the reflected registry explicitly — else a mothership-mode
994
- // node's artifact reads/writes come back `... is not wired`. The blob BYTES stay per-account
995
- // local; only the metadata is proxied.
996
- binaryArtifactMetadataStore: repos.binaryArtifactMetadataStore,
997
- // The sensitive per-service test-credential store is org/durable state the engine reads via
998
- // the `resolveTestSecretRefs` FUNCTION (never the repo directly), so it isn't in
999
- // `CoreDependencies` either — fold it in explicitly, else a mothership-mode node's tester
1000
- // run-path read + the inspector CRUD come back `... is not wired`. Only the SEALED blob is
1001
- // proxied (decrypted service-side under the LOCAL key), like the observability/runner-pool
1002
- // connections.
1003
- testSecretsRepository: repos.testSecretsRepository,
1004
- // GitHub projection + installation reads the mothership serves over the persistence RPC even
1005
- // when its OWN github service is off. A mothership-mode local node reaches GitHub by token
1006
- // DELEGATION (no local App), which enables `container.github`, so its board snapshot
1007
- // (`github.service.listRepos` → `repoProjectionRepository.list`) and run-path repo resolution
1008
- // (`githubInstallationRepository.getByWorkspace` + `repoProjectionRepository.list`) read the
1009
- // projection over RPC. Both are plain org tables the mothership owns, constructed
1010
- // unconditionally above — so reflect them regardless of `config.github.enabled` (they land in
1011
- // `dependencies` only when the github MODULE is wired), else a mothership without its own App
1012
- // configured 500s that board load with `... is not wired`. Allow-listed in
1013
- // `REMOTE_PERSISTENCE_METHODS`; folded in explicitly like the stores above.
1014
- repoProjectionRepository,
1015
- githubInstallationRepository,
1016
- },
1017
- // App-owned backend registries, surfaced so the workspace snapshot's backend-kind
1018
- // selectors (`environmentBackendKinds` / `runnerBackendKinds`) read the registered kinds.
1166
+ repos,
1167
+ appRegistry,
1168
+ options,
1169
+ repoProjectionRepository,
1170
+ githubInstallationRepository,
1019
1171
  environmentBackendRegistry,
1020
1172
  runnerBackendRegistry,
1021
- // The consensus transcript store, for the read endpoint (window load / reload).
1022
- consensusSessionRepository: repos.consensusSessionRepository,
1023
- // Resolves the per-account binary-artifact store (screenshots) for the artifact
1024
- // controllers + the visual-confirmation gate (configured per-account in the UI).
1025
1173
  resolveBinaryArtifactStore,
1026
- // Stock/remote Node has NO built-in container runtime, so container agents run ONLY on a
1027
- // self-hosted runner pool — an unregistered pool means no agent can run, which the infra-setup
1028
- // banner should surface. Local mode injects its own per-run-host-container `resolveTransport`
1029
- // (so the pool is optional there); detect that by the absence of the default pool transport.
1030
- agentExecutorRequiresRunnerPool: options.resolveTransport === undefined,
1031
- // A missing ephemeral-environment provider is a real setup gap ONLY when no zero-config
1032
- // in-container test-env default exists. Stock Node's sole test-env backend is the
1033
- // `environment-provider`, so it's required here; local mode on a Docker-family runtime
1034
- // advertises `local-compose` (docker-compose in the run's container, no connection), which
1035
- // flips this false so the "test environment not configured" banner stays quiet. Derived from
1036
- // the capability descriptor local already populated, so the two can't drift.
1037
- ephemeralEnvironmentsRequireProvider: !testEnvHasZeroConfigDefault(config.infrastructure),
1038
- // pg-boss-backed async GitHub ingest when the durable engine is wired (the real
1039
- // server drains the queue via `startGitHubSyncWorker`); inline fallback with no boss.
1040
- // Built once above so the skill-freshness fan-out shares this same instance.
1041
1174
  gateways,
1042
- // Source-control PAT login: lets a user sign in with their own GitHub/GitLab PAT via
1043
- // `/auth/pat`, held to the server's login/org/domain allowlist. Local mode overrides this
1044
- // (via its container spread) with a configured-token, allowlist-exempt registry.
1045
- vcsIdentity: buildNodeVcsIdentityRegistry(config),
1046
- // The app-owned VCS provider registry the neutral webhook route resolves a provider from.
1047
1175
  vcsRegistry,
1048
- // The sensitive per-service test-credential store the shared test-secrets controller reads;
1049
- // present when the shared ENCRYPTION_KEY is configured.
1050
- ...(testSecretsService ? { testSecrets: testSecretsService } : {}),
1051
- // The vendor-credential (subscription token pool) service the shared controller
1052
- // reads; present when the shared ENCRYPTION_KEY is configured.
1176
+ testSecretsService,
1053
1177
  subscriptions,
1054
- // The per-user individual-usage subscription store (Claude); present when the
1055
- // shared ENCRYPTION_KEY is configured.
1056
1178
  personalSubscriptions,
1057
- // The direct-provider API-key pool (account/workspace/user); present when the
1058
- // shared ENCRYPTION_KEY is configured.
1059
1179
  apiKeys,
1060
- // The inbound public-API key store; present when the shared ENCRYPTION_KEY is configured.
1061
1180
  publicApiKeys,
1062
- // Whether the opt-in Cloudflare Workers AI lib is enabled (REST creds present).
1063
1181
  cloudflareModelsEnabled,
1064
- // The direct-provider base-URL resolver the catalog uses to gate selectability on a
1065
- // resolvable endpoint (e.g. LiteLLM stays unselectable until LITELLM_BASE_URL is set).
1066
- baseUrlFor: (provider) => baseUrlForNode(provider, env),
1067
- // The per-user locally-run model endpoints store; present when ENCRYPTION_KEY is set.
1182
+ env,
1068
1183
  localModelEndpoints,
1069
- // The per-user generic secret store (GitHub PAT, …); present when ENCRYPTION_KEY is set.
1070
1184
  userSecrets,
1071
- // The per-user "repos my PAT can reach" projection (board redaction + picker expansion);
1072
- // Postgres-backed, so absent in the no-DB mothership node (redaction degrades to visible).
1073
- userRepoAccess: db ? new DrizzleUserRepoAccessRepository(db) : undefined,
1074
- // The sealed-secret inventory the key-drift sweep + drop remediation use (ADR 0026 D6.2/D6.3);
1075
- // Postgres-backed and gated on ENCRYPTION_KEY (no key ⇒ nothing is sealed to scan).
1076
- ...(db && env.ENCRYPTION_KEY?.trim()
1077
- ? { sealedSecretInventory: new DrizzleSealedSecretInventory(db) }
1078
- : {}),
1079
- // The per-workspace OpenRouter dynamic-catalog store; present when the API-key pool is.
1185
+ db,
1080
1186
  openRouterCatalog,
1081
- // Flush + release the external trace sink on graceful shutdown so the OpenTelemetry SDK
1082
- // exporter's final batch of spans/metrics isn't dropped and its background timers are
1083
- // cleared. Best-effort; a no-op for the fetch-based Langfuse sink and when nothing is
1084
- // wired. (The local facade composes this into its own `onShutdown` — see its container.)
1085
- onShutdown: async () => {
1086
- await traceSink?.shutdown?.();
1087
- },
1088
- };
1187
+ traceSink,
1188
+ });
1089
1189
  }
1090
1190
  /**
1091
1191
  * Wire the task-source integration for the Node facade when it is enabled (the