You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Remove the replicated-apply client id switch from core (post-release) #3435
Core still has sync-only behaviour keyed on a client id that only the Pro sync service sets. REPLICATED_APPLY_CLIENT_ID = "sync-service" lives in packages/core/src/db/events.rs (~277-285). Writes stamped with it:
skip playbook trigger evaluation (playbook/engine.rs ~455-473 and ~1525, is_replicated_apply);
get no invariant rules (services/node_service/invariants.rs ~87, ~157, ~215).
The daemon takes x-ns-client-id from any local gRPC client unvalidated (packages/daemon/src/services/node_service.rs ~232-243), so the CLI or an agent could set it. Nothing in core sets the value outside tests.
ADR-081 §1 says core has no sync semantics and no code path that behaves differently when talking to Pro.
Found by the release-readiness acceptance audit on main ab9961e6. Not a release blocker: the switch is dormant in core, and ADR-052 is a single-user trust boundary.
Options (decide against ADR-082)
Make it a named extension point in ADR-082 §5, with a neutral name and value that a Pro daemon registers; or
move the gate into nodespace-sync, so core's engine has no such branch.
At minimum, the daemon should not let a local client set a reserved client id.
Acceptance criteria
Core has no client-id string that changes trigger or invariant behaviour, unless it is a documented ADR-082 hook.
Design proposal (investigation only, nothing implemented; core is mid-release)
Checked against core origin/main2bf95fea6 and nodespace-sync origin/main334349c. Sync's core pin is 4f29efea3 (an ancestor of core main, 19 commits behind).
Where the switch is used today (line numbers in the issue have drifted)
packages/core/src/db/events.rs:285: REPLICATED_APPLY_CLIENT_ID = "sync-service", plus a test that pins its value because "writers outside core" set the literal.
packages/core/src/playbook/engine.rs:1597is_replicated_apply(&EventEnvelope), used at :596: skips trigger evaluation and runs dispatch_invariant_repair (ADR-060 §7 repair-and-log) instead.
packages/core/src/services/node_service/invariants.rs:131 (invariant_rules_for_creation), :277 (run_invariant_rules_for_update), :335 (dispatch_invariant_rules_for_relationship_in_tx): self.client_id == REPLICATED_APPLY_CLIENT_ID returns no rules. Bulk paths go through invariant_rules_for_creation.
Tests that depend on it: playbook/tests.rs (3 is_replicated_apply tests), tests/it/playbook_engine_integration_test.rs, playbook_invariant_integration_test.rs, bulk_invariant_dispatch_test.rs.
Daemon: packages/daemon/src/services/node_service.rsroute() calls with_client(<x-ns-client-id header>) for any value; client_id_header only checks ASCII.
Where Sync sets it
Sync does not go through the gRPC header. nodespaced-sync is an in-process composition over core's library, so it calls NodeService::with_client("sync-service") directly: cloud_sync/orchestrator.rs:109 (the apply service; the other cloud_sync/* files take that service), status.rs:257/415, and about 40 literal with_client("sync-service") calls across apply.rs, identity.rs, reconnect.rs, conflicts.rs, status.rs (mostly tests), plus examples/t6/t9*. Sync's own constant nodespace_sync::echo::SYNC_SERVICE_CLIENT_ID (src/echo.rs:59) is used for echo prevention: EchoGuard::is_own_event, set_excluded_event_origin(...) in orchestrator.rs:121 and status.rs:1191, and the push consumer's L1 skip (src/push/supabase.rs). That use is generic in core (set_excluded_event_origin takes any string) and needs no change.
So the string does two separate jobs: (a) echo prevention, which is Sync's business and already generic; (b) a core behaviour switch (skip local rules, repair instead), which is what the issue is about.
Option 2 (move the gate into Sync) does not work on its own
The branches are inside core's NodeService write paths (pre-commit invariant dispatch inside the transaction) and the engine's event loop (repair-and-log). Sync cannot remove them without core exposing a hook, so Option 2 collapses into Option 1. The semantics themselves (ADR-073 local-origin gate, ADR-060 §7) are not Pro semantics: any second writer applying already-committed data needs them.
Recommendation: Option 1 with a typed, in-process marker instead of a magic string
Core adds a neutral concept, "replicated apply" (a write whose effects were already committed elsewhere), carried as a typed field rather than a client-id value:
EventMetadata gets replicated: bool (or origin: WriteOrigin::{Local, Replicated}), filled from the NodeService that wrote it (events are built from self metadata at node_service/mod.rs ~523-532).
NodeService::as_replicated_apply(&self, client_id: impl Into<String>) -> Self (name to be settled in the ADR) sets both the client id (echo tag unchanged) and a private replicated flag. There is no gRPC or header path to set it.
is_replicated_apply reads envelope.metadata.replicated; the three invariants.rs guards read self.replicated. REPLICATED_APPLY_CLIENT_ID and its value-pin test are deleted.
Security: because the flag is not derivable from a header, a CLI or agent can no longer switch off rules or invariants by sending x-ns-client-id: sync-service. The second half of the issue's "at minimum" remains: a client claiming Sync's echo id would have its events dropped from the push channel (an availability problem, not an integrity one). Add DaemonExtensions::reserved_client_ids(...) (ADR-082 §5 row); route() answers permission_denied for a header value in that set. Sync registers "sync-service". Core's default set is empty, so core stays free of the string.
Behavioural equivalence: events keep source_client_id = Some("sync-service") (Sync still passes its id), so watch_nodes and set_excluded_event_origin L1 skipping is byte-for-byte the same. Playbook and invariant behaviour is the same for in-process Sync writes; the only behaviour change is that header-supplied ids stop having any effect on rules.
Versioning: ADR-082 §8 was amended on 2026-10-05, so there is no EXTENSION_API_VERSION to bump; the change reaches Sync through its core pin. The docs-first step is an ADR-082 §5 row (replicated-apply marker, reserved client ids) and an architecture/extension-points.md entry, no ticket numbers in the docs.
Change list
Core (one PR, after v0.3.5 is out):
db/events.rs: remove the constant and its test; add the replicated field to EventMetadata (all constructors and PartialEq users).
services/node_service/mod.rs: replicated flag on NodeService, as_replicated_apply, propagate into emitted metadata (single and bulk paths, transaction paths).
playbook/engine.rs: is_replicated_apply reads the field. invariants.rs: three guards read self.replicated.
packages/daemon/src/services/extensions.rs and node_service.rsroute(): reserved client ids.
Docs: rewrite the comments that name the constant (engine.rs ~585, events.rs, invariants.rs ~94).
Sync (one PR, in the same pin bump as the core PR):
Cargo.toml / nodespaced-sync/Cargo.toml: all nodespace-* pins to the new core rev (one rev, all crates).
Replace with_client("sync-service") with as_replicated_apply(SYNC_SERVICE_CLIENT_ID) on every apply-side service (orchestrator.rs:109, status.rs:257/415, apply.rs, identity.rs, reconnect.rs, conflicts.rs, examples, tests), keeping the literal out of the call sites.
Register reserved_client_ids([SYNC_SERVICE_CLIENT_ID]) where nodespaced-sync/src/main.rs builds DaemonExtensions.
Remove the stale reference entry in reference/core-sync-origin/INDEX.md if it names the old constant.
Tests
Core unit: a replicated-marked write skips triggers and the three invariant paths, and a plain with_client("sync-service") no longer does (mutation check: this is the regression for the old string).
Core: the existing playbook/invariant/bulk integration tests move from with_client(CONST) to as_replicated_apply(..); repair-and-log still runs for received creates and updates; the marker survives bulk and transaction paths.
Daemon: a header with a reserved id is refused; a header sync-service with no registered reservation is only an echo tag and does not skip rules.
Sync: an end-to-end apply of a pulled node with a reactive play and an invariant installed (no firing, repair runs), the L1 push skip still holds, and the daemon refuses a client claiming sync-service.
Order
Docs: ADR-082 §5 row and extension-points entry.
Core PR (after the v0.3.5 release), merged, new rev.
Sync PR with the pin bump and all call-site changes together. They must land as one change: a bump alone would make Sync's apply writes fire local rules once the old constant is gone.
This is coordinated with #3414 (closed; its mechanisms are on main) and nodespace-sync#518.
Picking this up now (user decision 2026-10-12: land it in core v0.3.5). Design: a neutral in-process typed write context on NodeService (skip playbook triggers / skip invariant rules), carried in event metadata, not settable from any wire field; plus DaemonExtensions::reserved_client_ids (empty by default). REPLICATED_APPLY_CLIENT_ID and is_replicated_apply get deleted. Comment here if this is already in progress elsewhere.
Phase 1 merged as #3752 (1b64164): typed in-process WriteContext via NodeService::with_write_context, plus DaemonExtensions::reserved_client_id. It is additive; the old id still behaves as before. Docs are on nodespace-docs main (e22a839): ADR-082 §5 rows and the extension-points guide. Phase 2, draft #3754, deletes REPLICATED_APPLY_CLIENT_ID/is_replicated_apply and closes this issue. It is held until Sync has moved to the typed context. Not in v0.3.5; this ships in the next core release.
Done. Core no longer has a client-id string that changes trigger or invariant behaviour. The issue closed automatically when #3754 merged; this comment records the evidence.
What landed
Add a typed in-process write context and reserved client ids (refs #3435) #3752 (phase 1, main 1b6416482) adds a typed, in-process WriteContext { skip_playbook_triggers, skip_invariant_rules }, set only through NodeService::with_write_context. No request field or header can set it. It also adds DaemonExtensions::reserved_client_id: a request presenting a reserved id gets PERMISSION_DENIED. Core reserves no id.
NodeSpaceAI/nodespace-sync#679 (Sync main 2b25acf): Sync's apply handle is built in one place (echo::replicated_apply_service) with both flags, and Sync's daemon reserves its own client id.
Remove the superseded replicated-apply client id (closes #3435) #3754 (phase 2, main af46b30f9) deletes REPLICATED_APPLY_CLIENT_ID, is_replicated_apply, write_context_of and the client-id comparison in skips_invariant_rules, and removes the last incidental sync-service strings from core. A client id is now only an echo-suppression label.
Docs (nodespace-docs e22a839): ADR-082 §5 gains the "Write context" and "Reserved client ids" rows with an amendment note, and architecture/extension-points.md documents both as generic extension points.
Acceptance criteria
Core has no client-id string that changes trigger or invariant behaviour.git grep on main af46b30f9 finds no REPLICATED_APPLY_CLIENT_ID, is_replicated_apply, write_context_of or sync-service. The only matches are the unrelated browser-sync-service module name.
Tests cover the design. Typed-context tests cover the engine trigger gate, the invariant skips (create, update, relationship, bulk) and repair-and-log. A reserved id is refused on writes and WatchNodes. The tests showing the old value no longer skips run at the core, daemon and fixture levels. Mutations that restore the old comparison are killed. The full merge gate passed on the PR stacked onto main.
Sync not broken, checked before merging and again after
Local canary, Sync main 2b25acf against the Remove the superseded replicated-apply client id (closes #3435) #3754 head c361cb5fe: canary OK. All 9 checks passed (lock alignment, cargo check --workspace --all-targets, tauri config, app crate tests, frontend build, svelte-check, Sync frontend tests, import boundary, mirrors).
Against the same core:
tests/replicated_apply_rules.rs: 4/4
sync_client_id_source_guard: 5/5
daemon sync_settings::: 12/12, including a_request_presenting_the_sync_client_id_is_refused
full Sync workspace: 1101 passed, 0 failed, 15 ignored
After merge, the hosted Core canary run 38225766715 (Sync 2b25acf against core main af46b30f9): success, same 9 checks.
NodeSpaceAI/nodespace-sync#681 hardens Sync's source guard. It currently matches only exact spellings, and its production-code scan stops at the first inline test module.
Activity
mstomar125 commentedon Oct 12, 2026
Design proposal (investigation only, nothing implemented; core is mid-release)
Checked against core
origin/main2bf95fea6and nodespace-syncorigin/main334349c. Sync's core pin is4f29efea3(an ancestor of core main, 19 commits behind).Where the switch is used today (line numbers in the issue have drifted)
packages/core/src/db/events.rs:285:REPLICATED_APPLY_CLIENT_ID = "sync-service", plus a test that pins its value because "writers outside core" set the literal.packages/core/src/playbook/engine.rs:1597is_replicated_apply(&EventEnvelope), used at:596: skips trigger evaluation and runsdispatch_invariant_repair(ADR-060 §7 repair-and-log) instead.packages/core/src/services/node_service/invariants.rs:131(invariant_rules_for_creation),:277(run_invariant_rules_for_update),:335(dispatch_invariant_rules_for_relationship_in_tx):self.client_id == REPLICATED_APPLY_CLIENT_IDreturns no rules. Bulk paths go throughinvariant_rules_for_creation.playbook/tests.rs(3is_replicated_applytests),tests/it/playbook_engine_integration_test.rs,playbook_invariant_integration_test.rs,bulk_invariant_dispatch_test.rs.packages/daemon/src/services/node_service.rsroute()callswith_client(<x-ns-client-id header>)for any value;client_id_headeronly checks ASCII.Where Sync sets it
Sync does not go through the gRPC header.
nodespaced-syncis an in-process composition over core's library, so it callsNodeService::with_client("sync-service")directly:cloud_sync/orchestrator.rs:109(the apply service; the othercloud_sync/*files take that service),status.rs:257/415, and about 40 literalwith_client("sync-service")calls acrossapply.rs,identity.rs,reconnect.rs,conflicts.rs,status.rs(mostly tests), plusexamples/t6/t9*. Sync's own constantnodespace_sync::echo::SYNC_SERVICE_CLIENT_ID(src/echo.rs:59) is used for echo prevention:EchoGuard::is_own_event,set_excluded_event_origin(...)inorchestrator.rs:121andstatus.rs:1191, and the push consumer's L1 skip (src/push/supabase.rs). That use is generic in core (set_excluded_event_origintakes any string) and needs no change.So the string does two separate jobs: (a) echo prevention, which is Sync's business and already generic; (b) a core behaviour switch (skip local rules, repair instead), which is what the issue is about.
Option 2 (move the gate into Sync) does not work on its own
The branches are inside core's
NodeServicewrite paths (pre-commit invariant dispatch inside the transaction) and the engine's event loop (repair-and-log). Sync cannot remove them without core exposing a hook, so Option 2 collapses into Option 1. The semantics themselves (ADR-073 local-origin gate, ADR-060 §7) are not Pro semantics: any second writer applying already-committed data needs them.Recommendation: Option 1 with a typed, in-process marker instead of a magic string
EventMetadatagetsreplicated: bool(ororigin: WriteOrigin::{Local, Replicated}), filled from theNodeServicethat wrote it (events are built fromselfmetadata atnode_service/mod.rs~523-532).NodeService::as_replicated_apply(&self, client_id: impl Into<String>) -> Self(name to be settled in the ADR) sets both the client id (echo tag unchanged) and a privatereplicatedflag. There is no gRPC or header path to set it.is_replicated_applyreadsenvelope.metadata.replicated; the threeinvariants.rsguards readself.replicated.REPLICATED_APPLY_CLIENT_IDand its value-pin test are deleted.x-ns-client-id: sync-service. The second half of the issue's "at minimum" remains: a client claiming Sync's echo id would have its events dropped from the push channel (an availability problem, not an integrity one). AddDaemonExtensions::reserved_client_ids(...)(ADR-082 §5 row);route()answerspermission_deniedfor a header value in that set. Sync registers"sync-service". Core's default set is empty, so core stays free of the string.source_client_id = Some("sync-service")(Sync still passes its id), sowatch_nodesandset_excluded_event_originL1 skipping is byte-for-byte the same. Playbook and invariant behaviour is the same for in-process Sync writes; the only behaviour change is that header-supplied ids stop having any effect on rules.EXTENSION_API_VERSIONto bump; the change reaches Sync through its core pin. The docs-first step is an ADR-082 §5 row (replicated-apply marker, reserved client ids) and anarchitecture/extension-points.mdentry, no ticket numbers in the docs.Change list
Core (one PR, after v0.3.5 is out):
db/events.rs: remove the constant and its test; add thereplicatedfield toEventMetadata(all constructors andPartialEqusers).services/node_service/mod.rs:replicatedflag onNodeService,as_replicated_apply, propagate into emitted metadata (single and bulk paths, transaction paths).playbook/engine.rs:is_replicated_applyreads the field.invariants.rs: three guards readself.replicated.packages/daemon/src/services/extensions.rsandnode_service.rsroute(): reserved client ids.Sync (one PR, in the same pin bump as the core PR):
Cargo.toml/nodespaced-sync/Cargo.toml: allnodespace-*pins to the new core rev (one rev, all crates).with_client("sync-service")withas_replicated_apply(SYNC_SERVICE_CLIENT_ID)on every apply-side service (orchestrator.rs:109,status.rs:257/415,apply.rs,identity.rs,reconnect.rs,conflicts.rs, examples, tests), keeping the literal out of the call sites.reserved_client_ids([SYNC_SERVICE_CLIENT_ID])wherenodespaced-sync/src/main.rsbuildsDaemonExtensions.reference/core-sync-origin/INDEX.mdif it names the old constant.Tests
with_client("sync-service")no longer does (mutation check: this is the regression for the old string).with_client(CONST)toas_replicated_apply(..); repair-and-log still runs for received creates and updates; the marker survives bulk and transaction paths.sync-servicewith no registered reservation is only an echo tag and does not skip rules.sync-service.Order
This is coordinated with #3414 (closed; its mechanisms are on main) and nodespace-sync#518.
🤖 Generated with Claude Code
mstomar125 commentedon Oct 12, 2026
Picking this up now (user decision 2026-10-12: land it in core v0.3.5). Design: a neutral in-process typed write context on
NodeService(skip playbook triggers / skip invariant rules), carried in event metadata, not settable from any wire field; plusDaemonExtensions::reserved_client_ids(empty by default).REPLICATED_APPLY_CLIENT_IDandis_replicated_applyget deleted. Comment here if this is already in progress elsewhere.🤖 Generated with Claude Code
mstomar125 commentedon Oct 12, 2026
Frontend baseline (worktree from origin/main 2bf95fe): 324 files / 5895 tests passed.
🤖 Generated with Claude Code
Add a typed in-process write context and reserved client ids (refs #3435
Add a typed in-process write context and reserved client ids (refs #3435
mstomar125 commentedon Oct 12, 2026
Phase 1 merged as #3752 (1b64164): typed in-process
WriteContextviaNodeService::with_write_context, plusDaemonExtensions::reserved_client_id. It is additive; the old id still behaves as before. Docs are on nodespace-docs main (e22a839): ADR-082 §5 rows and the extension-points guide. Phase 2, draft #3754, deletesREPLICATED_APPLY_CLIENT_ID/is_replicated_applyand closes this issue. It is held until Sync has moved to the typed context. Not in v0.3.5; this ships in the next core release.🤖 Generated with Claude Code
Remove the superseded replicated-apply client id (closes #3435)
mstomar125 commentedon Oct 12, 2026
Done. Core no longer has a client-id string that changes trigger or invariant behaviour. The issue closed automatically when #3754 merged; this comment records the evidence.
What landed
1b6416482) adds a typed, in-processWriteContext { skip_playbook_triggers, skip_invariant_rules }, set only throughNodeService::with_write_context. No request field or header can set it. It also addsDaemonExtensions::reserved_client_id: a request presenting a reserved id getsPERMISSION_DENIED. Core reserves no id.2b25acf): Sync's apply handle is built in one place (echo::replicated_apply_service) with both flags, and Sync's daemon reserves its own client id.af46b30f9) deletesREPLICATED_APPLY_CLIENT_ID,is_replicated_apply,write_context_ofand the client-id comparison inskips_invariant_rules, and removes the last incidentalsync-servicestrings from core. A client id is now only an echo-suppression label.e22a839): ADR-082 §5 gains the "Write context" and "Reserved client ids" rows with an amendment note, andarchitecture/extension-points.mddocuments both as generic extension points.Acceptance criteria
git grepon mainaf46b30f9finds noREPLICATED_APPLY_CLIENT_ID,is_replicated_apply,write_context_oforsync-service. The only matches are the unrelatedbrowser-sync-servicemodule name.WatchNodes. The tests showing the old value no longer skips run at the core, daemon and fixture levels. Mutations that restore the old comparison are killed. The full merge gate passed on the PR stacked onto main.Sync not broken, checked before merging and again after
2b25acfagainst the Remove the superseded replicated-apply client id (closes #3435) #3754 headc361cb5fe: canary OK. All 9 checks passed (lock alignment,cargo check --workspace --all-targets, tauri config, app crate tests, frontend build, svelte-check, Sync frontend tests, import boundary, mirrors).tests/replicated_apply_rules.rs: 4/4sync_client_id_source_guard: 5/5sync_settings::: 12/12, includinga_request_presenting_the_sync_client_id_is_refused2b25acfagainst core mainaf46b30f9): success, same 9 checks.Follow-ups, outside this issue
🤖 Generated with Claude Code