Skip to content

fix(eve)!: make the Upstash Box sandbox backend actually usable - #6

Merged
CahidArda merged 3 commits into
mainfrom
eve-sandbox-boxconfig
Jun 24, 2026
Merged

fix(eve)!: make the Upstash Box sandbox backend actually usable#6
CahidArda merged 3 commits into
mainfrom
eve-sandbox-boxconfig

Conversation

@CahidArda

Copy link
Copy Markdown
Collaborator

Summary

Three fixes to the eve upstash() sandbox backend (@upstash/agentkit-eve/sandbox),
prompted by running a real agent against Upstash Box ("write a node script that
prints hello world"). The backend type-checked but misbehaved at runtime:
config knobs that didn't map to Box, a stale snapshot that was never reused, the
wrong working directory, and a new box created on every session open.

Scoped to the eve sandbox; folded into the pending harden-tenant-isolation
changeset (no new changeset).

1. Config is Box's BoxConfig, verbatim (breaking)

UpstashBackendConfig invented Vercel-shaped knobs (resources.vcpus, runtime
strings like "node24", an Eve-shaped networkPolicy) that didn't correspond to
anything in Upstash Box.

  • Now Omit<BoxConfig, "networkPolicy"> & { redis?, templatePrefix? } — pass
    runtime / size / apiKey / keepAlive / initCommand / env / skills /
    … exactly as you would to Box.create({...}).
  • Removed resources.vcpus → size mapping and runtime-string coercion.
  • networkPolicy is no longer a config knob: egress is deny-all by default
    (enforced at creation) and opened only per-session via use({ networkPolicy }).

2. Prewarmed template snapshots are actually reused

prewarm (build/startup) recorded the templateKey → snapshotId mapping in an
in-memory map, which create (per request, a different process) couldn't see —
so it always built a fresh, empty box and the seed files never reached the agent.

  • The mapping now lives in a durable Redis registry (redis defaults to
    Redis.fromEnv()). Box has no static snapshot lookup, so this is what bridges
    prewarm → create across processes (the same role Vercel's named sandbox plays).
  • prewarm builds no box at all when there's nothing to bake.

3. One box per conversation, not one per session-open

The agent's logs showed three opening sandbox session per turn — a new box each
time. create ignored existingMetadata, and dispose called box.pause(),
which throws for keep-alive boxes.

  • create reattaches to existingMetadata.boxId (Box.get) before falling back
    to the template snapshot / a fresh box. Eve hands back the id we record in
    captureState, so the conversation keeps a single box.
  • dispose is a no-op (matching Eve's Vercel backend) so the box survives for the
    next open to reattach.
  • keepAlive now defaults to false (Box's pause-based idle lifecycle), so the
    no-op dispose doesn't leak — idle boxes auto-pause and are reaped.

4. /workspace/workspace/home path bridge

Eve roots its tools at /workspace (hardcoded in its glob/grep tools, e.g.
find /workspace …), but a Box session lives in /workspace/home and /workspace
itself is permission-denied — so file ops and find hit the wrong directory.

  • The backend bridges the two in resolvePath (file ops) and in raw commands
    (toBoxPath / rewriteWorkspacePaths).
  • The command rewrite is URL-safe (lookbehind): curl host/workspace/x is left
    alone; only genuine /workspace path tokens are mapped.
  • Known limitation (documented): /workspace hardcoded inside a file the model
    writes then executes isn't rewritten — inherent to Box making /workspace
    off-limits, mitigated because the model sees real /workspace/home paths in
    tool output.

Testing

  • Live Upstash Box (+ Redis) tests: create/run/file round-trip, box reuse across
    opens via existingMetadata
    , deny-by-default egress + per-session open, and
    cross-instance snapshot reuse via the Redis registry. Live tests delete boxes
    explicitly now that dispose is a no-op.
  • Offline unit tests for the path bridge (toBoxPath, rewriteWorkspacePaths,
    incl. the URL-safety cases).
  • pnpm typecheck / lint clean.

…Policy knobs)

The `upstash()` sandbox backend invented Vercel-shaped config (`resources.vcpus`,
runtime strings like "node24", an Eve-shaped `networkPolicy`) that didn't match
Upstash Box. Take the real `@upstash/box` `BoxConfig` verbatim instead.

- `UpstashBackendConfig = Omit<BoxConfig, "networkPolicy">` — pass `runtime`,
  `size`, `apiKey`, `keepAlive`, `initCommand`, `env`, `skills`, `mcpServers`,
  `timeout`, … exactly as you would to `Box.create({...})`. Removed the
  `resources.vcpus`→size mapping and the runtime-string coercion (use Box's
  `Runtime`/`BoxSize` directly).
- `networkPolicy` is no longer a config knob: egress is enforced deny-all
  atomically at creation (folded into `boxConfig()`, dropping the extra
  post-create `updateNetworkPolicy` round-trips) and opened only per-session via
  Eve's `use({ networkPolicy })`. Reworked the live egress test to open via the
  session `use()` flow.
- Updated the eve README, eve-demo sandbox, and docstring examples; folded into
  the pending harden-tenant-isolation changeset.
…path

Two sandbox bugs surfaced running an agent against Upstash Box.

1. Two boxes were created and the first (prewarmed) one was unused — its seed
   files never reached the session. `prewarm` (build/startup) recorded the
   template snapshot only in an in-memory map, which `create` (a different
   process, per request) can't see; Box has no static snapshot lookup, so create
   always fell back to a fresh, empty `Box.create`. Store `templateKey → snapshotId`
   in a durable Redis registry (`agentkit:sandbox:template:<name>:<templateKey>`,
   `redis` defaults to `Redis.fromEnv()`) so create restores the prewarmed
   snapshot. `prewarm` also no longer builds a throwaway box when there's nothing
   to bake (no seed files / bootstrap), and stale-snapshot restores fall back to
   a fresh box.

2. The agent ran `find /workspace …` but Box sessions live in `/workspace/home`
   (`/workspace` is off-limits). Eve hardcodes `/workspace` as its tool root, so
   the backend now bridges it to `/workspace/home` in both `resolvePath` (file
   ops) and raw commands (exported `toBoxPath` / `rewriteWorkspacePaths`).

Config gains optional `redis`/`templatePrefix` (stripped before `Box.create`).
Added offline path-bridge tests and a live Box+Redis test proving a second
backend instance reuses the prewarmed snapshot. Updated README, docstrings,
CLAUDE.md, and the changeset.
…path rewrite

Running an agent against Box created a new box on every session open (the logs
showed three "opening sandbox session" per turn).

- `create` now reattaches to the box from `input.existingMetadata.boxId`
  (`Box.get`) before falling back to the template snapshot or a fresh box. Eve
  re-opens a session many times per turn and hands back the box id we record in
  `captureState`, so without this every open spun a new box.
- `dispose` is now a no-op (matching Eve's Vercel backend): the box must survive
  for the next open to reattach. The old `dispose` called `box.pause()`, which
  THROWS for keep-alive boxes ("Keep-alive boxes cannot be paused"), so it both
  failed and defeated reuse.
- `keepAlive` now defaults to `false` (Box's pause-based idle lifecycle): idle
  boxes auto-pause and are reaped, so a no-op dispose doesn't leak. `true` opts
  into an always-running box the caller manages.
- `rewriteWorkspacePaths` is now URL-safe: a lookbehind stops it rewriting
  `/workspace` inside URLs/relative paths (e.g. `curl host/workspace/x`), while
  still mapping genuine `/workspace` path tokens to `/workspace/home`.

Added live tests for box reuse across opens and for the URL-safe rewrite; live
tests now delete boxes explicitly since dispose no longer does. Updated README,
docstrings, CLAUDE.md, and the changeset.
@CahidArda
CahidArda merged commit 4e303e6 into main Jun 24, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant