feat(bottle): per-bottle git user.name + user.email via manifest (issue #86) #87

Merged
didericis merged 4 commits from bottle-git-user-config-issue-86 into main 2026-05-27 23:03:19 -04:00
Collaborator

Closes #86.

Summary

Let each bottle declare a git identity (user.name + user.email) in its manifest. At provisioning time, the backend runs git config --global inside the bottle so the agent's commits land with that attribution — no more git refuses to commit until user.email is set on the first attempt.

Schema

git_user:
  name: "Eric Bauerfeld"
  email: "eric+claude@dideric.is"
  • Either field can be set independently — name-only / email-only configs apply just the field that's set.
  • Omitted block is the no-op path (provisioner skips entirely).
  • Explicit git_user: with both fields empty dies at parse time (half-finished edit hint).
  • Unknown sub-keys / non-string values die at parse with a pointer.

Changes (4 commits)

  1. feat(manifest): add git_user bottle field — new GitUser dataclass, Bottle.git_user field, parser, _BOTTLE_KEYS extension. 11 unit tests in test_manifest_git_user.py.
  2. feat(docker): apply git_user via git config --global on provision — new _provision_git_user subcase in backend/docker/provision/git.py. Runs docker exec -u node ... git config --global user.X <value> so --global lands in /home/node/.gitconfig. 4 unit tests.
  3. feat(smolmachines): apply git_user via git config --global on provision — same pattern in backend/smolmachines/provision/git.py, via smolvm machine exec -e HOME=/home/node -e USER=node -- runuser -u node -- git config --global .... The HOME/USER env is load-bearing because bare runuser without -l inherits root's HOME=/root, which would put --global in the wrong file. 4 unit tests.
  4. docs(readme): document git_user manifest field — add example block to the bottle frontmatter sample.

Tests

661 unit tests pass. New coverage:

  • Manifest: parse / unknown-key / wrong-type / both-empty / name-only / email-only.
  • Docker provision: no-op / both / name-only / email-only argv shape (incl. -u node).
  • Smolmachines provision: no-op / both / name-only / email-only argv shape (incl. runuser -u node -- + HOME=/home/node env).

Manual verification path

  1. Add git_user: to a bottle in ~/.claude-bottle/bottles/<bottle>.md.
  2. Launch an agent under that bottle (either backend).
  3. Inside the bottle, git config --global --get user.name / ... user.email returns the configured values.
  4. git commit works without prompting for identity.
Closes #86. ## Summary Let each bottle declare a git identity (`user.name` + `user.email`) in its manifest. At provisioning time, the backend runs `git config --global` inside the bottle so the agent's commits land with that attribution — no more `git refuses to commit until user.email is set` on the first attempt. ## Schema ```yaml git_user: name: "Eric Bauerfeld" email: "eric+claude@dideric.is" ``` - Either field can be set independently — name-only / email-only configs apply just the field that's set. - Omitted block is the no-op path (provisioner skips entirely). - Explicit `git_user:` with both fields empty dies at parse time (half-finished edit hint). - Unknown sub-keys / non-string values die at parse with a pointer. ## Changes (4 commits) 1. **`feat(manifest): add git_user bottle field`** — new `GitUser` dataclass, `Bottle.git_user` field, parser, `_BOTTLE_KEYS` extension. 11 unit tests in `test_manifest_git_user.py`. 2. **`feat(docker): apply git_user via git config --global on provision`** — new `_provision_git_user` subcase in `backend/docker/provision/git.py`. Runs `docker exec -u node ... git config --global user.X <value>` so `--global` lands in `/home/node/.gitconfig`. 4 unit tests. 3. **`feat(smolmachines): apply git_user via git config --global on provision`** — same pattern in `backend/smolmachines/provision/git.py`, via `smolvm machine exec -e HOME=/home/node -e USER=node -- runuser -u node -- git config --global ...`. The HOME/USER env is load-bearing because bare `runuser` without `-l` inherits root's HOME=/root, which would put `--global` in the wrong file. 4 unit tests. 4. **`docs(readme): document git_user manifest field`** — add example block to the bottle frontmatter sample. ## Tests 661 unit tests pass. New coverage: - Manifest: parse / unknown-key / wrong-type / both-empty / name-only / email-only. - Docker provision: no-op / both / name-only / email-only argv shape (incl. `-u node`). - Smolmachines provision: no-op / both / name-only / email-only argv shape (incl. `runuser -u node --` + `HOME=/home/node` env). ## Manual verification path 1. Add `git_user:` to a bottle in `~/.claude-bottle/bottles/<bottle>.md`. 2. Launch an agent under that bottle (either backend). 3. Inside the bottle, `git config --global --get user.name` / `... user.email` returns the configured values. 4. `git commit` works without prompting for identity.
didericis-claude added 4 commits 2026-05-27 23:01:26 -04:00
Per-bottle `git config --global user.name` / `user.email` pair
so the agent's commits inside the bottle land with a known
identity rather than the agent image's default (no user, or
whatever the image dropped in).

Schema:
  git_user:
    name: "Eric Bauerfeld"
    email: "eric+claude@dideric.is"

Either field can be set independently — name-only / email-only
configs are valid and apply just the field that's set. An
explicit `git_user:` block with both fields empty dies at parse
time rather than silently no-op'ing; an omitted block is the
no-op path (default GitUser is empty, provisioner skips).

Parse-time validation:
- Unknown sub-keys die (e.g., typo of `username`).
- Non-string name/email dies.
- Both-empty dies (half-finished edit hint).

11 unit tests in `test_manifest_git_user.py`; 653 unit tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a third provisioning subcase to
`backend/docker/provision/git.py`:

  _provision_git_user(plan, target)

Runs `docker exec -u node <container> git config --global
user.{name,email} <value>` for each field the bottle's
`git_user` declares. No-op when `git_user.is_empty()`.

`-u node` so `--global` lands in /home/node/.gitconfig (matching
the existing `_provision_git_gate_config` write location, so
agent-side `git` reads both configs from the same dotfile).

Name and email apply independently — a bottle declaring only
name runs just the user.name line, etc.

4 unit tests in `test_docker_provision_git_user.py`: no-op,
both-set, name-only, email-only. 657 unit tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mirror the docker backend's third provisioning subcase in
`backend/smolmachines/provision/git.py`:

  _provision_git_user(plan, target)

Runs `smolvm machine exec --name <M> -e HOME=/home/node -e
USER=node -- runuser -u node -- git config --global user.<X>
<value>` for each git_user field. No-op when
`git_user.is_empty()`.

`runuser -u node --` switches the UID without invoking a login
shell (matching the existing `Bottle.exec_claude` pattern).
HOME / USER are forced via `smolvm -e` because bare runuser
inherits root's HOME=/root, which would put --global in
/root/.gitconfig instead of /home/node/.gitconfig (where the
existing `_provision_git_gate_config` writes).

4 unit tests in test_smolmachines_provision.TestProvisionGitUser:
no-op, both-set (asserts runuser prefix + HOME/USER env),
name-only, email-only. 661 unit tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
docs(readme): document git_user manifest field (issue #86)
test / unit (pull_request) Successful in 27s
test / integration (pull_request) Successful in 42s
test / unit (push) Successful in 26s
test / integration (push) Successful in 44s
d0712fb757
Add a `git_user:` block to the example bottle frontmatter with a
one-paragraph note on what it does + that either field can be
set independently. Other doc surfaces (manifest module docstring,
provisioner module docstrings) were updated alongside the
implementation commits.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
didericis-claude requested review from didericis 2026-05-27 23:01:32 -04:00
didericis approved these changes 2026-05-27 23:02:50 -04:00
didericis merged commit d0712fb757 into main 2026-05-27 23:03:19 -04:00
didericis deleted branch bottle-git-user-config-issue-86 2026-05-27 23:03:19 -04:00
Sign in to join this conversation.