From 9b8b5c8feedb2095f9e30d54bcff1c86414bfa1b Mon Sep 17 00:00:00 2001 From: Evgeny Shurakov Date: Wed, 29 Apr 2026 13:20:11 +0200 Subject: [PATCH] refactor(cloud-agent-next): migrate to git-token-service RPC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the in-worker GitHubTokenService and InstallationLookupService with calls to the shared git-token-service Worker via a GIT_TOKEN_SERVICE service binding, and drop the now-redundant token fetching in the web app routers. - Wire GIT_TOKEN_SERVICE binding in wrangler.jsonc; drop GITHUB_APP_ID, GITHUB_LITE_APP_ID, and GITHUB_TOKEN_CACHE KV bindings. Restore the HYPERDRIVE binding for an upcoming feature. - Resolve GitHub tokens for repo + managed GitLab tokens through a new shared helper (src/services/git-token-service-client.ts) used from both session-prepare and async-preparation paths. - Persist gitlabTokenManaged in session metadata so the DO can refresh GitLab tokens on startExecutionV2 via refreshManagedGitLabToken. Successful refreshes are persisted via updateGitToken so later transient failures fall back to the last-known working token instead of the stale prepare-time token. Treat gitlabTokenManaged === undefined as managed for backwards compatibility with pre-existing sessions. - Fail closed on GitLab access revocation: no_integration_found and invalid_org_id reasons throw BAD_REQUEST at session prepare and startExecutionV2 instead of falling back to the stored token, so the session cannot keep using a managed token after the integration or org access was removed. Transient failures retain the last-known token fallback. - Parameterize DurableObject on the base class, removing 28 'as unknown as WorkerEnv' casts in CloudAgentSession and aligning with the rest of the repo's DO pattern. - Extract cloudflare-git-token-service into a standalone 'git-token-service' dev group shared by cloud-agent, app-builder, and gastown. Switch its dev script to 'wrangler dev --env dev' so the locally-running worker is named 'git-token-service-dev', matching what cloud-agent-next and the security workers reference in their dev service bindings. - Web routers (personal + org) no longer fetch GitHub/GitLab tokens for prepareSession/sendMessage — cloud-agent-next handles token resolution and refresh centrally. --- .../src/routers/cloud-agent-next-router.ts | 46 +---- .../organization-cloud-agent-next-router.ts | 54 ++---- dev/local/services.ts | 30 ++- pnpm-lock.yaml | 6 - services/cloud-agent-next/.dev.vars.example | 17 +- services/cloud-agent-next/AGENTS.md | 4 +- services/cloud-agent-next/README.md | 21 +- services/cloud-agent-next/package.json | 2 - .../src/persistence/CloudAgentSession.ts | 120 ++++++++---- .../src/persistence/async-preparation.ts | 65 ++++--- .../src/persistence/schemas.ts | 1 + .../cloud-agent-next/src/persistence/types.ts | 2 + services/cloud-agent-next/src/router.test.ts | 5 +- .../src/router/handlers/session-prepare.ts | 112 ++++------- .../src/services/git-token-service-client.ts | 85 ++++++++ .../src/services/github-token-service.ts | 183 ------------------ .../services/installation-lookup-service.ts | 119 ------------ services/cloud-agent-next/src/types.ts | 57 ++++-- .../worker-configuration.d.ts | 147 ++++++++------ services/cloud-agent-next/wrangler.jsonc | 30 ++- services/git-token-service/package.json | 2 +- 21 files changed, 446 insertions(+), 662 deletions(-) create mode 100644 services/cloud-agent-next/src/services/git-token-service-client.ts delete mode 100644 services/cloud-agent-next/src/services/github-token-service.ts delete mode 100644 services/cloud-agent-next/src/services/installation-lookup-service.ts diff --git a/apps/web/src/routers/cloud-agent-next-router.ts b/apps/web/src/routers/cloud-agent-next-router.ts index 0d28c2dac4..8429c8d8de 100644 --- a/apps/web/src/routers/cloud-agent-next-router.ts +++ b/apps/web/src/routers/cloud-agent-next-router.ts @@ -10,12 +10,8 @@ import { mergeProfileConfiguration, ProfileNotFoundError, } from '@/lib/agent/profile-session-config'; +import { fetchGitHubRepositoriesForUser } from '@/lib/cloud-agent/github-integration-helpers'; import { - getGitHubTokenForUser, - fetchGitHubRepositoriesForUser, -} from '@/lib/cloud-agent/github-integration-helpers'; -import { - getGitLabTokenForUser, getGitLabInstanceUrlForUser, buildGitLabCloneUrl, fetchGitLabRepositoriesForUser, @@ -80,28 +76,19 @@ export const cloudAgentNextRouter = createTRPCRouter({ setupCommands, }); - // Determine git source: GitLab uses gitUrl/gitToken, GitHub uses githubRepo/githubToken + // Determine git source: GitLab uses gitUrl, GitHub uses githubRepo. + // Tokens are resolved inside cloud-agent-next via GIT_TOKEN_SERVICE. let gitParams: { githubRepo?: string; gitUrl?: string; - gitToken?: string; platform?: 'github' | 'gitlab'; }; if (gitlabProject) { - // GitLab flow: convert gitlabProject to gitUrl + gitToken - const gitToken = await getGitLabTokenForUser(ctx.user.id); - if (!gitToken) { - throw new TRPCError({ - code: 'BAD_REQUEST', - message: 'No GitLab integration found. Please connect your GitLab account first.', - }); - } const instanceUrl = await getGitLabInstanceUrlForUser(ctx.user.id); const gitUrl = buildGitLabCloneUrl(gitlabProject, instanceUrl); - gitParams = { gitUrl, gitToken, platform: PLATFORM.GITLAB }; + gitParams = { gitUrl, platform: PLATFORM.GITLAB }; } else { - // GitHub flow: use githubRepo (token will be fetched in cloud-agent-next) gitParams = { githubRepo, platform: PLATFORM.GITHUB }; } @@ -163,29 +150,10 @@ export const cloudAgentNextRouter = createTRPCRouter({ const authToken = generateCloudAgentToken(ctx.user); const client = createCloudAgentNextClient(authToken); - // Determine platform to fetch the correct token - const session = await client.getSession(input.cloudAgentSessionId); - let githubToken: string | undefined; - let gitToken: string | undefined; - - if (session.platform === 'gitlab') { - gitToken = await getGitLabTokenForUser(ctx.user.id); - if (!gitToken) { - throw new TRPCError({ - code: 'BAD_REQUEST', - message: 'No GitLab integration found. Please connect your GitLab account first.', - }); - } - } else { - githubToken = await getGitHubTokenForUser(ctx.user.id); - } - + // Tokens are refreshed inside cloud-agent-next (GitHub App installation + // for GitHub, GIT_TOKEN_SERVICE for managed GitLab). try { - return await client.sendMessage({ - ...input, - githubToken, - gitToken, - }); + return await client.sendMessage(input); } catch (error) { rethrowAsPaymentRequired(error); throw error; diff --git a/apps/web/src/routers/organizations/organization-cloud-agent-next-router.ts b/apps/web/src/routers/organizations/organization-cloud-agent-next-router.ts index 563dedbcb1..75477e2dd8 100644 --- a/apps/web/src/routers/organizations/organization-cloud-agent-next-router.ts +++ b/apps/web/src/routers/organizations/organization-cloud-agent-next-router.ts @@ -14,12 +14,8 @@ import { organizationMemberProcedure, organizationMemberMutationProcedure, } from '@/routers/organizations/utils'; +import { fetchGitHubRepositoriesForOrganization } from '@/lib/cloud-agent/github-integration-helpers'; import { - getGitHubTokenForOrganization, - fetchGitHubRepositoriesForOrganization, -} from '@/lib/cloud-agent/github-integration-helpers'; -import { - getGitLabTokenForOrganization, getGitLabInstanceUrlForOrganization, buildGitLabCloneUrl, fetchGitLabRepositoriesForOrganization, @@ -141,28 +137,19 @@ export const organizationCloudAgentNextRouter = createTRPCRouter({ setupCommands, }); - // Determine git source: GitLab uses gitUrl/gitToken, GitHub uses githubRepo + // Determine git source: GitLab uses gitUrl, GitHub uses githubRepo. + // Tokens are resolved inside cloud-agent-next via GIT_TOKEN_SERVICE. let gitParams: { githubRepo?: string; gitUrl?: string; - gitToken?: string; platform?: 'github' | 'gitlab'; }; if (gitlabProject) { - // GitLab flow: convert gitlabProject to gitUrl + gitToken - const gitToken = await getGitLabTokenForOrganization(organizationId); - if (!gitToken) { - throw new TRPCError({ - code: 'BAD_REQUEST', - message: 'No GitLab integration found. Please connect your GitLab account first.', - }); - } const instanceUrl = await getGitLabInstanceUrlForOrganization(organizationId); const gitUrl = buildGitLabCloneUrl(gitlabProject, instanceUrl); - gitParams = { gitUrl, gitToken, platform: PLATFORM.GITLAB }; + gitParams = { gitUrl, platform: PLATFORM.GITLAB }; } else { - // GitHub flow: use githubRepo (token will be fetched in cloud-agent-next) gitParams = { githubRepo, platform: PLATFORM.GITHUB }; } @@ -226,30 +213,19 @@ export const organizationCloudAgentNextRouter = createTRPCRouter({ const authToken = generateCloudAgentToken(ctx.user); const client = createCloudAgentNextClient(authToken); - const { organizationId, ...messageInput } = input; - - // Determine platform to fetch the correct token - const session = await client.getSession(messageInput.cloudAgentSessionId); - let githubToken: string | undefined; - let gitToken: string | undefined; - - if (session.platform === 'gitlab') { - gitToken = await getGitLabTokenForOrganization(organizationId); - if (!gitToken) { - throw new TRPCError({ - code: 'BAD_REQUEST', - message: 'No GitLab integration found. Please connect your GitLab account first.', - }); - } - } else { - githubToken = await getGitHubTokenForOrganization(organizationId); - } - + // Tokens are refreshed inside cloud-agent-next (GitHub App installation + // for GitHub, GIT_TOKEN_SERVICE for managed GitLab). organizationId is + // consumed by the membership middleware; it is not forwarded. try { return await client.sendMessage({ - ...messageInput, - githubToken, - gitToken, + cloudAgentSessionId: input.cloudAgentSessionId, + prompt: input.prompt, + mode: input.mode, + model: input.model, + variant: input.variant, + autoCommit: input.autoCommit, + messageId: input.messageId, + images: input.images, }); } catch (error) { rethrowAsPaymentRequired(error); diff --git a/dev/local/services.ts b/dev/local/services.ts index 80e6316c3a..31e47772c6 100644 --- a/dev/local/services.ts +++ b/dev/local/services.ts @@ -15,11 +15,22 @@ type ServiceGroup = { const groups: ServiceGroup[] = [ { id: 'core', label: 'Core', alwaysOn: true }, - { id: 'kiloclaw', label: 'KiloClaw', alwaysOn: false, sectionBreakBefore: true }, - { id: 'cloud-agent', label: 'Cloud Agent', alwaysOn: false }, + { + id: 'git-token-service', + label: 'Git Tokens', + alwaysOn: false, + sectionBreakBefore: true, + }, + { id: 'kiloclaw', label: 'KiloClaw', alwaysOn: false }, + { + id: 'cloud-agent', + label: 'Cloud Agent', + alwaysOn: false, + groupDependsOn: ['git-token-service'], + }, { id: 'code-review', label: 'Code Review', alwaysOn: false, groupDependsOn: ['cloud-agent'] }, { id: 'app-builder', label: 'App Builder', alwaysOn: false, groupDependsOn: ['cloud-agent'] }, - { id: 'gastown', label: 'Gastown', alwaysOn: false }, + { id: 'gastown', label: 'Gastown', alwaysOn: false, groupDependsOn: ['git-token-service'] }, { id: 'auto-triage', label: 'Auto Triage', @@ -59,7 +70,7 @@ const serviceMeta: Record = { // cloud-agent 'cloud-agent-next': { group: 'cloud-agent', - dependsOn: ['postgres', 'nextjs', 'cloudflare-session-ingest'], + dependsOn: ['postgres', 'nextjs', 'cloudflare-session-ingest', 'cloudflare-git-token-service'], dir: 'services/cloud-agent-next', useLanIp: true, }, @@ -73,6 +84,12 @@ const serviceMeta: Record = { dependsOn: ['postgres'], dir: 'services/session-ingest', }, + // git-token-service (shared by cloud-agent, app-builder, gastown) + 'cloudflare-git-token-service': { + group: 'git-token-service', + dependsOn: ['postgres'], + dir: 'services/git-token-service', + }, // app-builder 'app-builder-tunnel': { group: 'app-builder', dependsOn: [] }, 'cloudflare-app-builder': { @@ -86,11 +103,6 @@ const serviceMeta: Record = { dependsOn: ['postgres'], dir: 'services/db-proxy', }, - 'cloudflare-git-token-service': { - group: 'app-builder', - dependsOn: ['postgres'], - dir: 'services/git-token-service', - }, // code-review 'cloudflare-code-review-infra': { group: 'code-review', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cab9ab7eaa..5f3b05b078 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1297,18 +1297,12 @@ importers: '@hono/trpc-server': specifier: ^0.4.2 version: 0.4.2(@trpc/server@11.13.0(typescript@5.9.3))(hono@4.12.8) - '@kilocode/db': - specifier: workspace:* - version: link:../../packages/db '@kilocode/encryption': specifier: workspace:* version: link:../../packages/encryption '@kilocode/worker-utils': specifier: workspace:* version: link:../../packages/worker-utils - '@octokit/auth-app': - specifier: 'catalog:' - version: 8.2.0 '@trpc/server': specifier: 'catalog:' version: 11.13.0(typescript@5.9.3) diff --git a/services/cloud-agent-next/.dev.vars.example b/services/cloud-agent-next/.dev.vars.example index e648c7522c..96504553ac 100644 --- a/services/cloud-agent-next/.dev.vars.example +++ b/services/cloud-agent-next/.dev.vars.example @@ -49,22 +49,7 @@ PER_SESSION_SANDBOX_ORG_IDS= GITHUB_APP_SLUG=kiloconnect-development GITHUB_APP_BOT_USER_ID=242397087 -# GitHub App credentials for generating installation access tokens -# Used by GitHubTokenService to authenticate with GitHub API on behalf of app installations -# GITHUB_APP_ID: The numeric App ID from GitHub App settings -# GITHUB_APP_PRIVATE_KEY: The raw PKCS#8 private key (use \n for newlines in env var) -# Note that the nextjs app uses PKCS#1 but the worker uses WebCrypto and requires a PKCS#8 -GITHUB_APP_ID=2245043 -# @pkcs8 -GITHUB_APP_PRIVATE_KEY= - -# GitHub Lite App credentials (for OSS organizations with read-only permissions) -# Same format as standard app credentials above -GITHUB_LITE_APP_ID= -# @pkcs8 -GITHUB_LITE_APP_PRIVATE_KEY= - -# GitHub Lite App slug and bot user ID for git commit attribution (optional) +# GitHub Lite App identity for git commit attribution on OSS organizations (optional) GITHUB_LITE_APP_SLUG= GITHUB_LITE_APP_BOT_USER_ID= diff --git a/services/cloud-agent-next/AGENTS.md b/services/cloud-agent-next/AGENTS.md index 6a10afff68..f0e17fc1b2 100644 --- a/services/cloud-agent-next/AGENTS.md +++ b/services/cloud-agent-next/AGENTS.md @@ -4,7 +4,7 @@ This file provides guidance to AI coding agents working in this repository. ## Project Overview -Cloudflare Worker that powers Kilocode Cloud Agents. It exposes a tRPC API for session preparation and execution, streams output over WebSockets, and runs the Kilocode CLI inside Cloudflare Sandbox containers. Durable Objects track sessions; Hyperdrive is used for Postgres lookups (for example, GitHub App installation IDs). The wrapper in `wrapper/` is a core component that brokers Kilocode CLI events into the worker’s `/ingest` WebSocket and handles job lifecycle. +Cloudflare Worker that powers Kilocode Cloud Agents. It exposes a tRPC API for session preparation and execution, streams output over WebSockets, and runs the Kilocode CLI inside Cloudflare Sandbox containers. Durable Objects track sessions; git tokens (GitHub App installation tokens, managed GitLab tokens) are resolved via the shared `git-token-service` Worker. The wrapper in `wrapper/` is a core component that brokers Kilocode CLI events into the worker’s `/ingest` WebSocket and handles job lifecycle. ## Development Commands @@ -108,7 +108,7 @@ This pattern blocks API endpoints from running for external contributors who don - `src/persistence/` - Durable Object schema + migrations - `src/websocket/` - WebSocket ingest + filters - `src/utils/` - Shared helpers (encryption, retries, SQL helpers) -- `wrangler.jsonc` - Bindings: R2, Hyperdrive, KV, queues, containers +- `wrangler.jsonc` - Bindings: R2, Hyperdrive, queues, containers, service bindings (`SESSION_INGEST`, `GIT_TOKEN_SERVICE`) - `vitest.config.ts` - Unit test config - `vitest.workers.config.ts` - Integration test config - `wrapper/` - Wrapper build shipped into the sandbox diff --git a/services/cloud-agent-next/README.md b/services/cloud-agent-next/README.md index 9f8997b9e7..42807af158 100644 --- a/services/cloud-agent-next/README.md +++ b/services/cloud-agent-next/README.md @@ -832,28 +832,21 @@ This enables: #### GitHub App Token Generation -For V2 routes (`sendMessageV2`, `initiateFromKilocodeSessionV2`), the cloud-agent generates GitHub App installation tokens on-demand. +For V2 routes (`sendMessageV2`, `initiateFromKilocodeSessionV2`), the cloud-agent resolves GitHub App installation tokens via the shared `git-token-service` Worker (`GIT_TOKEN_SERVICE` service binding). **How it works:** -1. **Automatic installation lookup**: The worker automatically looks up the GitHub App installation ID from the database via Hyperdrive. The lookup verifies the user has access to the repository's organization. -2. **On-demand token generation**: When execution starts, `GitHubTokenService` generates a fresh token using `@octokit/auth-app` -3. **KV caching**: Tokens are cached in Cloudflare KV with 30-minute TTL (tokens valid for 1 hour) -4. **Cache key format**: `github-token:installation:{installationId}` +1. **Delegated resolution**: The worker calls `git-token-service` RPC (see `src/services/git-token-service-client.ts`) to look up the installation ID for the repo and mint an installation access token. Token caching and GitHub App credentials live in `git-token-service`. +2. **Managed GitLab tokens**: The same client resolves and refreshes managed GitLab tokens; `gitlabTokenManaged` is persisted in session metadata so the session DO can refresh it on `startExecutionV2`. **Configuration:** -The worker requires these environment variables: - -- `GITHUB_APP_ID`: GitHub App ID (configured in `wrangler.jsonc`) -- `GITHUB_APP_PRIVATE_KEY`: RSA private key for the GitHub App (set via `wrangler secret put`) -- `GITHUB_TOKEN_CACHE`: KV namespace binding for token caching -- `HYPERDRIVE`: Hyperdrive binding for database access (installation ID lookup) +- `GIT_TOKEN_SERVICE`: service binding to the `git-token-service` Worker (prod) / `git-token-service-dev` (dev). Configured in `wrangler.jsonc`. +- `GITHUB_APP_SLUG` / `GITHUB_APP_BOT_USER_ID` (and the Lite equivalents): used only for git commit author attribution — not for token generation. **Benefits:** -- No need to pass `githubInstallationId` — automatically resolved from database -- Tokens generated closer to where they're used (reduced latency) +- No need to pass `githubInstallationId` — resolved centrally by `git-token-service` +- GitHub App credentials and token cache kept in a single shared Worker - Fresh tokens on-demand rather than at session start -- Rate limit protection via KV caching - No token expiry issues during long sessions diff --git a/services/cloud-agent-next/package.json b/services/cloud-agent-next/package.json index d42556317d..e2cfb4ea50 100644 --- a/services/cloud-agent-next/package.json +++ b/services/cloud-agent-next/package.json @@ -29,10 +29,8 @@ "dependencies": { "@cloudflare/sandbox": "0.8.9", "@hono/trpc-server": "^0.4.2", - "@kilocode/db": "workspace:*", "@kilocode/encryption": "workspace:*", "@kilocode/worker-utils": "workspace:*", - "@octokit/auth-app": "catalog:", "@trpc/server": "catalog:", "drizzle-orm": "catalog:", "hono": "catalog:", diff --git a/services/cloud-agent-next/src/persistence/CloudAgentSession.ts b/services/cloud-agent-next/src/persistence/CloudAgentSession.ts index c5c64d9880..a238300813 100644 --- a/services/cloud-agent-next/src/persistence/CloudAgentSession.ts +++ b/services/cloud-agent-next/src/persistence/CloudAgentSession.ts @@ -73,12 +73,12 @@ import { isExecutionError } from '../execution/errors.js'; import type { Env as WorkerEnv, SandboxId } from '../types.js'; import { generateSandboxId, getSandboxNamespace } from '../sandbox-id.js'; -import { GitHubTokenService } from '../services/github-token-service.js'; import { validateStreamTicket } from '../auth.js'; import { getSandbox } from '@cloudflare/sandbox'; import { stopWrapper } from '../kilo/wrapper-manager.js'; import { SessionService } from '../session-service.js'; import { executePreparationSteps } from './async-preparation.js'; +import { resolveManagedGitLabToken } from '../services/git-token-service-client.js'; // --------------------------------------------------------------------------- // Alarm Constants @@ -142,7 +142,7 @@ function extractAssistantTextFromParts(parts: AssistantMessagePart[]): string { return pieces.join('').trim(); } -export class CloudAgentSession extends DurableObject { +export class CloudAgentSession extends DurableObject { private executionQueries: ExecutionQueries; private eventQueries: EventQueries; private leaseQueries: LeaseQueries; @@ -165,7 +165,7 @@ export class CloudAgentSession extends DurableObject { gateResult?: 'pass' | 'fail' ): Promise { const metadata = await this.getMetadata(); - const callbackQueue = (this.env as unknown as WorkerEnv).CALLBACK_QUEUE; + const callbackQueue = this.env.CALLBACK_QUEUE; if (!metadata?.callbackTarget || !callbackQueue) { return; @@ -211,7 +211,7 @@ export class CloudAgentSession extends DurableObject { }); } - constructor(ctx: DurableObjectState, env: Env) { + constructor(ctx: DurableObjectState, env: WorkerEnv) { super(ctx, env); // Extract sessionId from DO name pattern: "userId:sessionId" @@ -865,6 +865,7 @@ export class CloudAgentSession extends DurableObject { gitUrl?: string; gitToken?: string; platform?: 'github' | 'gitlab'; + gitlabTokenManaged?: boolean; envVars?: Record; encryptedSecrets?: EncryptedSecrets; setupCommands?: string[]; @@ -1003,7 +1004,7 @@ export class CloudAgentSession extends DurableObject { private async runPreparationAsync(input: PreparationInput): Promise { const sessionId = input.sessionId as SessionId; const prepExecutionId: EventSourceId = `prep_${input.sessionId}`; - const env = this.env as unknown as WorkerEnv; + const env = this.env; const emitProgress = ( step: PreparingStep, @@ -1083,8 +1084,9 @@ export class CloudAgentSession extends DurableObject { githubInstallationId: result.resolvedInstallationId, githubAppType: result.resolvedGithubAppType, gitUrl: input.gitUrl, - gitToken: input.gitToken, + gitToken: result.resolvedGitToken, platform: input.platform, + gitlabTokenManaged: result.gitlabTokenManaged, envVars: input.envVars, encryptedSecrets: input.encryptedSecrets, setupCommands: input.setupCommands, @@ -1307,7 +1309,7 @@ export class CloudAgentSession extends DurableObject { await svc.deleteCliSessionViaSessionIngest( metadata.kiloSessionId, metadata.userId, - this.env as unknown as WorkerEnv, + this.env, { onlyIfEmpty: true } ); } catch { @@ -1604,22 +1606,22 @@ export class CloudAgentSession extends DurableObject { /** Initial reaper interval used only by {@link ensureAlarmScheduled}. * Steady-state intervals are {@link REAPER_IDLE_INTERVAL_MS} / {@link REAPER_ACTIVE_INTERVAL_MS}. */ private getReaperIntervalMs(): number { - const value = Number((this.env as unknown as WorkerEnv).REAPER_INTERVAL_MS); + const value = Number(this.env.REAPER_INTERVAL_MS); return Number.isFinite(value) && value > 0 ? value : REAPER_INTERVAL_MS_DEFAULT; } private getStaleThresholdMs(): number { - const value = Number((this.env as unknown as WorkerEnv).STALE_THRESHOLD_MS); + const value = Number(this.env.STALE_THRESHOLD_MS); return Number.isFinite(value) && value > 0 ? value : STALE_THRESHOLD_MS; } private getPendingStartTimeoutMs(): number { - const value = Number((this.env as unknown as WorkerEnv).PENDING_START_TIMEOUT_MS); + const value = Number(this.env.PENDING_START_TIMEOUT_MS); return Number.isFinite(value) && value > 0 ? value : PENDING_START_TIMEOUT_MS_DEFAULT; } private getKiloServerIdleTimeoutMs(): number { - const value = Number((this.env as unknown as WorkerEnv).KILO_SERVER_IDLE_TIMEOUT_MS); + const value = Number(this.env.KILO_SERVER_IDLE_TIMEOUT_MS); return Number.isFinite(value) && value > 0 ? value : KILO_SERVER_IDLE_TIMEOUT_MS_DEFAULT; } @@ -1670,17 +1672,16 @@ export class CloudAgentSession extends DurableObject { .info('Stopping idle kilo server'); try { - const workerEnv = this.env as unknown as WorkerEnv; const sandboxId = metadata.sandboxId ?? (await generateSandboxId( - workerEnv.PER_SESSION_SANDBOX_ORG_IDS, + this.env.PER_SESSION_SANDBOX_ORG_IDS, metadata.orgId, metadata.userId, metadata.sessionId, metadata.botId )); - const sandbox = getSandbox(getSandboxNamespace(workerEnv, sandboxId), sandboxId); + const sandbox = getSandbox(getSandboxNamespace(this.env, sandboxId), sandboxId); const rpcStart = Date.now(); logger @@ -1733,17 +1734,16 @@ export class CloudAgentSession extends DurableObject { const metadata = await this.getMetadata(); if (!metadata) return; - const workerEnvForKeepAlive = this.env as unknown as WorkerEnv; const sandboxId = metadata.sandboxId ?? (await generateSandboxId( - workerEnvForKeepAlive.PER_SESSION_SANDBOX_ORG_IDS, + this.env.PER_SESSION_SANDBOX_ORG_IDS, metadata.orgId, metadata.userId, metadata.sessionId, metadata.botId )); - const sandbox = getSandbox(getSandboxNamespace(workerEnvForKeepAlive, sandboxId), sandboxId); + const sandbox = getSandbox(getSandboxNamespace(this.env, sandboxId), sandboxId); await sandbox.setSleepAfter(SANDBOX_SLEEP_AFTER_SECONDS); } catch (error) { logger @@ -2227,23 +2227,21 @@ export class CloudAgentSession extends DurableObject { if (!this.orchestrator) { const deps: OrchestratorDeps = { getSandbox: async (sandboxId: string) => { - const workerEnvForOrch = this.env as unknown as WorkerEnv; - return getSandbox(getSandboxNamespace(workerEnvForOrch, sandboxId), sandboxId, { + return getSandbox(getSandboxNamespace(this.env, sandboxId), sandboxId, { sleepAfter: SANDBOX_SLEEP_AFTER_SECONDS, }); }, getSessionStub: (userId, sessionId) => { const doKey = `${userId}:${sessionId}`; - const id = (this.env as unknown as WorkerEnv).CLOUD_AGENT_SESSION.idFromName(doKey); - return (this.env as unknown as WorkerEnv).CLOUD_AGENT_SESSION.get(id); + const id = this.env.CLOUD_AGENT_SESSION.idFromName(doKey); + return this.env.CLOUD_AGENT_SESSION.get(id); }, getIngestUrl: (sessionId, userId) => { - const workerUrl = - (this.env as unknown as WorkerEnv).WORKER_URL || 'http://localhost:8788'; + const workerUrl = this.env.WORKER_URL || 'http://localhost:8788'; // Encode userId to handle OAuth IDs like "oauth/google:123" that contain slashes return `${workerUrl}/sessions/${encodeURIComponent(userId)}/${sessionId}/ingest`; }, - env: this.env as unknown as WorkerEnv, + env: this.env, }; this.orchestrator = new ExecutionOrchestrator(deps); } @@ -2271,15 +2269,51 @@ export class CloudAgentSession extends DurableObject { }; } - private getGitHubTokenService(): GitHubTokenService { - const env = this.env as unknown as WorkerEnv; - return new GitHubTokenService({ - GITHUB_TOKEN_CACHE: env.GITHUB_TOKEN_CACHE, - GITHUB_APP_ID: env.GITHUB_APP_ID, - GITHUB_APP_PRIVATE_KEY: env.GITHUB_APP_PRIVATE_KEY, - GITHUB_LITE_APP_ID: env.GITHUB_LITE_APP_ID, - GITHUB_LITE_APP_PRIVATE_KEY: env.GITHUB_LITE_APP_PRIVATE_KEY, + /** + * Refresh a managed GitLab token via GIT_TOKEN_SERVICE. Logs and returns + * the current value if the refresh fails with a transient reason so callers + * can keep running with the last-known token (best effort). Successful + * refreshes are persisted to metadata so a later refresh failure falls back + * to the most recent working token rather than a stale prepare-time token. + * + * Access-revocation reasons (`no_integration_found`, `invalid_org_id`) fail + * closed by throwing `BAD_REQUEST`: the stored token is no longer authorized + * (integration was removed, or user lost access to the org) and continuing + * to use it would bypass revocation. + * + * `gitlabTokenManaged === false` (explicitly set during prepare when the + * caller supplied their own PAT) skips refresh. `undefined` — i.e. sessions + * prepared before this flag existed — is treated as managed for backwards + * compatibility, since the previous code path relied on the web app + * injecting a fresh managed token on every `sendMessage`. + */ + private async refreshManagedGitLabToken( + metadata: CloudAgentSessionState, + current: string | undefined + ): Promise { + if (metadata.platform !== 'gitlab' || metadata.gitlabTokenManaged === false) { + return current; + } + const result = await resolveManagedGitLabToken(this.env, { + userId: metadata.userId, + orgId: metadata.orgId, }); + if (result.success) { + if (result.token !== current) { + await this.updateGitToken(result.token); + } + return result.token; + } + if (result.reason === 'no_integration_found' || result.reason === 'invalid_org_id') { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'No GitLab integration found. Please connect your GitLab account first.', + }); + } + logger + .withFields({ reason: result.reason, sessionId: metadata.sessionId }) + .warn('Managed GitLab token refresh failed; using last-known value'); + return current; } /** @@ -2333,7 +2367,7 @@ export class CloudAgentSession extends DurableObject { } const sandboxId = await generateSandboxId( - (this.env as unknown as WorkerEnv).PER_SESSION_SANDBOX_ORG_IDS, + this.env.PER_SESSION_SANDBOX_ORG_IDS, request.orgId, request.userId, sessionId, @@ -2451,7 +2485,7 @@ export class CloudAgentSession extends DurableObject { let githubToken = metadata.githubToken; if (metadata.githubInstallationId) { const appType = metadata.githubAppType || 'standard'; - githubToken = await this.getGitHubTokenService().getToken( + githubToken = await this.env.GIT_TOKEN_SERVICE.getToken( metadata.githubInstallationId, appType ); @@ -2463,10 +2497,12 @@ export class CloudAgentSession extends DurableObject { ); } + const gitToken = await this.refreshManagedGitLabToken(metadata, metadata.gitToken); + const sandboxId = metadata.sandboxId ?? (await generateSandboxId( - (this.env as unknown as WorkerEnv).PER_SESSION_SANDBOX_ORG_IDS, + this.env.PER_SESSION_SANDBOX_ORG_IDS, metadata.orgId, metadata.userId, metadata.sessionId, @@ -2478,7 +2514,7 @@ export class CloudAgentSession extends DurableObject { githubRepo: metadata.githubRepo, githubToken, gitUrl: metadata.gitUrl, - gitToken: metadata.gitToken, + gitToken, envVars: metadata.envVars, encryptedSecrets: metadata.encryptedSecrets, setupCommands: metadata.setupCommands, @@ -2545,7 +2581,7 @@ export class CloudAgentSession extends DurableObject { let githubToken = request.tokenOverrides?.githubToken ?? metadata.githubToken; if (!request.tokenOverrides?.githubToken && metadata.githubInstallationId) { const appType = metadata.githubAppType || 'standard'; - githubToken = await this.getGitHubTokenService().getToken( + githubToken = await this.env.GIT_TOKEN_SERVICE.getToken( metadata.githubInstallationId, appType ); @@ -2557,10 +2593,16 @@ export class CloudAgentSession extends DurableObject { ); } + // Refresh GitLab token if auto-managed (override wins when provided) + const overrideGitToken = request.tokenOverrides?.gitToken; + const gitToken = overrideGitToken + ? overrideGitToken + : await this.refreshManagedGitLabToken(metadata, metadata.gitToken); + const sandboxId = metadata.sandboxId ?? (await generateSandboxId( - (this.env as unknown as WorkerEnv).PER_SESSION_SANDBOX_ORG_IDS, + this.env.PER_SESSION_SANDBOX_ORG_IDS, metadata.orgId, metadata.userId, metadata.sessionId, @@ -2570,7 +2612,7 @@ export class CloudAgentSession extends DurableObject { kilocodeToken: metadata.kilocodeToken ?? '', kilocodeModel: model, githubToken, - gitToken: request.tokenOverrides?.gitToken, + gitToken, }; const plan = this.buildExecutionPlan({ diff --git a/services/cloud-agent-next/src/persistence/async-preparation.ts b/services/cloud-agent-next/src/persistence/async-preparation.ts index d141d81ecd..aec2b17b2f 100644 --- a/services/cloud-agent-next/src/persistence/async-preparation.ts +++ b/services/cloud-agent-next/src/persistence/async-preparation.ts @@ -2,8 +2,10 @@ import { dirname } from 'node:path'; import { logger } from '../logger.js'; import { SANDBOX_SLEEP_AFTER_SECONDS } from '../core/lease.js'; import { generateSandboxId, getSandboxNamespace } from '../sandbox-id.js'; -import { GitHubTokenService } from '../services/github-token-service.js'; -import { InstallationLookupService } from '../services/installation-lookup-service.js'; +import { + resolveGitHubTokenForRepo, + resolveManagedGitLabToken, +} from '../services/git-token-service-client.js'; import { getSandbox } from '@cloudflare/sandbox'; import { checkDiskAndCleanBeforeSetup, @@ -34,6 +36,8 @@ export type PreparationStepsResult = { kiloSessionId: string; resolvedInstallationId: string | undefined; resolvedGithubAppType: 'standard' | 'lite' | undefined; + resolvedGitToken: string | undefined; + gitlabTokenManaged: boolean; }; /** @@ -56,32 +60,45 @@ export async function executePreparationSteps( let resolvedGithubAppType: 'standard' | 'lite' | undefined; if (input.githubRepo && !input.githubToken) { - const lookupService = new InstallationLookupService(env); - if (lookupService.isConfigured()) { - const result = await lookupService.findInstallationId({ - githubRepo: input.githubRepo, - userId: input.userId, - orgId: input.orgId, - }); - if (result) { - resolvedInstallationId = result.installationId; - resolvedGithubAppType = result.githubAppType; - const tokenService = new GitHubTokenService(env); - resolvedGithubToken = await tokenService.getToken( - resolvedInstallationId, - resolvedGithubAppType ?? 'standard' - ); - } - } - if (!resolvedGithubToken) { + const result = await resolveGitHubTokenForRepo(env, { + githubRepo: input.githubRepo, + userId: input.userId, + orgId: input.orgId, + }); + if (result.success) { + resolvedGithubToken = result.value.token; + resolvedInstallationId = result.value.installationId; + resolvedGithubAppType = result.value.appType; + } else { emitProgress( 'failed', - 'GitHub token or active app installation required for this repository' + `GitHub token or active app installation required for this repository (${result.error.reason})` ); return undefined; } } + // Resolve managed GitLab token when no client token provided + let resolvedGitToken = input.gitToken; + let gitlabTokenManaged = false; + if (input.gitUrl && !input.gitToken && input.platform === 'gitlab') { + const result = await resolveManagedGitLabToken(env, { + userId: input.userId, + orgId: input.orgId, + }); + if (result.success) { + resolvedGitToken = result.token; + gitlabTokenManaged = true; + } + } + if (input.gitUrl && input.platform === 'gitlab' && !resolvedGitToken) { + emitProgress( + 'failed', + 'No GitLab integration found. Please connect your GitLab account first.' + ); + return undefined; + } + // 2. Disk check emitProgress('disk_check', 'Checking disk space…'); const sandboxId = await generateSandboxId( @@ -119,7 +136,7 @@ export async function executePreparationSteps( githubRepo: input.githubRepo, githubToken: resolvedGithubToken, gitUrl: input.gitUrl, - gitToken: input.gitToken, + gitToken: resolvedGitToken, platform: input.platform, upstreamBranch: input.upstreamBranch, botId: input.botId, @@ -144,7 +161,7 @@ export async function executePreparationSteps( session, workspacePath, input.gitUrl, - input.gitToken, + resolvedGitToken, undefined, cloneOptions ); @@ -228,5 +245,7 @@ export async function executePreparationSteps( kiloSessionId: input.kiloSessionId ?? wrapperSessionId, resolvedInstallationId, resolvedGithubAppType, + resolvedGitToken, + gitlabTokenManaged, }; } diff --git a/services/cloud-agent-next/src/persistence/schemas.ts b/services/cloud-agent-next/src/persistence/schemas.ts index ecc9be9360..e8501c3513 100644 --- a/services/cloud-agent-next/src/persistence/schemas.ts +++ b/services/cloud-agent-next/src/persistence/schemas.ts @@ -137,6 +137,7 @@ export const MetadataSchema = z.object({ gitUrl: z.string().optional(), gitToken: z.string().optional(), platform: z.enum(['github', 'gitlab']).optional(), + gitlabTokenManaged: z.boolean().optional(), envVars: z .record(z.string().max(256), z.string().max(256)) .refine(obj => Object.keys(obj).length <= 50, { diff --git a/services/cloud-agent-next/src/persistence/types.ts b/services/cloud-agent-next/src/persistence/types.ts index 093bc7d43b..550c867148 100644 --- a/services/cloud-agent-next/src/persistence/types.ts +++ b/services/cloud-agent-next/src/persistence/types.ts @@ -72,6 +72,8 @@ export type CloudAgentSessionState = { gitToken?: string; /** Git platform type for correct token/env var handling */ platform?: 'github' | 'gitlab'; + /** Whether the GitLab token was auto-looked up via git-token-service (enables refresh on resume) */ + gitlabTokenManaged?: boolean; /** Environment variables to inject into sandbox execution sessions (plaintext) */ envVars?: Record; /** diff --git a/services/cloud-agent-next/src/router.test.ts b/services/cloud-agent-next/src/router.test.ts index 2a6301968b..1f61519712 100644 --- a/services/cloud-agent-next/src/router.test.ts +++ b/services/cloud-agent-next/src/router.test.ts @@ -51,7 +51,7 @@ import { getSandbox } from '@cloudflare/sandbox'; import { generateSessionId, fetchSessionMetadata } from './session-service.js'; import { sessionIdSchema, envVarsSchema } from './types.js'; import { appRouter } from './router.js'; -import type { TRPCContext, SessionId } from './types.js'; +import type { Env, TRPCContext, SessionId } from './types.js'; import type { CloudAgentSessionState } from './persistence/types.js'; type MockSessionStub = { @@ -306,6 +306,7 @@ describe('router sessionId validation', () => { fetch: vi.fn(), } as unknown as TRPCContext['env']['SESSION_INGEST'], R2_BUCKET: {} as TRPCContext['env']['R2_BUCKET'], + GIT_TOKEN_SERVICE: {} as Env['GIT_TOKEN_SERVICE'], NEXTAUTH_SECRET: 'test-secret', INTERNAL_API_SECRET_PROD: { get: vi.fn().mockResolvedValue('test-secret'), @@ -676,6 +677,7 @@ describe('router sessionId validation', () => { fetch: vi.fn(), } as unknown as TRPCContext['env']['SESSION_INGEST'], R2_BUCKET: {} as TRPCContext['env']['R2_BUCKET'], + GIT_TOKEN_SERVICE: {} as Env['GIT_TOKEN_SERVICE'], NEXTAUTH_SECRET: 'test-secret', INTERNAL_API_SECRET_PROD: { get: vi.fn().mockResolvedValue('test-secret'), @@ -929,6 +931,7 @@ describe('router sessionId validation', () => { fetch: vi.fn(), } as unknown as TRPCContext['env']['SESSION_INGEST'], R2_BUCKET: {} as TRPCContext['env']['R2_BUCKET'], + GIT_TOKEN_SERVICE: {} as Env['GIT_TOKEN_SERVICE'], NEXTAUTH_SECRET: 'test-secret', INTERNAL_API_SECRET_PROD: { get: vi.fn().mockResolvedValue('test-secret'), diff --git a/services/cloud-agent-next/src/router/handlers/session-prepare.ts b/services/cloud-agent-next/src/router/handlers/session-prepare.ts index 962b32e256..6054774718 100644 --- a/services/cloud-agent-next/src/router/handlers/session-prepare.ts +++ b/services/cloud-agent-next/src/router/handlers/session-prepare.ts @@ -8,8 +8,7 @@ import { runSetupCommands, writeAuthFile, } from '../../session-service.js'; -import { InstallationLookupService } from '../../services/installation-lookup-service.js'; -import { GitHubTokenService } from '../../services/github-token-service.js'; + import { internalApiProtectedProcedure } from '../auth.js'; import { PrepareSessionInput, @@ -29,6 +28,10 @@ import { WrapperClient } from '../../kilo/wrapper-client.js'; import { withDORetry } from '../../utils/do-retry.js'; import { generateKiloSessionId } from '../../utils/kilo-session-id.js'; import { SANDBOX_SLEEP_AFTER_SECONDS } from '../../core/lease.js'; +import { + resolveGitHubTokenForRepo, + resolveManagedGitLabToken, +} from '../../services/git-token-service-client.js'; type SessionPrepareHandlers = { prepareSession: typeof prepareSessionHandler; @@ -116,89 +119,53 @@ const prepareSessionHandler = internalApiProtectedProcedure }); logger.info('Preparing new session with workspace setup'); - // 2. Lookup GitHub installation ID from database when using a GitHub repo without a token + // 2. Lookup GitHub installation + generate token via git-token-service RPC + let resolvedGithubToken = input.githubToken; let resolvedInstallationId: string | undefined; let resolvedGithubAppType: 'standard' | 'lite' | undefined; if (input.githubRepo && !input.githubToken) { - const lookupService = new InstallationLookupService(ctx.env); - logger - .withFields({ hyperdriveConfigured: lookupService.isConfigured() }) - .info('Checking for GitHub installation ID lookup'); - if (lookupService.isConfigured()) { - try { - const result = await lookupService.findInstallationId({ - githubRepo: input.githubRepo, - userId: ctx.userId, - orgId: input.kilocodeOrganizationId, - }); - logger - .withFields({ - found: !!result, - githubRepo: input.githubRepo, - userId: ctx.userId, - orgId: input.kilocodeOrganizationId, - }) - .info('Installation lookup result'); - if (result) { - resolvedInstallationId = result.installationId; - resolvedGithubAppType = result.githubAppType; - logger - .withFields({ - installationId: result.installationId, - accountLogin: result.accountLogin, - githubAppType: result.githubAppType, - }) - .info('Resolved GitHub installation ID from database'); - } - } catch (lookupError) { - logger - .withFields({ - error: lookupError instanceof Error ? lookupError.message : String(lookupError), - }) - .error('Failed to lookup GitHub installation ID'); - // Don't throw - fall through to the validation error - } + const result = await resolveGitHubTokenForRepo(ctx.env, { + githubRepo: input.githubRepo, + userId: ctx.userId, + orgId: input.kilocodeOrganizationId, + }); + if (result.success) { + resolvedGithubToken = result.value.token; + resolvedInstallationId = result.value.installationId; + resolvedGithubAppType = result.value.appType; } } // Validate that we have auth for GitHub repo - if (input.githubRepo && !input.githubToken && !resolvedInstallationId) { + if (input.githubRepo && !resolvedGithubToken) { throw new TRPCError({ code: 'BAD_REQUEST', message: 'GitHub token or active app installation required for this repository', }); } - // Generate token from installation ID if using GitHub App auth - let resolvedGithubToken = input.githubToken; - if (input.githubRepo && !input.githubToken && resolvedInstallationId) { - const tokenService = new GitHubTokenService(ctx.env); - if (!tokenService.isConfigured(resolvedGithubAppType ?? 'standard')) { - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: 'GitHub App credentials not configured', - }); - } - try { - resolvedGithubToken = await tokenService.getToken( - resolvedInstallationId, - resolvedGithubAppType ?? 'standard' - ); - logger.info('Generated GitHub token from installation'); - } catch (tokenError) { - logger - .withFields({ - error: tokenError instanceof Error ? tokenError.message : String(tokenError), - installationId: resolvedInstallationId, - }) - .error('Failed to generate GitHub token from installation'); - throw new TRPCError({ - code: 'BAD_GATEWAY', - message: `Failed to generate GitHub token: ${tokenError instanceof Error ? tokenError.message : String(tokenError)}`, - }); + // 2b. Lookup GitLab token via git-token-service RPC when no client token provided + let resolvedGitToken = input.gitToken; + let gitlabTokenManaged = false; + if (input.gitUrl && !input.gitToken && input.platform === 'gitlab') { + const result = await resolveManagedGitLabToken(ctx.env, { + userId: ctx.userId, + orgId: input.kilocodeOrganizationId, + }); + if (result.success) { + resolvedGitToken = result.token; + gitlabTokenManaged = true; } } + // Validate that we have auth for GitLab repo + if (input.gitUrl && input.platform === 'gitlab' && !resolvedGitToken) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'No GitLab integration found. Please connect your GitLab account first.', + }); + } + // --- Fast path: autoInitiate returns immediately, runs preparation asynchronously --- if (input.autoInitiate) { logger.info('autoInitiate=true: fast-path return, async preparation'); @@ -344,7 +311,7 @@ const prepareSessionHandler = internalApiProtectedProcedure githubRepo: input.githubRepo, githubToken: resolvedGithubToken, // Use resolved token (from input or generated from installation) gitUrl: input.gitUrl, - gitToken: input.gitToken, + gitToken: resolvedGitToken, platform: input.platform, upstreamBranch: input.upstreamBranch, botId: ctx.botId, @@ -372,7 +339,7 @@ const prepareSessionHandler = internalApiProtectedProcedure session, workspacePath, input.gitUrl, - input.gitToken, + resolvedGitToken, undefined, cloneOptions ); @@ -492,8 +459,9 @@ const prepareSessionHandler = internalApiProtectedProcedure githubInstallationId: resolvedInstallationId, githubAppType: resolvedGithubAppType, gitUrl: input.gitUrl, - gitToken: input.gitToken, + gitToken: resolvedGitToken, platform: input.platform, + gitlabTokenManaged, envVars: input.envVars, encryptedSecrets: input.encryptedSecrets, setupCommands: input.setupCommands, diff --git a/services/cloud-agent-next/src/services/git-token-service-client.ts b/services/cloud-agent-next/src/services/git-token-service-client.ts new file mode 100644 index 0000000000..2183af27e3 --- /dev/null +++ b/services/cloud-agent-next/src/services/git-token-service-client.ts @@ -0,0 +1,85 @@ +import { logger } from '../logger.js'; +import type { Env as WorkerEnv } from '../types.js'; + +export type ResolvedGitHubToken = { + token: string; + installationId: string; + appType: 'standard' | 'lite'; + accountLogin: string; +}; + +export type ResolveGitHubTokenError = { + reason: string; + message: string; +}; + +export type ResolveGitHubTokenResult = + | { success: true; value: ResolvedGitHubToken } + | { success: false; error: ResolveGitHubTokenError }; + +export async function resolveGitHubTokenForRepo( + env: WorkerEnv, + params: { githubRepo: string; userId: string; orgId?: string } +): Promise { + try { + const result = await env.GIT_TOKEN_SERVICE.getTokenForRepo(params); + if (result.success) { + logger + .withFields({ + installationId: result.installationId, + accountLogin: result.accountLogin, + githubAppType: result.appType, + }) + .info('Resolved GitHub token via git-token-service'); + return { + success: true, + value: { + token: result.token, + installationId: result.installationId, + appType: result.appType, + accountLogin: result.accountLogin, + }, + }; + } + logger + .withFields({ reason: result.reason, githubRepo: params.githubRepo }) + .info('GitHub token lookup failed'); + return { + success: false, + error: { + reason: result.reason, + message: `GitHub token lookup failed (${result.reason})`, + }, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + logger.withFields({ error: message }).error('Failed to call git-token-service getTokenForRepo'); + return { + success: false, + error: { reason: 'rpc_error', message: `git-token-service RPC failed: ${message}` }, + }; + } +} + +export type ResolveManagedGitLabTokenResult = + | { success: true; token: string } + | { success: false; reason: string }; + +export async function resolveManagedGitLabToken( + env: WorkerEnv, + params: { userId: string; orgId?: string } +): Promise { + try { + const result = await env.GIT_TOKEN_SERVICE.getGitLabToken(params); + if (result.success) { + logger.info('Resolved GitLab token via git-token-service'); + return { success: true, token: result.token }; + } + logger.withFields({ reason: result.reason }).info('GitLab token lookup failed'); + return { success: false, reason: result.reason }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + logger.withFields({ error: message }).error('Failed to call git-token-service getGitLabToken'); + return { success: false, reason: 'rpc_error' }; + } +} diff --git a/services/cloud-agent-next/src/services/github-token-service.ts b/services/cloud-agent-next/src/services/github-token-service.ts deleted file mode 100644 index f313f55a9d..0000000000 --- a/services/cloud-agent-next/src/services/github-token-service.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { createAppAuth } from '@octokit/auth-app'; -import { TRPCError } from '@trpc/server'; -import * as z from 'zod'; - -type TokenCacheEntry = { - token: string; - expiresAt: number; -}; - -type GitHubAppCredentials = { - appId: string; - privateKey: string; -}; - -type GeneratedToken = { - token: string; - expiresAt: number; -}; - -/** - * Type of GitHub App to use - * - 'standard': Full-featured KiloConnect app with read/write permissions - * - 'lite': Read-only KiloConnect-Lite app - */ -export type GitHubAppType = 'standard' | 'lite'; - -type GitHubTokenServiceEnv = { - GITHUB_TOKEN_CACHE?: KVNamespace; - // Standard app credentials - GITHUB_APP_ID?: string; - GITHUB_APP_PRIVATE_KEY?: string; - // Lite app credentials - GITHUB_LITE_APP_ID?: string; - GITHUB_LITE_APP_PRIVATE_KEY?: string; -}; - -const TokenCacheEntrySchema = z.object({ - token: z.string(), - expiresAt: z.number(), -}); - -const CACHE_TTL_MS = 30 * 60 * 1000; -const CACHE_KEY_PREFIX = 'gh-token:'; -const MIN_TTL_SECONDS = 60; -const EXPIRY_BUFFER_MS = 5 * 60 * 1000; - -export class GitHubTokenService { - constructor(private env: GitHubTokenServiceEnv) {} - - isConfigured(appType: GitHubAppType = 'standard'): boolean { - if (appType === 'lite') { - return Boolean(this.env.GITHUB_LITE_APP_ID && this.env.GITHUB_LITE_APP_PRIVATE_KEY); - } - return Boolean(this.env.GITHUB_APP_ID && this.env.GITHUB_APP_PRIVATE_KEY); - } - - async getToken(installationId: string, appType: GitHubAppType = 'standard'): Promise { - const numericId = this.validateInstallationId(installationId); - - // Include app type in cache key to prevent mixing tokens from different apps - const cacheKey = `${installationId}:${appType}`; - const cached = await this.getCachedToken(cacheKey); - if (cached) { - return cached; - } - - const credentials = this.getCredentials(appType); - const { token, expiresAt } = await this.generateToken(numericId, credentials); - await this.cacheToken(cacheKey, token, expiresAt); - - return token; - } - - private getCredentials(appType: GitHubAppType): GitHubAppCredentials { - if (appType === 'lite') { - const appId = this.env.GITHUB_LITE_APP_ID; - const privateKeyRaw = this.env.GITHUB_LITE_APP_PRIVATE_KEY; - if (!appId || !privateKeyRaw) { - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: 'GitHub Lite App credentials not configured', - }); - } - return { - appId, - privateKey: privateKeyRaw.replace(/\\n/g, '\n'), - }; - } - - const appId = this.env.GITHUB_APP_ID; - const privateKeyRaw = this.env.GITHUB_APP_PRIVATE_KEY; - if (!appId || !privateKeyRaw) { - throw new TRPCError({ - code: 'INTERNAL_SERVER_ERROR', - message: 'GitHub App credentials not configured', - }); - } - - return { - appId, - privateKey: privateKeyRaw.replace(/\\n/g, '\n'), - }; - } - - private validateInstallationId(installationId: string): number { - const numericId = Number(installationId); - const isValid = Number.isInteger(numericId) && numericId > 0; - if (!isValid) { - throw new TRPCError({ - code: 'BAD_REQUEST', - message: `Invalid GitHub installation ID: ${installationId}`, - }); - } - return numericId; - } - - private async getCachedToken(cacheKey: string): Promise { - if (!this.env.GITHUB_TOKEN_CACHE) { - return null; - } - - const key = `${CACHE_KEY_PREFIX}${cacheKey}`; - const cached = await this.env.GITHUB_TOKEN_CACHE.get(key, 'json'); - const parsed = TokenCacheEntrySchema.safeParse(cached); - if (!parsed.success) { - return null; - } - - const entry = parsed.data; - if (entry.expiresAt - Date.now() < EXPIRY_BUFFER_MS) { - return null; - } - - return entry.token; - } - - private async generateToken( - installationId: number, - credentials: GitHubAppCredentials - ): Promise { - try { - const auth = createAppAuth({ - appId: credentials.appId, - privateKey: credentials.privateKey, - installationId, - }); - - const result = await auth({ type: 'installation' }); - return { - token: result.token, - expiresAt: new Date(result.expiresAt).getTime(), - }; - } catch (error) { - console.error('Failed to generate GitHub token:', error); - const message = error instanceof Error ? error.message : 'Unknown error'; - throw new TRPCError({ - code: 'BAD_GATEWAY', - message: `Failed to generate GitHub installation token: ${message}`, - cause: error, - }); - } - } - - private async cacheToken(cacheKey: string, token: string, expiresAt: number): Promise { - if (!this.env.GITHUB_TOKEN_CACHE) { - return; - } - - const remainingSeconds = Math.floor((expiresAt - Date.now()) / 1000); - if (remainingSeconds < MIN_TTL_SECONDS) { - return; - } - - const entry = { token, expiresAt } satisfies TokenCacheEntry; - const maxTtlSeconds = Math.floor(CACHE_TTL_MS / 1000); - const ttlSeconds = Math.min(maxTtlSeconds, remainingSeconds); - const key = `${CACHE_KEY_PREFIX}${cacheKey}`; - - await this.env.GITHUB_TOKEN_CACHE.put(key, JSON.stringify(entry), { - expirationTtl: ttlSeconds, - }); - } -} diff --git a/services/cloud-agent-next/src/services/installation-lookup-service.ts b/services/cloud-agent-next/src/services/installation-lookup-service.ts deleted file mode 100644 index b9ba6a7d2c..0000000000 --- a/services/cloud-agent-next/src/services/installation-lookup-service.ts +++ /dev/null @@ -1,119 +0,0 @@ -import type { WorkerDb } from '@kilocode/db/client'; -import { platform_integrations, organization_memberships } from '@kilocode/db/schema'; -import { eq, and, isNotNull, or, sql } from 'drizzle-orm'; - -type InstallationLookupEnv = { - HYPERDRIVE?: { connectionString: string }; -}; - -type LookupParams = { - githubRepo: string; - userId: string; - orgId?: string; -}; - -type LookupResult = { - installationId: string; - accountLogin: string; - githubAppType: 'standard' | 'lite'; -} | null; - -export class InstallationLookupService { - private db: WorkerDb | null = null; - - constructor(private env: InstallationLookupEnv) {} - - isConfigured(): boolean { - return Boolean(this.env.HYPERDRIVE); - } - - private async getDb(): Promise { - if (!this.db) { - if (!this.env.HYPERDRIVE) { - throw new Error('Hyperdrive not configured'); - } - const { getWorkerDb } = await import('@kilocode/db/client'); - this.db = getWorkerDb(this.env.HYPERDRIVE.connectionString, { statement_timeout: 10_000 }); - } - return this.db; - } - - /** - * Find a GitHub App installation ID for a given repo owner and user/org context. - * - * SECURITY: When looking up org installations, we JOIN with organization_memberships - * to verify the user is actually a member of the organization. This prevents users - * from accessing installations for orgs they don't belong to. - * - * Prioritizes org installations over user installations. - */ - async findInstallationId(params: LookupParams): Promise { - if (!this.isConfigured()) { - return null; - } - - const [repoOwner] = params.githubRepo.split('/'); - - const db = await this.getDb(); - - const rows = await db - .select({ - platform_installation_id: platform_integrations.platform_installation_id, - platform_account_login: platform_integrations.platform_account_login, - github_app_type: platform_integrations.github_app_type, - }) - .from(platform_integrations) - // For org installations, verify user is a member of the org - .leftJoin( - organization_memberships, - and( - eq( - platform_integrations.owned_by_organization_id, - organization_memberships.organization_id - ), - eq(organization_memberships.kilo_user_id, params.userId) - ) - ) - .where( - and( - eq(platform_integrations.platform, 'github'), - eq(platform_integrations.integration_type, 'app'), - eq(platform_integrations.integration_status, 'active'), - eq(platform_integrations.platform_account_login, repoOwner), - isNotNull(platform_integrations.platform_installation_id), - isNotNull(platform_integrations.platform_account_login), - or( - // Org installation: must match org ID AND user must be a member - and( - isNotNull(platform_integrations.owned_by_organization_id), - eq( - platform_integrations.owned_by_organization_id, - sql`${params.orgId ?? null}::uuid` - ), - isNotNull(organization_memberships.id) - ), - // User installation: must match user ID directly - and( - isNotNull(platform_integrations.owned_by_user_id), - eq(platform_integrations.owned_by_user_id, params.userId) - ) - ) - ) - ) - .orderBy( - sql`CASE WHEN ${platform_integrations.owned_by_organization_id} IS NOT NULL THEN 0 ELSE 1 END` - ) - .limit(1); - - if (rows.length === 0) { - return null; - } - - const row = rows[0]; - return { - installationId: row.platform_installation_id ?? '', - accountLogin: row.platform_account_login ?? '', - githubAppType: row.github_app_type ?? 'standard', - }; - } -} diff --git a/services/cloud-agent-next/src/types.ts b/services/cloud-agent-next/src/types.ts index cb48f985ee..a17fa35b5d 100644 --- a/services/cloud-agent-next/src/types.ts +++ b/services/cloud-agent-next/src/types.ts @@ -84,6 +84,46 @@ export type InterruptResult = { processesFound: boolean; }; +type GetTokenForRepoResult = + | { + success: true; + token: string; + installationId: string; + accountLogin: string; + appType: 'standard' | 'lite'; + } + | { + success: false; + reason: + | 'database_not_configured' + | 'invalid_repo_format' + | 'no_installation_found' + | 'invalid_org_id'; + }; + +type GetGitLabTokenResult = + | { success: true; token: string; instanceUrl: string } + | { + success: false; + reason: + | 'database_not_configured' + | 'no_integration_found' + | 'invalid_org_id' + | 'no_token' + | 'token_refresh_failed' + | 'token_expired_no_refresh'; + }; + +export type GitTokenService = { + getTokenForRepo(params: { + githubRepo: string; + userId: string; + orgId?: string; + }): Promise; + getToken(installationId: string, appType?: 'standard' | 'lite'): Promise; + getGitLabToken(params: { userId: string; orgId?: string }): Promise; +}; + export type Env = { Sandbox: DurableObjectNamespace; /** Durable Object namespace for per-session sandbox containers (standard-2, experimental) */ @@ -98,16 +138,8 @@ export type Env = { R2_BUCKET: R2Bucket; /** Queue for callback messages (optional - supports incremental rollout) */ CALLBACK_QUEUE?: Queue; - /** KV namespace for caching GitHub installation tokens */ - GITHUB_TOKEN_CACHE?: KVNamespace; - /** GitHub App ID for token generation */ - GITHUB_APP_ID?: string; - /** GitHub App private key (PEM format) for token generation */ - GITHUB_APP_PRIVATE_KEY?: string; - /** GitHub Lite App ID for read-only token generation */ - GITHUB_LITE_APP_ID?: string; - /** GitHub Lite App private key (PEM format) for read-only token generation */ - GITHUB_LITE_APP_PRIVATE_KEY?: string; + /** Service binding for centralized git token generation */ + GIT_TOKEN_SERVICE: GitTokenService; /** GitHub Lite App slug for git commit attribution (e.g., 'kiloconnect-lite') */ GITHUB_LITE_APP_SLUG?: string; /** GitHub Lite App bot user ID for git commit email */ @@ -139,11 +171,6 @@ export type Env = { * Required when using encryptedSecrets feature. PEM format (base64-encoded). */ AGENT_ENV_VARS_PRIVATE_KEY?: string; - /** - * Hyperdrive binding for PostgreSQL connection pooling. - * Used for looking up GitHub installation IDs from the database. - */ - HYPERDRIVE?: { connectionString: string }; /** GitHub App slug for git commit attribution (e.g., 'kiloconnect') */ GITHUB_APP_SLUG?: string; /** GitHub App bot user ID for git commit email (e.g., '240665456') */ diff --git a/services/cloud-agent-next/worker-configuration.d.ts b/services/cloud-agent-next/worker-configuration.d.ts index 5b900cd6f5..9cceb19433 100644 --- a/services/cloud-agent-next/worker-configuration.d.ts +++ b/services/cloud-agent-next/worker-configuration.d.ts @@ -1,26 +1,18 @@ /* eslint-disable */ -// Generated by Wrangler by running `wrangler types` (hash: 3e3644facfe9d272ca2d013adc085d3d) -// Runtime types generated with workerd@1.20260305.0 2025-09-15 nodejs_compat +// Generated by Wrangler by running `wrangler types` (hash: edb7ebd5d1ef6ff409168c7d77a89113) +// Runtime types generated with workerd@1.20260312.1 2025-09-15 nodejs_compat declare namespace Cloudflare { interface GlobalProps { mainModule: typeof import("./src/index"); durableNamespaces: "Sandbox" | "CloudAgentSession" | "SandboxSmall"; } interface DevEnv { - GITHUB_TOKEN_CACHE: KVNamespace; R2_BUCKET: R2Bucket; HYPERDRIVE: Hyperdrive; CALLBACK_QUEUE: Queue; INTERNAL_API_SECRET_PROD: SecretsStoreSecret; - GITHUB_APP_SLUG: "kiloconnect-development"; - GITHUB_APP_BOT_USER_ID: "242397087"; - GITHUB_LITE_APP_ID: ""; GITHUB_LITE_APP_SLUG: ""; GITHUB_LITE_APP_BOT_USER_ID: ""; - CLI_TIMEOUT_SECONDS: "900"; - REAPER_INTERVAL_MS: "300000"; - STALE_THRESHOLD_MS: "600000"; - PENDING_START_TIMEOUT_MS: "300000"; R2_ATTACHMENTS_BUCKET: "cloud-agent-attachments-dev"; PER_SESSION_SANDBOX_ORG_IDS: "*"; NEXTAUTH_SECRET: string; @@ -34,14 +26,27 @@ declare namespace Cloudflare { R2_ATTACHMENTS_READONLY_ACCESS_KEY_ID: string; R2_ATTACHMENTS_READONLY_SECRET_ACCESS_KEY: string; AGENT_ENV_VARS_PRIVATE_KEY: string; - GITHUB_APP_ID: string; - GITHUB_APP_PRIVATE_KEY: string; + CLI_TIMEOUT_SECONDS: string; + REAPER_INTERVAL_MS: string; + STALE_THRESHOLD_MS: string; + PENDING_START_TIMEOUT_MS: string; + GITHUB_APP_SLUG: string; + GITHUB_APP_BOT_USER_ID: string; Sandbox: DurableObjectNamespace; SandboxSmall: DurableObjectNamespace; CLOUD_AGENT_SESSION: DurableObjectNamespace; SESSION_INGEST: Service /* entrypoint SessionIngestRPC from session-ingest */; + GIT_TOKEN_SERVICE: Service /* entrypoint GitTokenRPCEntrypoint from git-token-service-dev */; } interface Env { + R2_BUCKET: R2Bucket; + HYPERDRIVE: Hyperdrive; + CALLBACK_QUEUE: Queue; + INTERNAL_API_SECRET_PROD: SecretsStoreSecret; + GITHUB_LITE_APP_SLUG: "" | "kiloconnect-lite"; + GITHUB_LITE_APP_BOT_USER_ID: "" | "257753004"; + R2_ATTACHMENTS_BUCKET: "cloud-agent-attachments-dev" | "cloud-agent-attachments"; + PER_SESSION_SANDBOX_ORG_IDS?: "*"; NEXTAUTH_SECRET: string; KILO_SESSION_INGEST_URL: string; WORKER_URL: string; @@ -53,28 +58,17 @@ declare namespace Cloudflare { R2_ATTACHMENTS_READONLY_ACCESS_KEY_ID: string; R2_ATTACHMENTS_READONLY_SECRET_ACCESS_KEY: string; AGENT_ENV_VARS_PRIVATE_KEY: string; - GITHUB_APP_ID: string; - GITHUB_APP_PRIVATE_KEY: string; - GITHUB_TOKEN_CACHE: KVNamespace; - R2_BUCKET: R2Bucket; - HYPERDRIVE: Hyperdrive; - CALLBACK_QUEUE: Queue; - INTERNAL_API_SECRET_PROD: SecretsStoreSecret; - GITHUB_APP_SLUG: "kiloconnect-development" | "kiloconnect"; - GITHUB_APP_BOT_USER_ID: "242397087" | "240665456"; - GITHUB_LITE_APP_ID: "" | "2745442"; - GITHUB_LITE_APP_SLUG: "" | "kiloconnect-lite"; - GITHUB_LITE_APP_BOT_USER_ID: "" | "257753004"; - CLI_TIMEOUT_SECONDS: "900"; - REAPER_INTERVAL_MS: "300000"; - STALE_THRESHOLD_MS: "600000"; - PENDING_START_TIMEOUT_MS: "300000"; - R2_ATTACHMENTS_BUCKET: "cloud-agent-attachments-dev" | "cloud-agent-attachments"; - PER_SESSION_SANDBOX_ORG_IDS: "*" | ""; + CLI_TIMEOUT_SECONDS: string; + REAPER_INTERVAL_MS: string; + STALE_THRESHOLD_MS: string; + PENDING_START_TIMEOUT_MS: string; + GITHUB_APP_SLUG: string; + GITHUB_APP_BOT_USER_ID: string; Sandbox: DurableObjectNamespace; SandboxSmall: DurableObjectNamespace; CLOUD_AGENT_SESSION: DurableObjectNamespace; SESSION_INGEST: Service /* entrypoint SessionIngestRPC from session-ingest */; + GIT_TOKEN_SERVICE: Service /* entrypoint GitTokenRPCEntrypoint from git-token-service-dev */ | Service /* entrypoint GitTokenRPCEntrypoint from git-token-service */; } } interface Env extends Cloudflare.Env {} @@ -82,7 +76,7 @@ type StringifyValues> = { [Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string; }; declare namespace NodeJS { - interface ProcessEnv extends StringifyValues> {} + interface ProcessEnv extends StringifyValues> {} } declare module "*.sql" { const value: string; @@ -510,22 +504,22 @@ interface ExecutionContext { passThroughOnException(): void; readonly props: Props; } -type ExportedHandlerFetchHandler = (request: Request>, env: Env, ctx: ExecutionContext) => Response | Promise; -type ExportedHandlerTailHandler = (events: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise; -type ExportedHandlerTraceHandler = (traces: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise; -type ExportedHandlerTailStreamHandler = (event: TailStream.TailEvent, env: Env, ctx: ExecutionContext) => TailStream.TailEventHandlerType | Promise; -type ExportedHandlerScheduledHandler = (controller: ScheduledController, env: Env, ctx: ExecutionContext) => void | Promise; -type ExportedHandlerQueueHandler = (batch: MessageBatch, env: Env, ctx: ExecutionContext) => void | Promise; -type ExportedHandlerTestHandler = (controller: TestController, env: Env, ctx: ExecutionContext) => void | Promise; -interface ExportedHandler { - fetch?: ExportedHandlerFetchHandler; - tail?: ExportedHandlerTailHandler; - trace?: ExportedHandlerTraceHandler; - tailStream?: ExportedHandlerTailStreamHandler; - scheduled?: ExportedHandlerScheduledHandler; - test?: ExportedHandlerTestHandler; - email?: EmailExportedHandler; - queue?: ExportedHandlerQueueHandler; +type ExportedHandlerFetchHandler = (request: Request>, env: Env, ctx: ExecutionContext) => Response | Promise; +type ExportedHandlerTailHandler = (events: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerTraceHandler = (traces: TraceItem[], env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerTailStreamHandler = (event: TailStream.TailEvent, env: Env, ctx: ExecutionContext) => TailStream.TailEventHandlerType | Promise; +type ExportedHandlerScheduledHandler = (controller: ScheduledController, env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerQueueHandler = (batch: MessageBatch, env: Env, ctx: ExecutionContext) => void | Promise; +type ExportedHandlerTestHandler = (controller: TestController, env: Env, ctx: ExecutionContext) => void | Promise; +interface ExportedHandler { + fetch?: ExportedHandlerFetchHandler; + tail?: ExportedHandlerTailHandler; + trace?: ExportedHandlerTraceHandler; + tailStream?: ExportedHandlerTailStreamHandler; + scheduled?: ExportedHandlerScheduledHandler; + test?: ExportedHandlerTestHandler; + email?: EmailExportedHandler; + queue?: ExportedHandlerQueueHandler; } interface StructuredSerializeOptions { transfer?: any[]; @@ -3120,7 +3114,7 @@ declare var WebSocket: { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket) */ interface WebSocket extends EventTarget { - accept(): void; + accept(options?: WebSocketAcceptOptions): void; /** * The **`WebSocket.send()`** method enqueues the specified data to be transmitted to the server over the WebSocket connection, increasing the value of `bufferedAmount` by the number of bytes needed to contain the data. * @@ -3159,6 +3153,22 @@ interface WebSocket extends EventTarget { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/extensions) */ extensions: string | null; + /** + * The **`WebSocket.binaryType`** property controls the type of binary data being received over the WebSocket connection. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket/binaryType) + */ + binaryType: "blob" | "arraybuffer"; +} +interface WebSocketAcceptOptions { + /** + * When set to `true`, receiving a server-initiated WebSocket Close frame will not + * automatically send a reciprocal Close frame, leaving the connection in a half-open + * state. This is useful for proxying scenarios where you need to coordinate closing + * both sides independently. Defaults to `false` when the + * `no_web_socket_half_open_by_default` compatibility flag is enabled. + */ + allowHalfOpen?: boolean; } declare const WebSocketPair: { new (): { @@ -3277,6 +3287,8 @@ interface Container { signal(signo: number): void; getTcpPort(port: number): Fetcher; setInactivityTimeout(durationMs: number | bigint): Promise; + interceptOutboundHttp(addr: string, binding: Fetcher): Promise; + interceptAllOutboundHttp(binding: Fetcher): Promise; } interface ContainerStartupOptions { entrypoint?: string[]; @@ -3402,6 +3414,12 @@ declare abstract class Performance { get timeOrigin(): number; /* [Cloudflare Docs Reference](https://developers.cloudflare.com/workers/runtime-apis/performance/#performancenow) */ now(): number; + /** + * The **`toJSON()`** method of the Performance interface is a Serialization; it returns a JSON representation of the Performance object. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Performance/toJSON) + */ + toJSON(): object; } // AI Search V2 API Error Interfaces interface AiSearchInternalError extends Error { @@ -3410,16 +3428,6 @@ interface AiSearchNotFoundError extends Error { } interface AiSearchNameNotSetError extends Error { } -// Filter types (shared with AutoRAG for compatibility) -type ComparisonFilter = { - key: string; - type: 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte'; - value: string | number | boolean; -}; -type CompoundFilter = { - type: 'and' | 'or'; - filters: ComparisonFilter[]; -}; // AI Search V2 Request Types type AiSearchSearchRequest = { messages: Array<{ @@ -3433,7 +3441,7 @@ type AiSearchSearchRequest = { match_threshold?: number; /** Maximum number of results (1-50, default 10) */ max_num_results?: number; - filters?: CompoundFilter | ComparisonFilter; + filters?: VectorizeVectorMetadataFilter; /** Context expansion (0-3, default 0) */ context_expansion?: number; [key: string]: unknown; @@ -3467,7 +3475,7 @@ type AiSearchChatCompletionsRequest = { retrieval_type?: 'vector' | 'keyword' | 'hybrid'; match_threshold?: number; max_num_results?: number; - filters?: CompoundFilter | ComparisonFilter; + filters?: VectorizeVectorMetadataFilter; context_expansion?: number; [key: string]: unknown; }; @@ -8942,6 +8950,15 @@ interface AutoRAGUnauthorizedError extends Error { */ interface AutoRAGNameNotSetError extends Error { } +type ComparisonFilter = { + key: string; + type: 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte'; + value: string | number | boolean; +}; +type CompoundFilter = { + type: 'and' | 'or'; + filters: ComparisonFilter[]; +}; /** * @deprecated AutoRAG has been replaced by AI Search. * Use AiSearchSearchRequest with the new API instead. @@ -9998,7 +10015,7 @@ interface SendEmail { declare abstract class EmailEvent extends ExtendableEvent { readonly message: ForwardableEmailMessage; } -declare type EmailExportedHandler = (message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext) => void | Promise; +declare type EmailExportedHandler = (message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext) => void | Promise; declare module "cloudflare:email" { let _EmailMessage: { prototype: EmailMessage; @@ -10717,9 +10734,12 @@ declare namespace CloudflareWorkersModule { timestamp: Date; type: string; }; + export type WorkflowStepContext = { + attempt: number; + }; export abstract class WorkflowStep { - do>(name: string, callback: () => Promise): Promise; - do>(name: string, config: WorkflowStepConfig, callback: () => Promise): Promise; + do>(name: string, callback: (ctx: WorkflowStepContext) => Promise): Promise; + do>(name: string, config: WorkflowStepConfig, callback: (ctx: WorkflowStepContext) => Promise): Promise; sleep: (name: string, duration: WorkflowSleepDuration) => Promise; sleepUntil: (name: string, timestamp: Date | number) => Promise; waitForEvent>(name: string, options: { @@ -10787,6 +10807,7 @@ type ConversionOptions = { convertOGImage?: boolean; }; hostname?: string; + cssSelector?: string; }; docx?: { images?: EmbeddedImageConversionOptions; diff --git a/services/cloud-agent-next/wrangler.jsonc b/services/cloud-agent-next/wrangler.jsonc index b3bcf63cb3..6ca6a91568 100644 --- a/services/cloud-agent-next/wrangler.jsonc +++ b/services/cloud-agent-next/wrangler.jsonc @@ -43,8 +43,6 @@ "KILOCODE_BACKEND_BASE_URL": "https://api.kilo.ai", "GITHUB_APP_SLUG": "kiloconnect", "GITHUB_APP_BOT_USER_ID": "240665456", - "GITHUB_APP_ID": "2193792", - "GITHUB_LITE_APP_ID": "2745442", "GITHUB_LITE_APP_SLUG": "kiloconnect-lite", "GITHUB_LITE_APP_BOT_USER_ID": "257753004", "WORKER_URL": "https://cloud-agent-next.kilosessions.ai", @@ -87,6 +85,11 @@ "service": "session-ingest", "entrypoint": "SessionIngestRPC", }, + { + "binding": "GIT_TOKEN_SERVICE", + "service": "git-token-service", + "entrypoint": "GitTokenRPCEntrypoint", + }, ], "secrets_store_secrets": [ { @@ -194,15 +197,7 @@ "localConnectionString": "postgres://postgres:postgres@localhost:5432/postgres", }, ], - /** - * KV Namespace Bindings - */ - "kv_namespaces": [ - { - "binding": "GITHUB_TOKEN_CACHE", - "id": "ab4d777d134a43248639044613ea29ef", - }, - ], + /** * Queue Bindings * https://developers.cloudflare.com/queues/configuration/configure-queues/ @@ -237,19 +232,11 @@ "localConnectionString": "postgres://postgres:postgres@localhost:5432/postgres", }, ], - "kv_namespaces": [ - { - "binding": "GITHUB_TOKEN_CACHE", - "id": "33b5f1f1be064e919934bee83df4067c", - }, - ], "vars": { "KILOCODE_BACKEND_BASE_URL": "http://localhost:3000", "KILO_OPENROUTER_BASE": "http://localhost:3000/api", "GITHUB_APP_SLUG": "kiloconnect-development", "GITHUB_APP_BOT_USER_ID": "242397087", - "GITHUB_APP_ID": "2245043", - "GITHUB_LITE_APP_ID": "", "GITHUB_LITE_APP_SLUG": "", "GITHUB_LITE_APP_BOT_USER_ID": "", "WORKER_URL": "http://localhost:8794", @@ -268,6 +255,11 @@ "service": "session-ingest", "entrypoint": "SessionIngestRPC", }, + { + "binding": "GIT_TOKEN_SERVICE", + "service": "git-token-service-dev", + "entrypoint": "GitTokenRPCEntrypoint", + }, ], "secrets_store_secrets": [ { diff --git a/services/git-token-service/package.json b/services/git-token-service/package.json index 11077096b6..adbb8b7927 100644 --- a/services/git-token-service/package.json +++ b/services/git-token-service/package.json @@ -3,7 +3,7 @@ "private": true, "type": "module", "scripts": { - "dev": "wrangler dev", + "dev": "wrangler dev --env dev", "dev:test": "wrangler dev --config wrangler.test.jsonc", "deploy": "wrangler deploy", "typecheck": "tsgo --noEmit",