Skip to content

feat(sync): per-path sync direction (spec.sync.paths[].direction: bi|up|down)#155

Merged
acmore merged 2 commits into
mainfrom
feat/sync-direction-config
Jul 5, 2026
Merged

feat(sync): per-path sync direction (spec.sync.paths[].direction: bi|up|down)#155
acmore merged 2 commits into
mainfrom
feat/sync-direction-config

Conversation

@acmore

@acmore acmore commented Jul 5, 2026

Copy link
Copy Markdown
Owner

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.paths entries accept a structured form alongside the compact string (fully backward compatible; plain mappings marshal back to the string form):

sync:
  paths:
    - .:/workspace              # compact form, direction defaults to bi
    # or:
    - local: .
      remote: /workspace
      direction: down           # bi | 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 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 .stignore in 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.Pair carries the resolved direction (never empty).
  • okdev sync --mode defaults to the configured path's direction; an explicit flag overrides per-invocation. Mode values validated (two-phase stays internal).
  • okdev up starts 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; explicit bi hashes like the legacy empty value, so upgrading doesn't restart existing sessions.
  • Guards: up --reset-workspace and sync reset-remote error out under down — both reseed the remote from local, which would sync back as deletions over the pod's data.
  • status --details shows direction: down (<-); JSON gains sync.direction. Mesh receivers stay receiveonly regardless.
  • Docs: config-manifest direction guide (incl. keeping generated outputs out of the synced path, fetched via okdev cp / jobs logs), command-reference, multipod skill reference.

Testing

  • Unit: YAML union decode/round-trip (compact + structured + malformed), per-path direction validation, ParsePairs direction normalization, mode resolution/validation precedence, syncStartMode directional cases, config-hash direction sensitivity (incl. bi≡legacy), status rendering.
  • gofmt -l clean; go test -race ./... passes. E2E Kind previously green on this branch for the default bi path; re-dispatching after the rework.

🤖 Generated with Claude Code

…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>
@acmore acmore force-pushed the feat/sync-direction-config branch from a0ba5a4 to f2725ee Compare July 5, 2026 09:58
@acmore acmore changed the title feat(sync): configurable sync direction (spec.sync.direction: bi|up|down) feat(sync): per-path sync direction (spec.sync.paths[].direction: bi|up|down) Jul 5, 2026
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>
@acmore acmore merged commit 8a33a01 into main Jul 5, 2026
2 checks passed
@acmore acmore deleted the feat/sync-direction-config branch July 5, 2026 10:51
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