instar 1.3.526 → 1.3.528
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +135 -2
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +27 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/CoherenceJournal.d.ts +1 -1
- package/dist/core/CoherenceJournal.d.ts.map +1 -1
- package/dist/core/CoherenceJournal.js +23 -2
- package/dist/core/CoherenceJournal.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +43 -1
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/PreferencesReplicatedStore.d.ts +212 -0
- package/dist/core/PreferencesReplicatedStore.d.ts.map +1 -0
- package/dist/core/PreferencesReplicatedStore.js +330 -0
- package/dist/core/PreferencesReplicatedStore.js.map +1 -0
- package/dist/core/PreferencesSync.d.ts +8 -0
- package/dist/core/PreferencesSync.d.ts.map +1 -1
- package/dist/core/PreferencesSync.js +8 -0
- package/dist/core/PreferencesSync.js.map +1 -1
- package/dist/core/RelationshipManager.d.ts +61 -1
- package/dist/core/RelationshipManager.d.ts.map +1 -1
- package/dist/core/RelationshipManager.js +86 -2
- package/dist/core/RelationshipManager.js.map +1 -1
- package/dist/core/RelationshipsReplicatedStore.d.ts +298 -0
- package/dist/core/RelationshipsReplicatedStore.d.ts.map +1 -0
- package/dist/core/RelationshipsReplicatedStore.js +660 -0
- package/dist/core/RelationshipsReplicatedStore.js.map +1 -0
- package/dist/core/devGatedFeatures.d.ts.map +1 -1
- package/dist/core/devGatedFeatures.js +10 -0
- package/dist/core/devGatedFeatures.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +3 -1
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +4 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +1 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +7 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +19 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +64 -64
- package/src/scaffold/templates.ts +3 -1
- package/upgrades/1.3.527.md +31 -0
- package/upgrades/1.3.528.md +34 -0
- package/upgrades/side-effects/ws21-preferences-replicated-store.md +91 -0
- package/upgrades/side-effects/ws23-relationships.md +150 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
The **relationship registry is now the SECOND concrete consumer of the HLC replicated-store foundation and the FIRST PII kind** — `relationship-record` rides the foundation primitives (envelope / union-reader / conflict-store / rollback-unmerge / bounds) so a person the agent knows on one machine is known on the others. Per `docs/specs/ws23-relationships-userregistry-security.md` (§5 merge semantics, §4.2 tombstones, §4.3 right-to-erasure, §7 tests).
|
|
9
|
+
|
|
10
|
+
- **The `relationship-record` replicated kind** (`src/core/RelationshipsReplicatedStore.ts`) — a STRICT typed schema that is a **discriminated union on `op`** (a value schema AND a delete-tombstone schema coexist under one kind, REQ-D6) and **type-clamps every known field on receive** (`firstInteraction`/`lastInteraction` are ISO-8601-only, `interactionCount`/`significance` are finite numbers, free text is length-clamped — so a foreign, attacker-controlled record can't smuggle markup through a render slot that bypasses `sanitize()`, REQ-M3 gap #4/#8). The **disclosure-minimized projection** (REQ-M4) emits ONLY the enumerated resolution + merge-relevant fields — never the raw on-disk blob and never the local UUID `id`. The cross-machine `recordKey` is the **channel-set identity surface** (REQ-D17), derived deterministically from a person's sorted channel-uids, so the same human reaches the same record across machines even though each mints its own UUID. The per-entry cap is **raised to 64KB** for this PII kind (REQ-M3 gap #10) so a fat relationship replicates instead of wedging the stream; a record still over-cap after projection is a NAMED rejection, never a silent truncate. HIGH impact tier (append-both-and-flag, never a silent clobber of two divergent people).
|
|
11
|
+
- **DUAL REGISTRY** — `relationship-record` is registered in BOTH `JOURNAL_KINDS` (`CoherenceJournal.ts` — the static serve/apply/advert half, with a `DEFAULT_RETENTION` entry that is never `rotateKeep:0` for compliance) AND `ReplicatedKindRegistry` (the dynamic half). A kind in only one silently replicates nothing; the CI ratchet asserts the coupling.
|
|
12
|
+
- **Emit-on-mutation funnel + tombstones** — `RelationshipManager` routes every persistence mutation through its single `save()` funnel and every deletion through `delete()`/`mergeRelationships()`; an injected (dark-by-default) replication emitter emits a `put` on save and a channel-keyed `op:'delete'` **tombstone** on delete (so an erased person stays erased even on a machine that was offline at delete time, REQ-D4/§4.3). `mergeRelationships` emits a coherent put(survivor)+delete(merged) pair whose recordKeys are DISTINCT — no dangling tombstone, no replication loop.
|
|
13
|
+
- **Read-only neutralized union** — the peer-read surface ("what do my OTHER machines know about this person") resolves THROUGH the bypass-proof `ReplicatedStoreReader` and renders each foreign record inside a `<replicated-untrusted-data origin="…">` envelope (quoted data, never an instruction). It is DISTINCT from the local-authoritative `resolveByChannel`/`getContextForPerson` — identity RESOLUTION of an inbound principal stays local-only (REQ-M14).
|
|
14
|
+
- **Config + advert + awareness + migration** — `multiMachine.stateSync.relationships { enabled:false, dryRun:true }` added to ConfigDefaults (classified in `DARK_GATE_EXCLUSIONS`; the dark-gate line-map recomputed by hand; `applyDefaults` backfills existing agents); the `stateSyncReceive` advert self-reports `relationships` from the registry; the "One Memory" CLAUDE.md section gains a WS2.3 PII line (with the at-rest honesty note) in both `generateClaudeMd` and an idempotent `migrateClaudeMd` splicer (framework shadow markers already cover the section).
|
|
15
|
+
- **Slice** — this PR builds `relationship-record` ONLY. `user-registry` + `topic-operator` (the spec's other two PII kinds) reduce to "add schema + projection + flag" on this proven machinery and are a tracked follow-up (CMT-1416).
|
|
16
|
+
|
|
17
|
+
Pure MECHANISM, dark by default. A single-machine / flag-off agent is a strict no-op (no PII ever crosses a machine boundary while dark).
|
|
18
|
+
|
|
19
|
+
## What to Tell Your User
|
|
20
|
+
|
|
21
|
+
None while dark — internal substrate. The user-visible capability — a person I know on one machine is known on your others — becomes real only when an operator turns on cross-machine relationship replication, and it carries an **honest at-rest trade**: while on, every machine in the pool (including any cloud VM the operator rents but doesn't physically control) keeps a plaintext copy of everyone the agent knows under that machine's filesystem permissions, NOT the encrypted vault (the connection between machines IS encrypted, so nobody reads it in transit). That honesty ships in the CLAUDE.md "One Memory" section so the agent surfaces it the moment a user asks "is my contact data shared / encrypted on the other machine?".
|
|
22
|
+
|
|
23
|
+
## Summary of New Capabilities
|
|
24
|
+
|
|
25
|
+
None user-facing while dark. New internal module `RelationshipsReplicatedStore.ts`; `RelationshipManager` gains injected (dark) replication-emit + union-read seams. No new routes (the foundation `/state/conflicts` · `/state/resolve-conflict` · `/state/quarantine` surface is reused).
|
|
26
|
+
|
|
27
|
+
## Evidence
|
|
28
|
+
|
|
29
|
+
- `tests/unit/RelationshipsReplicatedStore.test.ts` (35) — dual-registry coupling; recordKey identity derivation (no split identity, no stranger-collision, not the UUID); disclosure-minimization (no local id, no extra field); `fat-record-replicates` + `fat-record-does-not-wedge-stream`; `foreign-record-type-clamped` (ISO-8601 / finite-number clamps reject smuggled markup; unknown field dropped; freetext clamped); `tombstone-coexists-with-value` (the op:'delete' schema branch); the HIGH-impact append-both union merge; foreign-record render safety. Green.
|
|
30
|
+
- `tests/unit/relationship-replication-emit.test.ts` (6) — emit-on-every-mutation; DARK no-op; delete emits a channel-keyed tombstone; `mergeRelationships` put+delete coherence (distinct recordKeys); a throwing emitter never breaks the local write. Green.
|
|
31
|
+
- `tests/unit/relationship-union-read.test.ts` (5) — through the REAL `ReplicatedStoreReader`: union-reader-cannot-be-bypassed (disabled = no-op); append-both; `erasure-reaches-offline-peer` (tombstone wins over a stale value); post-unmerge zero-dangling. Green.
|
|
32
|
+
- `tests/unit/ws23-relationships-wiring.test.ts` (9) — dual-registry + server.ts registration/union-reader/peer-read-seam wiring + ConfigDefaults dark default + the awareness section. Green.
|
|
33
|
+
- `tests/integration/ws23-relationships-routes.test.ts` (3) — the relationship routes still work with the replication funnel attached; a route-driven DELETE fires the channel-keyed tombstone through the funnel. Green.
|
|
34
|
+
- `tests/e2e/ws23-relationships-alive.test.ts` (3, Phase 1) — the WS2.3 path is ALIVE on the real AgentServer boot path with the REAL relationship-record schema registered: an enabled relationship-record conflict is open + readable + resolvable over HTTP (200), disabled returns 503, Bearer auth required. Green.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Side-Effects Review — WS2.1 preferences replicated store
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `ws21-preferences-replicated-store`
|
|
4
|
+
**Author:** `echo`
|
|
5
|
+
**Second-pass reviewer:** `not required`
|
|
6
|
+
**Spec:** `docs/specs/multi-machine-replicated-store-foundation.md` (converged + approved)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Wire the auto-learned preference store (`PreferencesManager`, `.instar/preferences.json`)
|
|
11
|
+
onto the HLC replicated-store foundation as the FIRST concrete `pref-record` replicated
|
|
12
|
+
kind. A preference learned on one machine is honored on the others — read through the
|
|
13
|
+
no-clobber union with operator-resolved conflicts. New module
|
|
14
|
+
`src/core/PreferencesReplicatedStore.ts` holds the store schema, impact tier (high),
|
|
15
|
+
per-kind bounds, the emit-envelope builder, and the load-bearing union-aware read
|
|
16
|
+
(`mergeUnionToPreferences` / `buildUnionSessionContext`). `pref-record` is registered in
|
|
17
|
+
BOTH `JOURNAL_KINDS` (CoherenceJournal — static half) AND `ReplicatedKindRegistry`
|
|
18
|
+
(dynamic half). The `/preferences/session-context` route consults a wired
|
|
19
|
+
`ReplicatedStoreReader` ONLY when `multiMachine.stateSync.preferences.enabled`. Ships
|
|
20
|
+
DARK (`enabled:false`, `dryRun:true`).
|
|
21
|
+
|
|
22
|
+
## Decision-point inventory
|
|
23
|
+
|
|
24
|
+
1. **Impact-tier resolution (spec §15.1):** preferences are HIGH-impact at the
|
|
25
|
+
replication layer (append-both-and-flag), but the consumer read injects BOTH
|
|
26
|
+
variants as advisory hints on an open conflict — never blocking. Decision recorded
|
|
27
|
+
in the build prompt + the spec; the conservative side (never silently clobber).
|
|
28
|
+
2. **Coordination with the legacy PreferencesSync (CMT-1416):** the foundation path
|
|
29
|
+
SUPERSEDES the seamlessness path; both ship dark → zero runtime duplication. The
|
|
30
|
+
route gives the foundation path precedence when its flag is on.
|
|
31
|
+
|
|
32
|
+
## 1. Over-block
|
|
33
|
+
|
|
34
|
+
**What legitimate inputs does this change reject?** The pref-record store schema
|
|
35
|
+
rejects a record with an empty `learning` or a path-shaped `provenance` (the §4 jail).
|
|
36
|
+
Both are malformed-data rejections at the RECEIVE door (a replicated record from a
|
|
37
|
+
peer), never a user-initiated action — a local `recordPreference` is unaffected. With
|
|
38
|
+
the flag OFF (the default), nothing is emitted, validated, or read through the union:
|
|
39
|
+
strict no-op, byte-identical single-machine behavior. The route's foundation branch is
|
|
40
|
+
gated on `stateSync.preferences.enabled` so a dark agent keeps its exact legacy read.
|
|
41
|
+
|
|
42
|
+
## 2. Under-block
|
|
43
|
+
|
|
44
|
+
**What does this still miss?** The `loadOriginRecords` seam reads only the OWN
|
|
45
|
+
preference store today — peer `pref-record` replicas land via the journal apply path in
|
|
46
|
+
a later rollout stage. So on a single-machine / pre-apply agent the union is a no-op
|
|
47
|
+
(just the local record). This is the honest dark-ship boundary, not a gap: the union /
|
|
48
|
+
conflict / both-variants logic is fully exercised by unit tests against synthetic
|
|
49
|
+
multi-origin inputs. The `dryRun` mode (log intended merges without mutating) is the
|
|
50
|
+
config ladder's middle rung; the emit/apply transport wiring is gated by the same flag.
|
|
51
|
+
|
|
52
|
+
## 3. Level-of-abstraction fit
|
|
53
|
+
|
|
54
|
+
**Right layer?** Yes. The new kind rides the existing foundation primitives
|
|
55
|
+
(ReplicatedRecordEnvelope / UnionReader / ConflictStore / RollbackUnmerge /
|
|
56
|
+
ReplicationBudget / StoreSnapshot) — no foundation change beyond registering the kind in
|
|
57
|
+
the dual registry. The bypass-proof read goes through `ReplicatedStoreReader` (the
|
|
58
|
+
single funnel), so no caller can read the raw store around the no-clobber rule. The
|
|
59
|
+
consumer-specific concerns (the dedupeKey↔recordKey mapping, the local-only
|
|
60
|
+
`violationPattern` strip, the both-variants reconciliation) live in one module.
|
|
61
|
+
|
|
62
|
+
## 4. Reversibility
|
|
63
|
+
|
|
64
|
+
Fully reversible. The whole feature is dark behind `multiMachine.stateSync.preferences`
|
|
65
|
+
(default `enabled:false`). Disabling it is a strict no-op (the route keeps its legacy
|
|
66
|
+
path; no emission, no union read). A peer's contribution can be rolled back via the
|
|
67
|
+
existing RollbackUnmerge (`/state/quarantine`), which the foundation already wires. No
|
|
68
|
+
destructive operation is introduced; the conflict ledger + dropped-origins set are the
|
|
69
|
+
foundation's existing durable state under `.instar/state/state-sync/`.
|
|
70
|
+
|
|
71
|
+
## 5. Migration / fleet-rollout surface
|
|
72
|
+
|
|
73
|
+
- **Config:** `multiMachine.stateSync.preferences { enabled:false, dryRun:true }` added
|
|
74
|
+
to `ConfigDefaults` — `applyDefaults` add-missing backfills existing agents on update
|
|
75
|
+
(Migration Parity; no bespoke migrateConfig block needed). Classified in
|
|
76
|
+
`DARK_GATE_EXCLUSIONS` (optional-integration, staged rollout) so the literal
|
|
77
|
+
`enabled:false` is accounted for by the dark-gate lint.
|
|
78
|
+
- **CLAUDE.md awareness:** the existing "One Memory (replicated stores)" section gains a
|
|
79
|
+
WS2.1 preferences-consumer line in BOTH `generateClaudeMd` (new agents) and
|
|
80
|
+
`migrateClaudeMd` (existing agents, via an idempotent content-sniff splicer guarded by
|
|
81
|
+
the unique "Preferences are the FIRST live store" marker).
|
|
82
|
+
- **Dual-registry CI ratchet:** the `ReplicatedRecordEnvelope.test.ts` coupling test now
|
|
83
|
+
asserts `pref-record` is coupled in both registries — a future edit removing it from
|
|
84
|
+
`JOURNAL_KINDS` would RED.
|
|
85
|
+
|
|
86
|
+
## Verdict
|
|
87
|
+
|
|
88
|
+
Safe to ship dark. Zero behavior change for any agent until an operator flips
|
|
89
|
+
`multiMachine.stateSync.preferences.enabled`. The advisory both-variants read is the
|
|
90
|
+
load-bearing correctness property and is proven by a §12 wiring test (an open conflict
|
|
91
|
+
never suppresses a usable hint).
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Side-Effects Review — WS2.3 relationships (first PII replicated store)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `ws23-relationships`
|
|
4
|
+
**Author:** `echo`
|
|
5
|
+
**Second-pass reviewer:** `not required (dark-ship, observe-only until flag flip)`
|
|
6
|
+
**Spec:** `docs/specs/ws23-relationships-userregistry-security.md` (converged + approved, CMT-1413)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Wire the relationship registry (`RelationshipManager`, `relationships/<id>.json`) onto
|
|
11
|
+
the HLC replicated-store foundation as the SECOND concrete consumer and the FIRST PII
|
|
12
|
+
kind, `relationship-record`. New module `src/core/RelationshipsReplicatedStore.ts` holds
|
|
13
|
+
the strict typed schema (discriminated union on `op` for value + tombstone, with
|
|
14
|
+
per-field type-clamps), the disclosure-minimized projection, the channel-set identity
|
|
15
|
+
recordKey derivation, the HIGH impact tier, per-kind bounds, the 64KB per-entry cap,
|
|
16
|
+
the tombstone builder, the union-aware read, and the foreign-record render-safety
|
|
17
|
+
helper. `relationship-record` is registered in BOTH `JOURNAL_KINDS` (CoherenceJournal —
|
|
18
|
+
static half) AND `ReplicatedKindRegistry` (dynamic half). `RelationshipManager` gains
|
|
19
|
+
two injected, dark-by-default seams: a replication emitter (a `put` on the save funnel,
|
|
20
|
+
a channel-keyed `op:'delete'` tombstone on delete/merge) and a union-read seam (the
|
|
21
|
+
read-only neutralized peer-context surface). Ships DARK
|
|
22
|
+
(`multiMachine.stateSync.relationships { enabled:false, dryRun:true }`).
|
|
23
|
+
|
|
24
|
+
## Decision-point inventory
|
|
25
|
+
|
|
26
|
+
1. **recordKey = channel-set identity surface, NOT the local UUID (REQ-D17).** The on-disk
|
|
27
|
+
`id` is per-machine; the same human has different UUIDs on different machines, so the
|
|
28
|
+
replicated key is a deterministic `sha256(sorted(channel-uids))`. This is the
|
|
29
|
+
single most load-bearing identity decision; it is documented in the module and proven
|
|
30
|
+
by the recordKey tests (same person → same key regardless of order; disjoint people →
|
|
31
|
+
different keys; channel-less → null, not replicable).
|
|
32
|
+
2. **64KB per-entry cap (REQ-M3 gap #10).** A fat relationship exceeds the default 8KB
|
|
33
|
+
applier cap; raised to 64KB so the highest-PII records replicate, with a NAMED
|
|
34
|
+
over-cap rejection (`RelationshipRecordTooLargeError`) rather than silent truncate or
|
|
35
|
+
suspect-wedge.
|
|
36
|
+
3. **mergeRelationships put+delete coherence (the sharp edge).** A local merge emits a
|
|
37
|
+
survivor `put` (now carrying the consolidated channel set) AND a tombstone keyed on
|
|
38
|
+
the merged record's OLD standalone channel set. The two recordKeys are DISTINCT (the
|
|
39
|
+
survivor subsumes the merged's channels), so the tombstone can never suppress the
|
|
40
|
+
survivor — no dangling tombstone, no replication loop. Proven by the merge test.
|
|
41
|
+
4. **Slice (CMT-1416).** Only `relationship-record` ships here; user-registry +
|
|
42
|
+
topic-operator are a tracked follow-up on the proven machinery.
|
|
43
|
+
|
|
44
|
+
## 1. Over-block
|
|
45
|
+
|
|
46
|
+
**What legitimate inputs does this change reject?** The receive-side schema rejects a
|
|
47
|
+
foreign record with a non-ISO-8601 `firstInteraction`/`lastInteraction`, a non-number
|
|
48
|
+
`interactionCount`/`significance`, a missing/empty `name`, a bad channel, or > MAX_CHANNELS
|
|
49
|
+
channels — all malformed-data rejections at the RECEIVE door (a peer's replicated
|
|
50
|
+
record), never a user-initiated local action. A local `findOrCreate`/`updateNotes`/… is
|
|
51
|
+
unaffected (it goes through the normal save path; the emit is best-effort AFTER the
|
|
52
|
+
durable local write and swallows its own failures). With the flag OFF (default), nothing
|
|
53
|
+
is emitted, validated, or read through the union: strict no-op — NO PII ever crosses a
|
|
54
|
+
machine boundary.
|
|
55
|
+
|
|
56
|
+
## 2. Under-block
|
|
57
|
+
|
|
58
|
+
**What does this still miss?** The `loadOriginRecords` seam reads only the OWN
|
|
59
|
+
relationship store today — peer `relationship-record` replicas land via the journal
|
|
60
|
+
apply path in a later rollout stage (the same staging as WS2.1's shipped reference). So
|
|
61
|
+
on a single-machine / pre-apply agent the union is a no-op (just the local record); the
|
|
62
|
+
union / conflict / append-both / tombstone-wins logic is fully exercised by unit tests
|
|
63
|
+
against synthetic multi-origin inputs and through the real `ReplicatedStoreReader`. The
|
|
64
|
+
spec's deeper PII obligations beyond this slice (operator-mandate-gated destructive
|
|
65
|
+
local delete on receive, the deferred-erasure queue, the merge-skew gate, AAD binding)
|
|
66
|
+
are tracked in the converged spec and ride later increments; this PR establishes the
|
|
67
|
+
schema/projection/recordKey/tombstone/cap machinery they build on. The at-rest
|
|
68
|
+
plaintext-rest exposure delta is the operator-accepted residual (REQ-A2) stated honestly
|
|
69
|
+
in the CLAUDE.md awareness section.
|
|
70
|
+
|
|
71
|
+
## 3. Level-of-abstraction fit
|
|
72
|
+
|
|
73
|
+
**Right layer?** Yes. The new kind rides the existing foundation primitives
|
|
74
|
+
(ReplicatedRecordEnvelope / UnionReader / ConflictStore / RollbackUnmerge /
|
|
75
|
+
ReplicationBudget) — no foundation change beyond registering the kind in the dual
|
|
76
|
+
registry. The bypass-proof read goes through `ReplicatedStoreReader` (the single
|
|
77
|
+
funnel). The PII-specific concerns (the channel-set recordKey, the type-clamp schema,
|
|
78
|
+
the disclosure-minimized projection, the tombstone, the untrusted-data render envelope)
|
|
79
|
+
live in one module. The emit/read seams on `RelationshipManager` are injected so the
|
|
80
|
+
manager stays pure-testable and dark by default.
|
|
81
|
+
|
|
82
|
+
## 4. Reversibility
|
|
83
|
+
|
|
84
|
+
Fully reversible. The whole feature is dark behind
|
|
85
|
+
`multiMachine.stateSync.relationships` (default `enabled:false`). Disabling it is a
|
|
86
|
+
strict no-op (no emission, no union read, no peer view). A peer's contribution rolls
|
|
87
|
+
back via the existing RollbackUnmerge (`/state/quarantine`). No destructive operation is
|
|
88
|
+
introduced in THIS slice: the emit funnel only adds a best-effort `put`/tombstone
|
|
89
|
+
AFTER the durable local write; it never blocks or alters a local mutation, and a
|
|
90
|
+
throwing emitter cannot break a local write (proven). The destructive-local-delete-on-
|
|
91
|
+
receive (mandate-gated) is explicitly OUT of this slice.
|
|
92
|
+
|
|
93
|
+
## 5. Migration / fleet-rollout surface
|
|
94
|
+
|
|
95
|
+
- **Config:** `multiMachine.stateSync.relationships { enabled:false, dryRun:true }` added
|
|
96
|
+
to `ConfigDefaults` — `applyDefaults` add-missing backfills existing agents (Migration
|
|
97
|
+
Parity; no bespoke migrateConfig block needed). Classified in `DARK_GATE_EXCLUSIONS`
|
|
98
|
+
(optional-integration); the hand-authored dark-gate line-map was recomputed via the
|
|
99
|
+
attributor (regeneration forbidden) and the new `relationships.enabled` path + the +14
|
|
100
|
+
shift below it hand-edited.
|
|
101
|
+
- **CLAUDE.md awareness:** the existing "One Memory (replicated stores)" section gains a
|
|
102
|
+
WS2.3 relationships-consumer line (WITH the at-rest PII honesty note) in BOTH
|
|
103
|
+
`generateClaudeMd` (new agents) and `migrateClaudeMd` (existing agents, via an
|
|
104
|
+
idempotent content-sniff splicer guarded by the unique "Relationships are the FIRST PII
|
|
105
|
+
store" marker). The framework shadow markers already cover the section (both `**` and
|
|
106
|
+
`### ` variants), so Codex/Gemini parity is inherited.
|
|
107
|
+
- **Dual-registry CI ratchet:** the `ReplicatedRecordEnvelope.test.ts` coupling test +
|
|
108
|
+
the new wiring test assert `relationship-record` is coupled in both registries — a
|
|
109
|
+
future edit removing it from `JOURNAL_KINDS` would RED.
|
|
110
|
+
|
|
111
|
+
## Adversarial review (5 lenses — PII-grade)
|
|
112
|
+
|
|
113
|
+
Total findings: **3 real, 3 folded** (0 deferred).
|
|
114
|
+
|
|
115
|
+
1. **Injection / type-clamp completeness** — FINDING (folded): the `firstInteraction`/
|
|
116
|
+
`lastInteraction` fields render UNSANITIZED in `getContextForPerson`, so a foreign
|
|
117
|
+
record could smuggle markup through them. Folded: the schema validates them as
|
|
118
|
+
ISO-8601-only (rejecting any string with `<`/`>`/`"` or a non-date), and the foreign
|
|
119
|
+
render path additionally escapes every field inside the untrusted-data envelope —
|
|
120
|
+
two independent defenses. Proven by `injection-neutralized-firstInteraction` +
|
|
121
|
+
`schema-type-clamp`.
|
|
122
|
+
2. **Disclosure minimization** — FINDING (folded): the local UUID `id` must never leave
|
|
123
|
+
the machine. Folded: it is absent from `RELATIONSHIP_STORE_KNOWN_FIELDS` and the
|
|
124
|
+
projection builder never emits it; the `disclosure-minimization` test asserts every
|
|
125
|
+
emitted field is in the allowlist and `id` is absent.
|
|
126
|
+
3. **Tombstone / erasure correctness** — FINDING (folded): `mergeRelationships` could
|
|
127
|
+
emit a tombstone that suppresses the survivor (the merged channels are now part of
|
|
128
|
+
the survivor). Folded: the tombstone keys on the merged's OLD standalone channel set,
|
|
129
|
+
whose recordKey is DISTINCT from the survivor's consolidated recordKey — proven by
|
|
130
|
+
the merge test (distinct keys) and the union test (`erasure-reaches-offline-peer`,
|
|
131
|
+
tombstone-wins-over-stale-value). No dangling tombstone, no loop.
|
|
132
|
+
4. **recordKey identity derivation** — no real finding: same channel set → same key
|
|
133
|
+
(order-independent); disjoint sets → different keys; the same person with different
|
|
134
|
+
UUIDs → same key. Collision requires the EXACT same full channel set, which is the
|
|
135
|
+
manager's own definition of "the same person". Proven by the recordKey tests.
|
|
136
|
+
5. **Flag-coherence PII-leak prevention** — no real finding: emission is gated on
|
|
137
|
+
`isStoreEmissionEnabled` + the per-peer `shouldEmitToPeer` advertise check (foundation
|
|
138
|
+
machinery, REQ-M5); a dark agent never emits, and a peer that does not advertise
|
|
139
|
+
`relationships` is never a forward target. The advert self-reports from registry
|
|
140
|
+
presence + the enabled flag. (Live serve/pull transport is the later rollout stage,
|
|
141
|
+
gated by the same flag; THIS slice keeps PII strictly local until the operator flips
|
|
142
|
+
the flag.)
|
|
143
|
+
|
|
144
|
+
## Verdict
|
|
145
|
+
|
|
146
|
+
Safe to ship dark. Zero behavior change — and zero PII egress — for any agent until an
|
|
147
|
+
operator flips `multiMachine.stateSync.relationships.enabled`. The channel-set recordKey,
|
|
148
|
+
the type-clamped discriminated-union schema, the disclosure-minimized projection, the
|
|
149
|
+
channel-keyed tombstone, and the untrusted-data render envelope are the load-bearing PII
|
|
150
|
+
correctness properties and are each proven by a named test.
|