Skip to content

id: investigate: trust/visibility model — servers accept all signed objects; views count only allowed writers #2030

Closed

Description

@dezren39

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.

Activity

  1. added
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    on Oct 29, 2026
  2. developing-today commented on Oct 29, 2026

    @developing-today
    Contributor
  3. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    Parent epic: #2056 — ID platform expansion (2026-10-28 directive).

  4. added
    priority/highImportant and time-sensitive
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 29, 2026
  5. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    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.

  6. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 29, 2026
  7. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  8. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    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)

    1. 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.
    2. 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.
    3. 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:

    • Per-author and per-topic storage quotas at the relay/server, enforced as resource refusals (over quota → 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.
    • 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

    1. 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.
    2. 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.
    3. Storage exhaustion against a relay: attacker submits validly-signed bulk to fill a server. This is a resource attack, answered by id: investigate: comms abuse controls — blocks, rate limits, and per-counterparty budgets #2032's budgets (per-IP/per-ID/per-topic MB windows, block lists) and the uncounted-tier expiry above. Validity is not a storage entitlement. ✅ With id: investigate: comms abuse controls — blocks, rate limits, and per-counterparty budgets #2032 controls in place; without them this is the model's real weak point.
    4. 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.
    5. 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.
    6. 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.
    7. 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)

    Follow-on routing (same pass)

    🤖 Agent note · done — queue worker (repo-worker-code), lane C, task 3346. Investigation recorded here; follow-on filed + enqueued in the same pass.

  9. 12 remaining items

  10. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · parked

  11. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Oct 30, 2026
  12. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up (resume)

  13. added a commit that references this issue on Oct 30, 2026
  14. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

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

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)priority/highImportant and time-sensitive

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions