Skip to content

fix(remote-worker): bake a system git identity into the sandbox image - #463

Merged
pdettori merged 2 commits into
rossoctl:mainfrom
pdettori:fix/415-sandbox-git-identity
Oct 8, 2026
Merged

pdettori merged 2 commits into
rossoctl:mainfrom
pdettori:fix/415-sandbox-git-identity

Conversation

@pdettori

@pdettori pdettori commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Fixes #415.

Problem

An agent's first git commit in a sandbox failed with exit 128 ("Please tell me who you are") until it configured an identity itself — observed live on the VM demo (docs/demos/vm-multi-user-demo.md, fix list 5), on both P4 sessions and on the container tier. Each workaround cost a tool call and a model round, in front of an audience.

Fix

A system gitconfig (/etc/gitconfig: user.name = MOCA sandbox, user.email = sandbox@moca.invalid) baked into both leaf images (remote-worker/Dockerfile, Dockerfile.runtime), exactly as the issue proposes:

Guards (per the issue)

  • research_tooling_test.go:
    • a static pin of the exact RUN printf instruction in both files, and its position before the USER 1001 drop;
    • a runtime check that extracts the printf's format string, installs it as a scratch environment's only git config, and runs a real no-prior-config commit — so a gitconfig the line still prints but git silently ignores (typo'd section header, literal \\t) fails CI, not the next demo.
  • build-snapshot.sh: check_git_identity runs a real empty commit (with user.useConfigOnly=true, so an unconfigured git fails instead of auto-guessing an identity) in the booted guest of both VMM arms, after probe_capabilities, before quiesce_guest. A rootfs built from an image that lost its gitconfig now fails the snapshot build at build time. build-snapshot.test.sh pins the gate's contract (existence, both arms, probe→check→quiesce order, useConfigOnly, real commit).

Verification

  • go test ./... in remote-worker passes; the new guards were mutation-tested (commented-out RUN, RUN-after-USER, typo'd section header each fail the respective test).
  • hadolint clean on both Dockerfiles; shellcheck -S warning clean.
  • build-snapshot.test.sh 209 ok / 0 fail; build-rootfs.test.sh PASS.

🤖 Assisted-By: Claude Code

…rossoctl#415)

An agent's first `git commit` in a sandbox failed with exit 128 ("Please
tell me who you are") until it configured an identity itself -- observed
live on the VM demo (docs/demos/vm-multi-user-demo.md, fix list 5), on both
P4 sessions and on the container tier. Each workaround cost a tool call and
a model round, in front of an audience.

Fix both leaf images the way the issue proposes: a system gitconfig
(/etc/gitconfig, user.name "MOCA sandbox" / user.email
sandbox@moca.invalid), which applies to ANY uid the images can run as --
including the arbitrary gid-0 uid under nonroot-v2 that rossoctl#372's writable
HOME prepares but cannot pre-configure -- and which a user's or agent's own
higher-precedence config still overrides. The P4 rootfs inherits it for
free, being exported from this image by build-rootfs.sh.

Guards, per the issue:

- research_tooling_test.go: a static pin of the exact RUN instruction (and
  its position before the USER drop), plus a runtime check that extracts
  the printf's format string, installs it as a scratch environment's ONLY
  git config, and runs a real no-prior-config commit -- so a gitconfig the
  line still prints but git silently ignores fails CI, not the next demo.
- build-snapshot.sh: check_git_identity runs a real empty commit (with
  user.useConfigOnly=true, so an unconfigured git fails instead of
  auto-guessing) in the booted guest of both VMM arms, before quiescing --
  a rootfs built from an image that lost its gitconfig now fails the
  snapshot build, not the demo. build-snapshot.test.sh pins the gate's
  contract (existence, both arms, probe->check->quiesce order,
  useConfigOnly, real commit, rossoctl#415 pointer).

Verified: go test ./... in remote-worker (including mutation tests on the
new guards: commented-out RUN, RUN-after-USER, and a typo'd section header
all fail), hadolint clean on both Dockerfiles, shellcheck -S warning clean,
build-snapshot.test.sh and build-rootfs.test.sh PASS.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Signed-off-by: Paolo Dettori <dettori@us.ibm.com>

@ingpaolodettori-dev ingpaolodettori-dev left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This bakes a system /etc/gitconfig (MOCA sandbox <sandbox@moca.invalid>) into both leaf images, so an agent's first git commit no longer exits 128. It adds two guards: a Go static pin plus a runtime check, and a snapshot-build gate that runs a real commit with useConfigOnly in the booted guest. I checked that guest_client passes the guest command's exit code through (os.Exit(e.ExitCode)), so the gate does fail closed. I found no blocking issues. Two suggestions: (1) the gate's /tmp/id-check repo gets captured into the memory snapshot and so ships in every restored sandbox, and (2) the Go runtime test should pin useConfigOnly and assert the resulting identity, so it can't pass on git's auto-detected identity.

Author: pdettori (MEMBER — maintainer)
Areas reviewed: Dockerfile, Shell, Go tests, Security
Agent/IDE config (.claude/.vscode): none
Commits: 1 commit, all signed-off: yes
CI status: passing (14/14, including hadolint, shellcheck, microvm-gates, k8s-kind-e2e)

Comment thread deploy/microvm/build-snapshot.sh Outdated
local uds="$1"
local out rc
out="$("$STAGE/guest_client" -uds "$uds" -port 1024 -timeout-s 30 \
-command 'git init -q /tmp/id-check && cd /tmp/id-check && git -c user.useConfigOnly=true commit --allow-empty -m x 2>&1' \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion — The repo this check creates ends up in every restored sandbox. check_git_identity runs before quiesce_guest and the snapshot. The snapshot captures guest RAM (mem_backend / memfile), and that includes the /tmp tmpfs. So every VM restored from this snapshot boots with /tmp/id-check, which holds a MOCA sandbox commit. The comment at L841 ("the throwaway repo leaves no trace") is only true for a cold boot, not for a snapshot restore.

You could remove it inside the same guest command and keep the commit's exit code:

-command 'git init -q /tmp/id-check && cd /tmp/id-check && git -c user.useConfigOnly=true commit --allow-empty -m x 2>&1; rc=$?; cd / && rm -rf /tmp/id-check; exit $rc'

It's also worth a build-snapshot.test.sh check that pins the rm -rf /tmp/id-check, and a fix to the L841 comment.

Comment thread deploy/microvm/build-snapshot.sh Outdated
local out rc
out="$("$STAGE/guest_client" -uds "$uds" -port 1024 -timeout-s 30 \
-command 'git init -q /tmp/id-check && cd /tmp/id-check && git -c user.useConfigOnly=true commit --allow-empty -m x 2>&1' \
2>/dev/null)" && rc=0 || rc=$?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit — 2>/dev/null throws away guest_client's own diagnostics (dial or CONNECT failures, guest error: …). A transport failure then gets reported as "the guest git has no usable identity", with an empty Guest output:. The guest command already merges its own stderr with 2>&1, so you could capture host-side stderr as well (2>&1) to make the error message accurate in both cases.

}
for _, args := range [][]string{
{"init", "-q"},
{"commit", "--allow-empty", "-m", "#415"},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion — This runtime check can pass without testing anything, which is the same failure mode the shell gate avoids with user.useConfigOnly=true. When git has no user.name/user.email, it doesn't fail outright. It falls back to an identity built from the passwd entry and the hostname, and it only refuses when that email isn't fully qualified. On a host whose hostname resolves to an FQDN (many CI runners and dev machines), a broken baked gitconfig (typo'd [user] header, literal \t) still commits. The test then passes, so it doesn't catch the drift its doc comment says it catches. Separately, GIT_CONFIG_SYSTEM needs git ≥ 2.32. Older git ignores it and reads the host's /etc/gitconfig.

Two changes would close both gaps:

{"-c", "user.useConfigOnly=true", "commit", "--allow-empty", "-m", "#415"},

and then assert the identity that was actually used:

out, err := run("log", "-1", "--format=%an <%ae>")
// want "MOCA sandbox <sandbox@moca.invalid>"

(Today the static systemGitconfig pin catches these mutations, but only because it pins the exact bytes. The runtime test should hold up by itself.)

…eck, stderr capture, vacuous-pass guard

Three review findings on the rossoctl#415 gate:

- check_git_identity's throwaway repo was captured in the snapshot: the
  check runs before quiesce_guest, and the snapshot captures guest RAM,
  /tmp's tmpfs included, so /tmp/id-check shipped in every restored
  sandbox. The repo is now removed INSIDE the same guest command, with
  the commit's exit code carried across the cleanup (rc=$?; ...; exit
  $rc) so the gate still fails closed. build-snapshot.test.sh pins both
  the rm and the exit-code carry, and the comment claiming "leaves no
  trace" is replaced with the accurate snapshot-RAM reasoning.

- guest_client's host-side stderr was discarded (2>/dev/null), so a
  transport failure (dial, CONNECT refused) was misreported as "the
  guest git has no usable identity" over an empty Guest output line.
  Now captured (2>&1) and the error message says either could have
  failed. Test pins the absence of 2>/dev/null in the gate body.

- TestTheBakedGitIdentitySatisfiesACommit could pass on git's
  auto-guessed identity: without user.useConfigOnly, an unconfigured
  git falls back to passwd/hostname and only refuses when the guessed
  email is not an FQDN -- on many CI runners a broken baked gitconfig
  still committed. The commit now runs with user.useConfigOnly=true
  (same as the shell gate) AND asserts the commit's author IS the baked
  identity, so no other identity can satisfy it. Doc comment also notes
  GIT_CONFIG_SYSTEM needs git >= 2.32.

Verified: full go test ./... in remote-worker, build-snapshot.test.sh,
build-rootfs.test.sh, shellcheck -S warning. Mutation tests: broken
gitconfig fails the Go test with and without useConfigOnly (the
identity assertion holds on its own), and removing the guest-side
cleanup fails both new shell checks.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Signed-off-by: Paolo Dettori <dettori@us.ibm.com>
@pdettori
pdettori merged commit 6831e9d into rossoctl:main Oct 8, 2026
18 of 19 checks passed
@pdettori
pdettori deleted the fix/415-sandbox-git-identity branch October 8, 2026 14:00
pdettori added a commit to pdettori/moca that referenced this pull request Oct 8, 2026
…tagged fallback, rossoctl#463 identity

- MI1 §6.6/§12.2: the 403 single_tenant_deployment message no longer
  offers MOCA_TENANCY=multi as the fix. Between S2 and S5 that is the
  posture §6.6 calls not honest, and nothing refuses it at startup, so
  the message makes multi conditional on per-user sandbox owner binding
  (S5 on the container tier) and cites §6.6; the S2 test pins that.
- Both runbooks: if v0.5.1 is not tagged yet, check out rossoctl#465's merge
  commit, which it marks.
- vm-multi-user-demo.md: fix-list row 5 reads Fixed (rossoctl#463), and 4d's
  run record says the failed first commit predates rossoctl#463.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Paolo Dettori <dettori@us.ibm.com>
pdettori added a commit that referenced this pull request Oct 8, 2026
#407)

S2's pin makes MOCA_TENANCY=single serve only the first subject, and
multi is only honest once S5 binds each container sandbox to one owner.
Decide the order: the two-user demo runs at v0.5.1 (tagged on this
change's merge commit, the last release before S2) until S5, then on
main under multi. v0.5.0 predates #463's sandbox git identity, which
the multi-user demo relies on.

- MI1 §6.6 records the sequencing and that setup-vm.sh adds no refusal
  of its own (it cannot tell how many people will log in); §10.3 points
  deploy/vm at it.
- MI1 §12.2 requires an S2 test pinning 403 single_tenant_deployment,
  so the runbooks can quote it.
- Both VM runbooks and deploy/vm/README.md state the tag and the
  refusal code.

Fixes #407

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Paolo Dettori <dettori@us.ibm.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sandbox image: no git identity, so an agent's first git commit fails

2 participants