Skip to content

Fix command wrapper symlink clobber#184

Open
LiranCohen wants to merge 9 commits into
mainfrom
fix/install-wrapper-symlink-clobber
Open

Fix command wrapper symlink clobber#184
LiranCohen wants to merge 9 commits into
mainfrom
fix/install-wrapper-symlink-clobber

Conversation

@LiranCohen

Copy link
Copy Markdown
Contributor

Summary

  • unlink existing install destinations before writing install.sh wrappers, so stale symlinks cannot redirect shell output into package files
  • make gitd setup install executable wrappers instead of symlinks to package internals
  • update setup checks/tests for wrapper-based helpers
  • add a patch changeset

Root cause

If ~/.gitd/bin/git-remote-did already existed as a symlink from a previous gitd setup, the installer's shell redirection followed that symlink and overwrote dist/esm/git-remote/main.js with wrapper text. Git then invoked the helper and Bun tried to parse exec ... as JavaScript.

Verification

  • bash -n install.sh
  • git diff --check
  • bun run build
  • bun run lint
  • HOME=$(mktemp -d)/home ENBOX_HOME=$(mktemp -d)/enbox bun test tests/cli.spec.ts -t setup
  • temp symlink-clobber reproduction: precreated ~/.gitd/bin/git-remote-did as a symlink, ran installer, confirmed symlink target remained unchanged and wrapper replaced the symlink
  • built setup wrapper smoke: bun dist/esm/cli/main.js setup --bin-dir <tmp> then setup --check

@LiranCohen LiranCohen mentioned this pull request Jun 24, 2026
LiranCohen and others added 5 commits June 23, 2026 20:53
…mpts (Phase 1) (#187)

* feat(auth): durable vault-unlock secret to stop repeated password prompts

Resolve the vault password through one path (explicit -> GITD_PASSWORD ->
public-reader -> durable secret store -> prompt) and persist a freshly entered
secret per profile in a durable store: the OS keychain (macOS security, Linux
secret-tool) when available, otherwise a machine-keyed AES-256-GCM encrypted
file under ~/.enbox/secrets/. After the first unlock, commands restore the
session without re-prompting for the vault password.

GITD_SECRET_BACKEND=keychain|file|none pins or disables the backend. A cached
secret that is later rejected is cleared automatically to avoid lock-out;
gitd auth logout / reset drop the stored secret.

Part of #186 (Phase 1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style: fix import order in vault-password.spec (sort-imports)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…188)

* feat(auth): durable vault-unlock secret to stop repeated password prompts

Resolve the vault password through one path (explicit -> GITD_PASSWORD ->
public-reader -> durable secret store -> prompt) and persist a freshly entered
secret per profile in a durable store: the OS keychain (macOS security, Linux
secret-tool) when available, otherwise a machine-keyed AES-256-GCM encrypted
file under ~/.enbox/secrets/. After the first unlock, commands restore the
session without re-prompting for the vault password.

GITD_SECRET_BACKEND=keychain|file|none pins or disables the backend. A cached
secret that is later rejected is cleared automatically to avoid lock-out;
gitd auth logout / reset drop the stored secret.

Part of #186 (Phase 1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style: fix import order in vault-password.spec (sort-imports)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(auth): make git push/fetch work without a running helper

getVaultPassword() now consults the durable secret store (OS keychain /
encrypted file) between the public-reader secret and the /dev/tty prompt, so the
credential helper's direct-signing fallback and the daemon auto-start are
non-interactive after the first unlock. git push / git fetch succeed even when
no local helper is running, and a dead or never-started helper no longer blocks
pushes.

Lockfiles are written atomically (temp + rename) to avoid a corrupt lockfile on
a crash mid-write.

Part of #186 (Phase 1b).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The OS keychain is machine-global (service `gitd`, account `vault:<profile>`)
and is NOT scoped to ENBOX_HOME, so a secret cached by one test leaked into
later tests that expect "no password" — making resolveGitEndpoint auto-start /
resolve a daemon instead of rejecting (CI showed it resolving to port 9418).

When ENBOX_HOME is overridden (tests, sandboxes, throwaway installs) and no
backend is explicitly pinned, use the encrypted file backend, which lives under
ENBOX_HOME and stays isolated. The default home (production) still uses the
keychain; GITD_SECRET_BACKEND overrides everything.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The resolveGitEndpoint reject / skip-local-daemon tests read the shared
~/.enbox daemon lockfile and depended on daemon auto-start failing. In CI,
concurrent test files (e.g. daemon-lifecycle.spec.ts) race on that lockfile and
auto-start behaves differently, so these tests resolved instead of rejecting.

- Isolate ENBOX_HOME per test so no leaked lockfile is read.
- Add a __setDaemonStarterForTests seam in resolve.ts (defaults to ensureDaemon
  — no production behavior change) and disable auto-start in the no-daemon tests
  so they reject deterministically regardless of port/daemon/TTY state.

Verified locally and under setsid (no controlling terminal, like CI).

Part of #186.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant