Skip to content

sandbox upload dereferences symlinks — uploaded as real directories, breaking git #1425

Closed

Description

@rh-hemartin

Agent Diagnostic

Investigated by running the reproduction steps manually using openshell sandbox upload against a minimal git repository containing a symlink. Confirmed the symlink is absent in the sandbox and replaced by a regular directory. Tested on v0.0.36 and v0.0.42 — both exhibit the same behavior. Could not resolve — this requires a fix in the upload tar-building logic.

Description

openshell sandbox upload resolves symlinks in the source tree when building the tar archive. Symlinks arrive in the sandbox as regular directories (their targets, expanded inline). Git tracks them as symlink blobs, so git status in the sandbox immediately reports every symlink path as deleted:.

What should happen: Symlinks should be preserved as symlinks in the sandbox, matching the source tree exactly.

Reproduction Steps — v0.0.36

# 1. Download openshell v0.0.36 (Linux x86_64)
curl -L https://github.com/NVIDIA/OpenShell/releases/download/v0.0.36/openshell-x86_64-unknown-linux-musl.tar.gz \
  | tar -xz -C /tmp/openshell-036 --one-top-level
chmod +x /tmp/openshell-036/openshell

# 2. Create a minimal git repo with a symlink
mkdir /tmp/symlink-repro && cd /tmp/symlink-repro
git init -q
mkdir real-dir && echo "hello" > real-dir/file.txt
ln -s real-dir link-dir
git add . && git commit -qm "init"

# 3. Create a sandbox (requires a running gateway)
/tmp/openshell-036/openshell sandbox create --name symlink-test --keep --no-tty -- true

# 4. Upload the repo
/tmp/openshell-036/openshell sandbox upload symlink-test /tmp/symlink-repro /sandbox/repo

# 5. Check for symlinks — expect one, get none
/tmp/openshell-036/openshell sandbox exec --name symlink-test --no-tty -- /usr/bin/find /sandbox/repo -type l
# (no output)

# 6. Confirm link-dir is a real directory instead of a symlink
/tmp/openshell-036/openshell sandbox exec --name symlink-test --no-tty -- /usr/bin/ls -la /sandbox/repo/symlink-repro/

# 7. Clean up
/tmp/openshell-036/openshell sandbox delete symlink-test

Reproduction Steps — v0.0.42 (standalone gateway)

# 1. Download openshell v0.0.42 binaries (Linux x86_64)
mkdir /tmp/openshell-042
curl -L https://github.com/NVIDIA/OpenShell/releases/download/v0.0.42/openshell-x86_64-unknown-linux-musl.tar.gz \
  | tar -xz -C /tmp/openshell-042
curl -L https://github.com/NVIDIA/OpenShell/releases/download/v0.0.42/openshell-gateway-x86_64-unknown-linux-gnu.tar.gz \
  | tar -xz -C /tmp/openshell-042
curl -L https://github.com/NVIDIA/OpenShell/releases/download/v0.0.42/openshell-sandbox-x86_64-unknown-linux-gnu.tar.gz \
  | tar -xz -C /tmp/openshell-042
chmod +x /tmp/openshell-042/openshell /tmp/openshell-042/openshell-gateway /tmp/openshell-042/openshell-sandbox

# 2. Start the standalone gateway (Podman driver, no TLS for local testing)
OPENSHELL_SSH_HANDSHAKE_SECRET=test-secret \
  /tmp/openshell-042/openshell-gateway \
  --disable-tls \
  --disable-gateway-auth \
  --drivers podman \
  --db-url sqlite:///tmp/openshell-042-test.db \
  --port 8081 \
  --bind-address 0.0.0.0 \
  --docker-supervisor-bin /tmp/openshell-042/openshell-sandbox \
  &

# 3. Register and select the gateway
/tmp/openshell-042/openshell gateway add http://127.0.0.1:8081 --name gw-042
/tmp/openshell-042/openshell gateway select gw-042

# 4. Create the same minimal git repo (skip if already done above)
mkdir -p /tmp/symlink-repro && cd /tmp/symlink-repro
git init -q
mkdir -p real-dir && echo "hello" > real-dir/file.txt
ln -sf real-dir link-dir
git add . && git commit -qm "init" 2>/dev/null || true

# 5. Create a sandbox
/tmp/openshell-042/openshell sandbox create --name symlink-test --keep --no-tty -- true

# 6. Upload the repo
/tmp/openshell-042/openshell sandbox upload symlink-test /tmp/symlink-repro /sandbox/repo

# 7. Check for symlinks — expect one, get none
/tmp/openshell-042/openshell sandbox exec --name symlink-test --no-tty -- /usr/bin/find /sandbox/repo -type l
# (no output)

# 8. Confirm link-dir is a real directory
/tmp/openshell-042/openshell sandbox exec --name symlink-test --no-tty -- /usr/bin/ls -la /sandbox/repo/symlink-repro/

# 9. Clean up
/tmp/openshell-042/openshell sandbox delete symlink-test
kill %1

Expected output (step 5 / step 7)

/sandbox/repo/symlink-repro/link-dir

Actual output (step 5 / step 7)

(empty)

ls -la shows link-dir as a regular directory in both versions:

drwxr-xr-x. 1 sandbox sandbox 16 ... link-dir
drwxr-xr-x. 1 sandbox sandbox 16 ... real-dir

Environment

  • openshell: 0.0.36 and 0.0.42 (both affected)
  • OS: Linux x86_64
  • Driver: Podman (rootless)

Impact

Any workflow uploading a git repo with symlinks will see those paths reported as deleted: by git status inside the sandbox, even though the working tree is otherwise intact. This causes spurious git noise and can lead agents to attempt restoring files that were never deleted.

Agent-First Checklist

  • I pointed my agent at the repo and had it investigate this issue
  • I loaded relevant skills (e.g., debug-openshell-cluster, debug-inference, openshell-cli)
  • My agent could not resolve this — the diagnostic above explains why

Activity

  1. omribz156 commented on May 20, 2026

    @omribz156

    I traced this before changing code. The upload archive path appears to be in crates/openshell-cli/src/ssh.rs, inside write_upload_archive.

    The current archive builder uses Path::is_file() / Path::is_dir() and then tar::Builder::append_path_with_name / append_dir_all. That lines up with the repro here: those path checks follow symlinks, and append_dir_all archives the target tree instead of preserving the symlink entry.

    There is already a symlink-preserving archive pattern elsewhere in the repo: crates/openshell-driver-vm/src/rootfs.rs uses fs::symlink_metadata, fs::read_link, a tar::Header, and append_link, with a regression test named create_rootfs_archive_preserves_broken_symlinks. The likely small fix is to bring that style into the CLI upload archive builder for both SinglePath and FileList, then add a unit test near the existing upload_archive_paths tests that asserts a symlink archive entry keeps its link target instead of expanding to a directory.

    I did not open a PR because OpenShell's vouch gate auto-closes first-time external PRs, and I am not listed in .github/VOUCHED.td. Leaving the source trace here so the next person does not have to rediscover the tar path.

    This was traced with Codex assistance, with the final note manually checked against the current source files.

  2. removed
    state:triage-neededOpened without agent diagnostics and needs triage
    on Oct 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions