@cat-factory/worker 0.128.0 → 0.128.2

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.
@@ -1,7 +1,6 @@
1
- import { CompositeNotificationChannel, composeTraceSinks, NoopWorkRunner, } from '@cat-factory/kernel';
2
- import { AiAgentExecutor, createTierInstallationResolvers, inlineWebSearchOptionsFromEnv, resolveAgentConfig, isProxyableProvider, vendorConcurrencyLimiterFromEnv, } from '@cat-factory/agents';
3
- import { cloudflareBindingRegistry } from '@cat-factory/provider-cloudflare';
4
- import { ConfluenceProvider, FigmaProvider, ZeplinProvider, GitHubDocsProvider, GitHubIssuesProvider, JiraProvider, LinearDocumentProvider, LinearTaskProvider, NotionProvider, EMAIL_CIPHER_INFO, PersonalSubscriptionService, ProviderSubscriptionService, RunnerPoolConnectionService, ProvisioningLogRecorder, LoggingRunnerTransport, SLACK_CIPHER_INFO, NOTIFICATION_WEBHOOK_CIPHER_INFO, SlackNotificationChannel, buildNotificationWebhookSupport, OBSERVABILITY_CIPHER_INFO, RegistryReleaseHealthProvider, defaultObservabilityRegistry, RegistrySubscriptionQuotaProvider, defaultSubscriptionQuotaRegistry, WorkspaceIncidentEnrichmentProvider, INCIDENT_ENRICHMENT_CIPHER_INFO, AccountSettingsService, ACCOUNT_SETTINGS_CIPHER_INFO, createEmailSender, } from '@cat-factory/integrations';
1
+ import { CompositeNotificationChannel, NoopWorkRunner, } from '@cat-factory/kernel';
2
+ import { createTierInstallationResolvers, resolveAgentConfig, isProxyableProvider, } from '@cat-factory/agents';
3
+ import { ConfluenceProvider, FigmaProvider, ZeplinProvider, GitHubDocsProvider, GitHubIssuesProvider, JiraProvider, LinearDocumentProvider, LinearTaskProvider, NotionProvider, EMAIL_CIPHER_INFO, ProvisioningLogRecorder, SLACK_CIPHER_INFO, NOTIFICATION_WEBHOOK_CIPHER_INFO, SlackNotificationChannel, buildNotificationWebhookSupport, OBSERVABILITY_CIPHER_INFO, RegistryReleaseHealthProvider, defaultObservabilityRegistry, WorkspaceIncidentEnrichmentProvider, INCIDENT_ENRICHMENT_CIPHER_INFO, AccountSettingsService, ACCOUNT_SETTINGS_CIPHER_INFO, createEmailSender, } from '@cat-factory/integrations';
5
4
  // Opt-in AWS EKS backends (runner + environment), registered by reference on BOTH facades so
6
5
  // the runtimes stay symmetric with the native `kubernetes` backend they extend (which likewise
7
6
  // rides both). They are pass-throughs until a workspace connects an `eks` backend, and carry NO
@@ -10,35 +9,34 @@ import { ConfluenceProvider, FigmaProvider, ZeplinProvider, GitHubDocsProvider,
10
9
  // custom CA (Node/local) — exactly like a private-CA `kubernetes` connection — so on the Worker
11
10
  // the kind is offered but a connection to such a cluster fails TLS at run time, not silently.
12
11
  import { resolveWorkerRegistries } from './container-registries.js';
13
- import { buildLangfuseSink, buildOtelSink, buildTraceSink, selectTraceSink, } from './container-trace-sinks.js';
12
+ import { selectTraceSink } from './container-trace-sinks.js';
14
13
  export { selectTraceSink };
15
14
  import { assembleWorkerContainer } from './container-assembly.js';
16
- import { AgentContextObservabilityService, SearchQueryObservabilityService, PACKAGE_REGISTRY_CIPHER_INFO, resolvePackageRegistriesForDispatch, LlmObservabilityService, makeHarnessCallRecorder, resolvePresetModelForKind, } from '@cat-factory/orchestration';
15
+ import { buildAppRegistry, buildResolveRepoTarget } from './container-vcs-identity.js';
16
+ // The App registry + repo-target resolvers moved to `container-vcs-identity.ts`; re-exported so
17
+ // the sibling modules that already read them off the composition root keep one import surface.
18
+ export { buildAppRegistry, buildResolveRepoTarget };
19
+ import { buildModelProviderResolver } from './container-model-resolver.js';
20
+ import { buildDefaultWebSearchUpstream, buildResolveTransport, maybeWrapConsensus, selectAgentExecutor, } from './container-executor-deps.js';
21
+ // The executor wiring moved to `container-executor-deps.ts`; re-exported so
22
+ // `container-assembly.ts` keeps importing the composition root's public surface from one place.
23
+ export { maybeWrapConsensus, selectAgentExecutor };
24
+ import { AgentContextObservabilityService, SearchQueryObservabilityService, PACKAGE_REGISTRY_CIPHER_INFO, resolvePackageRegistriesForDispatch, } from '@cat-factory/orchestration';
17
25
  import { ISOLATE_SAFE_APP_CACHES_PROFILE, createAppCaches } from '@cat-factory/caching';
18
- import { buildResolveRepoTarget as buildSharedResolveRepoTarget, buildResolveRepoTargets as buildSharedResolveRepoTargets, ContainerEnvConfigRepairer, makeResolveDeployCloneTarget, RunnerJobClient, makeResolveBinaryArtifactStore, ensureWorkBranchViaRest, FanOutEventPublisher, InAppNotificationChannel, PatPreferringAppRegistry, logger, buildInfrastructureCapabilities, createDefaultWebSearchUpstream, createWebSearchUpstream, createInlineInstrumentation, createScopedModelProviderResolver, wrapResolverWithTelemetry, ENV_HELP, configProblem, GitHubIdentityResolver, resolveUrlSafetyPolicy, noRunnerBackendAvailableError, } from '@cat-factory/server';
26
+ import { ContainerEnvConfigRepairer, makeResolveDeployCloneTarget, RunnerJobClient, makeResolveBinaryArtifactStore, FanOutEventPublisher, InAppNotificationChannel, PatPreferringAppRegistry, logger, buildInfrastructureCapabilities, GitHubIdentityResolver, resolveUrlSafetyPolicy, } from '@cat-factory/server';
19
27
  import { loadConfig } from './config';
20
- import { loadLangfuseConfig } from './config/langfuse';
21
- import { loadObservabilityConfig } from './config/observability';
22
- import { loadOtelConfig } from './config/otel';
23
28
  import { requireDb, requireTelemetryDb } from './env';
24
- import { baseUrlFor } from './ai/providerEndpoints';
25
- import { resolveExtraRegistries } from './ai/registries';
26
29
  import { CfGitHubWebhookIngest } from './gateways/GitHubGateways';
27
- import { ContainerAgentExecutor, } from './ai/ContainerAgentExecutor';
30
+ import {} from './ai/ContainerAgentExecutor';
28
31
  import { CloudflareContainerTransport } from './containers/CloudflareContainerTransport';
29
- import { ContainerInstanceRegistry } from './containers/ContainerInstanceRegistry';
30
- import { D1LiveContainerRepository } from './repositories/D1LiveContainerRepository';
31
32
  import { HttpRunnerPoolProvider } from './runners/HttpRunnerPoolProvider';
32
33
  import { D1RunnerPoolConnectionRepository } from './repositories/D1RunnerPoolConnectionRepository';
33
34
  import { ContainerRepoBootstrapper } from './ai/ContainerRepoBootstrapper';
34
- import { CompositeAgentExecutor } from './ai/CompositeAgentExecutor';
35
35
  import { ContainerSessionService } from './containers/ContainerSessionService';
36
36
  import { DurableObjectEventPublisher } from './events/DurableObjectEventPublisher';
37
37
  import { WorkflowsWorkRunner } from './workflows/WorkflowsWorkRunner';
38
38
  import { D1BlockRepository } from './repositories/D1BlockRepository';
39
- import { D1ServiceRepository } from './repositories/D1ServiceRepository';
40
39
  import { D1WorkspaceMountRepository } from './repositories/D1WorkspaceMountRepository';
41
- import { D1LlmCallMetricRepository } from './repositories/D1LlmCallMetricRepository';
42
40
  import { D1AgentContextSnapshotRepository } from './repositories/D1AgentContextSnapshotRepository';
43
41
  import { D1AgentSearchQueryRepository } from './repositories/D1AgentSearchQueryRepository';
44
42
  import { D1ProvisioningLogRepository } from './repositories/D1ProvisioningLogRepository';
@@ -62,8 +60,6 @@ import { D1RequirementReviewRepository } from './repositories/D1RequirementRevie
62
60
  import { D1DocInterviewRepository } from './repositories/D1DocInterviewRepository';
63
61
  import { D1KaizenGradingRepository } from './repositories/D1KaizenGradingRepository';
64
62
  import { D1KaizenVerifiedComboRepository } from './repositories/D1KaizenVerifiedComboRepository';
65
- import { D1ConsensusSessionRepository } from './repositories/D1ConsensusSessionRepository';
66
- import { ConsensusAgentExecutor, registerConsensusTraits } from '@cat-factory/consensus';
67
63
  import { D1ConsensusGroupRepository } from './repositories/D1ConsensusGroupRepository';
68
64
  import { D1ClarityReviewRepository } from './repositories/D1ClarityReviewRepository';
69
65
  import { D1BrainstormSessionRepository } from './repositories/D1BrainstormSessionRepository';
@@ -76,7 +72,6 @@ import { D1SandboxPromptVersionRepository, D1SandboxFixtureRepository, D1Sandbox
76
72
  import { D1WorkspaceSettingsRepository } from './repositories/D1WorkspaceSettingsRepository';
77
73
  import { D1UserSettingsRepository } from './repositories/D1UserSettingsRepository';
78
74
  import { D1ObservabilityConnectionRepository } from './repositories/D1ObservabilityConnectionRepository';
79
- import { D1SubscriptionQuotaCycleRepository } from './repositories/D1SubscriptionQuotaCycleRepository';
80
75
  import { D1PackageRegistryConnectionRepository } from './repositories/D1PackageRegistryConnectionRepository';
81
76
  import { D1IncidentEnrichmentConnectionRepository } from './repositories/D1IncidentEnrichmentConnectionRepository';
82
77
  import { D1AccountSettingsRepository } from './repositories/D1AccountSettingsRepository';
@@ -90,15 +85,13 @@ import { D1ServiceFragmentDefaultsRepository } from './repositories/D1ServiceFra
90
85
  // then wires each gate's provider below.
91
86
  import { wireCiStatusProvider, wireMergeabilityProvider, wireReleaseHealthProvider, wireIncidentEnrichment, wirePullRequestReviewProvider, wireDocQualityProvider, } from '@cat-factory/gates';
92
87
  import { buildGitLabEngineClient, GitLabIdentityResolver, registerGitLab, StaticGitLabTokenSource, } from '@cat-factory/gitlab';
93
- import { GitHubDocQualityProvider, GitHubPrReportPublisher, GitHubPullRequestReviewProvider, createEnvToolSecretResolver, } from '@cat-factory/server';
88
+ import { GitHubDocQualityProvider, GitHubPrReportPublisher, GitHubPullRequestReviewProvider, } from '@cat-factory/server';
94
89
  import { GitHubCiStatusProvider } from './github/GitHubCiStatusProvider';
95
90
  import { GitHubMergeabilityProvider } from './github/GitHubMergeabilityProvider';
96
91
  import { GitHubBranchUpdater } from './github/GitHubBranchUpdater';
97
92
  import { GitHubPullRequestMerger } from './github/GitHubPullRequestMerger';
98
93
  import { WebCryptoSecretCipher } from './environments/WebCryptoSecretCipher';
99
94
  import { D1NotificationWebhookRepository } from './repositories/D1NotificationWebhookRepository';
100
- import { GitHubAppAuth } from './github/GitHubAppAuth';
101
- import { GitHubAppRegistry } from './github/GitHubAppRegistry';
102
95
  import { FetchGitHubClient } from './github/FetchGitHubClient';
103
96
  import { D1TaskConnectionRepository } from './repositories/D1TaskConnectionRepository';
104
97
  import { D1TaskSourceSettingsRepository } from './repositories/D1TaskSourceSettingsRepository';
@@ -112,288 +105,6 @@ import { D1SkillSourceRepository } from './repositories/D1SkillSourceRepository'
112
105
  import { LlmFragmentSelector } from './ai/LlmFragmentSelector';
113
106
  import { buildApiKeyService, buildLocalModelEndpointService, buildOpenRouterCatalogService, buildPersonalSubscriptionService, buildPublicApiKeyService, buildResolveUserGitHubToken, buildSubscriptionService, buildTestSecretsService, buildUserSecretService, buildValidationConfigService, } from './wireCredentialServices';
114
107
  import { CryptoIdGenerator, SystemClock } from './runtime';
115
- /**
116
- * The Worker's {@link ModelProvider}: the base registry plus any extra provider
117
- * registries an installation registered (see ./ai/registries). Used everywhere a
118
- * model provider is needed so every path — agent executor, requirements reviewer,
119
- * doc planner, fragment selector — sees the same provider set. When Langfuse is
120
- * configured the provider is wrapped so those INLINE (non-proxied) calls surface on
121
- * the same trace sink the LLM proxy fans container calls out to.
122
- */
123
- // Memoised per `(Env, db)`: every inline consumer (agent executor, requirements
124
- // reviewer, doc planner, fragment selector) shares ONE resolver — and so ONE Langfuse
125
- // sink — for a container build. The resolver builds a per-scope provider from the
126
- // DB-backed API-key pool plus the opt-in Cloudflare binding + Bedrock registries.
127
- const modelResolverCache = new WeakMap();
128
- function buildModelProviderResolver(env, db) {
129
- const cached = modelResolverCache.get(env);
130
- if (cached)
131
- return cached;
132
- // Opt-in provider registries that need no per-scope DB key: the Cloudflare Workers
133
- // AI binding (when bound) and any extra registries (e.g. Bedrock). NOT assumed —
134
- // `workers-ai` resolves only when the `AI` binding is present.
135
- const extraRegistries = [
136
- ...(env.AI ? [cloudflareBindingRegistry({ binding: env.AI })] : []),
137
- ...resolveExtraRegistries(env),
138
- ];
139
- // Instrument inline (non-proxied) calls with the SAME composed trace sink the proxied
140
- // path uses — Langfuse and/or the OTLP exporter, whichever are enabled.
141
- const traceSink = composeTraceSinks([
142
- buildLangfuseSink(loadLangfuseConfig(env)),
143
- buildOtelSink(loadOtelConfig(env)),
144
- ]);
145
- // Persist inline calls to the SAME `llm_call_metrics` store the proxy writes for Pi and
146
- // the executor writes for a subscription harness, so an inline agent kind (`doc-researcher`,
147
- // the judges, consensus, the requirements writer) is visible to `ObservabilityPanel`, the
148
- // per-step rollups and `/api/v1/debug/*` rather than only to an external trace backend.
149
- // Composed through the shared factory so the recorder's service and the provider's fallback
150
- // sink cannot be handed two DIFFERENT instances — the service owns the fan-out for a call it
151
- // records, so a mismatch would split the trace (and wiring both to the provider would double
152
- // every inline generation on Langfuse/OTel). No cache handle is passed because
153
- // `workspaceSettings` is a pass-through in the isolate-safe profile.
154
- const instrument = createInlineInstrumentation({
155
- llmCallMetricRepository: new D1LlmCallMetricRepository({ db: requireTelemetryDb(env) }),
156
- ...(traceSink ? { traceSink } : {}),
157
- recordPrompts: loadObservabilityConfig(env).recordPrompts,
158
- workspaceSettingsRepository: new D1WorkspaceSettingsRepository({ db }),
159
- idGenerator: new CryptoIdGenerator(),
160
- clock: new SystemClock(),
161
- logger,
162
- });
163
- const localModelEndpoints = buildLocalModelEndpointService(env, db, { now: () => Date.now() });
164
- const scoped = createScopedModelProviderResolver({
165
- apiKeys: buildApiKeyService(env, db, { now: () => Date.now() }),
166
- baseUrlFor: (provider) => baseUrlFor(provider, env) ?? undefined,
167
- extraRegistries,
168
- localEndpointsFor: localModelEndpoints
169
- ? (userId) => localModelEndpoints.listResolved(userId)
170
- : undefined,
171
- });
172
- // Observe inline calls, then cap concurrency, through the ONE composer that owns their order
173
- // (instrumentation inside, limiter outermost). The Worker has no facade wrap that substitutes a
174
- // resolved model today, so it could not hit the local-mode blind spot this order exists for —
175
- // but going through the shared composer is what keeps that true for a Worker-side wrap added
176
- // later, and it keeps the two facades textually symmetric. The limiter bounds concurrency within
177
- // one isolate only (no cross-isolate/global limiting — see
178
- // backend/docs/concurrency-and-redis.md), and since the Worker's inline path degrades
179
- // subscription refs before resolve, it is a wired pass-through here in practice.
180
- const resolver = wrapResolverWithTelemetry(scoped, {
181
- ...(instrument ? { instrument } : {}),
182
- limiter: vendorConcurrencyLimiterFromEnv((key) => env[key]),
183
- });
184
- modelResolverCache.set(env, resolver);
185
- return resolver;
186
- }
187
- /**
188
- * The resolver every executor consults for a step's default model (block-pinned >
189
- * the task's selected/default model preset > env routing). Backed by the D1
190
- * model-preset repo; shared by the inline LLM executor and the container executor so
191
- * both honour the workspace presets identically. The built-in default preset points
192
- * every agent kind at Kimi K2.7, so an unpinned step resolves to it even before the
193
- * preset library is materialised.
194
- */
195
- function buildResolveWorkspaceModelDefault(db) {
196
- const repo = new D1ModelPresetRepository({ db });
197
- return (workspaceId, agentKind, modelPresetId) => resolvePresetModelForKind(repo, workspaceId, agentKind, modelPresetId);
198
- }
199
- export function selectAgentExecutor(deps) {
200
- const { env, config, db, agentKindRegistry } = deps;
201
- const inline = new AiAgentExecutor({
202
- modelProviderResolver: buildModelProviderResolver(env, db),
203
- agentRouting: config.agents.routing,
204
- resolveBlockModel: config.agents.resolveBlockModel,
205
- // Inline (non-sandbox) kinds honour the workspace's per-kind defaults too, so
206
- // the resolution precedence is uniform across every agent kind, not just the
207
- // container kinds.
208
- resolveWorkspaceModelDefault: buildResolveWorkspaceModelDefault(db),
209
- // Opt-in provider web search for the inline design/research kinds (no-op unless
210
- // INLINE_WEB_SEARCH_ENABLED and an Anthropic/OpenAI model).
211
- webSearch: inlineWebSearchOptionsFromEnv(env),
212
- agentKindRegistry,
213
- });
214
- // The sandbox MUST build — a null here means a prerequisite (GitHub App private
215
- // key, WORKER_PUBLIC_URL, AUTH_SESSION_SECRET, or a runner backend: the
216
- // EXEC_CONTAINER binding or a registered runner pool) is missing. We refuse to
217
- // start with a half-configured implementer rather than quietly running the
218
- // repo-operating steps as useless one-shot LLM calls.
219
- const container = buildContainerExecutor(deps);
220
- if (!container) {
221
- throw configProblem({ key: 'CONTAINER_EXECUTOR', ...ENV_HELP.CONTAINER_EXECUTOR });
222
- }
223
- // Always the composite: non-sandbox kinds run inline; sandbox kinds run in the
224
- // container.
225
- return new CompositeAgentExecutor(inline, container, agentKindRegistry);
226
- }
227
- /** Truthy env flag (`true`/`1`/`yes`). */
228
- function isTruthy(value) {
229
- return value === 'true' || value === '1' || value === 'yes';
230
- }
231
- /**
232
- * Wrap the standard executor with the optional consensus mechanism when
233
- * `CONSENSUS_ENABLED` is set: register the consensus capability traits (so the builder
234
- * offers "Enable Consensus" on eligible steps) and route consensus-enabled steps through
235
- * a multi-model process, persisting + pushing the transcript. Off ⇒ returns `standard`
236
- * unchanged (no traits, no wrapping), so behaviour is identical to before.
237
- */
238
- export function maybeWrapConsensus(standard, env, config, db, eventPublisher, agentKindRegistry) {
239
- if (!isTruthy(env.CONSENSUS_ENABLED))
240
- return standard;
241
- registerConsensusTraits(agentKindRegistry);
242
- return new ConsensusAgentExecutor({
243
- standard,
244
- modelProviderResolver: buildModelProviderResolver(env, db),
245
- agentRouting: config.agents.routing,
246
- resolveBlockModel: config.agents.resolveBlockModel,
247
- resolveWorkspaceModelDefault: buildResolveWorkspaceModelDefault(db),
248
- sessionRepository: new D1ConsensusSessionRepository({ db }),
249
- ...(eventPublisher ? { eventPublisher } : {}),
250
- agentKindRegistry,
251
- });
252
- }
253
- /**
254
- * Build the factory that picks a job's runner backend: a workspace's own
255
- * self-hosted runner pool when one is registered (and runner pools are enabled),
256
- * otherwise the per-run Cloudflare Container. Returns null when neither backend is
257
- * available, so {@link buildContainerExecutor} falls back to inline work.
258
- */
259
- function buildResolveTransport(deps) {
260
- const { env, config, db, clock, provisioningLog, runnerBackendRegistry, injectedPoolProvider } = deps;
261
- // The Cloudflare backend folds in instance-level reaping: the registry records
262
- // each dispatched container in the live inventory and clears it on release, so the
263
- // cron reaper (index.ts) can kill anything that outlived its lifetime — covering
264
- // run/blueprint/bootstrap through this one transport with no per-flow wiring.
265
- const cloudflare = env.EXEC_CONTAINER
266
- ? new CloudflareContainerTransport(env.EXEC_CONTAINER, new ContainerInstanceRegistry(env.EXEC_CONTAINER, new D1LiveContainerRepository({ db }), clock), env.HARNESS_SHARED_SECRET?.trim() || undefined)
267
- : null;
268
- // The self-hosted backend path: a connection service that resolves each workspace's
269
- // runner-backend config (manifest pool OR native Kubernetes) to a live transport via
270
- // the runner-backend provider registry. The shared manifest HTTP provider (its OAuth
271
- // cache reused) is threaded in for the `manifest` kind.
272
- let runnerService;
273
- if (config.runners.enabled) {
274
- const urlPolicy = resolveUrlSafetyPolicy(config.runners);
275
- runnerService = new RunnerPoolConnectionService({
276
- runnerPoolConnectionRepository: new D1RunnerPoolConnectionRepository({ db }),
277
- workspaceRepository: new D1WorkspaceRepository({ db }),
278
- secretCipher: new WebCryptoSecretCipher({
279
- masterKeyBase64: config.runners.encryptionKey,
280
- info: 'cat-factory:runners',
281
- }),
282
- clock,
283
- logger,
284
- runnerBackendRegistry,
285
- ...(urlPolicy ? { urlPolicy } : {}),
286
- runnerPoolProvider: injectedPoolProvider ?? new HttpRunnerPoolProvider(urlPolicy ? { urlPolicy } : {}),
287
- });
288
- }
289
- if (!cloudflare && !runnerService)
290
- return null;
291
- // Wrap a resolved transport so every dispatch/release/poll-failure appends a
292
- // provisioning-log event tagged with the right subsystem (a self-hosted pool vs a
293
- // per-run Cloudflare container). No-op when the separate log store isn't wired.
294
- // The dedup set is closure-owned so it outlives each (per-resolution) wrapper.
295
- const loggedPollFailures = new Set();
296
- const log = (inner, subsystem, workspaceId, providerId) => provisioningLog
297
- ? new LoggingRunnerTransport({
298
- inner,
299
- recorder: provisioningLog,
300
- workspaceId: workspaceId ?? '',
301
- subsystem,
302
- providerId,
303
- loggedPollFailures,
304
- })
305
- : inner;
306
- return async (workspaceId) => {
307
- if (runnerService && workspaceId) {
308
- const resolved = await runnerService.resolve(workspaceId);
309
- if (resolved) {
310
- return log(resolved.transport, 'runner-pool', workspaceId, resolved.providerId);
311
- }
312
- }
313
- if (cloudflare)
314
- return log(cloudflare, 'container', workspaceId);
315
- // The shared factory throws a ConflictError carrying the machine reason (see its doc): a clean
316
- // 409 synchronously, and classifyDispatchFailure lifts the reason onto the run's AgentFailure on
317
- // the async dispatch path (SPA shows "Agent backend not configured", not "container failed to
318
- // start"). The Cloudflare facade also offers "enable Cloudflare Containers" in the remedy.
319
- throw noRunnerBackendAvailableError(workspaceId, { cloudflareContainers: true });
320
- };
321
- }
322
- /**
323
- * Build the container-based implementation executor, or return null when its
324
- * prerequisites are missing (a runner backend — Cloudflare Containers and/or a
325
- * self-hosted pool — plus a configured GitHub App, the proxy's public URL and the
326
- * signing secret) — the caller then falls back to inline work.
327
- */
328
- /**
329
- * Build the multi-App registry (ADR 0005): the default App always, plus the
330
- * privileged App when configured. It resolves which App's key to use per
331
- * installation (from the binding's recorded appId), so every token mint / app-JWT
332
- * call routes correctly. Callers guard on `config.github.enabled`, which requires
333
- * GITHUB_APP_PRIVATE_KEY, so the default key is present.
334
- */
335
- export function buildAppRegistry(env, config, db, clock) {
336
- const installationRepository = new D1GitHubInstallationRepository({ db });
337
- const makeAuth = (appId, privateKeyPem) => new GitHubAppAuth({
338
- appId,
339
- privateKeyPem,
340
- installationRepository,
341
- clock,
342
- apiBase: config.github.apiBase,
343
- });
344
- const privileged = config.github.privilegedApp && env.GITHUB_PRIVILEGED_APP_PRIVATE_KEY
345
- ? {
346
- appId: config.github.privilegedApp.appId,
347
- auth: makeAuth(config.github.privilegedApp.appId, env.GITHUB_PRIVILEGED_APP_PRIVATE_KEY),
348
- }
349
- : undefined;
350
- return new GitHubAppRegistry({
351
- default: {
352
- appId: config.github.appId,
353
- auth: makeAuth(config.github.appId, env.GITHUB_APP_PRIVATE_KEY),
354
- },
355
- privileged,
356
- installationRepository,
357
- });
358
- }
359
- /**
360
- * Resolve the repo linked to a running block's enclosing service, via the shared
361
- * runtime-neutral `buildResolveRepoTarget` (the ancestry walk + no-fallback policy
362
- * live in `@cat-factory/server` so the Worker and Node service can't drift). This
363
- * wrapper just binds the D1 repositories. Shared by the container executor, the CI
364
- * status provider and the PR merger.
365
- *
366
- * No `repoProjectionCache` is threaded here (unlike the Node facade, which caches the
367
- * whole-projection re-list per workspace — caching-layer slice 3): the repo projection
368
- * is our own mutable D1 state, and the Worker's isolate-safe profile makes that cache
369
- * pass-through (no cross-isolate invalidation bus), so an in-isolate TTL would serve
370
- * stale repos after a write on another isolate. Reading live IS the isolate-safe
371
- * behaviour. The shared GitHub sync/webhook services still receive the (pass-through)
372
- * handle via `createGitHubModule`, so their invalidation code path stays symmetric.
373
- */
374
- export function buildResolveRepoTarget(db) {
375
- return buildSharedResolveRepoTarget({
376
- installationRepository: new D1GitHubInstallationRepository({ db }),
377
- repoProjectionRepository: new D1RepoProjectionRepository({ db }),
378
- blockRepository: new D1BlockRepository({ db }),
379
- serviceRepository: new D1ServiceRepository({ db }),
380
- });
381
- }
382
- /**
383
- * The MULTI-REPO resolver (service-connections phase 3): the task's own repo plus each
384
- * connected involved-service repo, deduped. Wired from the SAME D1 repos as the singular
385
- * resolver (the D1 service repo's batched `listByFrameBlocks` resolves the involved frames'
386
- * repos in one query). Fed to the container executor so the implementer can fan a
387
- * cross-service change out across sibling checkouts.
388
- */
389
- function buildResolveRepoTargets(db) {
390
- return buildSharedResolveRepoTargets({
391
- installationRepository: new D1GitHubInstallationRepository({ db }),
392
- repoProjectionRepository: new D1RepoProjectionRepository({ db }),
393
- blockRepository: new D1BlockRepository({ db }),
394
- serviceRepository: new D1ServiceRepository({ db }),
395
- });
396
- }
397
108
  /**
398
109
  * Build the merge-lifecycle ports. The notification repository + merge-preset
399
110
  * repository are wired unconditionally (the inbox + presets are always available);
@@ -836,181 +547,6 @@ function buildSystemEmailSender(config) {
836
547
  return undefined;
837
548
  return async () => sender;
838
549
  }
839
- // The deployment-wide trusted web-search upstream for CONTAINER agents, built from this
840
- // facade's own `WEB_SEARCH_*` env — the fallback the search proxy uses when a run's account
841
- // configured none of its own (see `createDefaultWebSearchUpstream` in @cat-factory/server).
842
- // Public endpoints only on workerd (no loopback-SearXNG story); kept symmetric with the Node
843
- // facade so a stock Cloudflare deployment can also set a deployment-wide default.
844
- function buildDefaultWebSearchUpstream(env) {
845
- return createDefaultWebSearchUpstream({
846
- braveApiKey: env.WEB_SEARCH_BRAVE_API_KEY,
847
- searxngUrl: env.WEB_SEARCH_SEARXNG_URL,
848
- searxngApiKey: env.WEB_SEARCH_SEARXNG_API_KEY,
849
- });
850
- }
851
- function buildContainerExecutor(deps) {
852
- const { env, config, db, clock, resolveTransport, agentKindRegistry, subscriptions, personalSubscriptions, agentContextObservability, } = deps;
853
- if (!config.github.enabled ||
854
- !env.GITHUB_APP_PRIVATE_KEY ||
855
- !env.WORKER_PUBLIC_URL ||
856
- !env.AUTH_SESSION_SECRET) {
857
- return null;
858
- }
859
- if (!resolveTransport)
860
- return null;
861
- const registry = buildAppRegistry(env, config, db, clock);
862
- const resolveRepoTarget = buildResolveRepoTarget(db);
863
- // Record a subscription harness's (Claude Code / Codex) per-call telemetry into the
864
- // SAME `llm_call_metrics` store the LLM proxy writes for Pi — those harnesses bypass
865
- // the proxy, so the executor lifts the metrics off the CLI stream and feeds them here.
866
- // A standalone service over the required telemetry DB (the proxy path builds its own
867
- // from the same table; both are stateless writers). The settings repository is REQUIRED
868
- // here, not optional hygiene: a subscription harness's `stream-json` carries the FULL
869
- // prompt and response, and an absent repository makes `createStoreAgentContextGate` an
870
- // open gate — so without it an opted-out workspace's bodies are retained anyway, which is
871
- // exactly the privacy half of C2 (observability-logging-gaps.md) wearing a different hat.
872
- const recordHarnessCalls = makeHarnessCallRecorder(new LlmObservabilityService({
873
- llmCallMetricRepository: new D1LlmCallMetricRepository({ db: requireTelemetryDb(env) }),
874
- idGenerator: new CryptoIdGenerator(),
875
- clock,
876
- recordPrompts: config.observability.recordPrompts,
877
- workspaceSettingsRepository: new D1WorkspaceSettingsRepository({ db }),
878
- logger,
879
- }));
880
- // Modeled subscription quota-cycle provider (usage-and-quota-tracking, Part B): folds a
881
- // finished subscription run's tokens into rolling windows (real vendor reads land in B2,
882
- // so its adapter registry is empty today — every vendor reports modeled).
883
- const subscriptionQuotaProvider = new RegistrySubscriptionQuotaProvider({
884
- subscriptionQuotaCycleRepository: new D1SubscriptionQuotaCycleRepository({ db }),
885
- idGenerator: new CryptoIdGenerator(),
886
- clock,
887
- registry: defaultSubscriptionQuotaRegistry,
888
- });
889
- // Prefer the run initiator's per-user PAT (when stored) over the App token, so the
890
- // container's clone/push/PR is attributed to them. Falls back to the App token.
891
- const resolveUserGitHubToken = buildResolveUserGitHubToken(env, db, clock);
892
- const mintInstallationToken = async (installationId, ctx) => {
893
- if (resolveUserGitHubToken && ctx?.initiatedBy) {
894
- const pat = await resolveUserGitHubToken(ctx.initiatedBy);
895
- if (pat)
896
- return pat;
897
- }
898
- return registry.installationToken(installationId);
899
- };
900
- // Advertise Pi's `web_search` tool to a run only when a usable upstream exists — either the
901
- // deployment-wide default below (⇒ always on) or the run's account has its own keys (else the
902
- // tool would just fail/return nothing). The per-account check runs off the account-settings
903
- // store (its own short-TTL cache).
904
- const resolvePackageRegistries = buildResolvePackageRegistries(env, db);
905
- // Decrypt the service frame's sensitive test credentials onto the tester job body (out of band).
906
- const testSecretsForDispatch = buildTestSecretsService(env, db, clock);
907
- const resolveTestSecrets = testSecretsForDispatch
908
- ? (workspaceId, blockId) => testSecretsForDispatch.resolveValuesForBlock(workspaceId, blockId)
909
- : undefined;
910
- const defaultWebSearchUpstream = buildDefaultWebSearchUpstream(env);
911
- // No `settingsCache` threaded to this dedicated web-search-availability instance: the
912
- // `accountSettings` slice is pass-through on the Worker's isolate-safe profile, so caching it
913
- // here is a no-op (the primary `accountSettings` instance in `buildContainer` — the one whose
914
- // decrypted view drives the runtime resolvers — gets the shared slice). The Node facade, where
915
- // the slice is enabled, wires its web-search instance from `options.caches` directly.
916
- const webSearchSettings = buildAccountSettings(env, db, clock);
917
- const resolveWebSearchAvailability = defaultWebSearchUpstream || webSearchSettings
918
- ? async (workspaceId) => {
919
- // Mirror the proxy's own resolution (`accountUpstream ?? defaultWebSearchUpstream`):
920
- // the run's account keys WIN and the deployment default is only the fallback, so the
921
- // surfaced provider matches the one that will actually serve the run's searches. Build
922
- // the account upstream the SAME way the proxy does before falling back to the default.
923
- if (webSearchSettings) {
924
- const accountId = await new D1WorkspaceRepository({ db }).accountOf(workspaceId);
925
- if (accountId) {
926
- const accountUpstream = createWebSearchUpstream((await webSearchSettings.resolve(accountId)).webSearch ?? {});
927
- if (accountUpstream)
928
- return { available: true, provider: accountUpstream.provider };
929
- }
930
- }
931
- if (defaultWebSearchUpstream)
932
- return { available: true, provider: defaultWebSearchUpstream.provider };
933
- return { available: false, provider: null };
934
- }
935
- : undefined;
936
- return new ContainerAgentExecutor({
937
- resolveTransport,
938
- agentRouting: config.agents.routing,
939
- resolveBlockModel: config.agents.resolveBlockModel,
940
- // The workspace's per-agent-kind default model, consulted when a block pins none
941
- // (block-pinned > workspace per-kind default > env routing > env default).
942
- resolveWorkspaceModelDefault: buildResolveWorkspaceModelDefault(db),
943
- resolveRepoTarget,
944
- // Multi-repo coding (service-connections phase 3): the implementer fans a cross-service
945
- // change out across the task's own repo + each connected involved-service repo.
946
- resolveRepoTargets: buildResolveRepoTargets(db),
947
- // Resolve the workspace's owning account so the proxy can lease account-scoped keys.
948
- resolveAccountId: (workspaceId) => new D1WorkspaceRepository({ db }).accountOf(workspaceId),
949
- mintInstallationToken,
950
- // Ensure the shared per-task work branch up front so every agent (including the
951
- // read-only architect) operates on the same branch — idempotent, best-effort. Writers
952
- // create it from base; read-only agents only probe (`options.create`).
953
- ensureWorkBranch: async (repo, branch, options) => ensureWorkBranchViaRest({
954
- ...(config.github.apiBase ? { apiBase: config.github.apiBase } : {}),
955
- token: await registry.installationToken(repo.installationId),
956
- owner: repo.owner,
957
- name: repo.name,
958
- baseBranch: repo.baseBranch,
959
- branch,
960
- create: options.create,
961
- }),
962
- sessionService: new ContainerSessionService({ secret: env.AUTH_SESSION_SECRET }),
963
- // The subscription harnesses (Claude Code / Codex) lease a pooled token and
964
- // attribute usage back for usage-aware rotation; absent ⇒ those harnesses are
965
- // unavailable and a subscription-only model fails loudly at dispatch.
966
- ...(subscriptions
967
- ? {
968
- leaseSubscriptionToken: (workspaceId, vendor) => subscriptions.leaseToken(workspaceId, vendor),
969
- recordSubscriptionUsage: (workspaceId, tokenId, usage) => subscriptions.recordTokenUsage(workspaceId, tokenId, usage),
970
- hasSubscriptionToken: (workspaceId, vendor) => subscriptions.hasToken(workspaceId, vendor),
971
- }
972
- : {}),
973
- // Per-call telemetry for the subscription harnesses (proxy-bypassing), recorded
974
- // into `llm_call_metrics` alongside the proxy-metered Pi rows.
975
- recordHarnessCalls,
976
- // Modeled subscription quota-cycle tracking (Part B): fold a finished subscription
977
- // run's tokens into the rolling windows, for BOTH pooled and personal runs.
978
- recordSubscriptionQuotaUsage: (target, usage) => subscriptionQuotaProvider.recordUsage(target, usage),
979
- // Individual-usage harnesses (Claude) lease the run-initiator's OWN activated
980
- // personal credential; absent ⇒ such models fail loudly at dispatch.
981
- ...(personalSubscriptions
982
- ? {
983
- leasePersonalSubscriptionToken: (executionId, userId, vendor) => personalSubscriptions.leaseForRun(executionId, userId, vendor),
984
- // Route a dual-mode individual model (GLM) to the initiator's own subscription
985
- // when they have one; otherwise dispatch keeps it on the Cloudflare base.
986
- hasPersonalSubscription: (userId, vendor) => personalSubscriptions.has(userId, vendor),
987
- }
988
- : {}),
989
- proxyBaseUrl: `${env.WORKER_PUBLIC_URL.replace(/\/+$/, '')}/v1`,
990
- // Point container agents' web search at the backend search proxy (no provider key in
991
- // the sandbox), but only for a run whose account has keys (see resolver above).
992
- ...(resolveWebSearchAvailability ? { resolveWebSearchAvailability } : {}),
993
- // Decrypt the workspace's private-registry entries onto the job body (rendered by
994
- // the harness into ~/.npmrc), so private dependencies resolve on install.
995
- ...(resolvePackageRegistries ? { resolvePackageRegistries } : {}),
996
- // Decrypt the service frame's SENSITIVE test credentials onto the tester job body (out of
997
- // band — injected as container env vars by the harness, never in the prompt/telemetry).
998
- ...(resolveTestSecrets ? { resolveTestSecrets } : {}),
999
- // Resolve the credentials a registered kind's TOOL SERVER (MCP) declared, off the Worker's own
1000
- // configured vars. A deployment needing per-workspace credentials replaces this with its own
1001
- // `ToolSecretResolver`; the rest of the dispatch path is unchanged either way.
1002
- resolveToolSecrets: createEnvToolSecretResolver(env),
1003
- logger,
1004
- githubApiBase: config.github.apiBase,
1005
- // Forward container tool spans to the external trace sink(s) (Langfuse and/or OTLP)
1006
- // grouped under the run trace — the same sink the LLM proxy fans generations to.
1007
- // (Langfuse nests them as children; the OTLP exporter groups them by shared trace id.)
1008
- llmTraceSink: buildTraceSink(config),
1009
- // Record the complete provided context per dispatch (best-effort, gated in the sink).
1010
- ...(agentContextObservability ? { agentContextObservability } : {}),
1011
- agentKindRegistry,
1012
- });
1013
- }
1014
550
  /**
1015
551
  * Pick how runs are driven:
1016
552
  * - a Workflows binding present → durable, server-driven execution
@@ -1690,6 +1226,11 @@ export function buildContainer(env, overrides = {}, opts = {}) {
1690
1226
  agentContextObservability,
1691
1227
  searchQueryObservability,
1692
1228
  accountSettings,
1229
+ // The executor's own two handles. `webSearchAccountSettings` is a SECOND, deliberately
1230
+ // uncached account-settings reader (see `WorkerExecutorDeps`); building both here keeps the
1231
+ // module graph one-way — `container-executor-deps.ts` never imports the root.
1232
+ executorPackageRegistries: buildResolvePackageRegistries(env, db),
1233
+ webSearchAccountSettings: buildAccountSettings(env, db, clock),
1693
1234
  defaultWebSearchUpstream,
1694
1235
  resolveBinaryArtifactStore,
1695
1236
  githubWebhookIngest,