Skip to content

Commit 5f577f2

Browse files
committed
fix(gastown): restrict container restart/destroy to org owners, skip triage-request beads in Rule 1b
- forceRestartContainer and destroyContainer now check org owner/creator role before allowing the operation, matching updateTownConfig's auth - reconciler Rule 1b skips gt:triage-request beads whose assignee is intentionally set without a hook (patrol routes them to specific agents)
1 parent 9f1ca1e commit 5f577f2

File tree

2 files changed

+33
-0
lines changed

2 files changed

+33
-0
lines changed

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,13 @@ export function reconcileBeads(sql: SqlStorage, opts?: { draining?: boolean }):
648648
// are handled by other subsystems and don't need dispatch.
649649
if (bead.assignee_agent_bead_id === 'system') continue;
650650

651+
// Skip triage-request beads — patrol.createTriageRequest() sets
652+
// assignee_agent_bead_id to route the request to a specific agent,
653+
// but hookBead() intentionally refuses to hook triage-request beads.
654+
// Without this skip, the reconciler would clear the assignee on
655+
// every tick because the hook will never exist.
656+
if (bead.labels.includes('gt:triage-request')) continue;
657+
651658
actions.push({
652659
type: 'clear_bead_assignee',
653660
bead_id: bead.bead_id,

cloudflare-gastown/src/trpc/router.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1071,6 +1071,19 @@ export const gastownRouter = router({
10711071
message: 'Admins cannot restart containers for towns they do not own',
10721072
});
10731073
}
1074+
if (ownership.type === 'org') {
1075+
const townStub = getTownDOStub(ctx.env, input.townId);
1076+
const config = await townStub.getTownConfig();
1077+
const membership = getOrgMembership(ctx.orgMemberships, ownership.orgId);
1078+
const isOrgOwner = membership?.role === 'owner';
1079+
const isTownCreator = ctx.userId === config.created_by_user_id;
1080+
if (!isOrgOwner && !isTownCreator) {
1081+
throw new TRPCError({
1082+
code: 'FORBIDDEN',
1083+
message: 'Only town creators and org owners can restart containers',
1084+
});
1085+
}
1086+
}
10741087
// stop() sends SIGTERM so the container's drain handler can run
10751088
// drainAll() — nudging agents to commit/push WIP before exiting.
10761089
const containerStub = getTownContainerStub(ctx.env, input.townId);
@@ -1087,6 +1100,19 @@ export const gastownRouter = router({
10871100
message: 'Admins cannot destroy containers for towns they do not own',
10881101
});
10891102
}
1103+
if (ownership.type === 'org') {
1104+
const townStub = getTownDOStub(ctx.env, input.townId);
1105+
const config = await townStub.getTownConfig();
1106+
const membership = getOrgMembership(ctx.orgMemberships, ownership.orgId);
1107+
const isOrgOwner = membership?.role === 'owner';
1108+
const isTownCreator = ctx.userId === config.created_by_user_id;
1109+
if (!isOrgOwner && !isTownCreator) {
1110+
throw new TRPCError({
1111+
code: 'FORBIDDEN',
1112+
message: 'Only town creators and org owners can destroy containers',
1113+
});
1114+
}
1115+
}
10901116
// destroy() sends SIGKILL — the container dies immediately with
10911117
// no graceful drain. Use when the container is stuck or unresponsive.
10921118
const containerStub = getTownContainerStub(ctx.env, input.townId);

0 commit comments

Comments
 (0)