ATProto Policy primitives concept
This document describes the idea of Policies as a basic protocol-level concept (primitive), which tell your PDS what to do with certain data. The idea for policies came first from thinking about access control, but I then thought it could be used for things like post expiration, scheduled posts, and locking posts instead.
Policies can be applied to:
Repos
Collections
Records
Blobs
Lists, if they get added
Policies probably have to be entirely spec-based because they're implemented almost entirely by the PDS (that is, except for default access control delegation), since the entire point is to tell your PDS how to handle a resource.
The following Policies may be available:
Access control policies
For Collections, "read" access corresponds to listing the records in it and "write" access corresponds to creating records. For Repos, "read" access constitutes listing the collections, blobs, and lists in it, and "write" access corresponds to creating lists and uploading blobs. Available access types may include read, update, delete, read policies, and update policies.
Allow users access contains a list of DIDs which are, barring other restrictions such as explicit denials, always allowed to get (read) the resource. This may be extended to also allow the listed users to update the resource.
Allow lists access contains a list of list-primitive references. Members of these lists are, barring other restrictions such as explicit denials, always allowed to get (read) the resource. This may be extended to also allow the listed users to update the resource.
Referenced lists have to be in the same repo, to ensure they will always be in the same PDS.
Deny users access contains a list of DIDs which are always forbidden from getting (reading) the resource or performing any other action on it.
Deny lists access is the same as Deny users access except for members of a primitive list rather than an explicit list of DIDs.
Must identify requires that the requesting user be known to the PDS, even if the PDS doesn't normally require this.
The PDS may require identification for other reasons.
Require CID requires that the CID for a resource is supplied in order to fetch it. This could be a way to gate a post to "anyone with a link", or to hide what the most recent version is. It isn't valid if the resource doesn't have a CID (like a repo or collection), or is only identified by CID (such as blobs).
Default access controls
If no other default access control policy is set, the current behavior continues to occur: the resource is publicly viewable by default, and all other privileges (including reading policies) are exclusive to the owner (and PDS).
Make private declares that, unless otherwise allowed, read access to the resource is denied for everyone (except for its owner, duly authorized).
Do not make private tells the PDS to ignore any Make private policy applied to a resource via Cascading policies or Inherited policies (see below).
Delegate default access declares a DID that is allowed to get (read) the resource and all associated policies. If a given user/DID is not explicitly allowed or denied, the specified delegate is checked at the specified service endpoint. The result is checked to match before it is returned. This should be investigated further. It is mutually exclusive with "Make private", as both policies set default access controls.
The delegate's granted read resource and read policy access cannot be overridden by a Deny users/lists access policy.
This policy may or may not specify what type of access to delegate (i.e. read, write, delete, all). That way you can have multiple delegates, or only delegate read access (for security).
The reason for this is to rely on Bluesky's knowledge of your follower graph to gate a post to only your followers, without having your PDS have to figure out your follower graph.
You can change the delegate to another service implementing the API if you want. Inherited policies (below) should make it a lot easier to change this in bulk.
The PDS itself won't look up the full record (unless it has to?).
Or, maybe the PDS does look it up and sends it to the delegate to ask for approval (so the delegate doesn't have to look it up)?
What should happen if the delegate fails or isn't available? Maybe we should fall back to ignoring the delegate and relying on Make private?
You could have multiple delegates with priority and/or allow/deny/fallback, maybe? So you might have one you can check first and then fall back to others, or have it fall back "all the way" to public/private.
Content policies
Delete at time tells the PDS to schedule the resource to be deleted at the given time. It is only valid for resources which can be directly deleted, such as records.
For expiring posts.
Not valid when applied to a resource referred to by Inherited policies.
Delete after duration is the same but with a duration instead of an explicit timestamp. When added to a resource directly, it is converted to a timestamp. Otherwise it is based on the time from which the policy was applied or the resource's creation time, whichever is most recent.
Update at time tells the PDS to update the resource to the included new value at the given time. Only one of this policy may exist on a resource at a given time. It is only valid for resources which can be directly updated, such as records. (There might also need to be more explicit policy types for certain resources for schema reasons.) This is not valid under Cascading policies or Inherited policies.
Update after duration is the same but with a duration instead of an explicit timestamp. When added to a resource directly, it is converted to a timestamp. Otherwise it is based on the time from which the policy was applied or the resource's creation time, whichever is most recent.
Do not delete at time tells the PDS to ignore a Delete at time policy inherited from Cascading policies or Inherited policies.
Meta policies
Add policies at time tells the PDS to add a new list of policies to the record at the given time. This list cannot include any other time-based policies. It MAY include Cascading policies or Inherited policies, if it is valid. The list should also be checked to make sure that any "At time" policies refer to a time after those new policies would be added. Where there may be conflicts, (by default) the new policies override, or in some cases add to or merge with, the old policies. See the "Merging added policies" section below.
The idea for this is to be able to "lock" posts after some time, or to make them private/unlisted rather than completely deleting them.
Add policies after duration is the same but with a duration instead of an explicit timestamp. When added to a resource directly, it is converted to a timestamp. Otherwise it is based on the time from which the policy was applied or the post's creation time, whichever is most recent.
Temporary policies represents a set of policies which the PDS should consider effective until the given time. Time-based policies are not valid in this list. Think of this as a "remove policies at time" policy.
Policies don't have IDs, so you can't handle them that way.
This and/or Add policies at time could be used to schedule posts to be published. You can even wrap Inherited policies with these, so you can switch between policy templates as needed.
Temporary policies with duration is the same but with a duration instead of an explicit timestamp. When added to a resource directly, it is converted to a timestamp. Otherwise it is based on the time from which the policy was applied or the resource's creation time, whichever is most recent.
Cascading policies sets a default list of policies for a resource's "children": for a Collection, this list applies by default to all records in that Collection; for a Repository, this list applies by default to every Collection, Record, Blob, and List in it. Invalid on blobs, lists, and records. Cascaded policies should be considered "low priority" and any policies specified on a resource itself should be applied instead.
Inherited policies refers to another resource (in the same repo) from which to inherit policies. Typically the referenced resource is a record. Similar to Cascading policies, the inherited policies should be considered "low priority", and any policies specified on the resource itself should be applied instead.
This allows you to set templates from which policies can be applied, such as public, unlisted, mutuals only, followers only, expiring after some time, etc.
Policies are inherited in the order they are added and merged according to "Merging added policies".
Changes to policies applied to a resource that's inherited propagate to the resources that inherit its policies. If the resource is deleted, the inherited policy list becomes empty. The PDS may attempt to prevent total deletion of inherited policies.
PDSes should cache inherited policy lists for some time. The cache key should be based on the list's contents, to optimize for common policy templates (Bluesky will likely put several of them in user repos on first use).
Not valid when inheriting from a resource with a Delete at time/after duration policy.
Non-heritable policies is a list of policies that do not get inherited. This can be used to make policy templates private, without making inherited resources private, or the other way around. Obviously, it cannot contain Non-heritable policies because it would have no effect. It MAY have Inherited policies.
Metadata policies
There should be a better way to do these.
Do not relay tells a relay not to relay the resource or any changes to it. Good for things like preferences or other private data which other entities need not know about. I do not know if this is possible. Since relays need to know about it, this policy is always public.
Relay anyway tells the PDS to ignore any Do not relay policies from Cascading policies or Inherited policies.
Merging added policies
Any time a policy is added to an existing explicit list of policies, either via the com.atproto.repo.add*Policy XRPC procedure or certain policies, it is eligible to be merged with existing policies. Only some policies do this, including:
Allow/deny users/lists access merge the existing and new lists together (union). This is an easy way to add users or lists to those policies.
Cascading/Inherited policies merges the inherited policies. Inherited policies take precedence over Cascading policies.
Add policies at time listed policies for the same time are joined into the same list.
When merging multiple Inherited policies or Add policies at time together, some conditions may fail:
Multiple effective Delete at time policies. Instead of trying to decide whether the higher or lower timestamp should be preferred, merging simply fails and the policies are not applied.
All possible conditions should be checked when policies are added or set, including on resources that are inherited with Inherited policies.
Effective policy list
The effective policy list is the list of policies effective at any given time. It notably does not include any time-based policies.
It is the result of these actions:
Applying Cascading policies and Inherited policies. Policies applied directly to a resource always override policies listed under either of these; they do not merge into existing policies, even though they merge with each other.
This step includes applying policies that negate certain Cascading policies and Inherited policies, such as Do not make private, by removing the overridden policies (and those negating policies).
Convert any remaining duration policies into time-based policies based on the time the duration policy went into effect (specifically, an update to Cascading policies or Inherited policies adding it) or the time the resource was created, whichever is most recent.
Merging certain policies according to "Merging added policies".
Applying time-based policies. That means unexpired Temporary policies are added to the list to replace that policy, and activated Add policy at time policies are replaced with their constituent policies as well.
Discarding any irrelevant policies, such as expired Temporary policies, Cascading policies directly on the resource (they only apply to children, not the resource itself), Add policy at time policies that aren't active yet, empty policies, and all Delete at time and Update at time policies.
This list should be cached when the resource is requested and used for subsequent lookups. If the resource's attached policy list changes, the cache for it should be deleted (maybe refreshed).
Updating the policy list
The actual policy list should be updated in certain circumstances:
When Temporary policies expire -- removing the expired policy.
When Add policies at time/after duration policies become effective -- adding the newly effective policies and removing the adding policy.
When Update at time/after duration policies are executed -- removing the executed policy.
When Delete at time/after duration policies are executed -- deleting the entire record and the policies with it.
It's up to the PDS to decide when to make these changes (compaction, on lookup, periodically, scheduled, etc).
Outstanding challenges
If you move to a PDS that doesn't implement the same policies as your old one, what happens to those policies? Should the import fail, reject those resources, reject those policies, or what?