feat(sync): per-path sync direction (spec.sync.paths[].direction: bi|up|down)#155
Merged
Conversation
…up|down)
A single bidirectional folder carries both inputs (code, local->pod) and
outputs (results, pod->local), so a bad local write can clobber
pod-generated data. Finer per-subpath direction inside one folder is
impossible in Syncthing (.stignore lives in the directory; per-path
folder types were rejected upstream), so direction is a per-mapping
(per-folder) setting.
Config: spec.sync.paths entries now accept a structured form alongside
the compact "local:remote" string (which stays fully backward
compatible and marshals back unchanged):
paths:
- local: .
remote: /workspace
direction: down # bi (default) | up | down
up = local sendonly / pod receiveonly (code push-only; nothing on the
pod can alter local files). down = pod sendonly / local receiveonly
(the pod is the authority; a local write can never clobber pod
results). The schema is per-path from day one so lifting the current
single-mapping limit later will not change config shape.
Plumbing: syncengine.Pair carries the resolved direction;
`okdev sync --mode` defaults to the configured path's direction with an
explicit flag winning; `okdev up` starts sync in that direction and
skips the two-phase bootstrap for directional folders (one side is
already sendonly, so the final folder types are safe from the first
start). Direction participates in the sync config hash (explicit bi
hashes like the legacy default, so upgrades do not restart existing
sessions). `up --reset-workspace` and `sync reset-remote` are rejected
under "down": they reseed the remote from local, which would sync back
as deletions over the pod's data. `status --details` shows the
direction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
a0ba5a4 to
f2725ee
Compare
Four scenarios on the live smoke session: rewriting the sync path entry to the structured form with direction "up" takes effect on the next `okdev up` (config-hash driven restart, verified via status --details); up-direction still pushes local edits while pod-side writes never flow back; down-direction returns pod-generated results while a stray local truncation cannot clobber the pod's copy (the reported data-loss incident, reproduced and asserted harmless); and `up --reset-workspace` is rejected under "down" with the remote file left intact. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Structural fix for the bidirectional-sync data-loss class (a stray local write — e.g. an empty file from a failed redirect — propagating over pod-generated results). Versioning (#147) remains the safety net; this makes the direction a design choice.
Config shape: per-path from day one
spec.sync.pathsentries accept a structured form alongside the compact string (fully backward compatible; plain mappings marshal back to the string form):up— local sendonly / pod receiveonly: code push-only; nothing on the pod can alter local files.down— pod sendonly / local receiveonly: the pod is the authority; a local write can never clobber pod results.The single-mapping limit is unchanged for now, but the schema is per-path so lifting it later (multiple disjoint folders, each with its own direction) won't change config shape.
Why per-folder, not per-subpath
A previous attempt (remoteIgnore, #70) was removed in #81: Syncthing stores
.stignorein the directory itself, and per-path folder types were explicitly rejected upstream (syncthing/syncthing#8061). Direction is therefore a property of a mapping's whole folder.Plumbing
syncengine.Paircarries the resolved direction (never empty).okdev sync --modedefaults to the configured path's direction; an explicit flag overrides per-invocation. Mode values validated (two-phasestays internal).okdev upstarts sync in the configured direction; directional folders skip the two-phase bootstrap (it only exists to protect the remote before flipping to sendreceive — with up/down one side is already sendonly from the start). Direction participates in the sync config hash; explicitbihashes like the legacy empty value, so upgrading doesn't restart existing sessions.up --reset-workspaceandsync reset-remoteerror out underdown— both reseed the remote from local, which would sync back as deletions over the pod's data.status --detailsshowsdirection: down (<-); JSON gainssync.direction. Mesh receivers stay receiveonly regardless.okdev cp/jobs logs), command-reference, multipod skill reference.Testing
syncStartModedirectional cases, config-hash direction sensitivity (incl. bi≡legacy), status rendering.gofmt -lclean;go test -race ./...passes. E2E Kind previously green on this branch for the defaultbipath; re-dispatching after the rework.🤖 Generated with Claude Code