Skip to content

Commit 98e042b

Browse files
committed
refactor(cloud-agent-next): migrate to git-token-service RPC
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<WorkerEnv> 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.
1 parent 34828cb commit 98e042b

21 files changed

Lines changed: 446 additions & 662 deletions

apps/web/src/routers/cloud-agent-next-router.ts

Lines changed: 7 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,8 @@ import {
1010
mergeProfileConfiguration,
1111
ProfileNotFoundError,
1212
} from '@/lib/agent/profile-session-config';
13+
import { fetchGitHubRepositoriesForUser } from '@/lib/cloud-agent/github-integration-helpers';
1314
import {
14-
getGitHubTokenForUser,
15-
fetchGitHubRepositoriesForUser,
16-
} from '@/lib/cloud-agent/github-integration-helpers';
17-
import {
18-
getGitLabTokenForUser,
1915
getGitLabInstanceUrlForUser,
2016
buildGitLabCloneUrl,
2117
fetchGitLabRepositoriesForUser,
@@ -80,28 +76,19 @@ export const cloudAgentNextRouter = createTRPCRouter({
8076
setupCommands,
8177
});
8278

83-
// Determine git source: GitLab uses gitUrl/gitToken, GitHub uses githubRepo/githubToken
79+
// Determine git source: GitLab uses gitUrl, GitHub uses githubRepo.
80+
// Tokens are resolved inside cloud-agent-next via GIT_TOKEN_SERVICE.
8481
let gitParams: {
8582
githubRepo?: string;
8683
gitUrl?: string;
87-
gitToken?: string;
8884
platform?: 'github' | 'gitlab';
8985
};
9086

9187
if (gitlabProject) {
92-
// GitLab flow: convert gitlabProject to gitUrl + gitToken
93-
const gitToken = await getGitLabTokenForUser(ctx.user.id);
94-
if (!gitToken) {
95-
throw new TRPCError({
96-
code: 'BAD_REQUEST',
97-
message: 'No GitLab integration found. Please connect your GitLab account first.',
98-
});
99-
}
10088
const instanceUrl = await getGitLabInstanceUrlForUser(ctx.user.id);
10189
const gitUrl = buildGitLabCloneUrl(gitlabProject, instanceUrl);
102-
gitParams = { gitUrl, gitToken, platform: PLATFORM.GITLAB };
90+
gitParams = { gitUrl, platform: PLATFORM.GITLAB };
10391
} else {
104-
// GitHub flow: use githubRepo (token will be fetched in cloud-agent-next)
10592
gitParams = { githubRepo, platform: PLATFORM.GITHUB };
10693
}
10794

@@ -163,29 +150,10 @@ export const cloudAgentNextRouter = createTRPCRouter({
163150
const authToken = generateCloudAgentToken(ctx.user);
164151
const client = createCloudAgentNextClient(authToken);
165152

166-
// Determine platform to fetch the correct token
167-
const session = await client.getSession(input.cloudAgentSessionId);
168-
let githubToken: string | undefined;
169-
let gitToken: string | undefined;
170-
171-
if (session.platform === 'gitlab') {
172-
gitToken = await getGitLabTokenForUser(ctx.user.id);
173-
if (!gitToken) {
174-
throw new TRPCError({
175-
code: 'BAD_REQUEST',
176-
message: 'No GitLab integration found. Please connect your GitLab account first.',
177-
});
178-
}
179-
} else {
180-
githubToken = await getGitHubTokenForUser(ctx.user.id);
181-
}
182-
153+
// Tokens are refreshed inside cloud-agent-next (GitHub App installation
154+
// for GitHub, GIT_TOKEN_SERVICE for managed GitLab).
183155
try {
184-
return await client.sendMessage({
185-
...input,
186-
githubToken,
187-
gitToken,
188-
});
156+
return await client.sendMessage(input);
189157
} catch (error) {
190158
rethrowAsPaymentRequired(error);
191159
throw error;

apps/web/src/routers/organizations/organization-cloud-agent-next-router.ts

Lines changed: 15 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,8 @@ import {
1414
organizationMemberProcedure,
1515
organizationMemberMutationProcedure,
1616
} from '@/routers/organizations/utils';
17+
import { fetchGitHubRepositoriesForOrganization } from '@/lib/cloud-agent/github-integration-helpers';
1718
import {
18-
getGitHubTokenForOrganization,
19-
fetchGitHubRepositoriesForOrganization,
20-
} from '@/lib/cloud-agent/github-integration-helpers';
21-
import {
22-
getGitLabTokenForOrganization,
2319
getGitLabInstanceUrlForOrganization,
2420
buildGitLabCloneUrl,
2521
fetchGitLabRepositoriesForOrganization,
@@ -141,28 +137,19 @@ export const organizationCloudAgentNextRouter = createTRPCRouter({
141137
setupCommands,
142138
});
143139

144-
// Determine git source: GitLab uses gitUrl/gitToken, GitHub uses githubRepo
140+
// Determine git source: GitLab uses gitUrl, GitHub uses githubRepo.
141+
// Tokens are resolved inside cloud-agent-next via GIT_TOKEN_SERVICE.
145142
let gitParams: {
146143
githubRepo?: string;
147144
gitUrl?: string;
148-
gitToken?: string;
149145
platform?: 'github' | 'gitlab';
150146
};
151147

152148
if (gitlabProject) {
153-
// GitLab flow: convert gitlabProject to gitUrl + gitToken
154-
const gitToken = await getGitLabTokenForOrganization(organizationId);
155-
if (!gitToken) {
156-
throw new TRPCError({
157-
code: 'BAD_REQUEST',
158-
message: 'No GitLab integration found. Please connect your GitLab account first.',
159-
});
160-
}
161149
const instanceUrl = await getGitLabInstanceUrlForOrganization(organizationId);
162150
const gitUrl = buildGitLabCloneUrl(gitlabProject, instanceUrl);
163-
gitParams = { gitUrl, gitToken, platform: PLATFORM.GITLAB };
151+
gitParams = { gitUrl, platform: PLATFORM.GITLAB };
164152
} else {
165-
// GitHub flow: use githubRepo (token will be fetched in cloud-agent-next)
166153
gitParams = { githubRepo, platform: PLATFORM.GITHUB };
167154
}
168155

@@ -226,30 +213,19 @@ export const organizationCloudAgentNextRouter = createTRPCRouter({
226213
const authToken = generateCloudAgentToken(ctx.user);
227214
const client = createCloudAgentNextClient(authToken);
228215

229-
const { organizationId, ...messageInput } = input;
230-
231-
// Determine platform to fetch the correct token
232-
const session = await client.getSession(messageInput.cloudAgentSessionId);
233-
let githubToken: string | undefined;
234-
let gitToken: string | undefined;
235-
236-
if (session.platform === 'gitlab') {
237-
gitToken = await getGitLabTokenForOrganization(organizationId);
238-
if (!gitToken) {
239-
throw new TRPCError({
240-
code: 'BAD_REQUEST',
241-
message: 'No GitLab integration found. Please connect your GitLab account first.',
242-
});
243-
}
244-
} else {
245-
githubToken = await getGitHubTokenForOrganization(organizationId);
246-
}
247-
216+
// Tokens are refreshed inside cloud-agent-next (GitHub App installation
217+
// for GitHub, GIT_TOKEN_SERVICE for managed GitLab). organizationId is
218+
// consumed by the membership middleware; it is not forwarded.
248219
try {
249220
return await client.sendMessage({
250-
...messageInput,
251-
githubToken,
252-
gitToken,
221+
cloudAgentSessionId: input.cloudAgentSessionId,
222+
prompt: input.prompt,
223+
mode: input.mode,
224+
model: input.model,
225+
variant: input.variant,
226+
autoCommit: input.autoCommit,
227+
messageId: input.messageId,
228+
images: input.images,
253229
});
254230
} catch (error) {
255231
rethrowAsPaymentRequired(error);

dev/local/services.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,22 @@ type ServiceGroup = {
1515

1616
const groups: ServiceGroup[] = [
1717
{ id: 'core', label: 'Core', alwaysOn: true },
18-
{ id: 'kiloclaw', label: 'KiloClaw', alwaysOn: false, sectionBreakBefore: true },
19-
{ id: 'cloud-agent', label: 'Cloud Agent', alwaysOn: false },
18+
{
19+
id: 'git-token-service',
20+
label: 'Git Tokens',
21+
alwaysOn: false,
22+
sectionBreakBefore: true,
23+
},
24+
{ id: 'kiloclaw', label: 'KiloClaw', alwaysOn: false },
25+
{
26+
id: 'cloud-agent',
27+
label: 'Cloud Agent',
28+
alwaysOn: false,
29+
groupDependsOn: ['git-token-service'],
30+
},
2031
{ id: 'code-review', label: 'Code Review', alwaysOn: false, groupDependsOn: ['cloud-agent'] },
2132
{ id: 'app-builder', label: 'App Builder', alwaysOn: false, groupDependsOn: ['cloud-agent'] },
22-
{ id: 'gastown', label: 'Gastown', alwaysOn: false },
33+
{ id: 'gastown', label: 'Gastown', alwaysOn: false, groupDependsOn: ['git-token-service'] },
2334
{
2435
id: 'auto-triage',
2536
label: 'Auto Triage',
@@ -59,7 +70,7 @@ const serviceMeta: Record<string, ServiceMeta> = {
5970
// cloud-agent
6071
'cloud-agent-next': {
6172
group: 'cloud-agent',
62-
dependsOn: ['postgres', 'nextjs', 'cloudflare-session-ingest'],
73+
dependsOn: ['postgres', 'nextjs', 'cloudflare-session-ingest', 'cloudflare-git-token-service'],
6374
dir: 'services/cloud-agent-next',
6475
useLanIp: true,
6576
},
@@ -73,6 +84,12 @@ const serviceMeta: Record<string, ServiceMeta> = {
7384
dependsOn: ['postgres'],
7485
dir: 'services/session-ingest',
7586
},
87+
// git-token-service (shared by cloud-agent, app-builder, gastown)
88+
'cloudflare-git-token-service': {
89+
group: 'git-token-service',
90+
dependsOn: ['postgres'],
91+
dir: 'services/git-token-service',
92+
},
7693
// app-builder
7794
'app-builder-tunnel': { group: 'app-builder', dependsOn: [] },
7895
'cloudflare-app-builder': {
@@ -86,11 +103,6 @@ const serviceMeta: Record<string, ServiceMeta> = {
86103
dependsOn: ['postgres'],
87104
dir: 'services/db-proxy',
88105
},
89-
'cloudflare-git-token-service': {
90-
group: 'app-builder',
91-
dependsOn: ['postgres'],
92-
dir: 'services/git-token-service',
93-
},
94106
// code-review
95107
'cloudflare-code-review-infra': {
96108
group: 'code-review',

pnpm-lock.yaml

Lines changed: 0 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

services/cloud-agent-next/.dev.vars.example

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -49,22 +49,7 @@ PER_SESSION_SANDBOX_ORG_IDS=
4949
GITHUB_APP_SLUG=kiloconnect-development
5050
GITHUB_APP_BOT_USER_ID=242397087
5151

52-
# GitHub App credentials for generating installation access tokens
53-
# Used by GitHubTokenService to authenticate with GitHub API on behalf of app installations
54-
# GITHUB_APP_ID: The numeric App ID from GitHub App settings
55-
# GITHUB_APP_PRIVATE_KEY: The raw PKCS#8 private key (use \n for newlines in env var)
56-
# Note that the nextjs app uses PKCS#1 but the worker uses WebCrypto and requires a PKCS#8
57-
GITHUB_APP_ID=2245043
58-
# @pkcs8
59-
GITHUB_APP_PRIVATE_KEY=
60-
61-
# GitHub Lite App credentials (for OSS organizations with read-only permissions)
62-
# Same format as standard app credentials above
63-
GITHUB_LITE_APP_ID=
64-
# @pkcs8
65-
GITHUB_LITE_APP_PRIVATE_KEY=
66-
67-
# GitHub Lite App slug and bot user ID for git commit attribution (optional)
52+
# GitHub Lite App identity for git commit attribution on OSS organizations (optional)
6853
GITHUB_LITE_APP_SLUG=
6954
GITHUB_LITE_APP_BOT_USER_ID=
7055

services/cloud-agent-next/AGENTS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This file provides guidance to AI coding agents working in this repository.
44

55
## Project Overview
66

7-
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.
7+
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.
88

99
## Development Commands
1010

@@ -108,7 +108,7 @@ This pattern blocks API endpoints from running for external contributors who don
108108
- `src/persistence/` - Durable Object schema + migrations
109109
- `src/websocket/` - WebSocket ingest + filters
110110
- `src/utils/` - Shared helpers (encryption, retries, SQL helpers)
111-
- `wrangler.jsonc` - Bindings: R2, Hyperdrive, KV, queues, containers
111+
- `wrangler.jsonc` - Bindings: R2, Hyperdrive, queues, containers, service bindings (`SESSION_INGEST`, `GIT_TOKEN_SERVICE`)
112112
- `vitest.config.ts` - Unit test config
113113
- `vitest.workers.config.ts` - Integration test config
114114
- `wrapper/` - Wrapper build shipped into the sandbox

services/cloud-agent-next/README.md

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -832,28 +832,21 @@ This enables:
832832

833833
#### GitHub App Token Generation
834834

835-
For V2 routes (`sendMessageV2`, `initiateFromKilocodeSessionV2`), the cloud-agent generates GitHub App installation tokens on-demand.
835+
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).
836836

837837
**How it works:**
838838

839-
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.
840-
2. **On-demand token generation**: When execution starts, `GitHubTokenService` generates a fresh token using `@octokit/auth-app`
841-
3. **KV caching**: Tokens are cached in Cloudflare KV with 30-minute TTL (tokens valid for 1 hour)
842-
4. **Cache key format**: `github-token:installation:{installationId}`
839+
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`.
840+
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`.
843841

844842
**Configuration:**
845843

846-
The worker requires these environment variables:
847-
848-
- `GITHUB_APP_ID`: GitHub App ID (configured in `wrangler.jsonc`)
849-
- `GITHUB_APP_PRIVATE_KEY`: RSA private key for the GitHub App (set via `wrangler secret put`)
850-
- `GITHUB_TOKEN_CACHE`: KV namespace binding for token caching
851-
- `HYPERDRIVE`: Hyperdrive binding for database access (installation ID lookup)
844+
- `GIT_TOKEN_SERVICE`: service binding to the `git-token-service` Worker (prod) / `git-token-service-dev` (dev). Configured in `wrangler.jsonc`.
845+
- `GITHUB_APP_SLUG` / `GITHUB_APP_BOT_USER_ID` (and the Lite equivalents): used only for git commit author attribution — not for token generation.
852846

853847
**Benefits:**
854848

855-
- No need to pass `githubInstallationId`automatically resolved from database
856-
- Tokens generated closer to where they're used (reduced latency)
849+
- No need to pass `githubInstallationId` — resolved centrally by `git-token-service`
850+
- GitHub App credentials and token cache kept in a single shared Worker
857851
- Fresh tokens on-demand rather than at session start
858-
- Rate limit protection via KV caching
859852
- No token expiry issues during long sessions

services/cloud-agent-next/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,8 @@
2929
"dependencies": {
3030
"@cloudflare/sandbox": "0.8.9",
3131
"@hono/trpc-server": "^0.4.2",
32-
"@kilocode/db": "workspace:*",
3332
"@kilocode/encryption": "workspace:*",
3433
"@kilocode/worker-utils": "workspace:*",
35-
"@octokit/auth-app": "catalog:",
3634
"@trpc/server": "catalog:",
3735
"drizzle-orm": "catalog:",
3836
"hono": "catalog:",

0 commit comments

Comments
 (0)