fix(eve)!: make the Upstash Box sandbox backend actually usable - #6
Merged
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-isolationchangeset (no new changeset).
1. Config is Box's
BoxConfig, verbatim (breaking)UpstashBackendConfiginvented Vercel-shaped knobs (resources.vcpus, runtimestrings like
"node24", an Eve-shapednetworkPolicy) that didn't correspond toanything in Upstash Box.
Omit<BoxConfig, "networkPolicy"> & { redis?, templatePrefix? }— passruntime/size/apiKey/keepAlive/initCommand/env/skills/… exactly as you would to
Box.create({...}).resources.vcpus→ size mapping and runtime-string coercion.networkPolicyis 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 thetemplateKey → snapshotIdmapping in anin-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.
redisdefaults toRedis.fromEnv()). Box has no static snapshot lookup, so this is what bridgesprewarm → create across processes (the same role Vercel's named sandbox plays).
prewarmbuilds 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 sessionper turn — a new box eachtime.
createignoredexistingMetadata, anddisposecalledbox.pause(),which throws for keep-alive boxes.
createreattaches toexistingMetadata.boxId(Box.get) before falling backto the template snapshot / a fresh box. Eve hands back the id we record in
captureState, so the conversation keeps a single box.disposeis a no-op (matching Eve's Vercel backend) so the box survives for thenext open to reattach.
keepAlivenow defaults tofalse(Box's pause-based idle lifecycle), so theno-op dispose doesn't leak — idle boxes auto-pause and are reaped.
4.
/workspace→/workspace/homepath bridgeEve roots its tools at
/workspace(hardcoded in its glob/grep tools, e.g.find /workspace …), but a Box session lives in/workspace/homeand/workspaceitself is permission-denied — so file ops and
findhit the wrong directory.resolvePath(file ops) and in raw commands(
toBoxPath/rewriteWorkspacePaths).curl host/workspace/xis leftalone; only genuine
/workspacepath tokens are mapped./workspacehardcoded inside a file the modelwrites then executes isn't rewritten — inherent to Box making
/workspaceoff-limits, mitigated because the model sees real
/workspace/homepaths intool output.
Testing
opens via
existingMetadata, deny-by-default egress + per-session open, andcross-instance snapshot reuse via the Redis registry. Live tests delete boxes
explicitly now that
disposeis a no-op.toBoxPath,rewriteWorkspacePaths,incl. the URL-safety cases).
pnpm typecheck/lintclean.