fix(cloud): attachments reach cloud workspaces, scoped to the box they belong to

Merged
SA
1mo ago
+13453-57

Summary

  • Attaching images to a cloud workspace (desktop new-workspace modal, mobile composer, mobile running session) never reached the box on the deployed API. Three breaks, fixed together.
  • A box may now fetch exactly the files attached to its own workspace, and nothing wider.
  • Renames the attachment parent kind cloud_workspace_prompt → cloud_workspace (migration 0132, RENAME VALUE).

Why / Context

Reproduced on prod with the calls the clients make: a cloud create with an image in the prompt came up with Claude saying "I don't see an image attached". Inside the box, host-service had SUPERSET_SANDBOX_AGENT and _PROMPT but no _ATTACHMENTS. Mobile's running-session path (attachments.materializeIntoWorkspace on the box) failed with Failed to mint JWT: 401. Desktop's running-session paste/drop (filesystem.writeFile) worked and is unchanged.

  1. Create path. /api/cloud-workspaces/provision re-validated launch with a schema lacking attachmentFileIds; zod stripped it. A local API calls the provisioner directly, skipping QStash, which is why #7622 looked verified.
  2. Box → API auth. host-service in a box ran JwtApiAuthProvider with AUTH_TOKEN=sandbox; /api/auth/token answers 401, so every call the box makes to the API failed, attachment.resolve included.
  3. Permission. Even with the firewall's workspace credential, attachment.resolve wasn't on SANDBOX_ALLOWED_PROCEDURES.
  4. Boot script. superset-boot forwards an allowlist of identity variables to host-service and SUPERSET_SANDBOX_AGENT_ATTACHMENTS wasn't on it: the ids reached /etc/superset/sandbox.conf and stopped. Found by booting a real box on this branch.

How It Works

  • cloudAgentLaunchSchema in @superset/shared/cloud-agent-launch is the one definition of a launch; the job route validates with it and CloudAgentLaunch is inferred from it.
  • SandboxApiAuthProvider (host-service, sandbox mode) sends no credential; the firewall adds x-superset-sandbox-credential. The relay keeps the JWT provider.
  • attachment.resolve for a box caller adds one condition: the file is attached to cloud_workspace / the caller's workspace id. Access to the box = access to its files.
  • Create already attaches the prompt's uploads to the workspace (#7932). For running sessions, cloudWorkspace.attachFiles (a person's call, checked with loadVisibleWorkspace) attaches them first; mobile's terminal composer calls it for cloud workspaces before asking the box to pull.

Manual QA

Local API + local DB (workspace Neon branch) + minio, with a box credential signed by the local gate secret:

  • Person attachFiles → row cloud_workspace on the workspace
  • Box resolves its own file and downloads the bytes
  • Box resolving an unattached file → NOT_FOUND
  • Box resolving another box's file → NOT_FOUND
  • Box calling attachFiles → FORBIDDEN (not allowlisted)
  • Person resolve unchanged
  • No credential / forged credential → UNAUTHORIZED

Real box, this branch end to end: local API behind a cloudflared tunnel as NEXT_PUBLIC_API_URL (so the firewall adds the workspace credential for it), minio behind a second tunnel, a bundle built from this branch (host-service asset rebuilt from source + the boot fix) on a dev environment, cloudWorkspace.create with an image, then the job POSTed to /api/cloud-workspaces/provision (the dev QStash token belongs to no region, and the route skips the signature in development):

  • Create path: the id reaches host-service's env, the box imports the image through attachment.resolve with its own credential (sha256 matches the upload), and the agent is launched with the "Attached files" block pointing at it
  • Running-session path: person attachFiles, then box attachments.materializeIntoWorkspace writes .superset/attachments/mobile.png (sha256 matches)
  • Box materializeIntoWorkspace for a file not attached to it → "Attachment not found"
  • Claude reading the image back: the dev image has no agent CLIs (claude not found in PATH), so the last hop is the prompt, not the reply. Prod check owed after release.

Testing

  • bun turbo run typecheck for shared, trpc, host-service, api, mobile, db, desktop
  • biome check on changed files
  • bun test packages/shared/src/cloud-agent-launch.test.ts (new: the job schema and env round-trip keep attachment ids)
  • Full bun test in trpc/host-service: 7 failures in lib/sandbox/credentials.test.ts (passes alone; untouched) and 6 timing-based git watcher tests in host-service; not caused by this diff, not confirmed against main

Migration (0132)

drizzle-kit generated a text cast + DROP TYPE / CREATE TYPE + cast back: it rewrites attachments twice and fails on prod's existing cloud_workspace_prompt row. The SQL is replaced with ALTER TYPE ... RENAME VALUE, a catalog-only change with the same end state as the generated snapshot (enum order unchanged). Applied on the workspace branch; a follow-up drizzle-kit generate reports no drift. Not rehearsed under load: it touches no table.

Risks / Rollout

  • Ordering: the API now writes cloud_workspace, so the migration must apply before the new API serves traffic.
  • Box side needs a sandbox release. Boxes run the host-service tarball pinned in packages/sandbox/bundle/assets.json (currently 1.29.0+bfc1ec72a), not host-service from main, and environments pin bundleSha. Merging changes nothing in boxes: the host-service asset must be rebuilt (bun run assets host-service) and a bundle released to environments for the auth fix and the superset-boot fix to reach them. Until then boxes still cannot pull attachments (as today).
  • Allowlist widening: attachment.resolve for boxes is limited to files attached to the calling workspace.
  • Rollback: revert the code; the enum rename can stay (or RENAME VALUE back).

Summary by cubic

Fixes cloud workspace attachments never reaching the box on the deployed API — files now arrive from desktop create, mobile composer, and running sessions, and a box can fetch only files attached to its own workspace, nothing wider.

Bug fixes

  • The provision job re-validated launch with a schema missing attachmentFileIds, so zod stripped them; the shared cloudAgentLaunchSchema in @superset/shared/cloud-agent-launch is now the single definition.
  • host-service in a box got 401 from /api/auth/token, failing every box→API call; sandbox mode now sends no credential and the firewall's workspace credential identifies it.
  • attachment.resolve is on the box allowlist, scoped to files attached to the caller's workspace.
  • Mobile composer attaches uploads via cloudWorkspace.attachFiles before the box pulls them.
  • anchorAttachments now marks a file ready and inserts its attachment in one transaction, so a failed insert no longer leaves a resolved-but-orphaned file.
  • superset-boot now passes SUPERSET_SANDBOX_AGENT_ATTACHMENTS to host-service; previously the ids stopped at sandbox.conf and the first-boot launch never imported them.

Migration

  • Renames the attachment parent kind cloud_workspace_prompt → cloud_workspace (migration 0132, via ALTER TYPE ... RENAME VALUE, replacing drizzle-kit's table-rewriting text cast); apply before the new API serves traffic.
  • The box-side change ships in a sandbox bundle; pinned bundleSha environments keep current behavior until they carry it.

Summary by CodeRabbit

  • New Features
    • Attach files to cloud workspaces and use them in the workspace terminal. Include up to 10 attachments when launching a cloud agent.
    • Cloud workspace attachments are available through the workspace attachment flow.
  • Bug Fixes
    • Improved attachment handling so files are associated with the correct cloud workspace and remain available.
    • Cloud agent launch details now receive consistent validation, including checks for supported agents, prompt length, and valid attachment IDs.