msb-edit

Editing an existing sandbox and the scriptable surface: msbctl edit's EDIT_SECTIONS each reusing the wizard's own pick_* function preselected from current state, set_toml_key() as the comment-preservi

SKILL.md

GitHub

Edit

Detail carved out of AGENTS.md, which keeps only what has to hold in every session. Repo-specific throughout; the facts here are verified against msb 0.7.6 and re-checked on 0.7.7.

Editing an existing sandbox

msbctl edit <name> (the picker's Enter, or e) is the wizard as a menu of sections, one at a time: EDIT_SECTIONS in msbctl (name & folder, features & agents, resources, package caches, container storage, extra folders, egress rules, published ports, SSH keys, git push, Claude token, GitHub token, secrets, identity & autostart) plus "view the CLAUDE.md it gets" (sandbox_claude_md(), read-only) and "edit the files in your editor". Each section_* reuses the wizard's own pick_* function preselected from the sandbox's current state, writes back with set_toml_key() (an in-place, comment-preserving, parse-checked single-key editor — never rewrite a whole file), and either applies live (resources via msb modify, keys, secrets) or adds to ctx["rebuild"], which Done turns into a rebuild offer. Portable settings go to the project's .msb/sandbox.toml, machine-local ones (mounts, published ports, identity, Claude token, ssh keys, autostart) to the registry entry; edit_target() writes to the registry only if it already overrides that key. Esc inside a section returns to the menu. If a hand edit leaves a file unparseable, _recover_broken() offers the editor instead of locking you out. A new wizard step should get a matching section. msb can rename neither a sandbox nor a volume, so rename_sandbox() (msbctl rename, the name & folder section) removes the VM, moves everything msbctl keys by name (entry, secrets/<name>.env, Claude state, agent log, versions cache) and recreates it; existing disk volumes keep their old prefix through the entry's volumes key, and name_problem() refuses a new name that is still another sandbox's prefix. move_project() (msbctl move) repoints project, copies .msb/ over if the new folder lacks it, re-derives repo, and needs a rebuild. Per-sandbox secrets (secrets/<name>.env) are written ONLY through set_env_file_value() (or try_set_env_file_value(), which returns False instead of dying when the keyring refuses: where the entry is already written, as in add, a refusal only warns) — never by truncating the file, which once erased every other secret in it. Those two and read_env_file() are the whole storage layer, with stored_env() beside them for whatever only asks whether a value is stored (secret ls, ls/show --json, the edit menu's summaries, claude_tokens() by default): it reads the file and never the keyring, so a listing never prompts. A value going into a file must pass env_value_problem() (a line break or quotes or spaces at its ends would not read back): [secrets] store (write_store()) puts a value in the file or, through secret-tool with the value on stdin, in the OS keyring with VAR=@keyring left in the file; read_env_file() resolves a marker whatever the setting. A locked keyring is used and shows its own unlock prompt (the operator wants that on a desktop; a headless host must have it unlocked or passwordless); keyring_status() reads the D-Bus Locked property without prompting, and _secret_tool() refuses to run only when there is no Secret Service at all. Over ssh with no display, a bus at /run/user/<uid>/bus is the desktop's: a locked keyring's prompt shows on its screen, which _note_remote_prompt() says once (no guard: the operator wants the prompt). Whatever needs only GH_TOKEN (repo_token(), refresh_versions(), github_languages()) reads it with only={"GH_TOKEN"}, and Sandbox.github_token_bound counts a marker only while _keyring_has() finds its item. Each value is looked up once per run (keyring_get()), and once a prompt is dismissed or unanswered (_keyring_failed() asks the bus afresh: unlocked means the item is gone; still locked means refused, unless _keyring_has() — secret-tool search, which lists a locked item and never prompts — finds no item, since libsecret fails a lookup or clear that matched nothing WITHOUT prompting) no call prompts again that run; keyring_forget() resets this in main() and each turn of the menu. A dismissed prompt reads as no value at start, and stops a write, migrate, rename or purge with nothing changed (spike 20 checks this against a real gnome-keyring with no display). A marker whose item was deleted outside msbctl is "missing" in keyring_why(): rename carries it, migrate --to file drops its line, both say so. add and add --repo check write_store() (and, for the file, env_value_problem()) before writing anything, and a keyring that then refuses a value only warns (the entry and policy are already written). Anything that moves or deletes secrets/<name>.env must move or clear its keyring items too, and before the file goes (rename_sandbox() copies them under the new name first, takes the copies back if anything fails before the files move, and clears the old ones after; cmd_purge() reads every value, then clears them before anything else, putting back what it cleared if a later clear fails; both undo steps run under keyring_second_chance(), which lifts a refusal latched this run so the undo is not a silent no-op; keyring_clear() counts an item already gone as cleared), since items are keyed by the file's name; autostart with the keyring is allowed: turning either on states AUTOSTART_KEYRING_CAVEAT and confirms on a terminal (autostart_keyring_ok(), only a warning off one), and start --autostart off a terminal skips each sandbox autostart_blocked() (autostart_needs_keyring(), an unreadable entry left for its own start to report) (the keyring values a start reads: Sandbox.own_secret_vars() of its own file, Sandbox.global_secret_vars() of global.env — nothing else in either is looked up, so a value left unbound by secret rm neither prompts nor holds it up) while the keyring is not unlocked, instead of hanging on a prompt nobody can answer. MSB_TEST_SECRET_STORE=keyring runs the whole suite on the fake keyring, as check.sh does.

Scripting interfaces

ls --json (ls_data()) and show --json (show_data()) print exactly one JSON document on stdout; everything else that runs meanwhile is redirected to stderr. They say whether a secret is stored, never its value. secret set <name> VAR [--stdin] [--restart] stores a value only for a variable the sandbox already binds (GH_TOKEN or a non-reserved secret_binds var) — binding a new one is secret add, a policy change — and bind_secret_live() asks msb (msb_bound_secrets(), from msb inspect) whether a binding exists before adding one with msb modify --secret (--next-start / --restart on a running sandbox). setup --claude-token NAME [--stdin] stores a Claude token with no menu. secret migrate --to keyring|file [--json] is its own sub-verb (cmd_secret_migrate), with no sandbox, refused under host serve; verb_args("secret", action=...) descends into the sub-verb. Values from stdin go through read_secret_input() (one trailing newline dropped; empty, whitespace or >8 KiB refused, never echoed).