1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
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?"