Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 7 additions & 39 deletions apps/web/src/routers/cloud-agent-next-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 };
}

Expand Down Expand Up @@ -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);
Comment thread
eshurakov marked this conversation as resolved.
} catch (error) {
rethrowAsPaymentRequired(error);
throw error;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 };
}

Expand Down Expand Up @@ -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);
Expand Down
30 changes: 21 additions & 9 deletions dev/local/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -59,7 +70,7 @@ const serviceMeta: Record<string, ServiceMeta> = {
// 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,
},
Expand All @@ -73,6 +84,12 @@ const serviceMeta: Record<string, ServiceMeta> = {
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': {
Expand All @@ -86,11 +103,6 @@ const serviceMeta: Record<string, ServiceMeta> = {
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',
Expand Down
6 changes: 0 additions & 6 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 1 addition & 16 deletions services/cloud-agent-next/.dev.vars.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=

Expand Down
4 changes: 2 additions & 2 deletions services/cloud-agent-next/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
21 changes: 7 additions & 14 deletions services/cloud-agent-next/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 0 additions & 2 deletions services/cloud-agent-next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
Expand Down
Loading