--- id: write-policy title: Which agents may write which record types status: open crates: [didbot-pds, didbot-serve] dependsOn: [pds-writes] exitCriterion: > An agent whose profile does not permit a collection is refused when it writes one, and the refusal names the policy. --- # write-policy **Superseded.** [policy](policy.md) settles the model this epic assumed — denials only, evaluated at the write, from three sources — and where the two disagree, this file is wrong. Read that one first. The server refuses collections it does not define and records that fail their lexicon. Neither is policy: both give the same answer for every agent. Missing is the per-agent question — this one may record and not remember. The natural key is the agent's type, which is what distinct subagent DIDs are for. - [ ] **A collection permission per agent, attached at provisioning.** - [ ] **Enforce it at the write, not only in the token.** A rule about what an account may ever write has to hold for sessions issued before the rule changed. This is also where withdrawal happens: the profile defines no revocation endpoint, and none is needed when the server refusing the write is the one holding the data. - [ ] **Who may write an account's own profile, and its picture.** The server draws an avatar at provisioning and writes `app.bsky.actor.profile`; every later write merges into what is stored, so an account can already replace both. **The factual half of this changed and this item used to say otherwise.** There *is* authentication on a repo write now: `createRecord`, `putRecord`, `deleteRecord` and `applyWrites` all go through `auth::require_agent_token_or_dpop`, which resolves an agent token or a DPoP-bound OAuth access token to a DID, and `routes::write_record` refuses with `repo_mismatch` when the resolved DID is not the repository named. So today's answer is "the account itself, holding a credential this server issued" — not "anyone who can reach the server". What is still undecided is the policy on top of that: whether an agent *should* be able to set its own picture and its own profile fields at all. Whether an agent may set its own picture, and through what, is a policy question rather than a drawing question: an avatar arriving from outside is a claim about an identity, and the rule that a profile is identity and never comes from the model is written in `didbot-pds`'s profile module. Settle it here before any tool offers it. - [ ] **A distinct refusal**, so "your policy forbids this" is not read as "this record is malformed". - [ ] **From server configuration first, from the owner's records second** ([policy-store](policy-store.md)). The owner's records are read: `crates/didbot-serve/src/policy_poll.rs` builds the operator's `bot.did.policy` and `bot.did.policyBinding` records into the gate every write is judged by. ## Done - [x] Collections outside this deployment's namespace are refused. - [x] `$type` inside a record may not contradict its collection. - [x] Records validated against their lexicons, explicitly, on write.