workspace: drop --readonly flag — advisory only against root guests

--readonly ran `chmod -R a-w` over the workspace after copying, but
every banger guest boots as root, and root bypasses DAC mode checks.
So a user running `vm workspace prepare ... --readonly` got the
mode bits set to 0444 but `echo x >> file` in the guest still
succeeded. The flag promised enforcement it couldn't deliver.

The feature also doesn't match the product model: workspaces are
prepared precisely so the guest CAN edit them, and `workspace
export` exists to pull those edits back as a patch. A
"read-only workspace" contradicts that loop.

Removed:
  - CLI flag `--readonly` on `vm workspace prepare`
  - api.VMWorkspacePrepareParams.ReadOnly field
  - model.WorkspacePrepareResult.ReadOnly field
  - daemon chmod dispatch in prepareVMWorkspaceGuestIO
  - smoke scenario pinning the (advisory) mode-bit behavior
  - misleading "exportbox-readonly" VM name in an unrelated export
    test (the test is about not mutating the real git index;
    renamed to exportbox-noindex-mutation)

If real enforcement becomes a user need later, the right primitive
is `chattr +i` (immutable bit — root CAN'T write) or a ro bind-mount.
Reintroducing a new flag is cheaper than debugging what the current
one actually guarantees.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Thales Maciel 2026-04-23 13:04:33 -03:00
parent bafe816fc7
commit 235758e5b2
No known key found for this signature in database
GPG key ID: 33112E6833C34679
6 changed files with 6 additions and 54 deletions

View file

@ -583,7 +583,6 @@ func (d *deps) newVMWorkspacePrepareCommand() *cobra.Command {
var branchName string
var fromRef string
var mode string
var readOnly bool
var includeUntracked bool
var dryRun bool
cmd := &cobra.Command{
@ -594,7 +593,7 @@ func (d *deps) newVMWorkspacePrepareCommand() *cobra.Command {
ValidArgsFunction: d.completeVMNameOnlyAtPos0,
Example: strings.TrimSpace(`
banger vm workspace prepare devbox
banger vm workspace prepare devbox ../repo --guest-path /root/repo --readonly
banger vm workspace prepare devbox ../repo --guest-path /root/repo
banger vm workspace prepare devbox ../repo --mode full_copy
`),
RunE: func(cmd *cobra.Command, args []string) error {
@ -634,7 +633,6 @@ func (d *deps) newVMWorkspacePrepareCommand() *cobra.Command {
Branch: branchName,
From: prepareFrom,
Mode: mode,
ReadOnly: readOnly,
IncludeUntracked: includeUntracked,
})
if err != nil {
@ -647,7 +645,6 @@ func (d *deps) newVMWorkspacePrepareCommand() *cobra.Command {
cmd.Flags().StringVar(&branchName, "branch", "", "create and switch to a new guest branch")
cmd.Flags().StringVar(&fromRef, "from", "HEAD", "base ref for --branch")
cmd.Flags().StringVar(&mode, "mode", string(model.WorkspacePrepareModeShallowOverlay), "workspace mode: shallow_overlay, full_copy, metadata_only")
cmd.Flags().BoolVar(&readOnly, "readonly", false, "make the prepared workspace read-only")
cmd.Flags().BoolVar(&includeUntracked, "include-untracked", false, "also copy untracked non-ignored files into the guest workspace (default: tracked files only)")
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "list the files that would be copied and exit without touching the guest")
return cmd