Skip to content

feat(watcher): sync SVN working copies automatically - #1127

Open
lzl51230 wants to merge 8 commits into
DeusData:mainfrom
lzl51230:main
Open

feat(watcher): sync SVN working copies automatically#1127
lzl51230 wants to merge 8 commits into
DeusData:mainfrom
lzl51230:main

Conversation

@lzl51230

@lzl51230 lzl51230 commented Jul 16, 2026

Copy link
Copy Markdown

What does this PR do?

SVN working copies can now keep their graph synchronized after local working-copy changes on the watcher's next eligible poll, without requiring a manual full regeneration. The same adaptive cadence and indexing pipeline used by Git remain authoritative: ordinary changes use incremental indexing, while the existing safety threshold can still select a full rebuild.

The watcher observes recursive SVN XML status plus content fingerprints for indexable candidates, pins a trusted system svn executable, and acknowledges a new baseline only after explicit indexing success. Failed or busy updates remain pending for retry, Git keeps precedence in mixed roots, and successful background publication invalidates cached MCP stores on the request thread before the next query. Windows probes use isolated direct-argv stdout capture so MSYS2 paths and filename metacharacters never pass through cmd.exe, while retaining the existing restricted handle-inheritance model.

Decision Behavior
Source of truth Current on-disk working-copy state, including eligible versioned and unversioned files
Scheduling Existing completion-relative adaptive watcher interval; no SVN-specific setting
Pipeline routing Existing incremental path with the existing full-rebuild safety fallback
Failure handling Preserve the last acknowledged observation and retry later

Session-settled decisions carried from planning: on-disk working-copy authority, SVN-aware polling, existing adaptive timing and pipeline routing, the system SVN CLI prerequisite, and Git precedence (all user-directed); using the fork's main and proceeding before maintainer feedback were carried out under flagged contribution-process concerns.

Validation

  • make -f Makefile.cbm test-focused TEST_SUITES='security svn_state watcher integration' (135 passed).
  • Production build with -Wall -Wextra -Werror completed successfully.
  • make -j2 -f Makefile.cbm lint-ci passed with CI-matched LLVM 20 clang-format plus cppcheck and the NOLINT whitelist.
  • scripts/security-audit.sh and the no-skips policy passed.
  • Real local SVN fixtures cover repeated tracked edits, unversioned additions, watcher retry behavior, Git/SVN precedence, incremental publication, and the existing full-route fallback.
  • Windows-only coverage checks direct argv stdout capture, literal %/& arguments, nonzero exit propagation, MSYS2 path conversion, and an SVN working-copy path containing metacharacters.
  • Browser testing was skipped because this PR changes no UI or browser route.

Related and follow-up tracking

Related: #1113

Checklist

  • Every commit is signed off (git commit -s).
  • Tests pass locally.
  • CI-matched lint passes locally.
  • New behavior is covered by focused and integration tests.

Compound Engineering

lzl51230 added 2 commits July 16, 2026 18:12
Signed-off-by: lzl51230 <14934913+lzl51230@users.noreply.github.com>
Signed-off-by: lzl51230 <14934913+lzl51230@users.noreply.github.com>
@lzl51230
lzl51230 requested a review from DeusData as a code owner July 16, 2026 10:26
lzl51230 added 5 commits July 16, 2026 18:30
Signed-off-by: lzl51230 <14934913+lzl51230@users.noreply.github.com>
Signed-off-by: lzl51230 <14934913+lzl51230@users.noreply.github.com>
Translate Windows repository paths for the POSIX SVN client installed by CI, while preserving native Windows SVN URLs. Skip the unrelated cached-store atomic replacement case on Windows until DeusData#1117 is resolved.

Signed-off-by: lzl51230 <14934913+lzl51230@users.noreply.github.com>
Track the pinned SVN client's path semantics and translate Windows drive paths for MSYS2 across status probes and test fixtures. Preserve the canonical Windows root for content fingerprinting while accepting the SVN runtime path in XML output.

Signed-off-by: lzl51230 <14934913+lzl51230@users.noreply.github.com>
Add an isolated direct-argv stdout capture path for Windows and use it for pinned SVN status probes. Preserve literal metacharacters, process exit codes, and the existing restricted handle inheritance model.

Signed-off-by: lzl51230 <14934913+lzl51230@users.noreply.github.com>
@DeusData

Copy link
Copy Markdown
Owner

Thanks for the substantial implementation work and for documenting the unresolved cases. The exact head cannot be accepted or expanded in place.

Product and supply-chain boundary

This introduces a conditional runtime dependency on a system Apache Subversion executable while the project currently promises a zero-runtime-dependency binary. It also changes .github/workflows/_test.yml so apt, Homebrew, and MSYS2 install Subversion across existing native and TSan jobs. That product/CI decision has not been approved; please do not add further commits around it until maintainers decide whether this dependency belongs at all.

Current correctness and security blockers

  1. The validation-then-open fingerprint path has a TOCTOU containment race (Make SVN fingerprint opens race-safe #1124): a symlink, reparse point, special file, or outside-root target can replace the validated path before open.
  2. Fingerprint reads do not enforce the byte cap against bytes actually read from a growing file (Enforce SVN fingerprint byte cap during reads #1123).
  3. Recursive candidate traversal can exhaust the watcher thread stack (Avoid recursive stack exhaustion in SVN candidate traversal #1125).
  4. The SVN subprocess has no bounded timeout/cancellation path, so a stuck client can block polling and shutdown.
  5. Observation acknowledgement can bless stale graph state after an A-to-B-to-A edit race (Detect ABA edits during watcher indexing #1126).
  6. A snapshotted retired watch can still invoke the callback after root replacement (Rewatch can index a retired project root #1116).
  7. Windows publication fails with a pre-warmed cached SQLite handle (Windows cached stores can block watcher publication #1117); the current Windows full-suite failure is therefore a real product failure, not a flake.
  8. One broken external can block primary-working-copy updates (Broken SVN external can freeze primary graph updates #1119), contrary to the PR's stated behavior.
  9. The recursive status/fingerprint work has no aggregate work budget or large-tree characterization.

Scope and repository hygiene

The PR combines general incremental dump-error propagation, watcher lifetime changes, MCP store invalidation, a new Windows process API, build/CI changes, and the SVN feature. Those need focused prerequisite PRs with their own RED guards. Replace the raw fopen() added in tests/test_integration.c with cbm_fopen(). Remove docs/plans/2026-07-16-001-feat-svn-auto-watch-graph-sync-plan.md; session execution plans and authority assertions are not repository documentation.

The current security review found no credential, hosted-service, telemetry, vendored-code, or data-exfiltration addition. The blocker is the new local executable/CI dependency plus the unresolved containment and lifecycle defects. Maintainers will decide the product direction separately; this head is not mergeable even if the remaining CI job turns green.

@lzl51230

Copy link
Copy Markdown
Author

please do not add further commits around it until maintainers decide whether this dependency belongs at all.

Acknowledged. I have stopped pushing commits to this head and will leave it unchanged while maintainers decide whether a system SVN dependency fits the product and supply-chain boundary.

If that direction is accepted, I will first split the general prerequisites into focused PRs with RED regression guards, then address the full correctness, lifecycle, timeout, work-budget, and repository-hygiene list before returning to the SVN feature. I will not treat a green CI run as making the current head mergeable.

Serialize MCP query handles with background graph publication on Windows so atomic SQLite replacement can complete. Keep the existing POSIX generation behavior, use a consistent store-gate-to-pipeline lock order, and cover both incremental and full watcher routes.

Signed-off-by: lzl51230 <14934913+lzl51230@users.noreply.github.com>
@DeusData

Copy link
Copy Markdown
Owner

Reviewed in full — diff read end to end, every svn invocation located. Two things to say, and I want to lead with the first.

The security engineering here is genuinely excellent. Specifically: the executable is pinned once at registration by scanning PATH for absolute entries only, canonicalizing, and then rejecting any candidate that resolves under the watched repo root or the CWD — which blocks a malicious repo shipping its own svn. On Windows you added cbm_popen_argv() and spawn a direct argv rather than going through cmd.exe, with literal %VAR% and & metacharacter tests to prove it. On POSIX both executable and root pass cbm_validate_shell_arg before a double-quoted string with a -- separator and an @ peg-escape suffix. That is precisely the level of paranoia this codebase wants, and it is not common.

I also want to confirm something readers of the title might assume otherwise: this never mutates the working copy. The only production invocation is svn status --xml --verbose --no-ignore --depth infinity --non-interactive; checkout and commit appear solely in tests/svn_test_helpers.h. "Sync" here means syncing the graph to the working copy, not the other way round. I checked because the title reads both ways.

And the honesty in the PR body about proceeding before maintainer feedback made this review easier, not harder. Thank you for that.

Why it is not a review-level decision. Two things in here are the maintainer's call, so I have escalated rather than deciding:

  1. Do we want SVN as a second watcher-owned VCS at all? That is a direction question about the project's scope, not about your implementation quality.
  2. The bundled MCP store-update gate. cbm_mcp_server_begin/end_store_update changes MCP-core locking for all VCSs and all platforms — it is the fix for the separate staleness issue riding along inside an SVN feature, which hides its blast radius. Concretely, the gate is taken at the top of watcher_index_fn and released at the end, so it is held across the entire background reindex; on Windows it also invalidates the cached store at begin and keeps new query handles from opening until publication completes. On a large repo that can gate MCP queries for the full minutes-long sync. That needs measuring and its own review, independently of the SVN question.

Three things to do regardless of how the direction call goes:

  • Split the store-update gate into its own PR. It stands alone, it affects git projects, and it deserves its own before/after numbers on Windows.
  • Drop the committed planning document. It creates a new docs/plans/ directory that does not exist on main; session artifacts should not land in the repo.
  • Rebase. The PR is CONFLICTING against current main — sharding, the single-binary change and daemon work have all landed since 16 July, and your green CI is against that older base.

One more thing the maintainer will want to weigh consciously: the PR installs subversion on the Ubuntu, macOS and Windows test legs, which is a package on every test job (the brew install step in particular is new cost and new flake surface), and scripts/security-allowlist.txt gains two entries for your own popen usage. Both are reasonable given the code — they simply need to be accepted deliberately rather than inherited.

I will come back to you as soon as I have the direction answer.

@DeusData

Copy link
Copy Markdown
Owner

Direction answer, as promised: yes, we will take SVN support — with two conditions that change the shape of the feature, plus the items already on the list.

I want to be straight about the reasoning, because "yes" here was not automatic. The watcher is 1,440 lines whose entire change-detection strategy is git — watcher_git_run, rev-parse --git-dir, rev-parse HEAD, status --porcelain, ls-files, submodule handling. A second VCS is not a feature we add once; it is a surface we maintain forever, in a project that deliberately stays lean. What tipped it is that your implementation is contained and unusually careful, and that SVN is still real in exactly the enterprise environments where this tool is most useful.

Condition 1 — it must be opt-in, defaulting off.

Today the SVN strategy is auto-selected whenever a watched root lacks git metadata and contains a .svn directory, with no config key anywhere. That means the presence of a .svn directory somewhere in a watched tree causes us to start spawning the system svn binary against it — a behaviour change nobody asked for, on machines where SVN may be installed for unrelated reasons. Please put it behind an explicit config key (the CONFIG_KEYS table in src/cli/cli.c is where it belongs so config list/config set can see it), defaulting to off, and document it in docs/CONFIGURATION.md.

I realise auto-detection is friendlier for SVN users and that opting in is a small tax on them. I would still rather charge that tax than change what the watcher does on machines that never asked for it.

Condition 2 — the subprocess must be bounded.

There is no timeout and no cancellation path around the svn child. svn status --depth infinity on a large or pathological working copy — or one on a stale network mount — can block indefinitely, and the watcher thread has no way out. Please add a timeout with cancellation, and treat expiry the same way you already treat a parse failure: CBM_SVN_PROBE_UNCERTAIN rather than a hard error. Your existing uncertain-state handling makes this a small change, which is exactly why it should not be left out.

One correction to the shell-argument validation. Both the executable and the status root go through cbm_validate_shell_arg, but src/foundation/str_util.h documents cbm_validate_shell_path_arg as the one to use — its comment says, in as many words, to use it and not cbm_validate_shell_arg for every path that crosses into a shell command, so the shell-out sites cannot drift apart again. On POSIX the two are behaviourally identical today and the Windows branch uses no shell, so this is not exploitable at this head. It is drift from a convention that exists precisely to stop these sites diverging, and the fix is one identifier.

And a small one: tests/test_integration.c adds a raw fopen(path, "w"). Repo rule is cbm_fopen() — your production code in svn_state.c already gets this right with cbm_fopen(absolute, "rb"), so it is just the test that slipped.

Still on the list from last time, unchanged: split the cbm_mcp_server_begin/end_store_update gate into its own PR with before/after numbers on Windows (it changes MCP-core locking for every VCS and every platform, and held across a full background reindex it can gate queries for minutes); drop the committed docs/plans/ document; and rebase, since the branch is CONFLICTING and your green CI is against a mid-July base.

Two costs I am accepting deliberately rather than inheriting, so they are on the record: installing subversion on the Ubuntu, macOS and Windows test legs is real time and new flake surface on every test job, and scripts/security-allowlist.txt gains two entries for your popen site. Both are justified by the code. With the feature opt-in and defaulting off, I would also like the SVN tests to keep running unconditionally — the config gate should govern the product's behaviour, not our coverage of it.

Thank you for your patience through a long direction call, and for the security work in here. Pinning the executable at registration and rejecting any candidate resolving under the watched repo root is the kind of thing most contributors would not think to do, and it is the reason this got a "yes" rather than a "not worth the surface."

@DeusData

Copy link
Copy Markdown
Owner

@lzl51230 — no new asks, and no pressure on timing. This is a heads-up that the ground moved under your rework since my 2026-08-20 answer, so you aim at the right place rather than discover it mid-rebase.

The watcher integration point you patch no longer exists. Your diff wires cbm_watcher_new and the index callback in src/main.c. On today's main (b0a3ca28) main.c does not construct the watcher at all — it moved to src/daemon/host.c:598, inside host_state_prepare, with a different callback (host_watcher_index) and different user_data (the host, not a standalone context). So that part is a redesign against the daemon architecture, not a conflict to resolve. I would rather you knew that before spending an evening on a rebase.

And Condition 1 now has a neighbour. #1105 merged today (b0a3ca28) and added a watcher_enabled config key, default true, which gates whether the watcher is built at all — host->watcher can now legitimately be NULL. Your SVN key should compose with it as a second-level gate: watcher off means no SVN activity at all, watcher on plus SVN key off means git-only. It also gives you a worked example of exactly the shape Condition 1 asks for — CONFIG_KEYS row, cbm_config_get_bool with an explicit default, docs entry, and a process-level test that asserts the subsystem does not start when the key is off, with a positive control so gate inversion fails too. That test is worth copying; it is the part that makes a config key more than a claim.

Good news on the rest: I re-verified your security work against the current tree and it stands. The executable pinning, the rejection of an svn binary under the watched root or CWD, and the Windows direct-argv path with no cmd.exe are all still correct, and the read-only nature of svn status --xml --verbose is exactly as you described. Nothing in the direction answer has changed — SVN support is still a yes.

Conditions 1 and 2 (opt-in key defaulting off; bounded subprocess with cancellation → CBM_SVN_PROBE_UNCERTAIN) stand as written, along with the split of the MCP store gate into its own PR, dropping the committed plan document, and the July-16 correctness list.

Take the time you need — you stopped pushing the moment I asked you to back in July, which was the right call and is why this thread has stayed easy to follow.

@DeusData

Copy link
Copy Markdown
Owner

@lzl51230 — thank you for this, and for doing exactly what I asked: you stopped pushing the day I requested it and then waited while I took five weeks to answer. That patience deserves a concrete update rather than another silence.

First, something is landing now, with credit to you. Buried in this PR is a ~17-line change to pipeline_incremental.c that makes dump_and_persist return its status, so a failed SQLite dump stops reporting success. That is a silent data-loss bug affecting every git user today, it has nothing to do with SVN, and it stands entirely alone. I'm shipping it as its own small PR with Co-authored-by: credit to you rather than leaving it behind a large feature review. Thank you for catching it.

Second, the direction answer from 2026-08-20 stands — SVN is still a yes. But the target moved underneath you again, and you should hear that from me rather than discover it in a rebase:

  • The watcher is no longer built in src/main.c; it moved to src/daemon/host.c:598.
  • defer_state_free, which this PR adds, already exists on main at watcher.c:829 and is already called at :1059.
  • Since your branch point, mcp.c has taken ~90 commits and main.c ~36.

Third, one correction I owe you. My 08-20 comment said a .svn directory "somewhere in a watched tree" triggers the SVN path. That was wrong — vcs_metadata_exists checks <root>/.svn only, and a root .git wins first. The blast radius is smaller than I described. Condition 1 (opt-in, default off) still stands on its own merits, since it does still spawn svn on machines that never asked for it.

What the remaining work actually is, so it's concrete:

  1. Condition 2 is a dropped contract, not a missing feature. watcher_git_run (src/watcher/watcher.c:322-361) already runs git through cbm_subprocess_spawn with a wall deadline, cancel_grace_ms and an output_limit. The SVN path uses bare cbm_popen with none of the four and blocks in fgetc until EOF. Use watcher_git_run as the template.
  2. The unbounded recursion at svn_state.c:702-745 is a merge blocker and wants a RED guard. It's reachable from ordinary repo content, not just hostile input: --no-ignore reports unversioned and ignored directories, and fingerprint_tree descends them itself. ~8.3 KB frames against a 4096-byte path budget permits ~2040 levels on an 8 MB stack.
  3. The MCP store gate needs redesign, not rebase. It gates a single cbm_mcp_server_t, but the daemon keeps a per-session list (application.c:2249), so one gate cannot invalidate the others. It also avoids self-deadlock only because Windows cbm_mutex_t is a re-entrant CRITICAL_SECTION while POSIX is not — a quiet platform dependency. And as written it makes every MCP query on Windows block for the full duration of a background reindex, including for git projects. I'd rather this were split out entirely.
  4. A design answer on per-poll cost. Unlike git's cheap metadata check, this content-hashes the full bytes of every candidate on every poll, uncapped, with --no-ignore pulling ignored subtrees into that set. On the multi-GB enterprise working copies that are the whole point of the feature, that's continuous full-tree I/O every 5-60s. Dropping --no-ignore, capping depth and capping bytes read may be enough — but I'd like your view.
  5. The local ladder needs subversion too. th_svn_fixture_init returns -1 when svn is absent and callers FAIL() rather than skip — so the three-OS local ladder, which is the primary gate here, would go hard red. No test-infrastructure/ image installs subversion and the Windows VM provisioning is untouched; this PR only edits .github/workflows/_test.yml. Either fixtures must skip, or provisioning has to come with it.

Credit where it's due: the security engineering is genuinely good and I want that on the record. One production svn invocation, pre-validated on POSIX and direct-argv on Windows so cmd.exe is never involved; -- separator present; the trailing @ correctly neutralising peg-revision syntax; and a PATH-hijack defence that rejects any svn resolving under the watched repo or CWD. The parser is bounds-checked throughout, and there is not a single sleep in the test suite — every test uses the cbm_watcher_touch() seam and asserts exact call counts. That is the shape I want tests to have.

Given points 3 and 5, I think this is cleanest as a fresh PR against the daemon with the MCP gate left out. Do you want to carry that? A straight "no" or silence is a completely fine answer after the wait I've put you through — say so and I'll take it from here with you credited. Either way the dump_and_persist fix ships this week.

@DeusData

Copy link
Copy Markdown
Owner

Correction to my previous comment, @lzl51230 — I was wrong about the dump_and_persist fix, and you should have the accurate version rather than the flattering one.

I said it was "a silent data-loss bug affecting every git user today" and that I'd ship it this week. Both halves are wrong, and I checked properly only after saying it.

It was already fixed, independently, on 2026-07-16 — commit 0e00ef57 ("feat: coordinate concurrent CBM sessions") made the identical three edits: the signature became static int, the dump failure returns early, and the caller captures and returns persist_rc. That landed in parallel with your PR, so your hunk is a no-op against current main (pipeline_incremental.c:1463, :1487, :2851, :2862).

And the severity I claimed was overstated regardless. dump_and_persist is now test-only — its own comment says "Publish the test-only legacy partial result", and reaching it requires force_legacy_partial, which is set only by a test-API function behind #if defined(CBM_INCREMENTAL_TEST_API). The production incremental route is run_closure_delta, which returns directly at :2543 and never falls through. So even before 0e00ef57 this was not on the path a normal user takes.

What stands: you identified a real bug by reading the code. Discarding a persistence status is exactly the class of defect worth catching, you spotted it in a file you were only passing through, and you were right that it needed fixing. It simply got fixed by someone else first, which is luck of timing and no reflection on the finding.

Practical consequence: when the SVN work is re-briefed against the daemon, drop this hunk rather than carrying it — it would now be an empty diff.

Sorry for the noise. I'd rather correct it the same day than let you discover it when the "shipping this week" PR never appeared.

@DeusData

DeusData commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Apologies first: this has been open since 16 July with no comment from us at all. Seven weeks of silence on 2,723 lines of work is not acceptable and I am sorry. This note is not a review verdict — it is to tell you where it actually stands.

This one needs a more careful review than we have given it, and that review has not started. It is not queued behind a small backlog; nobody has looked at it properly yet. I would rather say that plainly than leave you guessing.

Four things make it heavier than its line count suggests, and are why it cannot be waved through:

1. It adds its own entries to scripts/security-allowlist.txt. This is the one I need to flag now, because it is policy rather than opinion and it is actionable for you:

src/watcher/svn_state.c:cbm_popen:recursive local SVN status XML (executable pinned; args validated)
src/watcher/svn_state.c:popen:via cbm_popen wrapper call

A contribution may not supply its own exemption from a security gate, however accurate the wording — and yours reads accurately. The allowlist is maintainer-only precisely because an honest entry and a dishonest one look identical in a diff, and every entry on main today was added by a maintainer. If this lands, we add those lines ourselves after reviewing the popen sites, not as part of your diff. Please drop them from the PR.

2. It edits .github/workflows/_test.yml to apt-get install subversion and brew install subversion across three legs. CI workflow changes are the same maintainer-only class, and this one also makes svn a build-time requirement on every runner.

3. It introduces a new external binary dependencysvn — for end users as well as CI. That is a project-direction question, not a code-review question, and it belongs to the maintainer.

4. The popen surface itself deserves real scrutiny. Your description says the executable is pinned and arguments validated, and the Windows path uses direct-argv capture so nothing passes through cmd.exe. That is the right instinct and it is exactly the sort of claim that has to be verified line by line rather than accepted, which is a chunk of careful work rather than a skim.

It is also currently conflicting with main, so it needs a rebase regardless.

I am not asking you to do that rebase yet — it would be unfair to ask for work before we have committed to reviewing it. What I would suggest, if you want to move it forward: drop the security-allowlist.txt and _test.yml changes, since neither can be accepted from a contribution in any case. That makes the remaining diff a straight feature review rather than a policy question tangled with one.

Thank you for your patience, and again, sorry for the silence.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

editor/integration Editor compatibility and CLI integration enhancement New feature or request priority/normal Standard review queue; useful PR with ordinary maintainer urgency. security Security vulnerabilities, hardening stability/performance Server crashes, OOM, hangs, high CPU/memory

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants