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
id: investigate: trust/visibility model — servers accept all signed objects; views count only allowed writers #2030
LIST 7b. The ID trust model: any server accepts any validly-signed object — anyone may submit a signed claim (a person in Montana claims they walked my dog). But MY metrics and views count only my allowed writer set: specific IDs, a friends list, a group, or public. Other claims exist, signed with their author's name, and surface only if I go looking. Investigate: writer-set representation, view-time filtering vs ingest-time rejection (filtering is the design), what 'going looking' looks like in UI/API, and how this stays spam-proof when storage accepts everything. Deliverable: model write-up with abuse cases walked through (cross-ref the abuse-controls investigation).
Program: Drew's 2026-10-28 directive (full text: hatch-personal memory/topics/id-program-2026-10-28.md).
Working pattern: parallel investigations, then the named synthesis issue merges them into a full design. Route guidance: Workhorse by default; Specialist/Summit for synthesis and ramification-heavy work. A result that says "done" is not the endpoint — the orchestrator reviews it and may commission another round. Deliverables: a markdown design/investigation doc (mermaid/SVG diagrams welcome) linked from this issue, plus follow-up issues for anything buildable. Build a proof of concept where the design calls for one, but do NOT build the permanent feature on top of an unreviewed PoC.
Top priority — ID program wave-1. Drew directed this be picked up in the next runs (2026-10-29): enqueued directly as triage task 3346 in repo-health-code-pilot at queue priority 20 (only breakage-class work at priority 10 outranks it). Wave-1 is the ungated investigations and designs; syntheses and PoCs stay gated on their named inputs. Investigation route: Workhorse.
Investigation result — trust/visibility model (#2030, queue task 3346)
Verdict
The premise is verified, and it is already the shape of the code. In pkgs/id, ingest is a validity gate, not an authority gate, and "stored" is already separate from "counted":
pkgs/id/src/event_store/ingest.rs — EventStore::ingest verifies canonical encoding + signature (verify_strict via chain::decode) and extension rules; the only refusals are Malformed, duplicate, and Frozen (past an author's fork point). The module doc states the Phase 1 rule "has no membership or authority model to void by."
pkgs/id/src/event_store/claims.rs — a tag event "is applied like any content event: the resolution decides whether it applies, and the store keeps it either way." tag_claims() folds only applied events.
pkgs/id/src/membership/gate.rs — the admission gate explicitly "does not validate authority."
So the design below is not a new architecture; it is naming and completing a layer the store already has: validity at ingest → everything stored → per-view projection decides what counts. The missing piece is the writer-set projection, which today exists only implicitly (applied/not-applied per author chain), not as a per-view, per-owner concept.
The model (three layers, kept strictly separate)
Ingest = validity only. Signature valid, encoding canonical, chain rules hold (sequence/backlink/fork rules as today) → the object is stored, attributed to its signer. Ingest never asks "is this author allowed to affect anyone's view?" — that question has a different answer per viewer, so it cannot be asked once at ingest.
Storage = evidence. Stored objects are signed, attributable, and permanent by default (subject to the resource tiering under "costs" below). A stored claim is a fact about its author ("X signed this"), never by itself a fact about its subject.
View = projection over a writer set. Every metric, feed, or aggregate is computed by a named view owned by someone (a person, a group, an app). The view carries a writer set; only objects whose author ∈ the set count. Everything else is uncounted — present, signed, discoverable, with zero effect on the view.
Writer-set representation
A writer set is itself a signed object owned by the view owner, versioned by the owner's own chain (so set changes are auditable events, not silent config):
Entries are a union of: explicit IDs (public keys); a group reference (resolves through the existing groups layer — pkgs/id/src/groups/policy.rs, friendship.rs — so "my friends" and "the dog-care group" are live references, not copies); and the public sentinel (everyone — a deliberate per-view choice, never a default for metrics).
Per-topic scoping: an entry may be scoped to a topic/tag, so the dog-walker's claims count for dog events and nothing else. Unscoped entries are the exception, not the rule.
Depth rule for group references: one level only by default. If view → group G → group H expanded transitively without limit, one compromised or careless group admin silently rewrites many views. A view that wants H names H.
Set changes apply prospectively by default. Adding a writer counts their past stored claims only if the owner explicitly chooses "include history" (that is often exactly right for a friend — and exactly wrong to do silently). Removing a writer stops future counting immediately; their already-counted past claims stay counted unless the owner issues an explicit rescind, which excludes that writer's claims from a stated point (or entirely) and is itself a visible, signed set-version event. Silent history-rewriting is the one behaviour this model must never have: two viewers comparing "the same" metric must be able to see which set version produced it. Every computed view should therefore be able to name (set version, frontier) alongside its result.
View-time filtering vs ingest-time rejection
Ingest rejection (by writer)
View-time filtering (chosen)
Who decides?
The server, once, for everyone
Each view owner, per view
Same object, two viewers
Must agree or one is refused service
Counts for one, not the other — the normal case
"A stranger claims they walked my dog"
Server must judge a claim it has no standing to judge
Stored, signed, counts nowhere unless I look
Reversibility
Rejection destroys evidence
Adding the writer later can count stored history
Failure mode
Gatekeeper capture / censorship arguments
Storage cost (below)
Filtering wins on the program's own terms. Its honest cost: filtering makes spam ineffective, not free. Storage and bandwidth for objects nobody counts is the price, and it is paid with resource controls, never by quietly turning ingest back into an authority gate:
Retention tiering: counted objects are hot/permanent; uncounted objects are cold, and a server may expire uncounted objects after a published retention window unless someone pins, counts, or discovers-and-saves them. Expiry is a storage policy, stated up front, not a judgement on the claim.
Author-indexed projections: views must never scan the whole store. An author → objects index (the store's per-author chains already give this shape) means a view's cost is proportional to its writer set, not to global spam volume. This is what keeps a Sybil flood from becoming a compute DoS on every view.
"Going looking" — discovery of uncounted claims
Uncounted must mean findable, or the model is just a bin. The discovery surface (UI + API are the same query):
Query by subject/topic, not by author: "show me all claims about this dog / this event / this entity" returns counted and uncounted, visually and structurally separated, each uncounted claim showing: author ID (+ petname if known), signature status, and why it is not counted ("author not in this view's writer set").
Two promotion actions, deliberately different:count this claim (one-off inclusion of a single object, recorded as a view-level exception) vs add this writer (set change, prospective, optionally with history). Collapsing these into one button is how writer sets quietly bloat.
No ambient leakage: uncounted claims never appear inside the counted view itself — no greyed-out rows in the metric, no "plus 3 hidden claims" badge that functions as an advertisement. Discovery is a place you go, not a nudge. (A per-view opt-in "N uncounted claims exist about subjects in this view" indicator is acceptable; default off for metrics.)
Abuse cases walked through
The Montana stranger claims they walked my dog. Stored, signed by them. My did-it view counts only my writer set → the claim has zero effect on my metrics. If I go looking, I find it, attributed, and can ignore it, count just it, or (if it turns out to be my neighbour) add the writer. ✅ Model holds.
Sybil flood: an attacker mints 10,000 keys and floods claims about my dog to pollute discovery. Metrics: unaffected (none are in my set). Discovery: results are grouped by author with per-author counts, so the flood reads as one suspicious cluster, not 10,000 facts; server-side per-topic quotas + cold tiering bound the storage. Residual risk: discovery UX under flood needs author-clustering to stay usable — named as follow-on work, not solved here. ⚠️ Holds for metrics; discovery needs the clustering.
Confusion attack: spam claims crafted to look counted (similar petname, copied formatting). Defence is presentational and non-negotiable: counted status is computed from the set, never displayed from claim-supplied data; uncounted claims always render author + "not counted — author not in writer set." ✅ If the UI rule holds.
Writer-set poisoning: I am socially engineered into adding an attacker. Their claims now count — correctly, per my set. Mitigations: set changes are signed, versioned, and reviewable ("your set changed 3 times this week"); rescind exists; per-topic scoping limits blast radius. The model cannot fix an owner decision; it can make it visible and reversible. ⚠️ Accepted residual risk, made auditable.
Group-reference capture: my view references a group; its admin adds a spammer. One-level expansion + the view rendering which reference admitted this writer ("counted via group: Dog Care") makes it diagnosable; I can exclude a single writer with a view-level exception without leaving the group. ⚠️ Requires the exclusion-entry mechanism — included in the follow-on spec.
Removed writer disputes history: a dog-walker I removed claims their past walks must still count / must be erased. Default rule answers both: past stays counted (it was true under the set then), rescind is available to me as owner, and the walker's own stored claims remain their evidence regardless — removal from my view deletes nothing from the store. ✅
What this investigation does NOT decide (for the synthesis / follow-on)
Concrete wire/storage format for writer-set objects and view definitions in pkgs/id (follow-on spec, below).
Filed follow-on design-spec issue id: design: writer-set + view-projection spec for pkgs/id — representation, set-change semantics, discovery query (from #2030) #2238 for the buildable piece — writer-set object format, view-projection + author-index, exclusion entries, and discovery query in pkgs/id — and enqueued it to repo-health-code-pilot as triage task 3395 with queue/queued labelling on the issue. It is a design spec, not a build order: per the program rule, no permanent feature gets built on this investigation without the review round.
Activity
developing-today commentedon Oct 29, 2026
CODE-951
dezren39 commentedon Oct 29, 2026
Parent epic: #2056 — ID platform expansion (2026-10-28 directive).
dezren39 commentedon Oct 29, 2026
Top priority — ID program wave-1. Drew directed this be picked up in the next runs (2026-10-29): enqueued directly as triage task 3346 in repo-health-code-pilot at queue priority 20 (only breakage-class work at priority 10 outranks it). Wave-1 is the ungated investigations and designs; syntheses and PoCs stay gated on their named inputs. Investigation route: Workhorse.
dezren39 commentedon Oct 29, 2026
🤖 Agent note · picked up
dezren39 commentedon Oct 29, 2026
Investigation result — trust/visibility model (#2030, queue task 3346)
Verdict
The premise is verified, and it is already the shape of the code. In
pkgs/id, ingest is a validity gate, not an authority gate, and "stored" is already separate from "counted":pkgs/id/src/event_store/ingest.rs—EventStore::ingestverifies canonical encoding + signature (verify_strictviachain::decode) and extension rules; the only refusals areMalformed, duplicate, andFrozen(past an author's fork point). The module doc states the Phase 1 rule "has no membership or authority model to void by."pkgs/id/src/event_store/claims.rs— a tag event "is applied like any content event: the resolution decides whether it applies, and the store keeps it either way."tag_claims()folds only applied events.pkgs/id/src/membership/gate.rs— the admission gate explicitly "does not validate authority."So the design below is not a new architecture; it is naming and completing a layer the store already has: validity at ingest → everything stored → per-view projection decides what counts. The missing piece is the writer-set projection, which today exists only implicitly (applied/not-applied per author chain), not as a per-view, per-owner concept.
The model (three layers, kept strictly separate)
Writer-set representation
A writer set is itself a signed object owned by the view owner, versioned by the owner's own chain (so set changes are auditable events, not silent config):
pkgs/id/src/groups/policy.rs,friendship.rs— so "my friends" and "the dog-care group" are live references, not copies); and thepublicsentinel (everyone — a deliberate per-view choice, never a default for metrics).View-time filtering vs ingest-time rejection
Filtering wins on the program's own terms. Its honest cost: filtering makes spam ineffective, not free. Storage and bandwidth for objects nobody counts is the price, and it is paid with resource controls, never by quietly turning ingest back into an authority gate:
Retry-After, per the id: investigate: comms abuse controls — blocks, rate limits, and per-counterparty budgets #2032 abuse-controls investigation) — a quota refusal says "no room," never "not allowed," and the object remains valid and storable elsewhere."Going looking" — discovery of uncounted claims
Uncounted must mean findable, or the model is just a bin. The discovery surface (UI + API are the same query):
Abuse cases walked through
What this investigation does NOT decide (for the synthesis / follow-on)
pkgs/id(follow-on spec, below).publicviews need their own moderation layer (a public metric is an invitation to game it; likely public views should be read-only discoveries, not metrics — flagged for the core design id: design: ID core build v1 — app model + capability system, enough to start app examples #2028).Follow-on routing (same pass)
pkgs/id— and enqueued it torepo-health-code-pilotas triage task 3395 withqueue/queuedlabelling on the issue. It is a design spec, not a build order: per the program rule, no permanent feature gets built on this investigation without the review round.🤖 Agent note · done — queue worker (repo-worker-code), lane C, task 3346. Investigation recorded here; follow-on filed + enqueued in the same pass.
12 remaining items
dezren39 commentedon Oct 30, 2026
🤖 Agent note · parked
queue/3920-2030-trust-visibility-model· docdoc/2026-10-30T19-11-04Z_investigation_trust_visibility_model/docs: Sybil-resistant discovery is unresolved — correct the per-autho…
dezren39 commentedon Oct 30, 2026
🤖 Agent note · picked up (resume)
1ca3e9e1: §5 and abuse case 2 now state per-author grouping only collapses single-author floods, and Sybil-resistant discovery is explicitly unresolved (routed to the id: design: writer-set + view-projection spec for pkgs/id — representation, set-change semantics, discovery query (from #2030) #2238 spec and id: design: abuse-control primitives v1 for ID comms #2033). Replied on the Codex thread and resolved it.Merge pull request #2801 from developing-today/queue/3920-2030-trust-…
dezren39 commentedon Oct 30, 2026
🤖 Agent note · done
74093ef4). Doc:doc/2026-10-30T19-11-04Z_investigation_trust_visibility_model/2026-10-30T19-11-04Z_investigation_trust_visibility_model.md. Model: validity at ingest → everything stored as evidence → per-view projection over a writer set decides what counts; writer-set representation, view-time filtering placement/cost, "going looking" discovery, and seven abuse cases walked through.1ca3e9e1(§5/§6 now state Sybil-resistant discovery is explicitly unresolved, routed to id: design: writer-set + view-projection spec for pkgs/id — representation, set-change semantics, discovery query (from #2030) #2238/id: design: abuse-control primitives v1 for ID comms #2033), thread replied + resolved, Codex re-review of the new head completed with no findings.feat(id): issuer key rotation and compromise recovery — protocol core (…