[Fix] Modal snapshot resume auth and MCP job-token principal checks - #80
Conversation
The static Image.fromId resolves auth from the default profile/env vars, which are absent when Modal credentials come from the encrypted deployment env vars (the setup-flow storage) instead of process env — fresh spawns worked but every snapshot resume failed with 'Profile is missing token_id or token_secret'. Route the load through this.sdk.images.fromId so the explicit tokenId/tokenSecret travel with it.
Job tokens are minted once with the run's acting user at MINT time, but
task_runs.actingUserId is deliberately mutable mid-run: web steer and
follow-up delivery switch it to the latest human. The MCP/RPC handlers
required strict equality (run.actingUserId ?? null) === token.userId, so
the moment anyone steered or followed up — including a human replying in
an automation run's thread — every subsequent MCP call from that worker
403'd ("MCP token principal does not match cloud job"). Observed on
nightly: the worker could not post its Slack closeout.
The token's cloudJobId binding IS the authorization: only that run's
sandbox holds the run-scoped token, handlers verify the token targets
its own run, and Slack/integration calls go out via deployment-scoped
credentials (bot token / user_id IS NULL connections), so the exact-user
equality added no security and was wrong against a mutable column.
Removed the equality (run lookup + 404 stays) at: the five Slack MCP
endpoints, the shared proxy validator (verifyCloudJobTokenMatchesJob ->
verifyCloudJobTokenJobExists, assertCloudJobTokenMatchesCloudJobUser ->
assertCloudJobTokenJobExists, callers roomote.ts/manageSourceControl),
the four integration MCP handlers (Vercel/Grafana/Snowflake/Asana), and
the SDK's findCloudJobByJobTokenClaims (gating the artifacts route and
the tRPC jobScoped middleware — the same divergence broke those too).
Deliberately left: capability gates by principal KIND (human vs
deployment service principal, e.g. resolveActingUserId's 403), live
actor resolution that READS actingUserId for attribution/credentials,
the worker sandbox-server's token-cloudJobId-vs-sandbox check, and the
tRPC middlewares' token.cloudJobId === target id checks — those are the
run binding itself.
Tests now assert the divergence works: a token minted for user A keeps
working after actingUserId switches to user B, and a deployment-
principal (null) token keeps working after a human becomes the acting
user — including the Slack channel-post closeout path. auth.md updated:
token userId is mint-time attribution, never an authorization key.
|
No new code issues found. See task |
Review (Opus)Reviewed both commits against Commit 1 — Modal snapshot resume (
|
channel_post no longer reads actingUserId (thread_reply still does, for the attribution subject, so its select stays). The user-api-keys and resolve-actor-scoped-user comments now describe run-scoped token authorization with mint-time attribution instead of the old owner-equality rule.
Two production bugs found while testing the new data model on the nightly deployment (roomote.roomote.ai).
Modal snapshot resume fails with "Profile is missing token_id or token_secret"
resumeFromSnapshotloaded snapshot images through the Modal SDK's staticImage.fromId, which resolves auth from the default profile/process env — not from the client instance we construct with explicit credentials. When Modal credentials come from the encrypted deployment env vars (the setup-flow storage) instead of process env, fresh spawns worked but every snapshot resume failed after 3 retries. Fixed by routing the load throughthis.sdk.images.fromIdso the explicittokenId/tokenSecrettravel with it.Slack MCP calls 403 after a steer or follow-up ("MCP token principal does not match cloud job")
Job tokens snapshot the run's acting user at mint time, but
task_runs.actingUserIdis deliberately mutable: web steer and follow-up delivery both reassign it to the latest human. The MCP handlers (andfindCloudJobByJobTokenClaims, which gates the worker's entire sandbox→API RPC surface) required strict equality between the two — so the moment anyone steered or replied in-thread (including a human replying in an automation run's thread), every subsequent MCP/RPC call from that worker 403'd. Observed as a worker unable to post its Slack closeout.The run-scoped
cloudJobIdbinding is the actual authorization (only that sandbox holds the token; Slack posts go out via the deployment bot token, so there is no impersonation vector). This PR removes the exact-user equality everywhere the token is already run-bound — the 5 sites inmcp/slack.ts, the sharedproxy-utilsvalidator (renamed toassertCloudJobTokenJobExiststo match what it now does), the inline copies in the vercel/grafana/snowflake/asana integrations (all resolve credentials from deployment-scoped connections; token userId played no role), and the SDK-sidefindCloudJobByJobTokenClaims. Capability gates by principal kind (human vs deployment) and all live-actingUserIdattribution reads are unchanged.Tests cover both switch scenarios (user→user and null-principal→human) across the integration auth suites and the Slack channel-post path.
Both commits were validated with the full
pnpm checkgate (lint, types, all tests, knip).