Skip to content

Commit fbcf719

Browse files
Merge branch 'main' into fix/posthog-rewrites-incorrect-order
2 parents 20e3ee1 + 9c0cd61 commit fbcf719

35 files changed

Lines changed: 4379 additions & 228 deletions

cloudflare-gastown/container/src/agent-runner.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -439,8 +439,8 @@ export async function runAgent(originalRequest: StartAgentRequest): Promise<Mana
439439
let request = originalRequest;
440440
let workdir: string;
441441

442-
if (request.role === 'triage') {
443-
// Triage agents are pure reasoning — no code changes, no git needed.
442+
if (request.role === 'triage' || request.lightweight) {
443+
// Triage/lightweight agents are pure reasoning — no code changes, no git needed.
444444
// Use a lightweight workspace to avoid clone failures feeding the loop.
445445
workdir = await createLightweightWorkspace('triage', request.rigId);
446446
} else if (request.role === 'mayor') {

cloudflare-gastown/container/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export const StartAgentRequest = z.object({
2727
platformIntegrationId: z.string().optional(),
2828
/** Git ref to branch from (e.g. convoy feature branch). Falls back to HEAD if absent. */
2929
startPoint: z.string().optional(),
30+
/** Skip repo clone — use a lightweight git-init-only workspace (for reasoning-only agents like triage). */
31+
lightweight: z.boolean().optional(),
3032
/** Rig list for mayor agents — used to set up browse worktrees on fresh containers. */
3133
rigs: z
3234
.array(

cloudflare-gastown/src/dos/Town.do.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2640,9 +2640,7 @@ export class TownDO extends DurableObject<Env> {
26402640
userId: rigConfig.userId,
26412641
agentId: triageAgent.id,
26422642
agentName: triageAgent.name,
2643-
// Use 'triage' role so the container skips the git clone entirely.
2644-
// Triage work is purely reasoning — no code changes needed.
2645-
role: 'triage',
2643+
role: 'polecat',
26462644
identity: triageAgent.identity,
26472645
beadId: triageBead.bead_id,
26482646
beadTitle: triageBead.title,
@@ -2654,6 +2652,7 @@ export class TownDO extends DurableObject<Env> {
26542652
townConfig,
26552653
systemPromptOverride: systemPrompt,
26562654
platformIntegrationId: rigConfig.platformIntegrationId,
2655+
lightweight: true,
26572656
});
26582657

26592658
if (started) {

cloudflare-gastown/src/dos/town/beads.ts

Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -671,18 +671,11 @@ export function getConvoyDependencyEdges(
671671
}
672672

673673
/**
674-
* Find the convoy a bead belongs to (if any).
675-
*
676-
* Two cases:
677-
* 1. Normal source bead: tracked by a convoy via bead_dependencies
678-
* (bead_id = sourceBeadId, depends_on_bead_id = convoyId, type = 'tracks').
679-
* Returns the convoy bead_id.
680-
* 2. The bead IS the convoy (e.g. for the final landing MR where processConvoyLandings
681-
* passes the convoy bead_id as the source). Returns beadId itself.
674+
* Find the convoy a bead belongs to (if any) via 'tracks' dependencies.
675+
* Returns the convoy bead_id or null.
682676
*/
683677
export function getConvoyForBead(sql: SqlStorage, beadId: string): string | null {
684-
// Case 1: bead is tracked by a convoy
685-
const trackRows = [
678+
const rows = [
686679
...query(
687680
sql,
688681
/* sql */ `
@@ -694,24 +687,8 @@ export function getConvoyForBead(sql: SqlStorage, beadId: string): string | null
694687
[beadId]
695688
),
696689
];
697-
if (trackRows.length > 0) {
698-
return z.object({ depends_on_bead_id: z.string() }).parse(trackRows[0]).depends_on_bead_id;
699-
}
700-
701-
// Case 2: bead is itself a convoy (has convoy_metadata)
702-
const metaRows = [
703-
...query(
704-
sql,
705-
/* sql */ `
706-
SELECT 1 FROM ${convoy_metadata}
707-
WHERE ${convoy_metadata.bead_id} = ?
708-
`,
709-
[beadId]
710-
),
711-
];
712-
if (metaRows.length > 0) return beadId;
713-
714-
return null;
690+
if (rows.length === 0) return null;
691+
return z.object({ depends_on_bead_id: z.string() }).parse(rows[0]).depends_on_bead_id;
715692
}
716693

717694
/**

cloudflare-gastown/src/dos/town/container-dispatch.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,8 @@ export async function startAgentInContainer(
240240
platformIntegrationId?: string;
241241
/** For convoy beads: the convoy's feature branch to branch from instead of defaultBranch. */
242242
convoyFeatureBranch?: string;
243+
/** Skip repo clone — use a lightweight workspace (for reasoning-only agents like triage). */
244+
lightweight?: boolean;
243245
/** All rigs in the town (mayor only) — used to set up browse worktrees on fresh containers. */
244246
rigs?: Array<{
245247
rigId: string;
@@ -346,6 +348,7 @@ export async function startAgentInContainer(
346348
// For convoy agents, start from the convoy's feature branch so the
347349
// worktree includes all previously merged convoy work.
348350
startPoint: params.convoyFeatureBranch ? `origin/${params.convoyFeatureBranch}` : undefined,
351+
lightweight: params.lightweight,
349352
rigs: params.rigs,
350353
}),
351354
});

cloudflare-gastown/src/dos/town/review-queue.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -277,13 +277,10 @@ export function completeReviewWithResult(
277277
const mergeTimestamp = now();
278278
closeBead(sql, entry.bead_id, entry.agent_id);
279279

280-
// Explicitly trigger convoy progress for the source bead after the MR closes.
281-
// closeBead → updateBeadStatus → updateConvoyProgress, but only if the source
282-
// bead's status actually changes. If the polecat already closed the source bead
283-
// before submitting to the review queue, the guard in updateBeadStatus short-
284-
// circuits and updateConvoyProgress is never called. Calling it here directly
285-
// ensures the convoy recounts after the MR bead is now closed (not in-flight),
286-
// so the source bead passes the NOT EXISTS guard and counts toward closedCount.
280+
// closeBead → updateBeadStatus short-circuits when completeReview already
281+
// set the status to 'closed' via direct SQL, so updateConvoyProgress is
282+
// never reached transitively. Call it explicitly to ensure the convoy
283+
// recounts after the MR bead is closed.
287284
updateConvoyProgress(sql, entry.bead_id, mergeTimestamp);
288285

289286
// If this was a convoy landing MR, also set landed_at on the convoy metadata

cloudflare-gastown/src/trpc/init.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,15 @@ export const gastownProcedure = procedure.use(async ({ ctx, next }) => {
3636
}
3737
return next({ ctx });
3838
});
39+
40+
/**
41+
* Admin-only procedure — requires `isAdmin` on the JWT. Used for admin
42+
* panel endpoints that bypass per-user ownership checks (e.g. town-wide
43+
* bead/agent listing for support diagnostics).
44+
*/
45+
export const adminProcedure = procedure.use(async ({ ctx, next }) => {
46+
if (!ctx.isAdmin) {
47+
throw new TRPCError({ code: 'FORBIDDEN', message: 'Admin access required' });
48+
}
49+
return next({ ctx });
50+
});

cloudflare-gastown/src/trpc/router.ts

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
/* eslint-disable @typescript-eslint/await-thenable -- DO RPC stubs return Rpc.Promisified which is thenable at runtime */
99
import { TRPCError } from '@trpc/server';
1010
import { z } from 'zod';
11-
import { router, gastownProcedure } from './init';
11+
import { router, gastownProcedure, adminProcedure } from './init';
1212
import { getTownDOStub } from '../dos/Town.do';
1313
import { getTownContainerStub } from '../dos/TownContainer.do';
1414
import { getGastownUserStub } from '../dos/GastownUser.do';
@@ -598,6 +598,104 @@ export const gastownRouter = router({
598598
const status = await townStub.getConvoyStatus(input.convoyId);
599599
return status ?? { ...convoy, beads: [] };
600600
}),
601+
602+
// ── Admin-only routes (bypass ownership checks) ──────────────────────
603+
604+
adminListBeads: adminProcedure
605+
.input(
606+
z.object({
607+
townId: z.string().uuid(),
608+
status: z.enum(['open', 'in_progress', 'closed', 'failed']).optional(),
609+
type: z
610+
.enum(['issue', 'message', 'escalation', 'merge_request', 'convoy', 'molecule', 'agent'])
611+
.optional(),
612+
limit: z.number().int().positive().max(500).default(200),
613+
})
614+
)
615+
.output(z.array(RpcBeadOutput))
616+
.query(async ({ ctx, input }) => {
617+
const townStub = getTownDOStub(ctx.env, input.townId);
618+
return townStub.listBeads({
619+
status: input.status,
620+
type: input.type,
621+
limit: input.limit,
622+
});
623+
}),
624+
625+
adminListAgents: adminProcedure
626+
.input(z.object({ townId: z.string().uuid() }))
627+
.output(z.array(RpcAgentOutput))
628+
.query(async ({ ctx, input }) => {
629+
const townStub = getTownDOStub(ctx.env, input.townId);
630+
return townStub.listAgents({});
631+
}),
632+
633+
adminForceRestartContainer: adminProcedure
634+
.input(z.object({ townId: z.string().uuid() }))
635+
.mutation(async ({ ctx, input }) => {
636+
const containerStub = getTownContainerStub(ctx.env, input.townId);
637+
await containerStub.destroy();
638+
}),
639+
640+
adminForceResetAgent: adminProcedure
641+
.input(z.object({ townId: z.string().uuid(), agentId: z.string().uuid() }))
642+
.mutation(async ({ ctx, input }) => {
643+
const townStub = getTownDOStub(ctx.env, input.townId);
644+
await townStub.unhookBead(input.agentId);
645+
await townStub.updateAgentStatus(input.agentId, 'idle');
646+
}),
647+
648+
adminForceCloseBead: adminProcedure
649+
.input(z.object({ townId: z.string().uuid(), beadId: z.string().uuid() }))
650+
.output(RpcBeadOutput)
651+
.mutation(async ({ ctx, input }) => {
652+
const townStub = getTownDOStub(ctx.env, input.townId);
653+
return townStub.closeBead(input.beadId, 'admin');
654+
}),
655+
656+
adminForceFailBead: adminProcedure
657+
.input(z.object({ townId: z.string().uuid(), beadId: z.string().uuid() }))
658+
.output(RpcBeadOutput)
659+
.mutation(async ({ ctx, input }) => {
660+
const townStub = getTownDOStub(ctx.env, input.townId);
661+
return townStub.updateBeadStatus(input.beadId, 'failed', 'admin');
662+
}),
663+
664+
adminGetAlarmStatus: adminProcedure
665+
.input(z.object({ townId: z.string().uuid() }))
666+
.output(RpcAlarmStatusOutput)
667+
.query(async ({ ctx, input }) => {
668+
const townStub = getTownDOStub(ctx.env, input.townId);
669+
await townStub.setTownId(input.townId);
670+
return townStub.getAlarmStatus();
671+
}),
672+
673+
adminGetTownEvents: adminProcedure
674+
.input(
675+
z.object({
676+
townId: z.string().uuid(),
677+
beadId: z.string().uuid().optional(),
678+
since: z.string().optional(),
679+
limit: z.number().int().positive().max(500).default(100),
680+
})
681+
)
682+
.output(z.array(RpcBeadEventOutput))
683+
.query(async ({ ctx, input }) => {
684+
const townStub = getTownDOStub(ctx.env, input.townId);
685+
return townStub.listBeadEvents({
686+
beadId: input.beadId,
687+
since: input.since,
688+
limit: input.limit,
689+
});
690+
}),
691+
692+
adminGetBead: adminProcedure
693+
.input(z.object({ townId: z.string().uuid(), beadId: z.string().uuid() }))
694+
.output(RpcBeadOutput.nullable())
695+
.query(async ({ ctx, input }) => {
696+
const townStub = getTownDOStub(ctx.env, input.townId);
697+
return townStub.getBeadAsync(input.beadId);
698+
}),
601699
});
602700

603701
export type GastownRouter = typeof gastownRouter;

kiloclaw/src/schemas/instance-config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ export const InstanceConfigSchema = z.object({
4343
kilocodeApiKey: z.string().nullable().optional(),
4444
kilocodeApiKeyExpiresAt: z.string().nullable().optional(),
4545
kilocodeDefaultModel: z.string().nullable().optional(),
46+
// TODO: Legacy hardcoded channel storage. Kept for backward compat with
47+
// existing DO state and the decryptChannelTokens/buildEnvVars startup path.
48+
// Migrate to read from encryptedSecrets via catalog, then remove.
4649
channels: z
4750
.object({
4851
telegramBotToken: EncryptedEnvelopeSchema.optional(),
@@ -66,6 +69,8 @@ export type InstanceConfig = z.infer<typeof InstanceConfigSchema>;
6669
export type EncryptedEnvelope = z.infer<typeof EncryptedEnvelopeSchema>;
6770
export type EncryptedChannelTokens = NonNullable<InstanceConfig['channels']>;
6871

72+
// TODO: Legacy — no UI callers remain. Remove alongside patchChannels tRPC
73+
// mutation and PATCH /api/platform/channels worker route.
6974
export const ChannelsPatchSchema = z.object({
7075
userId: z.string().min(1),
7176
channels: z.object({

0 commit comments

Comments
 (0)