@cat-factory/node-server 0.107.6 → 0.107.8
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-executor-deps.d.ts +114 -0
- package/dist/container-executor-deps.d.ts.map +1 -0
- package/dist/container-executor-deps.js +405 -0
- package/dist/container-executor-deps.js.map +1 -0
- package/dist/container.d.ts +3 -22
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js +10 -394
- package/dist/container.js.map +1 -1
- package/package.json +18 -18
package/dist/container.js
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
|
-
import { AiAgentExecutor, defaultAgentKindRegistry, defaultInitiativePresetRegistry, inlineWebSearchOptionsFromEnv,
|
|
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,
|
|
9
|
-
import { CompositeNotificationChannel, DEFAULT_MODEL_PRESET_ID,
|
|
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 {
|
|
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
|
-
//
|
|
51
|
-
//
|
|
52
|
-
|
|
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
|