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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ supabase/.temp

.kilo/plans

# ESLint cache
.eslintcache

# @kilocode/trpc build output (rebuilt by: pnpm --filter @kilocode/trpc run build)
packages/trpc/dist/
.plan/
68 changes: 56 additions & 12 deletions cloudflare-gastown/container/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,18 +1,62 @@
FROM oven/bun:1-slim

# Install git, gh CLI, and Node.js (required by @kilocode/cli which uses #!/usr/bin/env node)
# Install dev toolchain, search tools, build deps, gh CLI, and Node.js
# Package categories:
# version control: git, git-lfs
# network/download: curl, wget, ca-certificates, gnupg, unzip
# build toolchain: build-essential, autoconf
# search tools: ripgrep, jq
# compression: bzip2, zstd
# SSL/crypto: libssl-dev, libffi-dev
# database client libs: libdb-dev, libgdbm-dev, libgdbm6
# Python build deps: libbz2-dev, liblzma-dev, libncurses5-dev, libreadline-dev, zlib1g-dev
# Ruby build deps: libyaml-dev
# image processing: libvips-dev
# browser/rendering: libgbm1
# C++ stdlib: libc++1
# math: libgmp-dev
# timezone data: tzdata
RUN apt-get update && \
apt-get install -y --no-install-recommends git git-lfs curl ca-certificates && \
curl -fsSL https://deb.nodesource.com/setup_24.x | bash - && \
apt-get install -y --no-install-recommends nodejs && \
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
-o /usr/share/keyrings/githubcli-archive-keyring.gpg && \
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
> /etc/apt/sources.list.d/github-cli.list && \
apt-get update && \
apt-get install -y --no-install-recommends gh && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
apt-get install -y --no-install-recommends \
git \
git-lfs \
curl \
wget \
ca-certificates \
gnupg \
unzip \
build-essential \
autoconf \
ripgrep \
jq \
bzip2 \
zstd \
libssl-dev \
libffi-dev \
libdb-dev \
libgdbm-dev \
libgdbm6 \
libbz2-dev \
liblzma-dev \
libncurses5-dev \
libreadline-dev \
zlib1g-dev \
libyaml-dev \
libvips-dev \
libgbm1 \
libc++1 \
libgmp-dev \
tzdata \
&& curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
-o /usr/share/keyrings/githubcli-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
> /etc/apt/sources.list.d/github-cli.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends gh \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

RUN git lfs install --system

Expand Down
68 changes: 56 additions & 12 deletions cloudflare-gastown/container/Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -1,18 +1,62 @@
FROM --platform=linux/arm64 oven/bun:1-slim

# Install git, gh CLI, and Node.js (required by @kilocode/cli which uses #!/usr/bin/env node)
# Install dev toolchain, search tools, build deps, gh CLI, and Node.js
# Package categories:
# version control: git, git-lfs
# network/download: curl, wget, ca-certificates, gnupg, unzip
# build toolchain: build-essential, autoconf
# search tools: ripgrep, jq
# compression: bzip2, zstd
# SSL/crypto: libssl-dev, libffi-dev
# database client libs: libdb-dev, libgdbm-dev, libgdbm6
# Python build deps: libbz2-dev, liblzma-dev, libncurses5-dev, libreadline-dev, zlib1g-dev
# Ruby build deps: libyaml-dev
# image processing: libvips-dev
# browser/rendering: libgbm1
# C++ stdlib: libc++1
# math: libgmp-dev
# timezone data: tzdata
RUN apt-get update && \
apt-get install -y --no-install-recommends git git-lfs curl ca-certificates && \
curl -fsSL https://deb.nodesource.com/setup_24.x | bash - && \
apt-get install -y --no-install-recommends nodejs && \
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
-o /usr/share/keyrings/githubcli-archive-keyring.gpg && \
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
> /etc/apt/sources.list.d/github-cli.list && \
apt-get update && \
apt-get install -y --no-install-recommends gh && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
apt-get install -y --no-install-recommends \
git \
git-lfs \
curl \
wget \
ca-certificates \
gnupg \
unzip \
build-essential \
autoconf \
ripgrep \
jq \
bzip2 \
zstd \
libssl-dev \
libffi-dev \
libdb-dev \
libgdbm-dev \
libgdbm6 \
libbz2-dev \
liblzma-dev \
libncurses5-dev \
libreadline-dev \
zlib1g-dev \
libyaml-dev \
libvips-dev \
libgbm1 \
libc++1 \
libgmp-dev \
tzdata \
&& curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
-o /usr/share/keyrings/githubcli-archive-keyring.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
> /etc/apt/sources.list.d/github-cli.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends gh \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

RUN git lfs install --system

Expand Down
1 change: 1 addition & 0 deletions cloudflare-gastown/container/plugin/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ export class MayorGastownClient {
title: string;
body?: string;
metadata?: Record<string, unknown>;
labels?: string[];
}): Promise<SlingResult> {
return this.request<SlingResult>(this.mayorPath('/sling'), {
method: 'POST',
Expand Down
29 changes: 10 additions & 19 deletions cloudflare-gastown/container/plugin/mayor-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,6 @@ function parseUiAction(value: unknown): UiActionInput {
return value as UiActionInput;
}

function parseJsonObject(value: string, label: string): Record<string, unknown> {
let parsed: unknown;
try {
parsed = JSON.parse(value);
} catch {
throw new Error(`Invalid JSON in "${label}"`);
}
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
throw new Error(
`"${label}" must be a JSON object, got ${Array.isArray(parsed) ? 'array' : typeof parsed}`
);
}
return parsed as Record<string, unknown>;
}

/**
* Mayor-specific tools for cross-rig delegation.
* These are only registered when `GASTOWN_AGENT_ROLE=mayor`.
Expand All @@ -64,17 +49,23 @@ export function createMayorTools(client: MayorGastownClient) {
)
.optional(),
metadata: tool.schema
.string()
.describe('JSON-encoded metadata object for additional context')
.record(tool.schema.string(), tool.schema.unknown())
.describe(
'Metadata object for additional context (e.g. { pr_url, branch, target_branch })'
)
.optional(),
labels: tool.schema
.array(tool.schema.string())
.describe('Labels to attach to the bead (e.g. ["gt:pr-fixup"])')
.optional(),
},
async execute(args) {
const metadata = args.metadata ? parseJsonObject(args.metadata, 'metadata') : undefined;
const result = await client.sling({
rig_id: args.rig_id,
title: args.title,
body: args.body,
metadata,
metadata: args.metadata,
labels: args.labels,
});
return [
`Task slung successfully.`,
Expand Down
27 changes: 6 additions & 21 deletions cloudflare-gastown/container/plugin/tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,33 +232,18 @@ describe('tools', () => {
expect(result).toContain('priority: high');
});

it('parses metadata JSON string', async () => {
await tools.gt_escalate.execute({ title: 'Test', metadata: '{"key": "value"}' }, CTX);
it('passes metadata object through to createEscalation', async () => {
await tools.gt_escalate.execute(
{ title: 'Test', metadata: { key: 'value', nested: 123 } },
CTX
);
expect(client.createEscalation).toHaveBeenCalledWith({
title: 'Test',
body: undefined,
priority: undefined,
metadata: { key: 'value' },
metadata: { key: 'value', nested: 123 },
});
});

it('throws on invalid metadata JSON', async () => {
await expect(
tools.gt_escalate.execute({ title: 'Test', metadata: 'not json' }, CTX)
).rejects.toThrow('Invalid JSON in "metadata"');
});

it('throws when metadata is a JSON array instead of object', async () => {
await expect(
tools.gt_escalate.execute({ title: 'Test', metadata: '[1, 2]' }, CTX)
).rejects.toThrow('"metadata" must be a JSON object, got array');
});

it('throws when metadata is a JSON string instead of object', async () => {
await expect(
tools.gt_escalate.execute({ title: 'Test', metadata: '"hello"' }, CTX)
).rejects.toThrow('"metadata" must be a JSON object, got string');
});
});

describe('gt_checkpoint', () => {
Expand Down
17 changes: 3 additions & 14 deletions cloudflare-gastown/container/plugin/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,6 @@ function parseJsonArg(value: string, label: string): unknown {
}
}

function parseJsonObject(value: string, label: string): Record<string, unknown> {
const parsed = parseJsonArg(value, label);
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
throw new Error(
`"${label}" must be a JSON object, got ${Array.isArray(parsed) ? 'array' : typeof parsed}`
);
}
return Object.fromEntries(Object.entries(parsed as object));
}

export function createTools(client: GastownClient) {
return {
gt_prime: tool({
Expand Down Expand Up @@ -155,17 +145,16 @@ export function createTools(client: GastownClient) {
.describe('Severity level (defaults to medium)')
.optional(),
metadata: tool.schema
.string()
.describe('JSON-encoded metadata object for additional context')
.record(tool.schema.string(), tool.schema.unknown())
.describe('Metadata object for additional context')
.optional(),
},
async execute(args) {
const metadata = args.metadata ? parseJsonObject(args.metadata, 'metadata') : undefined;
const bead = await client.createEscalation({
title: args.title,
body: args.body,
priority: args.priority,
metadata,
metadata: args.metadata,
});
return `Escalation created: ${bead.bead_id} (priority: ${bead.priority})`;
},
Expand Down
19 changes: 19 additions & 0 deletions cloudflare-gastown/container/src/control-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,15 @@ function syncTownConfigToProcessEnv(): void {
} else {
delete process.env.GASTOWN_DISABLE_AI_COAUTHOR;
}

// Keep the standalone env var in sync with the town config so org
// billing context is never lost across model changes.
const orgId = cfg.organization_id;
if (typeof orgId === 'string' && orgId) {
process.env.GASTOWN_ORGANIZATION_ID = orgId;
} else {
delete process.env.GASTOWN_ORGANIZATION_ID;
}
}

export const app = new Hono();
Expand Down Expand Up @@ -216,6 +225,11 @@ app.post('/agents/start', async c => {
return c.json({ error: 'Invalid request body', issues: parsed.error.issues }, 400);
}

// Persist the organization ID as a standalone env var so it survives
// config rebuilds (e.g. model hot-swap). The env var is the primary
// source of truth; KILO_CONFIG_CONTENT extraction is the fallback.
process.env.GASTOWN_ORGANIZATION_ID = parsed.data.organizationId ?? '';

console.log(
`[control-server] /agents/start: role=${parsed.data.role} name=${parsed.data.name} rigId=${parsed.data.rigId} agentId=${parsed.data.agentId}`
);
Expand Down Expand Up @@ -285,6 +299,11 @@ app.patch('/agents/:agentId/model', async c => {
return c.json({ error: 'Invalid request body', issues: parsed.error.issues }, 400);
}

// Update org billing context from the request body if provided.
if (parsed.data.organizationId) {
process.env.GASTOWN_ORGANIZATION_ID = parsed.data.organizationId;
}

// Sync config-derived env vars from X-Town-Config into process.env so
// the SDK server restart picks up fresh tokens and git identity.
// The middleware already parsed the header into lastKnownTownConfig.
Expand Down
Loading
Loading