diff --git a/policy-site/recommended/no-deleting-old-posts.cases.json b/policy-site/recommended/no-deleting-old-posts.cases.json new file mode 100644 index 00000000..5935a05c --- /dev/null +++ b/policy-site/recommended/no-deleting-old-posts.cases.json @@ -0,0 +1,100 @@ +[ + { + "name": "deleting a ten-day-old post", + "expect": "deny", + "write": { + "account": { "did": "did:web:agent.pds.example", "handle": "agent.pds.example" }, + "clientId": "https://harness.example/oauth-client-metadata.json", + "collection": "app.bsky.feed.post", + "rkey": "3lbwaaaaaaa2a", + "before": { + "$type": "app.bsky.feed.post", + "createdAt": "2026-09-01T12:00:00.000Z", + "text": "old news" + } + } + }, + { + "name": "a post dated next week", + "expect": "deny", + "write": { + "account": { "did": "did:web:agent.pds.example", "handle": "agent.pds.example" }, + "clientId": "https://harness.example/oauth-client-metadata.json", + "collection": "app.bsky.feed.post", + "rkey": "3lbwaaaaaaa2a", + "after": { + "$type": "app.bsky.feed.post", + "createdAt": "2026-09-18T12:00:00.000Z", + "text": "hello" + } + } + }, + { + "name": "an edit that moves createdAt into the future", + "expect": "deny", + "write": { + "account": { "did": "did:web:agent.pds.example", "handle": "agent.pds.example" }, + "clientId": "https://harness.example/oauth-client-metadata.json", + "collection": "app.bsky.feed.post", + "rkey": "3lbwaaaaaaa2a", + "before": { + "$type": "app.bsky.feed.post", + "createdAt": "2026-09-11T11:00:00.000Z", + "text": "hello" + }, + "after": { + "$type": "app.bsky.feed.post", + "createdAt": "2026-09-18T12:00:00.000Z", + "text": "hello" + } + } + }, + { + "name": "deleting a six-hour-old post", + "expect": "allow", + "write": { + "account": { "did": "did:web:agent.pds.example", "handle": "agent.pds.example" }, + "clientId": "https://harness.example/oauth-client-metadata.json", + "collection": "app.bsky.feed.post", + "rkey": "3lbwaaaaaaa2a", + "before": { + "$type": "app.bsky.feed.post", + "createdAt": "2026-09-11T06:00:00.000Z", + "text": "typo" + } + } + }, + { + "name": "a post dated now", + "expect": "allow", + "write": { + "account": { "did": "did:web:agent.pds.example", "handle": "agent.pds.example" }, + "clientId": "https://harness.example/oauth-client-metadata.json", + "collection": "app.bsky.feed.post", + "rkey": "3lbwaaaaaaa2a", + "after": { + "$type": "app.bsky.feed.post", + "createdAt": "2026-09-11T11:59:00.000Z", + "text": "hello" + } + } + }, + { + "name": "deleting an old like", + "expect": "allow", + "write": { + "account": { "did": "did:web:agent.pds.example", "handle": "agent.pds.example" }, + "clientId": "https://harness.example/oauth-client-metadata.json", + "collection": "app.bsky.feed.like", + "rkey": "3lbwaaaaaaa2a", + "before": { + "$type": "app.bsky.feed.like", + "createdAt": "2026-09-01T12:00:00.000Z", + "subject": { + "uri": "at://did:web:other.example/app.bsky.feed.post/otherpost1", + "cid": "bafyreie5737gdxlw5i64vzichcalba3z2v5n6icifvx5xytvske7mr3hpm" + } + } + } + } +] diff --git a/policy-site/recommended/no-deleting-old-posts.json b/policy-site/recommended/no-deleting-old-posts.json new file mode 100644 index 00000000..da258523 --- /dev/null +++ b/policy-site/recommended/no-deleting-old-posts.json @@ -0,0 +1,10 @@ +{ + "$type": "bot.did.policy", + "name": "no-deleting-old-posts", + "description": "Refuses deleting a post whose createdAt is more than a day old, and refuses a post whose createdAt is more than five minutes in the future, so a post cannot be dated to stay deletable.", + "actions": ["record.write"], + "document": { + "$type": "bot.did.policy#cedar", + "source": "@id(\"no-late-deletes\")\n@reason(\"posts more than a day old may not be deleted\")\nforbid(principal, action == Didbot::Action::\"record.write\", resource)\nwhen {\n resource.collection == \"app.bsky.feed.post\" &&\n context.operation == \"delete\" &&\n context has existing &&\n context.now.durationSince(datetime(context.existing.createdAt)) > duration(\"1d\")\n};\n\n@id(\"no-future-posts\")\n@reason(\"a post's createdAt may not be in the future\")\nforbid(principal, action == Didbot::Action::\"record.write\", resource)\nwhen {\n resource.collection == \"app.bsky.feed.post\" &&\n context.operation != \"delete\" &&\n context has value &&\n context.value has createdAt &&\n datetime(context.value.createdAt) > context.now.offset(duration(\"5m\"))\n};\n" + } +} diff --git a/policy-site/recommended/only-one-client.cases.json b/policy-site/recommended/only-one-client.cases.json new file mode 100644 index 00000000..0921e5e9 --- /dev/null +++ b/policy-site/recommended/only-one-client.cases.json @@ -0,0 +1,64 @@ +[ + { + "name": "another app's sign-in", + "expect": "deny", + "signIn": { + "clientId": "https://other-app.example/oauth-client-metadata.json", + "scopes": ["atproto"] + } + }, + { + "name": "another app's token", + "expect": "deny", + "token": { + "account": { "did": "did:web:agent.pds.example", "handle": "agent.pds.example" }, + "clientId": "https://other-app.example/oauth-client-metadata.json", + "scopes": ["atproto"] + } + }, + { + "name": "a development client's sign-in", + "expect": "deny", + "signIn": { "clientId": "http://localhost", "scopes": ["atproto"] } + }, + { + "name": "a host that only begins the same way", + "expect": "deny", + "signIn": { + "clientId": "https://harness.example.other.example/oauth-client-metadata.json", + "scopes": ["atproto"] + } + }, + { + "name": "the harness app's sign-in", + "expect": "allow", + "signIn": { + "clientId": "https://harness.example/oauth-client-metadata.json", + "scopes": ["atproto"] + } + }, + { + "name": "the harness app's token", + "expect": "allow", + "token": { + "account": { "did": "did:web:agent.pds.example", "handle": "agent.pds.example" }, + "clientId": "https://harness.example/oauth-client-metadata.json", + "scopes": ["atproto"] + } + }, + { + "name": "a write through another app, which it does not judge", + "expect": "allow", + "write": { + "account": { "did": "did:web:agent.pds.example", "handle": "agent.pds.example" }, + "clientId": "https://other-app.example/oauth-client-metadata.json", + "collection": "app.bsky.feed.post", + "rkey": "3lbwaaaaaaa2a", + "after": { + "$type": "app.bsky.feed.post", + "createdAt": "2026-09-11T11:59:00.000Z", + "text": "hello" + } + } + } +] diff --git a/policy-site/recommended/only-one-client.json b/policy-site/recommended/only-one-client.json new file mode 100644 index 00000000..b368b3de --- /dev/null +++ b/policy-site/recommended/only-one-client.json @@ -0,0 +1,10 @@ +{ + "$type": "bot.did.policy", + "name": "only-one-client", + "description": "Refuses sign-in and token issue to every app except the one whose client metadata is served from harness.example. It judges no writes, so a token issued before it applied keeps working until it expires.", + "actions": ["oauth.authorize", "oauth.token"], + "document": { + "$type": "bot.did.policy#cedar", + "source": "@id(\"only-harness\")\n@reason(\"only the operator's harness app may act for this account\")\nforbid(\n principal,\n action in [Didbot::Action::\"oauth.authorize\", Didbot::Action::\"oauth.token\"],\n resource\n)\nunless { resource.id like \"https://harness.example/*\" };\n" + } +} diff --git a/policy-site/recommended/order.txt b/policy-site/recommended/order.txt index 6589246f..69d58d2c 100644 --- a/policy-site/recommended/order.txt +++ b/policy-site/recommended/order.txt @@ -1,5 +1,6 @@ deny-client no-loopback-clients +only-one-client no-secret-leaks no-contact-details no-links @@ -7,7 +8,9 @@ no-mentions no-images-or-video replies-to-self-only permanent-posts +no-deleting-old-posts no-posts +quiet-hours frozen-social-graph no-likes-or-reposts no-thread-or-quote-gates @@ -15,4 +18,5 @@ dms-closed-to-strangers fixed-display-name display-name-holds-handle bio-discloses-agent +profile-keeps-name-and-bio fixed-profile-images-and-labels diff --git a/policy-site/recommended/profile-keeps-name-and-bio.cases.json b/policy-site/recommended/profile-keeps-name-and-bio.cases.json new file mode 100644 index 00000000..d365dac1 --- /dev/null +++ b/policy-site/recommended/profile-keeps-name-and-bio.cases.json @@ -0,0 +1,140 @@ +[ + { + "name": "removing the bio", + "expect": "deny", + "write": { + "account": { "did": "did:web:agent.pds.example", "handle": "agent.pds.example" }, + "clientId": "https://harness.example/oauth-client-metadata.json", + "collection": "app.bsky.actor.profile", + "rkey": "self", + "before": { + "$type": "app.bsky.actor.profile", + "createdAt": "2026-09-01T00:00:00.000Z", + "displayName": "Agent", + "description": "[agent] Posts tide tables." + }, + "after": { + "$type": "app.bsky.actor.profile", + "createdAt": "2026-09-01T00:00:00.000Z", + "displayName": "Agent" + } + } + }, + { + "name": "removing the display name", + "expect": "deny", + "write": { + "account": { "did": "did:web:agent.pds.example", "handle": "agent.pds.example" }, + "clientId": "https://harness.example/oauth-client-metadata.json", + "collection": "app.bsky.actor.profile", + "rkey": "self", + "before": { + "$type": "app.bsky.actor.profile", + "createdAt": "2026-09-01T00:00:00.000Z", + "displayName": "Agent", + "description": "[agent] Posts tide tables." + }, + "after": { + "$type": "app.bsky.actor.profile", + "createdAt": "2026-09-01T00:00:00.000Z", + "description": "[agent] Posts tide tables." + } + } + }, + { + "name": "emptying the display name", + "expect": "deny", + "write": { + "account": { "did": "did:web:agent.pds.example", "handle": "agent.pds.example" }, + "clientId": "https://harness.example/oauth-client-metadata.json", + "collection": "app.bsky.actor.profile", + "rkey": "self", + "before": { + "$type": "app.bsky.actor.profile", + "createdAt": "2026-09-01T00:00:00.000Z", + "displayName": "Agent", + "description": "[agent] Posts tide tables." + }, + "after": { + "$type": "app.bsky.actor.profile", + "createdAt": "2026-09-01T00:00:00.000Z", + "displayName": "", + "description": "[agent] Posts tide tables." + } + } + }, + { + "name": "rewriting the bio", + "expect": "allow", + "write": { + "account": { "did": "did:web:agent.pds.example", "handle": "agent.pds.example" }, + "clientId": "https://harness.example/oauth-client-metadata.json", + "collection": "app.bsky.actor.profile", + "rkey": "self", + "before": { + "$type": "app.bsky.actor.profile", + "createdAt": "2026-09-01T00:00:00.000Z", + "displayName": "Agent", + "description": "[agent] Posts tide tables." + }, + "after": { + "$type": "app.bsky.actor.profile", + "createdAt": "2026-09-01T00:00:00.000Z", + "displayName": "Agent", + "description": "[agent] Posts tide tables and weather." + } + } + }, + { + "name": "a first profile with no bio", + "expect": "allow", + "write": { + "account": { "did": "did:web:agent.pds.example", "handle": "agent.pds.example" }, + "clientId": "https://harness.example/oauth-client-metadata.json", + "collection": "app.bsky.actor.profile", + "rkey": "self", + "after": { + "$type": "app.bsky.actor.profile", + "createdAt": "2026-09-01T00:00:00.000Z", + "displayName": "Agent" + } + } + }, + { + "name": "adding a bio", + "expect": "allow", + "write": { + "account": { "did": "did:web:agent.pds.example", "handle": "agent.pds.example" }, + "clientId": "https://harness.example/oauth-client-metadata.json", + "collection": "app.bsky.actor.profile", + "rkey": "self", + "before": { + "$type": "app.bsky.actor.profile", + "createdAt": "2026-09-01T00:00:00.000Z", + "displayName": "Agent" + }, + "after": { + "$type": "app.bsky.actor.profile", + "createdAt": "2026-09-01T00:00:00.000Z", + "displayName": "Agent", + "description": "[agent] Posts tide tables." + } + } + }, + { + "name": "deleting the profile", + "expect": "allow", + "write": { + "account": { "did": "did:web:agent.pds.example", "handle": "agent.pds.example" }, + "clientId": "https://harness.example/oauth-client-metadata.json", + "collection": "app.bsky.actor.profile", + "rkey": "self", + "before": { + "$type": "app.bsky.actor.profile", + "createdAt": "2026-09-01T00:00:00.000Z", + "displayName": "Agent", + "description": "[agent] Posts tide tables." + } + } + } +] diff --git a/policy-site/recommended/profile-keeps-name-and-bio.json b/policy-site/recommended/profile-keeps-name-and-bio.json new file mode 100644 index 00000000..6f911841 --- /dev/null +++ b/policy-site/recommended/profile-keeps-name-and-bio.json @@ -0,0 +1,10 @@ +{ + "$type": "bot.did.policy", + "name": "profile-keeps-name-and-bio", + "description": "Refuses an edit that removes or empties the profile's display name or bio. Changing either to other text is allowed.", + "actions": ["record.write"], + "document": { + "$type": "bot.did.policy#cedar", + "source": "@id(\"keeps-display-name\")\n@reason(\"the display name may not be removed\")\nforbid(principal, action == Didbot::Action::\"record.write\", resource)\nwhen {\n resource.collection == \"app.bsky.actor.profile\" &&\n context.operation == \"update\" &&\n context has existing &&\n context.existing has displayName &&\n !(context has value && context.value has displayName && context.value.displayName != \"\")\n};\n\n@id(\"keeps-bio\")\n@reason(\"the bio may not be removed\")\nforbid(principal, action == Didbot::Action::\"record.write\", resource)\nwhen {\n resource.collection == \"app.bsky.actor.profile\" &&\n context.operation == \"update\" &&\n context has existing &&\n context.existing has description &&\n !(context has value && context.value has description && context.value.description != \"\")\n};\n" + } +} diff --git a/policy-site/recommended/quiet-hours.cases.json b/policy-site/recommended/quiet-hours.cases.json new file mode 100644 index 00000000..19b8afcc --- /dev/null +++ b/policy-site/recommended/quiet-hours.cases.json @@ -0,0 +1,100 @@ +[ + { + "name": "a post at 03:00", + "expect": "deny", + "now": "2026-09-11T03:00:00Z", + "write": { + "account": { "did": "did:web:agent.pds.example", "handle": "agent.pds.example" }, + "clientId": "https://harness.example/oauth-client-metadata.json", + "collection": "app.bsky.feed.post", + "rkey": "3lbwaaaaaaa2a", + "after": { + "$type": "app.bsky.feed.post", + "createdAt": "2026-09-11T03:00:00.000Z", + "text": "hello" + } + } + }, + { + "name": "a post at 05:59", + "expect": "deny", + "now": "2026-09-11T05:59:00Z", + "write": { + "account": { "did": "did:web:agent.pds.example", "handle": "agent.pds.example" }, + "clientId": "https://harness.example/oauth-client-metadata.json", + "collection": "app.bsky.feed.post", + "rkey": "3lbwaaaaaaa2a", + "after": { + "$type": "app.bsky.feed.post", + "createdAt": "2026-09-11T05:59:00.000Z", + "text": "hello" + } + } + }, + { + "name": "a post at 06:00", + "expect": "allow", + "now": "2026-09-11T06:00:00Z", + "write": { + "account": { "did": "did:web:agent.pds.example", "handle": "agent.pds.example" }, + "clientId": "https://harness.example/oauth-client-metadata.json", + "collection": "app.bsky.feed.post", + "rkey": "3lbwaaaaaaa2a", + "after": { + "$type": "app.bsky.feed.post", + "createdAt": "2026-09-11T06:00:00.000Z", + "text": "hello" + } + } + }, + { + "name": "a post at noon", + "expect": "allow", + "write": { + "account": { "did": "did:web:agent.pds.example", "handle": "agent.pds.example" }, + "clientId": "https://harness.example/oauth-client-metadata.json", + "collection": "app.bsky.feed.post", + "rkey": "3lbwaaaaaaa2a", + "after": { + "$type": "app.bsky.feed.post", + "createdAt": "2026-09-11T11:59:00.000Z", + "text": "hello" + } + } + }, + { + "name": "a like at 03:00", + "expect": "allow", + "now": "2026-09-11T03:00:00Z", + "write": { + "account": { "did": "did:web:agent.pds.example", "handle": "agent.pds.example" }, + "clientId": "https://harness.example/oauth-client-metadata.json", + "collection": "app.bsky.feed.like", + "rkey": "3lbwaaaaaaa2a", + "after": { + "$type": "app.bsky.feed.like", + "createdAt": "2026-09-11T03:00:00.000Z", + "subject": { + "uri": "at://did:web:other.example/app.bsky.feed.post/otherpost1", + "cid": "bafyreie5737gdxlw5i64vzichcalba3z2v5n6icifvx5xytvske7mr3hpm" + } + } + } + }, + { + "name": "deleting a post at 03:00", + "expect": "allow", + "now": "2026-09-11T03:00:00Z", + "write": { + "account": { "did": "did:web:agent.pds.example", "handle": "agent.pds.example" }, + "clientId": "https://harness.example/oauth-client-metadata.json", + "collection": "app.bsky.feed.post", + "rkey": "3lbwaaaaaaa2a", + "before": { + "$type": "app.bsky.feed.post", + "createdAt": "2026-09-10T12:00:00.000Z", + "text": "hello" + } + } + } +] diff --git a/policy-site/recommended/quiet-hours.json b/policy-site/recommended/quiet-hours.json new file mode 100644 index 00000000..ffe6d1e0 --- /dev/null +++ b/policy-site/recommended/quiet-hours.json @@ -0,0 +1,10 @@ +{ + "$type": "bot.did.policy", + "name": "quiet-hours", + "description": "Refuses new posts between 00:00 and 06:00 UTC. Other writes, and deleting posts, are allowed at any hour.", + "actions": ["record.write"], + "document": { + "$type": "bot.did.policy#cedar", + "source": "@id(\"quiet-hours\")\n@reason(\"posts may not be written between 00:00 and 06:00 UTC\")\nforbid(principal, action == Didbot::Action::\"record.write\", resource)\nwhen {\n resource.collection == \"app.bsky.feed.post\" &&\n context.operation == \"create\" &&\n context.now.toTime() < duration(\"6h\")\n};\n" + } +}