Skip to content

Add Mirage sandbox connector#70

Draft
FredKSchott wants to merge 3 commits intomainfrom
add-mirage-connector
Draft

Add Mirage sandbox connector#70
FredKSchott wants to merge 3 commits intomainfrom
add-mirage-connector

Conversation

@FredKSchott
Copy link
Copy Markdown
Member

Summary

  • Adds connectors/sandbox--mirage.md, a sandbox connector for Mirage — Strukto AI's in-process unified virtual filesystem.
  • Connector code is runtime-agnostic (imports types from @struktoai/mirage-core); users install @struktoai/mirage-node on --target node or @struktoai/mirage-browser on --target cloudflare.
  • Follows the structure prescribed by connectors/README.md; node-vs-browser specifics are folded into the existing "What this connector does" and "Required dependencies" slots rather than introducing new top-level sections.

@stainlu
Copy link
Copy Markdown
Contributor

stainlu commented May 7, 2026

i think this connector is directionally very right, esp after the no-default-sandbox thread: Mirage is the Environment adapter, Flue still owns Agent/Session.

one thing i'd fix before merge: the connector currently drops Flue's session id and flattens everything onto Mirage's default session. createSessionEnv({ id }) is probably worth threading into Mirage's native sessionId, otherwise cwd/env/history can leak when a user reuses one Workspace across multiple Flue sessions.

related: i tested @struktoai/mirage-node@0.0.1 and the current env prefix doesn't seem to satisfy exec(..., { env }):

await ws.execute("FOO=bar printenv FOO"); // exit 1
await ws.execute("FOO=bar echo $FOO"); // stdout: "echo\n"

export FOO=bar works, but it persists into the Mirage session, so it's not really per-call env.

shape i'd lean toward:

  • MirageSandboxApi(workspace, flueSessionId)
  • create/get the Mirage session in createSessionEnv({ id })
  • pass { sessionId: id, signal } to workspace.execute(...)
  • for per-call cwd/env, snapshot the Mirage session's cwd/env, set merged values around the execute, and restore in finally - or use op-scoped temp Mirage sessions if we want parallel-safe per-call env

not trying to bikeshed the connector; this feels like the managed-agents boundary: Flue Session should map cleanly to the substrate session when the sandbox has one.

@ketankhairnar
Copy link
Copy Markdown
Contributor

@FredKSchott — read through this carefully because the connector pattern is something I've been studying. Mostly notes for the next reader; flagging a couple of small things you may or may not want to fold in.

Things I picked up from reading it back to back with the vercel connector:

  • readFileBuffer returns whatever Mirage hands back directly. The vercel adapter wraps in new Uint8Array(...) — small defensive copy in case Mirage ever hands back a view onto a pooled buffer. One-line change, no-op if Mirage already returns a fresh array. Probably worth it for symmetry with the rest of the connector corpus.

  • readdir's entries.map((p) => p.slice(p.lastIndexOf('/') + 1)).filter((n) => n.length > 0) works, but the trailing-slash filter is the only clue that Mirage may return paths with a trailing /. A one-line comment naming Mirage's actual return shape would save the next reader a docs spelunk. Something like // Mirage returns absolute paths; some impls trail with / is enough.

  • mtime: s.modified ? new Date(s.modified) : new Date() quietly synthesizes "now" when mtime is null. new Date(0) (Unix epoch) might be a less surprising sentinel — it can't be confused with a real recent modification by anything that compares mtimes (e.g. a future cache layer). Edge case, but cheap to fix now.

Polish-level things, take or leave:

  • aliases in the frontmatter — vercel ships "aliases": ["@vercel/sandbox"]. Adding ["mirage", "@struktoai/mirage-node", "@struktoai/mirage-browser"] here would let flue add mirage (and the package names) all hit this recipe.
  • The cwd-and-env shell wrap puts envPrefix after &&, so a bad cwd short-circuits before env is evaluated. That's the right behavior, but it reads as accidental — a two-word comment lands it.
  • The abort-detection in runShell recognizes AbortError and TimeoutError. If a future Mirage release surfaces a different err.name for the same condition, this collapses silently to "abort recognized." Worth aligning across vercel + mirage + future connectors rather than fixing only here, so probably not a this-PR thing.

Things that aren't issues but caught my eye:

  • shellQuote matches the canonical implementation in agent.ts:395 exactly (single-quote wrapped with '\''). Nicely safer than the older escapeShellArg it would have copied if the reference were stale. Good catch.
  • The "If you've seen @struktoai/mirage-agents, don't install it for Flue" line is the kind of detail that saves people hours.
  • The Node-only resource list (SSHResource, PostgresResource, MongoDBResource, EmailResource, FUSE) is exactly the constraint a future LLM doing the install needs surfaced — well done.

Overall this is a clean conformant connector and a great template for future ones — the multi-target story (mirage-core types + node-vs-browser runtime packages) is handled cleanly enough that I expect it to get copied for any future connector that has both runtimes.

FredKSchott added a commit that referenced this pull request May 7, 2026
Threads each Flue session id through Mirage's session manager so cwd,
env, history, and lastExitCode stay isolated when one Workspace is
shared across multiple Flue sessions (per @stainlu's review on #70).

Documents the remaining workarounds (env shell-prefix, cwd shell-prefix,
client-side timeout) as known limitations awaiting upstream fixes:

  - strukto-ai/mirage#4 (per-call cwd in ExecuteOptions)
  - strukto-ai/mirage#5 (per-call env in ExecuteOptions)
  - strukto-ai/mirage#6 (mid-flight AbortSignal observation)

Once those land, the workarounds become removable in a follow-up.
@FredKSchott
Copy link
Copy Markdown
Member Author

Thanks @stainlu — your review was right on, and digging into the Mirage source confirmed the issues run a little deeper than just session mapping.

Just pushed 657e487 which threads id through to workspace.createSession(id) and execute({ sessionId: id }), plus per-Flue-session cleanup via closeSession(id). That fixes the cross-session leakage you flagged.

The other three connector bugs I found (per-call env is silently wrong because Mirage's parser passes FOO=bar as argv rather than honoring it as command env; per-call cwd is shell-prefix-wrapped today; AbortSignal is only checked at entry so mid-flight timeouts don't fire) all need upstream fixes. Filed as:

Holding this PR until those three are resolved. The current code keeps the existing workarounds with TODO pointers; once Mirage ships the fixes, dropping them is mechanical. I don't want to land a connector where session.shell(cmd, { env }) silently does the wrong thing.

@FredKSchott FredKSchott marked this pull request as draft May 7, 2026 19:32
@FredKSchott FredKSchott changed the title Add Mirage sandbox connector [BLOCKED] Add Mirage sandbox connector May 7, 2026
@FredKSchott FredKSchott changed the title [BLOCKED] Add Mirage sandbox connector Add Mirage sandbox connector May 9, 2026
Threads each Flue session id through Mirage's session manager so cwd,
env, history, and lastExitCode stay isolated when one Workspace is
shared across multiple Flue sessions (per @stainlu's review on #70).

Documents the remaining workarounds (env shell-prefix, cwd shell-prefix,
client-side timeout) as known limitations awaiting upstream fixes:

  - strukto-ai/mirage#4 (per-call cwd in ExecuteOptions)
  - strukto-ai/mirage#5 (per-call env in ExecuteOptions)
  - strukto-ai/mirage#6 (mid-flight AbortSignal observation)

Once those land, the workarounds become removable in a follow-up.
Mirage 0.0.2 ships first-class support for per-call cwd, per-call env, and
mid-flight AbortSignal observation, resolving strukto-ai/mirage#4-#6. The
connector now passes those options straight through to Workspace.execute()
instead of wrapping with cd/env shell prefixes and entry-time signal
checks. Verified end-to-end: per-call cwd/env do not leak into the Mirage
session, and mid-flight aborts fire at signal time rather than after the
command completes.

Also folds in review polish from #70: defensive Uint8Array copy on
readFileBuffer, Unix-epoch sentinel for missing mtime, comment on
Mirage's readdir return shape, and aliases for the two runtime packages
so flue add @struktoai/mirage-{node,browser} both resolve here. Drops
the cleanup option to match the post-#85 connector contract.
@FredKSchott FredKSchott force-pushed the add-mirage-connector branch from 657e487 to d365a5e Compare May 9, 2026 04:00
@stainlu
Copy link
Copy Markdown
Contributor

stainlu commented May 9, 2026

nice, the latest shape looks much cleaner now that Mirage has first-class cwd / env / mid-flight signal.

one small edge case i think is worth checking before this lands: runShell() composes caller options.signal with AbortSignal.timeout(...), but then the catch path treats any AbortError / TimeoutError as a numeric timeout whenever options.timeout is set.

that means if Flue passes both, and the caller aborts first, the connector can return a 124-shaped timeout result instead of surfacing caller cancellation.

this can happen through the LLM-facing bash tool path: bash tool timeout gives exec(..., { timeout }), while prompt/task cancellation also passes signal down.

Flue core seems to intentionally keep those different:

  • numeric timeout -> normal ShellResult / exit 124 so the model can recover
  • caller signal -> abort/reject so the outer call is cancelled

maybe keep the timeout signal separate and only synthesize 124 when the timeout signal is the one that won. if caller signal won, rethrow the abort error.

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.

3 participants