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",