Skip to content
Open
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
2 changes: 2 additions & 0 deletions cloudflare-gastown/container/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
.wrangler
5 changes: 5 additions & 0 deletions cloudflare-gastown/container/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Generated by scripts/prepare-container.mjs for Docker builds.
# pnpm needs the workspace yaml and lockfile to resolve catalog: references.
pnpm-workspace.yaml
pnpm-lock.yaml
package.prod.json
14 changes: 11 additions & 3 deletions cloudflare-gastown/container/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,23 @@ RUN cd /opt/gastown-plugin && npm install --omit=dev && \

WORKDIR /app

# Copy package files and install deps deterministically
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile --production
# ── Install production deps via pnpm ────────────────────────────────
# package.json uses pnpm catalog: references for shared versions.
# pnpm-workspace.yaml (catalog only) and pnpm-lock.yaml are copied
# from the monorepo root by `pnpm container:prepare`.
# package.prod.json is package.json with workspace: devDeps removed.
COPY pnpm-workspace.yaml pnpm-lock.yaml ./
COPY package.prod.json package.json
RUN pnpm install --prod

# Copy source (bun runs TypeScript directly — no build step needed)
COPY src/ ./src/

RUN chown -R agent:agent /app

# Explicitly set HOME so kilo resolves XDG paths correctly.
# Some container runtimes don't set HOME when switching USER.
ENV HOME=/home/agent
USER agent

EXPOSE 8080
Expand Down
14 changes: 11 additions & 3 deletions cloudflare-gastown/container/Dockerfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,23 @@ RUN cd /opt/gastown-plugin && npm install --omit=dev && \

WORKDIR /app

# Copy package files and install deps deterministically
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile --production
# ── Install production deps via pnpm ────────────────────────────────
# package.json uses pnpm catalog: references for shared versions.
# pnpm-workspace.yaml (catalog only) and pnpm-lock.yaml are copied
# from the monorepo root by `pnpm container:prepare`.
# package.prod.json is package.json with workspace: devDeps removed.
COPY pnpm-workspace.yaml pnpm-lock.yaml ./
COPY package.prod.json package.json
RUN pnpm install --prod

# Copy source (bun runs TypeScript directly — no build step needed)
COPY src/ ./src/

RUN chown -R agent:agent /app

# Explicitly set HOME so kilo resolves XDG paths correctly.
# Some container runtimes don't set HOME when switching USER.
ENV HOME=/home/agent
USER agent

EXPOSE 8080
Expand Down
241 changes: 0 additions & 241 deletions cloudflare-gastown/container/bun.lock

This file was deleted.

4 changes: 2 additions & 2 deletions cloudflare-gastown/container/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
"lint": "eslint --config eslint.config.mjs --cache 'src/**/*.ts'"
},
"dependencies": {
"@kilocode/plugin": "1.0.23",
"@kilocode/sdk": "1.0.23",
"@kilocode/plugin": "7.0.37",
"@kilocode/sdk": "7.0.37",
"hono": "catalog:",
"zod": "catalog:"
},
Expand Down
1 change: 0 additions & 1 deletion cloudflare-gastown/container/plugin/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ export class GastownClient {
private agentId: string;
private rigId: string;
private townId: string;

constructor(env: GastownEnv) {
this.baseUrl = env.apiUrl.replace(/\/+$/, '');
this.token = env.sessionToken;
Expand Down
12 changes: 12 additions & 0 deletions cloudflare-gastown/container/src/agent-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,18 @@ function buildAgentEnv(request: StartAgentRequest): Record<string, string> {
console.warn('[buildAgentEnv] No KILOCODE_TOKEN available — KILO_CONFIG_CONTENT not set');
}

// Authenticate the gh CLI via GH_TOKEN so agents can use `gh` commands.
// GIT_TOKEN is a GitHub access token set by the town config's git_auth.
// Set before the envVars loop so user-provided GH_TOKEN in town env vars
// cannot override the platform credential (intentional — prevents agents
// from being pointed at a different GitHub identity).
const ghToken = resolveEnv(request, 'GIT_TOKEN') ?? resolveEnv(request, 'GITHUB_TOKEN');
if (ghToken) {
env.GH_TOKEN = ghToken;
}

// Town-level env vars. The `!(key in env)` guard means infra-set vars
// (GASTOWN_*, KILO_*, GH_TOKEN, etc.) take precedence over user config.
if (request.envVars) {
for (const [key, value] of Object.entries(request.envVars)) {
if (!(key in env)) {
Expand Down
16 changes: 8 additions & 8 deletions cloudflare-gastown/container/src/process-manager.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/**
* Agent manager — tracks agents as SDK-managed opencode sessions.
* Agent manager — tracks agents as SDK-managed kilo sessions.
*
* Uses @kilocode/sdk's createOpencode() to start server instances in-process
* Uses @kilocode/sdk's createKilo() to start server instances in-process
* and client.event.subscribe() for typed event streams. No subprocesses,
* no SSE text parsing, no ring buffers.
*/

import { createOpencode, type OpencodeClient } from '@kilocode/sdk';
import { createKilo, type KiloClient } from '@kilocode/sdk';
import { z } from 'zod';
import type { ManagedAgent, StartAgentRequest } from './types';
import { reportAgentCompleted } from './completion-reporter';
Expand All @@ -18,7 +18,7 @@ const MANAGER_LOG = '[process-manager]';
const SessionResponse = z.object({ id: z.string().min(1) }).passthrough();

type SDKInstance = {
client: OpencodeClient;
client: KiloClient;
server: { url: string; close(): void };
sessionCount: number;
};
Expand Down Expand Up @@ -119,7 +119,7 @@ function broadcastEvent(agentId: string, event: string, data: unknown): void {
async function ensureSDKServer(
workdir: string,
env: Record<string, string>
): Promise<{ client: OpencodeClient; port: number }> {
): Promise<{ client: KiloClient; port: number }> {
const existing = sdkInstances.get(workdir);
if (existing) {
return {
Expand All @@ -131,7 +131,7 @@ async function ensureSDKServer(
const port = nextPort++;
console.log(`${MANAGER_LOG} Starting SDK server on port ${port} for ${workdir}`);

// Save env vars that we'll mutate, set them for createOpencode, then restore.
// Save env vars that we'll mutate, set them for createKilo, then restore.
// This avoids permanent global mutation when multiple agents start with
// different env — each server gets the env it was started with.
const envSnapshot: Record<string, string | undefined> = {};
Expand All @@ -144,7 +144,7 @@ async function ensureSDKServer(
const prevCwd = process.cwd();
try {
process.chdir(workdir);
const { client, server } = await createOpencode({
const { client, server } = await createKilo({
hostname: '127.0.0.1',
port,
timeout: 30_000,
Expand Down Expand Up @@ -172,7 +172,7 @@ async function ensureSDKServer(
* Subscribe to SDK events for an agent's session and forward them.
*/
async function subscribeToEvents(
client: OpencodeClient,
client: KiloClient,
agent: ManagedAgent,
request: StartAgentRequest
): Promise<void> {
Expand Down
9 changes: 5 additions & 4 deletions cloudflare-gastown/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
"description": "Gastown: AI agent orchestration via Durable Objects",
"scripts": {
"preinstall": "npx only-allow pnpm",
"deploy:prod": "wrangler deploy --env=\"\"",
"deploy:dev": "wrangler deploy --env dev",
"dev": "wrangler dev --env dev",
"start": "wrangler dev --env dev",
"container:prepare": "node scripts/prepare-container.mjs",
"deploy:prod": "pnpm container:prepare && wrangler deploy --env=\"\"",
"deploy:dev": "pnpm container:prepare && wrangler deploy --env dev",
"dev": "pnpm container:prepare && wrangler dev --env dev --ip 0.0.0.0",
"start": "pnpm container:prepare && wrangler dev --env dev --ip 0.0.0.0",
"types": "wrangler types",
"test": "vitest run",
"test:watch": "vitest",
Expand Down
61 changes: 61 additions & 0 deletions cloudflare-gastown/scripts/prepare-container.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Copy the root pnpm-workspace.yaml (catalog only) and pnpm-lock.yaml
* into the container build context so pnpm can resolve catalog: references.
*
* The packages: section and other non-catalog sections are stripped because
* those workspace paths don't exist inside the container and would cause
* pnpm to error on workspace: references.
*/

import { readFileSync, writeFileSync, copyFileSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const gastownRoot = resolve(__dirname, '..');
const repoRoot = resolve(gastownRoot, '..');
const containerDir = resolve(gastownRoot, 'container');

// Read root workspace yaml and extract only the catalog: section.
// Parse line-by-line: keep lines from `catalog:` until the next
// top-level key (a line starting with a non-space, non-comment char).
const lines = readFileSync(resolve(repoRoot, 'pnpm-workspace.yaml'), 'utf8').split('\n');
const catalogLines = [];
let inCatalog = false;

for (const line of lines) {
if (line.startsWith('catalog:')) {
inCatalog = true;
catalogLines.push(line);
continue;
}
if (inCatalog) {
// Still in catalog if line is indented, empty, or a comment
if (line === '' || line.startsWith(' ') || line.startsWith('\t') || line.startsWith('#')) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION]: Empty lines at the end of the catalog: section are included in the output.

If pnpm-workspace.yaml has blank lines between catalog: entries and the next top-level key, those trailing blank lines are captured. This is benign for YAML parsing but could be tightened by trimming trailing empty lines from catalogLines before writing.

catalogLines.push(line);
} else {
break;
}
}
}

writeFileSync(resolve(containerDir, 'pnpm-workspace.yaml'), catalogLines.join('\n') + '\n');

// Create a production-only package.json that strips workspace: references
// (they can't be resolved outside the monorepo).
const pkg = JSON.parse(readFileSync(resolve(containerDir, 'package.json'), 'utf8'));
if (pkg.devDependencies) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION]: Only strips workspace: references from devDependencies, not dependencies

If a production dependency in container/package.json ever uses a workspace: specifier (currently they use catalog: so this is fine today), the generated package.prod.json would still contain the unresolvable reference and pnpm install --prod would fail inside the container.

Consider also stripping workspace: from dependencies for robustness:

for (const section of ['dependencies', 'devDependencies']) {
  if (pkg[section]) {
    for (const [name, version] of Object.entries(pkg[section])) {
      if (typeof version === 'string' && version.startsWith('workspace:')) {
        delete pkg[section][name];
      }
    }
  }
}

for (const [name, version] of Object.entries(pkg.devDependencies)) {
if (typeof version === 'string' && version.startsWith('workspace:')) {
delete pkg.devDependencies[name];
}
}
}
writeFileSync(resolve(containerDir, 'package.prod.json'), JSON.stringify(pkg, null, 2) + '\n');

// Copy the lockfile as-is
copyFileSync(resolve(repoRoot, 'pnpm-lock.yaml'), resolve(containerDir, 'pnpm-lock.yaml'));

console.log(
'Prepared container build context with pnpm-workspace.yaml (catalog only) and pnpm-lock.yaml'
);
2 changes: 2 additions & 0 deletions cloudflare-gastown/src/db/tables/bead-events.table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export const BeadEventType = z.enum([
'review_completed',
'agent_spawned',
'agent_exited',
'pr_created',
'pr_creation_failed',
]);

export type BeadEventType = z.infer<typeof BeadEventType>;
Expand Down
Loading
Loading