A reproducible Apptainer/Podman container that runs a Marimo reactive-notebook server with a Python environment built by pixi, and bundles four AI coding-agent CLIs plus their ACP adapters:
| CLI | Agent | Install source | ACP |
|---|---|---|---|
claude |
Claude Code | @anthropic-ai/claude-code (npm) |
adapter: @zed-industries/claude-code-acp |
codex |
OpenAI Codex | @openai/codex (npm) |
adapter: @zed-industries/codex-acp |
gemini |
Gemini CLI | @google/gemini-cli (npm) |
native (gemini --acp) |
agy |
Antigravity | native Go binary from antigravity.google |
not yet |
The container is a read-only sandbox: the host filesystem is mounted
read-only so agents and notebooks can read code and data but cannot mutate the
host. Exactly one writable directory (./work → /work) holds Marimo notebooks
and any files the agents create.
pixi.toml / pixi.lock Agent-CLI-runtime env baked into the image (nodejs, uv, git, ...)
container/common.sh shared bash lib: WORK/PORT/RO_PATHS/ALLOW_HOSTS, binds, GPU detection, bsub setup
container/entrypoint.sh Seeds + installs the pixi env under /work, then serves Marimo
container/apptainer/marimo.def Apptainer build recipe (installs everything at build time)
container/apptainer/{build,marimo,shell}.sh Apptainer build / serve / interactive-shell scripts
container/apptainer/lib.sh Apptainer-only: wires the shared network allowlist below
container/podman/Containerfile Podman/Docker build recipe
container/podman/{build,marimo,shell}.sh Podman build / serve / interactive-shell scripts
container/podman/lib.sh Podman-only hardening: storage isolation/staleness recovery,
catatonit watchdog, plus wiring the shared network allowlist
container/allowlist_proxy.py / relay.py the network allowlist mechanism itself, shared by both
backends (see "Sandbox strength" below)
container/bsub-wrapper/bin/bsub opt-in bsub wrapper -- see "Submitting LSF jobs" below
container/app/AGENTS.md seeded into /work; CLAUDE.md/GEMINI.md symlink to it
container/app/agents_demo.py starter Marimo notebook that calls an agent via subprocess
container/caddy-lib.sh shared Caddy/TLS-cert helpers used by https-wrap.sh and
terminal-wrap.sh
container/https-wrap.sh fronts Marimo with Caddy TLS -- see "HTTPS (optional)" below
container/terminal-wrap.sh fronts a web terminal (ttyd) with Caddy TLS, an alternative
to Marimo -- see "Web terminal" below
runnables.yaml Fileglancer app manifest: "Marimo AI Sandbox" (marimo-https,
marimo-podman-https -- HTTPS-only, see "HTTPS (optional)"
below)
terminal/runnables.yaml Separate Fileglancer app manifest: "Web Terminal AI Sandbox"
(terminal-https, terminal-podman-https -- see "Web terminal"
below for why this is a second manifest, not part of the one
above)
work/ runtime writable dir (created on first run; git-ignored)
(start.sh/run_podman.sh/shell_podman.sh/build_podman.sh are the old
names from before the container/{apptainer,podman}/ reorganization --
scripts below are invoked as pixi run {build,marimo,shell}-{apptainer,podman},
or directly via their paths above.)
Building from source is optional. pixi run marimo-apptainer and
pixi run marimo-podman both default to pulling the pre-built image published
by .github/workflows/publish-image.yml at
ghcr.io/janeliascicomp/marimo_ai_sandbox:latest (Apptainer converts it to a
local .sif on first use) instead of building locally, falling back to a
local build only if the pull fails and no local image/.sif exists yet. Use
the steps below if you want to build from source anyway -- e.g. to test an
unpublished change, or on a host without egress to ghcr.io.
Requires network access and unprivileged build support (--fakeroot). On
Janelia HPC, run on a node where apptainer build --fakeroot is permitted, or
build elsewhere and copy the .sif over.
pixi install # generates / refreshes pixi.lock (already committed)
pixi run build-apptainer # -> marimo_sandbox.sif/etc/subuid//etc/subgid range for your account, and that's not
something you can set up yourself — reach out to the HPC team first if
you haven't run rootless Podman on this cluster before. Without it, Podman
can't create its user namespace at all, and you'll never get as far as the
storage setup this repo's scripts do automatically. This has to be
requested per-account, individually, by HPC -- it isn't granted by
default.
A range-enabled account isn't required for anything in this repo to work
-- everything described below (ignore_chown_errors=true,
TAR_OPTIONS=--no-same-owner, podman unshare rm -rf instead of a plain
rm -rf) is a fallback for the no-range case, expected to remain harmless
for an account that has a range too (not yet verified live either way
for that combination).
If you do have a range, opt in to running the Podman backend's container as
your real host uid/gid instead of root-mapped-through-the-user-namespace
with --keep-id/KEEP_ID=1 (--userns=keep-id --user "$(id -u):$(id -g)"
under the hood; see the "Identity" bullet below):
pixi run shell-podman --keep-id # id inside now matches your real uid/gid
KEEP_ID=1 pixi run marimo-podmanVerified live: id inside the container reports your real uid/gid instead
of root, and files written to /work come out owned by you on the host,
same as without it -- the only thing that changes is what the container
itself sees as its own identity. Off by default (unchanged root-inside
behavior) since it needs a range most accounts don't have.
For Podman support:
pixi install
pixi run build-podman # -> builds marimo_sandbox:latest# Provide whichever API keys you need (forwarded automatically):
export ANTHROPIC_API_KEY=sk-...
export OPENAI_API_KEY=sk-...
export GEMINI_API_KEY=... # or GOOGLE_API_KEY
pixi run marimo-apptainer # serves http://<host>:8080 (Apptainer)
# or
pixi run marimo-podman # serves http://<host>:8080 (Podman)Open the printed URL (with the access token) in a browser. The notebook
app/agents_demo.py is copied into ./work on first run.
Marimo has no built-in TLS support, so pixi run marimo-https fronts the
same launch flow with a local Caddy reverse
proxy:
pixi run marimo-https # serves https://<host>:8443 -> internal :8080
BACKEND=podman pixi run marimo-https # force Podman, even if Apptainer is also on PATHcontainer/https-wrap.sh picks the same backend pixi run marimo would by
default (Apptainer if it's on PATH, else Podman) -- set BACKEND=podman
(or BACKEND=apptainer) to override that, e.g. to get Podman+HTTPS on a
host that also has Apptainer installed. runnables.yaml's
marimo-podman-https runnable uses this to force Podman, since it's the
backend preferred by HPC admins (see "GPU passthrough" and "Podman storage
isolation" above) but the plain marimo-https runnable would otherwise
always prefer Apptainer when present.
runnables.yaml only exposes HTTPS runnables (marimo-https,
marimo-podman-https, and the web-terminal ones below) -- the plain-HTTP
pixi run marimo-apptainer/marimo-podman commands above still work
directly, just aren't offered as Fileglancer jobs, since an HTTP job would
carry Marimo's access token in the URL unencrypted.
Caddy terminates TLS using a self-signed certificate that the wrapper script
generates itself (via openssl) and hands to Caddy as a static cert file,
rather than Caddy's own internal-CA issuer — that issuer's first run tries to
install its CA root into the OS trust store via sudo, which hangs/fails on
a host with no interactive sudo session (e.g. a compute node). The cert is
stored in the work directory (https-cert/marimo-https.crt) and reused
across restarts instead of being regenerated (it's only regenerated if the
hostname changes, e.g. a new compute-node allocation). On startup the script
prints the cert's path; install it in your browser's trust store to avoid
the untrusted-certificate warning (Chrome: Settings → Privacy and security →
Security → Manage certificates → Authorities → Import; Firefox: Settings →
Privacy & Security → Certificates → View Certificates → Authorities →
Import). This is entirely self-contained — it doesn't depend on Fileglancer
to obtain a cert.
pixi run terminal-https serves a web-based terminal instead of Marimo --
same sandbox (read-only host, writable /work, GPU passthrough), same
Caddy TLS-terminating setup as marimo-https above, just fronting
ttyd (a conda-forge package, baked into
the image via pixi.toml) instead:
pixi run terminal-https # serves https://<host>:<port>
BACKEND=podman pixi run terminal-https # force PodmanFileglancer app: the web terminal's runnables live in their own
manifest, terminal/runnables.yaml, rather than the root runnables.yaml
-- Fileglancer discovers every runnables.yaml in a repo and offers each
as an independently addable app, so "Marimo AI Sandbox" and "Web Terminal
AI Sandbox" show up as two separate app cards instead of one, even though
both live in this same repo. Fileglancer always runs a job's commands from
the repo root, regardless of which subdirectory the manifest itself
lives in -- confirmed live (a job actually failed with No such file or directory when terminal/runnables.yaml's command: field used
../container/terminal-wrap.sh, assuming it would run from terminal/).
So terminal/runnables.yaml's command: fields use the same
container/terminal-wrap.sh path as the root manifest's https-wrap.sh
calls, not a ../-relative one.
Useful for driving the agent CLIs (claude, codex, gemini, agy) or a
plain shell from a browser, with no separate SSH/terminal client needed --
e.g. from a Fileglancer job with no other terminal access. Under the hood,
container/terminal-wrap.sh runs ttyd inside the sandbox via
shell.sh's command-override support (./shell.sh -- ttyd ..., added
alongside this feature) rather than a separate code path, so it gets the
exact same --ro-paths/--work/GPU handling an interactive shell.sh
session does. shell.sh now pulls the pre-built image from ghcr.io
first, same as marimo.sh always has, falling back to a local build only
if the pull fails -- it used to always build locally from scratch, so the
first-ever terminal-https/shell-* invocation on a given node paid a
multi-minute cold build. Confirmed live: this was the actual root cause of
a real Fileglancer terminal-https job showing a confusing 502 for
several minutes (Caddy only waits 30s for ttyd before starting anyway --
see the --allow 502 note below for the identical Caddy-vs-backend-
timing shape).
Auth is ttyd's own HTTP Basic Auth (-c user:pass), not a query-string
token like Marimo's -- the launch URL embeds the credential directly
(https://terminal:<token>@host:port/), which browsers use to
auto-authenticate the same way. The token is resolved the same way
Marimo's is ($FG_SERVICE_TOKEN if this is a Fileglancer job, else a
random token persisted at $WORK/.terminal-token), kept in its own file so
the two services don't share a credential even against the same --work.
Not compatible with --allow/$ALLOW_HOSTS (see "Sandbox strength"
below) -- same reason marimo-https isn't: the egress allowlist isolates
the container's network namespace, including its own loopback, so Caddy
(running outside the container, on the host) can no longer reach it.
terminal-wrap.sh hard-errors immediately if it's set, same as
https-wrap.sh. Use pixi run shell-apptainer/shell-podman -- ttyd ...
directly with --allow instead if you need both at once (no HTTPS
fronting in that case, so it isn't a Fileglancer-servable job).
common.sh detects a GPU at launch time (nvidia-smi -L) and passes it
through automatically — no flag needed, and no harm on a non-GPU node:
- Apptainer: adds
--nv. - Podman: adds
--device nvidia.com/gpu=all(CDI-based, matching this host'snvidia-container-toolkitsetup).
Verified end-to-end on an LSF gpu_short job: apptainer exec --nv marimo_sandbox.sif nvidia-smi correctly sees the node's GPU.
Podman's CDI path is now also verified end-to-end (previously blocked in
testing by a stale /etc/cdi/nvidia.yaml on an earlier test node): on a real
gpu_l4 LSF allocation, pixi run shell-podman correctly reported a real
GPU (nvidia-smi -L → GPU 0: NVIDIA L4 (UUID: ...)), including with two
concurrent Podman jobs from the same user on the same GPU node (see "Podman
storage isolation" below) and with the egress allowlist enabled at the same
time. If you hit a crun: cannot stat ... libnvidia-nscq.so... error
instead, that's the stale-CDI-spec issue: /etc/cdi/nvidia.yaml needs
regenerating (nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml,
requires root) after a driver upgrade -- ask the HPC team, this repo can't
fix it from inside a container.
container/podman/lib.sh gives every marimo.sh/shell.sh invocation its
own Podman --root/--runroot, keyed by $LSB_JOBID (or $$-$RANDOM
outside LSF), instead of a single storage location shared by every job from
that user. Without this, two concurrent Podman jobs landing on the same GPU
node can corrupt each other's storage -- confirmed independently by
JaneliaScientificComputingSystems/agentic-sandbox
(a related sandboxing project on this same cluster), whose Podman wrapper
this hardening is adapted from. The per-job store still shares the durable,
/scratch-backed image cache via --storage-opt additionalimagestore, so a
second concurrent job on the same node doesn't pay for a re-pull/re-build.
Also included: automatic storage staleness recovery after a node reboot
(podman system migrate/reset), and a watchdog for catatonit (Podman's
container-init) occasionally not getting reaped before being reparented to
PID 1, which otherwise can block podman run itself from ever returning.
Verified live: two concurrent shell.sh GPU sessions on the same gpu_l4
node both saw the GPU and returned their own distinct exit codes correctly,
with no storage corruption and no leftover per-job storage directories
afterward. One caveat found in testing, being transparent about it rather
than overclaiming: an orphaned catatonit occasionally still lingers past
job completion despite the watchdog (observed once in this testing, cleaned
up by bkill, which -- per the "Running under LSF" section below -- reaps
the entire job's process tree regardless of individual process reparenting).
This matches a known limitation agentic-sandbox's own investigation
documents as a low-frequency residual risk, not something this port
introduced.
Cleanup of a per-job storage directory uses podman unshare rm -rf, not a
plain rm -rf -- accounts on this cluster get no /etc/subuid//etc/subgid
range unless one is explicitly requested from HPC (see the "Podman Build"
section's prerequisite note above), so without one, a plain rm -rf run as
your real user hits Permission denied on every file inside an overlay
diff layer -- confirmed live. This is a real difference from
agentic-sandbox's own cluster setup, which requires a subuid range as a
prerequisite for everyone; podman unshare rm -rf is expected to keep
working correctly for an account that has requested a range too (it
doesn't depend on there being no range), but that combination hasn't been
verified live yet -- only the no-range case has.
container/apptainer/marimo.sh launches the container with --contain (host home and CWD are NOT
mounted; /tmp is a private tmpfs) and then:
- bind-mounts each path in
RO_PATHSread-only; - binds
./work→/workread-write (override withWORK=/pathor--work /path); - sets
HOME=/work/home(via--home, the only mechanism that works — apptainer refuses to setHOMEvia--env) andTMPDIR=/work/tmp, so Marimo notebooks live in/work; agent config dirs (~/.claude, ...) are not mounted by default at all — see Sandbox strength below; - leaves the container rootfs itself read-only.
So an agent can read anything under the bound read-only paths but can only write
into /work. Attempts to modify the host filesystem fail by design.
⚠️ Read-only caveat on Janelia (autofs + NFS)./groups,/nrsand/scratchare autofs parents with a separate NFS mount per lab. A read-only bind is not recursive, so binding"/groups:ro"leaves the nested per-lab NFS mounts writable — a silent leak that defeats the sandbox. You must bind the leaf per-lab paths instead, e.g./groups/scicompsoft.start.sh/shell.shdefaultRO_PATHSto your lab dirs (/groups/scicompsoft,/nrs/scicompsoft) and refuse bare autofs parents. Set your own withRO_PATHS="/groups/<lab> /nrs/<lab> ...", or equivalently:pixi run marimo-apptainer --ro-paths "/groups/<lab> /nrs/<lab> ..." pixi run marimo "/groups/<lab> /nrs/<lab> ..."Three single-directory slots (
--ro-path-1/--ro-path-2/--ro-path-3) are also accepted, each bound the same way as one entry in--ro-paths-- these exist so Fileglancer can offer a directory picker for a few common extra paths instead of forcing free-text entry. Both forms combine (all given--ro-path-Nvalues are added to whatever--ro-paths/RO_PATHSresolves to), regardless of which order they're given in.
WORKandPORTaccept the same treatment (--work/--portflags, or the 2nd/3rd positional pixi task arguments — pass""to skip one and set a later one, e.g.pixi run marimo "" "" 9999for just the port). CLI flags / pixi task arguments take precedence over the env vars, which take precedence overconf/config.toml.Verified: binding
/groups/scicompsoft:romakes writes there fail (Read-only file system), while binding the parent/groups:rodoes not.
This is a convenience sandbox (protects the base image, scopes writable
storage to /work) rather than a security sandbox — it does not isolate
identity, network egress, or secrets. Concretely, what's in place and what
isn't:
- Process visibility — isolated. Apptainer runs with
--pid(its own PID namespace; without it,ps auxinside would show the whole compute node's processes, not just this job's — verified). Podman isolates PIDs by default, no flag needed. - Resource limits — partial.
container/entrypoint.shsets conservative rlimits (ulimit -f/-u) inside the container. Native cgroup-based limits (Apptainer's--memory/--cpus/--pids-limit, Podman's--memory) do not work reliably on Janelia's current HPC setup — Apptainer hard-fails withoutsystemd cgroupsenabled inapptainer.conf, and Podman silently accepts--memorywithout enforcing it under--cgroup-manager=cgroupfs. Both need infra changes outside this repo. Theresources:block inrunnables.yaml(cpus/memory/walltime) is the primary real control today, enforced by LSF at the job level. - Network egress — unrestricted by default. Outbound internet access
from inside the container is not blocked or allowlisted unless you opt in.
HTTP_PROXY/HTTPS_PROXY/NO_PROXY(and lowercase) are forwarded automatically if the launching shell already has them set, but there's no proxy or firewall provided by this repo by default — a compromised agent or a prompt-injected tool call could reach the open internet. Opt-in egress allowlist, both backends: pass--allow HOST(repeatable) or setALLOW_HOSTS="host1 host2"tomarimo.sh/shell.sh. When set, the container's network is fully isolated (Podman:--network=none; Apptainer:--net --network none, confirmed to give the same isolated-loopback-only result -- no bridge/NAT is configured on this host's unprivileged Apptainer install) and a host-side allowlist proxy (container/allowlist_proxy.py, reached via an in-container relay,container/relay.py) permits only the listed hostnames plusconda.anaconda.org(always included --entrypoint.sh's first-runpixi installneeds conda-forge regardless of what a task itself needs allowed) — everything else gets an HTTP 403, logged toproxy.loginside the proxy's own private temp dir (printed at startup). Covers HTTPS too, not just plain HTTP: bothhttp_proxyandhttps_proxypoint at the same relay, and the proxy understands HTTPCONNECT(the standard way an HTTPS request reaches a proxy) -- it checks theCONNECTtarget hostname against the allowlist, then just relays the still-encrypted bytes without ever decrypting TLS. Confirmed live:conda.anaconda.orgis fetched over HTTPS, and thepixi installverification above only succeeded through this proxy path. The real limit of this model: it's hostname-based (from theCONNECTline or the plain-HTTPHost:header), not deep packet inspection -- once a hostname is allowed, everything reachable through that encrypted connection (redirects, CDN backends, etc.) isn't separately checked. Adapted from JaneliaScientificComputingSystems/agentic-sandbox, which uses the identical mechanism (and the identical trust boundary) for its bwrap sandbox. Not compatible with the HTTPS runnables (marimo-https/marimo-podman-https): the allowlist isolates the container's network namespace, including its own loopback, so Caddy -- which runs outside the container, on the host -- can no longer reach the service it's supposed to front. Confirmed live: this used to fail silently with a plain Caddy 502, no explanation;https-wrap.shnow hard-errors immediately instead if--allow/$ALLOW_HOSTSis set. Use the plain-HTTPmarimo-apptainer/marimo-podmantasks (orshell-apptainer/shell-podman) with--allowinstead. - Identity — not isolated. Whatever files the container touches on
the host resolve to your real uid/gid with all your real HHMI/Janelia
group memberships, not a scoped service account -- this is inherent to
how Fileglancer/LSF jobs execute today, not something a container launch
script controls. This is unrelated to whether the container's own
internal view of itself is root or you (Podman defaults to root inside
its user namespace; opt in to your real uid/gid there too with
--keep-id, see "Podman Build" above) -- either way, the host-visible file ownership is the same. - Agent config dirs (
~/.claude,~/.gemini,~/.codex) are not mounted by default at all — a fresh sandbox starts with no host credentials/settings for any of them. Opt in per tool withCLAUDE_CONFIG/GEMINI_CONFIG/CODEX_CONFIG, each set torw(writable — needed for things like asetup-tokencredential file, conversation history, or session resume) orro(read-only — e.g. if you're using an API key instead of a subscription login, and want to remove settings/hooks self-tampering as a persistence vector). Whether to seed/work/home/.claude(etc.) by copying your real config there first, instead of a live bind, is up to you — this repo doesn't do that automatically. /workis a regular NFS bind, notnoexec. The user-editable pixi environment (container/app/pyproject.toml, seeded byentrypoint.sh) installs Python/marimo/every console script into/work/.pixiand execs them from there — anoexecmount would break marimo itself. Properly separating an exec-allowed subtree (the pixi env) from anoexecone (notebooks/data) would be a real architecture change, not a flag.bsub(LSF job submission) is off by default. A container launched without--enable-bsub/ENABLE_BSUB=1has no LSF client at all inside it — an agent callingbsubgets a clear error, not a silent failure. Turning this on hands the sandbox the ability to submit new cluster jobs under your own identity, on top of the one it's already running in — see Submitting LSF jobs from inside the sandbox.
Marimo, Python, and the data-science packages (numpy, pandas, polars, altair)
run out of a user-editable pixi environment, not the read-only image.
container/app/pyproject.toml + pixi.lock are the seed for this project;
on first run they're copied into ./work (the one writable, host-visible
directory) and installed into ./work/.pixi. container/entrypoint.sh then
serves Marimo from that environment instead of anything baked into the
image.
Because ./work is a real directory on the host, the project is editable
two ways:
- From inside the container — Marimo's own "install missing package"
prompt (and a shell's
pixi add <package>/pixi remove <package>) act on this project, since[tool.marimo.package_management] manager = "pixi"is set in the seededpyproject.toml. - From the host — edit
./work/pyproject.tomldirectly and re-runpixi run marimo ...; the next container start reinstalls it.
Once seeded, ./work/pyproject.toml is never overwritten automatically (so
your edits persist); delete it (and ./work/pixi.lock, ./work/.pixi) to
reseed from the image's current version.
entrypoint.sh also seeds the marimo-pair
Claude Code skill (vendored at container/skills/marimo-pair, from
marimo-team/marimo-pair,
Apache-2.0 -- see the LICENSE file alongside it -- baked into the image)
into ./work/.claude/skills/marimo-pair, so an agent CLI can pair-program
against the live notebook kernel with no extra setup and no network fetch at
runtime. This is a project skill: it's only picked up by an agent CLI whose
working directory is /work (the notebook's own embedded terminal, or a
shell started via pixi run shell-apptainer/pixi run shell-podman after cd /work). Its reference/finding-marimo.md
has been locally modified to point directly at this sandbox's pixi-managed
Python environment (/work/pyproject.toml) instead of the generic
uv/global/sandbox decision tree.
entrypoint.sh also seeds AGENTS.md into ./work (with CLAUDE.md/
GEMINI.md symlinked to it, so Claude Code/Codex/Gemini CLI all read the
same content) — a short note telling an agent CLI to fetch
hpc.int.janelia.org/docs/ai-agent-hints
before touching the Janelia cluster (LSF, storage tiers, GPU queues,
common agent mistakes), since that page is kept current while any
pasted/cached copy goes stale.
pixi run shell-apptainer # Apptainer
# or
pixi run shell-podman # Podman
# then, inside the container:
claude -p "summarize this project"
codex exec "..."
gemini -p "..."
agy -p "..."LSF has no notion of "run my job inside a container" — a bare bsub -n 4 python train.py runs train.py directly on whatever node LSF picks, outside
the sandbox entirely. This repo ships an opt-in bsub wrapper
(container/bsub-wrapper/bin/bsub, always on PATH inside the image) that
lets a wrapped job re-enter the same sandbox image on that other node
instead.
Off by default. Without --enable-bsub (or ENABLE_BSUB=1), the real
LSF client isn't even bind-mounted in, so calling bsub inside the container
fails immediately with an explanatory error — enabling this is a deliberate
expansion of what the sandbox can do (it can now submit new cluster jobs
under your identity, on top of the one it's already running in), not
something that happens implicitly.
pixi run marimo-apptainer --enable-bsub --ro-paths "/groups/scicompsoft /nrs/scicompsoft"Apptainer only, for now. Verified end-to-end: a wrapped job submitted
from inside a live Apptainer sandbox landed on a different real node and
correctly re-entered the same sandbox image there. The Podman backend hits
a real, unresolved blocker: LSF's eauth (external authentication) uses
Kerberos via sssd-kcm, and fails inside a rootless Podman container
(Failed in an LSF library call: External authentication failed) even
after fixing UID mapping (--userns=keep-id), bind-mounting the SSSD/KCM
sockets, and granting all capabilities/unconfined seccomp — this is a
deeper incompatibility between rootless Podman's isolation and this
cluster's Kerberos-based LSF auth, not a bug in the wrapper itself.
--enable-bsub therefore refuses to start on the Podman backend (a
hard error, not just a warning) until this is root-caused -- likely needs
Janelia IT input on what eauth/Kerberos actually requires inside a
container's network/mount namespace.
$WORK must be on shared storage (/groups or /nrs, not /scratch, which
is local to a single node) — the wrapper writes a small re-entry script
there that the job needs to see from whatever node it lands on.
Inside the container, wrap a job's command with a literal --:
bsub -n 4 -W 1:00 -o out.log -- python train.py --epochs 5Everything before -- is passed straight through to the real bsub as LSF
options; everything after becomes the job's command, re-entered inside this
same sandbox image (same binds, same env) wherever LSF schedules it. A
bsub call with no -- (e.g. bsub -Is /bin/bash, or a script piped on
stdin) is passed through completely unwrapped — it runs bare-metal on the
target node, exactly as it would with no wrapper present.
Credentials are never baked into the image. Any host env var matching
ANTHROPIC_*, OPENAI_*, GEMINI_*, GOOGLE_*, *_API_KEY, or *_AUTH_TOKEN
is forwarded into the container by start.sh / shell.sh.
If you'd rather use a subscription login (e.g. claude setup-token) than an
API key, its credential file lives under ~/.claude, which isn't mounted by
default — set CLAUDE_CONFIG=rw (or GEMINI_CONFIG/CODEX_CONFIG=rw for
the other CLIs) to bind it in. See
Sandbox strength
for the rw/ro distinction.
To drive these agents from an external ACP client (e.g. Zed):
- Claude Code:
npx --prefix /opt/npm-global claude-code-acp(the@zed-industries/claude-code-acpadapter on PATH). - Codex:
@zed-industries/codex-acpadapter. - Gemini:
gemini --acp(native, no adapter needed). - Antigravity: no ACP support yet.
- Antigravity self-update:
agytries to self-update in the background; on the read-only rootfs that write fails harmlessly.DISABLE_AUTOUPDATER=1is set in the image to suppress it. - Tool versions: conda packages in
pixi.toml/container/app/pyproject.tomlare pinned to a version range (e.g.>=26.6.0,<26.7) and locked viapixi.lock; npm agent CLIs are unpinned, so rebuilding always picks up their latest release. Bump the conda ranges and re-runpixi updatefor newer versions; there's no equivalent lock for the npm packages. - Marimo auth:
marimo editprints a per-session access token; use it (or--token-password) when exposing the port beyond localhost. - HTTPS: plain
marimo/start.shserve HTTP only. Usepixi run marimo-httpsfor a locally TLS-terminated option (see above); its self-signed cert requires installing the printed CA cert to avoid browser warnings.