From 1cea78db417d13f536765a0ba45591cc685a76d8 Mon Sep 17 00:00:00 2001 From: Tomasz Mazur Date: Sun, 30 Aug 2026 21:21:03 +0200 Subject: [PATCH] feat: seed machine-level agent instructions into both agents Claude Code and Copilot CLI each read a home-directory instructions file at the start of every session -- ~/.claude/CLAUDE.md and ~/.copilot/copilot-instructions.md. Neither was written, so every session began by rediscovering the machine's conventions, or guessing wrong about them. Writes one source, /etc/orbx/agent-instructions.md, and copies it into both paths from user-setup.sh. Not the project's AGENTS.md: that file is the user's and lives in the repo mounted from the host, so writing it would dirty the tree and eventually get committed. Machine facts only -- no package inventory. Agents discover tools by running them; what they cannot discover is a convention whose failure mode is the agent "fixing" the machine. A failed psql does not make an agent stop and ask, it makes it sudo -u postgres, set a password and rewrite pg_hba.conf, leaving the sandbox diverged for every later session. Three facts clear that bar: the Postgres role is $USER with trust auth, Postgres and Redis are running systemd units (an agent's prior for a container is that there is no systemd), and there is no host filesystem. The mise shims and /etc/sandbox-status were considered and cut -- `ruby -v` just works, and agents only ever run after ready. The fourth is git. #3 already turned the credential-prompt hang into a fast failure, which an instruction file cannot do: by the time git blocks on "Username for 'https://github.com':" the agent has issued the command and has no turn left in which to read advice. What the guard cannot do is control how the error is read. "terminal prompts disabled" names its own knob, so it reads as an obstacle with a workaround rather than as policy, and the workarounds get worse as they go: GIT_TERMINAL_PROMPT=1 restores the hang, a credential helper writes plaintext creds, and a token in a remote URL lands in .git/config -- which is bind-mounted, so it is a credential written to the host's disk that outlives the sandbox. The file prohibits all three by name and points at `gh auth login` as an escalation: that flow is interactive, so the agent must ask rather than attempt it. Authentication is not blocked, only interactive authentication -- a credential helper works fine under the guard. The file defers to the repository's own instructions and carries a maintainers' note holding the line at machine facts. Copilot CLI ranks personal instructions above the repo's AGENTS.md, so a style or workflow opinion added here would silently outrank the user's project instructions in every sandbox, with nothing to indicate why. Both lookup paths are a contract with each vendor, not a convention we picked. If either moves upstream this fails silently -- provisioning still succeeds and agents go back to guessing -- so the tests pin both paths and the seed logs a warning when the source is missing. Guarded on the file rather than the directory: the Claude Code and Copilot installers run earlier in user-setup.sh and may already have created ~/.claude and ~/.copilot, where a dir check would skip the copy on a fresh machine. A file check also preserves a user's own file. Closes #6 --- templates/default.yaml | 80 ++++++++++++++++++++++++++++++++++++++++++ test/template.bats | 64 +++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+) diff --git a/templates/default.yaml b/templates/default.yaml index 10934f1..ec72f81 100644 --- a/templates/default.yaml +++ b/templates/default.yaml @@ -165,6 +165,69 @@ write_files: │ Postgres role = your username (superuser, trust auth, local). │ └──────────────────────────────────────────────────────────────┘ + # Machine-level agent instructions. Seeded into BOTH agents' home-directory + # instruction paths by user-setup.sh below, from this single source. + # + # Those paths are a contract with each vendor, not a convention we picked: + # Claude Code ~/.claude/CLAUDE.md + # Copilot CLI ~/.copilot/copilot-instructions.md + # If either ever moves, this fails SILENTLY -- provisioning still succeeds and + # agents simply go back to guessing. There is no error to catch. + # + # Machine facts only. See the "Maintainers" note in the file itself for why. + - path: /etc/orbx/agent-instructions.md + permissions: '0644' + content: | + # Machine notes (orbx sandbox) + + Facts about the machine you are running on, provisioned by orbx. These are + not project instructions: the repository's own AGENTS.md / CLAUDE.md still + governs how to work on the code, and wins wherever the two could disagree. + + > **Maintainers:** machine facts only -- nothing here that a project could + > reasonably want to override. No style, workflow, testing or commit + > conventions. Copilot CLI ranks personal instructions *above* the + > repository's AGENTS.md, so an opinion added here would silently outrank + > the user's own project instructions in every sandbox, with nothing to + > indicate why. orbx provisions the machine, so it describes the machine. + + ## This is an isolated VM + + There is no host filesystem. Only the project directory is shared, as a + live bind mount -- writes there land on the host immediately and survive + this machine being deleted. Everything outside it is disposable. + + ## PostgreSQL and Redis are already running + + Both are systemd units, enabled and started at provisioning. Use + `systemctl status postgresql` / `systemctl status redis-server`. Do not + hand-start them, and do not assume this container has no systemd. + + The Postgres superuser role is your Unix username, with trust auth on + local and loopback connections. No password is needed or wanted -- a bare + `psql` and `bin/rails db:create` work as-is. If a connection fails, report + the error; do not create roles or edit `pg_hba.conf`. + + ## git cannot prompt for credentials + + `GIT_TERMINAL_PROMPT=0` is set machine-wide, deliberately. Without it a + fetch or push against a private repo blocks on + `Username for 'https://github.com':` until the session is killed, which + reads as a slow network rather than as a stuck process. + + If a git operation fails on authentication, **stop and ask the human to + run `gh auth login`**. That flow is interactive, so you cannot run it. + + Do not work around it: + + - do not set `GIT_TERMINAL_PROMPT=1` -- that restores the hang + - do not add a credential helper or write `~/.git-credentials` + - do not put a token in a remote URL -- `.git/config` lives on the host's + disk, so a credential written there outlives this sandbox + + Authentication is not blocked, only *interactive* authentication: once + `gh auth login` has configured a credential helper, git works normally. + # Default Starship prompt: icon-free and low-noise. Shows user, machine as # @orb, path, and git only -- no language versions or container badge. # Seeded into each user's ~/.config on first boot (see user-setup.sh) so it @@ -391,6 +454,23 @@ write_files: cp /etc/starship.toml "$HOME/.config/starship.toml" fi + # Seed machine-level instructions into both agents' home-directory paths + # from the single source in /etc/orbx. Guarded on the FILE, not the dir: + # the Claude Code and Copilot installers above may already have created + # ~/.claude and ~/.copilot, and a dir check would then skip the copy. + # Never touches the project's AGENTS.md -- that file is the user's, and it + # lives in the repo mounted from the host. + if [ -f /etc/orbx/agent-instructions.md ]; then + mkdir -p "$HOME/.claude" "$HOME/.copilot" + [ -f "$HOME/.claude/CLAUDE.md" ] \ + || cp /etc/orbx/agent-instructions.md "$HOME/.claude/CLAUDE.md" + [ -f "$HOME/.copilot/copilot-instructions.md" ] \ + || cp /etc/orbx/agent-instructions.md "$HOME/.copilot/copilot-instructions.md" + log "Seeded machine-level agent instructions." + else + log "WARNING: /etc/orbx/agent-instructions.md missing; agents unguided" + fi + # Sensible git defaults (name/email left for you to set) git config --global init.defaultBranch main git config --global push.autoSetupRemote true diff --git a/test/template.bats b/test/template.bats index 258b0a6..0c6d310 100644 --- a/test/template.bats +++ b/test/template.bats @@ -195,3 +195,67 @@ load helpers/test_helper run grep -E "^\s+- postgresql-contrib$" "$ORBX_TEST_ROOT/templates/default.yaml" [ "$status" -ne 0 ] } + +# --- bundled default template: machine-level agent instructions (issue #6) --- +# Both agents read a home-directory instructions file at the start of every +# session. orbx seeds both from one source so agents stop rediscovering (or +# guessing wrong about) the machine's conventions. + +@test "default template ships a single agent-instructions source" { + run grep -cE "^\s+- path: /etc/orbx/agent-instructions\.md$" \ + "$ORBX_TEST_ROOT/templates/default.yaml" + [ "$status" -eq 0 ] + [ "$output" -eq 1 ] # one source; both agent files are copies of it +} + +@test "default template seeds both agents' instruction paths" { + # Vendor-fixed lookup paths: Claude Code reads ~/.claude/CLAUDE.md, Copilot + # CLI reads ~/.copilot/copilot-instructions.md. If either moves upstream this + # breaks silently, so pin both. + run grep -F '"$HOME/.claude/CLAUDE.md"' "$ORBX_TEST_ROOT/templates/default.yaml" + [ "$status" -eq 0 ] + run grep -F '"$HOME/.copilot/copilot-instructions.md"' \ + "$ORBX_TEST_ROOT/templates/default.yaml" + [ "$status" -eq 0 ] +} + +@test "instruction seeding is guarded on the file, not the directory" { + # The Claude Code and Copilot installers run earlier in user-setup.sh and may + # already have created ~/.claude and ~/.copilot; a dir check would then skip + # the copy on a fresh machine. A file check also preserves a user's own file. + run grep -F '[ -f "$HOME/.claude/CLAUDE.md" ]' \ + "$ORBX_TEST_ROOT/templates/default.yaml" + [ "$status" -eq 0 ] + run grep -F '[ -f "$HOME/.copilot/copilot-instructions.md" ]' \ + "$ORBX_TEST_ROOT/templates/default.yaml" + [ "$status" -eq 0 ] +} + +@test "default template never writes the project's AGENTS.md" { + # That file lives in the repo mounted from the host. Writing it would dirty + # the user's git tree and eventually get committed. + run grep -E "(^\s+- path:.*AGENTS\.md|cp .*AGENTS\.md|> *\"?\\\$HOME.*AGENTS\.md)" \ + "$ORBX_TEST_ROOT/templates/default.yaml" + [ "$status" -ne 0 ] +} + +@test "agent instructions carry the facts an agent would otherwise misdiagnose" { + # Not a tool inventory -- agents discover tools by running them. These are the + # conventions whose failure mode is the agent "fixing" the machine instead. + run grep -F 'trust auth on' "$ORBX_TEST_ROOT/templates/default.yaml" + [ "$status" -eq 0 ] + run grep -F 'do not assume this container has no systemd' \ + "$ORBX_TEST_ROOT/templates/default.yaml" + [ "$status" -eq 0 ] + run grep -F 'do not set `GIT_TERMINAL_PROMPT=1`' \ + "$ORBX_TEST_ROOT/templates/default.yaml" + [ "$status" -eq 0 ] +} + +@test "agent instructions state that project instructions win" { + # Copilot CLI ranks personal instructions above the repo's AGENTS.md, so this + # file must defer explicitly and stay to machine facts. + run grep -F 'wins wherever the two could disagree' \ + "$ORBX_TEST_ROOT/templates/default.yaml" + [ "$status" -eq 0 ] +}