Skip to content

Commit 44e8166

Browse files
raphaeltmclaude
andcommitted
fix: set workspace-scoped runtime callback token on wake so replies persist
The message reporter and snapshot callbacks use the workspace-scoped runtime.CallbackToken (workspaceCallbackToken), which has NO node-scoped fallback. A freshly-woken container never runs create-workspace, so that token was unset — the reporter hit "no auth token" and chat replies were silently discarded (restored sessions accepted a prompt but never answered). The DO now signs a workspace-scoped callback token and passes it on the restore request; the vm-agent persists it via upsertWorkspaceRuntime before resolving the snapshot callback token. Combined with the node-scoped CALLBACK_TOKEN, this matches the normal launch (node token for node callbacks + workspace token for message/snapshot callbacks). Diagnosed from staging Worker logs: workspace_auth.rejected_node_scoped_token on /session-snapshot/restore and reporter "no auth token". Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 85f0c79 commit 44e8166

2 files changed

Lines changed: 20 additions & 4 deletions

File tree

apps/api/src/durable-objects/vm-agent-container.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { drizzle } from 'drizzle-orm/d1';
55
import * as schema from '../db/schema';
66
import type { Env } from '../env';
77
import { log } from '../lib/logger';
8-
import { signNodeCallbackToken, signNodeManagementToken } from '../services/jwt';
8+
import { signCallbackToken, signNodeCallbackToken, signNodeManagementToken } from '../services/jwt';
99

1010
export const DEFAULT_CF_CONTAINER_SLEEP_AFTER = '1h';
1111
export const DEFAULT_CF_CONTAINER_ACTIVE_WORK_MAX_MS = 2 * 60 * 60 * 1000;
@@ -354,6 +354,12 @@ export class VmAgentContainer extends Container<Env> {
354354
const callbackToken = await signNodeCallbackToken(config.nodeId, this.env);
355355
await this.launch(config, { nodeCallbackToken: callbackToken });
356356

357+
// The fresh container never ran create-workspace, so its workspace-scoped
358+
// runtime.CallbackToken is unset. The message reporter and snapshot
359+
// callbacks require it (they do NOT fall back to the node-scoped token), so
360+
// pass it on the restore request; without it, restored sessions accept a
361+
// prompt but silently discard the agent's reply ("no auth token").
362+
const workspaceCallbackToken = await signCallbackToken(config.workspaceId, this.env);
357363
const { token } = await signNodeManagementToken(workspace.userId, config.nodeId, config.workspaceId, this.env);
358364
const restoreUrl = new URL(`http://localhost:${config.vmAgentPort}/workspaces/${config.workspaceId}/agent-sessions/${agentSession.id}/restore`);
359365
const restoreResponse = await this.containerFetch(
@@ -369,6 +375,7 @@ export class VmAgentContainer extends Container<Env> {
369375
chatSessionId: workspace.chatSessionId,
370376
runtime: 'cf-container',
371377
agentType: agentSession.agentType,
378+
workspaceCallbackToken,
372379
}),
373380
}),
374381
config.vmAgentPort

packages/vm-agent/internal/server/session_snapshot.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,10 @@ func (s *Server) sessionSnapshotHandlerInput(w http.ResponseWriter, r *http.Requ
120120
return nil, false
121121
}
122122
var body struct {
123-
ChatSessionID string `json:"chatSessionId"`
124-
Runtime string `json:"runtime"`
125-
AgentType string `json:"agentType"`
123+
ChatSessionID string `json:"chatSessionId"`
124+
Runtime string `json:"runtime"`
125+
AgentType string `json:"agentType"`
126+
WorkspaceCallbackToken string `json:"workspaceCallbackToken"`
126127
}
127128
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
128129
writeError(w, http.StatusBadRequest, "invalid request body")
@@ -133,6 +134,14 @@ func (s *Server) sessionSnapshotHandlerInput(w http.ResponseWriter, r *http.Requ
133134
writeError(w, http.StatusBadRequest, "chatSessionId is required")
134135
return nil, false
135136
}
137+
// A freshly-woken container never ran create-workspace, so its
138+
// runtime.CallbackToken (the workspace-scoped token used by the message
139+
// reporter and the snapshot callbacks) is unset. Persist the token the
140+
// control plane provides on the restore request so chat replies and
141+
// snapshot callbacks can authenticate after a wake.
142+
if wsToken := strings.TrimSpace(body.WorkspaceCallbackToken); wsToken != "" {
143+
s.upsertWorkspaceRuntime(workspaceID, "", "", "", wsToken)
144+
}
136145
runtime, ok := s.getWorkspaceRuntime(workspaceID)
137146
if !ok {
138147
writeError(w, http.StatusNotFound, "workspace not found")

0 commit comments

Comments
 (0)