Skip to content

Fix Mac workspace sync for guest symlinks - #45

Merged
gitcommit90 merged 1 commit into
mainfrom
fix/mac-venv-mirror
Aug 1, 2026
Merged

Fix Mac workspace sync for guest symlinks#45
gitcommit90 merged 1 commit into
mainfrom
fix/mac-venv-mirror

Conversation

@gitcommit90

@gitcommit90 gitcommit90 commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Fixes the release-blocking macOS channel failure where ordinary guest symlinks, especially Python virtual environments, caused command completion, stop, and archive to fail during guest-to-host mirror sync. Guest symlinks now remain durable inside the isolated Linux computer and are omitted from the deliberately symlink-free host mirror. Adds a focused Python-style venv regression and bumps the candidate to 0.0.32.\n\nVerification: npm run typecheck; node --test test/channel-computers.mjs test/desktop.mjs; git diff --check.\n\nSigned-off-by: Joseph Yaksich gitcommit90@users.noreply.github.com

Summary by CodeRabbit

  • New Features

    • Updated the default channel machine image to version 0.0.32.
  • Bug Fixes

    • Prevented guest-created symlinks from crossing into the host workspace mirror.
    • Preserved symlink functionality within the guest environment.
    • Improved workspace archiving to include only regular files and directories.
  • Documentation

    • Added the 0.0.32 release notes and updated documented image versions and comparison links.

Signed-off-by: Joseph Yaksich <gitcommit90@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The release updates version and channel-machine image references to 0.0.32. Workspace export now excludes guest symlinks while preserving ordinary files and directories. Apple channel tests verify guest symlink behavior and host mirror contents.

Changes

Channel image and workspace export

Layer / File(s) Summary
Release version alignment
package.json, README.md, src/server/channel-computers.ts, src/server/db.ts, CHANGELOG.md
The package, documentation, server defaults, and changelog now reference release 0.0.32.
Symlink-safe workspace export
src/server/channel-computers.ts, test/fake-container.mjs
Workspace export uses find -P -xdev to list directories and regular files, then creates a non-recursive archive from that listing.
Workspace mirroring contract
test/channel-computers.mjs
Apple channel tests verify that Python virtual-environment symlinks work in the guest but do not enter the host mirror. The expected image pin is updated to 0.0.32.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the fix and lists some verification, but it omits the required template sections, release notes, change type, and most verification checks. Use the repository template and include the change type, release-note bullets, applicable acceptance evidence, all verification checkboxes, and post-merge items.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main macOS workspace synchronization fix for guest symlinks.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/mac-venv-mirror

Comment @coderabbitai help to get the list of available commands.

@gitcommit90
gitcommit90 merged commit 7a1cca7 into main Aug 1, 2026
5 of 6 checks passed
@gitcommit90
gitcommit90 deleted the fix/mac-venv-mirror branch August 1, 2026 05:34

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/server/channel-computers.ts`:
- Around line 1036-1043: Update the export flow around isolatedInvocation and
validateMirrorTree so symlink exclusion is atomic with archiving: use a
read-only snapshot of /workspace or quiesce guest writes before generating and
archiving the file list, ensuring tar cannot follow a path replaced after find
emits it. Preserve ordinary directory/file export semantics, and add a
regression test covering replacement of an emitted directory with a symlink
during export.

In `@src/server/db.ts`:
- Line 942: Update the channel-computer provisioning flow in src/server/db.ts at
lines 942-942 and src/server/channel-computers.ts at lines 71-71 so existing
rows using image 0.0.31 are migrated to 0.0.32, and explicitly replace or
reprovision their machines rather than only restarting them; preserve INSERT OR
IGNORE behavior for unaffected records and ensure ensureAppleProvisioned uses
the updated image.

In `@test/channel-computers.mjs`:
- Line 108: Update the test around the guest workspace Python path to import and
use lstatSync from node:fs instead of existsSync, so the assertion inspects the
directory entry itself without following a potentially dangling symlink and
verifies that the entry is absent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 27f9dfa8-0f74-4ecf-8d18-67741d8a7ddc

📥 Commits

Reviewing files that changed from the base of the PR and between d2ce524 and 6bb6cd5.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (7)
  • CHANGELOG.md
  • README.md
  • package.json
  • src/server/channel-computers.ts
  • src/server/db.ts
  • test/channel-computers.mjs
  • test/fake-container.mjs

Comment on lines +1036 to +1043
// Guest workspaces legitimately contain symlinks (Python venvs are the
// common case). The host mirror deliberately has no symlink semantics,
// so export only ordinary directories/files. Symlinks remain durable in
// the guest VM and can never be followed across the host boundary.
const invocation = isolatedInvocation([
"/bin/bash", "-lc",
"set -o pipefail; cd /; find -P workspace -xdev \\( -type d -o -type f \\) -print0 | tar --null --no-recursion -T - -cf -",
], computer, "agent", "/workspace");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT

mkdir -p "$tmp/workspace" "$tmp/outside"
printf '%s\n' outside-secret > "$tmp/outside/secret"
ln -s "$tmp/outside" "$tmp/workspace/ancestor"

printf '%s\0' 'workspace/ancestor/secret' |
  (cd "$tmp" && tar --null --no-recursion -T - -cf "$tmp/archive.tar") \
  2>"$tmp/tar.err" || true

if tar -xOf "$tmp/archive.tar" 'workspace/ancestor/secret' 2>/dev/null |
  grep -Fxq outside-secret; then
  echo "FAIL: tar follows a symlink ancestor" >&2
  exit 1
fi

echo "PASS: tar did not archive through the symlink ancestor"

Repository: gitcommit90/1Helm

Length of output: 192


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- target code ---'
sed -n '1000,1075p' src/server/channel-computers.ts

printf '%s\n' '--- validation and export references ---'
rg -n -C 4 'validateMirrorTree|isolatedInvocation|--no-recursion|find -P|mirror' src/server/channel-computers.ts src test tests 2>/dev/null || true

printf '%s\n' '--- tar implementation ---'
tar --version | head -n 2

Repository: gitcommit90/1Helm

Length of output: 50373


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
mkdir -p "$tmp/workspace/ancestor" "$tmp/outside"
printf '%s\n' guest-secret >"$tmp/workspace/ancestor/secret"
printf '%s\n' outside-secret >"$tmp/outside/secret"

(
  cd "$tmp"
  find -P workspace -xdev \( -type d -o -type f \) -print0 >"$tmp/entries"
)

# Model the TOCTOU window between find's path emission and tar's path lookup.
mv "$tmp/workspace/ancestor" "$tmp/workspace/ancestor.real"
ln -s "$tmp/outside" "$tmp/workspace/ancestor"

(
  cd "$tmp"
  tar --null --no-recursion -T "$tmp/entries" -cf "$tmp/archive.tar"
)

if tar -xOf "$tmp/archive.tar" 'workspace/ancestor/secret' 2>/dev/null |
  grep -Fxq outside-secret; then
  echo "FAIL: the exact find/tar sequence archived data through a replaced ancestor"
  exit 1
fi

echo "PASS: the exact find/tar sequence did not archive outside data"

Repository: gitcommit90/1Helm

Length of output: 231


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- synchronization and guest-write context ---'
sed -n '740,885p' src/server/channel-computers.ts
printf '%s\n' '--- terminal and command call sites ---'
sed -n '885,970p' src/server/channel-computers.ts

Repository: gitcommit90/1Helm

Length of output: 15962


Make symlink exclusion atomic with the archive.

find -P passes paths to a separate tar process while active guest commands and terminals can modify /workspace. If a guest process replaces an emitted directory with a symlink, tar --no-recursion -T can archive data outside /workspace. validateMirrorTree cannot detect the source inode that tar read.

Use a read-only snapshot or quiesce guest writes during export. Add a race regression test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/channel-computers.ts` around lines 1036 - 1043, Update the export
flow around isolatedInvocation and validateMirrorTree so symlink exclusion is
atomic with archiving: use a read-only snapshot of /workspace or quiesce guest
writes before generating and archiving the file list, ensuring tar cannot follow
a path replaced after find emits it. Preserve ordinary directory/file export
semantics, and add a regression test covering replacement of an emitted
directory with a symlink during export.

Comment thread src/server/db.ts
const configuredBackend = String(process.env.HELM_CHANNEL_COMPUTER_BACKEND || platformBackend);
const backend = ["apple", "oci", "native", "mock"].includes(configuredBackend) ? configuredBackend : platformBackend;
const image = String(process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.31");
const image = String(process.env.HELM_CHANNEL_MACHINE_IMAGE || "local/1helm-channel-machine:0.0.32");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 5 \
  'DEFAULT_CHANNEL_IMAGE|HELM_CHANNEL_MACHINE_IMAGE|INSERT OR IGNORE INTO channel_computers|UPDATE channel_computers.*image|reprovision|repair' \
  src test

Repository: gitcommit90/1Helm

Length of output: 38056


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- db.ts outline and initialization region ---'
ast-grep outline src/server/db.ts --match 'function $_($$$) { $$$ }' --view summary | head -200
sed -n '700,780p' src/server/db.ts
sed -n '900,980p' src/server/db.ts

printf '%s\n' '--- channel-computers.ts outline ---'
ast-grep outline src/server/channel-computers.ts --match 'function $_($$$) { $$$ }' --view summary | head -260

printf '%s\n' '--- candidate lifecycle and migration code ---'
rg -n -C 8 \
  'channelComputer\(|ensureChannelComputer|reconcileChannelComputers|provision_status|computer\.image|image: computer|image\)|CREATE TABLE.*channel_computers|ALTER TABLE.*channel_computers|PRAGMA user_version|schema|migration' \
  src/server/channel-computers.ts src/server/db.ts src/server/*.ts

Repository: gitcommit90/1Helm

Length of output: 354


🏁 Script executed:

#!/usr/bin/env bash
set -u

printf '%s\n' '--- db.ts initialization region ---'
sed -n '700,780p' src/server/db.ts
sed -n '900,980p' src/server/db.ts

printf '%s\n' '--- channel-computers.ts declarations ---'
ast-grep outline src/server/channel-computers.ts --view signatures | head -260

printf '%s\n' '--- lifecycle and migration references ---'
rg -n -C 8 \
  'channelComputer\(|ensureChannelComputer|reconcileChannelComputers|provision_status|computer\.image|image: computer|CREATE TABLE.*channel_computers|ALTER TABLE.*channel_computers|PRAGMA user_version|migration|schema' \
  src/server/channel-computers.ts src/server/db.ts src/server/*.ts

Repository: gitcommit90/1Helm

Length of output: 50374


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- record creation and provisioning ---'
sed -n '120,230p' src/server/channel-computers.ts
sed -n '583,805p' src/server/channel-computers.ts

printf '%s\n' '--- reconciliation and update paths ---'
sed -n '1310,1515p' src/server/channel-computers.ts

printf '%s\n' '--- all channel_computers image writes and image consumers ---'
rg -n -C 3 \
  'channel_computers.*image|SET[^"\n]*image|image[^"\n]*channel_computers|ensureAppleProvisioned|ensureOciProvisioned|provisionChannelComputer|reconcileOne' \
  src/server/db.ts src/server/channel-computers.ts

Repository: gitcommit90/1Helm

Length of output: 43491


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

python3 - <<'PY'
import sqlite3

db = sqlite3.connect(":memory:")
db.executescript("""
CREATE TABLE channel_computers (
  channel_id INTEGER PRIMARY KEY,
  image TEXT NOT NULL
);
INSERT INTO channel_computers VALUES (7, 'local/1helm-channel-machine:0.0.31');
""")
db.execute(
    """INSERT OR IGNORE INTO channel_computers(channel_id, image)
       VALUES (?, ?)""",
    (7, "local/1helm-channel-machine:0.0.32"),
)
row = db.execute(
    "SELECT channel_id, image FROM channel_computers WHERE channel_id=7"
).fetchone()
print({"after_insert_or_ignore": row})

db.execute(
    "UPDATE channel_computers SET image=? WHERE channel_id=?",
    ("local/1helm-channel-machine:0.0.32", 7),
)
row = db.execute(
    "SELECT channel_id, image FROM channel_computers WHERE channel_id=7"
).fetchone()
print({"after_explicit_update": row})
PY

printf '%s\n' '--- image versions and machine replacement controls ---'
rg -n -C 3 \
  '0\.0\.3[01-9]|image-contract|machine (create|delete|rm|stop|run)|container (create|rm|stop|start)|DELETE FROM channel_computers|UPDATE channel_computers SET[^;]*image' \
  src container scripts test

Repository: gitcommit90/1Helm

Length of output: 17475


Migrate existing channel computers to image 0.0.32.

INSERT OR IGNORE preserves existing channel_computers.image values. ensureAppleProvisioned uses the stored image only when it creates a missing machine. Update existing rows and explicitly replace or reprovision machines that still use 0.0.31; restarting them does not load the new image.

📍 Affects 2 files
  • src/server/db.ts#L942-L942 (this comment)
  • src/server/channel-computers.ts#L71-L71
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/db.ts` at line 942, Update the channel-computer provisioning flow
in src/server/db.ts at lines 942-942 and src/server/channel-computers.ts at
lines 71-71 so existing rows using image 0.0.31 are migrated to 0.0.32, and
explicitly replace or reprovision their machines rather than only restarting
them; preserve INSERT OR IGNORE behavior for unaffected records and ensure
ensureAppleProvisioned uses the updated image.

result = await computers.runChannelCommand(alpha.channelId, "mkdir -p .venv/bin; ln -s /usr/bin/python3 .venv/bin/python3; ln -s python3 .venv/bin/python; printf venv-ok > .venv/marker");
assert.equal(result.exit_code, 0, "a normal Python-style venv does not make the command fail during mirror sync");
assert.equal(readFileSync(join(dataDir, "channels", String(alpha.channelId), "workspace", ".venv", "marker"), "utf8"), "venv-ok");
assert.equal(existsSync(join(dataDir, "channels", String(alpha.channelId), "workspace", ".venv", "bin", "python3")), false, "guest symlinks stay in the VM instead of crossing the host mirror boundary");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

node <<'NODE'
const { existsSync, lstatSync, mkdtempSync, rmSync, symlinkSync } = require("node:fs");
const { join } = require("node:path");
const { tmpdir } = require("node:os");

const dir = mkdtempSync(join(tmpdir(), "symlink-test-"));
const link = join(dir, "python3");

try {
  symlinkSync("/definitely-missing-python3", link);
  if (existsSync(link)) throw new Error("existsSync unexpectedly returned true");
  if (!lstatSync(link).isSymbolicLink()) throw new Error("lstatSync did not detect the symlink");
} finally {
  rmSync(dir, { recursive: true, force: true });
}
NODE

Repository: gitcommit90/1Helm

Length of output: 155


🏁 Script executed:

sed -n '1,125p' test/channel-computers.mjs

Repository: gitcommit90/1Helm

Length of output: 10019


🏁 Script executed:

rg -n -C 5 'symlink|mirror|lstat|existsSync|workspace' src test/fake-container.mjs

Repository: gitcommit90/1Helm

Length of output: 50374


🏁 Script executed:

fd -i 'channel-computers|fake-container' src test

Repository: gitcommit90/1Helm

Length of output: 324


🏁 Script executed:

rg -n -C 8 'sync|mirror|symlink|workspace|files' src/server/channel-computers.ts test/fake-container.mjs

Repository: gitcommit90/1Helm

Length of output: 50374


🏁 Script executed:

sed -n '808,980p' src/server/channel-computers.ts

Repository: gitcommit90/1Helm

Length of output: 11571


🏁 Script executed:

rg -n -A 90 'function syncGuestToHostUnlocked|validateMirrorTree' src/server/channel-computers.ts

Repository: gitcommit90/1Helm

Length of output: 18663


Inspect the directory entry instead of following the symlink.

existsSync follows symlinks and returns false for a dangling symlink. Use lstatSync and assert that the entry itself is absent. Import lstatSync from node:fs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/channel-computers.mjs` at line 108, Update the test around the guest
workspace Python path to import and use lstatSync from node:fs instead of
existsSync, so the assertion inspects the directory entry itself without
following a potentially dangling symlink and verifies that the entry is absent.

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