Skip to content

feat(mount): add deterministic pull-only sync - #431

Merged
khaliqgant merged 3 commits into
mainfrom
fix/mount-pull-only
Aug 18, 2026
Merged

feat(mount): add deterministic pull-only sync#431
khaliqgant merged 3 commits into
mainfrom
fix/mount-pull-only

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Aug 18, 2026

Copy link
Copy Markdown
Member

Problem

Chief mounts several Relayfile provider subtrees with read-only scopes: GitHub, Notion, and Digests. Relayfile currently offers mirror mode (pull plus writeback) and write-only mode, but no remote-to-local-only mode.

Running those read-scoped subtrees in mirror mode still enters write-producing paths such as local reconciliation, durable outbox dispatch, and digest writes. The provider rejects those attempts with 403. Because the cycle fails before Relayfile records a successful reconcile and advances its durable state, the next supervisor interval repeats expensive recovery/full-tree work instead of staying on the cheap incremental path.

This is especially damaging for the GitHub projection: the local tree is roughly 1.5 GiB and 141,000 files in the affected Chief installation. Repeated scans and materialization make the system mount and interactive work crawl. Live credentials are not the problem; the credentials are intentionally read-scoped. The missing primitive is a deterministic mode that permits remote reads while making every remote mutation impossible.

Increasing the interval or request timeout would only reduce the frequency of the loop. Inferring read-only behavior from individual token checks is also insufficient because Relayfile has multiple independent mutation paths. The mode must be enforced centrally and defensively at every writer.

Fix

  • add pull-only sync: remote bootstrap, incremental events, polling, and WebSocket maintenance continue
  • disable local watchers, local scanning/writeback, digest-generated writes, outbox dispatch, merge attempts, and remote deletes
  • reject explicit outbox-flush and push-local one-shot commands
  • force materialized files to read-only permissions even if the token contains broader scopes
  • preserve stale outbox records without dispatching them, so changing modes does not silently destroy queued work
  • report pull-only in private state, public .relay/state.json, CLI help, and the TypeScript SDK
  • retain WebSocket cadence without requiring a local watcher, avoiding unnecessary full reconcile cadence

Review feedback addressed

  • Pull-only FUSE was unsafe because the FUSE mutation handlers do not consume the sync mode. The CLI now rejects --mode=fuse --sync-mode=pull-only before acquiring a mount lease or dispatching a runner, and the TypeScript SDK rejects the combination before making an HTTP request. Use poll mode for pull-only mounts.
  • An existing mirror could enter pull-only on a quiet events cursor while its unchanged files remained writable. Loading a mirror-mode state as pull-only now performs a one-time pass over tracked regular files and applies read-only permissions before reconciliation.
  • Returning from pull-only to mirror now recomputes each tracked file's permission from the current scopes, restoring write-scoped files while keeping read-scoped siblings read-only.
  • Non-regular tracked targets are inspected with Lstat, logged, and skipped. A directory or symlink replacement can no longer wedge the transition, and symlinks are never followed by chmod.
  • Regression tests cover the completed-mirror quiet cursor, permission round-trip with mixed scopes, successful writeback after returning to mirror, and directory/symlink replacements.

Why this is deterministic

The safety property comes from the selected sync mode, not from an LLM decision, network error interpretation, or a best-effort scope check. Pull-only short-circuits every remote mutation entry point, while read-side reconciliation remains unchanged.

Drawbacks and sandbox impact

  • Existing callers remain in mirror mode by default. This PR does not silently change ordinary sandbox behavior.
  • Pull-only is poll-mount-only. FUSE callers must continue using mirror mode until mountfuse has a complete read-only implementation.
  • Switching a large existing mirror into pull-only performs one metadata pass over all tracked files. On a 141k-file tree that creates one-time startup I/O, but avoids the recurring scans/full pulls that caused the crawl.
  • POSIX 0444 is a usability guard, not a security boundary. A privileged sandbox process can chmod a file, but the daemon still will not write it back. A later remote refresh may overwrite that local edit.
  • Local drafts and pre-existing outbox records are intentionally not delivered in pull-only. They remain local/durable. If the same tree later returns to mirror mode, operators must decide whether those local files should be discarded or allowed to enter writeback.
  • Sandbox teardown flows that expect --flush-outbox-once or --push-local-once to deliver handler output are incompatible with pull-only and will fail loudly. Pull-only is appropriate for context/research sandboxes; action-capable sandboxes need mirror mode or a separate read-only lifecycle that skips writeback cleanup.
  • Remote freshness still comes from WebSocket events or polling. Pull-only itself keeps WebSockets supported; a caller that disables WebSockets accepts its configured polling delay.
  • Live credentials and credential refresh remain in use. Pull-only changes allowed operations, not authentication.

The current AgentWorkforce/sandbox launcher does not pass a sync mode and therefore remains mirror. Its lifecycle explicitly detects local changes and upgrades teardown to --push-local-once, so enabling pull-only there requires an explicit lifecycle policy change rather than changing a default.

QA

  • go test ./...
  • npm test -- --run src/setup.test.ts src/mount-launcher.test.ts (69 SDK tests)
  • npm run typecheck (packages/sdk/typescript)
  • git diff --check

The regression suite proves that:

  1. remote content is materialized locally
  2. mirrored files have no write bits
  3. a local edit and a new local draft never reach the remote
  4. a stale pending outbox record is not dispatched or deleted
  5. no bulk write, merge, or remote delete occurs
  6. explicit write one-shots are rejected
  7. public and private state both report pull-only
  8. an existing quiet mirror becomes read-only without waiting for a remote event
  9. pull-only FUSE is rejected in both the CLI and SDK
  10. returning to mirror restores only scope-authorized write permissions and writeback works again
  11. directory and symlink replacements do not block healthy files or chmod an external symlink target

End-to-end dogfood check

  1. Start relayfile-mount with --mode poll --sync-mode pull-only against a read-scoped subtree.
  2. Confirm the initial remote tree materializes and .relay/state.json reports syncMode: "pull-only".
  3. Confirm subsequent remote changes arrive through events/polling.
  4. Temporarily make a mirrored file writable, edit it, and create a local draft.
  5. Confirm no write, bulk write, merge, or delete reaches Relayfile and the remote content remains unchanged.
  6. Confirm Relayfile writeback flush and the push-local one-shot reject the mount.
  7. Leave the daemon running across multiple intervals and confirm successful reconcile timestamps/cursors advance without repeated 403/full-pull recovery.
  8. For an existing mirror, switch to pull-only with no new events and confirm tracked files become read-only during the first cycle.
  9. Switch back to mirror and confirm write-scoped files become writable again while read-scoped files remain read-only.

@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 13dceba7-41bc-4264-ad4b-da4b56d8c727

📥 Commits

Reviewing files that changed from the base of the PR and between 07397c3 and a007973.

📒 Files selected for processing (9)
  • cmd/relayfile-mount/main.go
  • cmd/relayfile-mount/main_test.go
  • docs/integrations/SDK-SURFACE.md
  • internal/mountsync/syncer.go
  • internal/mountsync/syncer_test.go
  • packages/sdk/typescript/src/mount-launcher.ts
  • packages/sdk/typescript/src/setup-types.ts
  • packages/sdk/typescript/src/setup.test.ts
  • packages/sdk/typescript/src/setup.ts

Included review availability: Your plan includes up to 1 review per rolling hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The change adds pull-only as a supported mount synchronization mode. Pull-only mounts keep remote synchronization active, disable local watchers and writeback, reject local push operations, and report their mode in public state. SDK and CLI tests cover the new behavior.

Changes

Pull-only synchronization

Layer / File(s) Summary
SDK sync-mode contract
packages/sdk/typescript/src/setup-types.ts, packages/sdk/typescript/src/setup.ts, packages/sdk/typescript/src/mount-launcher.ts, packages/sdk/typescript/src/setup.test.ts, docs/integrations/SDK-SURFACE.md
The SDK and integration contract accept pull-only. The launcher receives the mode, while the mount-session request remains unchanged.
CLI mode resolution and watcher behavior
cmd/relayfile-mount/main.go, cmd/relayfile-mount/main_test.go
The CLI accepts and documents pull-only. Pull-only mounts skip local watchers and retain WebSocket reconciliation cadence.
Pull-only synchronization enforcement
internal/mountsync/syncer.go
The syncer pulls remote content while skipping local scanning, writeback, digest jobs, and outbox delivery. Local writes and push operations are rejected. Public state reports pull-only.
Pull-only behavior validation
internal/mountsync/syncer_test.go
Tests verify remote hydration, read-only files, suppressed writes, preserved outbox records, rejected push operations, and persisted pull-only state.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: ⚪ Minimal · up to a0079

This PR adds a pull-only synchronization mode and exposes it across the CLI and SDK; no actionable merge-blocking risk remains after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant MountCLI
  participant Syncer
  participant RemoteWorkspace
  participant LocalFilesystem
  MountCLI->>Syncer: configure pull-only mode
  Syncer->>RemoteWorkspace: reconcile remote changes
  RemoteWorkspace-->>Syncer: return remote content
  Syncer->>LocalFilesystem: write read-only files
  Syncer-->>MountCLI: report pull-only state
Loading

Poem

A rabbit checks the mount at dawn,
Pulls fresh files while writes are gone.
The watcher naps, the socket sings,
No local push disturbs the springs.
Read-only paws keep data bright.
Hop, hop—sync stays light!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly summarizes the main change: adding deterministic pull-only mount synchronization.
Description check ✅ Passed The description directly explains the pull-only mode, its safety behavior, affected interfaces, tests, and operational limitations.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/mount-pull-only

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown

Relayfile Eval Review

Run: .relayfile/evals/runs/2026-08-18T08-46-56-765Z-HEAD-provider
Mode: provider
Git SHA: 262ed41

Passed: 4 | Needs human: 0 | Reviewable: 0 | Missing output: 0 | Failed: 0 | Skipped: 0

Human Review Cases

No reviewable human-review cases captured Relayfile output.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a007973b0d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cmd/relayfile-mount/main.go
Comment thread internal/mountsync/syncer.go

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 9 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread internal/mountsync/syncer.go
Comment thread cmd/relayfile-mount/main.go

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 7 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread internal/mountsync/syncer.go Outdated
Comment thread internal/mountsync/syncer.go Outdated
@khaliqgant
khaliqgant merged commit b3a06ff into main Aug 18, 2026
10 checks passed
@khaliqgant
khaliqgant deleted the fix/mount-pull-only branch August 18, 2026 08:49
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