~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
130 lines · 9.9 KB Raw History
plan: "0036"
title: "Record-based association: an operator marks a record (claim, association lexicon, bsky or delve post) that associates a requester or bidder repo DID, checked in both repos by the counterparty's policy trust, so agents without OAuth can declare requesters as theirs"
status: done
source: >
  2026-10-30 atirc, the user's request (binding): mark a configurable record (any record type, e.g. a bsky
  post) as the method of association so agents which cannot do OAuth can declare requesters as theirs;
  this is the foundation for subagents in the association pattern. Owner answers 07:27Z: both a
  contract-scoped and a standing-until-deleted variant; record types: a dedicated lexicon,
  app.bsky.feed.post and town.delve.feed.post (context: delve.town thread
  at://johnandersen777.bsky.social/.../3mxndop7gnk2n, pure-social agents can only post). Research:
  linux-kcp-assoc (socialweb), wasm-assoc (kcp-wasm page, kcp docs/plans/0036-wasm-research.md),
  atirc-dev-assoc (atirc). Lead and plan owner: linux-kcp.
principle: >
  The authority is always the repo in the record's at-uri; never a record field and never an issuer
  claim. The subject is always the counterparty's repo DID; its RFP or bid signature by a key bound to
  that DID is the possession proof. An association counts only when both repos agree: the authority's
  record and the subject's mirror. Matchers are exact, never substrings. Association logic is plain Go
  (trust, discovery, MarketIdentity), never only a policy action, because the page's policy engine is
  not fenced. Reads are pinned getRecord by strongRef where possible; listRecords pages with a cursor.
decisions:
  security_first: >
    Verified hole: internal/policyengine/trust/trust.go:115-134 ResolveOperatorDidFor takes the
    counterparty's own badgeBlueKeys mirror as its operator and never reads the operator repo, so any
    DID can claim any operator and pass bidder-only-me / requester-only-me and borrow the victim's
    vouches. Fix: return an operator only when the operator repo also holds the matching claim
    (deterministic rkey <service>-<did> getRecord, else paged listRecords; did-key-associator writes TID
    rkeys), the rule requester/discovery.go:103 operatedBy already uses. Declared spec.approvedAssociations
    (association.go:15) stop being mirrored as if verified. Prod check 2026-10-30: bob and alice run as
    their own accounts; john's repo holds the operator claims for its associated subjects.
  subject: >
    A requester or bidder identity is a repo DID (RFPs and bids are records in it). A new requester
    started by an agent (alice -> R2 in her VM) gets a repo: its own did:plc + Pds in the VM (owner
    2026-10-30), public ingress by default through xrpc.fedproxy.com (the did:key ingress).
  kinds: >
    claim: com.publicdomainrelay.temp.badgeBlueKeys (existing; keyId == subject, challenge ==
    authority, service == role) keeps today's operators and scripts/associate-bidder.ts working.
    association: new lexicon com.publicdomainrelay.temp.market.association {subject did, service
    requester_associate|bidder_associate, contract? strongRef, expiresAt? datetime, parent? strongRef,
    note?, createdAt}, rkey <service>-<subject with : -> ->; consider the shape of the existing
    market.bidderAssociation. bskyPost (app.bsky.feed.post) and delvePost (town.delve.feed.post): text
    equals the template (default "associate {did}", trimmed) AND one mention facet
    (app.bsky.richtext.facet#mention) whose did == subject; delve uses the same facet shape (atirc-dev-assoc).
  config: >
    Per verifier (MarketBidder for requesters, MarketRequester for bidders): association {kinds,
    postTemplate, maxDepth, requireContract}. Default kinds [claim, association]; posts are opt-in.
    v1 maxDepth 1; the parent field is in the format for chains.
  variants: >
    standing: no contract/expiresAt, valid until deleted or edited (cid pin; posts are not editable).
    contract-scoped: contract (the authority's market.accept for the VM running the subject) and
    always expiresAt (the contract deadline; atirc rejects a contract ref without it and clamps a
    minted token's exp to it); invalid after expiresAt, when the contract ref no longer resolves, or when that contract is no
    longer Held (the verifier checks the contract state, owner 2026-10-30). The authority's
    requester writes it as an AtprotoRecord owned by its ComputeContract, so release deletes it.
  proof: >
    The subject's mirror badgeBlueKeys `association` record gains proof {uri, cid} (from
    MarketIdentity status). trust.go reads the mirror, then getRecord(proof) in the challenge repo:
    cid equal, kind matcher, expiry, contract; else the claim lookup. This covers the scope lane
    (bids.go:249, DIDs only) and the full lane. An optional association strongRef in market.rfp is a
    later addition, not v1.
  bootstrap: >
    The subject starts from a pinned ref, not the record watch (the watch is stream-triggered and
    misses a record written before it exists): `run request --association-ref at://...#<cid>` and the
    same through the 0035 secrets bundle; MarketIdentity spec.associationRef {uri, cid} verified by one
    getRecord, then status.associations / associationRef and the mirror with proof. The CLI no longer
    forces Skip for a new identity that carries a ref (request.go:170).
  page: >
    The page is the operator side only: an "associate agent DID" action that writes the record (claim
    or association; posts are made from any client and pasted as an at-uri, no feed.post scope) and
    prints its at-uri, plus a display of MarketIdentity Associated / status.associations. No page
    localRepo requester mode in v1. trust.go/discovery.go reach the page from the socialweb mirror; the
    overlays compose/identity.go and app/marketrequester.go need the 3-way merge for new fields.
  atirc: >
    atirc-dev side (adopted): agentOperator gains optional assoc {uri, cid}; Owners::lookup follows it
    with the same exact matcher (uri repo == owner DID, cid pinned); mint accepts an assoc strongRef
    and embeds it; contract-scoped clamps the token exp to expiresAt; revoke via chat.atirc.revoke or
    record delete (60 s owner cache). Token exp + revoke ships first as a 0035 follow-up.
  scopes: >
    The association lexicon adds create/update/delete to RequesterOAuthScope and BidderOAuthScope,
    mirrored token for token in typescript-helpers lib/oauth-scope/mod.ts, did-key-associator
    generated/oauth-scope.js and kcp-wasm scripts/serve-public.ts; prod sessions re-sign in. App
    password agents need no scope.
state_rules:
  - "Nothing is associated without both records; a declared approval alone never produces a mirror a bidder accepts."
  - "A deleted or edited authority record revokes on the next trust refresh; the trust cache TTL bounds the delay (5 min today; wire ApplyEvent/Invalidate to the record watch)."
  - "PDS outage fails closed and is logged."
  - "A standing operator claim (badgeBlueKeys) is an independent standing grant: it associates the subject even when a contract-scoped association for the same subject has expired or its contract is released. An operator that wants scope-only association for a subject must not also hold a standing claim for it (decided 2026-10-30 by linux-kcp, owner asleep; review 2 finding)."
items:
  - id: S1
    title: "Two-repo operator check in trust.go (security fix)"
    detail: "ResolveOperatorDidFor requires the operator-side claim; paged listRecords; deterministic-rkey getRecord first; stop mirroring declared-only approvals as verified. Ships ahead of the rest in the next rc. Prod bidders run bidder-tangled-vouch since 2026-10-30, so it changes live decisions; bob's localRepo bidders resolve to bob through the operator claim."
    status: done
  - id: S2
    title: "listRecords paging and badge-binding cache TTL"
    detail: "trust.go:115/:160 and badgeblue.go read 100 records without a cursor; alice's repo holds ~85 per-run market_requester key bindings and grows. Page with a cursor, add a TTL to BadgeBlueKeyBinding, and GC stale per-run key bindings."
    status: dropped
  - id: A1
    title: "association lexicon + scopes (Go/TS mirrors)"
    status: done
  - id: A2
    title: "matcher framework: kinds claim, association, bskyPost, delvePost; config on MarketBidder/MarketRequester"
    status: done
  - id: A3
    title: "mirror proof strongRef + trust.go proof verification (both lanes), contract/expiry checks"
    status: done
  - id: A4
    title: "MarketIdentity spec.associationRef + CLI --association-ref + bundle path; pinned-getRecord bootstrap"
    status: done
  - id: A5
    title: "authority write path: requester writes a contract-owned association record for a subject it starts (app password or OAuth)"
    status: done
  - id: A6
    title: "R2 joins the owner's atirc channel: the authority's requester mints a child atirc token for the verified guest requester (subhandle r2-<accept rkey>.<authority handle>) into the bundle, revoked on release"
    status: done
  - id: A7
    title: "zero-command guest requester: a requester-originated guest.r2Start event makes socialweb-guest-agent start socialweb-r2.service (run request --guest-requester as the agent user), started from the page or run request"
    status: done
  - id: A8
    title: "parent release cascades to the guest requester (guest.r2Release/r2Released), bidder backstop at the association expiresAt, bounded denied-accept release"
    status: done
  - id: P1
    title: "page: associate-agent-DID action + Associated display (wasm)"
    status: done
  - id: R1
    title: "atirc: agentOperator.assoc, mint embeds assoc, exp clamp (atirc-dev)"
    status: done
  - id: V1
    title: "acceptance: alice (app password) requests a VM whose CLI requester R2 is associated by a contract-scoped record; bidder-only-me allows R2; a forged self-claim is denied; deleting the record revokes"
    status: done
open_questions:
  - "Q4 chains (later): does only-me compare the root authority or the immediate one; default maxDepth?"