@cat-factory/node-server 0.107.11 → 0.107.13
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/config.d.ts.map +1 -1
- package/dist/config.js +240 -198
- package/dist/config.js.map +1 -1
- package/dist/container-account-deps.d.ts +40 -0
- package/dist/container-account-deps.d.ts.map +1 -0
- package/dist/container-account-deps.js +142 -0
- package/dist/container-account-deps.js.map +1 -0
- package/dist/container-github-deps.d.ts +56 -0
- package/dist/container-github-deps.d.ts.map +1 -0
- package/dist/container-github-deps.js +275 -0
- package/dist/container-github-deps.js.map +1 -0
- package/dist/container-model-deps.d.ts +50 -0
- package/dist/container-model-deps.d.ts.map +1 -0
- package/dist/container-model-deps.js +127 -0
- package/dist/container-model-deps.js.map +1 -0
- package/dist/container-realtime-deps.d.ts +34 -0
- package/dist/container-realtime-deps.d.ts.map +1 -0
- package/dist/container-realtime-deps.js +112 -0
- package/dist/container-realtime-deps.js.map +1 -0
- package/dist/container-run-services-deps.d.ts +44 -0
- package/dist/container-run-services-deps.d.ts.map +1 -0
- package/dist/container-run-services-deps.js +148 -0
- package/dist/container-run-services-deps.js.map +1 -0
- package/dist/container-transport-deps.d.ts +72 -0
- package/dist/container-transport-deps.d.ts.map +1 -0
- package/dist/container-transport-deps.js +88 -0
- package/dist/container-transport-deps.js.map +1 -0
- package/dist/container.d.ts +2 -23
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js +173 -785
- package/dist/container.js.map +1 -1
- package/package.json +8 -8
package/dist/container.js
CHANGED
|
@@ -1,39 +1,40 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defaultAgentKindRegistry, defaultInitiativePresetRegistry, } from '@cat-factory/agents';
|
|
2
2
|
// Opt-in AWS EKS backends (runner + environment), registered by reference below (the Worker
|
|
3
3
|
// facade registers the same pair, keeping the runtimes symmetric with the native `kubernetes`
|
|
4
4
|
// backend these extend). They are pass-throughs until a workspace actually connects an `eks`
|
|
5
5
|
// backend, and carry NO runtime AWS SDK dependency (the token is minted with WebCrypto), so this
|
|
6
6
|
// adds no cost to a deployment that never uses EKS.
|
|
7
7
|
import { eksEnvironmentBackend, eksRunnerBackend } from '@cat-factory/eks';
|
|
8
|
-
import { ConfluenceProvider, FigmaProvider, ZeplinProvider, GitHubDocsProvider,
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { CompositeAgentExecutor, ContainerSessionService,
|
|
8
|
+
import { ConfluenceProvider, FigmaProvider, ZeplinProvider, GitHubDocsProvider, LinearDocumentProvider, createBackendRegistries, HttpRunnerPoolProvider, NotionProvider, EMAIL_CIPHER_INFO, createEmailSender, TicketTrackerService, } from '@cat-factory/integrations';
|
|
9
|
+
import { DEFAULT_MODEL_PRESET_ID, defaultProviderRegistry, defaultVcsRegistry, } from '@cat-factory/kernel';
|
|
10
|
+
import { createCore, defaultStepResolverRegistry, resolvePresetModelForKind, } from '@cat-factory/orchestration';
|
|
11
|
+
import { CompositeAgentExecutor, ContainerSessionService, GitHubAppAuth, GitHubAppRegistry, GitHubIdentityResolver, runWithInitiator, WebCryptoPasswordHasher, WebCryptoSecretCipher, buildInfrastructureCapabilities, testEnvHasZeroConfigDefault, buildResolveRepoTarget, buildResolveRepoTargets, makePreviewJobBuilder, logger, resolveUrlSafetyPolicy, resolveWorkspaceCapabilities, } from '@cat-factory/server';
|
|
12
12
|
// The built-in polling-gate suite (ci / conflicts / post-release-health + on-call). The facade
|
|
13
13
|
// builds an app-owned `GateRegistry` pre-loaded with the suite via `gateRegistryWithBuiltins()`
|
|
14
14
|
// below, then wires each gate's provider.
|
|
15
|
-
import { applyGateProviders, gateRegistryWithBuiltins,
|
|
15
|
+
import { applyGateProviders, gateRegistryWithBuiltins, warnUnwiredGates, } from '@cat-factory/gates';
|
|
16
16
|
import { buildGitLabEngineClient, GitLabIdentityResolver, registerGitLab, StaticGitLabTokenSource, } from '@cat-factory/gitlab';
|
|
17
17
|
import { loadNodeConfig } from './config.js';
|
|
18
|
+
import { selectNodeGitHubDeps } from './container-github-deps.js';
|
|
19
|
+
import { buildNodeModelDeps } from './container-model-deps.js';
|
|
20
|
+
import { buildNodeRunServices } from './container-run-services-deps.js';
|
|
21
|
+
import { buildNodeBootstrapper, buildNodeTransportDeploy } from './container-transport-deps.js';
|
|
22
|
+
import { buildNodeAccountDeps } from './container-account-deps.js';
|
|
23
|
+
import { buildNodeRealtimeDeps } from './container-realtime-deps.js';
|
|
18
24
|
import { executionRuntime } from './execution/config.js';
|
|
19
25
|
import { PgBossBootstrapRunner } from './execution/bootstrapRunner.js';
|
|
20
26
|
import { PgBossEnvConfigRepairRunner } from './execution/envConfigRepairRunner.js';
|
|
21
27
|
import { PgBossEnvironmentTestRunner } from './execution/envTestRunner.js';
|
|
22
28
|
import { PgBossWorkRunner } from './execution/pgBossRunner.js';
|
|
23
29
|
import { createNodeGateways } from './gateways.js';
|
|
24
|
-
import { baseUrlForNode
|
|
25
|
-
import { ConsensusAgentExecutor, registerConsensusTraits } from '@cat-factory/consensus';
|
|
30
|
+
import { baseUrlForNode } from './modelProvider.js';
|
|
26
31
|
import { LocalMachineEventRelay } from './machineEventRelay.js';
|
|
27
|
-
import { NodeEventPublisher } from './realtime.js';
|
|
28
32
|
import { DrizzleGitHubInstallationRepository, DrizzleRunnerPoolConnectionRepository, } from './repositories/containerExecution.js';
|
|
29
|
-
import {
|
|
33
|
+
import { DrizzleRepoProjectionRepository } from './repositories/github.js';
|
|
30
34
|
import { DrizzleSubscriptionActivationRepository } from './repositories/personalSubscription.js';
|
|
31
35
|
import { DrizzleUserRepoAccessRepository } from './repositories/userRepoAccess.js';
|
|
32
36
|
import { createDrizzleRepositories, createDrizzleSandboxDeps } from './repositories/drizzle.js';
|
|
33
|
-
import {
|
|
34
|
-
import { FilesystemBinaryBlobBackend } from './storage/FilesystemBinaryBlobBackend.js';
|
|
35
|
-
import { S3BinaryBlobBackend } from '@cat-factory/provider-s3';
|
|
36
|
-
import { DrizzleBootstrapJobRepository, DrizzleReferenceArchitectureRepository, } from './repositories/bootstrap.js';
|
|
37
|
+
import { DrizzleReferenceArchitectureRepository } from './repositories/bootstrap.js';
|
|
37
38
|
import { DrizzleEnvConfigRepairJobRepository } from './repositories/envConfigRepair.js';
|
|
38
39
|
import { DrizzleEnvironmentTestRunRepository } from './repositories/environmentTest.js';
|
|
39
40
|
import { DrizzleDocumentConnectionRepository, DrizzleDocumentRepository, } from './repositories/documents.js';
|
|
@@ -41,14 +42,14 @@ import { DrizzleEnvironmentConnectionRepository, DrizzleEnvironmentRegistryRepos
|
|
|
41
42
|
import { DrizzleCustomManifestTypeRepository } from './repositories/customManifestType.js';
|
|
42
43
|
import { DrizzleNotificationRepository } from './repositories/notifications.js';
|
|
43
44
|
import { selectNodeFragmentLibraryDeps, selectNodeSkillLibraryDeps, } from './container-content-library-deps.js';
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
45
|
+
import {} from './repositories/slack.js';
|
|
46
|
+
import {} from './repositories/tasks.js';
|
|
46
47
|
import { CryptoIdGenerator, SystemClock } from './runtime.js';
|
|
47
|
-
import {
|
|
48
|
+
import {} from './wireCredentialServices.js';
|
|
48
49
|
// The container-agent-executor wiring (transport resolver, provisioning-log wrapper, container
|
|
49
50
|
// executor + bootstrapper + env-config repairer, GitHub-issue filer, trace-sink builder), lifted
|
|
50
51
|
// into a sibling module so this composition root stays within the file-size budget.
|
|
51
|
-
import { RUNNERS_CIPHER_INFO, buildNodeContainerExecutor,
|
|
52
|
+
import { RUNNERS_CIPHER_INFO, buildNodeContainerExecutor, buildTraceSink, selectNodeEnvConfigRepairer, } from './container-executor-deps.js';
|
|
52
53
|
// Re-export the public seams the local facade + tests still import from `./container.js`.
|
|
53
54
|
export { buildNodeResolveTransport, missingContainerExecutorPrereqs, withProvisioningLog, } from './container-executor-deps.js';
|
|
54
55
|
/**
|
|
@@ -72,79 +73,6 @@ export function pickRepoSource(remote, name, build) {
|
|
|
72
73
|
// own. Mirrors the Worker's `buildModelProvider` memoisation. Memoisation matters more for
|
|
73
74
|
// OTel than Langfuse: the SDK sink owns batch processors/exporters, so it must be built
|
|
74
75
|
// once per config, not per wiring site.
|
|
75
|
-
/** Truthy env flag (`true`/`1`/`yes`). */
|
|
76
|
-
function isTruthy(value) {
|
|
77
|
-
return value === 'true' || value === '1' || value === 'yes';
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* The Node model-provider RESOLVER (instrumented when Langfuse is on), shared per
|
|
81
|
-
* `(env, db)`. Builds a per-scope provider from the DB-backed API-key pool plus opt-in
|
|
82
|
-
* Cloudflare-REST / Bedrock registries. Mirrors the Worker's buildModelProviderResolver.
|
|
83
|
-
*/
|
|
84
|
-
const modelResolverCache = new WeakMap();
|
|
85
|
-
function buildModelProviderResolver(env, db, apiKeys, localModelEndpoints,
|
|
86
|
-
// The shared inline instrument (one trace sink for the proxied path, the core AND the
|
|
87
|
-
// inline calls) so the OTel SDK exporter isn't rebuilt per wiring site.
|
|
88
|
-
instrument) {
|
|
89
|
-
// The cache keys on the db handle (one resolver per Drizzle client). Mothership mode has no
|
|
90
|
-
// db, so skip the cache entirely (WeakMap keys must be objects) and build a fresh resolver —
|
|
91
|
-
// a mothership node builds one container, so there is nothing to share it with anyway.
|
|
92
|
-
if (!db)
|
|
93
|
-
return createNodeModelProviderResolver(env, apiKeys, localModelEndpoints, instrument);
|
|
94
|
-
const cached = modelResolverCache.get(db);
|
|
95
|
-
if (cached)
|
|
96
|
-
return cached;
|
|
97
|
-
const resolver = createNodeModelProviderResolver(env, apiKeys, localModelEndpoints, instrument);
|
|
98
|
-
modelResolverCache.set(db, resolver);
|
|
99
|
-
return resolver;
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Wire the Slack integration when enabled: the notification *channel* (an extra
|
|
103
|
-
* delivery transport composed onto the notification mechanism — Node has no in-app
|
|
104
|
-
* channel, so this is its only one) plus the management repositories (per-account
|
|
105
|
-
* connect + per-workspace routing + member map) and the bot-token cipher. The
|
|
106
|
-
* per-account bot token is sealed with the shared ENCRYPTION_KEY under a
|
|
107
|
-
* slack-scoped HKDF info, mirroring the Worker. OAuth credentials are optional.
|
|
108
|
-
*/
|
|
109
|
-
function selectNodeSlackDeps(config, repos,
|
|
110
|
-
// The remote-source seam (mothership mode): `sourced('name', build)` returns the remote registry
|
|
111
|
-
// entry when there's no `db`, else builds the Drizzle repo. Routing the three Slack repos through
|
|
112
|
-
// it makes the connect / route / member-map management surface functional in mothership mode —
|
|
113
|
-
// AND keeps the `SlackNotificationChannel` (which captures these repos directly) reading the SAME
|
|
114
|
-
// remote-backed repos, so it can't drift to the broken db-less Drizzle instances. The bot token
|
|
115
|
-
// rides a SEALED `tokenCipher` (sealed/decrypted under the LOCAL key), so the sealed blob — never
|
|
116
|
-
// plaintext — crosses the machine API; the settings + member-mapping rows carry no secrets. The
|
|
117
|
-
// RPC allow-list gates each method by its account/workspace scope. (Mothership-SIDE delivery for a
|
|
118
|
-
// hosted teammate's notification — the mothership decrypting a laptop-sealed token — is the later
|
|
119
|
-
// secrets-delegation slice; local delivery, where the run's own node holds the key, works.)
|
|
120
|
-
sourced) {
|
|
121
|
-
if (!config.slack.enabled || !config.slack.encryptionKey)
|
|
122
|
-
return {};
|
|
123
|
-
const secretCipher = new WebCryptoSecretCipher({
|
|
124
|
-
masterKeyBase64: config.slack.encryptionKey,
|
|
125
|
-
info: SLACK_CIPHER_INFO,
|
|
126
|
-
});
|
|
127
|
-
const slackConnectionRepository = sourced('slackConnectionRepository', (d) => new DrizzleSlackConnectionRepository(d));
|
|
128
|
-
const slackSettingsRepository = sourced('slackSettingsRepository', (d) => new DrizzleSlackSettingsRepository(d));
|
|
129
|
-
const slackMemberMappingRepository = sourced('slackMemberMappingRepository', (d) => new DrizzleSlackMemberMappingRepository(d));
|
|
130
|
-
return {
|
|
131
|
-
notificationChannel: new SlackNotificationChannel({
|
|
132
|
-
workspaceRepository: repos.workspaceRepository,
|
|
133
|
-
slackConnectionRepository,
|
|
134
|
-
slackSettingsRepository,
|
|
135
|
-
slackMemberMappingRepository,
|
|
136
|
-
blockRepository: repos.blockRepository,
|
|
137
|
-
secretCipher,
|
|
138
|
-
// Best-effort delivery still surfaces failures (revoked token, missing channel
|
|
139
|
-
// invite) through the structured logger so a broken route is diagnosable.
|
|
140
|
-
onError: (error, ctx) => logger.warn({ err: error instanceof Error ? error.message : String(error), ...ctx }, 'slack notification delivery failed'),
|
|
141
|
-
}),
|
|
142
|
-
slackConnectionRepository,
|
|
143
|
-
slackSettingsRepository,
|
|
144
|
-
slackMemberMappingRepository,
|
|
145
|
-
slackSecretCipher: secretCipher,
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
76
|
/**
|
|
149
77
|
* Wire account invitations + per-account email senders for the Node facade (parity
|
|
150
78
|
* with the Worker's `selectEmailInvitationDeps`). Invitations are always available (an
|
|
@@ -193,14 +121,6 @@ function buildSystemEmailSender(config) {
|
|
|
193
121
|
* Rate-limit accounting is best-effort telemetry the Worker persists to D1; the Node
|
|
194
122
|
* facade has no such table, so it drops the snapshots (exactly like the local facade).
|
|
195
123
|
*/
|
|
196
|
-
class NoopRateLimitRepository {
|
|
197
|
-
record(_snapshot) {
|
|
198
|
-
return Promise.resolve();
|
|
199
|
-
}
|
|
200
|
-
deleteOlderThan(_epochMs) {
|
|
201
|
-
return Promise.resolve(0);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
124
|
/**
|
|
205
125
|
* The workspace-spanning GitHub App registry, built once and shared by everything that
|
|
206
126
|
* needs an App credential: the container executor's push-token mint, the tech-debt
|
|
@@ -278,6 +198,49 @@ function buildNodeVcsIdentityRegistry(config) {
|
|
|
278
198
|
* App, `PUBLIC_URL`, `AUTH_SESSION_SECRET`, `ENCRYPTION_KEY`) are absent the
|
|
279
199
|
* composite still serves inline kinds but fails container kinds loudly.
|
|
280
200
|
*/
|
|
201
|
+
/**
|
|
202
|
+
* Resolve every app-owned registry the container wires: the backend-kind registries (env + runner
|
|
203
|
+
* + custom-manifest + user-secret), the agent-kind / gate / step-resolver / initiative-preset /
|
|
204
|
+
* VCS-provider / gate-provider registries. Each is the injected instance when `options` supplies it
|
|
205
|
+
* (a deployment's custom entries, or the conformance harness's pre-loaded instance) else the
|
|
206
|
+
* built-ins-only default, and the opt-in AWS EKS backends are registered by reference. Extracted
|
|
207
|
+
* from {@link buildNodeContainer} to keep it under the complexity ceiling.
|
|
208
|
+
*/
|
|
209
|
+
function resolveNodeAppRegistries(options) {
|
|
210
|
+
const { environmentBackendRegistry, runnerBackendRegistry, customManifestTypeRegistry, userSecretKindRegistry, } = options.backendRegistries ?? createBackendRegistries();
|
|
211
|
+
// Register the opt-in AWS EKS backends by reference (the default registries stay AWS-free).
|
|
212
|
+
// Reuses the native Kubernetes transport/provider behind a minted IAM apiserver token; a
|
|
213
|
+
// pass-through until a workspace connects an `eks` backend. Registered on BOTH facades (the
|
|
214
|
+
// Worker registers the same pair in its container build) so the runtimes stay symmetric with
|
|
215
|
+
// the native `kubernetes` backend these extend — a real EKS cluster's private-CA apiserver is
|
|
216
|
+
// only reachable from a runtime that can pin a custom CA (Node/local), the same constraint a
|
|
217
|
+
// private-CA `kubernetes` connection already carries.
|
|
218
|
+
runnerBackendRegistry.register(eksRunnerBackend);
|
|
219
|
+
environmentBackendRegistry.register(eksEnvironmentBackend);
|
|
220
|
+
return {
|
|
221
|
+
environmentBackendRegistry,
|
|
222
|
+
runnerBackendRegistry,
|
|
223
|
+
customManifestTypeRegistry,
|
|
224
|
+
userSecretKindRegistry,
|
|
225
|
+
// The app-owned agent-kind registry: the injected instance (so a deployment's custom kinds
|
|
226
|
+
// are visible) else the built-ins-only default.
|
|
227
|
+
agentKindRegistry: options.agentKindRegistry ?? defaultAgentKindRegistry(),
|
|
228
|
+
// The app-owned gate registry: the injected instance (conformance / a deployment pre-loads it),
|
|
229
|
+
// else a fresh one with the built-in `@cat-factory/gates` suite installed.
|
|
230
|
+
gateRegistry: options.gateRegistry ?? gateRegistryWithBuiltins(),
|
|
231
|
+
// The app-owned step-resolver registry: the injected instance else an empty default (the
|
|
232
|
+
// built-in `merger` resolver is a privileged engine built-in, not a registry entry).
|
|
233
|
+
stepResolverRegistry: options.stepResolverRegistry ?? defaultStepResolverRegistry(),
|
|
234
|
+
// The app-owned initiative-preset registry: the injected instance else the built-ins-only
|
|
235
|
+
// default (generic / docs-refresh / tech-migration).
|
|
236
|
+
initiativePresetRegistry: options.initiativePresetRegistry ?? defaultInitiativePresetRegistry(),
|
|
237
|
+
// Opt-in GitLab VCS provider registry (a no-op unless GITLAB_TOKEN is set; the wiring lives in
|
|
238
|
+
// the caller, symmetric with the Worker facade per "keep the runtimes symmetric").
|
|
239
|
+
vcsRegistry: options.vcsRegistry ?? defaultVcsRegistry(),
|
|
240
|
+
// The app-owned provider registry the built-in gates probe through (fresh empty unless injected).
|
|
241
|
+
providerRegistry: options.providerRegistry ?? defaultProviderRegistry(),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
281
244
|
export function buildNodeContainer(options) {
|
|
282
245
|
const env = options.env ?? process.env;
|
|
283
246
|
const config = options.config ?? loadNodeConfig(env);
|
|
@@ -330,91 +293,19 @@ export function buildNodeContainer(options) {
|
|
|
330
293
|
// `remoteRepos` + `db` are fixed for this build, so bind them once: `sourced('name', (d) => …)`
|
|
331
294
|
// picks the remote registry entry in mothership mode, else builds the Drizzle repo over `db`.
|
|
332
295
|
const sourced = (name, build) => pickRepoSource(remoteRepos, name, () => build(db));
|
|
333
|
-
// The app-owned
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
const { environmentBackendRegistry, runnerBackendRegistry, customManifestTypeRegistry, userSecretKindRegistry, } = options
|
|
338
|
-
// The
|
|
339
|
-
//
|
|
340
|
-
// createCore and the ServerContainer snapshot projection.
|
|
341
|
-
const agentKindRegistry = options.agentKindRegistry ?? defaultAgentKindRegistry();
|
|
342
|
-
// The app-owned gate registry: the injected instance (conformance / a deployment pre-loads it),
|
|
343
|
-
// else a fresh one with the built-in `@cat-factory/gates` suite installed via
|
|
344
|
-
// `gateRegistryWithBuiltins()`. Flows into createCore (the engine's gate machine) and is
|
|
345
|
-
// re-exposed on Core so `start()` validates the SAME instance.
|
|
346
|
-
const gateRegistry = options.gateRegistry ?? gateRegistryWithBuiltins();
|
|
347
|
-
// The app-owned step-resolver registry: the injected instance else an empty default (the
|
|
348
|
-
// built-in `merger` resolver is a privileged engine built-in, not a registry entry).
|
|
349
|
-
const stepResolverRegistry = options.stepResolverRegistry ?? defaultStepResolverRegistry();
|
|
350
|
-
// The app-owned initiative-preset registry: the injected instance else the built-ins-only
|
|
351
|
-
// default (generic / docs-refresh / tech-migration). Flows into createCore (initiative services
|
|
352
|
-
// + spawned-run preset context) and the ServerContainer snapshot descriptors + preset probe.
|
|
353
|
-
const initiativePresetRegistry = options.initiativePresetRegistry ?? defaultInitiativePresetRegistry();
|
|
354
|
-
// Register the opt-in AWS EKS backends by reference (the default registries stay AWS-free).
|
|
355
|
-
// Reuses the native Kubernetes transport/provider behind a minted IAM apiserver token; a
|
|
356
|
-
// pass-through until a workspace connects an `eks` backend. Registered on BOTH facades (the
|
|
357
|
-
// Worker registers the same pair in its container build) so the runtimes stay symmetric with
|
|
358
|
-
// the native `kubernetes` backend these extend — a real EKS cluster's private-CA apiserver is
|
|
359
|
-
// only reachable from a runtime that can pin a custom CA (Node/local), the same constraint a
|
|
360
|
-
// private-CA `kubernetes` connection already carries.
|
|
361
|
-
runnerBackendRegistry.register(eksRunnerBackend);
|
|
362
|
-
environmentBackendRegistry.register(eksEnvironmentBackend);
|
|
363
|
-
// Binary-artifact storage (UI screenshots + reference design images) for the
|
|
364
|
-
// visual-confirmation gate. The backend is configured PER ACCOUNT in the UI (no env vars):
|
|
365
|
-
// the metadata always lives in Postgres; the bytes go to the account's chosen blob backend
|
|
366
|
-
// (`fs` → the local filesystem; `db` → a Postgres `bytea` table; `s3` → an S3 bucket). The
|
|
367
|
-
// composed store is resolved per request/run from the account settings (see
|
|
368
|
-
// `resolveBinaryArtifactStore`, built below once `accountSettings` exists).
|
|
369
|
-
const contentStorageCapability = {
|
|
370
|
-
supportedBackends: ['off', 'fs', 's3', 'db'],
|
|
371
|
-
defaultBackend: options.contentStorageDefaultBackend ?? 'off',
|
|
372
|
-
};
|
|
373
|
-
const buildNodeBlobBackend = (kind, opts) => {
|
|
374
|
-
switch (kind) {
|
|
375
|
-
case 'fs':
|
|
376
|
-
// NOTE: the filesystem backend is local-disk only. It is correct for the local facade
|
|
377
|
-
// and a single-instance Node deployment with a persistent volume, but NOT for a scaled
|
|
378
|
-
// (multi-replica) or ephemeral-disk deployment — bytes written on one replica are
|
|
379
|
-
// invisible to the others and lost on redeploy. Scaled deployments should pick `s3`.
|
|
380
|
-
return new FilesystemBinaryBlobBackend({ basePath: opts.fs?.basePath });
|
|
381
|
-
case 'db':
|
|
382
|
-
return new PostgresBinaryBlobBackend(db);
|
|
383
|
-
case 's3':
|
|
384
|
-
if (!opts.s3)
|
|
385
|
-
return null;
|
|
386
|
-
// Omitting credentials is intentional: the S3 client then falls back to the ambient AWS
|
|
387
|
-
// credential chain (instance role / `AWS_*` env), which is the right behaviour for a
|
|
388
|
-
// deployment running on AWS with an attached role. The UI requires explicit keys, so this
|
|
389
|
-
// path is only reached by a config written through another channel.
|
|
390
|
-
return new S3BinaryBlobBackend({
|
|
391
|
-
...opts.s3,
|
|
392
|
-
...(opts.s3Credentials ? { credentials: opts.s3Credentials } : {}),
|
|
393
|
-
});
|
|
394
|
-
default:
|
|
395
|
-
// `r2`/`memory` are not served on Node/local — null ⇒ storage unavailable.
|
|
396
|
-
return null;
|
|
397
|
-
}
|
|
398
|
-
};
|
|
399
|
-
// The built-in gates' providers are wired onto the app-owned `providerRegistry` (news'd below,
|
|
400
|
-
// fresh unless injected via `options`). The GitHub + release-health wiring runs only inside its
|
|
296
|
+
// The app-owned registries (backend kinds, agent kinds, gates, step resolvers, initiative
|
|
297
|
+
// presets, VCS providers, gate-provider registry) — injected instances when supplied (a
|
|
298
|
+
// deployment's custom entries / the conformance harness) else the built-ins-only defaults.
|
|
299
|
+
// The SAME instances flow to the executors, createCore, and the ServerContainer projection.
|
|
300
|
+
const { environmentBackendRegistry, runnerBackendRegistry, customManifestTypeRegistry, userSecretKindRegistry, agentKindRegistry, gateRegistry, stepResolverRegistry, initiativePresetRegistry, vcsRegistry, providerRegistry, } = resolveNodeAppRegistries(options);
|
|
301
|
+
// The built-in gates' providers are wired onto the app-owned `providerRegistry` (fresh unless
|
|
302
|
+
// injected via `options`). The GitHub + release-health wiring runs only inside its
|
|
401
303
|
// `enabled`/`githubClient` branches; a fresh registry starts empty, so an unconfigured gate just
|
|
402
304
|
// stays unwired (pass-through) — no reset needed (the former `clearGateProviders()` guarded a
|
|
403
305
|
// module-global that no longer exists). Mirrors the Worker facade (keep the runtimes symmetric).
|
|
404
306
|
// Any test-injected gate providers (`options.gateProviders`) are applied at the END of this build
|
|
405
307
|
// so they OVERRIDE the config wiring (local mode wires a PAT-backed CI provider here that would
|
|
406
308
|
// otherwise clobber a faked one) — gates read their provider lazily at probe time, last write wins.
|
|
407
|
-
// Opt-in GitLab VCS provider (single-token model, mirroring local-mode's PAT). Registered on
|
|
408
|
-
// the app-owned `vcsRegistry` so the neutral webhook route + any VcsConnectionRef holder
|
|
409
|
-
// resolves it. A no-op unless GITLAB_TOKEN is set; symmetric with the Worker facade (and
|
|
410
|
-
// inherited by local) per "keep the runtimes symmetric".
|
|
411
|
-
const vcsRegistry = options.vcsRegistry ?? defaultVcsRegistry();
|
|
412
|
-
// The app-owned provider registry the built-in gates probe through. A single-process facade, so
|
|
413
|
-
// one instance for the process (the injected one via `options`, else a fresh empty one). The
|
|
414
|
-
// GitHub CI/mergeability/review/doc-quality + release-health + incident providers are wired onto
|
|
415
|
-
// it below when configured; injected into `createCore` so the gate machine reads the SAME
|
|
416
|
-
// instance. A fresh instance starts empty, so the former `clearGateProviders()` reset is gone.
|
|
417
|
-
const providerRegistry = options.providerRegistry ?? defaultProviderRegistry();
|
|
418
309
|
let gitlabEngineClient;
|
|
419
310
|
if (config.gitlab?.enabled && env.GITLAB_TOKEN) {
|
|
420
311
|
registerGitLab(vcsRegistry, {
|
|
@@ -436,79 +327,28 @@ export function buildNodeContainer(options) {
|
|
|
436
327
|
// selected/default model preset > env routing), uniformly for inline and container
|
|
437
328
|
// kinds. The built-in default preset points every agent kind at Kimi K2.7.
|
|
438
329
|
const resolveWorkspaceModelDefault = (workspaceId, agentKind, modelPresetId) => resolvePresetModelForKind(repos.modelPresetRepository, workspaceId, agentKind, modelPresetId);
|
|
439
|
-
// The
|
|
440
|
-
//
|
|
441
|
-
//
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
// controller and the run-initiator PAT resolver below.
|
|
451
|
-
const userSecrets = buildNodeUserSecretService(env, db, clock, userSecretKindRegistry, options.caches?.viewerRepos);
|
|
452
|
-
// Resolve the run initiator's stored GitHub PAT (when set) — preferred over the
|
|
453
|
-
// App/env token by the container push-token mint + the engine GitHub client.
|
|
454
|
-
const resolveUserGitHubToken = userSecrets
|
|
455
|
-
? (userId) => userSecrets.resolve(userId, 'github_pat')
|
|
456
|
-
: undefined;
|
|
457
|
-
// The per-workspace OpenRouter dynamic-catalog store — shared by the catalog controller,
|
|
458
|
-
// the per-workspace model catalog's dynamic OpenRouter entries, and the spend overlay.
|
|
459
|
-
const openRouterCatalog = buildNodeOpenRouterCatalogService(env, db, clock, apiKeys, config.spend.currency);
|
|
460
|
-
// The subscription-token pool (Claude Code / Codex credentials), shared by the
|
|
461
|
-
// container executor (lease + usage feedback) and the vendor-credential controller.
|
|
462
|
-
// Built HERE (before the model-provider wrap below) so its lease closures can be handed
|
|
463
|
-
// to `wrapModelProviderResolver` — the local facade's inline-harness wrap serves an
|
|
464
|
-
// inline subscription ref through a warm container on a LEASED credential, so it needs the
|
|
465
|
-
// same lease seams the container executor uses (built once, shared by both).
|
|
466
|
-
const subscriptions = buildNodeSubscriptionService(env, db, repos.workspaceRepository, idGenerator, clock, options.providerSubscriptionTokenRepository);
|
|
467
|
-
// The per-user individual-usage subscription store (Claude), shared by the
|
|
468
|
-
// container executor's personal lease, the personal-subscription controller, and the
|
|
469
|
-
// inline-harness wrap's per-run personal lease.
|
|
470
|
-
const personalSubscriptions = buildNodePersonalSubscriptionService(env, db, idGenerator, clock, options.personalSubscriptionRepository, options.subscriptionActivationRepository);
|
|
471
|
-
// The ONE external trace sink for this container (memoised per config): the core, the
|
|
472
|
-
// container executor AND the inline model-provider instrumentation all share this single
|
|
473
|
-
// instance, so the OTel SDK exporter's batch processors/timers exist exactly once (and its
|
|
474
|
-
// shutdown is wired below). Its `recordPrompts` matches the proxied path's gating.
|
|
475
|
-
const traceSink = buildTraceSink(config);
|
|
476
|
-
const baseModelProviderResolver = buildModelProviderResolver(env, db, apiKeys, localModelEndpoints, traceSink ? { traceSink, recordPrompts: config.observability.recordPrompts } : undefined);
|
|
477
|
-
const wrappedModelProviderResolver = options.wrapModelProviderResolver
|
|
478
|
-
? options.wrapModelProviderResolver(baseModelProviderResolver, {
|
|
479
|
-
...(personalSubscriptions
|
|
480
|
-
? {
|
|
481
|
-
leasePersonalSubscriptionToken: (executionId, userId, vendor) => personalSubscriptions.leaseForRun(executionId, userId, vendor),
|
|
482
|
-
}
|
|
483
|
-
: {}),
|
|
484
|
-
...(subscriptions
|
|
485
|
-
? {
|
|
486
|
-
leaseSubscriptionToken: (workspaceId, vendor) => subscriptions.leaseToken(workspaceId, vendor),
|
|
487
|
-
}
|
|
488
|
-
: {}),
|
|
489
|
-
})
|
|
490
|
-
: baseModelProviderResolver;
|
|
491
|
-
// Cap concurrent inline calls to a subscription vendor, OUTERMOST so it sits outside the
|
|
492
|
-
// local facade's subscription-inline harness wrap above (and therefore sees the un-degraded
|
|
493
|
-
// subscription ref). One limiter per container = per process for a stock node, per tenant in
|
|
494
|
-
// mothership mode; a pass-through when nothing is capped. Symmetric with the Worker's wrap in
|
|
495
|
-
// `buildModelProviderResolver` (see "Keep the runtimes symmetric").
|
|
496
|
-
const modelProviderResolver = wrapResolverWithLimiter(wrappedModelProviderResolver, vendorConcurrencyLimiterFromEnv((key) => env[key]));
|
|
497
|
-
// Cloudflare Workers AI is opt-in on Node: enabled when the REST creds are present.
|
|
498
|
-
const cloudflareModelsEnabled = options.cloudflareModelsEnabled ?? !!(env.CLOUDFLARE_ACCOUNT_ID && env.CLOUDFLARE_API_TOKEN);
|
|
499
|
-
const inline = new AiAgentExecutor({
|
|
500
|
-
modelProviderResolver,
|
|
501
|
-
agentRouting: config.agents.routing,
|
|
502
|
-
resolveBlockModel: config.agents.resolveBlockModel,
|
|
503
|
-
resolveWorkspaceModelDefault,
|
|
504
|
-
// In local mode this keeps an ambient-eligible subscription harness ref so the inline
|
|
505
|
-
// design/research kinds run on the developer's Claude Code / Codex CLI; undefined on
|
|
506
|
-
// stock Node (no inline harness), where such a ref degrades to the routing default.
|
|
507
|
-
...(config.agents.inlineHarnessRef ? { runsInline: config.agents.inlineHarnessRef } : {}),
|
|
508
|
-
// Opt-in provider web search for the inline design/research kinds (no-op unless
|
|
509
|
-
// INLINE_WEB_SEARCH_ENABLED and an Anthropic/OpenAI model).
|
|
510
|
-
webSearch: inlineWebSearchOptionsFromEnv(env),
|
|
330
|
+
// The credential/token stores + the model-provisioning stack (API-key pool, public-API +
|
|
331
|
+
// local-model-endpoint + user-secret + OpenRouter + subscription + personal-subscription
|
|
332
|
+
// stores, the trace sink, the model-provider resolver, and the inline executor), lifted into
|
|
333
|
+
// `container-model-deps.ts` so this composition root stays within the file-size budget.
|
|
334
|
+
const { apiKeys, publicApiKeys, localModelEndpoints, userSecrets, resolveUserGitHubToken, openRouterCatalog, subscriptions, personalSubscriptions, traceSink, modelProviderResolver, cloudflareModelsEnabled, inline, } = buildNodeModelDeps({
|
|
335
|
+
env,
|
|
336
|
+
config,
|
|
337
|
+
db,
|
|
338
|
+
workspaceRepository: repos.workspaceRepository,
|
|
339
|
+
idGenerator,
|
|
340
|
+
clock,
|
|
511
341
|
agentKindRegistry,
|
|
342
|
+
userSecretKindRegistry,
|
|
343
|
+
resolveWorkspaceModelDefault,
|
|
344
|
+
providerApiKeyRepository: options.providerApiKeyRepository,
|
|
345
|
+
localModelEndpointRepository: options.localModelEndpointRepository,
|
|
346
|
+
providerSubscriptionTokenRepository: options.providerSubscriptionTokenRepository,
|
|
347
|
+
personalSubscriptionRepository: options.personalSubscriptionRepository,
|
|
348
|
+
subscriptionActivationRepository: options.subscriptionActivationRepository,
|
|
349
|
+
wrapModelProviderResolver: options.wrapModelProviderResolver,
|
|
350
|
+
cloudflareModelsEnabled: options.cloudflareModelsEnabled,
|
|
351
|
+
caches: options.caches,
|
|
512
352
|
});
|
|
513
353
|
// Persistence the container-execution path needs (built from the same db). The
|
|
514
354
|
// runner-pool repo also backs the `runners` Core module so a pool is registrable
|
|
@@ -548,174 +388,34 @@ export function buildNodeContainer(options) {
|
|
|
548
388
|
blockRepository: repos.blockRepository,
|
|
549
389
|
serviceRepository: repos.serviceRepository,
|
|
550
390
|
});
|
|
551
|
-
//
|
|
552
|
-
//
|
|
553
|
-
//
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
});
|
|
559
|
-
// A sibling facade (local mode) may inject its own transport — even `null` — which
|
|
560
|
-
// replaces the default self-hosted-pool resolution; undefined keeps Node's default
|
|
561
|
-
// (a self-hosted pool, optionally driven by an injected native `runnerPoolProvider`).
|
|
562
|
-
// The injected transport is a per-run container (local mode), the default is a
|
|
563
|
-
// self-hosted pool — tag each accordingly so the logs drawer can filter by subsystem.
|
|
564
|
-
// A facade that pre-wraps its branches with their own subsystem tags (local mode) sets
|
|
565
|
-
// `skipProvisioningLogWrap` so we don't double-wrap.
|
|
566
|
-
const baseResolveTransport = options.resolveTransport !== undefined
|
|
567
|
-
? options.resolveTransport
|
|
568
|
-
: buildNodeResolveTransport(config, runnerPoolConnectionRepository, repos.workspaceRepository, clock, runnerBackendRegistry, options.runnerPoolProvider);
|
|
569
|
-
const resolveTransport = options.skipProvisioningLogWrap
|
|
570
|
-
? baseResolveTransport
|
|
571
|
-
: withProvisioningLog(baseResolveTransport, provisioningLogRecorder, options.resolveTransport !== undefined ? 'container' : 'runner-pool');
|
|
572
|
-
// The async, container-backed Kubernetes deploy lifecycle (slice 9's `deployJobClient` +
|
|
573
|
-
// `resolveDeployCloneTarget` seams). Node deploys on the workspace's self-hosted runner pool
|
|
574
|
-
// (which pulls the `imageDeploy` variant), so the default deploy client wraps the SAME
|
|
575
|
-
// `resolveTransport` the agent executor uses — the pool is Node's analogue of the Worker's
|
|
576
|
-
// DeployContainer. The clone-target resolver mints a short-lived install token + a github.com
|
|
577
|
-
// origin from the App registry. The local facade injects BOTH (a deploy-dedicated native/
|
|
578
|
-
// container transport + a PAT/GitLab clone target) via `options`, which win here. Absent any
|
|
579
|
-
// backend ⇒ unwired, so a render-needing config fails loudly (the raw REST path is unaffected).
|
|
580
|
-
const baseDeployMint = options.mintInstallationToken ??
|
|
581
|
-
(appRegistry ? (id) => appRegistry.installationToken(id) : undefined);
|
|
582
|
-
const deployJobClient = options.deployJobClient ??
|
|
583
|
-
(options.disableDefaultDeployJobClient || !resolveTransport
|
|
584
|
-
? undefined
|
|
585
|
-
: new RunnerJobClient(resolveTransport));
|
|
586
|
-
const resolveDeployCloneTarget = options.resolveDeployCloneTarget ??
|
|
587
|
-
(baseDeployMint
|
|
588
|
-
? makeResolveDeployCloneTarget(resolveRepoTarget, (id) => baseDeployMint(id), options.resolveRepoOrigin
|
|
589
|
-
? { resolveCloneUrl: (t) => options.resolveRepoOrigin(t).cloneUrl }
|
|
590
|
-
: {})
|
|
591
|
-
: undefined);
|
|
592
|
-
const deployDeps = config.environments.encryptionKey
|
|
593
|
-
? {
|
|
594
|
-
...(deployJobClient ? { deployJobClient } : {}),
|
|
595
|
-
...(resolveDeployCloneTarget ? { resolveDeployCloneTarget } : {}),
|
|
596
|
-
}
|
|
597
|
-
: {};
|
|
598
|
-
// Agent-context observability sink: records the complete, redacted context provided
|
|
599
|
-
// to each container agent (composed prompts + folded-in fragments + injected files).
|
|
600
|
-
// Gated by the deployment prompt-recording switch + the workspace storeAgentContext
|
|
601
|
-
// setting. Wired into the executor (write) AND createCore (read). The telemetry rows
|
|
602
|
-
// live in the `telemetry` Postgres schema (see schema.ts).
|
|
603
|
-
const agentContextObservability = new AgentContextObservabilityService({
|
|
604
|
-
agentContextSnapshotRepository: repos.agentContextSnapshotRepository,
|
|
605
|
-
workspaceSettingsRepository: repos.workspaceSettingsRepository,
|
|
606
|
-
idGenerator,
|
|
607
|
-
clock,
|
|
608
|
-
recordPrompts: config.observability.recordPrompts,
|
|
609
|
-
});
|
|
610
|
-
// Agent-search-query observability sink: records each web search a container agent
|
|
611
|
-
// performed through the search proxy. Same double gate + retention window as the
|
|
612
|
-
// agent-context sink. Wired into the search proxy (write, via the container) AND
|
|
613
|
-
// createCore (read). Telemetry rows live in the `telemetry` Postgres schema.
|
|
614
|
-
const searchQueryObservability = new SearchQueryObservabilityService({
|
|
615
|
-
agentSearchQueryRepository: repos.agentSearchQueryRepository,
|
|
616
|
-
workspaceSettingsRepository: repos.workspaceSettingsRepository,
|
|
617
|
-
idGenerator,
|
|
618
|
-
clock,
|
|
619
|
-
recordPrompts: config.observability.recordPrompts,
|
|
620
|
-
});
|
|
621
|
-
// Record a subscription harness's (Claude Code / Codex) per-call telemetry into the
|
|
622
|
-
// SAME `llm_call_metrics` store the LLM proxy writes for Pi — those harnesses bypass
|
|
623
|
-
// the proxy, so the executor lifts the metrics off the CLI stream and feeds them here.
|
|
624
|
-
const recordHarnessCalls = makeHarnessCallRecorder(new LlmObservabilityService({
|
|
625
|
-
llmCallMetricRepository: repos.llmCallMetricRepository,
|
|
626
|
-
idGenerator,
|
|
627
|
-
clock,
|
|
628
|
-
recordPrompts: config.observability.recordPrompts,
|
|
629
|
-
}));
|
|
630
|
-
// A deployment-wide trusted web-search upstream, built from this facade's own `WEB_SEARCH_*`
|
|
631
|
-
// env, used by the search proxy as a fallback when a run's account has no web-search config
|
|
632
|
-
// (local mode defaults `WEB_SEARCH_SEARXNG_URL` to its self-hosted SearXNG). Distinct from the
|
|
633
|
-
// harness's own `SEARXNG_URL`/`BRAVE_SEARCH_API_KEY` runner-pool autodetect — those are for
|
|
634
|
-
// self-hosted pool containers; these keys stay on the backend. Surfaced on the ServerContainer
|
|
635
|
-
// below and read by `WebSearchProxyController`.
|
|
636
|
-
const defaultWebSearchUpstream = createDefaultWebSearchUpstream({
|
|
637
|
-
braveApiKey: env.WEB_SEARCH_BRAVE_API_KEY,
|
|
638
|
-
searxngUrl: env.WEB_SEARCH_SEARXNG_URL,
|
|
639
|
-
searxngApiKey: env.WEB_SEARCH_SEARXNG_API_KEY,
|
|
640
|
-
});
|
|
641
|
-
// Web-search keys live per-account; advertise Pi's `web_search` tool to a run only when a
|
|
642
|
-
// usable upstream exists — either the deployment default above (⇒ always on) or the run's
|
|
643
|
-
// account has its own keys (else the tool would just fail/return nothing). The per-account
|
|
644
|
-
// check runs off a dedicated account-settings instance (short-TTL cache).
|
|
645
|
-
const webSearchAccountKey = env.ENCRYPTION_KEY?.trim();
|
|
646
|
-
const webSearchAccountSettings = webSearchAccountKey
|
|
647
|
-
? new AccountSettingsService({
|
|
648
|
-
accountSettingsRepository: repos.accountSettingsRepository,
|
|
649
|
-
secretCipher: new WebCryptoSecretCipher({
|
|
650
|
-
masterKeyBase64: webSearchAccountKey,
|
|
651
|
-
info: ACCOUNT_SETTINGS_CIPHER_INFO,
|
|
652
|
-
}),
|
|
653
|
-
clock,
|
|
654
|
-
...(options.caches ? { settingsCache: options.caches.accountSettings } : {}),
|
|
655
|
-
})
|
|
656
|
-
: undefined;
|
|
657
|
-
const resolveWebSearchAvailability = defaultWebSearchUpstream || webSearchAccountSettings
|
|
658
|
-
? async (workspaceId) => {
|
|
659
|
-
// Mirror the proxy's own resolution (`accountUpstream ?? defaultWebSearchUpstream`):
|
|
660
|
-
// the run's account keys WIN and the deployment default is only the fallback, so the
|
|
661
|
-
// surfaced provider matches the one that will actually serve the run's searches. Build
|
|
662
|
-
// the account upstream the SAME way the proxy does before falling back to the default.
|
|
663
|
-
if (webSearchAccountSettings) {
|
|
664
|
-
const accountId = await repos.workspaceRepository.accountOf(workspaceId);
|
|
665
|
-
if (accountId) {
|
|
666
|
-
const accountUpstream = createWebSearchUpstream((await webSearchAccountSettings.resolve(accountId)).webSearch ?? {});
|
|
667
|
-
if (accountUpstream)
|
|
668
|
-
return { available: true, provider: accountUpstream.provider };
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
if (defaultWebSearchUpstream)
|
|
672
|
-
return { available: true, provider: defaultWebSearchUpstream.provider };
|
|
673
|
-
return { available: false, provider: null };
|
|
674
|
-
}
|
|
675
|
-
: undefined;
|
|
676
|
-
// Private package registries (npm private orgs, GitHub Packages): sealed per-workspace
|
|
677
|
-
// entries decrypted only at container dispatch, rendered by the harness into ~/.npmrc.
|
|
678
|
-
// The cipher is shared by the dispatch resolver here and the management service below.
|
|
679
|
-
const packageRegistryEncryptionKey = env.ENCRYPTION_KEY?.trim();
|
|
680
|
-
const packageRegistrySecretCipher = packageRegistryEncryptionKey
|
|
681
|
-
? new WebCryptoSecretCipher({
|
|
682
|
-
masterKeyBase64: packageRegistryEncryptionKey,
|
|
683
|
-
info: PACKAGE_REGISTRY_CIPHER_INFO,
|
|
684
|
-
})
|
|
685
|
-
: undefined;
|
|
686
|
-
const resolvePackageRegistries = packageRegistrySecretCipher
|
|
687
|
-
? (workspaceId) => resolvePackageRegistriesForDispatch(repos.packageRegistryConnectionRepository, packageRegistrySecretCipher, workspaceId)
|
|
688
|
-
: undefined;
|
|
689
|
-
// Sensitive per-service test credentials (sealed): the service backs the CRUD controller, the
|
|
690
|
-
// engine's prompt refs (via `resolveTestSecretRefs`) and the executor's out-of-band value
|
|
691
|
-
// injection (via `resolveTestSecrets`). Guarded by ENCRYPTION_KEY like the other sealed stores.
|
|
692
|
-
const testSecretsEncryptionKey = env.ENCRYPTION_KEY?.trim();
|
|
693
|
-
const testSecretsService = testSecretsEncryptionKey
|
|
694
|
-
? new TestSecretsService({
|
|
695
|
-
testSecretsRepository: repos.testSecretsRepository,
|
|
696
|
-
secretCipher: new WebCryptoSecretCipher({
|
|
697
|
-
masterKeyBase64: testSecretsEncryptionKey,
|
|
698
|
-
info: TEST_SECRETS_CIPHER_INFO,
|
|
699
|
-
}),
|
|
700
|
-
blockRepository: repos.blockRepository,
|
|
701
|
-
clock,
|
|
702
|
-
})
|
|
703
|
-
: undefined;
|
|
704
|
-
const resolveTestSecrets = testSecretsService
|
|
705
|
-
? (workspaceId, blockId) => testSecretsService.resolveValuesForBlock(workspaceId, blockId)
|
|
706
|
-
: undefined;
|
|
707
|
-
const resolveTestSecretRefs = testSecretsService
|
|
708
|
-
? (workspaceId, blockId) => testSecretsService.resolveRefsForBlock(workspaceId, blockId)
|
|
709
|
-
: undefined;
|
|
710
|
-
// Modeled subscription quota-cycle provider (usage-and-quota-tracking, Part B): folds a
|
|
711
|
-
// finished subscription run's tokens into rolling windows (real reads land in B2). The
|
|
712
|
-
// registry of REAL vendor adapters is empty today, so every vendor reports modeled.
|
|
713
|
-
const subscriptionQuotaProvider = new RegistrySubscriptionQuotaProvider({
|
|
714
|
-
subscriptionQuotaCycleRepository: repos.subscriptionQuotaCycleRepository,
|
|
391
|
+
// The runner-transport resolver + the container-backed deploy lifecycle seams (resolve the
|
|
392
|
+
// workspace's transport, wrap it with the provisioning-log decorator, build the deploy job
|
|
393
|
+
// client + clone-target resolver), lifted into `container-transport-deps.ts` to keep this
|
|
394
|
+
// root within budget.
|
|
395
|
+
const { resolveTransport, baseDeployMint, deployDeps } = buildNodeTransportDeploy({
|
|
396
|
+
config,
|
|
397
|
+
repos,
|
|
715
398
|
idGenerator,
|
|
716
399
|
clock,
|
|
717
|
-
|
|
400
|
+
runnerPoolConnectionRepository,
|
|
401
|
+
runnerBackendRegistry,
|
|
402
|
+
appRegistry,
|
|
403
|
+
resolveRepoTarget,
|
|
404
|
+
workspaceRepository: repos.workspaceRepository,
|
|
405
|
+
resolveTransportOverride: options.resolveTransport,
|
|
406
|
+
runnerPoolProvider: options.runnerPoolProvider,
|
|
407
|
+
skipProvisioningLogWrap: options.skipProvisioningLogWrap,
|
|
408
|
+
mintInstallationToken: options.mintInstallationToken,
|
|
409
|
+
deployJobClientOverride: options.deployJobClient,
|
|
410
|
+
disableDefaultDeployJobClient: options.disableDefaultDeployJobClient,
|
|
411
|
+
resolveDeployCloneTargetOverride: options.resolveDeployCloneTarget,
|
|
412
|
+
resolveRepoOrigin: options.resolveRepoOrigin,
|
|
718
413
|
});
|
|
414
|
+
// The per-run agent-observability + web-search + sealed-secret services (agent-context /
|
|
415
|
+
// search-query / harness-call telemetry sinks, the web-search upstream + availability
|
|
416
|
+
// resolver, the package-registry + test-secret dispatch resolvers, the subscription-quota
|
|
417
|
+
// provider), lifted into `container-run-services-deps.ts` to keep this root within budget.
|
|
418
|
+
const { agentContextObservability, searchQueryObservability, recordHarnessCalls, defaultWebSearchUpstream, resolveWebSearchAvailability, packageRegistrySecretCipher, resolvePackageRegistries, testSecretsService, resolveTestSecrets, resolveTestSecretRefs, subscriptionQuotaProvider, } = buildNodeRunServices({ env, config, repos, idGenerator, clock, caches: options.caches });
|
|
719
419
|
const container = buildNodeContainerExecutor({
|
|
720
420
|
env,
|
|
721
421
|
config,
|
|
@@ -743,354 +443,73 @@ export function buildNodeContainer(options) {
|
|
|
743
443
|
// Optionally wrapped with the consensus mechanism below (after the event publisher
|
|
744
444
|
// is built, so live consensus pushes ride the same hub).
|
|
745
445
|
const standardAgentExecutor = new CompositeAgentExecutor(inline, container, agentKindRegistry);
|
|
746
|
-
// GitHub-
|
|
747
|
-
//
|
|
748
|
-
//
|
|
749
|
-
|
|
750
|
-
//
|
|
751
|
-
//
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
:
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
apiBase: config.github.apiBase,
|
|
769
|
-
})
|
|
770
|
-
: undefined);
|
|
771
|
-
// The client the engine's gate / merge / RepoFiles seams read through: the real GitHub client
|
|
772
|
-
// when present, else the GitLab-backed fallback so a GitLab-only deployment still gates on real
|
|
773
|
-
// CI and merges for real (the GitHub App wins when both are configured). Kept SEPARATE from
|
|
774
|
-
// `githubClient` on purpose — the GitHub-issue-specific consumers below (the GitHub Issues task
|
|
775
|
-
// source, issue writeback, the App projection module) must NOT be fed the GitLab client, or a
|
|
776
|
-
// GitLab-only deployment would offer a non-functional "GitHub Issues" source (it resolves the
|
|
777
|
-
// empty github_installations projection). Parity with the Worker, which keeps the App client
|
|
778
|
-
// distinct from its GitLab engine fallback.
|
|
779
|
-
const engineVcsClient = githubClient ?? gitlabEngineClient;
|
|
780
|
-
// Task-source integration (Jira + GitHub issues). Tenants connect their own Jira
|
|
781
|
-
// site through the UI (credentials stored per-workspace, encrypted at rest); the
|
|
782
|
-
// tracker resolves each workspace's own credentials from this same store. GitHub
|
|
783
|
-
// issues reuse the workspace's installed App, so they wire only when `githubClient`
|
|
784
|
-
// is available — kept here, after the client is built, for parity with the Worker.
|
|
785
|
-
const tasks = selectNodeTasksDeps(config, db, githubClient, githubInstallationRepository);
|
|
786
|
-
// Issue-tracker writeback (comment-on-PR-open + close-on-merge of a task's linked
|
|
787
|
-
// issue), gated per workspace + per task inside the provider. GitHub uses the same
|
|
788
|
-
// per-tenant client + installation lookup as the tracker/CI/merge providers; Jira
|
|
789
|
-
// reuses the workspace's encrypted connection. Wired whenever the tracker-settings
|
|
790
|
-
// repo exists (always on Node) so the engine can write back when a tracker is set.
|
|
791
|
-
const resolveWritebackIssue = githubClient
|
|
792
|
-
? async (workspaceId, externalId) => {
|
|
793
|
-
const parsed = githubIssuesLogic.parseGitHubIssueExternalId(externalId);
|
|
794
|
-
if (!parsed)
|
|
795
|
-
return null;
|
|
796
|
-
const installation = await githubInstallationRepository.getByWorkspace(workspaceId);
|
|
797
|
-
if (!installation)
|
|
798
|
-
return null;
|
|
799
|
-
return { installationId: installation.installationId, parsed };
|
|
800
|
-
}
|
|
801
|
-
: undefined;
|
|
802
|
-
const issueWritebackProvider = new IssueWritebackService({
|
|
446
|
+
// The GitHub-client-dependent slice of the composition root: the engine's GitHub client, the
|
|
447
|
+
// CI / mergeability / review / doc-quality gate-provider wiring (registered onto
|
|
448
|
+
// `providerRegistry` as a side effect — kept BEFORE `applyGateProviders` below), the task-source
|
|
449
|
+
// deps, issue writeback, and the GitHub gate + projection/sync module deps. Lifted into
|
|
450
|
+
// `container-github-deps.ts` (mirroring the Worker's `selectGitHubDeps`) so this composition root
|
|
451
|
+
// stays within the file-size budget — same reason `container-executor-deps.ts` exists.
|
|
452
|
+
const { githubClient, tasks, fileGitHubIssue, issueWritebackProvider, githubGateDeps, githubModuleDeps, } = selectNodeGitHubDeps({
|
|
453
|
+
config,
|
|
454
|
+
db,
|
|
455
|
+
remoteRepos,
|
|
456
|
+
sourced,
|
|
457
|
+
idGenerator,
|
|
458
|
+
clock,
|
|
459
|
+
appRegistry,
|
|
460
|
+
githubClientOverride: options.githubClient,
|
|
461
|
+
resolveUserGitHubToken,
|
|
462
|
+
gitlabEngineClient,
|
|
463
|
+
providerRegistry,
|
|
464
|
+
resolveRepoTarget,
|
|
465
|
+
githubInstallationRepository,
|
|
466
|
+
repoProjectionRepository,
|
|
467
|
+
blockRepository: repos.blockRepository,
|
|
803
468
|
trackerSettingsRepository: repos.trackerSettingsRepository,
|
|
804
|
-
|
|
805
|
-
fetchImpl: fetch,
|
|
806
|
-
...(githubClient && resolveWritebackIssue
|
|
807
|
-
? {
|
|
808
|
-
commentOnGitHubIssue: async (workspaceId, externalId, body) => {
|
|
809
|
-
const target = await resolveWritebackIssue(workspaceId, externalId);
|
|
810
|
-
if (!target)
|
|
811
|
-
return;
|
|
812
|
-
await githubClient.comment(target.installationId, { owner: target.parsed.owner, repo: target.parsed.repo }, target.parsed.number, body);
|
|
813
|
-
},
|
|
814
|
-
closeGitHubIssue: async (workspaceId, externalId) => {
|
|
815
|
-
const target = await resolveWritebackIssue(workspaceId, externalId);
|
|
816
|
-
if (!target)
|
|
817
|
-
return;
|
|
818
|
-
await githubClient.closeIssue(target.installationId, { owner: target.parsed.owner, repo: target.parsed.repo }, target.parsed.number);
|
|
819
|
-
},
|
|
820
|
-
labelGitHubIssue: async (workspaceId, externalId, label) => {
|
|
821
|
-
const target = await resolveWritebackIssue(workspaceId, externalId);
|
|
822
|
-
if (!target)
|
|
823
|
-
return;
|
|
824
|
-
await githubClient.applyIssueLabel?.(target.installationId, { owner: target.parsed.owner, repo: target.parsed.repo }, target.parsed.number, label);
|
|
825
|
-
},
|
|
826
|
-
}
|
|
827
|
-
: {}),
|
|
828
|
-
...(tasks.taskConnectionRepository
|
|
829
|
-
? {
|
|
830
|
-
resolveJiraConnection: async (workspaceId) => {
|
|
831
|
-
const connection = await tasks.taskConnectionRepository.getByWorkspace(workspaceId, 'jira');
|
|
832
|
-
const { baseUrl, accountEmail, apiToken } = connection?.credentials ?? {};
|
|
833
|
-
if (!baseUrl || !accountEmail || !apiToken)
|
|
834
|
-
return null;
|
|
835
|
-
return { baseUrl, accountEmail, apiToken };
|
|
836
|
-
},
|
|
837
|
-
resolveLinearConnection: async (workspaceId) => {
|
|
838
|
-
const connection = await tasks.taskConnectionRepository.getByWorkspace(workspaceId, 'linear');
|
|
839
|
-
const { apiKey, token } = connection?.credentials ?? {};
|
|
840
|
-
return apiKey || token ? { apiKey, token } : null;
|
|
841
|
-
},
|
|
842
|
-
}
|
|
843
|
-
: {}),
|
|
469
|
+
caches: options.caches,
|
|
844
470
|
});
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
// the gate suite instead of onto the engine's CoreDependencies (single-process startup, so
|
|
849
|
-
// the deployment-global handles are set once here). Parity with the Worker's selectGitHubDeps.
|
|
850
|
-
// These read through `engineVcsClient` (GitHub App or the GitLab fallback), so a GitLab-only
|
|
851
|
-
// deployment gates + merges for real too.
|
|
852
|
-
wireCiStatusProvider(providerRegistry, new GitHubCiStatusProvider({
|
|
853
|
-
githubClient: engineVcsClient,
|
|
854
|
-
resolveRepoTarget,
|
|
855
|
-
blockRepository: repos.blockRepository,
|
|
856
|
-
}));
|
|
857
|
-
wireMergeabilityProvider(providerRegistry, new GitHubMergeabilityProvider({
|
|
858
|
-
githubClient: engineVcsClient,
|
|
859
|
-
resolveRepoTarget,
|
|
860
|
-
blockRepository: repos.blockRepository,
|
|
861
|
-
}));
|
|
862
|
-
wirePullRequestReviewProvider(providerRegistry, new GitHubPullRequestReviewProvider({
|
|
863
|
-
githubClient: engineVcsClient,
|
|
864
|
-
resolveRepoTarget,
|
|
865
|
-
blockRepository: repos.blockRepository,
|
|
866
|
-
}));
|
|
867
|
-
wireDocQualityProvider(providerRegistry, new GitHubDocQualityProvider({
|
|
868
|
-
githubClient: engineVcsClient,
|
|
869
|
-
resolveRepoTarget,
|
|
870
|
-
blockRepository: repos.blockRepository,
|
|
871
|
-
// The gate resolves a workspace-linked template (WS1) for the block's kind, so it checks
|
|
872
|
-
// against the SAME sections the doc-writer followed. In db-less mothership mode the writer
|
|
873
|
-
// resolves the template through the RPC-proxied documents repo (getRoleLink is run-path
|
|
874
|
-
// allow-listed), so the gate MUST use that same repo — not `undefined` — or a doc written
|
|
875
|
-
// to the workspace template would be graded against the built-in skeleton (writer/gate drift).
|
|
876
|
-
documentRepository: db
|
|
877
|
-
? new DrizzleDocumentRepository(db)
|
|
878
|
-
: remoteRepos?.documentRepository,
|
|
879
|
-
}));
|
|
880
|
-
githubGateDeps = {
|
|
881
|
-
// The engine binds a registered custom kind's pre/post-op hooks to a run's repo
|
|
882
|
-
// via this checkout-free RepoFiles resolver, composed from the same client +
|
|
883
|
-
// repo-target walk the gates/merger use — parity with the Worker. The `repoFiles`
|
|
884
|
-
// cache (slice 4) makes the post-op idempotency re-reads a read-through hit.
|
|
885
|
-
resolveRunRepoContext: makeResolveRunRepoContext(engineVcsClient, resolveRepoTarget, options.caches?.repoFiles),
|
|
886
|
-
// Block-less repo resolver for the environments module's on-demand repo
|
|
887
|
-
// validation / config bootstrap (operator names owner+repo).
|
|
888
|
-
resolveRepoFilesForCoords: makeResolveRepoFilesForCoords(engineVcsClient, githubInstallationRepository, repoProjectionRepository),
|
|
889
|
-
branchUpdater: new GitHubBranchUpdater({
|
|
890
|
-
githubClient: engineVcsClient,
|
|
891
|
-
resolveRepoTarget,
|
|
892
|
-
blockRepository: repos.blockRepository,
|
|
893
|
-
}),
|
|
894
|
-
pullRequestMerger: new GitHubPullRequestMerger({
|
|
895
|
-
githubClient: engineVcsClient,
|
|
896
|
-
resolveRepoTarget,
|
|
897
|
-
blockRepository: repos.blockRepository,
|
|
898
|
-
}),
|
|
899
|
-
};
|
|
900
|
-
}
|
|
901
|
-
// GitHub installation + projections + sync/webhook module: wired when the App is
|
|
902
|
-
// configured (a real githubClient), mirroring the Worker's selectGitHubDeps. This
|
|
903
|
-
// turns the GitHub read endpoints + the inline webhook/backfill sync on for Node —
|
|
904
|
-
// the sync engine (GitHubSyncService) is runtime-neutral, so populating the
|
|
905
|
-
// projection repos here makes the inline ingest actually persist (parity with the
|
|
906
|
-
// Worker, which fans the same sync through a queue/Workflow). `canCreateRepos` /
|
|
907
|
-
// `workflowsGranted` come from the App registry when present (advisory).
|
|
908
|
-
const githubModuleDeps = config.github.enabled && githubClient
|
|
909
|
-
? {
|
|
910
|
-
githubClient,
|
|
911
|
-
githubInstallationRepository,
|
|
912
|
-
repoProjectionRepository,
|
|
913
|
-
// The five GitHub projection repos share one shape (remote in mothership mode, else
|
|
914
|
-
// Drizzle over `db`), routed through the shared `sourced` helper.
|
|
915
|
-
branchProjectionRepository: sourced('branchProjectionRepository', (d) => new DrizzleBranchProjectionRepository(d)),
|
|
916
|
-
pullRequestProjectionRepository: sourced('pullRequestProjectionRepository', (d) => new DrizzlePullRequestProjectionRepository(d)),
|
|
917
|
-
issueProjectionRepository: sourced('issueProjectionRepository', (d) => new DrizzleIssueProjectionRepository(d)),
|
|
918
|
-
commitProjectionRepository: sourced('commitProjectionRepository', (d) => new DrizzleCommitProjectionRepository(d)),
|
|
919
|
-
checkRunProjectionRepository: sourced('checkRunProjectionRepository', (d) => new DrizzleCheckRunProjectionRepository(d)),
|
|
920
|
-
// Per-user PAT-reachable repo projection (picker expansion + redaction); Postgres-only,
|
|
921
|
-
// so absent in a no-DB mothership node (the picker keeps its App-only behaviour there).
|
|
922
|
-
userRepoAccessRepository: db ? new DrizzleUserRepoAccessRepository(db) : undefined,
|
|
923
|
-
webhookVerifier: new WebCryptoWebhookVerifier(config.github.webhookSecret),
|
|
924
|
-
// Bound the initial backfill to the commit retention horizon (0 = full).
|
|
925
|
-
commitBackfillHorizonMs: config.retention.commitMs || undefined,
|
|
926
|
-
...(appRegistry
|
|
927
|
-
? {
|
|
928
|
-
// Privileged App tier (ADR 0005): when configured, its client backs the
|
|
929
|
-
// create-repo endpoint; `canCreateRepos` flags a connection whose
|
|
930
|
-
// installation is owned by the privileged App. Absent → repo creation
|
|
931
|
-
// stays the manual flow (parity with the Worker's selectGitHubDeps).
|
|
932
|
-
repoProvisioningClient: config.github.privilegedApp
|
|
933
|
-
? new FetchGitHubProvisioningClient({
|
|
934
|
-
registry: appRegistry,
|
|
935
|
-
apiBase: config.github.apiBase,
|
|
936
|
-
})
|
|
937
|
-
: undefined,
|
|
938
|
-
canCreateRepos: (installation) => appRegistry.canCreateRepos(installation),
|
|
939
|
-
workflowsGranted: async (installation) => {
|
|
940
|
-
const perms = await appRegistry.installationPermissions(installation.installationId);
|
|
941
|
-
return perms.workflows === 'write';
|
|
942
|
-
},
|
|
943
|
-
}
|
|
944
|
-
: {}),
|
|
945
|
-
}
|
|
946
|
-
: {};
|
|
947
|
-
// Repo-bootstrap: the reference-architecture library + the bootstrap runs (stored as
|
|
948
|
-
// kind='bootstrap' rows of agent_runs). The repos are wired unconditionally (the
|
|
949
|
-
// module + ref-arch CRUD then work like the Worker); the container-dispatching
|
|
950
|
-
// `repoBootstrapper` wires only when its prerequisites are met (transport + proxy +
|
|
951
|
-
// token + GitHub client) — the same token source the container executor uses.
|
|
952
|
-
const bootstrapJobRepository = sourced('bootstrapJobRepository', (d) => new DrizzleBootstrapJobRepository(d));
|
|
953
|
-
const bootstrapMintInstallationToken = options.mintInstallationToken ??
|
|
954
|
-
(appRegistry ? (id) => appRegistry.installationToken(id) : undefined);
|
|
955
|
-
const repoBootstrapper = selectNodeRepoBootstrapper({
|
|
471
|
+
// Repo-bootstrap: the reference-architecture library + the container-dispatching
|
|
472
|
+
// `repoBootstrapper`, lifted into `container-transport-deps.ts` to keep this root within budget.
|
|
473
|
+
const { bootstrapJobRepository, bootstrapMintInstallationToken, repoBootstrapper } = buildNodeBootstrapper({
|
|
956
474
|
env,
|
|
957
475
|
config,
|
|
476
|
+
sourced,
|
|
958
477
|
resolveTransport,
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
...(options.caches?.repoProjection
|
|
963
|
-
? { repoProjectionCache: options.caches.repoProjection }
|
|
964
|
-
: {}),
|
|
478
|
+
githubInstallationRepository,
|
|
479
|
+
repoProjectionRepository,
|
|
480
|
+
appRegistry,
|
|
965
481
|
githubClient,
|
|
966
|
-
mintInstallationToken:
|
|
967
|
-
|
|
482
|
+
mintInstallationToken: options.mintInstallationToken,
|
|
483
|
+
resolvePackageRegistries,
|
|
484
|
+
caches: options.caches,
|
|
968
485
|
});
|
|
969
|
-
// Real-time
|
|
970
|
-
//
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
//
|
|
983
|
-
//
|
|
984
|
-
//
|
|
985
|
-
const
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
agentRouting: config.agents.routing,
|
|
991
|
-
resolveBlockModel: config.agents.resolveBlockModel,
|
|
992
|
-
resolveWorkspaceModelDefault,
|
|
993
|
-
// Consensus runs its participants INLINE, so in local mode keep an ambient-eligible
|
|
994
|
-
// subscription harness ref (served via the CLI) instead of degrading it; undefined on
|
|
995
|
-
// stock Node/Worker, where such a ref degrades to the routing default as before.
|
|
996
|
-
...(config.agents.inlineHarnessRef ? { runsInline: config.agents.inlineHarnessRef } : {}),
|
|
997
|
-
sessionRepository: repos.consensusSessionRepository,
|
|
998
|
-
...(executionEventPublisher ? { eventPublisher: executionEventPublisher } : {}),
|
|
999
|
-
agentKindRegistry,
|
|
1000
|
-
}))
|
|
1001
|
-
: standardAgentExecutor;
|
|
1002
|
-
const notificationChannels = [];
|
|
1003
|
-
if (executionEventPublisher)
|
|
1004
|
-
notificationChannels.push(new InAppNotificationChannel(executionEventPublisher));
|
|
1005
|
-
if (slackDeps.notificationChannel)
|
|
1006
|
-
notificationChannels.push(slackDeps.notificationChannel);
|
|
1007
|
-
const notificationChannel = notificationChannels.length === 0
|
|
1008
|
-
? undefined
|
|
1009
|
-
: notificationChannels.length === 1
|
|
1010
|
-
? notificationChannels[0]
|
|
1011
|
-
: new CompositeNotificationChannel(notificationChannels);
|
|
1012
|
-
// Observability post-release-health: wire the gate + the release-health settings module
|
|
1013
|
-
// when enabled (+ ENCRYPTION_KEY), mirroring the Worker's `selectReleaseHealthDeps`. Off →
|
|
1014
|
-
// the `post-release-health` gate is a pass-through and the module isn't assembled.
|
|
1015
|
-
const releaseHealthDeps = {};
|
|
1016
|
-
if (config.releaseHealth.enabled && config.releaseHealth.encryptionKey) {
|
|
1017
|
-
const observabilitySecretCipher = new WebCryptoSecretCipher({
|
|
1018
|
-
masterKeyBase64: config.releaseHealth.encryptionKey,
|
|
1019
|
-
info: OBSERVABILITY_CIPHER_INFO,
|
|
1020
|
-
});
|
|
1021
|
-
releaseHealthDeps.observabilityConnectionRepository = repos.observabilityConnectionRepository;
|
|
1022
|
-
releaseHealthDeps.releaseHealthConfigRepository = repos.releaseHealthConfigRepository;
|
|
1023
|
-
releaseHealthDeps.observabilitySecretCipher = observabilitySecretCipher;
|
|
1024
|
-
// The post-release-health gate + on-call escalation now live in `@cat-factory/gates`; wire
|
|
1025
|
-
// their providers into the gate suite. The observability repos/cipher above stay on
|
|
1026
|
-
// CoreDependencies — they power the management API (ReleaseHealthService), not the gate.
|
|
1027
|
-
wireReleaseHealthProvider(providerRegistry, new RegistryReleaseHealthProvider({
|
|
1028
|
-
observabilityConnectionRepository: repos.observabilityConnectionRepository,
|
|
1029
|
-
releaseHealthConfigRepository: repos.releaseHealthConfigRepository,
|
|
1030
|
-
blockRepository: repos.blockRepository,
|
|
1031
|
-
secretCipher: observabilitySecretCipher,
|
|
1032
|
-
registry: defaultObservabilityRegistry,
|
|
1033
|
-
}));
|
|
1034
|
-
}
|
|
1035
|
-
// Per-workspace incident-enrichment (PagerDuty + incident.io): credentials moved out of
|
|
1036
|
-
// env into a sealed per-workspace row, resolved + decrypted at enrichment time. Wired
|
|
1037
|
-
// whenever the shared ENCRYPTION_KEY is present (independent of the release-health gate).
|
|
1038
|
-
const encryptionKey = env.ENCRYPTION_KEY?.trim();
|
|
1039
|
-
// Per-workspace private package registries (npm private orgs, GitHub Packages): the
|
|
1040
|
-
// management API over the same repo + cipher the dispatch resolver above rides.
|
|
1041
|
-
const packageRegistryDeps = packageRegistrySecretCipher
|
|
1042
|
-
? {
|
|
1043
|
-
packageRegistryConnectionRepository: repos.packageRegistryConnectionRepository,
|
|
1044
|
-
packageRegistrySecretCipher,
|
|
1045
|
-
}
|
|
1046
|
-
: {};
|
|
1047
|
-
const incidentEnrichmentDeps = {};
|
|
1048
|
-
if (encryptionKey) {
|
|
1049
|
-
const incidentEnrichmentSecretCipher = new WebCryptoSecretCipher({
|
|
1050
|
-
masterKeyBase64: encryptionKey,
|
|
1051
|
-
info: INCIDENT_ENRICHMENT_CIPHER_INFO,
|
|
1052
|
-
});
|
|
1053
|
-
incidentEnrichmentDeps.incidentEnrichmentConnectionRepository =
|
|
1054
|
-
repos.incidentEnrichmentConnectionRepository;
|
|
1055
|
-
incidentEnrichmentDeps.incidentEnrichmentSecretCipher = incidentEnrichmentSecretCipher;
|
|
1056
|
-
// The on-call enrichment provider now lives in `@cat-factory/gates`; wire the
|
|
1057
|
-
// workspace-backed provider into the gate suite. The connection repo + cipher above
|
|
1058
|
-
// stay on CoreDependencies to power the management API.
|
|
1059
|
-
wireIncidentEnrichment(providerRegistry, new WorkspaceIncidentEnrichmentProvider({
|
|
1060
|
-
incidentEnrichmentConnectionRepository: repos.incidentEnrichmentConnectionRepository,
|
|
1061
|
-
secretCipher: incidentEnrichmentSecretCipher,
|
|
1062
|
-
}));
|
|
1063
|
-
}
|
|
1064
|
-
// Per-account deployment settings (Slack OAuth + web-search keys + content-storage), built
|
|
1065
|
-
// once so the service's short-TTL cache spans requests; the Slack OAuth + content-storage
|
|
1066
|
-
// resolvers derive from it.
|
|
1067
|
-
const accountSettings = encryptionKey
|
|
1068
|
-
? new AccountSettingsService({
|
|
1069
|
-
accountSettingsRepository: repos.accountSettingsRepository,
|
|
1070
|
-
secretCipher: new WebCryptoSecretCipher({
|
|
1071
|
-
masterKeyBase64: encryptionKey,
|
|
1072
|
-
info: ACCOUNT_SETTINGS_CIPHER_INFO,
|
|
1073
|
-
}),
|
|
1074
|
-
clock,
|
|
1075
|
-
contentStorageCapability,
|
|
1076
|
-
...(options.caches ? { settingsCache: options.caches.accountSettings } : {}),
|
|
1077
|
-
})
|
|
1078
|
-
: undefined;
|
|
1079
|
-
// Resolve the binary-artifact store for a workspace's account from its content-storage
|
|
1080
|
-
// settings (the blob backend is per-account; the metadata is the shared Postgres store).
|
|
1081
|
-
// Without `accountSettings` (no encryption key) there is no per-account override, so every
|
|
1082
|
-
// workspace falls back to the runtime default — which on Node is `off`, so the resolver then
|
|
1083
|
-
// returns null and the controllers 503 / the gate passes through. Caches per account, so a
|
|
1084
|
-
// backend switch rebuilds and the many workspaces under one account share a store.
|
|
1085
|
-
const resolveBinaryArtifactStore = makeResolveBinaryArtifactStore({
|
|
1086
|
-
accountSettings,
|
|
1087
|
-
accountOf: (workspaceId) => repos.workspaceRepository.accountOf(workspaceId),
|
|
1088
|
-
metadata: repos.binaryArtifactMetadataStore,
|
|
486
|
+
// Real-time event publisher + notification channel + optional consensus wrap, lifted into
|
|
487
|
+
// `container-realtime-deps.ts` to keep this root within the file-size budget.
|
|
488
|
+
const { slackDeps, executionEventPublisher, agentExecutor, notificationChannel } = buildNodeRealtimeDeps({
|
|
489
|
+
env,
|
|
490
|
+
config,
|
|
491
|
+
repos,
|
|
492
|
+
sourced,
|
|
493
|
+
realtimeSink: options.realtimeSink,
|
|
494
|
+
standardAgentExecutor,
|
|
495
|
+
modelProviderResolver,
|
|
496
|
+
resolveWorkspaceModelDefault,
|
|
497
|
+
agentKindRegistry,
|
|
498
|
+
});
|
|
499
|
+
// Per-account settings + binary-artifact storage + the observability/incident gate-provider
|
|
500
|
+
// wiring (onto `providerRegistry`, before `applyGateProviders` below), plus the package-registry
|
|
501
|
+
// management deps, lifted into `container-account-deps.ts` to keep this root within budget.
|
|
502
|
+
const { releaseHealthDeps, packageRegistryDeps, incidentEnrichmentDeps, accountSettings, resolveBinaryArtifactStore, } = buildNodeAccountDeps({
|
|
503
|
+
env,
|
|
504
|
+
config,
|
|
505
|
+
db,
|
|
506
|
+
repos,
|
|
1089
507
|
idGenerator,
|
|
1090
508
|
clock,
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
509
|
+
providerRegistry,
|
|
510
|
+
packageRegistrySecretCipher,
|
|
511
|
+
contentStorageDefaultBackend: options.contentStorageDefaultBackend,
|
|
512
|
+
caches: options.caches,
|
|
1094
513
|
});
|
|
1095
514
|
// Runner-pool URL/host guard, scoped to its own config (independent of the environment
|
|
1096
515
|
// allow-list); absent => strict public-https.
|
|
@@ -1657,37 +1076,6 @@ export function buildNodeContainer(options) {
|
|
|
1657
1076
|
* No registered providers → `{ deps: {} }` and both the tasks module and the Jira
|
|
1658
1077
|
* tracker stay off (the encryption key is guaranteed present by `loadTasksConfig`).
|
|
1659
1078
|
*/
|
|
1660
|
-
function selectNodeTasksDeps(config, db, githubClient, installations) {
|
|
1661
|
-
if (!config.tasks.enabled || !config.tasks.encryptionKey)
|
|
1662
|
-
return { deps: {} };
|
|
1663
|
-
// Jira and Linear are always registered (their credentials are per-workspace, entered in the UI).
|
|
1664
|
-
const providers = [new JiraProvider(), new LinearTaskProvider()];
|
|
1665
|
-
// GitHub Issues reuse the workspace's installed GitHub App, so this provider is
|
|
1666
|
-
// wired whenever a GitHub client is available (the App is configured) — it has no
|
|
1667
|
-
// credentials of its own and resolves the installation per issue. Mirrors the
|
|
1668
|
-
// Cloudflare facade's `config.github.enabled` gate (see CLAUDE.md parity rule).
|
|
1669
|
-
// Whether a workspace OFFERS a source is the per-workspace toggle
|
|
1670
|
-
// (task_source_settings), not a deployment env gate.
|
|
1671
|
-
if (githubClient) {
|
|
1672
|
-
providers.push(new GitHubIssuesProvider({ githubClient, installations }));
|
|
1673
|
-
}
|
|
1674
|
-
const taskConnectionRepository = new DrizzleTaskConnectionRepository(db,
|
|
1675
|
-
// Source credentials are encrypted at rest under a tasks-scoped HKDF info (the
|
|
1676
|
-
// same domain the Cloudflare facade uses), keyed by the shared ENCRYPTION_KEY.
|
|
1677
|
-
new WebCryptoSecretCipher({
|
|
1678
|
-
masterKeyBase64: config.tasks.encryptionKey,
|
|
1679
|
-
info: 'cat-factory:tasks',
|
|
1680
|
-
}));
|
|
1681
|
-
return {
|
|
1682
|
-
deps: {
|
|
1683
|
-
taskSourceProviders: providers,
|
|
1684
|
-
taskConnectionRepository,
|
|
1685
|
-
taskSourceSettingsRepository: new DrizzleTaskSourceSettingsRepository(db),
|
|
1686
|
-
taskRepository: new DrizzleTaskRepository(db),
|
|
1687
|
-
},
|
|
1688
|
-
taskConnectionRepository,
|
|
1689
|
-
};
|
|
1690
|
-
}
|
|
1691
1079
|
/**
|
|
1692
1080
|
* Wire the document-source integration for the Node facade, mirroring the Worker's
|
|
1693
1081
|
* `selectDocumentsDeps`: the shared `@cat-factory/integrations` provider shells
|