@cat-factory/node-server 0.107.7 → 0.107.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/container.js CHANGED
@@ -1,16 +1,14 @@
1
- import { AiAgentExecutor, defaultAgentKindRegistry, defaultInitiativePresetRegistry, inlineWebSearchOptionsFromEnv, resolveAgentConfig, isProxyableProvider, vendorConcurrencyLimiterFromEnv, } from '@cat-factory/agents';
1
+ import { AiAgentExecutor, defaultAgentKindRegistry, defaultInitiativePresetRegistry, inlineWebSearchOptionsFromEnv, vendorConcurrencyLimiterFromEnv, } 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, GitHubIssuesProvider, JiraProvider, LinearDocumentProvider, LinearTaskProvider, createBackendRegistries, HttpRunnerPoolProvider, NotionProvider, ApiKeyService, LocalModelEndpointService, PersonalSubscriptionService, ProviderSubscriptionService, RunnerPoolConnectionService, ProvisioningLogRecorder, LoggingRunnerTransport, EMAIL_CIPHER_INFO, createEmailSender, SLACK_CIPHER_INFO, SlackNotificationChannel, TicketTrackerService, IssueWritebackService, githubIssuesLogic, createGitHubIssueViaToken, OBSERVABILITY_CIPHER_INFO, RegistryReleaseHealthProvider, defaultObservabilityRegistry, RegistrySubscriptionQuotaProvider, defaultSubscriptionQuotaRegistry, WorkspaceIncidentEnrichmentProvider, INCIDENT_ENRICHMENT_CIPHER_INFO, AccountSettingsService, ACCOUNT_SETTINGS_CIPHER_INFO, TestSecretsService, TEST_SECRETS_CIPHER_INFO, } from '@cat-factory/integrations';
9
- import { CompositeNotificationChannel, DEFAULT_MODEL_PRESET_ID, SUBSCRIPTION_VENDORS, composeTraceSinks, defaultProviderRegistry, defaultVcsRegistry, isAmbientNativeVendor, } from '@cat-factory/kernel';
8
+ import { ConfluenceProvider, FigmaProvider, ZeplinProvider, GitHubDocsProvider, GitHubIssuesProvider, JiraProvider, LinearDocumentProvider, LinearTaskProvider, createBackendRegistries, HttpRunnerPoolProvider, NotionProvider, ApiKeyService, LocalModelEndpointService, ProvisioningLogRecorder, EMAIL_CIPHER_INFO, createEmailSender, SLACK_CIPHER_INFO, SlackNotificationChannel, TicketTrackerService, IssueWritebackService, githubIssuesLogic, OBSERVABILITY_CIPHER_INFO, RegistryReleaseHealthProvider, defaultObservabilityRegistry, RegistrySubscriptionQuotaProvider, defaultSubscriptionQuotaRegistry, WorkspaceIncidentEnrichmentProvider, INCIDENT_ENRICHMENT_CIPHER_INFO, AccountSettingsService, ACCOUNT_SETTINGS_CIPHER_INFO, TestSecretsService, TEST_SECRETS_CIPHER_INFO, } from '@cat-factory/integrations';
9
+ import { CompositeNotificationChannel, DEFAULT_MODEL_PRESET_ID, defaultProviderRegistry, defaultVcsRegistry, } from '@cat-factory/kernel';
10
10
  import { AgentContextObservabilityService, SearchQueryObservabilityService, createCore, defaultStepResolverRegistry, PACKAGE_REGISTRY_CIPHER_INFO, resolvePackageRegistriesForDispatch, LlmObservabilityService, makeHarnessCallRecorder, resolvePresetModelForKind, } from '@cat-factory/orchestration';
11
- import { createLangfuseSink } from '@cat-factory/observability-langfuse';
12
- import { createNodeOtelSink } from '@cat-factory/observability-otel/node';
13
- import { CompositeAgentExecutor, ContainerAgentExecutor, ContainerEnvConfigRepairer, ContainerRepoBootstrapper, ContainerSessionService, FanOutEventPublisher, FetchGitHubClient, FetchGitHubProvisioningClient, GitHubAppAuth, GitHubAppRegistry, GitHubIdentityResolver, GitHubCiStatusProvider, GitHubDocQualityProvider, GitHubMergeabilityProvider, GitHubPullRequestReviewProvider, GitHubBranchUpdater, GitHubPullRequestMerger, InAppNotificationChannel, PatPreferringAppRegistry, runWithInitiator, WebCryptoPasswordHasher, WebCryptoSecretCipher, WebCryptoWebhookVerifier, buildInfrastructureCapabilities, testEnvHasZeroConfigDefault, buildResolveRepoTarget, buildResolveRepoTargets, createDefaultWebSearchUpstream, createWebSearchUpstream, makePreviewJobBuilder, makeResolveDeployCloneTarget, makeResolveRunRepoContext, makeResolveRepoFilesForCoords, makeResolveBinaryArtifactStore, RunnerJobClient, DOCS, ENV_VARS_ANCHORS, noRunnerBackendAvailableError, ensureWorkBranchViaRest, logger, resolveUrlSafetyPolicy, resolveWorkspaceCapabilities, wrapResolverWithLimiter, } from '@cat-factory/server';
11
+ import { CompositeAgentExecutor, ContainerSessionService, FanOutEventPublisher, FetchGitHubClient, FetchGitHubProvisioningClient, GitHubAppAuth, GitHubAppRegistry, GitHubIdentityResolver, GitHubCiStatusProvider, GitHubDocQualityProvider, GitHubMergeabilityProvider, GitHubPullRequestReviewProvider, GitHubBranchUpdater, GitHubPullRequestMerger, InAppNotificationChannel, PatPreferringAppRegistry, runWithInitiator, WebCryptoPasswordHasher, WebCryptoSecretCipher, WebCryptoWebhookVerifier, buildInfrastructureCapabilities, testEnvHasZeroConfigDefault, buildResolveRepoTarget, buildResolveRepoTargets, createDefaultWebSearchUpstream, createWebSearchUpstream, makePreviewJobBuilder, makeResolveDeployCloneTarget, makeResolveRunRepoContext, makeResolveRepoFilesForCoords, makeResolveBinaryArtifactStore, RunnerJobClient, logger, resolveUrlSafetyPolicy, resolveWorkspaceCapabilities, wrapResolverWithLimiter, } from '@cat-factory/server';
14
12
  // The built-in polling-gate suite (ci / conflicts / post-release-health + on-call). The facade
15
13
  // builds an app-owned `GateRegistry` pre-loaded with the suite via `gateRegistryWithBuiltins()`
16
14
  // below, then wires each gate's provider.
@@ -47,9 +45,12 @@ import { DrizzleSlackConnectionRepository, DrizzleSlackMemberMappingRepository,
47
45
  import { DrizzleTaskConnectionRepository, DrizzleTaskRepository, DrizzleTaskSourceSettingsRepository, } from './repositories/tasks.js';
48
46
  import { CryptoIdGenerator, SystemClock } from './runtime.js';
49
47
  import { buildNodeApiKeyService, buildNodeLocalModelEndpointService, buildNodeOpenRouterCatalogService, buildNodePersonalSubscriptionService, buildNodePublicApiKeyService, buildNodeSubscriptionService, buildNodeUserSecretService, } from './wireCredentialServices.js';
50
- // HKDF domain tag separating runner-pool scheduler secrets from any other use of
51
- // the same master key (mirrors the Worker's `cat-factory:runners`).
52
- const RUNNERS_CIPHER_INFO = 'cat-factory:runners';
48
+ // The container-agent-executor wiring (transport resolver, provisioning-log wrapper, container
49
+ // executor + bootstrapper + env-config repairer, GitHub-issue filer, trace-sink builder), lifted
50
+ // into a sibling module so this composition root stays within the file-size budget.
51
+ import { RUNNERS_CIPHER_INFO, buildNodeContainerExecutor, buildNodeGitHubIssueFiler, buildNodeResolveTransport, buildTraceSink, selectNodeEnvConfigRepairer, selectNodeRepoBootstrapper, withProvisioningLog, } from './container-executor-deps.js';
52
+ // Re-export the public seams the local facade + tests still import from `./container.js`.
53
+ export { buildNodeResolveTransport, missingContainerExecutorPrereqs, withProvisioningLog, } from './container-executor-deps.js';
53
54
  /**
54
55
  * Source one org/durable repository that a standard build constructs directly from the Drizzle
55
56
  * `db`. In mothership mode (no Postgres) `remote` is the full-surface remote registry — a
@@ -71,7 +72,6 @@ export function pickRepoSource(remote, name, build) {
71
72
  // own. Mirrors the Worker's `buildModelProvider` memoisation. Memoisation matters more for
72
73
  // OTel than Langfuse: the SDK sink owns batch processors/exporters, so it must be built
73
74
  // once per config, not per wiring site.
74
- const traceSinkCache = new WeakMap();
75
75
  /** Truthy env flag (`true`/`1`/`yes`). */
76
76
  function isTruthy(value) {
77
77
  return value === 'true' || value === '1' || value === 'yes';
@@ -98,38 +98,6 @@ instrument) {
98
98
  modelResolverCache.set(db, resolver);
99
99
  return resolver;
100
100
  }
101
- /**
102
- * Build the opt-in external trace sink(s) — Langfuse and/or OpenTelemetry — composed into
103
- * the single sink slot; the observability service then fans every recorded LLM call out to
104
- * whichever are wired. Memoised per config so both wiring sites share one instance.
105
- *
106
- * Langfuse uses the fetch-based sink (identical to the Worker). OpenTelemetry uses the
107
- * OFFICIAL `@opentelemetry/*` SDK exporter (`createNodeOtelSink`) — the Node counterpart of
108
- * the Worker's fetch OTLP exporter, kept conformant by the shared mapping layer + tests.
109
- */
110
- function buildTraceSink(config) {
111
- if (traceSinkCache.has(config))
112
- return traceSinkCache.get(config);
113
- const langfuse = !config.langfuse.enabled || !config.langfuse.publicKey || !config.langfuse.secretKey
114
- ? undefined
115
- : createLangfuseSink({
116
- publicKey: config.langfuse.publicKey,
117
- secretKey: config.langfuse.secretKey,
118
- baseUrl: config.langfuse.baseUrl,
119
- logger,
120
- });
121
- const otel = !config.otel.enabled || !config.otel.endpoint
122
- ? undefined
123
- : createNodeOtelSink({
124
- endpoint: config.otel.endpoint,
125
- headers: config.otel.headers,
126
- serviceName: config.otel.serviceName,
127
- logger,
128
- });
129
- const sink = composeTraceSinks([langfuse, otel]);
130
- traceSinkCache.set(config, sink);
131
- return sink;
132
- }
133
101
  /**
134
102
  * Wire the Slack integration when enabled: the notification *channel* (an extra
135
103
  * delivery transport composed onto the notification mechanism — Node has no in-app
@@ -297,358 +265,6 @@ function buildNodeVcsIdentityRegistry(config) {
297
265
  * when runner pools are not enabled. Mirrors the Worker's `buildResolveTransport`,
298
266
  * minus the Cloudflare-container path.
299
267
  */
300
- export function buildNodeResolveTransport(config,
301
- // The port, not the Drizzle concrete: in mothership mode the local facade passes a remote
302
- // (RPC-backed) connection repo, and the service layer only ever uses the port methods.
303
- runnerPoolConnectionRepository, workspaceRepository, clock,
304
- // The app-owned runner-backend registry the service resolves a stored `kind` through.
305
- runnerBackendRegistry,
306
- // The shared HTTP provider the built-in `manifest` backend reuses when supplied (e.g.
307
- // tests). NOT the custom-kind seam — a bespoke runner backend is registered by reference
308
- // into `runnerBackendRegistry`. Absent → the generic manifest-driven HTTP provider.
309
- injectedPoolProvider) {
310
- if (!config.runners.enabled || !config.runners.encryptionKey)
311
- return null;
312
- const urlPolicy = resolveUrlSafetyPolicy(config.runners);
313
- const runnerService = new RunnerPoolConnectionService({
314
- runnerPoolConnectionRepository,
315
- workspaceRepository,
316
- secretCipher: new WebCryptoSecretCipher({
317
- masterKeyBase64: config.runners.encryptionKey,
318
- info: RUNNERS_CIPHER_INFO,
319
- }),
320
- clock,
321
- runnerBackendRegistry,
322
- ...(urlPolicy ? { urlPolicy } : {}),
323
- runnerPoolProvider: injectedPoolProvider ?? new HttpRunnerPoolProvider(urlPolicy ? { urlPolicy } : {}),
324
- });
325
- return async (workspaceId) => {
326
- if (workspaceId) {
327
- const resolved = await runnerService.resolve(workspaceId);
328
- if (resolved)
329
- return resolved.transport;
330
- }
331
- // The shared factory throws a ConflictError carrying the machine reason (see its doc): a clean
332
- // 409 synchronously, and classifyDispatchFailure lifts the reason onto the run's AgentFailure on
333
- // the async dispatch path (SPA shows "Agent backend not configured", not "container failed to
334
- // start"). The Node facade has no per-run container backend, so the remedy points only at the
335
- // self-hosted runner pool / Kubernetes.
336
- throw noRunnerBackendAvailableError(workspaceId);
337
- };
338
- }
339
- /**
340
- * Wrap a transport resolver so every dispatch/release/poll-failure appends a
341
- * provisioning-log event. A no-op when there's no resolver. `subsystem` tags the
342
- * rows (a self-hosted pool vs a per-run container) so the logs drawer can filter.
343
- */
344
- export function withProvisioningLog(resolve, recorder, subsystem) {
345
- if (!resolve)
346
- return null;
347
- // Closure-owned so it survives each (per-resolution) wrapper: a terminal `failed`
348
- // job re-polled by a replay/re-drive logs its poll-failure only once.
349
- const loggedPollFailures = new Set();
350
- return async (workspaceId) => {
351
- const inner = await resolve(workspaceId);
352
- return new LoggingRunnerTransport({
353
- inner,
354
- recorder,
355
- workspaceId: workspaceId ?? '',
356
- subsystem,
357
- loggedPollFailures,
358
- });
359
- };
360
- }
361
- /**
362
- * Which of the container-executor prerequisites are missing, as the human labels the boot
363
- * warning names. Empty ⇒ all three are present. `PUBLIC_URL` is this service's externally
364
- * reachable base backing the LLM proxy, `AUTH_SESSION_SECRET` signs the harness↔proxy tokens,
365
- * and a runner backend is what a dispatch is handed to. Pure so the "name exactly what's
366
- * missing" logic is unit-tested (error-message coverage A5).
367
- */
368
- export function missingContainerExecutorPrereqs(input) {
369
- const missing = [];
370
- if (!input.publicUrl)
371
- missing.push('PUBLIC_URL');
372
- if (!input.sessionSecret)
373
- missing.push('AUTH_SESSION_SECRET (>= 32 chars)');
374
- if (!input.hasRunnerBackend)
375
- missing.push('a runner backend (self-hosted runner pool)');
376
- return missing;
377
- }
378
- /**
379
- * Build the container agent executor (repo-operating steps: coder, mocker,
380
- * playwright, blueprints, ci-fixer, conflict-resolver, merger) when its
381
- * prerequisites are configured: a token source for the push/clone token, the public
382
- * URL backing the LLM proxy, the session secret to sign proxy tokens, and a runner
383
- * backend. Returns null when any is missing, so the composite fails those kinds
384
- * loudly rather than running them as useless one-shot LLM calls.
385
- *
386
- * The token source is pluggable: a sibling facade may pass `mintInstallationToken`
387
- * (e.g. a static PAT for local mode), otherwise it is minted via the GitHub App
388
- * registry (which additionally requires the App private key + `github.enabled`).
389
- */
390
- function buildNodeContainerExecutor(env, config, appRegistry, resolveRepoTarget, resolveRepoTargets, resolveTransport, resolveWorkspaceModelDefault, agentKindRegistry, mintInstallationTokenOverride, subscriptions, personalSubscriptions, resolveAccountId, resolveUserGitHubToken, agentContextObservability, resolveWebSearchAvailability, resolveRepoOrigin, resolvePackageRegistries, resolveTestSecrets, recordHarnessCalls, recordSubscriptionQuotaUsage) {
391
- // The harness reaches models only through this service's LLM proxy; `PUBLIC_URL`
392
- // is this service's externally reachable base (the runner pool / local container
393
- // must be able to reach it). Pi posts to `${PUBLIC_URL}/v1/chat/completions`.
394
- const publicUrl = env.PUBLIC_URL?.trim();
395
- const sessionSecret = config.auth.sessionSecret;
396
- if (!publicUrl || !sessionSecret || !resolveTransport) {
397
- // The executor is disabled but the service still boots "healthy" — repo-operating steps
398
- // (coder/mocker/tester/blueprints/ci-fixer/conflict-resolver/merger) then fail only at
399
- // dispatch, deep in a request, with no boot signal. Emit a greppable line naming exactly
400
- // which prerequisite is missing so the gap is visible up front (error-message coverage A5).
401
- const missing = missingContainerExecutorPrereqs({
402
- publicUrl,
403
- sessionSecret,
404
- hasRunnerBackend: !!resolveTransport,
405
- });
406
- logger.warn({ missing, docsUrl: DOCS.envVars(ENV_VARS_ANCHORS.coreServiceNetworking) }, `container agent steps are DISABLED: missing ${missing.join(', ')}. Repo-operating steps ` +
407
- `(coder/mocker/tester/merger/…) will fail at dispatch until configured. See ` +
408
- `${DOCS.envVars(ENV_VARS_ANCHORS.coreServiceNetworking)}.`);
409
- return null;
410
- }
411
- // Token source: an explicit override (e.g. a static PAT in local mode) wins; else
412
- // the GitHub App registry mints a per-installation token (when the App is configured).
413
- const baseMint = mintInstallationTokenOverride ??
414
- (appRegistry ? (id) => appRegistry.installationToken(id) : undefined);
415
- if (!baseMint) {
416
- // Every other prerequisite is set but there is no GitHub token source, so the harness
417
- // could never clone/push. Name the fix (App creds) rather than disabling silently (A5).
418
- logger.warn({ missing: ['GITHUB_APP_ID + GITHUB_APP_PRIVATE_KEY'], docsUrl: DOCS.githubOperations() }, `container agent steps are DISABLED: no GitHub token source — set GITHUB_APP_ID + ` +
419
- `GITHUB_APP_PRIVATE_KEY so the harness can mint a push/clone token. Repo-operating steps ` +
420
- `will fail at dispatch until configured. See ${DOCS.githubOperations()}.`);
421
- return null;
422
- }
423
- // Prefer the run initiator's per-user PAT (when stored) over the App/env token, so
424
- // pushes/PRs are attributed to them. Falls back to the base mint otherwise.
425
- const mintInstallationToken = async (installationId, ctx) => {
426
- if (resolveUserGitHubToken && ctx?.initiatedBy) {
427
- const pat = await resolveUserGitHubToken(ctx.initiatedBy);
428
- if (pat)
429
- return pat;
430
- }
431
- return baseMint(installationId);
432
- };
433
- return new ContainerAgentExecutor({
434
- resolveTransport,
435
- agentRouting: config.agents.routing,
436
- resolveBlockModel: config.agents.resolveBlockModel,
437
- resolveWorkspaceModelDefault,
438
- resolveRepoTarget,
439
- // Multi-repo coding (service-connections phase 3): the implementer fans a cross-service
440
- // change out across the task's own repo + each connected involved-service repo.
441
- resolveRepoTargets,
442
- ...(resolveAccountId ? { resolveAccountId } : {}),
443
- mintInstallationToken,
444
- // Ensure the shared per-task work branch up front so every agent (including the
445
- // read-only architect) operates on the same branch — idempotent, best-effort. Writers
446
- // create it from base; read-only agents only probe (`options.create`).
447
- ensureWorkBranch: async (repo, branch, options) => ensureWorkBranchViaRest({
448
- ...(config.github.apiBase ? { apiBase: config.github.apiBase } : {}),
449
- token: await mintInstallationToken(repo.installationId),
450
- owner: repo.owner,
451
- name: repo.name,
452
- baseBranch: repo.baseBranch,
453
- branch,
454
- create: options.create,
455
- }),
456
- sessionService: new ContainerSessionService({ secret: sessionSecret }),
457
- // The subscription harnesses (Claude Code / Codex) lease a pooled token and
458
- // attribute usage back for usage-aware rotation; absent ⇒ those harnesses are
459
- // unavailable and a subscription-only model fails loudly at dispatch.
460
- ...(subscriptions
461
- ? {
462
- leaseSubscriptionToken: (workspaceId, vendor) => subscriptions.leaseToken(workspaceId, vendor),
463
- recordSubscriptionUsage: (workspaceId, tokenId, usage) => subscriptions.recordTokenUsage(workspaceId, tokenId, usage),
464
- hasSubscriptionToken: (workspaceId, vendor) => subscriptions.hasToken(workspaceId, vendor),
465
- }
466
- : {}),
467
- // Per-call telemetry for the subscription harnesses (proxy-bypassing), recorded
468
- // into `llm_call_metrics` alongside the proxy-metered Pi rows.
469
- ...(recordHarnessCalls ? { recordHarnessCalls } : {}),
470
- // Modeled subscription quota-cycle tracking (Part B): fold a finished subscription
471
- // run's tokens into the rolling windows, for BOTH pooled and personal runs.
472
- ...(recordSubscriptionQuotaUsage ? { recordSubscriptionQuotaUsage } : {}),
473
- // Individual-usage harnesses (Claude) lease the run-initiator's OWN activated
474
- // personal credential; absent ⇒ such models fail loudly at dispatch.
475
- ...(personalSubscriptions
476
- ? {
477
- leasePersonalSubscriptionToken: (executionId, userId, vendor) => personalSubscriptions.leaseForRun(executionId, userId, vendor),
478
- // Route a dual-mode individual model (GLM) to the initiator's own subscription
479
- // when they have one; otherwise dispatch keeps it on the Cloudflare base.
480
- hasPersonalSubscription: (userId, vendor) => personalSubscriptions.has(userId, vendor),
481
- }
482
- : {}),
483
- // Native local execution (local facade, opt-in): run subscription-harness agents with
484
- // the developer's OWN installed CLI + ambient login instead of leasing a credential.
485
- // Ambient auth applies ONLY when the resolved harness is in the allow-list AND the
486
- // vendor is that CLI's NATIVE vendor (no Anthropic-compatible base URL of its own:
487
- // `claude` / `codex`). A non-native vendor reusing the `claude-code` harness
488
- // (GLM/Kimi/DeepSeek carries its own `baseUrl`) is leased normally — otherwise ambient
489
- // auth would silently drop that base URL and run the step on the developer's own
490
- // Anthropic login instead of the pinned vendor.
491
- ...(config.nativeAmbientAuth && config.nativeAmbientAuth.length > 0
492
- ? {
493
- // The allow-list + no-`baseUrl` check is the shared `isAmbientNativeVendor`
494
- // predicate (so this can't drift from the personal-credential gate); the extra
495
- // `harness === h` guard ensures the RESOLVED harness matches the vendor's own.
496
- nativeAmbientAuth: (h, vendor) => vendor !== undefined &&
497
- SUBSCRIPTION_VENDORS[vendor].harness === h &&
498
- isAmbientNativeVendor(config.nativeAmbientAuth, vendor),
499
- }
500
- : {}),
501
- proxyBaseUrl: `${publicUrl.replace(/\/+$/, '')}/v1`,
502
- // Point container agents' web search at the backend search proxy (no provider key in
503
- // the sandbox), but only for a run whose account has keys (resolved per run — see the
504
- // call site), so the tool is never advertised to a run where it would just fail.
505
- ...(resolveWebSearchAvailability ? { resolveWebSearchAvailability } : {}),
506
- // Decrypt the workspace's private-registry entries onto the job body (rendered by
507
- // the harness into ~/.npmrc), so private dependencies resolve on install.
508
- ...(resolvePackageRegistries ? { resolvePackageRegistries } : {}),
509
- // Decrypt the service frame's SENSITIVE test credentials onto the tester job body (out of
510
- // band — injected as container env vars by the harness, never in the prompt/telemetry).
511
- ...(resolveTestSecrets ? { resolveTestSecrets } : {}),
512
- githubApiBase: config.github.apiBase,
513
- // Resolve the clone URL + provider per repo. The local GitLab facade injects a GitLab
514
- // origin so containers clone gitlab.com (or a self-managed host) and open MRs; absent ⇒
515
- // the default github.com origin.
516
- ...(resolveRepoOrigin ? { resolveRepoOrigin } : {}),
517
- // Forward container tool spans to the external trace sink(s) (Langfuse and/or OTLP)
518
- // grouped under the run trace — the same sink the LLM proxy fans generations to.
519
- // (Langfuse nests them as children; the OTLP exporter groups them by shared trace id.)
520
- llmTraceSink: buildTraceSink(config),
521
- // Record the complete provided context per dispatch (best-effort, gated in the sink).
522
- ...(agentContextObservability ? { agentContextObservability } : {}),
523
- agentKindRegistry,
524
- });
525
- }
526
- /**
527
- * Build the repo bootstrapper (the "bootstrap repo" container dispatch) when its
528
- * prerequisites are configured — mirroring the Worker's `selectRepoBootstrapper` and
529
- * the container-executor prerequisites: a resolvable runner transport, the public URL
530
- * + session secret backing the LLM proxy, a token source, and a GitHub client.
531
- * Returns undefined otherwise (the bootstrap module then has no runner and the service
532
- * reports a clean dispatch failure). Bootstrap is an `architect`-kind run, so it
533
- * follows that kind's routing. The promoted `ContainerRepoBootstrapper` dispatches
534
- * through the same shared runner seam the container executor uses, so on Node it runs
535
- * against the self-hosted pool and on local against the per-job Docker container.
536
- */
537
- function selectNodeRepoBootstrapper(deps) {
538
- const publicUrl = deps.env.PUBLIC_URL?.trim();
539
- const sessionSecret = deps.config.auth.sessionSecret;
540
- if (!deps.resolveTransport ||
541
- !publicUrl ||
542
- !sessionSecret ||
543
- !deps.githubClient ||
544
- !deps.mintInstallationToken) {
545
- return undefined;
546
- }
547
- return new ContainerRepoBootstrapper({
548
- resolveTransport: deps.resolveTransport,
549
- installationRepository: deps.installationRepository,
550
- bootstrapJobRepository: deps.bootstrapJobRepository,
551
- repoRepository: deps.repoRepository,
552
- ...(deps.repoProjectionCache ? { repoProjectionCache: deps.repoProjectionCache } : {}),
553
- githubClient: deps.githubClient,
554
- mintInstallationToken: deps.mintInstallationToken,
555
- sessionService: new ContainerSessionService({ secret: sessionSecret }),
556
- model: resolveAgentConfig(deps.config.agents.routing, 'architect').ref,
557
- proxyBaseUrl: `${publicUrl.replace(/\/+$/, '')}/v1`,
558
- githubApiBase: deps.config.github.apiBase,
559
- // The scaffolder installs dependencies too — forward the workspace's
560
- // private-registry entries exactly as the implementation executor does.
561
- ...(deps.resolvePackageRegistries
562
- ? { resolvePackageRegistries: deps.resolvePackageRegistries }
563
- : {}),
564
- });
565
- }
566
- /**
567
- * Build the live ENVIRONMENT-PROVIDER CONFIG REPAIR agent (PR #416 increment 2) when its
568
- * prerequisites are met — the same container prerequisites as the bootstrapper PLUS a
569
- * registered backend that supports agent repair (`describeRepairAgent`). The stock manifest
570
- * provider has no repair support, so this stays undefined there; it wires only when a custom
571
- * backend registered into the env-backend registry implements repair (so local inherits it
572
- * too). NOT the repo bootstrapper: an ordinary clone→edit→push coding job, no history reset.
573
- */
574
- function selectNodeEnvConfigRepairer(deps) {
575
- const publicUrl = deps.env.PUBLIC_URL?.trim();
576
- const sessionSecret = deps.config.auth.sessionSecret;
577
- // Prefer the internal override (the conformance suite's fake repair provider), else scan
578
- // the env-backend registry for the first repair-capable backend. Built-ins don't support
579
- // repair, so this is undefined on a stock deployment; a third-party backend wires it.
580
- const repairUrlPolicy = resolveUrlSafetyPolicy(deps.config.environments);
581
- const environmentProvider = !deps.resolveTransport
582
- ? undefined
583
- : (deps.override ??
584
- deps.environmentBackendRegistry.findRepairCapable(repairUrlPolicy ? { urlPolicy: repairUrlPolicy } : {}));
585
- if (!deps.resolveTransport ||
586
- !publicUrl ||
587
- !sessionSecret ||
588
- !deps.mintInstallationToken ||
589
- !environmentProvider ||
590
- typeof environmentProvider.describeRepairAgent !== 'function') {
591
- return undefined;
592
- }
593
- // A config fix is coding work, so it follows the `coder` kind's routing. The repair runs on
594
- // the Pi harness over the LLM proxy, so the routed model MUST be proxyable. Surface a
595
- // misconfiguration HERE (at wiring) rather than letting every repair dispatch throw deep in a
596
- // request: if `coder` is routed to a non-proxyable model (e.g. an individual subscription
597
- // vendor), leave the fallback unwired — bootstrap then returns the validation issues, exactly
598
- // as it does when no provider supports repair.
599
- const model = resolveAgentConfig(deps.config.agents.routing, 'coder').ref;
600
- if (!isProxyableProvider(model.provider)) {
601
- logger.warn({ provider: model.provider }, 'env-config repair: the coder routing model is not proxyable by the LLM proxy; ' +
602
- 'the agent config-repair fallback is disabled.');
603
- return undefined;
604
- }
605
- return new ContainerEnvConfigRepairer({
606
- resolveTransport: deps.resolveTransport,
607
- installationRepository: deps.installationRepository,
608
- mintInstallationToken: deps.mintInstallationToken,
609
- sessionService: new ContainerSessionService({ secret: sessionSecret }),
610
- environmentProvider,
611
- model,
612
- proxyBaseUrl: `${publicUrl.replace(/\/+$/, '')}/v1`,
613
- githubApiBase: deps.config.github.apiBase,
614
- });
615
- }
616
- /**
617
- * Build the GitHub-issue tracker filer for the tech-debt pipeline when the GitHub
618
- * App is configured. It resolves the service's repo from the workspace's
619
- * `github_repos` projection and mints a short-lived token from that workspace's OWN
620
- * App installation (per-tenant) — the same infra the container executor uses — then
621
- * files the issue via the token. Returns undefined when the App isn't configured (the
622
- * GitHub tracker then passes through). A run whose service isn't linked to a repo
623
- * resolves to null (a clean pass-through, not a run failure).
624
- */
625
- function buildNodeGitHubIssueFiler(config, registry, resolveRepoTarget) {
626
- if (!registry)
627
- return undefined;
628
- return async (request) => {
629
- let repo;
630
- try {
631
- repo = await resolveRepoTarget(request.workspaceId, request.frameId);
632
- }
633
- catch {
634
- // The service isn't linked to a repo — nothing to file against; pass through.
635
- return null;
636
- }
637
- if (!repo)
638
- return null;
639
- const token = await registry.installationToken(repo.installationId);
640
- const issue = await createGitHubIssueViaToken({
641
- fetchImpl: fetch,
642
- token,
643
- owner: repo.owner,
644
- repo: repo.name,
645
- title: request.title,
646
- body: request.body,
647
- apiBase: config.github.apiBase,
648
- });
649
- return { externalId: `${repo.owner}/${repo.name}#${issue.number}`, url: issue.url };
650
- };
651
- }
652
268
  /**
653
269
  * The Node composition root: assemble the framework-agnostic domain `Core` with
654
270
  * Drizzle/Postgres repositories + Node implementations of the runtime ports, then