Skip to content

Commit 8cbcf7d

Browse files
authored
fix(gastown): propagate container-push errors in manual token refresh (#1106)
ensureContainerToken silently swallows network errors when pushing the fresh JWT to the container (by design — the alarm path tolerates a downed container since the token is persisted in envVars for next boot). But forceRefreshContainerToken, the user-triggered path, was calling the same function, so a failed push resulted in a false 'success' toast while the container still held the old expired token — causing persistent 401s for the mayor and other agents. Add a dedicated forceRefreshContainerToken in container-dispatch that propagates ALL errors from the container push, so the UI shows a real failure when the container never received the fresh JWT. Closes #1101
1 parent 5de9b52 commit 8cbcf7d

2 files changed

Lines changed: 55 additions & 1 deletion

File tree

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,13 +454,17 @@ export class TownDO extends DurableObject<Env> {
454454
* Force-refresh the container token, bypassing the 1-hour throttle.
455455
* Called from the user-facing tRPC mutation so operators can manually
456456
* push a fresh JWT to the running container.
457+
*
458+
* Unlike the alarm-driven refreshContainerToken, this propagates ALL
459+
* errors (including container-down) so the UI can show a real failure
460+
* instead of a false success.
457461
*/
458462
async forceRefreshContainerToken(): Promise<void> {
459463
const townId = this.townId;
460464
if (!townId) throw new Error('townId not set');
461465
const townConfig = await this.getTownConfig();
462466
const userId = townConfig.owner_user_id ?? townId;
463-
await dispatch.refreshContainerToken(this.env, townId, userId);
467+
await dispatch.forceRefreshContainerToken(this.env, townId, userId);
464468
this.lastContainerTokenRefreshAt = Date.now();
465469
}
466470

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,56 @@ export async function ensureContainerToken(
127127
*/
128128
export const refreshContainerToken = ensureContainerToken;
129129

130+
/**
131+
* Force-refresh variant for manual user-triggered refreshes.
132+
*
133+
* Unlike ensureContainerToken (which tolerates a downed container
134+
* because the token is persisted in envVars for next boot), this
135+
* function throws on ANY failure to push the token to the running
136+
* container — including network errors. This ensures the UI reports
137+
* a real failure instead of a false success when the container
138+
* never actually received the fresh JWT.
139+
*/
140+
export async function forceRefreshContainerToken(
141+
env: Env,
142+
townId: string,
143+
userId: string
144+
): Promise<string> {
145+
const jwtSecret = await resolveJWTSecret(env);
146+
if (!jwtSecret) {
147+
throw new Error('No JWT secret available — cannot mint container token');
148+
}
149+
150+
const token = signContainerJWT({ townId, userId }, jwtSecret);
151+
const container = getTownContainerStub(env, townId);
152+
153+
// Store for next boot (best-effort — the critical step is the live push below)
154+
try {
155+
await container.setEnvVar('GASTOWN_CONTAINER_TOKEN', token);
156+
} catch (err) {
157+
console.warn(
158+
`${TOWN_LOG} forceRefreshContainerToken: setEnvVar failed:`,
159+
err instanceof Error ? err.message : err
160+
);
161+
}
162+
163+
// Push to running container — propagate ALL errors so the caller
164+
// (and ultimately the UI) knows the refresh didn't land.
165+
const resp = await container.fetch('http://container/refresh-token', {
166+
method: 'POST',
167+
headers: { 'Content-Type': 'application/json' },
168+
body: JSON.stringify({ token }),
169+
});
170+
if (!resp.ok) {
171+
const body = await resp.text().catch(() => '');
172+
throw new Error(
173+
`Container rejected token refresh (HTTP ${resp.status})${body ? `: ${body.slice(0, 200)}` : ''}`
174+
);
175+
}
176+
177+
return token;
178+
}
179+
130180
/** Build the initial prompt for an agent from its bead. */
131181
export function buildPrompt(params: {
132182
beadTitle: string;

0 commit comments

Comments
 (0)