Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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