Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed

- The shipped default template is `muse` (Meta's Muse Code), not `base`: a new box
with nothing configured boots an agent ready to work instead of an empty image.
`base` stays in the picker, stays the create-time fallback for a template that is
not on your cluster, and is still excluded from fleet rosters. A fleet roster now
drops templates that are agentless (`base`, or `[fleet.agents] <name> = ""`)
rather than whatever `[sandbox] template` names, so the default is a member.
`[sandbox] template = "base"` restores the old behaviour.

### Added

- `[templates.<name>] model = "…"` and `reasoning = "…"`: pin which model a
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,22 +245,22 @@ either, you get the chooser:
```
E2B template for my-worktree

▸ [1] base default
▸ [1] muse default
[2] claude
[3] codex
[4] opencode
[5] amp
[6] grok
[7] droid
[8] prime
[9] muse
[9] base

↑/↓ · j/k move enter confirm number jumps t type a name q default
```

`t` takes any name, so the menu is a shortcut and not a whitelist. `base` is
E2B's minimal image — fine for trying the flow, tight on disk for real work. For
that, [build a custom template](https://e2b.dev/docs/sandbox-template) with your
`t` takes any name, so the menu is a shortcut and not a whitelist. `muse` (Meta's
Muse Code) is the shipped default; `base` is E2B's minimal image, fine for trying
the flow or a plain shell, tight on disk for real work. For that, [build a custom template](https://e2b.dev/docs/sandbox-template) with your
toolchain and roomier resources, and point `[sandbox].template` at it.

> **Regions are named, not spelled as hosts.** `[sandbox] region = "us" | "eu"`
Expand Down
18 changes: 11 additions & 7 deletions config/config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,18 @@
# the plugin owns. An old `domain =` is an error, not an ignored line — silently
# dropping it would move an EU user's boxes to US without a word.

# E2B template. "base" (default) is minimal and always available — fine to try
# the flow, but tight on disk/tools for real work.
# RECOMMENDED: build a bigger CUSTOM template (more disk + CPU, your toolchain
# baked in) and set it here — see the README and install.sh. Public agent
# templates (claude, codex, opencode, … https://e2b.dev/docs/agents) also
# work but can run out of space. (Claude Code's template is named `claude`;
# E2B template a NEW box boots from when nothing else decided (no --template, no
# branch rule, no picker answer). "muse" (default) is Meta's Muse Code agent, ready
# to work once [templates.muse.env] holds a META_API_KEY. "base" is E2B's minimal
# image, always available and always in the picker: fine to try the flow or for a
# plain shell, tight on disk/tools for real work, and the image a create falls back
# to when the chosen template is not built on your cluster.
# RECOMMENDED for real work: build a bigger CUSTOM template (more disk + CPU, your
# toolchain baked in) and set it here, see the README and install.sh. The other
# public agent templates (claude, codex, opencode, … https://e2b.dev/docs/agents)
# also work but can run out of space. (Claude Code's template is named `claude`;
# Devin has no prebuilt template — install it into a base sandbox yourself.)
template = "base"
template = "muse"

# The menu `e2b-box open` offers when it has to BUILD a sandbox and nothing else
# decided which template (no --template, no matching rule below). The plugin
Expand Down
3 changes: 1 addition & 2 deletions docs/headless-run.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,7 @@ The template decides which agent works the task, and `run` needs that agent's
| `muse` | `muse exec --yolo --user-input-auto-resolve --prompt-file $HOME/.herdr-e2b-task.md` |
| `prime` | `prime-agent -p --no-session "$(cat $HOME/.herdr-e2b-task.md)"` |

`base` (the default) and your own templates are refused by name before anything
boots. An invented flag fails to launch and reads exactly like an agent
`base` and your own templates are refused by name before anything boots. An invented flag fails to launch and reads exactly like an agent
that did nothing. Teach one in `[run.agents]`:

```toml
Expand Down
23 changes: 17 additions & 6 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ export { AUTH_PATH, CONFIG_PATH } from "./config-paths.js"
const CLI_CONFIG_PATH = path.join(os.homedir(), ".e2b", "config.json")

const DEFAULTS = {
// Safe minimal default that always exists. For real work build a bigger
// custom template (more disk/CPU + your toolchain) and set it here — see the
// README "Recommended: a bigger custom template" and install.sh.
template: "base",
// What a box boots from when nothing else decided: Meta's Muse Code template,
// an agent ready to work rather than an empty image to install one into. `base`
// (E2B's minimal image, always present) stays in the picker and stays the
// fallback a create lands on when the chosen template is not on the cluster.
// For real work build a bigger custom template (more disk/CPU + your toolchain)
// and set it here, see the README "Recommended: a bigger custom template".
template: "muse",
templateRules: [], // [{pattern, template}] per-branch overrides
// The `open` picker's menu, in the order shown. Ships with the plugin so the
// picker is useful out of the box; `[sandbox] templates` in config REPLACES it.
Expand Down Expand Up @@ -1013,7 +1016,7 @@ export function templateRuleMatches(branch, cfg) {
* always offered so "just boot the usual one" is one keypress.
*/
/**
* The templates a FLEET may be built from: the same list, minus the plain default.
* The templates a FLEET may be built from: the same list, minus the agentless ones.
*
* A fleet is several coding agents working one checkout in parallel, so a box with
* no agent in it is not a member — it is a shell nobody asked for. `base` is also
Expand All @@ -1026,7 +1029,15 @@ export function templateRuleMatches(branch, cfg) {
* keeps offering it: one plain box is a perfectly good thing to want.
*/
export function fleetTemplateChoices(cfg) {
return templateChoices(cfg).filter((t) => t !== cfg.template)
// Agentless means: `base` (E2B's plain image, the one public template whose name
// is not a CLI), or a template the user mapped to "" in `[fleet.agents]`, the
// documented "plain shell, start nothing" for a control arm or their own plain
// image. NOT the configured default: since the default became an agent template
// (`muse`), filtering it out would drop the one member most people want.
const agents = cfg?.fleetAgents || {}
return templateChoices(cfg).filter(
(t) => t !== "base" && !(Object.prototype.hasOwnProperty.call(agents, t) && agents[t] === ""),
)
}

export function templateChoices(cfg) {
Expand Down
20 changes: 13 additions & 7 deletions test/cli.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -484,15 +484,21 @@ out=$(cd "$RUNREPO" && "$E2B" run --task x --dashboard 2>&1); rc=$?
{ [ "$rc" -eq 2 ] && printf '%s' "$out" | grep -q "doesn't take"; } \
&& ok "run with an unknown flag → exit 2" || bad "run unknown flag (rc=$rc, out=$out)"

# The default template is `base`, which ships no agent, and a template nobody has
# verified a headless mode for has no default either. Both are refused BY NAME with
# the fix on the line, and nothing boots.
# An empty config dir, so the developer's own [sandbox] template cannot answer here.
NOCFG="$TMP/nocfg"; mkdir -p "$NOCFG"
out=$(cd "$RUNREPO" && HERDR_PLUGIN_CONFIG_DIR="$NOCFG" "$E2B" run --task x 2>&1); rc=$?
# `base` ships no agent, and a template nobody has verified a headless mode for has
# no default either. Both are refused BY NAME with the fix on the line, and nothing
# boots.
out=$(cd "$RUNREPO" && "$E2B" run -t base --task x 2>&1); rc=$?
{ [ "$rc" -eq 2 ] && printf '%s' "$out" | grep -q "no headless agent command for template 'base'" \
&& printf '%s' "$out" | grep -q "\[run.agents\]"; } \
&& ok "run on the default template → refused by name, exit 2" || bad "run on base (rc=$rc, out=$out)"
&& ok "run -t base → refused by name, exit 2" || bad "run on base (rc=$rc, out=$out)"
# With nothing configured the shipped default template is `muse`, an agent, so a bare
# `run` has something to run. An empty config dir, so the developer's own [sandbox]
# template cannot answer here.
NOCFG="$TMP/nocfg"; mkdir -p "$NOCFG"
out=$(cd "$RUNREPO" && HERDR_PLUGIN_CONFIG_DIR="$NOCFG" "$E2B" run --task x --dry-run 2>/dev/null); rc=$?
{ [ "$rc" -eq 0 ] && [ "$(printf '%s' "$out" | jq -r '.template')" = "muse" ] \
&& printf '%s' "$out" | jq -e '.agentCommand | startswith("muse exec ")' >/dev/null; } \
&& ok "run with no -t and no config → the shipped default, muse" || bad "default template (rc=$rc, out=$out)"
out=$(cd "$RUNREPO" && "$E2B" run -t my-project/my-template --template-any --task x 2>&1); rc=$?
{ [ "$rc" -eq 2 ] && printf '%s' "$out" | grep -q "for template 'my-project/my-template'" \
&& printf '%s' "$out" | grep -q "pass -t one of: claude, codex, opencode, amp, grok, droid, muse, prime"; } \
Expand Down
13 changes: 8 additions & 5 deletions test/config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -662,17 +662,20 @@ test("resolveEnv: amp gets the welcome-splash skip, and a user can still overrid
)
})

test("fleetTemplateChoices: a fleet is agents only — the plain default is not a member", () => {
test("fleetTemplateChoices: a fleet is agents only: agentless templates are not members", () => {
// A fleet is several coding agents on one checkout, so a box with no agent in it
// is a shell nobody asked for. `base` is also the one template whose name is not
// a CLI, so a member built from it typed `base` into a sandbox shell and waited
// for a TUI that never came.
const cfg = { template: "base", templates: ["claude", "codex", "base"], templateRules: [] }
assert.deepEqual(fleetTemplateChoices(cfg), ["claude", "codex"])
// Filtered by the CONFIGURED default, not the literal "base", so someone whose
// plain image is called something else gets the same treatment.
const mine = { template: "plain", templates: ["plain", "claude"], templateRules: [] }
assert.deepEqual(fleetTemplateChoices(mine), ["claude"])
// The configured default is NOT what is filtered: the shipped default is `muse`, an
// agent, and the member most people want. A plain image of your own leaves the
// roster the documented way, `[fleet.agents] plain = ""`.
const mine = { template: "muse", templates: ["muse", "plain", "claude"], templateRules: [], fleetAgents: { plain: "" } }
assert.deepEqual(fleetTemplateChoices(mine), ["muse", "claude"])
const unmapped = { template: "muse", templates: ["muse", "plain", "claude"], templateRules: [] }
assert.deepEqual(fleetTemplateChoices(unmapped), ["muse", "plain", "claude"])
// And the box picker is untouched — one plain box is a fine thing to want.
assert.deepEqual(templateChoices(cfg), ["claude", "codex", "base"])
})
Expand Down
Loading