diff --git a/README.md b/README.md index 31fead4b..b7418cee 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ To that end, here is a non-exhaustive list of recommendations for a respectful ` - Do not operate `did.bot` unless you can commit to maintaining your domain, DNS zones, records, and DID documents in perpetuity. Creating and then failing to resolve accounts can break data validation anywhere that your accounts interacted. -- Do not block access to public `bot.did.*` endpoints such as `listAgents`. Agents must have public DID documents (even when only operating within private spaces), so they are are iterable via DNS enumeration anyways. +- Do not block access to public `bot.did.*` endpoints such as `listAccounts`. Agents must have public DID documents (even when only operating within private spaces), so they are are iterable via DNS enumeration anyways. - Do not consume atproto ecosystem resources. `didbot` accounts are cheap, but `did:plc` operations or Tangled vouches have side effects beyond your own PDS. Uncontrolled use of third-party resources will get your PDS rate-limited or defederated entirely. diff --git a/crates/didbot-agentd/src/bin/didbot-oauth.rs b/crates/didbot-agentd/src/bin/didbot-oauth.rs index b6095b87..339cb824 100644 --- a/crates/didbot-agentd/src/bin/didbot-oauth.rs +++ b/crates/didbot-agentd/src/bin/didbot-oauth.rs @@ -20,7 +20,7 @@ use didbot_agentd::cli::{ask, choose, flag, one_line, positional, printable, soc use didbot_agentd::decisions::Record; use didbot_agentd::direct::{Direct, Named}; use didbot_agentd::protocol::{ - Approve, DecisionForAgent, Decline, Message, Pending, Show, VERSION, + Approve, DecisionForAccount, Decline, Message, Pending, Show, VERSION, }; const USAGE: &str = "\ @@ -40,8 +40,8 @@ Nothing signs in until you choose, and a request nobody answers expires on its own. A token names one request: it is single use and it is not an account. With no daemon on this host -- a CI job that provisioned itself, or a host -handed a token by an operator -- set DIDBOT_PDS and DIDBOT_AGENT_TOKEN (or -DIDBOT_AGENT_TOKEN_FILE) and the same commands talk to that server as that one +handed a token by an operator -- set DIDBOT_PDS and DIDBOT_ACCOUNT_TOKEN (or +DIDBOT_ACCOUNT_TOKEN_FILE) and the same commands talk to that server as that one account. --direct insists on it rather than trying the socket first. Run the binary by name, didbot-oauth, for that: `didbot oauth` hands nothing from the environment's credentials to what it runs. @@ -328,7 +328,7 @@ fn one_of(args: &[String]) -> Result, String> { /// Records printed exactly as the daemon's `pending` prints them. fn print_records(records: &[Record]) { for record in records { - println!("{}", one_line(&DecisionForAgent::from(record))); + println!("{}", one_line(&DecisionForAccount::from(record))); } } diff --git a/crates/didbot-agentd/src/cli.rs b/crates/didbot-agentd/src/cli.rs index d1a20679..26a4a8b2 100644 --- a/crates/didbot-agentd/src/cli.rs +++ b/crates/didbot-agentd/src/cli.rs @@ -13,7 +13,7 @@ use std::io::{BufRead, BufReader, Write}; use std::os::unix::net::UnixStream; -use crate::protocol::{Answer, DecisionForAgent, Message}; +use crate::protocol::{Answer, DecisionForAccount, Message}; use crate::socket::default_socket_path; /// Which way `didbot-oauth` is talking to the world this time. @@ -170,7 +170,7 @@ const NARROW_APPROVES: &str = "approves=granted ceiling-checked=each-use"; /// rather than the thing being named. Fields that say nothing are left off /// entirely: a request granted exactly as asked does not repeat itself, and /// a verdict that refused nothing carries no rule. -pub fn one_line(decision: &DecisionForAgent) -> String { +pub fn one_line(decision: &DecisionForAccount) -> String { let mut line = format!( "{} {} asked={} verdict={}", printable(&decision.client_origin), @@ -327,8 +327,8 @@ mod tests { assert_eq!(flag(&args(&["--as", "did:web:a"]), "as"), Some("did:web:a")); } - fn decision() -> DecisionForAgent { - DecisionForAgent { + fn decision() -> DecisionForAccount { + DecisionForAccount { token: Some("k7f3".into()), client_origin: "http://127.0.0.1:40831".into(), first_time: true, diff --git a/crates/didbot-agentd/src/decisions.rs b/crates/didbot-agentd/src/decisions.rs index b8ea4aa1..4d903b9a 100644 --- a/crates/didbot-agentd/src/decisions.rs +++ b/crates/didbot-agentd/src/decisions.rs @@ -22,7 +22,7 @@ use std::time::Duration; use serde::{Deserialize, Serialize}; use url::Url; -use crate::protocol::DecisionForAgent; +use crate::protocol::DecisionForAccount; use crate::secret::Secret; /// How long the server is asked to hold a poll open, in seconds. @@ -159,7 +159,7 @@ impl Record { } } -impl From<&Record> for DecisionForAgent { +impl From<&Record> for DecisionForAccount { fn from(record: &Record) -> Self { // The rule and the reason stay two fields. The rule is an identifier // an operator can look up in their own policy; the reason is a sentence @@ -479,7 +479,7 @@ mod tests { }, Some("k7f3"), ); - let shown = DecisionForAgent::from(&narrowed); + let shown = DecisionForAccount::from(&narrowed); assert_eq!(shown.verdict, "narrow"); assert_eq!(shown.cut, vec!["repo:com.example.thing".to_owned()]); assert_eq!(shown.rule.as_deref(), Some("ceiling")); @@ -630,7 +630,7 @@ mod tests { }, None, ); - let shown = DecisionForAgent::from(&denied); + let shown = DecisionForAccount::from(&denied); assert_eq!(shown.verdict, "deny"); assert!(shown.granted.is_empty()); // No token, because there is no choice: the agent is being told what diff --git a/crates/didbot-agentd/src/direct.rs b/crates/didbot-agentd/src/direct.rs index 8deffb07..25e2cbe9 100644 --- a/crates/didbot-agentd/src/direct.rs +++ b/crates/didbot-agentd/src/direct.rs @@ -31,7 +31,7 @@ pub const SERVER: &str = "DIDBOT_PDS"; /// secret usually reaches a CI job: a path is safe in an environment listing /// and a token is not. Named in `didbot-cli` so the dispatcher scrubs the /// same two variables this reads. -pub use didbot_cli::env::{AGENT_TOKEN as TOKEN, AGENT_TOKEN_FILE as TOKEN_FILE}; +pub use didbot_cli::env::{ACCOUNT_TOKEN as TOKEN, ACCOUNT_TOKEN_FILE as TOKEN_FILE}; /// One account, one server, no daemon. /// diff --git a/crates/didbot-agentd/src/node.rs b/crates/didbot-agentd/src/node.rs index f9795ad8..d11f8f72 100644 --- a/crates/didbot-agentd/src/node.rs +++ b/crates/didbot-agentd/src/node.rs @@ -526,7 +526,7 @@ mod tests { let now = OffsetDateTime::now_utc(); let asking = ProvisioningRequest { - agent_id: "ctx-1", + account_id: "ctx-1", handle: None, parent: Some("did:web:one.example"), }; diff --git a/crates/didbot-agentd/src/protocol.rs b/crates/didbot-agentd/src/protocol.rs index f94bd085..8836e803 100644 --- a/crates/didbot-agentd/src/protocol.rs +++ b/crates/didbot-agentd/src/protocol.rs @@ -240,7 +240,7 @@ pub struct Pending { /// that holds it. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct DecisionForAgent { +pub struct DecisionForAccount { /// What to approve or decline with, absent when the verdict is `deny` /// and there is therefore nothing to choose. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -261,7 +261,7 @@ pub struct DecisionForAgent { /// narrowed. /// /// Outside the approval: a ceiling loosened afterwards still answers - /// [`DecisionForAgent::granted`], and one of these takes a fresh + /// [`DecisionForAccount::granted`], and one of these takes a fresh /// sign-in to ask for. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub cut: Vec, @@ -270,7 +270,7 @@ pub struct DecisionForAgent { pub rule: Option, /// Why it was refused, in the policy's own sentence. /// - /// Separate from [`DecisionForAgent::rule`] rather than folded into it: + /// Separate from [`DecisionForAccount::rule`] rather than folded into it: /// the rule is an identifier an operator can go and look up, the reason is /// prose for the agent to read, and an adapter rendering them wants to /// treat the two differently. Absent unless the verdict is `deny`. @@ -337,7 +337,7 @@ pub struct Answer { pub trouble: Option, /// Sign-ins waiting on a decision, for the context that asked. #[serde(default, skip_serializing_if = "Option::is_none")] - pub pending: Option>, + pub pending: Option>, /// What the approved login may do now: the request as the ceiling grants /// it, which is less than was asked for while the ceiling narrows it. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -403,7 +403,7 @@ impl Answer { /// adapter reading this in another language should be able to test for /// the field's presence and not have to distinguish two kinds of nothing. #[must_use] - pub fn and_pending(mut self, pending: Vec) -> Self { + pub fn and_pending(mut self, pending: Vec) -> Self { if !pending.is_empty() { self.pending = Some(pending); } @@ -581,7 +581,7 @@ mod tests { #[test] fn an_answer_carrying_a_decision_is_the_shape_the_adapter_renders() { - let answer = Answer::quiet().and_pending(vec![DecisionForAgent { + let answer = Answer::quiet().and_pending(vec![DecisionForAccount { token: Some("k7f3".into()), client_origin: "http://127.0.0.1:40831".into(), first_time: true, @@ -601,7 +601,7 @@ mod tests { #[test] fn a_refusal_carries_the_rule_and_the_reason_as_two_fields() { - let answer = Answer::quiet().and_pending(vec![DecisionForAgent { + let answer = Answer::quiet().and_pending(vec![DecisionForAccount { token: None, client_origin: "http://127.0.0.1:40831".into(), first_time: true, @@ -621,7 +621,7 @@ mod tests { #[test] fn and_a_verdict_that_refused_nothing_carries_no_reason() { - let answer = Answer::quiet().and_pending(vec![DecisionForAgent { + let answer = Answer::quiet().and_pending(vec![DecisionForAccount { token: Some("k7f3".into()), client_origin: "http://127.0.0.1:40831".into(), first_time: false, @@ -671,18 +671,19 @@ mod tests { identity: Option, } - let answer = Answer::identity("did:web:one.example").and_pending(vec![DecisionForAgent { - token: Some("k7f3".into()), - client_origin: "http://127.0.0.1:40831".into(), - first_time: false, - requested: vec!["atproto".into()], - granted: vec!["atproto".into()], - cut: Vec::new(), - rule: None, - reason: None, - verdict: "allow".into(), - expires_at: "2026-09-09T12:04:00Z".into(), - }]); + let answer = + Answer::identity("did:web:one.example").and_pending(vec![DecisionForAccount { + token: Some("k7f3".into()), + client_origin: "http://127.0.0.1:40831".into(), + first_time: false, + requested: vec!["atproto".into()], + granted: vec!["atproto".into()], + cut: Vec::new(), + rule: None, + reason: None, + verdict: "allow".into(), + expires_at: "2026-09-09T12:04:00Z".into(), + }]); let line = serde_json::to_string(&answer).unwrap(); let old: AsVersionOne = serde_json::from_str(&line).unwrap(); diff --git a/crates/didbot-agentd/src/registrar.rs b/crates/didbot-agentd/src/registrar.rs index 85395132..28140861 100644 --- a/crates/didbot-agentd/src/registrar.rs +++ b/crates/didbot-agentd/src/registrar.rs @@ -17,7 +17,7 @@ use crate::secret::Secret; pub struct Wanted { /// The harness's identifier for the context, which becomes the leftmost /// label of its DID. - pub agent_id: String, + pub account_id: String, /// The harness's word for what kind of context this is. pub kind: Option, /// The identity of whatever spawned it, when it has one. @@ -100,7 +100,7 @@ pub struct Pds { struct Minted { did: String, handle: String, - agent_token: String, + account_token: String, } impl Pds { @@ -116,9 +116,9 @@ impl Pds { impl Registrar for Pds { async fn provision(&self, wanted: Wanted) -> Result { - let url = format!("{}/xrpc/bot.did.provisionAgent", self.base); + let url = format!("{}/xrpc/bot.did.createAccount", self.base); let body = serde_json::json!({ - "agentId": wanted.agent_id, + "accountId": wanted.account_id, "registration": { "harness": self.harness, "agentType": wanted.kind, @@ -154,7 +154,7 @@ impl Registrar for Pds { Ok(Identity { did: minted.did, handle: minted.handle, - token: Secret::new(minted.agent_token), + token: Secret::new(minted.account_token), }) } } @@ -183,7 +183,7 @@ mod tests { body: Value, } - /// A `bot.did.provisionAgent` on loopback that keeps the body it was + /// A `bot.did.createAccount` on loopback that keeps the body it was /// sent and answers with whatever the test set. async fn serve(status: StatusCode, body: Value) -> (String, Seen, tokio::task::JoinHandle<()>) { let seen = Seen::default(); @@ -194,7 +194,7 @@ mod tests { }; let app = axum::Router::new() .route( - "/xrpc/bot.did.provisionAgent", + "/xrpc/bot.did.createAccount", post( |State(double): State, Json(sent): Json| async move { *double.seen.lock().unwrap() = Some(sent); @@ -218,14 +218,14 @@ mod tests { "n0", time::OffsetDateTime::now_utc(), &didbot_attest::ProvisioningRequest { - agent_id: "a-1", + account_id: "a-1", handle: None, parent: Some("did:web:host.example"), }, ) .unwrap(); Wanted { - agent_id: "a-1".into(), + account_id: "a-1".into(), kind: Some("Explore".into()), parent: Some(claim.node_id.clone()), claim, @@ -238,7 +238,7 @@ mod tests { async fn the_provisioning_body_carries_the_signed_claim() { let (origin, seen, _server) = serve( StatusCode::OK, - json!({ "did": "did:web:a-1.example", "handle": "a-1.example", "agentToken": "t" }), + json!({ "did": "did:web:a-1.example", "handle": "a-1.example", "accountToken": "t" }), ) .await; let wanted = wanted(); diff --git a/crates/didbot-agentd/src/serve.rs b/crates/didbot-agentd/src/serve.rs index c0b9d1ab..1c998c03 100644 --- a/crates/didbot-agentd/src/serve.rs +++ b/crates/didbot-agentd/src/serve.rs @@ -25,7 +25,7 @@ use crate::decisions::{Account, Record}; use crate::node::Node; use crate::pending::Held; use crate::protocol::{ - Answer, Approve, Become, DecisionForAgent, Decline, HostAnswer, Message, Observed, Report, + Answer, Approve, Become, DecisionForAccount, Decline, HostAnswer, Message, Observed, Report, MAX_LINE, VERSION, }; use crate::registrar::{Registrar, Trouble, Wanted}; @@ -141,7 +141,7 @@ impl Daemon { match self.find(&show.url).await { Ok(holding) => { info!(request_uri = %holding.record.request_uri, "showed a decision"); - Answer::quiet().and_pending(vec![DecisionForAgent::from(&holding.record)]) + Answer::quiet().and_pending(vec![DecisionForAccount::from(&holding.record)]) } Err(why) => Answer::trouble(why), } @@ -220,7 +220,7 @@ impl Daemon { } self.held.lock().await.sweep(OffsetDateTime::now_utc()); let held = self.held.lock().await.all(); - Answer::quiet().and_pending(held.iter().map(DecisionForAgent::from).collect()) + Answer::quiet().and_pending(held.iter().map(DecisionForAccount::from).collect()) } /// Redeem an approval token and hand the client its code. @@ -432,7 +432,7 @@ impl Daemon { self.held.lock().await.sweep(OffsetDateTime::now_utc()); let waiting = self.held.lock().await.for_context(&key); ( - answer.and_pending(waiting.iter().map(DecisionForAgent::from).collect()), + answer.and_pending(waiting.iter().map(DecisionForAccount::from).collect()), told, ) } @@ -485,7 +485,7 @@ impl Daemon { // context's provisioning and nothing else: a claim read off the path // cannot be spent on somebody else's account. The daemon asks for no // handle, and the parent is the host that is signing. - let agent_id = agent_id(key); + let account_id = account_id(key); let Some(host) = self.node.host() else { warn!("could not sign for a context"); return ( @@ -494,7 +494,7 @@ impl Daemon { ); }; let asking = didbot_attest::ProvisioningRequest { - agent_id: &agent_id, + account_id: &account_id, handle: None, parent: Some(&host.did), }; @@ -509,7 +509,7 @@ impl Daemon { } }; let wanted = Wanted { - agent_id, + account_id, kind: report.kind.clone(), parent: Some(claim.node_id.clone()), claim, @@ -805,7 +805,7 @@ fn asker(report: &Report) -> &str { /// this narrows the alphabet rather than shortening: what comes back has to /// survive being a DNS label, and a harness that starts issuing ids with an /// underscore in them should produce a duller name, not a broken account. -fn agent_id(key: &Key) -> String { +fn account_id(key: &Key) -> String { let raw = key.context.as_deref().unwrap_or(&key.session); let clean: String = raw .chars() @@ -868,7 +868,7 @@ mod tests { } let n = self.minted.fetch_add(1, Ordering::SeqCst); Ok(Identity { - did: format!("did:web:{}.example", wanted.agent_id), + did: format!("did:web:{}.example", wanted.account_id), handle: format!("{n}.example"), token: Secret::new("agent-token"), }) @@ -1113,7 +1113,7 @@ mod tests { let now = wanted.claim.issued_at; // The very request the daemon sent, which is what the claim covers. let asking = didbot_attest::ProvisioningRequest { - agent_id: &wanted.agent_id, + account_id: &wanted.account_id, handle: None, parent: Some(HOST), }; @@ -1129,7 +1129,7 @@ mod tests { // Anything but the request it was signed for, under the right key. let stolen = didbot_attest::ProvisioningRequest { - agent_id: "somebody-else", + account_id: "somebody-else", ..asking }; assert_eq!( @@ -1624,7 +1624,7 @@ mod tests { session: "s".into(), context: Some("Agent_07:B".into()), }; - assert_eq!(agent_id(&key), "agent-07-b"); + assert_eq!(account_id(&key), "agent-07-b"); } #[tokio::test] diff --git a/crates/didbot-attest/src/lib.rs b/crates/didbot-attest/src/lib.rs index d819271d..b67b0cdd 100644 --- a/crates/didbot-attest/src/lib.rs +++ b/crates/didbot-attest/src/lib.rs @@ -28,7 +28,7 @@ //! //! # What runs //! -//! [`NodeCredentialBackend`] gates `bot.did.provisionAgent`: +//! [`NodeCredentialBackend`] gates `bot.did.createAccount`: //! [`didbot_pds::Provisioner`](../didbot_pds/struct.Provisioner.html) holds //! one keyed by the node keys of the hosts it has admitted, and a context is //! provisioned only under a claim one of those keys signed. diff --git a/crates/didbot-attest/src/node_credential.rs b/crates/didbot-attest/src/node_credential.rs index 647bd745..3a03a24a 100644 --- a/crates/didbot-attest/src/node_credential.rs +++ b/crates/didbot-attest/src/node_credential.rs @@ -246,7 +246,7 @@ mod tests { /// The request every claim in these tests is signed for. fn asking() -> ProvisioningRequest<'static> { ProvisioningRequest { - agent_id: "ctx-1", + account_id: "ctx-1", handle: None, parent: Some("did:web:host.pds.example"), } @@ -404,7 +404,7 @@ mod tests { for stolen in [ ProvisioningRequest { - agent_id: "somebody-else", + account_id: "somebody-else", ..asking() }, ProvisioningRequest { diff --git a/crates/didbot-attest/src/signing.rs b/crates/didbot-attest/src/signing.rs index 5414094a..9ea0ac7c 100644 --- a/crates/didbot-attest/src/signing.rs +++ b/crates/didbot-attest/src/signing.rs @@ -83,7 +83,7 @@ pub fn canonical_signing_string_in( #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ProvisioningRequest<'a> { /// The DNS label the account's DID would be minted from. - pub agent_id: &'a str, + pub account_id: &'a str, /// The handle the request asks to claim, if it asks for one. pub handle: Option<&'a str>, /// The account the request names as this one's parent, if it names one. @@ -102,7 +102,7 @@ impl ProvisioningRequest<'_> { #[must_use] pub fn digest(&self) -> String { let mut hasher = Sha256::new(); - for field in [Some(self.agent_id), self.handle, self.parent] { + for field in [Some(self.account_id), self.handle, self.parent] { match field { Some(value) => { hasher.update(value.len().to_string().as_bytes()); @@ -126,13 +126,13 @@ mod tests { #[test] fn every_field_of_a_request_reaches_its_digest() { let base = ProvisioningRequest { - agent_id: "ctx-1", + account_id: "ctx-1", handle: Some("ctx-1.agents.example"), parent: Some("did:web:host.pds.example"), }; let variants = [ ProvisioningRequest { - agent_id: "ctx-2", + account_id: "ctx-2", ..base }, ProvisioningRequest { @@ -165,12 +165,12 @@ mod tests { #[test] fn a_character_moved_between_fields_is_a_different_request() { let first = ProvisioningRequest { - agent_id: "ab", + account_id: "ab", handle: Some("c"), parent: None, }; let second = ProvisioningRequest { - agent_id: "a", + account_id: "a", handle: Some("bc"), parent: None, }; diff --git a/crates/didbot-claim-check/src/agree.rs b/crates/didbot-claim-check/src/agree.rs index e46b2d29..5a11c35a 100644 --- a/crates/didbot-claim-check/src/agree.rs +++ b/crates/didbot-claim-check/src/agree.rs @@ -10,7 +10,7 @@ //! and unaffected by the server's own claim state, specifically so this //! can be checked before a server is claimed. -use didbot_identity::{hostname_is_at_or_below, AgentDid}; +use didbot_identity::{hostname_is_at_or_below, AccountDid}; /// Why the two unauthenticated reads of the server's identity disagreed. #[derive(Debug, Clone, thiserror::Error)] @@ -46,7 +46,7 @@ pub fn describes_same_server(document_did: &str, described_did: &str) -> bool { /// same consistency check as for a server, read across a zone rather than /// an exact match. fn hosted_by(subject: &str, server: &str) -> bool { - let (Ok(subject), Ok(server)) = (AgentDid::parse(subject), AgentDid::parse(server)) else { + let (Ok(subject), Ok(server)) = (AccountDid::parse(subject), AccountDid::parse(server)) else { return false; }; subject.host() != server.host() && hostname_is_at_or_below(subject.host(), server.host()) diff --git a/crates/didbot-claim-check/src/document.rs b/crates/didbot-claim-check/src/document.rs index 09fd25a1..92657336 100644 --- a/crates/didbot-claim-check/src/document.rs +++ b/crates/didbot-claim-check/src/document.rs @@ -6,7 +6,7 @@ //! back is here, so both read the document the same way and refuse the same //! documents. -use didbot_identity::did::AgentDid; +use didbot_identity::did::AccountDid; use didbot_identity::document::DidDocument; use didbot_identity::resolve::ResolveError; @@ -16,7 +16,7 @@ use didbot_identity::resolve::ResolveError; /// its failure as its own thing rather than as a document failure -- can /// build the same URL this module would have. pub fn did_json_url(hostname: &str) -> Result { - let did = AgentDid::parse(&format!("did:web:{hostname}")).map_err(|error| { + let did = AccountDid::parse(&format!("did:web:{hostname}")).map_err(|error| { ResolveError::Malformed { url: hostname.to_owned(), message: format!("{hostname:?} is not a usable did:web hostname: {error}"), @@ -140,8 +140,8 @@ mod tests { let node_key = didbot_key::SigningKey::generate() .verifying_key() .to_multibase(); - let did = - didbot_identity::AgentDid::parse("did:web:mossy-vole.pds.example.com").expect("a did"); + let did = didbot_identity::AccountDid::parse("did:web:mossy-vole.pds.example.com") + .expect("a did"); let server = DidDocument::for_account( &did, &repository_key, diff --git a/crates/didbot-claim-check/src/record.rs b/crates/didbot-claim-check/src/record.rs index 378e9e41..25adefef 100644 --- a/crates/didbot-claim-check/src/record.rs +++ b/crates/didbot-claim-check/src/record.rs @@ -7,7 +7,7 @@ //! server polling for its own standing and a verifier checking somebody //! else's read it the same way. -use didbot_identity::did::AgentDid; +use didbot_identity::did::AccountDid; use didbot_identity::resolve::ResolveError; use serde_json::{json, Value}; use time::format_description::well_known::Rfc3339; @@ -24,14 +24,14 @@ pub const COLLECTION: &str = didbot_lexicon::nsid::OPERATOR; /// server on a port: `did:web` writes that colon percent-encoded, and `%` is /// not in atproto's record-key character set, so keying by the argument /// produces a key no repository will accept. The server's own poll reads at -/// `AgentDid::authority` too; deriving it from the same place is what keeps +/// `AccountDid::authority` too; deriving it from the same place is what keeps /// the writer and the reader on one key. /// /// # Errors /// /// [`ResolveError::Malformed`] when `subject` is not a usable `did:web`. pub fn record_key(subject: &str) -> Result { - Ok(AgentDid::parse(subject) + Ok(AccountDid::parse(subject) .map_err(|error| ResolveError::Malformed { url: subject.to_owned(), message: format!("{subject:?} is not a usable did:web: {error}"), diff --git a/crates/didbot-cli/src/lib.rs b/crates/didbot-cli/src/lib.rs index 347911a6..0d0ed379 100644 --- a/crates/didbot-cli/src/lib.rs +++ b/crates/didbot-cli/src/lib.rs @@ -27,11 +27,11 @@ use serde::Serialize; /// caller's shell happened to hold. A verb authenticates for itself. pub mod env { /// An agent account's own token, for a host with no daemon. - pub const AGENT_TOKEN: &str = "DIDBOT_AGENT_TOKEN"; - /// A file holding [`AGENT_TOKEN`]'s value. - pub const AGENT_TOKEN_FILE: &str = "DIDBOT_AGENT_TOKEN_FILE"; + pub const ACCOUNT_TOKEN: &str = "DIDBOT_ACCOUNT_TOKEN"; + /// A file holding [`ACCOUNT_TOKEN`]'s value. + pub const ACCOUNT_TOKEN_FILE: &str = "DIDBOT_ACCOUNT_TOKEN_FILE"; /// Every variable the dispatcher scrubs. - pub const CREDENTIALS: &[&str] = &[AGENT_TOKEN, AGENT_TOKEN_FILE]; + pub const CREDENTIALS: &[&str] = &[ACCOUNT_TOKEN, ACCOUNT_TOKEN_FILE]; } /// ` `, as the binary this expands in answers `--version`. diff --git a/crates/didbot-config/src/lib.rs b/crates/didbot-config/src/lib.rs index 7ff603f0..7e3b4745 100644 --- a/crates/didbot-config/src/lib.rs +++ b/crates/didbot-config/src/lib.rs @@ -225,7 +225,7 @@ mod tests { assert_eq!(blobs.account_quota_bytes, Some(16777216)); let disclosure = config.disclosure.unwrap(); assert_eq!(disclosure.stats, Some(false)); - assert_eq!(disclosure.list_agents, None); + assert_eq!(disclosure.list_accounts, None); } #[test] diff --git a/crates/didbot-config/src/sections.rs b/crates/didbot-config/src/sections.rs index f2631f98..d41c532a 100644 --- a/crates/didbot-config/src/sections.rs +++ b/crates/didbot-config/src/sections.rs @@ -299,12 +299,12 @@ pub struct OperatorSection { #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct DisclosureSection { - /// `bot.did.listAgents`. - pub list_agents: Option, - /// `bot.did.listAgentLedgers`. - pub list_agent_ledgers: Option, - /// `bot.did.getAgentLedger`. - pub get_agent_ledger: Option, + /// `bot.did.listAccounts`. + pub list_accounts: Option, + /// `bot.did.listAccountLedgers`. + pub list_account_ledgers: Option, + /// `bot.did.getAccountLedger`. + pub get_account_ledger: Option, /// `bot.did.stats`. pub stats: Option, /// `bot.did.listReservations`. diff --git a/crates/didbot-dispatch/tests/dispatch.rs b/crates/didbot-dispatch/tests/dispatch.rs index dc12d85b..6a1c88c7 100644 --- a/crates/didbot-dispatch/tests/dispatch.rs +++ b/crates/didbot-dispatch/tests/dispatch.rs @@ -52,8 +52,8 @@ impl Fixture { Command::new(env!("CARGO_BIN_EXE_didbot")) .args(args) .env("PATH", format!("{}:/usr/bin:/bin", self.dir.display())) - .env("DIDBOT_AGENT_TOKEN", "a-secret") - .env("DIDBOT_AGENT_TOKEN_FILE", "/run/secrets/token") + .env("DIDBOT_ACCOUNT_TOKEN", "a-secret") + .env("DIDBOT_ACCOUNT_TOKEN_FILE", "/run/secrets/token") .env("DIDBOT_PDS", "pds.example") .output() .expect("run didbot") @@ -91,7 +91,7 @@ fn a_verb_receives_argv_and_no_credential() { "the verb did not get the command line as typed:\n{seen}" ); assert!( - !seen.contains("DIDBOT_AGENT_TOKEN"), + !seen.contains("DIDBOT_ACCOUNT_TOKEN"), "a credential variable reached the verb:\n{seen}" ); assert!( @@ -129,7 +129,7 @@ fn another_word_runs_the_binary_of_that_name() { let seen = stdout(&output); assert!(seen.starts_with("argv: one --two\n"), "{seen}"); assert!( - !seen.contains("DIDBOT_AGENT_TOKEN"), + !seen.contains("DIDBOT_ACCOUNT_TOKEN"), "a credential variable reached an external verb:\n{seen}" ); } diff --git a/crates/didbot-identity/src/did.rs b/crates/didbot-identity/src/did.rs index 9a7edfef..9b8a07d2 100644 --- a/crates/didbot-identity/src/did.rs +++ b/crates/didbot-identity/src/did.rs @@ -61,9 +61,9 @@ pub enum DidError { /// A hostname under `.arpa`, which handles and `did:web` both exclude. #[error("the .arpa top-level domain is not allowed")] ArpaDomain, - /// The agent id offered to [`AgentDid::mint`] is not a single DNS label. + /// The agent id offered to [`AccountDid::mint`] is not a single DNS label. #[error("agent id {0:?} is not a usable hostname label")] - InvalidAgentId(String), + InvalidAccountId(String), /// The zone host offered to [`Zone::new`] is not a usable hostname. #[error("zone host {0:?} is not a usable hostname")] InvalidZoneHost(String), @@ -169,7 +169,7 @@ impl Zone { /// deployment's several zones is covered. /// /// Returned as a string. It is a `did:web` for a service, not for an - /// account, and handing back an [`AgentDid`] would say otherwise. + /// account, and handing back an [`AccountDid`] would say otherwise. pub fn service_did(&self) -> String { match self.port { Some(port) => format!("{DID_WEB_PREFIX}{}{ENCODED_COLON}{port}", self.host), @@ -324,7 +324,7 @@ pub fn hostname_is_at_or_below(host: &str, ancestor: &str) -> bool { /// the identifier nor the fetch URL has to be recomputed — and so that the /// decoded form can never disagree with the encoded one. #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct AgentDid { +pub struct AccountDid { /// The DID exactly as it is written, port colon still encoded. did: String, /// Decoded hostname, without a port. @@ -333,20 +333,21 @@ pub struct AgentDid { port: Option, } -impl AgentDid { - /// Mints the DID for `agent_id` within `zone`. +impl AccountDid { + /// Mints the DID for `account_id` within `zone`. /// - /// Produces `did:web:.` in production, and - /// `did:web:.%3A` when the zone has a + /// Produces `did:web:.` in production, and + /// `did:web:.%3A` when the zone has a /// development port. The colon is percent-encoded because that is what /// the spec requires: "Port numbers (with separating colon hex-encoded)". /// An unencoded colon would read as a `did:web` path separator instead. - pub fn mint(zone: &Zone, agent_id: &str) -> Result { - validate_label(agent_id).map_err(|_| DidError::InvalidAgentId(agent_id.to_owned()))?; + pub fn mint(zone: &Zone, account_id: &str) -> Result { + validate_label(account_id) + .map_err(|_| DidError::InvalidAccountId(account_id.to_owned()))?; let mut did = - String::with_capacity(DID_WEB_PREFIX.len() + agent_id.len() + zone.host.len() + 8); + String::with_capacity(DID_WEB_PREFIX.len() + account_id.len() + zone.host.len() + 8); did.push_str(DID_WEB_PREFIX); - did.push_str(agent_id); + did.push_str(account_id); did.push('.'); did.push_str(&zone.host); if let Some(port) = zone.port { @@ -401,7 +402,7 @@ impl AgentDid { } /// The agent's own label: the leftmost component of the hostname. - pub fn agent_id(&self) -> &str { + pub fn account_id(&self) -> &str { self.host.split('.').next().unwrap_or(&self.host) } @@ -455,13 +456,16 @@ impl AgentDid { } } -impl fmt::Display for AgentDid { +impl fmt::Display for AccountDid { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.did) } } -impl std::str::FromStr for AgentDid { +/// The name `document.rs` still reaches [`AccountDid`] by. +pub type AgentDid = AccountDid; + +impl std::str::FromStr for AccountDid { type Err = DidError; fn from_str(s: &str) -> Result { @@ -474,8 +478,8 @@ impl std::str::FromStr for AgentDid { /// `u16::from_str` is not that: it accepts a leading `+` and any number of /// leading zeros, so `%3A3000`, `%3A03000` and `%3A+3000` would all decode to /// port 3000. That is the two-spellings-one-identity problem, and it is not -/// hypothetical here — [`AgentDid`] compares and hashes on the DID string it -/// was given, while [`AgentDid::did_json_url`] and [`AgentDid::authority`] +/// hypothetical here — [`AccountDid`] compares and hashes on the DID string it +/// was given, while [`AccountDid::did_json_url`] and [`AccountDid::authority`] /// derive from the decoded port, so the spellings are distinct identifiers /// naming one account. The `+` form is /// worse still: [`validate_did`] refuses `+` outright, so accepting it here @@ -554,7 +558,7 @@ fn validate_hostname(host: &str) -> Result<(), DidError> { /// is compared byte for byte across the network, so two spellings of one /// identity would be two identities. /// -/// This is the authority on what an agent id may be: [`AgentDid::mint`] and +/// This is the authority on what an agent id may be: [`AccountDid::mint`] and /// `validate_hostname` (this crate's `did:web` host check) both call it /// rather than a copy, and anything upstream that generates a label — a /// namer, or any derivation — should too. Two validators that disagree about which @@ -586,7 +590,7 @@ pub fn validate_label(label: &str) -> Result<(), DidError> { /// only `localhost` when it permits ports; treating its subdomains the same /// way is what makes per-agent hostnames usable at all in development. /// The IPv6 literal is listed for completeness of the scheme decision; it -/// cannot reach here through [`AgentDid::parse`], because a bracketed or +/// cannot reach here through [`AccountDid::parse`], because a bracketed or /// colon-bearing authority is rejected as path-based first. pub(crate) fn is_loopback_host(host: &str) -> bool { host == "localhost" @@ -632,16 +636,16 @@ pub enum SyntaxError { /// Checks a string against the generic atproto DID syntax rules. /// -/// # This is not [`AgentDid::parse`], and the difference is deliberate +/// # This is not [`AccountDid::parse`], and the difference is deliberate /// -/// [`AgentDid`] is much stricter: `did:web` only, hostname-level only, ports +/// [`AccountDid`] is much stricter: `did:web` only, hostname-level only, ports /// only on loopback, lowercase only. That strictness is this project's /// security posture rather than atproto's grammar — see [`Zone`]. Running the -/// protocol's generic DID vectors against `AgentDid` would report failures +/// protocol's generic DID vectors against `AccountDid` would report failures /// that are the entire point of the type, so they are run against this /// function instead. /// -/// The two are still connected: every DID [`AgentDid::mint`] produces is also +/// The two are still connected: every DID [`AccountDid::mint`] produces is also /// valid here, and the conformance suite asserts it. /// /// ``` diff --git a/crates/didbot-identity/src/handle.rs b/crates/didbot-identity/src/handle.rs index e27936bf..9fe23b0f 100644 --- a/crates/didbot-identity/src/handle.rs +++ b/crates/didbot-identity/src/handle.rs @@ -127,7 +127,7 @@ pub fn normalize(handle: &str) -> String { /// Where `handle` is resolved over HTTPS, or why it cannot be. /// /// `port` is for development only and follows the same rule -/// [`crate::AgentDid`] applies to a `did:web`: a port is allowed on loopback, +/// [`crate::AccountDid`] applies to a `did:web`: a port is allowed on loopback, /// where the whole zone is one listener on an ephemeral port, and refused /// anywhere else. The scheme follows from the same fact — nothing is /// listening on TLS on a developer's loopback interface. @@ -153,7 +153,7 @@ pub fn atproto_did_url(handle: &str, port: Option) -> Result Result { if did.starts_with("did:web:") { - let parsed = AgentDid::parse(did).map_err(|err| ResolveError::InvalidDid { + let parsed = AccountDid::parse(did).map_err(|err| ResolveError::InvalidDid { did: did.to_owned(), method: "did:web".to_owned(), message: err.to_string(), @@ -232,7 +232,7 @@ fn is_plc_identifier(identifier: &str) -> bool { /// A [`DidDocumentSource`] backed by a map from URL to response body. /// /// The test double, and also a usable cache or fixture loader. Keys are the -/// full URLs [`AgentDid::did_json_url`] produces, so a test that gets the +/// full URLs [`AccountDid::did_json_url`] produces, so a test that gets the /// scheme or the port wrong sees a [`ResolveError::NotFound`] rather than /// silently passing. #[derive(Debug, Clone, Default)] @@ -258,7 +258,7 @@ impl InMemoryDocuments { /// their no-`unwrap` discipline. pub fn insert( &mut self, - did: &AgentDid, + did: &AccountDid, document: &DidDocument, ) -> Result<&mut Self, serde_json::Error> { let body = serde_json::to_string(document)?; diff --git a/crates/didbot-identity/tests/identity.rs b/crates/didbot-identity/tests/identity.rs index 72f12089..e71b6258 100644 --- a/crates/didbot-identity/tests/identity.rs +++ b/crates/didbot-identity/tests/identity.rs @@ -10,7 +10,8 @@ use didbot_identity::document::{ }; use didbot_identity::resolve::ResolveError; use didbot_identity::{ - hostname_is_at_or_below, resolve, validate_did, AgentDid, InMemoryDocuments, Zone, ZoneRegistry, + hostname_is_at_or_below, resolve, validate_did, AccountDid, InMemoryDocuments, Zone, + ZoneRegistry, }; /// A realistic multibase secp256k1 public key. Only its shape matters here. @@ -45,8 +46,9 @@ fn the_zone_names_the_server_itself() { #[test] fn the_service_did_parses_and_contains_every_agent_it_covers() { for zone in [production_zone(), dev_zone()] { - let service = AgentDid::parse(&zone.service_did()).expect("a service did:web is a did:web"); - let agent = AgentDid::mint(&zone, "a1").expect("mint succeeds"); + let service = + AccountDid::parse(&zone.service_did()).expect("a service did:web is a did:web"); + let agent = AccountDid::mint(&zone, "a1").expect("mint succeeds"); assert!(hostname_is_at_or_below(agent.host(), service.host())); } } @@ -68,9 +70,9 @@ fn a_service_document_says_where_the_server_is_and_nothing_else() { #[test] fn mints_production_did() { - let did = AgentDid::mint(&production_zone(), "a1").expect("mint succeeds"); + let did = AccountDid::mint(&production_zone(), "a1").expect("mint succeeds"); assert_eq!(did.as_str(), "did:web:a1.agents.example.com"); - assert_eq!(did.agent_id(), "a1"); + assert_eq!(did.account_id(), "a1"); assert_eq!(did.host(), "a1.agents.example.com"); assert_eq!(did.port(), None); assert!(!did.is_loopback()); @@ -78,7 +80,7 @@ fn mints_production_did() { #[test] fn mints_development_did_with_encoded_port() { - let did = AgentDid::mint(&dev_zone(), "a1").expect("mint succeeds"); + let did = AccountDid::mint(&dev_zone(), "a1").expect("mint succeeds"); // The colon is hex-encoded because the spec requires it, and because an // unencoded one would read as a did:web path separator. assert_eq!(did.as_str(), "did:web:a1.agents.localhost%3A3000"); @@ -89,7 +91,7 @@ fn mints_development_did_with_encoded_port() { #[test] fn display_matches_as_str() { - let did = AgentDid::mint(&dev_zone(), "a1").expect("mint succeeds"); + let did = AccountDid::mint(&dev_zone(), "a1").expect("mint succeeds"); assert_eq!(did.to_string(), did.as_str()); } @@ -105,18 +107,18 @@ fn parse_round_trips_everything_mint_produces() { Zone::new("agents.example.co.uk").expect("a pds may delegate its own hostname"), ]; for zone in &zones { - for agent_id in ["a1", "a1b2c3", "0", "x-y"] { - let minted = AgentDid::mint(zone, agent_id).expect("mint succeeds"); - let parsed = AgentDid::parse(minted.as_str()).expect("mint output parses"); + for account_id in ["a1", "a1b2c3", "0", "x-y"] { + let minted = AccountDid::mint(zone, account_id).expect("mint succeeds"); + let parsed = AccountDid::parse(minted.as_str()).expect("mint output parses"); assert_eq!(parsed, minted); - assert_eq!(parsed.agent_id(), agent_id); + assert_eq!(parsed.account_id(), account_id); } } } #[test] fn document_url_uses_http_for_loopback() { - let did = AgentDid::parse("did:web:localhost%3A3000").expect("valid dev did"); + let did = AccountDid::parse("did:web:localhost%3A3000").expect("valid dev did"); assert_eq!( did.did_json_url(), "http://localhost:3000/.well-known/did.json" @@ -125,7 +127,7 @@ fn document_url_uses_http_for_loopback() { #[test] fn document_url_uses_https_in_production() { - let did = AgentDid::parse("did:web:a1.agents.example.com").expect("valid production did"); + let did = AccountDid::parse("did:web:a1.agents.example.com").expect("valid production did"); assert_eq!( did.did_json_url(), "https://a1.agents.example.com/.well-known/did.json" @@ -137,7 +139,7 @@ fn rejects_path_based_did() { // The colon is did:web's path separator, and atproto supports only // hostname-level DIDs. assert!(matches!( - AgentDid::parse("did:web:example.com:agents:a1"), + AccountDid::parse("did:web:example.com:agents:a1"), Err(DidError::PathBased(_)) )); } @@ -147,7 +149,7 @@ fn rejects_unencoded_port_colon() { // Indistinguishable from a path segment, which is exactly why the spec // requires the colon to be hex-encoded. assert!(matches!( - AgentDid::parse("did:web:localhost:3000"), + AccountDid::parse("did:web:localhost:3000"), Err(DidError::PathBased(_)) )); } @@ -155,7 +157,7 @@ fn rejects_unencoded_port_colon() { #[test] fn rejects_port_on_non_localhost() { assert!(matches!( - AgentDid::parse("did:web:a1.agents.example.com%3A3000"), + AccountDid::parse("did:web:a1.agents.example.com%3A3000"), Err(DidError::PortOnNonLocalhost(_)) )); assert!(matches!( @@ -169,12 +171,12 @@ fn rejects_port_on_non_localhost() { /// `u16::from_str` accepts a leading `+` and any run of leading zeros, so /// without an explicit check these four byte strings decode to one port, /// produce one document URL and one operator record key, and yet compare and -/// hash as four distinct `AgentDid`s. The `+` form is not even a +/// hash as four distinct `AccountDid`s. The `+` form is not even a /// syntactically valid DID: `validate_did` refuses it, so accepting it made /// the stricter parser the more permissive one. #[test] fn rejects_a_port_spelled_more_than_one_way() { - let canonical = AgentDid::parse("did:web:agents.localhost%3A3000").expect("the one spelling"); + let canonical = AccountDid::parse("did:web:agents.localhost%3A3000").expect("the one spelling"); assert_eq!(canonical.port(), Some(3000)); for spelling in [ @@ -183,7 +185,7 @@ fn rejects_a_port_spelled_more_than_one_way() { "did:web:agents.localhost%3A+3000", ] { assert!( - matches!(AgentDid::parse(spelling), Err(DidError::InvalidPort(_))), + matches!(AccountDid::parse(spelling), Err(DidError::InvalidPort(_))), "{spelling} decoded to the same account as {canonical}" ); } @@ -194,7 +196,7 @@ fn rejects_a_port_spelled_more_than_one_way() { // Port zero reaches nothing, so it is not a development affordance. assert!(matches!( - AgentDid::parse("did:web:agents.localhost%3A0"), + AccountDid::parse("did:web:agents.localhost%3A0"), Err(DidError::InvalidPort(_)) )); } @@ -204,11 +206,11 @@ fn rejects_encoded_path_separator() { // A crafted DID whose decoded host would carry a path, turning the fetch // URL into https://evil.example.com/../whatever. assert!(matches!( - AgentDid::parse("did:web:evil.example.com%2Fpath"), + AccountDid::parse("did:web:evil.example.com%2Fpath"), Err(DidError::EncodedPathSeparator(_)) )); assert!(matches!( - AgentDid::parse("did:web:evil.example.com%2fpath"), + AccountDid::parse("did:web:evil.example.com%2fpath"), Err(DidError::EncodedPathSeparator(_)) )); } @@ -216,17 +218,17 @@ fn rejects_encoded_path_separator() { #[test] fn rejects_other_percent_escapes() { assert!(matches!( - AgentDid::parse("did:web:a1.agents.example.com%20"), + AccountDid::parse("did:web:a1.agents.example.com%20"), Err(DidError::BadPercentEscape(_)) )); assert!(matches!( - AgentDid::parse("did:web:a1.agents.example.com%"), + AccountDid::parse("did:web:a1.agents.example.com%"), Err(DidError::BadPercentEscape(_)) )); // Lowercase hex is a second spelling of one identity; DIDs are compared // byte for byte, so it is rejected rather than normalized. assert!(matches!( - AgentDid::parse("did:web:localhost%3a3000"), + AccountDid::parse("did:web:localhost%3a3000"), Err(DidError::BadPercentEscape(_)) )); } @@ -234,11 +236,11 @@ fn rejects_other_percent_escapes() { #[test] fn rejects_uppercase_hosts() { assert!(matches!( - AgentDid::parse("did:web:A1.agents.example.com"), + AccountDid::parse("did:web:A1.agents.example.com"), Err(DidError::InvalidCharacter('A')) )); assert!(matches!( - AgentDid::parse("did:web:a1.Agents.EXAMPLE.com"), + AccountDid::parse("did:web:a1.Agents.EXAMPLE.com"), Err(DidError::InvalidCharacter(_)) )); } @@ -251,12 +253,12 @@ fn rejects_empty_labels() { "did:web:example.com.", ] { assert!( - matches!(AgentDid::parse(did), Err(DidError::EmptyLabel(_))), + matches!(AccountDid::parse(did), Err(DidError::EmptyLabel(_))), "expected an empty-label rejection for {did:?}" ); } assert!(matches!( - AgentDid::parse("did:web:"), + AccountDid::parse("did:web:"), Err(DidError::EmptyIdentifier) )); } @@ -273,7 +275,7 @@ fn rejects_over_length_did() { host.push_str("example.com"); let did = format!("did:web:{host}"); assert!(did.len() > MAX_DID_LENGTH); - assert!(matches!(AgentDid::parse(&did), Err(DidError::TooLong(_)))); + assert!(matches!(AccountDid::parse(&did), Err(DidError::TooLong(_)))); } #[test] @@ -286,7 +288,7 @@ fn rejects_other_methods() { "", ] { assert!( - matches!(AgentDid::parse(did), Err(DidError::NotDidWeb(_))), + matches!(AccountDid::parse(did), Err(DidError::NotDidWeb(_))), "expected a method rejection for {did:?}" ); } @@ -295,11 +297,11 @@ fn rejects_other_methods() { #[test] fn rejects_arpa_and_hyphen_edges() { assert!(matches!( - AgentDid::parse("did:web:a1.agents.arpa"), + AccountDid::parse("did:web:a1.agents.arpa"), Err(DidError::ArpaDomain) )); assert!(matches!( - AgentDid::parse("did:web:-a1.example.com"), + AccountDid::parse("did:web:-a1.example.com"), Err(DidError::HyphenEdge(_)) )); } @@ -307,10 +309,10 @@ fn rejects_arpa_and_hyphen_edges() { #[test] fn rejects_bad_agent_ids() { let zone = production_zone(); - for agent_id in ["", "a.b", "A1", "a_1", "-a"] { + for account_id in ["", "a.b", "A1", "a_1", "-a"] { assert!( - AgentDid::mint(&zone, agent_id).is_err(), - "expected {agent_id:?} to be refused as an agent id" + AccountDid::mint(&zone, account_id).is_err(), + "expected {account_id:?} to be refused as an agent id" ); } } @@ -365,7 +367,7 @@ fn fixture_document() -> DidDocument { #[test] fn builds_the_fixture_document() { - let did = AgentDid::mint(&production_zone(), "a1").expect("mint succeeds"); + let did = AccountDid::mint(&production_zone(), "a1").expect("mint succeeds"); let built = DidDocument::for_account( &did, KEY, @@ -408,7 +410,7 @@ fn document_json_round_trips() { #[test] fn resolves_from_the_in_memory_source() { - let did = AgentDid::mint(&dev_zone(), "a1").expect("mint succeeds"); + let did = AccountDid::mint(&dev_zone(), "a1").expect("mint succeeds"); let document = DidDocument::for_account(&did, KEY, "a1.agents.localhost", "http://localhost:3000"); let mut source = InMemoryDocuments::new(); @@ -423,8 +425,8 @@ fn resolves_from_the_in_memory_source() { fn resolution_rejects_a_document_claiming_another_did() { // The failure this guards against: a host serving a document that names // somebody else's DID, which without the check would be accepted whole. - let requested = AgentDid::mint(&production_zone(), "a1").expect("mint succeeds"); - let other = AgentDid::mint(&production_zone(), "a2").expect("mint succeeds"); + let requested = AccountDid::mint(&production_zone(), "a1").expect("mint succeeds"); + let other = AccountDid::mint(&production_zone(), "a2").expect("mint succeeds"); let impostor = DidDocument::for_account( &other, KEY, @@ -449,7 +451,7 @@ fn resolution_rejects_a_document_claiming_another_did() { #[test] fn resolution_reports_a_missing_document() { - let did = AgentDid::mint(&production_zone(), "a1").expect("mint succeeds"); + let did = AccountDid::mint(&production_zone(), "a1").expect("mint succeeds"); let source = InMemoryDocuments::new(); assert!(matches!( resolve(&source, did.as_str()), @@ -459,7 +461,7 @@ fn resolution_reports_a_missing_document() { #[test] fn resolution_reports_a_malformed_document() { - let did = AgentDid::mint(&production_zone(), "a1").expect("mint succeeds"); + let did = AccountDid::mint(&production_zone(), "a1").expect("mint succeeds"); let mut source = InMemoryDocuments::new(); source.insert_body(&did.did_json_url(), "{\"id\": 7}"); assert!(matches!( @@ -509,7 +511,7 @@ fn a_pds_may_delegate_its_own_hostname_and_subdomains() { assert_eq!(same.host(), "foo.bar"); assert_eq!(same.pds_host(), "foo.bar"); assert_eq!( - AgentDid::mint(&same, "myagent") + AccountDid::mint(&same, "myagent") .expect("mint succeeds") .as_str(), "did:web:myagent.foo.bar" @@ -517,7 +519,7 @@ fn a_pds_may_delegate_its_own_hostname_and_subdomains() { let nested = Zone::delegated("foo.bar", "agents.foo.bar").expect("subdomain zone"); assert_eq!( - AgentDid::mint(&nested, "myagent") + AccountDid::mint(&nested, "myagent") .expect("mint succeeds") .as_str(), "did:web:myagent.agents.foo.bar" @@ -534,7 +536,7 @@ fn a_pds_may_delegate_an_unrelated_zone() { assert_eq!(zone.host(), "garden.zone"); assert_eq!(zone.pds_host(), "foo.bar"); assert_eq!( - AgentDid::mint(&zone, "myagent") + AccountDid::mint(&zone, "myagent") .expect("mint succeeds") .as_str(), "did:web:myagent.garden.zone" @@ -553,7 +555,7 @@ fn a_server_may_delegate_a_sibling_of_its_own_hostname() { assert_eq!(zone.host(), "agents.example.com"); assert_eq!(zone.pds_host(), "pds.example.com"); - let did = AgentDid::mint(&zone, "a1").expect("mint succeeds from a sibling zone"); + let did = AccountDid::mint(&zone, "a1").expect("mint succeeds from a sibling zone"); assert_eq!(did.as_str(), "did:web:a1.agents.example.com"); } @@ -701,7 +703,7 @@ fn a_document_claims_only_the_first_valid_handle_back() { // entry mentions it": `alsoKnownAs` is an ordered list its controller // writes, so answering yes to a later entry would confirm a name no // resolver would ever return for this account. - let did = AgentDid::parse("did:web:kestrel.agents.example.com").expect("a valid did"); + let did = AccountDid::parse("did:web:kestrel.agents.example.com").expect("a valid did"); let mut document = DidDocument::for_account( &did, "zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme", diff --git a/crates/didbot-identity/tests/spec_conformance.rs b/crates/didbot-identity/tests/spec_conformance.rs index 5babf9e4..221e704b 100644 --- a/crates/didbot-identity/tests/spec_conformance.rs +++ b/crates/didbot-identity/tests/spec_conformance.rs @@ -27,7 +27,7 @@ //! Every test below names the clause it checks. use didbot_identity::document::{ATPROTO_KEY_FRAGMENT, ATPROTO_PDS_FRAGMENT, MULTIKEY_TYPE}; -use didbot_identity::{AgentDid, DidDocument, Zone}; +use didbot_identity::{AccountDid, DidDocument, Zone}; const KEY: &str = "zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF"; @@ -43,7 +43,7 @@ fn zone() -> Zone { /// the method and the end of the string decodes straight to that hostname. #[test] fn the_identifier_is_the_method_prefix_and_nothing_but_the_hostname() { - let did = AgentDid::mint(&zone(), "scribe").expect("mint succeeds"); + let did = AccountDid::mint(&zone(), "scribe").expect("mint succeeds"); assert_eq!(did.as_str(), "did:web:scribe.agents.example.com"); assert_eq!(did.host(), "scribe.agents.example.com"); // No `%3A` in this one at all: a port is a percent-encoded colon and none @@ -59,7 +59,7 @@ fn the_identifier_is_the_method_prefix_and_nothing_but_the_hostname() { /// not hide the mismatch from itself. #[test] fn a_port_is_carried_as_a_percent_encoded_colon() { - let did = AgentDid::parse("did:web:agents.localhost%3A3000").expect("spec-shaped example"); + let did = AccountDid::parse("did:web:agents.localhost%3A3000").expect("spec-shaped example"); assert_eq!(did.host(), "agents.localhost"); assert_eq!(did.as_str(), "did:web:agents.localhost%3A3000"); } @@ -69,7 +69,7 @@ fn a_port_is_carried_as_a_percent_encoded_colon() { /// string a caller asked to resolve, not merely "some non-empty id". #[test] fn the_documents_id_is_exactly_the_did_it_was_built_for() { - let did = AgentDid::mint(&zone(), "scribe").expect("mint succeeds"); + let did = AccountDid::mint(&zone(), "scribe").expect("mint succeeds"); let document = DidDocument::for_account( &did, KEY, @@ -85,7 +85,7 @@ fn the_documents_id_is_exactly_the_did_it_was_built_for() { /// a second lookup. #[test] fn the_signing_key_is_fragment_qualified_and_self_controlled() { - let did = AgentDid::mint(&zone(), "scribe").expect("mint succeeds"); + let did = AccountDid::mint(&zone(), "scribe").expect("mint succeeds"); let document = DidDocument::for_account( &did, KEY, @@ -109,7 +109,7 @@ fn the_signing_key_is_fragment_qualified_and_self_controlled() { /// as a document-only property. #[test] fn a_claimed_handle_resolves_back_to_the_same_did() { - let did = AgentDid::mint(&zone(), "scribe").expect("mint succeeds"); + let did = AccountDid::mint(&zone(), "scribe").expect("mint succeeds"); let handle = "scribe.agents.example.com"; let document = DidDocument::for_account(&did, KEY, handle, "https://pds.example.com"); @@ -130,7 +130,7 @@ fn a_claimed_handle_resolves_back_to_the_same_did() { #[test] fn an_encoded_path_separator_is_not_a_usable_did() { use didbot_identity::did::DidError; - let err = AgentDid::parse("did:web:example.com%2Fpath") + let err = AccountDid::parse("did:web:example.com%2Fpath") .expect_err("a did:web with an encoded path separator must be refused"); assert!( matches!( diff --git a/crates/didbot-name/src/generated.rs b/crates/didbot-name/src/generated.rs index 91586edd..a444c8cc 100644 --- a/crates/didbot-name/src/generated.rs +++ b/crates/didbot-name/src/generated.rs @@ -395,12 +395,12 @@ mod tests { use std::sync::atomic::{AtomicU64, Ordering}; /// The zone this crate's tests mint labels under. They assert against - /// `didbot_identity::AgentDid::mint` rather than a local copy of the + /// `didbot_identity::AccountDid::mint` rather than a local copy of the /// rules, so a test can only pass by agreeing with the actual authority. fn mints(label: &str) -> bool { let zone = didbot_identity::Zone::new("agents.example") .expect("agents.example is a valid zone host"); - didbot_identity::AgentDid::mint(&zone, label).is_ok() + didbot_identity::AccountDid::mint(&zone, label).is_ok() } #[derive(Debug)] diff --git a/crates/didbot-name/src/lib.rs b/crates/didbot-name/src/lib.rs index 7ddb5b41..4943fc4e 100644 --- a/crates/didbot-name/src/lib.rs +++ b/crates/didbot-name/src/lib.rs @@ -191,7 +191,7 @@ impl Namer for Fallback { /// trustworthy than the rest for having been written here. /// /// The character rules are not re-derived: they are -/// [`didbot_identity::validate_label`], the same function `AgentDid::mint` +/// [`didbot_identity::validate_label`], the same function `AccountDid::mint` /// checks an agent id against before it becomes part of a DID. A second, /// hand-maintained copy of "lowercase ascii, digits, hyphens, no hyphen at /// either end" is exactly how a name legal here and illegal there gets diff --git a/crates/didbot-onboarding/src/run.rs b/crates/didbot-onboarding/src/run.rs index 70b05dcd..363114dd 100644 --- a/crates/didbot-onboarding/src/run.rs +++ b/crates/didbot-onboarding/src/run.rs @@ -547,7 +547,7 @@ async fn policy_revision(env: &E, target: &Target) -> Verdict { } } -/// The deployment reports that `bot.did.provisionAgent` answers. +/// The deployment reports that `bot.did.createAccount` answers. async fn provisioning_open( env: &E, hostname: &str, @@ -562,19 +562,19 @@ async fn provisioning_open( .and_then(Value::as_array) .and_then(|surfaces| { surfaces.iter().find(|surface| { - surface.get("surface").and_then(Value::as_str) == Some("bot.did.provisionAgent") + surface.get("surface").and_then(Value::as_str) == Some("bot.did.createAccount") }) }) .and_then(|surface| surface.get("answering")) .and_then(Value::as_bool); match answering { - Some(true) => Verdict::passed("bot.did.provisionAgent answers".to_owned()), + Some(true) => Verdict::passed("bot.did.createAccount answers".to_owned()), Some(false) => Verdict::failed(format!( - "{hostname} is {} and bot.did.provisionAgent does not answer there", + "{hostname} is {} and bot.did.createAccount does not answer there", text(&status, "state").unwrap_or_else(|| "not ready".to_owned()) )), None => Verdict::failed(format!( - "{hostname}'s onboarding status does not list bot.did.provisionAgent" + "{hostname}'s onboarding status does not list bot.did.createAccount" )), } } diff --git a/crates/didbot-onboarding/src/step.rs b/crates/didbot-onboarding/src/step.rs index 20e9748e..805438f7 100644 --- a/crates/didbot-onboarding/src/step.rs +++ b/crates/didbot-onboarding/src/step.rs @@ -234,7 +234,7 @@ pub enum Check { ClaimObserved, /// The deployment reports which operator policy revision it enforces. PolicyRevision, - /// The deployment reports that `bot.did.provisionAgent` answers. + /// The deployment reports that `bot.did.createAccount` answers. ProvisioningOpen, } diff --git a/crates/didbot-onboarding/tests/steps.rs b/crates/didbot-onboarding/tests/steps.rs index 9b00a42b..ae99f5f1 100644 --- a/crates/didbot-onboarding/tests/steps.rs +++ b/crates/didbot-onboarding/tests/steps.rs @@ -157,7 +157,7 @@ impl Fixture { "claimStanding": !self.is(Broken::Claim), "surfaces": [ {"surface": "/.well-known/did.json", "answering": true}, - {"surface": "bot.did.provisionAgent", "answering": !self.is(Broken::Agent)}, + {"surface": "bot.did.createAccount", "answering": !self.is(Broken::Agent)}, ], }) .to_string() diff --git a/crates/didbot-operator/src/operate/write.rs b/crates/didbot-operator/src/operate/write.rs index 5404f52f..a7435089 100644 --- a/crates/didbot-operator/src/operate/write.rs +++ b/crates/didbot-operator/src/operate/write.rs @@ -107,7 +107,7 @@ pub trait RecordWriter { /// now lives at. /// /// `rkey` is the *decoded* authority of the server's own DID - /// ([`didbot_identity::did::AgentDid::authority`]), which is what the + /// ([`didbot_identity::did::AccountDid::authority`]), which is what the /// server's operator poll reads at -- not the hostname the command was /// given. /// The two differ for a development server on a port, and the encoded diff --git a/crates/didbot-pds/src/account.rs b/crates/didbot-pds/src/account.rs index 5f04e343..764a8b6f 100644 --- a/crates/didbot-pds/src/account.rs +++ b/crates/didbot-pds/src/account.rs @@ -9,11 +9,11 @@ use crate::lockout::{Hold, Holds, Lock, Locks, Tag}; use crate::subscribe::AccountStatus; use didbot_attest::Provenance; use didbot_fsm::LifecycleState; -use didbot_identity::AgentDid; +use didbot_identity::AccountDid; use didbot_key::{SigningKey, VerifyingKey}; use time::OffsetDateTime; -/// RFC 3339 serialization for [`AgentAccount::created_at`]. +/// RFC 3339 serialization for [`HostedAccount::created_at`]. /// /// Hand-written for the same reason the attestation crate's copy is: the /// `time` crate's serde support is not enabled in this workspace, and atproto @@ -47,13 +47,13 @@ mod rfc3339 { /// The identity crate does not derive serde, on purpose: a DID is only ever /// valid in its parsed form, and a derived implementation would let a /// malformed one in through a deserializer without going through -/// [`AgentDid::parse`]. This module keeps the parse on the way back in, and +/// [`AccountDid::parse`]. This module keeps the parse on the way back in, and /// re-attaches the hosting proof through [`HostedDid::replayed`]: the only /// thing that deserializes an account is this store's own log, which wrote /// the account down with the proof already checked. mod did_string { use crate::hosted::HostedDid; - use didbot_identity::AgentDid; + use didbot_identity::AccountDid; use serde::{Deserialize, Deserializer, Serialize, Serializer}; pub(super) fn serialize(did: &HostedDid, ser: S) -> Result { @@ -62,16 +62,16 @@ mod did_string { pub(super) fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result { let raw = String::deserialize(de)?; - AgentDid::parse(&raw) + AccountDid::parse(&raw) .map(HostedDid::replayed) .map_err(serde::de::Error::custom) } } -/// [`did_string`] over an `Option`, for [`AgentAccount::parent`]. +/// [`did_string`] over an `Option`, for [`HostedAccount::parent`]. mod optional_did_string { use crate::hosted::HostedDid; - use didbot_identity::AgentDid; + use didbot_identity::AccountDid; use serde::{Deserialize, Deserializer, Serialize, Serializer}; pub(super) fn serialize( @@ -86,7 +86,7 @@ mod optional_did_string { ) -> Result, D::Error> { Option::::deserialize(de)? .map(|raw| { - AgentDid::parse(&raw) + AccountDid::parse(&raw) .map(HostedDid::replayed) .map_err(serde::de::Error::custom) }) @@ -113,7 +113,7 @@ mod optional_did_string { /// [`AccountState::policy`]: does the DID document serve, does it carry a /// verification method, does the repository serve, and are writes accepted. /// That is the state's *baseline*. Whether the account serves right now is -/// the baseline with its locks subtracted — [`AgentAccount::policy`], over +/// the baseline with its locks subtracted — [`HostedAccount::policy`], over /// [`crate::lockout`] — which is what every enforcement point reads. /// /// Hard delete is deliberately not a variant here. There is no row left to @@ -225,7 +225,7 @@ impl LifecycleState for AccountState { /// /// See the type's own documentation for why this is the one match every /// other check goes through rather than asking `match state` itself. - /// [`AgentAccount::policy`] is what an enforcement point reads. + /// [`HostedAccount::policy`] is what an enforcement point reads. fn policy(self) -> StatePolicy { use AccountState::{Active, Decommissioned, Decommissioning, Provisioning, Reserved}; match self { @@ -389,10 +389,10 @@ pub struct SyncAccountStatus { /// under it. #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "camelCase")] -pub struct AgentAccount { +pub struct HostedAccount { /// The agent's `did:web`, minted under a zone this deployment serves. /// - /// A [`HostedDid`] rather than a bare [`AgentDid`]: the account store is + /// A [`HostedDid`] rather than a bare [`AccountDid`]: the account store is /// keyed by it, and the type is what makes "every account is a name in /// a zone this deployment serves" a fact about construction rather than /// a check some caller has to remember. @@ -400,13 +400,13 @@ pub struct AgentAccount { pub did: HostedDid, /// The label the DID was minted from, kept separately because the caller /// supplied it and round-tripping it out of the DID is a parse. - pub agent_id: String, + pub account_id: String, /// The claimed atproto handle, if the request asked for one. pub handle: Option, /// The harness that asked for the account, if the caller named one. /// /// Here rather than in the ledger, which records what this deployment did - /// or checked: the identifier was minted from `agent_id`, the attestation + /// or checked: the identifier was minted from `account_id`, the attestation /// established the backend and the node, and the parent was verified to be /// an account this server hosts. Nothing was done with this. It is the /// harness's word, passing through to the registration record, and it sits @@ -440,7 +440,7 @@ pub struct AgentAccount { /// How long the account keeps resolving once it stops being used. /// /// The one mechanism, and the pin is one of its settings: see - /// [`Retention`] and [`AgentAccount::pinned`]. [`AccountStore::remove`] + /// [`Retention`] and [`HostedAccount::pinned`]. [`AccountStore::remove`] /// refuses an account held forever rather than leaving the protection to /// callers to remember. /// @@ -451,7 +451,7 @@ pub struct AgentAccount { /// What the attestation backend was willing to record about the request. /// /// Absent for an account no backend admitted, which is exactly one - /// account: this server's own, built by [`AgentAccount::server`]. Nothing + /// account: this server's own, built by [`HostedAccount::server`]. Nothing /// attests the party that does the attesting, and a synthetic claim here /// would be a stored record saying a check happened. Absent on the wire /// rather than null, so a log written before this was optional replays @@ -501,7 +501,7 @@ pub struct AgentAccount { /// key from the one this server signs the repository with: the host /// minted this one where nothing else can read it, and it is what an /// operator's `bot.did.operator` claim binds — see - /// [`AgentAccount::is_reservation`]. + /// [`HostedAccount::is_reservation`]. #[serde(default, skip_serializing_if = "Option::is_none")] pub node_key: Option, /// The operator DID a reservation named as the one it expects a vouch from. @@ -515,7 +515,7 @@ pub struct AgentAccount { pub expected_operator: Option, } -impl AgentAccount { +impl HostedAccount { /// What this account serves and accepts right now: the state's baseline /// with every hung lock subtracted. /// @@ -565,7 +565,7 @@ impl AgentAccount { /// Whether the account is protected from garbage collection. /// /// Derived, not stored: a pin is [`Retention::Forever`] and nothing else. - /// It stays a question worth asking by that name — `bot.did.setAgentPinned` + /// It stays a question worth asking by that name — `bot.did.setAccountPinned` /// asks it, and so does every sweep — but the answer comes from the one /// value that decides it. pub fn pinned(&self) -> bool { @@ -578,7 +578,7 @@ impl AgentAccount { /// to admit, no identifier to mint, and no hostname to publish, because /// the apex is the name the server is already served on. What is left is /// what every other account also has — a DID, a key, a repository — which - /// is the whole reason it is an [`AgentAccount`] and not a second kind of + /// is the whole reason it is an [`HostedAccount`] and not a second kind of /// thing. See [`Provisioner::ensure_server_account`]. /// /// Two fields say what it is not. `provenance` is absent, because @@ -586,7 +586,7 @@ impl AgentAccount { /// the apex hostname is claimed anyway, by the same default every account /// gets when it stored no handle. /// - /// `agent_id` is the apex hostname. Every other account's is the label + /// `account_id` is the apex hostname. Every other account's is the label /// its DID was minted from, and the apex was minted from nothing, so the /// honest answer is the name itself rather than a label that was never /// chosen. @@ -594,7 +594,7 @@ impl AgentAccount { /// [`Provisioner::ensure_server_account`]: crate::Provisioner::ensure_server_account pub fn server(did: HostedDid, created_at: OffsetDateTime) -> Self { Self { - agent_id: did.host().to_owned(), + account_id: did.host().to_owned(), did, handle: None, // No harness asked for this one: the server adopted its own apex. @@ -706,10 +706,10 @@ pub trait AccountStore: Send + Sync { /// Records a new account and its signing key. /// /// Fails with [`StoreError::AlreadyExists`] rather than overwriting. - fn insert(&self, account: AgentAccount, key: SigningKey) -> Result<(), StoreError>; + fn insert(&self, account: HostedAccount, key: SigningKey) -> Result<(), StoreError>; /// The account for `did`, if it exists. - fn get(&self, did: &HostedDid) -> Option; + fn get(&self, did: &HostedDid) -> Option; /// The signing key for `did`, if it exists. /// @@ -737,7 +737,7 @@ pub trait AccountStore: Send + Sync { /// /// Must refuse a pinned account with [`StoreError::Pinned`] and one /// holding [`Hold::PreventDataDeletion`] with [`StoreError::Held`]. - fn remove(&self, did: &HostedDid) -> Result; + fn remove(&self, did: &HostedDid) -> Result; /// Sets an account's retention. /// @@ -788,10 +788,10 @@ pub trait AccountStore: Send + Sync { /// Callers that count references filter by /// [`AccountState::is_live`]; callers that cascade a lock walk all of /// them. - fn children(&self, parent: &HostedDid) -> Vec; + fn children(&self, parent: &HostedDid) -> Vec; /// Every account, ordered by DID. - fn list(&self) -> Vec; + fn list(&self) -> Vec; /// How many accounts are stored. /// @@ -822,11 +822,11 @@ pub trait AccountStore: Send + Sync { /// this the choice is a wrapper newtype at every call site or a fourth type /// parameter, and both are worse than four lines of forwarding. impl AccountStore for std::sync::Arc { - fn insert(&self, account: AgentAccount, key: SigningKey) -> Result<(), StoreError> { + fn insert(&self, account: HostedAccount, key: SigningKey) -> Result<(), StoreError> { (**self).insert(account, key) } - fn get(&self, did: &HostedDid) -> Option { + fn get(&self, did: &HostedDid) -> Option { (**self).get(did) } @@ -838,7 +838,7 @@ impl AccountStore for std::sync::Arc { (**self).verifying_key(did) } - fn remove(&self, did: &HostedDid) -> Result { + fn remove(&self, did: &HostedDid) -> Result { (**self).remove(did) } @@ -870,11 +870,11 @@ impl AccountStore for std::sync::Arc { (**self).set_parent(did, parent) } - fn children(&self, parent: &HostedDid) -> Vec { + fn children(&self, parent: &HostedDid) -> Vec { (**self).children(parent) } - fn list(&self) -> Vec { + fn list(&self) -> Vec { (**self).list() } @@ -908,7 +908,7 @@ pub struct MemoryAccountStore { #[derive(Debug, Clone)] struct Entry { - account: AgentAccount, + account: HostedAccount, key: SigningKey, } @@ -986,7 +986,7 @@ impl MemoryAccountStore { fn update( &self, did: &HostedDid, - change: impl FnOnce(&mut AgentAccount), + change: impl FnOnce(&mut HostedAccount), ) -> Result<(), StoreError> { let mut entries = self.entries(); let entry = entries @@ -998,7 +998,7 @@ impl MemoryAccountStore { Ok(()) } - pub(crate) fn snapshot_with_keys(&self) -> Vec<(AgentAccount, SigningKey)> { + pub(crate) fn snapshot_with_keys(&self) -> Vec<(HostedAccount, SigningKey)> { self.entries() .values() .map(|entry| (entry.account.clone(), entry.key.clone())) @@ -1007,7 +1007,7 @@ impl MemoryAccountStore { } impl AccountStore for MemoryAccountStore { - fn insert(&self, account: AgentAccount, key: SigningKey) -> Result<(), StoreError> { + fn insert(&self, account: HostedAccount, key: SigningKey) -> Result<(), StoreError> { let did = account.did.as_str().to_owned(); let mut entries = self.entries(); if entries.contains_key(&did) { @@ -1020,7 +1020,7 @@ impl AccountStore for MemoryAccountStore { Ok(()) } - fn get(&self, did: &HostedDid) -> Option { + fn get(&self, did: &HostedDid) -> Option { self.entries().get(did.as_str()).map(|e| e.account.clone()) } @@ -1036,7 +1036,7 @@ impl AccountStore for MemoryAccountStore { .map(|e| e.key.verifying_key()) } - fn remove(&self, did: &HostedDid) -> Result { + fn remove(&self, did: &HostedDid) -> Result { let mut entries = self.entries(); let entry = entries .get(did.as_str()) @@ -1129,7 +1129,7 @@ impl AccountStore for MemoryAccountStore { } } - fn children(&self, parent: &HostedDid) -> Vec { + fn children(&self, parent: &HostedDid) -> Vec { let dids: Vec = self .children_index() .get(parent.as_str()) @@ -1141,7 +1141,7 @@ impl AccountStore for MemoryAccountStore { .collect() } - fn list(&self) -> Vec { + fn list(&self) -> Vec { self.entries().values().map(|e| e.account.clone()).collect() } @@ -1186,7 +1186,7 @@ impl crate::journal::Journaled for MemoryAccountStore { // own schedule", which are two of the retentions an account can // hold. crate::wal::Entry::AccountPinned { did, pinned } => { - if let Ok(did) = AgentDid::parse(&did).map(HostedDid::replayed) { + if let Ok(did) = AccountDid::parse(&did).map(HostedDid::replayed) { let retention = if pinned { Retention::Forever } else { @@ -1197,39 +1197,39 @@ impl crate::journal::Journaled for MemoryAccountStore { } } crate::wal::Entry::AccountRetained { did, retention } => { - if let Ok(did) = AgentDid::parse(&did).map(HostedDid::replayed) { + if let Ok(did) = AccountDid::parse(&did).map(HostedDid::replayed) { let _ = self.set_retention(&did, retention); } } crate::wal::Entry::AccountStateChanged { did, state } => { - if let Ok(did) = AgentDid::parse(&did).map(HostedDid::replayed) { + if let Ok(did) = AccountDid::parse(&did).map(HostedDid::replayed) { let _ = self.set_state(&did, state); } } crate::wal::Entry::AccountLocked { did, lock, party } => { - if let Ok(did) = AgentDid::parse(&did).map(HostedDid::replayed) { + if let Ok(did) = AccountDid::parse(&did).map(HostedDid::replayed) { let _ = self.hang(&did, Tag::new(lock, party)); } } crate::wal::Entry::AccountUnlocked { did, lock, party } => { - if let Ok(did) = AgentDid::parse(&did).map(HostedDid::replayed) { + if let Ok(did) = AccountDid::parse(&did).map(HostedDid::replayed) { let _ = self.lift(&did, Tag::new(lock, party)); } } crate::wal::Entry::AccountHoldSet { did, hold } => { - if let Ok(did) = AgentDid::parse(&did).map(HostedDid::replayed) { + if let Ok(did) = AccountDid::parse(&did).map(HostedDid::replayed) { let _ = self.set_hold(&did, hold); } } crate::wal::Entry::AccountHoldCleared { did, hold } => { - if let Ok(did) = AgentDid::parse(&did).map(HostedDid::replayed) { + if let Ok(did) = AccountDid::parse(&did).map(HostedDid::replayed) { let _ = self.clear_hold(&did, hold); } } crate::wal::Entry::AccountParented { did, parent } => { if let (Ok(did), Ok(parent)) = ( - AgentDid::parse(&did).map(HostedDid::replayed), - AgentDid::parse(&parent).map(HostedDid::replayed), + AccountDid::parse(&did).map(HostedDid::replayed), + AccountDid::parse(&parent).map(HostedDid::replayed), ) { let _ = self.set_parent(&did, &parent); } @@ -1268,15 +1268,15 @@ mod tests { didbot_identity::Zone::new("agents.localhost").expect("a valid zone") } - fn hosted(did: AgentDid) -> HostedDid { + fn hosted(did: AccountDid) -> HostedDid { HostedDid::host(did, &didbot_identity::ZoneRegistry::single(zone())).expect("hosted") } - fn account() -> AgentAccount { + fn account() -> HostedAccount { let zone = zone(); - AgentAccount { - did: hosted(AgentDid::mint(&zone, "kestrel").expect("a mintable label")), - agent_id: "kestrel".to_owned(), + HostedAccount { + did: hosted(AccountDid::mint(&zone, "kestrel").expect("a mintable label")), + account_id: "kestrel".to_owned(), handle: None, harness: None, agent_type: None, @@ -1306,7 +1306,7 @@ mod tests { object.remove("nameProvenance"); object.insert("pinned".to_owned(), serde_json::Value::Bool(true)); - let read: AgentAccount = serde_json::from_value(written).expect("deserializes"); + let read: HostedAccount = serde_json::from_value(written).expect("deserializes"); assert_eq!(read.retention, Retention::Forever); assert!(read.pinned(), "the pin must survive the rename"); assert_eq!(read.kind, AccountKind::Agent); @@ -1321,7 +1321,7 @@ mod tests { object.remove("retention"); object.insert("pinned".to_owned(), serde_json::Value::Bool(false)); - let read: AgentAccount = serde_json::from_value(written).expect("deserializes"); + let read: HostedAccount = serde_json::from_value(written).expect("deserializes"); assert_eq!(read.retention, Retention::Deployment); assert!(!read.pinned()); } @@ -1337,7 +1337,7 @@ mod tests { .expect("an account is an object") .remove("retention"); - let read: AgentAccount = serde_json::from_value(written).expect("deserializes"); + let read: HostedAccount = serde_json::from_value(written).expect("deserializes"); assert_eq!(read.retention, Retention::Deployment); } @@ -1372,15 +1372,16 @@ mod tests { #[test] fn an_unknown_account_has_no_public_half() { let store = MemoryAccountStore::new(); - let did = hosted(AgentDid::parse("did:web:nobody.agents.localhost").expect("a valid did")); + let did = + hosted(AccountDid::parse("did:web:nobody.agents.localhost").expect("a valid did")); assert!(store.verifying_key(&did).is_none()); assert!(store.signing_key(&did).is_none()); } #[test] fn the_servers_own_account_is_held_forever() { - let did = hosted(AgentDid::parse("did:web:agents.localhost").expect("a valid apex did")); - let server = AgentAccount::server( + let did = hosted(AccountDid::parse("did:web:agents.localhost").expect("a valid apex did")); + let server = HostedAccount::server( did, OffsetDateTime::from_unix_timestamp(1_700_000_000).expect("a valid clock"), ); @@ -1396,8 +1397,8 @@ mod tests { let store = MemoryAccountStore::new(); let first = account(); let mut second = account(); - second.did = hosted(AgentDid::mint(&zone(), "murrelet").expect("a mintable label")); - second.agent_id = "murrelet".to_owned(); + second.did = hosted(AccountDid::mint(&zone(), "murrelet").expect("a mintable label")); + second.account_id = "murrelet".to_owned(); let did = first.did.clone(); store .insert(first, SigningKey::generate()) diff --git a/crates/didbot-pds/src/bsky.rs b/crates/didbot-pds/src/bsky.rs index 6fa2c66a..60d5eaf3 100644 --- a/crates/didbot-pds/src/bsky.rs +++ b/crates/didbot-pds/src/bsky.rs @@ -41,7 +41,7 @@ use serde_json::{json, Map, Value}; -use crate::account::AgentAccount; +use crate::account::HostedAccount; use time::format_description::well_known::Rfc3339; /// The profile record an ordinary atproto client reads. @@ -74,7 +74,7 @@ pub const BOT_LABEL: &str = "bot"; /// a present value came from the account and is a better answer than one /// derived from a handle or a hash. Every other field is passed through /// untouched, including ones this server has never heard of. -pub fn build(account: &AgentAccount, existing: Option<&Value>, avatar: Option<&Value>) -> Value { +pub fn build(account: &HostedAccount, existing: Option<&Value>, avatar: Option<&Value>) -> Value { let mut record = existing .and_then(Value::as_object) .cloned() @@ -138,7 +138,7 @@ fn with_bot_label(existing: Option<&Value>) -> Value { /// default when nothing names the agents: that label is the opaque one the /// DID was minted from, and putting it in a display name would dress an /// identifier up as a name. -pub(crate) fn display_name(account: &AgentAccount) -> Option { +pub(crate) fn display_name(account: &HostedAccount) -> Option { let handle = account.handle.as_deref()?; if handle.eq_ignore_ascii_case(account.did.host()) { return None; @@ -157,7 +157,7 @@ pub(crate) fn display_name(account: &AgentAccount) -> Option { /// this crate produced with `OffsetDateTime::now_utc`. A profile with a wrong /// `createdAt` is worth more than no profile, because the field the record /// exists to carry is the lineage beside it. -pub(crate) fn created_at(account: &AgentAccount) -> String { +pub(crate) fn created_at(account: &HostedAccount) -> String { account .created_at .to_offset(time::UtcOffset::UTC) @@ -169,21 +169,21 @@ pub(crate) fn created_at(account: &AgentAccount) -> String { mod tests { use super::*; use didbot_attest::{Assurance, Provenance}; - use didbot_identity::{AgentDid, Zone}; + use didbot_identity::{AccountDid, Zone}; use time::OffsetDateTime; - fn account(handle: Option<&str>) -> AgentAccount { + fn account(handle: Option<&str>) -> HostedAccount { let zone = Zone::new("agents.example").expect("zone"); let did = crate::hosted::HostedDid::host( - AgentDid::mint(&zone, "a3f9c1").expect("mint"), + AccountDid::mint(&zone, "a3f9c1").expect("mint"), &didbot_identity::ZoneRegistry::single(zone.clone()), ) .expect("hosted"); - AgentAccount { + HostedAccount { harness: None, agent_type: None, did, - agent_id: "a3f9c1".to_owned(), + account_id: "a3f9c1".to_owned(), handle: handle.map(str::to_owned), created_at: OffsetDateTime::UNIX_EPOCH, kind: crate::kind::AccountKind::Agent, diff --git a/crates/didbot-pds/src/credential.rs b/crates/didbot-pds/src/credential.rs index e551e33b..539a94df 100644 --- a/crates/didbot-pds/src/credential.rs +++ b/crates/didbot-pds/src/credential.rs @@ -18,7 +18,7 @@ //! //! # Why the token is hashed rather than stored //! -//! The bytes that leave this process in a `provisionAgent` response are the +//! The bytes that leave this process in a `createAccount` response are the //! only copy: what the store keeps is a SHA-256 digest, the same shape a //! content identifier already takes elsewhere in this crate. A fast //! cryptographic hash rather than a password's slow key-derivation function, @@ -52,9 +52,9 @@ const TOKEN_BYTES: usize = 32; /// A freshly minted token, and when it stops working. /// /// The plaintext token is not retrievable again: what -/// [`AgentTokenStore::issue`] hands back here is the only copy that will ever +/// [`AccountTokenStore::issue`] hands back here is the only copy that will ever /// leave this process, and every later check goes through -/// [`AgentTokenStore::verify`] against the hash the store actually kept. +/// [`AccountTokenStore::verify`] against the hash the store actually kept. #[derive(Debug, Clone, PartialEq, Eq)] pub struct IssuedToken { /// The bearer token, exactly as a request should present it. @@ -83,7 +83,7 @@ pub enum TokenError { /// deployment that wants several credentials per account — one per harness /// process, say — is naming a need `plan/credentials.md` has not settled /// yet. -pub trait AgentTokenStore: Send + Sync { +pub trait AccountTokenStore: Send + Sync { /// Mints a fresh token for `did`, replacing whatever token that account /// held before. /// @@ -127,7 +127,7 @@ struct Record { /// `issue` and `revoke` are given the account and need to find (and drop) its /// current token hash. #[derive(Debug, Default)] -pub struct MemoryAgentTokenStore { +pub struct MemoryAccountTokenStore { /// Token hash to record. What a `verify` call walks. by_hash: Mutex>, /// Account to its current token hash, so `issue` and `revoke` do not have @@ -135,7 +135,7 @@ pub struct MemoryAgentTokenStore { by_did: Mutex>, } -impl MemoryAgentTokenStore { +impl MemoryAccountTokenStore { /// An empty store. pub fn new() -> Self { Self::default() @@ -145,7 +145,7 @@ impl MemoryAgentTokenStore { /// replay, where the plaintext is gone and only the hash was ever kept. /// /// No expiry check here: replay reconstructs whatever the log says was - /// true, including a token that has since expired. [`AgentTokenStore::verify`] + /// true, including a token that has since expired. [`AccountTokenStore::verify`] /// is where an expired token is refused, at the moment somebody presents /// it, which is the same rule every other store in this crate follows — /// replay does not re-adjudicate a fact that was already true when it was @@ -169,8 +169,8 @@ impl MemoryAgentTokenStore { /// Whether `did` currently holds a live token. /// /// Cheap on purpose — a `by_did` lookup rather than a scan of - /// [`MemoryAgentTokenStore::snapshot`] — so a caller like - /// [`crate::FileAgentTokenStore::revoke`] can decide whether there is + /// [`MemoryAccountTokenStore::snapshot`] — so a caller like + /// [`crate::FileAccountTokenStore::revoke`] can decide whether there is /// anything worth writing a log entry over without paying for a full walk /// on every account deletion. pub fn holds(&self, did: &str) -> bool { @@ -181,7 +181,7 @@ impl MemoryAgentTokenStore { } /// Removes a token this process forgot, by hash — the mirror of - /// [`MemoryAgentTokenStore::restore`], for replaying a revocation. + /// [`MemoryAccountTokenStore::restore`], for replaying a revocation. pub fn forget(&self, did: &str) { let mut by_hash = self.by_hash.lock().unwrap_or_else(|p| p.into_inner()); let mut by_did = self.by_did.lock().unwrap_or_else(|p| p.into_inner()); @@ -202,7 +202,7 @@ impl MemoryAgentTokenStore { } } -impl AgentTokenStore for MemoryAgentTokenStore { +impl AccountTokenStore for MemoryAccountTokenStore { fn issue(&self, did: &str, ttl: time::Duration) -> IssuedToken { let mut bytes = [0u8; TOKEN_BYTES]; OsRng @@ -228,25 +228,25 @@ impl AgentTokenStore for MemoryAgentTokenStore { } } -impl crate::journal::Journaled for MemoryAgentTokenStore { +impl crate::journal::Journaled for MemoryAccountTokenStore { const STORE: &'static str = "credentials"; fn owns(entry: &crate::wal::Entry) -> bool { matches!( entry, - crate::wal::Entry::AgentTokenIssued { .. } - | crate::wal::Entry::AgentTokenRevoked { .. } + crate::wal::Entry::AccountTokenIssued { .. } + | crate::wal::Entry::AccountTokenRevoked { .. } ) } fn apply(&self, entry: crate::wal::Entry) { match entry { - crate::wal::Entry::AgentTokenIssued { + crate::wal::Entry::AccountTokenIssued { did, token_hash, expires_at, } => self.restore(&did, &token_hash, expires_at), - crate::wal::Entry::AgentTokenRevoked { did } => self.forget(&did), + crate::wal::Entry::AccountTokenRevoked { did } => self.forget(&did), // Only what `owns` claims reaches here. _ => {} } @@ -261,7 +261,7 @@ impl crate::journal::Journaled for MemoryAgentTokenStore { self.snapshot() .into_iter() .map( - |(did, token_hash, expires_at)| crate::wal::Entry::AgentTokenIssued { + |(did, token_hash, expires_at)| crate::wal::Entry::AccountTokenIssued { did, token_hash, expires_at, @@ -281,20 +281,20 @@ mod tests { #[test] fn an_issued_token_verifies_as_its_own_did() { - let store = MemoryAgentTokenStore::new(); + let store = MemoryAccountTokenStore::new(); let issued = store.issue("did:web:a", DEFAULT_AGENT_TOKEN_TTL); assert_eq!(store.verify(&issued.token), Ok("did:web:a".to_owned())); } #[test] fn an_unknown_token_is_unknown() { - let store = MemoryAgentTokenStore::new(); + let store = MemoryAccountTokenStore::new(); assert_eq!(store.verify("not a real token"), Err(TokenError::Unknown)); } #[test] fn issuing_again_revokes_the_previous_token() { - let store = MemoryAgentTokenStore::new(); + let store = MemoryAccountTokenStore::new(); let first = store.issue("did:web:a", DEFAULT_AGENT_TOKEN_TTL); let second = store.issue("did:web:a", DEFAULT_AGENT_TOKEN_TTL); assert_eq!(store.verify(&first.token), Err(TokenError::Unknown)); @@ -303,7 +303,7 @@ mod tests { #[test] fn revoking_forgets_the_token() { - let store = MemoryAgentTokenStore::new(); + let store = MemoryAccountTokenStore::new(); let issued = store.issue("did:web:a", DEFAULT_AGENT_TOKEN_TTL); store.revoke("did:web:a"); assert_eq!(store.verify(&issued.token), Err(TokenError::Unknown)); @@ -311,13 +311,13 @@ mod tests { #[test] fn revoking_an_account_with_no_token_is_a_no_op() { - let store = MemoryAgentTokenStore::new(); + let store = MemoryAccountTokenStore::new(); store.revoke("did:web:nobody"); } #[test] fn a_token_past_its_expiry_is_expired_not_unknown() { - let store = MemoryAgentTokenStore::new(); + let store = MemoryAccountTokenStore::new(); let issued = store.issue("did:web:a", time::Duration::seconds(0)); // The TTL already elapsed the instant it was issued. std::thread::sleep(std::time::Duration::from_millis(5)); @@ -326,7 +326,7 @@ mod tests { #[test] fn two_accounts_have_independent_tokens() { - let store = MemoryAgentTokenStore::new(); + let store = MemoryAccountTokenStore::new(); let a = store.issue("did:web:a", DEFAULT_AGENT_TOKEN_TTL); let b = store.issue("did:web:b", DEFAULT_AGENT_TOKEN_TTL); assert_eq!(store.verify(&a.token), Ok("did:web:a".to_owned())); @@ -335,7 +335,7 @@ mod tests { #[test] fn restore_replaces_whatever_hash_the_did_held() { - let store = MemoryAgentTokenStore::new(); + let store = MemoryAccountTokenStore::new(); let now = OffsetDateTime::now_utc() + time::Duration::days(1); store.restore("did:web:a", "hash-one", now); store.restore("did:web:a", "hash-two", now); @@ -347,11 +347,11 @@ mod tests { /// the whole of what this store holds. #[test] fn a_checkpoint_of_the_credentials_replays_into_the_same_tokens() { - let store = MemoryAgentTokenStore::new(); + let store = MemoryAccountTokenStore::new(); let issued = store.issue("did:web:a", DEFAULT_AGENT_TOKEN_TTL); let other = store.issue("did:web:b", DEFAULT_AGENT_TOKEN_TTL); - let fresh = MemoryAgentTokenStore::new(); + let fresh = MemoryAccountTokenStore::new(); let (written, replayed) = crate::journal::round_trip(&store, &fresh); assert_eq!(written, replayed); assert_eq!(fresh.verify(&issued.token), Ok("did:web:a".to_owned())); @@ -364,7 +364,7 @@ mod tests { /// the `Unknown` a dropped entry would give. #[test] fn a_checkpoint_carries_an_expired_token_so_it_still_answers_expired() { - let store = MemoryAgentTokenStore::new(); + let store = MemoryAccountTokenStore::new(); let issued = store.issue("did:web:a", time::Duration::seconds(1)); store.restore( "did:web:a", @@ -372,7 +372,7 @@ mod tests { OffsetDateTime::now_utc() - time::Duration::days(1), ); - let fresh = MemoryAgentTokenStore::new(); + let fresh = MemoryAccountTokenStore::new(); let (written, replayed) = crate::journal::round_trip(&store, &fresh); assert_eq!(written, replayed); assert_eq!(fresh.verify(&issued.token), Err(TokenError::Expired)); diff --git a/crates/didbot-pds/src/durable.rs b/crates/didbot-pds/src/durable.rs index a7d2d405..2351137c 100644 --- a/crates/didbot-pds/src/durable.rs +++ b/crates/didbot-pds/src/durable.rs @@ -64,16 +64,16 @@ use didbot_key::SigningKey; use serde_json::Value; use time::OffsetDateTime; -use crate::account::{AccountState, AccountStore, AgentAccount, MemoryAccountStore, StoreError}; +use crate::account::{AccountState, AccountStore, HostedAccount, MemoryAccountStore, StoreError}; use crate::blobs::{ canonical_cid, did_path, BlobError, BlobIndex, BlobLimits, BlobRef, BlobStats, BlobStore, BlobTally, BlobUpload, CollectedBlob, Fetch, }; -use crate::credential::{AgentTokenStore, IssuedToken, MemoryAgentTokenStore, TokenError}; +use crate::credential::{AccountTokenStore, IssuedToken, MemoryAccountTokenStore, TokenError}; use crate::heap::Heap; use crate::history::{CommitStore, Head, HistoryStats, MemoryCommitStore}; use crate::journal::{Derived, Journaled, Mark, Stores}; -use crate::ledger::{AgentLedger, LedgerEntry, LedgerEvent, LedgerStore, MemoryLedger}; +use crate::ledger::{AccountLedger, LedgerEntry, LedgerEvent, LedgerStore, MemoryLedger}; use crate::names::{DurableCounter, NameRegistry}; use crate::oauth::{ Grant, GrantRequest, Lifetimes, MemoryOAuthGrantStore, MintedGrant, OAuthGrantStore, Rotation, @@ -104,7 +104,7 @@ pub struct Durable { ledger: Arc, history: Arc, names: Arc, - credentials: Arc, + credentials: Arc, oauth: Arc, counter: Arc, sequence: Arc, @@ -240,7 +240,7 @@ impl Durable { let ledger = MemoryLedger::new(); let commits = MemoryCommitStore::new(); let names = NameRegistry::new(hold); - let credentials = MemoryAgentTokenStore::new(); + let credentials = MemoryAccountTokenStore::new(); let oauth = MemoryOAuthGrantStore::new(); let counter = DurableCounter::new(); let floor = Arc::new(SequenceFloor::new()); @@ -460,7 +460,7 @@ impl Durable { inner: commits, wal: wal.clone(), }), - credentials: Arc::new(FileAgentTokenStore { + credentials: Arc::new(FileAccountTokenStore { inner: credentials, wal: wal.clone(), }), @@ -522,7 +522,7 @@ impl Durable { } /// The agent token store, already writing what it issues into the log. - pub fn credentials(&self) -> Arc { + pub fn credentials(&self) -> Arc { self.credentials.clone() } @@ -596,7 +596,7 @@ fn apply( ledger: &MemoryLedger, commits: &MemoryCommitStore, names: &NameRegistry, - credentials: &MemoryAgentTokenStore, + credentials: &MemoryAccountTokenStore, oauth: &MemoryOAuthGrantStore, counter: &DurableCounter, floor: &SequenceFloor, @@ -671,7 +671,7 @@ fn apply( commits.apply(entry); } else if NameRegistry::owns(&entry) { names.apply(entry); - } else if MemoryAgentTokenStore::owns(&entry) { + } else if MemoryAccountTokenStore::owns(&entry) { credentials.apply(entry); } else if MemoryOAuthGrantStore::owns(&entry) { oauth.apply(entry); @@ -700,7 +700,7 @@ fn snapshot( ledger: &MemoryLedger, commits: &MemoryCommitStore, names: &NameRegistry, - credentials: &MemoryAgentTokenStore, + credentials: &MemoryAccountTokenStore, oauth: &MemoryOAuthGrantStore, counter: &DurableCounter, floor: &SequenceFloor, @@ -1024,7 +1024,7 @@ impl FileAccountStore { } impl AccountStore for FileAccountStore { - fn insert(&self, account: AgentAccount, key: SigningKey) -> Result<(), StoreError> { + fn insert(&self, account: HostedAccount, key: SigningKey) -> Result<(), StoreError> { let _write = self.write(); if self.inner.get(&account.did).is_some() { return Err(StoreError::AlreadyExists { @@ -1038,7 +1038,7 @@ impl AccountStore for FileAccountStore { self.inner.insert(account, key) } - fn get(&self, did: &HostedDid) -> Option { + fn get(&self, did: &HostedDid) -> Option { self.inner.get(did) } @@ -1050,7 +1050,7 @@ impl AccountStore for FileAccountStore { self.inner.verifying_key(did) } - fn remove(&self, did: &HostedDid) -> Result { + fn remove(&self, did: &HostedDid) -> Result { let _write = self.write(); let account = self.inner.get(did).ok_or_else(|| StoreError::NotFound { did: did.as_str().to_owned(), @@ -1171,11 +1171,11 @@ impl AccountStore for FileAccountStore { self.inner.set_parent(did, parent) } - fn children(&self, parent: &HostedDid) -> Vec { + fn children(&self, parent: &HostedDid) -> Vec { self.inner.children(parent) } - fn list(&self) -> Vec { + fn list(&self) -> Vec { self.inner.list() } @@ -2299,7 +2299,7 @@ impl LedgerStore for FileLedger { Ok(entry) } - fn read(&self, did: &str) -> Option { + fn read(&self, did: &str) -> Option { self.inner.read(did) } @@ -2307,7 +2307,7 @@ impl LedgerStore for FileLedger { self.inner.tip(did) } - fn all(&self) -> Vec { + fn all(&self) -> Vec { self.inner.all() } @@ -2322,27 +2322,27 @@ impl LedgerStore for FileLedger { /// Synced without exception, the same line [`FileAccountStore`] and /// [`FileLedger`] draw: this token is what stands between an authenticated /// write and an open one, so an issuance a power cut undid would come back as -/// a credential a `provisionAgent` response already handed out that this +/// a credential a `createAccount` response already handed out that this /// server no longer recognises. #[derive(Debug)] -pub struct FileAgentTokenStore { - inner: MemoryAgentTokenStore, +pub struct FileAccountTokenStore { + inner: MemoryAccountTokenStore, wal: Arc, } -impl AgentTokenStore for FileAgentTokenStore { +impl AccountTokenStore for FileAccountTokenStore { fn issue(&self, did: &str, ttl: time::Duration) -> IssuedToken { // The memory store computes the token and its hash; what is appended // is the hash alone, which is the same split [`FileBlobStore`] makes // between an upload's bytes and its reference. A log failure is - // logged rather than returned — `AgentTokenStore::issue` has no + // logged rather than returned — `AccountTokenStore::issue` has no // `Result` to put it in, the same way `CommitStore::advance` does // not, and the token this call hands back still works until the next // restart, at which point the log is what the account will be // verified against again. let issued = self.inner.issue(did, ttl); if let Err(error) = self.wal.append( - &Entry::AgentTokenIssued { + &Entry::AccountTokenIssued { did: did.to_owned(), token_hash: crate::credential::digest(&issued.token), expires_at: issued.expires_at, @@ -2368,7 +2368,7 @@ impl AgentTokenStore for FileAgentTokenStore { } self.inner.revoke(did); if let Err(error) = self.wal.append( - &Entry::AgentTokenRevoked { + &Entry::AccountTokenRevoked { did: did.to_owned(), }, Durability::Sync, @@ -2478,7 +2478,7 @@ mod tests { ledger: MemoryLedger, commits: MemoryCommitStore, names: NameRegistry, - credentials: MemoryAgentTokenStore, + credentials: MemoryAccountTokenStore, oauth: MemoryOAuthGrantStore, counter: DurableCounter, /// The store [`Entry::StreamReserved`] belongs to, replayed through @@ -2525,7 +2525,7 @@ mod tests { ledger: MemoryLedger::new(), commits: MemoryCommitStore::new(), names: NameRegistry::new(hold()), - credentials: MemoryAgentTokenStore::new(), + credentials: MemoryAccountTokenStore::new(), oauth: MemoryOAuthGrantStore::new(), counter: DurableCounter::new(), floor: SequenceFloor::new(), @@ -2782,9 +2782,9 @@ mod tests { let entry = match seq.pick(16) { 0 => { let parsed = - didbot_identity::AgentDid::parse(&did).expect("a did the alphabet holds"); + didbot_identity::AccountDid::parse(&did).expect("a did the alphabet holds"); Entry::AccountInserted { - account: Box::new(AgentAccount::server(HostedDid::replayed(parsed), at)), + account: Box::new(HostedAccount::server(HostedDid::replayed(parsed), at)), key: SigningKey::generate(), } } @@ -2972,9 +2972,9 @@ mod tests { } 14 => { if seq.pick(3) == 0 { - Entry::AgentTokenRevoked { did } + Entry::AccountTokenRevoked { did } } else { - Entry::AgentTokenIssued { + Entry::AccountTokenIssued { did, token_hash: format!("{:064x}", seq.next()), expires_at: at + time::Duration::days(30), @@ -3202,7 +3202,7 @@ mod tests { /// The OAuth grant store, with every family it mints, rotates and revokes in /// the log. /// -/// Applied and then appended, which [`FileAgentTokenStore`]'s own `issue` and +/// Applied and then appended, which [`FileAccountTokenStore`]'s own `issue` and /// the commit store's own `advance` already do and for the same reason: the token /// strings and the family identifier are what the store under this one works /// out, and there is nothing to write down until it has. diff --git a/crates/didbot-pds/src/estop.rs b/crates/didbot-pds/src/estop.rs index dab50ada..6df33d59 100644 --- a/crates/didbot-pds/src/estop.rs +++ b/crates/didbot-pds/src/estop.rs @@ -22,7 +22,7 @@ //! //! `plan/e-stop.md` asks for scope selected "using only selectors an agent //! cannot assert about itself", and a halt narrower than everything has to -//! leave some provisioning permitted. `bot.did.provisionAgent` authenticates +//! leave some provisioning permitted. `bot.did.createAccount` authenticates //! a host and nothing narrower: the daemon on a host signs for every context //! on it, and the profile on the request is the caller's word about itself. //! So a halted agent that can still provision takes a fresh account under diff --git a/crates/didbot-pds/src/hosted.rs b/crates/didbot-pds/src/hosted.rs index 0e5936f5..a031dbc0 100644 --- a/crates/didbot-pds/src/hosted.rs +++ b/crates/didbot-pds/src/hosted.rs @@ -1,6 +1,6 @@ //! A DID this deployment hosts, as distinct from one it can resolve. //! -//! [`AgentDid::parse`] admits any hostname-level `did:web`, which is the +//! [`AccountDid::parse`] admits any hostname-level `did:web`, which is the //! right rule for a resolver: the operator claim resolves `did:plc` through //! `plc.directory`, and a `did:web` on somebody else's domain is a document //! this server fetches like anyone else. It is the wrong rule for the @@ -9,7 +9,7 @@ //! resolvable — and a store keyed by a type that admits any hostname is a //! store an unchecked `insert` can put a foreign name into. //! -//! [`HostedDid`] is the type that carries the check. It is an [`AgentDid`] +//! [`HostedDid`] is the type that carries the check. It is an [`AccountDid`] //! plus the proof that the DID sits at or below a zone in the deployment's //! [`ZoneRegistry`], and [`HostedDid::host`] is the only public way to make //! one. [`AccountStore`](crate::AccountStore) is keyed by it, so an account @@ -19,11 +19,11 @@ use std::fmt; use std::ops::Deref; -use didbot_identity::{AgentDid, ZoneRegistry}; +use didbot_identity::{AccountDid, ZoneRegistry}; -/// An [`AgentDid`] that sits in a zone this deployment serves. +/// An [`AccountDid`] that sits in a zone this deployment serves. /// -/// Dereferences to the [`AgentDid`] it wraps, so everything that reads a DID +/// Dereferences to the [`AccountDid`] it wraps, so everything that reads a DID /// — its string, its host, its port — reads it the same way whether or not /// the proof is attached. What the proof adds is the right to be a key in /// the account store. @@ -32,7 +32,7 @@ use didbot_identity::{AgentDid, ZoneRegistry}; /// account exactly when their DIDs are, and the zone that hosts one is a fact /// about the deployment's configuration rather than about the identifier. #[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct HostedDid(AgentDid); +pub struct HostedDid(AccountDid); /// The DID is not in any zone this deployment serves. #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] @@ -51,7 +51,7 @@ impl HostedDid { /// zone — [`ZoneRegistry::containing`]'s rule, which compares DNS labels /// from the right so that `evilfoo.bar` is not under `foo.bar`. The /// zone's own apex qualifies: the server's own account lives there. - pub fn host(did: AgentDid, zones: &ZoneRegistry) -> Result { + pub fn host(did: AccountDid, zones: &ZoneRegistry) -> Result { if zones.containing(did.host()).is_some() { Ok(Self(did)) } else { @@ -74,36 +74,36 @@ impl HostedDid { /// written. Crate-private: the log's replay is the one reader that has a /// hosted DID in hand and no registry to re-check it against, and /// nothing outside this crate gets to skip the check. - pub(crate) fn replayed(did: AgentDid) -> Self { + pub(crate) fn replayed(did: AccountDid) -> Self { Self(did) } /// The DID without its proof. - pub fn did(&self) -> &AgentDid { + pub fn did(&self) -> &AccountDid { &self.0 } /// Consumes the proof, keeping the DID. - pub fn into_did(self) -> AgentDid { + pub fn into_did(self) -> AccountDid { self.0 } } impl Deref for HostedDid { - type Target = AgentDid; + type Target = AccountDid; - fn deref(&self) -> &AgentDid { + fn deref(&self) -> &AccountDid { &self.0 } } -impl PartialEq for HostedDid { - fn eq(&self, other: &AgentDid) -> bool { +impl PartialEq for HostedDid { + fn eq(&self, other: &AccountDid) -> bool { &self.0 == other } } -impl PartialEq for AgentDid { +impl PartialEq for AccountDid { fn eq(&self, other: &HostedDid) -> bool { self == &other.0 } @@ -115,7 +115,7 @@ impl fmt::Display for HostedDid { } } -impl From for AgentDid { +impl From for AccountDid { fn from(hosted: HostedDid) -> Self { hosted.0 } @@ -142,7 +142,7 @@ mod tests { "did:web:kestrel.dev.agents.localhost", "did:web:agents.localhost", ] { - let parsed = AgentDid::parse(did).expect("a valid did"); + let parsed = AccountDid::parse(did).expect("a valid did"); let hosted = HostedDid::host(parsed.clone(), &zones).expect("hosted"); assert_eq!(hosted, parsed); assert_eq!(hosted.as_str(), parsed.as_str()); @@ -161,7 +161,7 @@ mod tests { "did:web:agents.localhost.attacker.example", "did:web:localhost", ] { - let parsed = AgentDid::parse(did).expect("a valid did"); + let parsed = AccountDid::parse(did).expect("a valid did"); let refused = HostedDid::host(parsed, &zones).expect_err("foreign"); assert_eq!(refused.did, did); assert!(refused.zones.contains("agents.localhost"), "{refused}"); diff --git a/crates/didbot-pds/src/journal.rs b/crates/didbot-pds/src/journal.rs index 3090d41a..5f64420b 100644 --- a/crates/didbot-pds/src/journal.rs +++ b/crates/didbot-pds/src/journal.rs @@ -18,7 +18,7 @@ //! `checkpoint()` the state it has now, and where the checkpoint holds less //! than the history did it must say which observable answers change — the //! agent token store is the standing example and -//! [`MemoryAgentTokenStore`](crate::credential::MemoryAgentTokenStore)'s +//! [`MemoryAccountTokenStore`](crate::credential::MemoryAccountTokenStore)'s //! `checkpoint` argues it. The caller must apply facts in journal order and //! must never apply one twice. That second one is not a convenience: most of //! replay is idempotent and [`Entry::LedgerAppended`] is not, so the strict diff --git a/crates/didbot-pds/src/kind.rs b/crates/didbot-pds/src/kind.rs index 322395b0..b0fca335 100644 --- a/crates/didbot-pds/src/kind.rs +++ b/crates/didbot-pds/src/kind.rs @@ -224,7 +224,7 @@ pub enum Retention { /// Reads a retention, or the boolean pin that used to stand in for one. /// -/// `AgentAccount` aliases this field to `pinned`, so an account written to the +/// `HostedAccount` aliases this field to `pinned`, so an account written to the /// write-ahead log before retention existed deserializes through here with a /// `true` or a `false` where the enum now goes. `true` was "never collect /// this", which is [`Retention::Forever`]; `false` was "collect it on the @@ -269,7 +269,7 @@ impl Default for Retention { impl Retention { /// Whether this is a pin. /// - /// The whole of what `AgentAccount::pinned` used to be, and the reason it + /// The whole of what `HostedAccount::pinned` used to be, and the reason it /// is a method here rather than a second field there. pub fn is_pin(self) -> bool { matches!(self, Self::Forever) diff --git a/crates/didbot-pds/src/layout.rs b/crates/didbot-pds/src/layout.rs index a0da4f0e..84a7c728 100644 --- a/crates/didbot-pds/src/layout.rs +++ b/crates/didbot-pds/src/layout.rs @@ -147,7 +147,7 @@ use serde::{Deserialize, Serialize}; /// This is **not** what decides whether a directory is accepted — see the /// module docs. Bump it, or don't; a stale or colliding value only makes a /// refusal's message less helpful, never wrong. -pub const LAYOUT: u32 = 16; +pub const LAYOUT: u32 = 17; /// Every [`Entry`](crate::wal::Entry) variant, and the fields it writes on /// the wire, sorted by variant name and then by field name. @@ -166,12 +166,12 @@ pub const ENTRY_SHAPE: &[(&str, &[&str])] = &[ ("accountRemoved", &["did", "op"]), ("accountRetained", &["did", "op", "retention"]), ("accountStateChanged", &["did", "op", "state"]), - ("accountUnlocked", &["did", "lock", "op", "party"]), ( - "agentTokenIssued", + "accountTokenIssued", &["did", "expires_at", "op", "token_hash"], ), - ("agentTokenRevoked", &["did", "op"]), + ("accountTokenRevoked", &["did", "op"]), + ("accountUnlocked", &["did", "lock", "op", "party"]), ("blobCollected", &["cid", "did", "op"]), ("blobReferenced", &["cid", "did", "op", "refs"]), ( diff --git a/crates/didbot-pds/src/ledger.rs b/crates/didbot-pds/src/ledger.rs index d2ffd311..21c6f951 100644 --- a/crates/didbot-pds/src/ledger.rs +++ b/crates/didbot-pds/src/ledger.rs @@ -78,8 +78,8 @@ mod rfc3339 { /// it. #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] // `rename_all` renames the variants and `rename_all_fields` renames the -// fields inside them. Both are needed: without the second, `agentId` and -// `nodeId` would go out as `agent_id` and `node_id`, and every other JSON this +// fields inside them. Both are needed: without the second, `accountId` and +// `nodeId` would go out as `account_id` and `node_id`, and every other JSON this // server emits is camelCase. #[serde( tag = "event", @@ -97,7 +97,7 @@ pub enum LedgerEvent { /// provisionings with no deprovisioning between them. Provisioned { /// The label the DID was minted from. - agent_id: String, + account_id: String, /// The attestation backend that admitted the request. backend: String, /// How much that backend's check proves, as a kebab-case label. @@ -268,7 +268,7 @@ pub struct LedgerEntry { /// One agent's whole history. #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "camelCase")] -pub struct AgentLedger { +pub struct AccountLedger { /// The agent's DID. Permanent, and the ledger's key: the handle changes /// and the repository can be dropped, so neither can key a history that /// outlives them. @@ -277,7 +277,7 @@ pub struct AgentLedger { pub entries: Vec, } -impl AgentLedger { +impl AccountLedger { /// How many entries this ledger holds. /// /// This is the revision number the public identity record mirrors: a @@ -401,7 +401,7 @@ pub trait LedgerStore: Send + Sync { ) -> Result; /// One agent's ledger, or `None` if this deployment never minted that DID. - fn read(&self, did: &str) -> Option; + fn read(&self, did: &str) -> Option; /// Where one agent's ledger currently stands, without a copy of it. /// @@ -417,7 +417,7 @@ pub trait LedgerStore: Send + Sync { } /// Every ledger, in DID order. - fn all(&self) -> Vec; + fn all(&self) -> Vec; /// How many entries the whole ledger store holds. /// @@ -437,7 +437,7 @@ impl LedgerStore for std::sync::Arc { (**self).record(did, event, at) } - fn read(&self, did: &str) -> Option { + fn read(&self, did: &str) -> Option { (**self).read(did) } @@ -445,7 +445,7 @@ impl LedgerStore for std::sync::Arc { (**self).tip(did) } - fn all(&self) -> Vec { + fn all(&self) -> Vec { (**self).all() } @@ -538,8 +538,8 @@ impl LedgerStore for MemoryLedger { Ok(entry) } - fn read(&self, did: &str) -> Option { - self.ledgers().get(did).map(|entries| AgentLedger { + fn read(&self, did: &str) -> Option { + self.ledgers().get(did).map(|entries| AccountLedger { did: did.to_owned(), entries: entries.clone(), }) @@ -557,10 +557,10 @@ impl LedgerStore for MemoryLedger { }) } - fn all(&self) -> Vec { + fn all(&self) -> Vec { self.ledgers() .iter() - .map(|(did, entries)| AgentLedger { + .map(|(did, entries)| AccountLedger { did: did.clone(), entries: entries.clone(), }) @@ -621,7 +621,7 @@ mod tests { fn provisioned() -> LedgerEvent { LedgerEvent::Provisioned { - agent_id: "quartz-vole".to_owned(), + account_id: "quartz-vole".to_owned(), backend: "dev-shared-secret".to_owned(), assurance: "shared-secret".to_owned(), node_id: Some("node-7".to_owned()), @@ -818,7 +818,7 @@ mod tests { event: provisioned(), }; let json = serde_json::to_string(&entry).expect("an entry serializes"); - assert!(json.contains("\"agentId\""), "{json}"); + assert!(json.contains("\"accountId\""), "{json}"); assert!(json.contains("\"nodeId\""), "{json}"); assert!(!json.contains('_'), "no snake_case anywhere: {json}"); let back: LedgerEntry = serde_json::from_str(&json).expect("and reads back"); diff --git a/crates/didbot-pds/src/lib.rs b/crates/didbot-pds/src/lib.rs index d604e60d..3454d5a1 100644 --- a/crates/didbot-pds/src/lib.rs +++ b/crates/didbot-pds/src/lib.rs @@ -114,7 +114,7 @@ pub use lockout::{Actor, Hold, Holds, Lock, Locks, Party, Removes, Tag}; pub use zones::{ZoneError, ZoneManager, ZoneRouter}; pub use account::{ - AccountState, AccountStore, AgentAccount, MemoryAccountStore, StatePolicy, StoreError, + AccountState, AccountStore, HostedAccount, MemoryAccountStore, StatePolicy, StoreError, SyncAccountStatus, }; pub use admission::{ @@ -130,12 +130,12 @@ pub use blobs::{ }; pub use commit::{CommitEvent, CommitOp, CommitSink, Position, RecordChange, RecordingCommitSink}; pub use credential::{ - AgentTokenStore, IssuedToken, MemoryAgentTokenStore, TokenError, DEFAULT_AGENT_TOKEN_TTL, + AccountTokenStore, IssuedToken, MemoryAccountTokenStore, TokenError, DEFAULT_AGENT_TOKEN_TTL, }; pub use didbot_fsm::{LifecycleState, TransitionError}; pub use durable::{ - Durable, FileAccountStore, FileAgentTokenStore, FileBlobStore, FileLedger, FileOAuthGrantStore, - FileRecordStore, FileSequenceLog, Reconciled, BLOB_DIR, MISSING_SAMPLE, + Durable, FileAccountStore, FileAccountTokenStore, FileBlobStore, FileLedger, + FileOAuthGrantStore, FileRecordStore, FileSequenceLog, Reconciled, BLOB_DIR, MISSING_SAMPLE, }; pub use estop::{ Cause as EstopCause, Estop, Halted, Mode as EstopMode, Refusal as EstopRefusal, @@ -168,7 +168,7 @@ pub use subscribe::{ pub use format::StringFormat; pub use history::{CommitStore, Head, HistoryStats, MemoryCommitStore}; -pub use ledger::{AgentLedger, LedgerEntry, LedgerEvent, LedgerStore, LedgerTip, MemoryLedger}; +pub use ledger::{AccountLedger, LedgerEntry, LedgerEvent, LedgerStore, LedgerTip, MemoryLedger}; pub use lifecycle::{LifecycleEvent, LifecycleSink, RecordingSink}; pub use names::{ DurableCounter, NameError as NameConflict, NameRegistry, Naming, Reservation, DEFAULT_ATTEMPTS, diff --git a/crates/didbot-pds/src/lifecycle.rs b/crates/didbot-pds/src/lifecycle.rs index b6c7ce4d..6ff55222 100644 --- a/crates/didbot-pds/src/lifecycle.rs +++ b/crates/didbot-pds/src/lifecycle.rs @@ -19,7 +19,7 @@ pub enum LifecycleEvent { /// The account's DID. did: String, /// The label the DID was minted from. - agent_id: String, + account_id: String, /// The attestation backend that admitted the request. backend: String, /// How much that backend's check proves, as a kebab-case label. @@ -40,8 +40,8 @@ pub enum LifecycleEvent { /// compiling: this is the one variant every future state /// (`revoked`, `deleted-but-data-kept`, `migrated`, …) needs, and none of /// them needs a variant of their own here — the state itself carries the - /// meaning, and a sink that cares reads [`AgentAccount::policy`](crate::AgentAccount::policy) or - /// [`AgentAccount::sync_status`](crate::AgentAccount::sync_status) rather than matching this enum. + /// meaning, and a sink that cares reads [`HostedAccount::policy`](crate::HostedAccount::policy) or + /// [`HostedAccount::sync_status`](crate::HostedAccount::sync_status) rather than matching this enum. StateChanged { /// The DID that moved. did: String, diff --git a/crates/didbot-pds/src/names.rs b/crates/didbot-pds/src/names.rs index 901eb8db..3f681029 100644 --- a/crates/didbot-pds/src/names.rs +++ b/crates/didbot-pds/src/names.rs @@ -111,7 +111,7 @@ pub enum Reservation { /// it does not start a hold that later expires: only an administrator's /// hard delete, via [`NameRegistry::release`] overwriting this /// reservation, ever frees the name again. - FormerAgent { + FormerAccount { /// The DID that held the name, for an operator debugging a refusal. did: String, }, @@ -122,7 +122,7 @@ impl fmt::Display for Reservation { match self { Self::ZoneApex { zone } => write!(f, "it is the apex of the zone {zone:?}"), Self::Operational { reason } => write!(f, "operational hold ({reason})"), - Self::FormerAgent { did } => { + Self::FormerAccount { did } => { write!(f, "it was permanently retired by the deleted agent {did:?}") } } @@ -749,12 +749,12 @@ impl Naming { /// /// Takes the handle rather than the label for the same reason /// [`release_handle`](Self::release_handle) does. Unlike that method, - /// this never expires: see [`Reservation::FormerAgent`]. + /// this never expires: see [`Reservation::FormerAccount`]. pub fn retire_handle(&self, handle: &str, zone: &str, did: &str) { if let Some(label) = label_of(handle, zone) { self.registry.retire( &label, - Reservation::FormerAgent { + Reservation::FormerAccount { did: did.to_owned(), }, ); @@ -1103,7 +1103,7 @@ mod tests { }, NameError::Reserved { name: "junco".to_owned(), - reservation: Reservation::FormerAgent { + reservation: Reservation::FormerAccount { did: "did:web:junco.example".to_owned(), }, }, diff --git a/crates/didbot-pds/src/provision.rs b/crates/didbot-pds/src/provision.rs index 4dd4c197..e63fc0b8 100644 --- a/crates/didbot-pds/src/provision.rs +++ b/crates/didbot-pds/src/provision.rs @@ -8,13 +8,13 @@ use didbot_attest::{ Assurance, AttestError, AttestationBackend, AttestationClaim, NodeCredentialBackend, Provenance, }; use didbot_identity::{ - hostname_is_at_or_below, validate_handle, AgentDid, DidDocument, DidError, HandleError, Zone, + hostname_is_at_or_below, validate_handle, AccountDid, DidDocument, DidError, HandleError, Zone, ZoneRegistry, }; use didbot_key::{SigningKey, VerifyingKey}; use time::OffsetDateTime; -use crate::account::{AccountState, AccountStore, AgentAccount, StoreError}; +use crate::account::{AccountState, AccountStore, HostedAccount, StoreError}; use crate::admission::{ AdmissionClaim, AdmissionError, EdgeKind, Lineage, NodeMembership, Trust, VerifiedEdge, }; @@ -24,12 +24,12 @@ use crate::blobs::{ use crate::bsky; use crate::commit::{CommitEvent, CommitOp, CommitSink}; use crate::credential::{ - AgentTokenStore, MemoryAgentTokenStore, TokenError, DEFAULT_AGENT_TOKEN_TTL, + AccountTokenStore, MemoryAccountTokenStore, TokenError, DEFAULT_AGENT_TOKEN_TTL, }; use crate::history::{CommitStore, MemoryCommitStore}; use crate::hosted::HostedDid; use crate::kind::{AccountKind, NameProvenance, Retention}; -use crate::ledger::{AgentLedger, LedgerEvent, LedgerStore, MemoryLedger}; +use crate::ledger::{AccountLedger, LedgerEvent, LedgerStore, MemoryLedger}; use crate::lifecycle::{LifecycleEvent, LifecycleSink}; use crate::lockout::{Actor, Hold, Lock, Locks, Party, Tag}; use crate::names::{NameError, Naming}; @@ -57,7 +57,7 @@ use didbot_fsm::LifecycleState; #[derive(Debug, Clone)] pub struct ProvisionRequest { /// The DNS label the DID will be minted from. One label, not a hostname. - pub agent_id: String, + pub account_id: String, /// An atproto handle to claim in the DID document, if the caller has one. pub handle: Option, /// A host's signed word that this context runs on it. Verified against @@ -65,7 +65,7 @@ pub struct ProvisionRequest { /// active, unlocked account its operator has vouched for — becomes the /// account's parent; see `Provisioner::node_backend`. Absent, the /// account is admitted under no check and as a root, as - /// `UNAUTHENTICATED_BACKEND` records: `bot.did.provisionAgent` refuses + /// `UNAUTHENTICATED_BACKEND` records: `bot.did.createAccount` refuses /// such a request unless the deployment says otherwise, and only the /// `.localhost` development stack does. pub attestation: Option, @@ -80,9 +80,9 @@ pub struct ProvisionRequest { impl ProvisionRequest { /// Assembles a request. - pub fn new(agent_id: impl Into, handle: Option) -> Self { + pub fn new(account_id: impl Into, handle: Option) -> Self { Self { - agent_id: agent_id.into(), + account_id: account_id.into(), handle, attestation: None, profile: RegistrationFacts::default(), @@ -140,7 +140,7 @@ impl RegistrationFacts { #[derive(Debug, Clone, PartialEq, Eq)] pub struct Provisioned { /// The stored account record. - pub account: AgentAccount, + pub account: HostedAccount, /// The DID document now served at the account's hostname. pub document: DidDocument, /// The bearer credential this account writes with, in plaintext. @@ -149,7 +149,7 @@ pub struct Provisioned { /// `crate::credential`. A caller that loses it has no way to recover it; /// the account has to be provisioned again, which mints a fresh one and /// revokes whatever this one authenticated. - pub agent_token: String, + pub account_token: String, } /// What `admittedBy` says for an account that entered through a reservation. @@ -217,7 +217,7 @@ pub struct ReservationRequest { /// What kind of account is being reserved; see [`AccountKind::reservable`]. pub kind: AccountKind, /// The operator DID the host expects a vouch from, if it named one; see - /// [`AgentAccount::expected_operator`]. + /// [`HostedAccount::expected_operator`]. pub expected_operator: Option, } @@ -260,7 +260,7 @@ impl ReservationRequest { #[derive(Debug, Clone, PartialEq, Eq)] pub struct ReservedIdentity { /// The stored account, `Provisioning` and waiting on a vouch. - pub account: AgentAccount, + pub account: HostedAccount, /// The DID document now served at the account's hostname. pub document: DidDocument, /// When the sweep takes the name back if nobody has vouched by then. @@ -291,8 +291,8 @@ pub enum ProvisionError { Identity(#[from] DidError), /// A minted did does not sit at or below the zone it was minted from. /// - /// Unreachable against a correct [`AgentDid::mint`], which always builds - /// `.`. Kept as the last chance to catch a minting + /// Unreachable against a correct [`AccountDid::mint`], which always builds + /// `.`. Kept as the last chance to catch a minting /// change that lets an identifier escape its own zone before anything is /// published for it. #[error("minted did {did:?} does not sit at or below its own zone {zone:?}")] @@ -378,7 +378,7 @@ pub enum ProvisionError { /// The caller asked for a handle that is not a handle. /// /// Its own variant rather than a [`DidError`], because the handle is not - /// the identifier: a request can carry a perfectly good `agent_id` and a + /// the identifier: a request can carry a perfectly good `account_id` and a /// malformed handle, and saying "identity error" about the second would /// send the caller looking at the first. #[error("requested handle is not usable: {0}")] @@ -1116,7 +1116,7 @@ impl PolicyGate for FreezeSyncingGate<'_, S> { // this method never depends on that being true. let evaluation_id = self.inner.last_evaluation_id(); self.inner.froze(agent, reason); - match AgentDid::parse(agent) + match AccountDid::parse(agent) .map_err(|error| error.to_string()) .and_then(|did| HostedDid::host(did, self.hosted).map_err(|error| error.to_string())) { @@ -1185,7 +1185,7 @@ type AppliedBatch = (Vec, Committed, Vec>); /// judges again with both. A refused upload is dropped, which discards it. struct JudgedUpload<'a, S> { provisioner: &'a Provisioner, - account: AgentAccount, + account: HostedAccount, /// The application whose OAuth token presented the upload. client_id: Option, mime_type: String, @@ -1261,7 +1261,7 @@ where enum WriteAuthor<'a> { /// An external agent, authenticating with its bearer credential: checked - /// against [`AgentAccount::policy`] and announced immediately. Every + /// against [`HostedAccount::policy`] and announced immediately. Every /// caller but one. `client_id` is the application whose OAuth token /// presented the write, or `None` for the account's own credential. External { client_id: Option<&'a ClientId> }, @@ -1279,7 +1279,7 @@ enum WriteAuthor<'a> { /// `status` is still not here: every [`RepoHead`] this server hands out /// describes a repository that is being served right now, because /// `Provisioner::list_repos` and `Provisioner::head` both refuse an -/// account whose [`AgentAccount::policy`] does not serve one — a frozen +/// account whose [`HostedAccount::policy`] does not serve one — a frozen /// account's repository still qualifies, a soft-deleted or suspended /// account's does not and is filtered out of the listing entirely rather /// than reported inactive. A @@ -1355,7 +1355,7 @@ pub trait Registry: Send + Sync { /// Every reservation still waiting on a vouch, ordered by DID. An /// expired one is left out even before the sweep has taken it. - fn reservations(&self) -> Vec; + fn reservations(&self) -> Vec; /// Finishes provisioning a reservation `operator` has vouched for with /// `vouch`, moving it to [`AccountState::Active`] under the server's own @@ -1368,7 +1368,7 @@ pub trait Registry: Send + Sync { did: &str, operator: &str, vouch: &OperatorClaim, - ) -> Result; + ) -> Result; /// Makes the server's mirror of the operator's vouch for the host at `did` /// say what `vouch` says, and answers whether the record changed. @@ -1441,7 +1441,7 @@ pub trait Registry: Send + Sync { /// /// This is `crate::credential`'s bearer credential, minted once at /// provisioning — not a legacy session and not an OAuth token; see that - /// module for why. `didbot-serve`'s `AgentToken` extractor is the only + /// module for why. `didbot-serve`'s `AccountToken` extractor is the only /// caller. fn verify_agent_token(&self, token: &str) -> Result; @@ -1483,7 +1483,7 @@ pub trait Registry: Send + Sync { fn handle_did(&self, host: &str) -> Option; /// Every account, ordered by DID. - fn accounts(&self) -> Vec; + fn accounts(&self) -> Vec; /// The zone hostname accounts are minted under. fn zone(&self) -> &str; @@ -1657,7 +1657,7 @@ pub trait Registry: Send + Sync { /// /// Answers `None` for a DID that does not parse as well as for one nobody /// holds, because neither is an account this deployment has. - fn account(&self, did: &str) -> Option; + fn account(&self, did: &str) -> Option; /// Every account admitted under `did`, whatever its state. /// @@ -1666,7 +1666,7 @@ pub trait Registry: Send + Sync { /// [`AccountState::is_live`]. Refused with /// [`ProvisionError::UnknownAccount`] for a DID this deployment does not /// hold, so an empty answer always means an account with no children. - fn children(&self, did: &str) -> Result, ProvisionError>; + fn children(&self, did: &str) -> Result, ProvisionError>; /// Verifies a claim's chain up to a root and records the subject's /// parent. See [`crate::admission`]. @@ -1779,13 +1779,13 @@ pub trait Registry: Send + Sync { /// by DID must work for an agent that has been deprovisioned — that is /// the case the ledger exists for. `None` means this deployment never /// minted that DID at all. - fn ledger(&self, did: &str) -> Option; + fn ledger(&self, did: &str) -> Option; /// Every agent this deployment has ever provisioned, in DID order. /// /// Longer than [`Registry::accounts`] by exactly the agents that have /// been deprovisioned. - fn ledgers(&self) -> Vec; + fn ledgers(&self) -> Vec; /// Opens a blob upload into an account's repository. /// @@ -1948,7 +1948,7 @@ pub struct Provisioner { /// reason `records` is: a choice every deployment currently makes the /// same way, and an issuance happens once per provisioning rather than on /// a hot path. - credentials: Arc, + credentials: Arc, /// Called with a DID whenever this registry ends an account's /// credentials, so the stores it does not hold end theirs too. /// @@ -2155,7 +2155,7 @@ where blobs: Arc::new(MemoryBlobStore::new()), ledger: Arc::new(MemoryLedger::new()), history: Arc::new(MemoryCommitStore::new()), - credentials: Arc::new(MemoryAgentTokenStore::new()), + credentials: Arc::new(MemoryAccountTokenStore::new()), revisions: Minter::new(), writing: RwLock::new(()), repositories: Mutex::new(std::collections::HashMap::new()), @@ -2213,7 +2213,7 @@ where /// have minted, so the answer is the one every lookup gives for an /// identifier it does not hold — see /// [`ProvisionError::UnknownAccount`] on why that and not a parse error. - fn hosted(&self, did: AgentDid) -> Result { + fn hosted(&self, did: AccountDid) -> Result { HostedDid::host(did, &self.hosted).map_err(|foreign| { tracing::debug!(%foreign, "a did outside every served zone is no account"); ProvisionError::UnknownAccount { did: foreign.did } @@ -2314,15 +2314,15 @@ where /// commit history forgets its heads: a deployment that takes it gets a /// server that starts every account's write credential over from /// scratch. A durable one is what makes a credential handed out in a - /// `provisionAgent` response still work after a restart. + /// `createAccount` response still work after a restart. #[must_use] - pub fn with_agent_tokens(mut self, credentials: Arc) -> Self { + pub fn with_agent_tokens(mut self, credentials: Arc) -> Self { self.credentials = credentials; self } /// The ledger, for callers that need to read the bookkeeping directly. - pub fn agent_ledger(&self) -> &Arc { + pub fn account_ledger(&self) -> &Arc { &self.ledger } @@ -2462,7 +2462,7 @@ where // // A reservation whose window ran out is the `Provisioning` case: // `remove_row` gives its name back on the ordinary hold, never - // burns it. `Reservation::FormerAgent` is for a name whose + // burns it. `Reservation::FormerAccount` is for a name whose // document once stood for an agent's signatures, and nothing was // ever signed as a reservation. // One turn across both halves, the way a hard delete takes one: @@ -2551,14 +2551,14 @@ where /// reason: the repository is seeded before the account exists, because /// every read resolves the account first and a repository that is briefly /// empty is one a reader can catch mid-birth. - pub fn ensure_server_account(&self) -> Result { - let did = self.hosted(AgentDid::parse(&self.zone.service_did())?)?; + pub fn ensure_server_account(&self) -> Result { + let did = self.hosted(AccountDid::parse(&self.zone.service_did())?)?; if let Some(existing) = self.store.get(&did) { tracing::debug!(did = did.as_str(), "the server account is already present"); return Ok(existing); } - let account = AgentAccount::server(did.clone(), OffsetDateTime::now_utc()); + let account = HostedAccount::server(did.clone(), OffsetDateTime::now_utc()); // Written through the record store rather than `put_record`, for the // reason `publish_identity` is: the repository is seeded before the // account exists, and `put_record` resolves an account first. The key @@ -2600,7 +2600,7 @@ where self.note_event( did.as_str(), LedgerEvent::Provisioned { - agent_id: "server".to_owned(), + account_id: "server".to_owned(), backend: "self-provisioned".to_owned(), assurance: assurance_label(Assurance::SelfAsserted).to_owned(), node_id: Some(did.host().to_owned()), @@ -2633,7 +2633,7 @@ where /// Writes this server's own registration record, composed fresh from its /// ledger and naming the configured operator. - fn publish_service_identity(&self, did: &AgentDid) -> Result<(), ProvisionError> { + fn publish_service_identity(&self, did: &AccountDid) -> Result<(), ProvisionError> { let Some(ledger) = self.ledger.read(did.as_str()) else { return Err(ProvisionError::UnknownAccount { did: did.as_str().to_owned(), @@ -2666,12 +2666,16 @@ where /// the account, because the answer has to be the same for a DID nothing /// has stored yet — a delete naming the apex is refused whether or not /// [`Provisioner::ensure_server_account`] has run. - fn is_server(&self, did: &AgentDid) -> bool { + fn is_server(&self, did: &AccountDid) -> bool { did.as_str() == self.zone.service_did() } /// Refuses an operation that would treat the server as one of its agents. - fn refuse_server(&self, did: &AgentDid, operation: &'static str) -> Result<(), ProvisionError> { + fn refuse_server( + &self, + did: &AccountDid, + operation: &'static str, + ) -> Result<(), ProvisionError> { if !self.is_server(did) { return Ok(()); } @@ -2819,8 +2823,8 @@ where /// otherwise. A record that is behind says so, in `revision`. fn publish_identity( &self, - did: &AgentDid, - account: &AgentAccount, + did: &AccountDid, + account: &HostedAccount, ) -> Result<(), ProvisionError> { let Some(ledger) = self.ledger.read(did.as_str()) else { return Err(ProvisionError::UnknownAccount { @@ -2865,7 +2869,7 @@ where /// here. fn commit_identity( &self, - did: &AgentDid, + did: &AccountDid, publish: impl FnOnce() -> Result<(), ProvisionError>, ) -> Result, ProvisionError> { self.writing_to(did.as_str(), || { @@ -2915,7 +2919,7 @@ where /// that call it. fn announce_identity( &self, - did: &AgentDid, + did: &AccountDid, previous: Option, publish: impl FnOnce() -> Result<(), ProvisionError>, ) -> Result<(), ProvisionError> { @@ -2957,7 +2961,7 @@ where /// Read before a path rewrites it, so the announcement that follows can /// name what the update replaced. `None` for an account that has none /// yet, which is the create case. - fn registration_cid(&self, did: &AgentDid) -> Option { + fn registration_cid(&self, did: &AccountDid) -> Option { previous_cid( self.records .get(did.as_str(), registration::COLLECTION, registration::RKEY) @@ -3077,7 +3081,7 @@ where /// hostname, because naming that separately would be naming one name /// twice. The lexicon asks for "the current handle for the account", /// which an account storing no handle of its own still has. - fn announce_repo_identity(&self, account: &AgentAccount) { + fn announce_repo_identity(&self, account: &HostedAccount) { let Some(repos) = &self.repos else { return; }; @@ -3091,12 +3095,12 @@ where } /// Says on the firehose whether an account's repository is fetchable - /// here, as [`AgentAccount::sync_status`] answers it. + /// here, as [`HostedAccount::sync_status`] answers it. /// /// Nothing for an account whose state announces nothing: see /// [`AccountState::sync_status`] on why the states before activation /// map to no frame at all. - fn announce_repo_account(&self, account: &AgentAccount) { + fn announce_repo_account(&self, account: &HostedAccount) { let Some(repos) = &self.repos else { return; }; @@ -3591,7 +3595,7 @@ where /// A DID that resolves to an account with no key is an account that could /// never have written anything, so it is the same answer as a DID nobody /// minted. - fn signing_key(&self, account: &AgentAccount) -> Result { + fn signing_key(&self, account: &HostedAccount) -> Result { self.store .signing_key(&account.did) .ok_or_else(|| ProvisionError::UnknownAccount { @@ -3636,7 +3640,7 @@ where /// at it. [`Self::verify_repository`] is the check that it still holds. fn commit_write( &self, - account: &AgentAccount, + account: &HostedAccount, touched: BTreeSet, ) -> Result { let did = account.did.as_str(); @@ -3785,14 +3789,14 @@ where /// current PDS endpoint, and storing it would let the two disagree the /// first time that endpoint changes. /// - /// Whether the document serves at all is [`AgentAccount::policy`]'s + /// Whether the document serves at all is [`HostedAccount::policy`]'s /// `serves_did_document`, which no lock touches: every state a live /// account can be in serves one, so this returns `None` only for a DID /// nothing here ever minted or a hard-deleted one, where there is no key /// to sign it with either. `serves_verification_method` is checked here /// regardless, so a state that keeps the document and disavows the key /// needs no change anywhere else that calls this function. - fn document_for(&self, account: &AgentAccount) -> Option { + fn document_for(&self, account: &HostedAccount) -> Option { if !account.policy().serves_did_document { return None; } @@ -3823,7 +3827,7 @@ where /// /// An account whose handle is its own DID hostname has nothing extra to /// resolve, and is reported as having no issued handle. - fn issued_handle<'a>(&self, account: &'a AgentAccount) -> Option<&'a str> { + fn issued_handle<'a>(&self, account: &'a HostedAccount) -> Option<&'a str> { let handle = account.handle.as_deref()?; (self.naming.is_some() && !handle.eq_ignore_ascii_case(account.did.host())) .then_some(handle) @@ -3903,7 +3907,7 @@ where /// Every other case is a map lookup. fn check_commit_for( &self, - account: &AgentAccount, + account: &HostedAccount, named: Option<&Cid>, ) -> Result<(), ProvisionError> { let Some(named) = named else { @@ -3949,7 +3953,11 @@ where /// with the DID that was named. It is the same choice `build_document` /// makes for an unclaimable handle: assert nothing rather than assert /// something this deployment cannot back. - fn believable_parent<'a>(&self, child: &AgentDid, claimed: Option<&'a str>) -> Option<&'a str> { + fn believable_parent<'a>( + &self, + child: &AccountDid, + claimed: Option<&'a str>, + ) -> Option<&'a str> { let claimed = claimed.map(str::trim).filter(|value| !value.is_empty())?; if claimed == child.as_str() { tracing::warn!( @@ -3958,7 +3966,7 @@ where ); return None; } - let held = AgentDid::parse(claimed) + let held = AccountDid::parse(claimed) .ok() .and_then(|parent| self.hosted(parent).ok()) .is_some_and(|parent| self.store.get(&parent).is_some()); @@ -3985,7 +3993,7 @@ where /// returned [`DeferredWrite`] itself, once the account is `Active`. fn write_bot_profile( &self, - account: &AgentAccount, + account: &HostedAccount, defer: bool, ) -> Result<(String, Option), ProvisionError> { let did = account.did.as_str(); @@ -4046,8 +4054,8 @@ where } /// Resolves a DID string to a stored account, or reports it unknown. - fn lookup(&self, did: &str) -> Result { - let parsed = AgentDid::parse(did).map_err(|_| ProvisionError::UnknownAccount { + fn lookup(&self, did: &str) -> Result { + let parsed = AccountDid::parse(did).map_err(|_| ProvisionError::UnknownAccount { did: did.to_owned(), })?; let hosted = self.hosted(parsed)?; @@ -4146,7 +4154,7 @@ where } /// Adds `host`'s key to the attestation verifier, once the host stands. - fn register_node(&self, host: &AgentAccount) { + fn register_node(&self, host: &HostedAccount) { let Some((node_id, key)) = node_credential(host) else { return; }; @@ -4190,7 +4198,7 @@ where /// what moves it to `Active`; a lock hung on it afterwards — its /// operator's freeze, or the quarantine a revoked vouch brings — stops /// it minting anything new beneath itself without taking its key away. - fn standing_host(&self, node_id: &str) -> Result { + fn standing_host(&self, node_id: &str) -> Result { let host = self.lookup(node_id)?; if host.state != AccountState::Active { return Err(ProvisionError::HostNotAdmitted { @@ -4214,7 +4222,7 @@ where /// Every other parent vouches, with a record in its repository. /// /// [`Membership`]: crate::admission::Membership - fn admits_by_attestation(&self, parent: &AgentAccount) -> bool { + fn admits_by_attestation(&self, parent: &HostedAccount) -> bool { self.trust.platform(parent.did.as_str()).is_some() || parent.node_key.is_some() } @@ -4222,7 +4230,7 @@ where /// is inside; see [`Self::admits_by_attestation`] for what admits. fn attestation_refusal( &self, - parent: &AgentAccount, + parent: &HostedAccount, provenance: &Provenance, ) -> Option { if let Some(pool) = self.trust.platform(parent.did.as_str()) { @@ -4245,7 +4253,7 @@ where /// /// The same comparison the operator poll makes for the server itself: /// the record exists at the child's hostname and names the child's DID. - fn vouch_refusal(&self, parent: &AgentAccount, child: &AgentAccount) -> Option { + fn vouch_refusal(&self, parent: &HostedAccount, child: &HostedAccount) -> Option { let collection = didbot_lexicon::nsid::OPERATOR; let Some(record) = self .records @@ -4276,7 +4284,7 @@ where /// tick, and a commit for it would be a head moved over nothing. fn write_vouch_mirror( &self, - host: &AgentAccount, + host: &HostedAccount, vouch: Option<&OperatorClaim>, ) -> Result { let server = self.zone.service_did(); @@ -4664,7 +4672,7 @@ where ); Ok((written, None)) } - /// Refuses a write on an account whose [`AgentAccount::policy`] does + /// Refuses a write on an account whose [`HostedAccount::policy`] does /// not accept one. /// /// Called right after every [`Self::lookup`] on a write path — a fact @@ -4696,7 +4704,7 @@ where )) } - fn require_writable(&self, account: &AgentAccount) -> Result<(), ProvisionError> { + fn require_writable(&self, account: &HostedAccount) -> Result<(), ProvisionError> { if account.policy().accepts_external_writes { return Ok(()); } @@ -4716,7 +4724,7 @@ where /// /// Every eraser calls this: the hold is the one thing an operator's /// erasure respects, and the store refuses the removal again behind it. - fn require_unheld(&self, account: &AgentAccount) -> Result<(), ProvisionError> { + fn require_unheld(&self, account: &HostedAccount) -> Result<(), ProvisionError> { if !account.holds.has(Hold::PreventDataDeletion) { return Ok(()); } @@ -4734,10 +4742,10 @@ where /// itself. See [`ProvisionError::LiveChildren`]. fn require_no_live_children( &self, - account: &AgentAccount, + account: &HostedAccount, operation: &'static str, ) -> Result<(), ProvisionError> { - let live: Vec = self + let live: Vec = self .store .children(&account.did) .into_iter() @@ -4764,7 +4772,7 @@ where /// /// The one place the self-service rule lives: an operator's erasure and /// the deployment's own retention sweep never call this. - fn require_unlocked(&self, account: &AgentAccount) -> Result<(), ProvisionError> { + fn require_unlocked(&self, account: &HostedAccount) -> Result<(), ProvisionError> { if account.locks.is_empty() { return Ok(()); } @@ -4822,7 +4830,7 @@ where /// upload is one attempt to a stateful evaluator counting them. fn judge_blob( &self, - account: &AgentAccount, + account: &HostedAccount, client_id: Option<&ClientId>, mime_type: &str, sniffed: Option<&str>, @@ -4866,7 +4874,7 @@ where } } - fn freeze_syncing_gate(&self, account: &AgentAccount) -> FreezeSyncingGate<'_, S> { + fn freeze_syncing_gate(&self, account: &HostedAccount) -> FreezeSyncingGate<'_, S> { FreezeSyncingGate { inner: self.policy_gate.as_ref(), store: &self.store, @@ -4891,8 +4899,8 @@ where /// it is gone — `records.rs` holds nothing under this DID any more — so /// this is the same answer a caller gets for a repository that was never /// here. The `com.atproto.sync.*` routes read the account's - /// [`AgentAccount::sync_status`] to name a suspended or deactivated one. - fn require_repository(&self, account: &AgentAccount) -> Result<(), ProvisionError> { + /// [`HostedAccount::sync_status`] to name a suspended or deactivated one. + fn require_repository(&self, account: &HostedAccount) -> Result<(), ProvisionError> { if account.policy().serves_repository { return Ok(()); } @@ -4936,7 +4944,7 @@ where /// takes up where it stopped. `by` says who is erasing, which decides /// what is respected: the account itself respects every lock, and every /// eraser respects the hold. - fn erase(&self, did: &str, by: Eraser<'_>) -> Result { + fn erase(&self, did: &str, by: Eraser<'_>) -> Result { self.lifecycle_of(did, || self.erase_locked(did, by)) } @@ -4947,7 +4955,7 @@ where /// destruction starts — so the whole of this has to be one atom against /// one DID. A hard delete holds the same turn across this and the row's /// removal, which is why the two are split. - fn erase_locked(&self, did: &str, by: Eraser<'_>) -> Result { + fn erase_locked(&self, did: &str, by: Eraser<'_>) -> Result { let account = self.lookup(did)?; self.refuse_server(&account.did, "erasure")?; self.require_unheld(&account)?; @@ -4997,7 +5005,7 @@ where // account still exists and still answers to its document, so its // name must never be handed to a second agent while that stays // true. Only taking the row out frees it — see - // `crate::names::Reservation::FormerAgent`. + // `crate::names::Reservation::FormerAccount`. if let Some(handle) = self.issued_handle(&account) { self.retire_name(Some(handle), account.did.as_str()); } @@ -5038,7 +5046,7 @@ where // Gone from the index in the same step it leaves the store, so no // lookup can see a hostname the store no longer answers for. self.hosts.forget(&removed); - tracing::info!(agent_id = %removed.agent_id, "removed account"); + tracing::info!(account_id = %removed.account_id, "removed account"); // After the account, not before: a removal that failed on the pin or // on the store would otherwise have already discarded the records of @@ -5149,7 +5157,7 @@ where /// refresh, the relay announcement and the policy gate. fn hang_tag( &self, - account: &AgentAccount, + account: &HostedAccount, tag: Tag, operator: Option, policy_evaluation: Option, @@ -5234,7 +5242,7 @@ where /// entails; the counterpart of [`Self::hang_tag`]. fn lift_tag( &self, - account: &AgentAccount, + account: &HostedAccount, tag: Tag, operator: Option, ) -> Result<(), ProvisionError> { @@ -5326,7 +5334,7 @@ where /// Whether `account`'s parent confines it under `lock` right now: the /// parent holds any tag of it, or the lock is a quarantine and the /// parent's vouch for this account has lapsed. - fn parent_confines(&self, account: &AgentAccount, lock: Lock) -> bool { + fn parent_confines(&self, account: &HostedAccount, lock: Lock) -> bool { let Some(parent_did) = &account.parent else { return false; }; @@ -5368,7 +5376,7 @@ where /// is the caller's, because the two callers hold different things. fn land_initial_records( &self, - account: &AgentAccount, + account: &HostedAccount, ) -> Result, ProvisionError> { let did = &account.did; // The account's first real commit — no previous head, a tree that @@ -5424,9 +5432,9 @@ where /// sweep can still reap, never a repository with no row. fn activate( &self, - mut account: AgentAccount, + mut account: HostedAccount, deferred: Vec, - ) -> Result { + ) -> Result { let did = account.did.clone(); if let Err(error) = self.store.set_state(&did, AccountState::Active) { tracing::error!(%error, "could not activate the account"); @@ -5457,7 +5465,7 @@ where } /// Every reservation still inside its window at `now`, by DID. - fn pending_reservations(&self, now: OffsetDateTime) -> Vec { + fn pending_reservations(&self, now: OffsetDateTime) -> Vec { self.accounts() .into_iter() .filter(|account| { @@ -5475,7 +5483,7 @@ where /// shape for every transition. What differs — tearing down the /// repository, retiring the name — is what the caller does with the /// account this hands back. - fn transition(&self, did: &str, to: AccountState) -> Result { + fn transition(&self, did: &str, to: AccountState) -> Result { let account = self.lookup(did)?; self.refuse_server(&account.did, "changing state")?; if account.state == to || !account.state.can_transition_to(to) { @@ -5521,7 +5529,7 @@ where /// handle before an account exists, and a `did:web` hostname is a handle by /// construction. Reaching it means the document claims nothing and the /// well-known answers nothing, which is a pair that still agrees. -fn claimed_handle<'a>(did: &'a AgentDid, handle: Option<&'a str>) -> Option<&'a str> { +fn claimed_handle<'a>(did: &'a AccountDid, handle: Option<&'a str>) -> Option<&'a str> { let handle = handle.unwrap_or_else(|| did.host()); validate_handle(handle).is_ok().then_some(handle) } @@ -5567,7 +5575,7 @@ impl HostIndex { /// before a [`Provisioner`] ever sees it, so accounts can already be /// there — this is what makes the index agree with a store that was not /// built up one `provision` call at a time. - fn from_accounts(accounts: &[AgentAccount]) -> Self { + fn from_accounts(accounts: &[HostedAccount]) -> Self { let index = Self::default(); for account in accounts { index.insert(account); @@ -5599,7 +5607,7 @@ impl HostIndex { /// `BTreeMap` keyed by DID string, so it returned the account whose DID /// sorts first. `from_accounts` walks `store.list()` in that same order, /// so keeping the first registration here answers exactly the same way. - fn insert(&self, account: &AgentAccount) { + fn insert(&self, account: &HostedAccount) { self.lock_host() .entry(account.did.host().to_ascii_lowercase()) .or_insert_with(|| account.did.clone()); @@ -5616,7 +5624,7 @@ impl HostIndex { /// under it, which matters only in the collision case `insert` guards /// against: if two accounts ever claimed one name, deleting the loser /// must not evict the winner's mapping. - fn forget(&self, account: &AgentAccount) { + fn forget(&self, account: &HostedAccount) { let mut by_host = self.lock_host(); if by_host.get(&account.did.host().to_ascii_lowercase()) == Some(&account.did) { by_host.remove(&account.did.host().to_ascii_lowercase()); @@ -5644,7 +5652,7 @@ impl HostIndex { /// Builds the DID document for an account. fn build_document( - did: &AgentDid, + did: &AccountDid, key: &VerifyingKey, handle: Option<&str>, pds_endpoint: &str, @@ -5690,7 +5698,7 @@ pub const UNAUTHENTICATED_BACKEND: &str = "unauthenticated"; /// credential is secp256k1, and a host that presented a P-256 key has its /// key in the document for an operator to bind and nothing here to verify a /// context against. -fn node_credential(account: &AgentAccount) -> Option<(String, VerifyingKey)> { +fn node_credential(account: &HostedAccount) -> Option<(String, VerifyingKey)> { let key = VerifyingKey::from_multibase(account.node_key.as_deref()?).ok()?; Some((account.did.as_str().to_owned(), key)) } @@ -5750,7 +5758,7 @@ where #[tracing::instrument( name = "provision", skip(self, request), - fields(agent_id = %request.agent_id, zone = tracing::field::Empty, did = tracing::field::Empty), + fields(account_id = %request.account_id, zone = tracing::field::Empty, did = tracing::field::Empty), )] fn provision(&self, request: ProvisionRequest) -> Result { // 1. Record how this account got in. A request carrying no claim @@ -5797,7 +5805,7 @@ where .attest( claim, &didbot_attest::ProvisioningRequest { - agent_id: &request.agent_id, + account_id: &request.account_id, handle: request.handle.as_deref(), parent: request.profile.parent.as_deref(), }, @@ -5871,7 +5879,7 @@ where // this deployment's own — and the proof is attached here so that // everything after this line, the store included, works on a name // this deployment serves. - let did = self.hosted(AgentDid::mint(zone, &request.agent_id)?)?; + let did = self.hosted(AccountDid::mint(zone, &request.account_id)?)?; // Recorded on the span so every later event in this provisioning // carries it without each one repeating the field. tracing::Span::current().record("did", tracing::field::display(&did)); @@ -5913,7 +5921,7 @@ where None => request.handle.clone(), Some(naming) => { let label = naming.issue( - &request.agent_id, + &request.account_id, zone.host(), request.handle.as_deref(), None, @@ -5974,7 +5982,7 @@ where if let Err(error) = self.record_event( did.as_str(), LedgerEvent::Provisioned { - agent_id: request.agent_id.clone(), + account_id: request.account_id.clone(), backend: provenance.backend.clone(), assurance: assurance_label(provenance.assurance).to_owned(), // The node the backend verified, for a request that carried @@ -6016,9 +6024,9 @@ where // each become their own real commit below, in order, each with a // `prevData` that is genuinely the tree before it: there is a key to // sign with from the first write. - let account = AgentAccount { + let account = HostedAccount { did: did.clone(), - agent_id: request.agent_id.clone(), + account_id: request.account_id.clone(), handle: handle.clone(), harness: trimmed(request.profile.harness.as_deref()), agent_type: trimmed(request.profile.agent_type.as_deref()), @@ -6156,14 +6164,14 @@ where // account already exists and already resolves by the time this runs. // `plan/auth-types.md`'s "Provisioning issues a session-scoped bearer // credential bound to the account's DID"; see `crate::credential`. - let agent_token = self + let account_token = self .credentials .issue(did.as_str(), DEFAULT_AGENT_TOKEN_TTL) .token; self.emit(LifecycleEvent::Provisioned { did: did.as_str().to_owned(), - agent_id: request.agent_id.clone(), + account_id: request.account_id.clone(), // Provisioning is the one path that always has it: every account // that reaches the store here was recorded on the way in. backend: provenance_backend.clone(), @@ -6174,7 +6182,7 @@ where Ok(Provisioned { account, document, - agent_token, + account_token, }) } @@ -6227,13 +6235,13 @@ where // The repository key, this server's, generated before the name so // its public half can seed the draw. The host's own key is a - // different key for a different job — see `AgentAccount::node_key`. + // different key for a different job — see `HostedAccount::node_key`. let key = SigningKey::generate(); let public = key.verifying_key(); let zone = &self.zone; let label = naming.issue(&public.to_multibase(), zone.host(), None, None, now)?; let handle = format!("{label}.{}", zone.host()); - let did = match AgentDid::mint(zone, &label) + let did = match AccountDid::mint(zone, &label) .map_err(ProvisionError::from) .and_then(|did| self.hosted(did)) { @@ -6266,7 +6274,7 @@ where if let Err(error) = self.record_event( did.as_str(), LedgerEvent::Provisioned { - agent_id: label.clone(), + account_id: label.clone(), backend: RESERVATION_BACKEND.to_owned(), assurance: assurance_label(Assurance::SelfAsserted).to_owned(), node_id: None, @@ -6285,9 +6293,9 @@ where ); let ttl = self.reservations.ttl; - let account = AgentAccount { + let account = HostedAccount { did: did.clone(), - agent_id: label, + account_id: label, handle: Some(handle.clone()), harness: None, agent_type: None, @@ -6364,7 +6372,7 @@ where }) } - fn reservations(&self) -> Vec { + fn reservations(&self) -> Vec { self.pending_reservations(OffsetDateTime::now_utc()) } @@ -6404,7 +6412,7 @@ where did: &str, operator: &str, vouch: &OperatorClaim, - ) -> Result { + ) -> Result { let account = self.lookup(did)?; if !account.is_reservation() { tracing::info!(state = ?account.state, "refused: not a pending reservation"); @@ -6457,7 +6465,7 @@ where ); self.emit(LifecycleEvent::Provisioned { did: active.did.as_str().to_owned(), - agent_id: active.agent_id.clone(), + account_id: active.account_id.clone(), backend: RESERVATION_BACKEND.to_owned(), assurance: assurance_label(Assurance::SelfAsserted).to_owned(), }); @@ -6490,7 +6498,7 @@ where /// DID being destroyed. /// /// This is the *only* path that frees a burned name — see - /// [`crate::names::Reservation::FormerAgent`] — and the only one that + /// [`crate::names::Reservation::FormerAccount`] — and the only one that /// takes down a document a soft delete would have kept serving forever. /// Both are hard to undo, so both are hard to reach by accident: /// `confirm_did` must equal `did` exactly, byte for byte, or nothing @@ -6703,7 +6711,7 @@ where // method, a handle in `alsoKnownAs`, and the same endpoint. Built // by `document_for` rather than here, so the apex and every agent // are rendered by one function. - if let Some(account) = AgentDid::parse(&self.zone.service_did()) + if let Some(account) = AccountDid::parse(&self.zone.service_did()) .ok() .and_then(|did| self.hosted(did).ok()) .and_then(|did| self.store.get(&did)) @@ -6751,7 +6759,7 @@ where /// is a repository this server serves and a DID it answers for, so the /// sync surface lists it, and it is not an agent, so an agent listing /// does not. `list_repos` is the other side of that distinction. - fn accounts(&self) -> Vec { + fn accounts(&self) -> Vec { self.store .list() .into_iter() @@ -7176,11 +7184,11 @@ where Ok(self.records.collections(account.did.as_str())) } - fn account(&self, did: &str) -> Option { + fn account(&self, did: &str) -> Option { self.lookup(did).ok() } - fn children(&self, did: &str) -> Result, ProvisionError> { + fn children(&self, did: &str) -> Result, ProvisionError> { let account = self.lookup(did)?; Ok(self.store.children(&account.did)) } @@ -7515,11 +7523,11 @@ where self.blobs.limits() } - fn ledger(&self, did: &str) -> Option { + fn ledger(&self, did: &str) -> Option { self.ledger.read(did) } - fn ledgers(&self) -> Vec { + fn ledgers(&self) -> Vec { self.ledger.all() } diff --git a/crates/didbot-pds/src/registration.rs b/crates/didbot-pds/src/registration.rs index 3023ef82..21091010 100644 --- a/crates/didbot-pds/src/registration.rs +++ b/crates/didbot-pds/src/registration.rs @@ -32,7 +32,7 @@ use time::format_description::well_known::Rfc3339; use time::OffsetDateTime; use crate::kind::AccountKind; -use crate::ledger::{AgentLedger, LedgerEvent}; +use crate::ledger::{AccountLedger, LedgerEvent}; /// The collection this module writes. pub const COLLECTION: &str = didbot_lexicon::nsid::REGISTRATION; @@ -63,7 +63,7 @@ fn datetime(at: OffsetDateTime) -> String { /// against a DID that has no ledger, so a ledger that exists begins with a /// provisioning. It is `None` rather than a panic because the alternative is a /// server that stops on a bookkeeping shape it did not expect. -pub fn compose(ledger: &AgentLedger, kind: AccountKind, operator: &str) -> Option { +pub fn compose(ledger: &AccountLedger, kind: AccountKind, operator: &str) -> Option { let LedgerEvent::Provisioned { .. } = ledger.provisioning()? else { return None; }; @@ -105,7 +105,7 @@ pub fn compose(ledger: &AgentLedger, kind: AccountKind, operator: &str) -> Optio /// and a reference from an earlier generation points at something that is /// gone. Counted from the ledger rather than stored, for the reason every /// other field here is: a second copy is a second thing to be wrong. -fn generation(ledger: &AgentLedger) -> u64 { +fn generation(ledger: &AccountLedger) -> u64 { ledger .entries .iter() @@ -129,7 +129,7 @@ mod tests { fn provisioned() -> LedgerEvent { LedgerEvent::Provisioned { - agent_id: "kestrel".to_owned(), + account_id: "kestrel".to_owned(), backend: "dev-shared-secret".to_owned(), assurance: "shared-secret".to_owned(), node_id: Some("node-alpha".to_owned()), @@ -137,7 +137,7 @@ mod tests { } } - fn ledger_with(events: Vec<(LedgerEvent, i64)>) -> AgentLedger { + fn ledger_with(events: Vec<(LedgerEvent, i64)>) -> AccountLedger { let store = MemoryLedger::new(); for (event, offset) in events { store diff --git a/crates/didbot-pds/src/server.rs b/crates/didbot-pds/src/server.rs index 027bf503..56f7f52d 100644 --- a/crates/didbot-pds/src/server.rs +++ b/crates/didbot-pds/src/server.rs @@ -43,7 +43,7 @@ use serde_json::{Map, Value}; -use crate::account::AgentAccount; +use crate::account::HostedAccount; use crate::bsky; /// Placeholder display name. Replace it per deployment. @@ -58,7 +58,7 @@ pub const DESCRIPTION: &str = "widdershins — placeholder, not deployment copy" /// timestamp, the picture, the self-label — and this fills the two fields it /// leaves empty for an account with no issued handle. The merge rule is the /// same one it uses: a field that is already there is not overwritten. -pub fn profile(account: &AgentAccount, avatar: Option<&Value>) -> Value { +pub fn profile(account: &HostedAccount, avatar: Option<&Value>) -> Value { let mut record = bsky::build(account, None, avatar); let Some(fields) = record.as_object_mut() else { return record; diff --git a/crates/didbot-pds/src/server_state.rs b/crates/didbot-pds/src/server_state.rs index 61fa965d..ce3d9131 100644 --- a/crates/didbot-pds/src/server_state.rs +++ b/crates/didbot-pds/src/server_state.rs @@ -306,7 +306,7 @@ pub struct ServerPolicy { /// That is deliberate and is what keeps the handshake from deadlocking; /// see the module doc. pub accepts_record_writes: bool, - /// Whether `bot.did.provisionAgent` mints new agent accounts. + /// Whether `bot.did.createAccount` mints new agent accounts. /// /// Enforced in `didbot-serve`'s `routes::require_lifecycle`, before the /// attestation claim is read: a server that will not provision has no diff --git a/crates/didbot-pds/src/wal/mod.rs b/crates/didbot-pds/src/wal/mod.rs index f01cf9b9..60575b64 100644 --- a/crates/didbot-pds/src/wal/mod.rs +++ b/crates/didbot-pds/src/wal/mod.rs @@ -139,7 +139,7 @@ use didbot_key::SigningKey; use serde_json::Value; use time::OffsetDateTime; -use crate::account::AgentAccount; +use crate::account::HostedAccount; use crate::journal::Mark; /// Longest a [`Durability::Deferred`] append waits before it is synced. @@ -385,7 +385,7 @@ pub enum Entry { /// An account was provisioned, with the key it signs as. AccountInserted { /// Everything the deployment recorded about it. - account: Box, + account: Box, /// The account's whole secret. See the module docs. #[serde(with = "key_hex")] key: SigningKey, @@ -464,7 +464,7 @@ pub enum Entry { /// /// Written once per account at most: the vouched path reserves the /// account first and learns its parent when the parent's record - /// appears, and [`AgentAccount::parent`] is set exactly once. An account + /// appears, and [`HostedAccount::parent`] is set exactly once. An account /// whose parent was known at insert carries it in /// [`Entry::AccountInserted`] and never writes this. AccountParented { @@ -618,9 +618,9 @@ pub enum Entry { /// in [`Entry::BlobUploaded`]: what is durable is the fact that this hash /// authenticates this DID, which is everything a restart needs to keep /// verifying requests. The plaintext left this process once, in the - /// `provisionAgent` response, and is unrecoverable from the log by + /// `createAccount` response, and is unrecoverable from the log by /// design — see `crate::credential`. - AgentTokenIssued { + AccountTokenIssued { /// The account this token authenticates as. did: String, /// SHA-256 of the token, hex-encoded. @@ -632,9 +632,9 @@ pub enum Entry { /// An account's write credential was revoked, with nothing reissued. /// /// Reissuing already implies revoking the previous token — see - /// [`Entry::AgentTokenIssued`] — so this entry is only for the case where + /// [`Entry::AccountTokenIssued`] — so this entry is only for the case where /// nothing replaces it: an account being deleted. - AgentTokenRevoked { + AccountTokenRevoked { /// Whose. did: String, }, @@ -681,7 +681,7 @@ pub enum Entry { }, /// An OAuth grant family was minted, with the pair it starts on. /// - /// The token strings are not here, for [`Entry::AgentTokenIssued`]'s + /// The token strings are not here, for [`Entry::AccountTokenIssued`]'s /// reason and by the same means: what is durable is that these two /// digests are this family's current pair. See [`crate::oauth`]. OauthGrantIssued { @@ -2777,9 +2777,9 @@ mod tests { fn every_entry_variant_matches_its_declared_shape() { let now = OffsetDateTime::now_utc(); let did = crate::hosted::HostedDid::replayed( - didbot_identity::AgentDid::parse("did:web:shape.example").expect("a valid did"), + didbot_identity::AccountDid::parse("did:web:shape.example").expect("a valid did"), ); - let account = AgentAccount::server(did.clone(), now); + let account = HostedAccount::server(did.clone(), now); let key = SigningKey::generate(); // One instance of every variant. Each is the cheapest value that @@ -2876,12 +2876,12 @@ mod tests { name: "shape".to_owned(), at: now, }, - Entry::AgentTokenIssued { + Entry::AccountTokenIssued { did: did.to_string(), token_hash: "deadbeef".to_owned(), expires_at: now, }, - Entry::AgentTokenRevoked { + Entry::AccountTokenRevoked { did: did.to_string(), }, Entry::StreamReserved { through: 1 }, diff --git a/crates/didbot-pds/src/zones.rs b/crates/didbot-pds/src/zones.rs index 61b7b33a..752c7ab0 100644 --- a/crates/didbot-pds/src/zones.rs +++ b/crates/didbot-pds/src/zones.rs @@ -19,7 +19,7 @@ use time::OffsetDateTime; use didbot_identity::{hostname_is_at_or_below, Zone, ZoneRegistry, ZoneRegistryError}; -use crate::account::AgentAccount; +use crate::account::HostedAccount; use crate::names::{label_of, NameError, NameRegistry, Naming, Reservation}; use crate::provision::RegistrationFacts; @@ -40,7 +40,7 @@ pub enum ZoneError { #[error( "cannot create zone {zone:?}: agent {did:?} already answers at that hostname; delete it first" )] - ApexIsLiveAgent { + ApexIsLiveAccount { /// The zone hostname that was asked for. zone: String, /// The agent already there. @@ -197,7 +197,7 @@ impl ZoneManager { &mut self, zone: Zone, parent: &Zone, - accounts: &[AgentAccount], + accounts: &[HostedAccount], names: &NameRegistry, now: OffsetDateTime, may_create_zones: bool, @@ -219,7 +219,7 @@ impl ZoneManager { .iter() .find(|a| a.did.host().eq_ignore_ascii_case(&zone_host)) { - return Err(ZoneError::ApexIsLiveAgent { + return Err(ZoneError::ApexIsLiveAccount { zone: zone_host, did: account.did.as_str().to_owned(), }); @@ -243,7 +243,7 @@ impl ZoneManager { // caught a live label when Naming and the account store agree. // `check` never returns `Exhausted`, `Generator` or `Journal`. Err(NameError::Live(_)) => { - return Err(ZoneError::ApexIsLiveAgent { + return Err(ZoneError::ApexIsLiveAccount { zone: zone_host, did: "unknown: live in the name registry but absent from the account store" .to_owned(), @@ -280,7 +280,7 @@ impl ZoneManager { pub fn can_remove_zone( &self, zone_host: &str, - accounts: &[AgentAccount], + accounts: &[HostedAccount], naming: Option<&Naming>, now: OffsetDateTime, ) -> Result<(), ZoneError> { @@ -397,7 +397,7 @@ impl ZoneRouter { mod tests { use super::*; use crate::account::AccountState; - use didbot_identity::AgentDid; + use didbot_identity::AccountDid; use didbot_name::{Namer, Seed}; use std::sync::Arc; use time::Duration; @@ -406,14 +406,14 @@ mod tests { OffsetDateTime::UNIX_EPOCH + Duration::days(days) } - fn account_at(host: &str) -> AgentAccount { + fn account_at(host: &str) -> HostedAccount { let did = crate::hosted::HostedDid::replayed( - AgentDid::parse(&format!("did:web:{host}")).expect("valid did"), + AccountDid::parse(&format!("did:web:{host}")).expect("valid did"), ); - AgentAccount { + HostedAccount { harness: None, agent_type: None, - agent_id: did.agent_id().to_owned(), + account_id: did.account_id().to_owned(), did, handle: None, created_at: OffsetDateTime::UNIX_EPOCH, @@ -505,7 +505,7 @@ mod tests { let err = manager .add_zone(claudes(), &root(), &accounts, &names, at(0), true) .expect_err("an agent already answers there"); - assert!(matches!(err, ZoneError::ApexIsLiveAgent { .. })); + assert!(matches!(err, ZoneError::ApexIsLiveAccount { .. })); assert!(err.to_string().contains("claudes.pds.did.bot")); // Refusal must not have mutated anything. assert!(manager.registry().find("claudes.pds.did.bot").is_none()); diff --git a/crates/didbot-pds/tests/admission_tree.rs b/crates/didbot-pds/tests/admission_tree.rs index fab931f0..8622da23 100644 --- a/crates/didbot-pds/tests/admission_tree.rs +++ b/crates/didbot-pds/tests/admission_tree.rs @@ -9,13 +9,13 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use didbot_attest::{Assurance, NodeCredentialBackend, Provenance}; -use didbot_identity::{AgentDid, Zone, ZoneRegistry}; +use didbot_identity::{AccountDid, Zone, ZoneRegistry}; use didbot_key::SigningKey; use didbot_name::Namer; use didbot_pds::{ - AccountKind, AccountState, AccountStore, Actor, AdmissionClaim, AdmissionError, AgentAccount, - Durable, EdgeKind, FileAccountStore, Holds, HostedDid, LedgerEvent, Lock, Locks, Membership, - MemoryAccountStore, NameProvenance, Naming, OperatorClaim, Party, ProvisionError, + AccountKind, AccountState, AccountStore, Actor, AdmissionClaim, AdmissionError, Durable, + EdgeKind, FileAccountStore, Holds, HostedAccount, HostedDid, LedgerEvent, Lock, Locks, + Membership, MemoryAccountStore, NameProvenance, Naming, OperatorClaim, Party, ProvisionError, ProvisionRequest, Provisioner, Registry, ReservationRequest, Retention, StoreError, Swap, Tag, Trust, }; @@ -55,7 +55,7 @@ fn boot(dir: &Path) -> (Durable, Pds) { fn hosted(did: &str) -> HostedDid { HostedDid::host( - AgentDid::parse(did).expect("a valid did"), + AccountDid::parse(did).expect("a valid did"), &ZoneRegistry::single(zone()), ) .expect("a did in the served zone") @@ -74,9 +74,9 @@ fn child_of(store: &dyn AccountStore, id: &str, parent: &HostedDid) -> HostedDid let did = hosted(&format!("did:web:{id}.{ZONE_HOST}")); store .insert( - AgentAccount { + HostedAccount { did: did.clone(), - agent_id: id.to_owned(), + account_id: id.to_owned(), handle: None, harness: None, agent_type: None, @@ -98,7 +98,7 @@ fn child_of(store: &dyn AccountStore, id: &str, parent: &HostedDid) -> HostedDid did } -fn dids(accounts: &[AgentAccount]) -> Vec { +fn dids(accounts: &[HostedAccount]) -> Vec { accounts .iter() .map(|account| account.did.as_str().to_owned()) @@ -205,7 +205,7 @@ fn an_admitted_host_provisions_beneath_itself_across_a_restart() { "n0", OffsetDateTime::now_utc(), &didbot_attest::ProvisioningRequest { - agent_id: "ctx", + account_id: "ctx", handle: None, parent: None, }, @@ -253,7 +253,7 @@ fn the_child_index_follows_every_write_that_can_move_a_link() { store .set_state(&erased, AccountState::Decommissioned) .expect("a stored account"); - let live: Vec = pds + let live: Vec = pds .children(host.as_str()) .expect("known") .into_iter() @@ -375,9 +375,9 @@ fn reserved_with( let did = hosted(&format!("did:web:{id}.{ZONE_HOST}")); store .insert( - AgentAccount { + HostedAccount { did: did.clone(), - agent_id: id.to_owned(), + account_id: id.to_owned(), handle: None, harness: None, agent_type: None, diff --git a/crates/didbot-pds/tests/authored.rs b/crates/didbot-pds/tests/authored.rs index 88664e7e..d421b835 100644 --- a/crates/didbot-pds/tests/authored.rs +++ b/crates/didbot-pds/tests/authored.rs @@ -49,8 +49,8 @@ fn provisioner() -> Provisioner { .with_ledger(Arc::new(MemoryLedger::new()) as Arc) } -fn account(pds: &impl Registry, agent_id: &str) -> String { - pds.provision(ProvisionRequest::new(agent_id, None)) +fn account(pds: &impl Registry, account_id: &str) -> String { + pds.provision(ProvisionRequest::new(account_id, None)) .expect("provisioning should succeed") .account .did diff --git a/crates/didbot-pds/tests/blob_declared_type.rs b/crates/didbot-pds/tests/blob_declared_type.rs index 0b88de2b..728849b1 100644 --- a/crates/didbot-pds/tests/blob_declared_type.rs +++ b/crates/didbot-pds/tests/blob_declared_type.rs @@ -40,8 +40,8 @@ fn provisioner() -> Provisioner { .with_blob_store(Arc::new(MemoryBlobStore::new())) } -fn account(pds: &impl Registry, agent_id: &str) -> String { - pds.provision(ProvisionRequest::new(agent_id, None)) +fn account(pds: &impl Registry, account_id: &str) -> String { + pds.provision(ProvisionRequest::new(account_id, None)) .expect("provisioning succeeds") .account .did diff --git a/crates/didbot-pds/tests/blob_references.rs b/crates/didbot-pds/tests/blob_references.rs index 3719cfdc..0611dbf0 100644 --- a/crates/didbot-pds/tests/blob_references.rs +++ b/crates/didbot-pds/tests/blob_references.rs @@ -65,8 +65,8 @@ fn boot(dir: &Path) -> (Durable, Pds) { (durable, pds) } -fn account(pds: &Pds, agent_id: &str) -> String { - pds.provision(ProvisionRequest::new(agent_id, None)) +fn account(pds: &Pds, account_id: &str) -> String { + pds.provision(ProvisionRequest::new(account_id, None)) .expect("provisioning should succeed") .account .did diff --git a/crates/didbot-pds/tests/blobs.rs b/crates/didbot-pds/tests/blobs.rs index adc4b021..79f04fed 100644 --- a/crates/didbot-pds/tests/blobs.rs +++ b/crates/didbot-pds/tests/blobs.rs @@ -51,8 +51,8 @@ fn provisioner() -> Provisioner { }))) } -fn account(pds: &impl Registry, agent_id: &str) -> String { - pds.provision(ProvisionRequest::new(agent_id, None)) +fn account(pds: &impl Registry, account_id: &str) -> String { + pds.provision(ProvisionRequest::new(account_id, None)) .expect("provisioning should succeed") .account .did diff --git a/crates/didbot-pds/tests/born.rs b/crates/didbot-pds/tests/born.rs index f457f0f4..0c5bd4f3 100644 --- a/crates/didbot-pds/tests/born.rs +++ b/crates/didbot-pds/tests/born.rs @@ -40,8 +40,8 @@ fn provisioner() -> Provisioner { } /// Provisions an account carrying `facts`, and returns its DID. -fn account(pds: &impl Registry, agent_id: &str, facts: RegistrationFacts) -> String { - let request = ProvisionRequest::new(agent_id, None).with_profile(facts); +fn account(pds: &impl Registry, account_id: &str, facts: RegistrationFacts) -> String { + let request = ProvisionRequest::new(account_id, None).with_profile(facts); match pds.provision(request) { Ok(provisioned) => provisioned.account.did.as_str().to_owned(), Err(err) => panic!("provisioning should succeed: {err}"), @@ -150,7 +150,7 @@ fn a_parent_that_is_not_even_a_did_is_dropped() { #[test] fn an_account_cannot_be_its_own_parent() { let pds = provisioner(); - let own = didbot_identity::AgentDid::mint(&zone(), "ouroboros") + let own = didbot_identity::AccountDid::mint(&zone(), "ouroboros") .expect("the label mints") .as_str() .to_owned(); diff --git a/crates/didbot-pds/tests/durability.rs b/crates/didbot-pds/tests/durability.rs index cb34eb81..9ae17737 100644 --- a/crates/didbot-pds/tests/durability.rs +++ b/crates/didbot-pds/tests/durability.rs @@ -53,8 +53,8 @@ fn zone() -> Zone { Zone::delegated("localhost", ZONE_HOST).expect("test zone should be constructible") } -fn request(agent_id: &str) -> ProvisionRequest { - ProvisionRequest::new(agent_id, None) +fn request(account_id: &str) -> ProvisionRequest { + ProvisionRequest::new(account_id, None) } /// Hands out one name per call, so a test can assert on which one it got. @@ -500,7 +500,7 @@ fn accounts_records_and_keys_all_come_back() { .into_iter() .find(|a| a.did == did) .expect("the account should have come back"); - assert_eq!(account.agent_id, "kestrel"); + assert_eq!(account.account_id, "kestrel"); let key_after = durable .accounts() @@ -619,8 +619,8 @@ fn a_burned_name_is_still_reserved_after_a_restart() { let names = durable.names(); assert_eq!(names.reserved_count(), 1); match names.reservation_of("basalt-otter") { - Some(didbot_pds::Reservation::FormerAgent { did: held }) => assert_eq!(held, did), - other => panic!("expected the burn to come back as a FormerAgent hold, got {other:?}"), + Some(didbot_pds::Reservation::FormerAccount { did: held }) => assert_eq!(held, did), + other => panic!("expected the burn to come back as a FormerAccount hold, got {other:?}"), } // Long past any hold window: a reservation does not age out. assert!(!names.is_free( @@ -1651,8 +1651,8 @@ fn a_singleton_record_replays_as_one_record_at_self() { // --- blobs --- /// Provisions one account and hands back its DID. -fn provision(pds: &Pds, agent_id: &str) -> String { - pds.provision(request(agent_id)) +fn provision(pds: &Pds, account_id: &str) -> String { + pds.provision(request(account_id)) .expect("provision") .account .did @@ -1923,7 +1923,7 @@ fn an_agent_token_survives_the_compaction_that_rewrites_the_log() { .did; pds.delete(churn.as_str()).expect("delete"); } - provisioned.agent_token + provisioned.account_token }; let before = log_bytes(&dir); @@ -2403,8 +2403,8 @@ fn a_deleted_accounts_agent_token_is_refused_after_a_restart() { let (_durable, pds) = boot_with_credentials(&dir); let hard = pds.provision(request("hard")).expect("provision"); let soft = pds.provision(request("soft")).expect("provision"); - assert!(pds.verify_agent_token(&hard.agent_token).is_ok()); - assert!(pds.verify_agent_token(&soft.agent_token).is_ok()); + assert!(pds.verify_agent_token(&hard.account_token).is_ok()); + assert!(pds.verify_agent_token(&soft.account_token).is_ok()); pds.hard_delete( hard.account.did.as_str(), hard.account.did.as_str(), @@ -2414,9 +2414,9 @@ fn a_deleted_accounts_agent_token_is_refused_after_a_restart() { pds.delete(soft.account.did.as_str()).expect("erase"); ( hard.account.did.as_str().to_owned(), - hard.agent_token, + hard.account_token, soft.account.did.as_str().to_owned(), - soft.agent_token, + soft.account_token, ) }; @@ -2435,7 +2435,7 @@ fn a_deleted_accounts_agent_token_is_refused_after_a_restart() { ); pds.provision(request("live")) .expect("provision") - .agent_token + .account_token }; let (_durable, pds) = boot_with_credentials(&dir); @@ -2734,7 +2734,7 @@ const WRITTEN_ENTRIES: &[(&str, &str)] = &[ ( "accountInserted", r#"{"op":"accountInserted","account":{"did":"did:web:shearwater.agents.localhost", - "agentId":"shearwater","handle":null,"createdAt":"2026-01-01T00:00:00Z"}, + "accountId":"shearwater","handle":null,"createdAt":"2026-01-01T00:00:00Z"}, "key":"0101010101010101010101010101010101010101010101010101010101010101"}"#, ), ( @@ -2760,13 +2760,13 @@ const WRITTEN_ENTRIES: &[(&str, &str)] = &[ r#"{"op":"accountStateChanged","did":"did:web:a.agents.localhost","state":"active"}"#, ), ( - "agentTokenIssued", - r#"{"op":"agentTokenIssued","did":"did:web:a.agents.localhost","token_hash":"ab01", + "accountTokenIssued", + r#"{"op":"accountTokenIssued","did":"did:web:a.agents.localhost","token_hash":"ab01", "expires_at":"2026-06-01T00:00:00Z"}"#, ), ( - "agentTokenRevoked", - r#"{"op":"agentTokenRevoked","did":"did:web:a.agents.localhost"}"#, + "accountTokenRevoked", + r#"{"op":"accountTokenRevoked","did":"did:web:a.agents.localhost"}"#, ), ( "blobCollected", @@ -2784,7 +2784,7 @@ const WRITTEN_ENTRIES: &[(&str, &str)] = &[ "ledgerAppended", r#"{"op":"ledgerAppended","did":"did:web:a.agents.localhost", "entry":{"seq":1,"at":"2026-01-01T00:00:00Z","event":"provisioned", - "agentId":"shearwater","backend":"loopback","assurance":"self-asserted"}}"#, + "accountId":"shearwater","backend":"loopback","assurance":"self-asserted"}}"#, ), ("nameClaimed", r#"{"op":"nameClaimed","name":"quernstone"}"#), ( diff --git a/crates/didbot-pds/tests/erasure.rs b/crates/didbot-pds/tests/erasure.rs index 259bd03f..d1fc2a96 100644 --- a/crates/didbot-pds/tests/erasure.rs +++ b/crates/didbot-pds/tests/erasure.rs @@ -14,7 +14,7 @@ use std::sync::Arc; use didbot_identity::Zone; use didbot_key::SigningKey; use didbot_pds::{ - AccountState, AccountStore, AgentAccount, Hold, HostedDid, MemoryAccountStore, ProvisionError, + AccountState, AccountStore, Hold, HostedAccount, HostedDid, MemoryAccountStore, ProvisionError, ProvisionRequest, Provisioner, Registry, Retention, StoreError, Swap, Tag, }; @@ -41,16 +41,16 @@ impl DiesAt { } impl AccountStore for DiesAt { - fn insert(&self, account: AgentAccount, key: SigningKey) -> Result<(), StoreError> { + fn insert(&self, account: HostedAccount, key: SigningKey) -> Result<(), StoreError> { self.inner.insert(account, key) } - fn get(&self, did: &HostedDid) -> Option { + fn get(&self, did: &HostedDid) -> Option { self.inner.get(did) } fn signing_key(&self, did: &HostedDid) -> Option { self.inner.signing_key(did) } - fn remove(&self, did: &HostedDid) -> Result { + fn remove(&self, did: &HostedDid) -> Result { self.inner.remove(did) } fn set_retention(&self, did: &HostedDid, retention: Retention) -> Result<(), StoreError> { @@ -78,10 +78,10 @@ impl AccountStore for DiesAt { fn set_parent(&self, did: &HostedDid, parent: &HostedDid) -> Result<(), StoreError> { self.inner.set_parent(did, parent) } - fn children(&self, parent: &HostedDid) -> Vec { + fn children(&self, parent: &HostedDid) -> Vec { self.inner.children(parent) } - fn list(&self) -> Vec { + fn list(&self) -> Vec { self.inner.list() } } @@ -274,7 +274,7 @@ fn a_crash_after_the_reservation_leaves_a_row_that_serves_nothing_and_is_reaped( let account = pds .accounts() .into_iter() - .find(|account| account.agent_id == "reserved") + .find(|account| account.account_id == "reserved") .expect("the reservation is stored"); assert_eq!(account.state, AccountState::Reserved); let host = account.did.host().to_owned(); diff --git a/crates/didbot-pds/tests/judged_record.rs b/crates/didbot-pds/tests/judged_record.rs index 94efd32a..8ee7733a 100644 --- a/crates/didbot-pds/tests/judged_record.rs +++ b/crates/didbot-pds/tests/judged_record.rs @@ -147,7 +147,7 @@ struct Harness { did: String, } -fn harness(agent_id: &str) -> Harness { +fn harness(account_id: &str) -> Harness { let zone = Zone::delegated("localhost", ZONE_HOST).expect("the test zone is valid"); let records = Arc::new(RacingStore::over(Arc::new(MemoryRecordStore::new()))); let pds = Provisioner::new( @@ -158,7 +158,7 @@ fn harness(agent_id: &str) -> Harness { ) .with_record_store(records.clone()); let did = pds - .provision(ProvisionRequest::new(agent_id, None)) + .provision(ProvisionRequest::new(account_id, None)) .expect("provisioning succeeds") .account .did diff --git a/crates/didbot-pds/tests/ledger.rs b/crates/didbot-pds/tests/ledger.rs index 6cce43cd..36091728 100644 --- a/crates/didbot-pds/tests/ledger.rs +++ b/crates/didbot-pds/tests/ledger.rs @@ -23,8 +23,8 @@ fn zone() -> Zone { Zone::delegated("localhost", ZONE_HOST).expect("test zone should be constructible") } -fn request(agent_id: &str) -> ProvisionRequest { - ProvisionRequest::new(agent_id, None) +fn request(account_id: &str) -> ProvisionRequest { + ProvisionRequest::new(account_id, None) } /// Hands out one name per call, so a test can assert on which one it got. @@ -93,7 +93,7 @@ fn boot(dir: &Path, names: Option>) -> (Durable, Pds) { (durable, pds) } -fn labels(ledger: &didbot_pds::AgentLedger) -> Vec<&'static str> { +fn labels(ledger: &didbot_pds::AccountLedger) -> Vec<&'static str> { ledger.entries.iter().map(|e| e.event.label()).collect() } @@ -113,13 +113,13 @@ fn provisioning_opens_a_ledger_naming_the_backend_that_admitted_it() { assert_eq!(ledger.entries[0].seq, 1); match ledger.provisioning().expect("a provisioning entry") { LedgerEvent::Provisioned { - agent_id, + account_id, backend, assurance, node_id, .. } => { - assert_eq!(agent_id, "kestrel"); + assert_eq!(account_id, "kestrel"); assert_eq!(backend, didbot_pds::UNAUTHENTICATED_BACKEND); assert_eq!(assurance, "self-asserted"); assert_eq!(node_id, &None); diff --git a/crates/didbot-pds/tests/lifecycle_race.rs b/crates/didbot-pds/tests/lifecycle_race.rs index 3cd2d9a2..2c5e8c68 100644 --- a/crates/didbot-pds/tests/lifecycle_race.rs +++ b/crates/didbot-pds/tests/lifecycle_race.rs @@ -15,7 +15,7 @@ use std::time::Duration; use didbot_identity::Zone; use didbot_key::SigningKey; use didbot_pds::{ - AccountState, AccountStore, Actor, AgentAccount, Hold, HostedDid, Lock, MemoryAccountStore, + AccountState, AccountStore, Actor, Hold, HostedAccount, HostedDid, Lock, MemoryAccountStore, ProvisionRequest, Provisioner, Registry, Retention, StoreError, Tag, }; @@ -56,11 +56,11 @@ impl RacingStore { } impl AccountStore for RacingStore { - fn insert(&self, account: AgentAccount, key: SigningKey) -> Result<(), StoreError> { + fn insert(&self, account: HostedAccount, key: SigningKey) -> Result<(), StoreError> { self.inner.insert(account, key) } - fn get(&self, did: &HostedDid) -> Option { + fn get(&self, did: &HostedDid) -> Option { let account = self.inner.get(did); if self.armed.swap(false, Ordering::SeqCst) { let entered = self.entered.lock().expect("poisoned").take(); @@ -78,7 +78,7 @@ impl AccountStore for RacingStore { self.inner.signing_key(did) } - fn remove(&self, did: &HostedDid) -> Result { + fn remove(&self, did: &HostedDid) -> Result { self.inner.remove(did) } @@ -110,11 +110,11 @@ impl AccountStore for RacingStore { self.inner.set_parent(did, parent) } - fn children(&self, parent: &HostedDid) -> Vec { + fn children(&self, parent: &HostedDid) -> Vec { self.inner.children(parent) } - fn list(&self) -> Vec { + fn list(&self) -> Vec { self.inner.list() } } diff --git a/crates/didbot-pds/tests/logging.rs b/crates/didbot-pds/tests/logging.rs index 79f49b01..d5e763ef 100644 --- a/crates/didbot-pds/tests/logging.rs +++ b/crates/didbot-pds/tests/logging.rs @@ -103,7 +103,7 @@ fn a_provisioning_logs_every_step_and_leaks_nothing() { ); } // Structured fields, not interpolated strings. - assert!(rendered.contains("agent_id=discreet")); + assert!(rendered.contains("account_id=discreet")); assert!(rendered.contains(provisioned.account.did.as_str())); let key = match pds.store().signing_key(&provisioned.account.did) { diff --git a/crates/didbot-pds/tests/naming.rs b/crates/didbot-pds/tests/naming.rs index e55f67fa..2afa65c2 100644 --- a/crates/didbot-pds/tests/naming.rs +++ b/crates/didbot-pds/tests/naming.rs @@ -20,8 +20,8 @@ fn zone() -> Zone { Zone::delegated("localhost", ZONE_HOST).expect("test zone should be constructible") } -fn request(agent_id: &str, handle: Option<&str>) -> ProvisionRequest { - ProvisionRequest::new(agent_id, handle.map(str::to_owned)) +fn request(account_id: &str, handle: Option<&str>) -> ProvisionRequest { + ProvisionRequest::new(account_id, handle.map(str::to_owned)) } fn pds(naming: Naming) -> Provisioner { @@ -600,10 +600,10 @@ fn an_erased_agents_name_is_burned_and_never_reissued() { OffsetDateTime::now_utc() + Duration::days(36500) )); match naming.registry().reservation_of("basalt-otter") { - Some(didbot_pds::Reservation::FormerAgent { did }) => { + Some(didbot_pds::Reservation::FormerAccount { did }) => { assert_eq!(did, provisioned.account.did.as_str()); } - other => panic!("expected a FormerAgent reservation, got {other:?}"), + other => panic!("expected a FormerAccount reservation, got {other:?}"), } // The document still resolves — erasure keeps it — but the hostname diff --git a/crates/didbot-pds/tests/provisioning.rs b/crates/didbot-pds/tests/provisioning.rs index 7044bde7..4ba5c572 100644 --- a/crates/didbot-pds/tests/provisioning.rs +++ b/crates/didbot-pds/tests/provisioning.rs @@ -12,10 +12,10 @@ use std::sync::Arc; use didbot_attest::Assurance; -use didbot_identity::{AgentDid, Zone, ZoneRegistry}; +use didbot_identity::{AccountDid, Zone, ZoneRegistry}; use didbot_key::SigningKey; use didbot_pds::{ - AccountKind, AccountState, AccountStore, Actor, AgentAccount, Hold, HostedDid, LedgerEvent, + AccountKind, AccountState, AccountStore, Actor, Hold, HostedAccount, HostedDid, LedgerEvent, LifecycleEvent, Lock, MemoryAccountStore, NameProvenance, Party, ProvisionError, ProvisionRequest, Provisioner, RecordingSink, Registry, ReservationRequest, Retention, StoreError, Swap, Tag, @@ -36,8 +36,8 @@ fn zone() -> Zone { } } -fn request(agent_id: &str, handle: Option<&str>) -> ProvisionRequest { - ProvisionRequest::new(agent_id, handle.map(str::to_owned)) +fn request(account_id: &str, handle: Option<&str>) -> ProvisionRequest { + ProvisionRequest::new(account_id, handle.map(str::to_owned)) } fn provisioner() -> Provisioner { @@ -49,8 +49,8 @@ fn provisioner() -> Provisioner { ) } -fn did_of(agent_id: &str) -> String { - format!("did:web:{agent_id}.{ZONE_HOST}") +fn did_of(account_id: &str) -> String { + format!("did:web:{account_id}.{ZONE_HOST}") } // --------------------------------------------------------------------------- @@ -70,7 +70,7 @@ fn a_successful_provision_publishes_stores_and_describes_the_account() { let did = did_of("scribe"); assert_eq!(provisioned.account.did.as_str(), did); - assert_eq!(provisioned.account.agent_id, "scribe"); + assert_eq!(provisioned.account.account_id, "scribe"); assert_eq!(provisioned.account.handle.as_deref(), Some(&*handle)); // Both directions of atproto's rule agree about this account: the // document claims the handle and the well-known answers for it. @@ -199,19 +199,19 @@ struct FailingInsertStore { } impl AccountStore for FailingInsertStore { - fn insert(&self, account: AgentAccount, _key: SigningKey) -> Result<(), StoreError> { + fn insert(&self, account: HostedAccount, _key: SigningKey) -> Result<(), StoreError> { Err(StoreError::Backend(format!( "disk on fire while storing {}", account.did ))) } - fn get(&self, did: &HostedDid) -> Option { + fn get(&self, did: &HostedDid) -> Option { self.inner.get(did) } fn signing_key(&self, did: &HostedDid) -> Option { self.inner.signing_key(did) } - fn remove(&self, did: &HostedDid) -> Result { + fn remove(&self, did: &HostedDid) -> Result { self.inner.remove(did) } fn set_retention(&self, did: &HostedDid, retention: Retention) -> Result<(), StoreError> { @@ -220,7 +220,7 @@ impl AccountStore for FailingInsertStore { fn set_state(&self, did: &HostedDid, state: AccountState) -> Result<(), StoreError> { self.inner.set_state(did, state) } - fn list(&self) -> Vec { + fn list(&self) -> Vec { self.inner.list() } fn hang(&self, did: &HostedDid, tag: Tag) -> Result<(), StoreError> { @@ -238,7 +238,7 @@ impl AccountStore for FailingInsertStore { fn set_parent(&self, did: &HostedDid, parent: &HostedDid) -> Result<(), StoreError> { self.inner.set_parent(did, parent) } - fn children(&self, parent: &HostedDid) -> Vec { + fn children(&self, parent: &HostedDid) -> Vec { self.inner.children(parent) } } @@ -276,16 +276,16 @@ struct WedgingStore { } impl AccountStore for WedgingStore { - fn insert(&self, account: AgentAccount, key: SigningKey) -> Result<(), StoreError> { + fn insert(&self, account: HostedAccount, key: SigningKey) -> Result<(), StoreError> { self.inner.insert(account, key) } - fn get(&self, did: &HostedDid) -> Option { + fn get(&self, did: &HostedDid) -> Option { self.inner.get(did) } fn signing_key(&self, did: &HostedDid) -> Option { self.inner.signing_key(did) } - fn remove(&self, did: &HostedDid) -> Result { + fn remove(&self, did: &HostedDid) -> Result { if !self .refused_removal .swap(true, std::sync::atomic::Ordering::SeqCst) @@ -307,7 +307,7 @@ impl AccountStore for WedgingStore { } self.inner.set_state(did, state) } - fn list(&self) -> Vec { + fn list(&self) -> Vec { self.inner.list() } fn hang(&self, did: &HostedDid, tag: Tag) -> Result<(), StoreError> { @@ -325,7 +325,7 @@ impl AccountStore for WedgingStore { fn set_parent(&self, did: &HostedDid, parent: &HostedDid) -> Result<(), StoreError> { self.inner.set_parent(did, parent) } - fn children(&self, parent: &HostedDid) -> Vec { + fn children(&self, parent: &HostedDid) -> Vec { self.inner.children(parent) } } @@ -405,7 +405,7 @@ fn a_wedged_account_can_be_deleted() { } /// And the automated path that reaches it must get there too: the HTTP -/// delete route is `Credential::AgentSelf`, and an account stranded before +/// delete route is `Credential::AccountSelf`, and an account stranded before /// activation has no write credential, so `sweep_stale` is the remedy that /// has to work. #[test] @@ -638,7 +638,7 @@ fn lifecycle_events_arrive_in_the_order_the_operations_happened() { }, LifecycleEvent::Provisioned { did: did.clone(), - agent_id: "lively".to_string(), + account_id: "lively".to_string(), backend: "unauthenticated".to_string(), assurance: "self-asserted".to_string(), }, @@ -747,12 +747,12 @@ fn a_provisioner_is_usable_as_a_registry_trait_object() { #[test] fn accounts_are_listed_in_did_order() { let pds = provisioner(); - for agent_id in ["gamma", "alpha", "beta"] { - if let Err(err) = pds.provision(request(agent_id, None)) { - panic!("provisioning {agent_id} should succeed: {err}"); + for account_id in ["gamma", "alpha", "beta"] { + if let Err(err) = pds.provision(request(account_id, None)) { + panic!("provisioning {account_id} should succeed: {err}"); } } - let ids: Vec = pds.accounts().into_iter().map(|a| a.agent_id).collect(); + let ids: Vec = pds.accounts().into_iter().map(|a| a.account_id).collect(); assert_eq!(ids, vec!["alpha", "beta", "gamma"]); } @@ -780,7 +780,7 @@ fn a_malformed_handle_is_refused_before_anything_is_published() { // Both used to be `store.list().into_iter().find(..)` / `find_map(..)`: a // walk of every account, per request. `plan/index.md` flagged this crate's // half of the fix after the same shape was closed in vibescrobble.com's -// index, in its `View::agent_by_handle`. The fix is a lookup index kept in +// index, in its `View::account_by_handle`. The fix is a lookup index kept in // step with the store on every insert and removal (see `HostIndex` in // `provision.rs`), and the tests below are the deterministic proof: `list()` // is called a fixed number of times no matter how many accounts exist or how @@ -802,16 +802,16 @@ impl CountingStore { } impl AccountStore for CountingStore { - fn insert(&self, account: AgentAccount, key: SigningKey) -> Result<(), StoreError> { + fn insert(&self, account: HostedAccount, key: SigningKey) -> Result<(), StoreError> { self.inner.insert(account, key) } - fn get(&self, did: &HostedDid) -> Option { + fn get(&self, did: &HostedDid) -> Option { self.inner.get(did) } fn signing_key(&self, did: &HostedDid) -> Option { self.inner.signing_key(did) } - fn remove(&self, did: &HostedDid) -> Result { + fn remove(&self, did: &HostedDid) -> Result { self.inner.remove(did) } fn set_retention(&self, did: &HostedDid, retention: Retention) -> Result<(), StoreError> { @@ -820,7 +820,7 @@ impl AccountStore for CountingStore { fn set_state(&self, did: &HostedDid, state: AccountState) -> Result<(), StoreError> { self.inner.set_state(did, state) } - fn list(&self) -> Vec { + fn list(&self) -> Vec { self.list_calls .fetch_add(1, std::sync::atomic::Ordering::SeqCst); self.inner.list() @@ -840,7 +840,7 @@ impl AccountStore for CountingStore { fn set_parent(&self, did: &HostedDid, parent: &HostedDid) -> Result<(), StoreError> { self.inner.set_parent(did, parent) } - fn children(&self, parent: &HostedDid) -> Vec { + fn children(&self, parent: &HostedDid) -> Vec { self.inner.children(parent) } } @@ -859,13 +859,13 @@ fn did_document_and_handle_did_do_not_scan_the_store() { let mut hosts = Vec::new(); for n in 0..50 { - let agent_id = format!("agent-{n}"); + let account_id = format!("agent-{n}"); let handle = format!("handle-{n}.{ZONE_HOST}"); - match pds.provision(request(&agent_id, Some(&handle))) { + match pds.provision(request(&account_id, Some(&handle))) { Ok(_) => {} - Err(err) => panic!("provisioning {agent_id} should succeed: {err}"), + Err(err) => panic!("provisioning {account_id} should succeed: {err}"), } - hosts.push((format!("{agent_id}.{ZONE_HOST}"), handle)); + hosts.push((format!("{account_id}.{ZONE_HOST}"), handle)); } // Provisioning still scans once per call today, in // `check_requested_handle`'s own uniqueness check — a write-path check @@ -947,15 +947,15 @@ fn a_collision_the_store_should_never_hold_resolves_the_way_the_scan_it_replaced let z = zone(); let handle = format!("shared.{ZONE_HOST}"); - for agent_id in ["beta", "alpha"] { + for account_id in ["beta", "alpha"] { let did = HostedDid::host( - AgentDid::mint(&z, agent_id).expect("mint"), + AccountDid::mint(&z, account_id).expect("mint"), &ZoneRegistry::single(z.clone()), ) .expect("hosted"); - let account = AgentAccount { + let account = HostedAccount { did: did.clone(), - agent_id: agent_id.to_owned(), + account_id: account_id.to_owned(), handle: Some(handle.clone()), harness: None, agent_type: None, @@ -1504,8 +1504,8 @@ fn a_deleted_accounts_agent_token_stops_verifying() { let hard = pds.provision(request("hard", None)).expect("provision"); let soft = pds.provision(request("soft", None)).expect("provision"); - assert!(pds.verify_agent_token(&hard.agent_token).is_ok()); - assert!(pds.verify_agent_token(&soft.agent_token).is_ok()); + assert!(pds.verify_agent_token(&hard.account_token).is_ok()); + assert!(pds.verify_agent_token(&soft.account_token).is_ok()); pds.hard_delete( hard.account.did.as_str(), @@ -1515,6 +1515,6 @@ fn a_deleted_accounts_agent_token_stops_verifying() { .expect("hard delete"); pds.delete(soft.account.did.as_str()).expect("erase"); - assert!(pds.verify_agent_token(&hard.agent_token).is_err()); - assert!(pds.verify_agent_token(&soft.agent_token).is_err()); + assert!(pds.verify_agent_token(&hard.account_token).is_err()); + assert!(pds.verify_agent_token(&soft.account_token).is_err()); } diff --git a/crates/didbot-pds/tests/records.rs b/crates/didbot-pds/tests/records.rs index 8ea92c48..071d3007 100644 --- a/crates/didbot-pds/tests/records.rs +++ b/crates/didbot-pds/tests/records.rs @@ -106,8 +106,8 @@ fn written(pds: &impl Registry) -> usize { } /// Provisions an account and returns its DID. -fn account(pds: &impl Registry, agent_id: &str) -> String { - let request = ProvisionRequest::new(agent_id, None); +fn account(pds: &impl Registry, account_id: &str) -> String { + let request = ProvisionRequest::new(account_id, None); match pds.provision(request) { Ok(provisioned) => provisioned.account.did.as_str().to_owned(), Err(err) => panic!("provisioning should succeed: {err}"), diff --git a/crates/didbot-pds/tests/refusal.rs b/crates/didbot-pds/tests/refusal.rs index e63f27ba..3f571fc9 100644 --- a/crates/didbot-pds/tests/refusal.rs +++ b/crates/didbot-pds/tests/refusal.rs @@ -10,10 +10,10 @@ //! and a restart afterwards replays every write that was acknowledged and no //! write that was not. -use didbot_identity::{AgentDid, Zone, ZoneRegistry}; +use didbot_identity::{AccountDid, Zone, ZoneRegistry}; use didbot_key::SigningKey; use didbot_pds::{ - AccountKind, AccountState, AccountStore, AgentAccount, Durable, HostedDid, ListParams, + AccountKind, AccountState, AccountStore, Durable, HostedAccount, HostedDid, ListParams, NameProvenance, Precondition, RecordError, RecordStore, Retention, StoreError, MAX_ENTRY, }; use serde_json::json; @@ -64,15 +64,15 @@ fn write(store: &impl RecordStore, text: &str) -> Result { } /// An account this deployment could hold, and the DID naming it. -fn account(agent_id: &str) -> (HostedDid, AgentAccount) { +fn account(account_id: &str) -> (HostedDid, HostedAccount) { let did = HostedDid::host( - AgentDid::parse(&format!("did:web:{agent_id}.agents.localhost")).expect("a did"), + AccountDid::parse(&format!("did:web:{account_id}.agents.localhost")).expect("a did"), &ZoneRegistry::single(Zone::new("agents.localhost").expect("a zone")), ) .expect("hosted"); - let account = AgentAccount { + let account = HostedAccount { did: did.clone(), - agent_id: agent_id.to_owned(), + account_id: account_id.to_owned(), handle: None, harness: None, agent_type: None, diff --git a/crates/didbot-pds/tests/registration.rs b/crates/didbot-pds/tests/registration.rs index 2d1ccaf9..a0ee43d0 100644 --- a/crates/didbot-pds/tests/registration.rs +++ b/crates/didbot-pds/tests/registration.rs @@ -23,8 +23,8 @@ fn zone() -> Zone { Zone::delegated("localhost", ZONE_HOST).expect("test zone should be constructible") } -fn request(agent_id: &str) -> ProvisionRequest { - ProvisionRequest::new(agent_id, None) +fn request(account_id: &str) -> ProvisionRequest { + ProvisionRequest::new(account_id, None) } #[derive(Debug)] diff --git a/crates/didbot-pds/tests/repo_cost.rs b/crates/didbot-pds/tests/repo_cost.rs index d1e609c5..669c0e99 100644 --- a/crates/didbot-pds/tests/repo_cost.rs +++ b/crates/didbot-pds/tests/repo_cost.rs @@ -50,8 +50,8 @@ fn provisioner() -> Provisioner { ) } -fn account(pds: &impl Registry, agent_id: &str) -> String { - pds.provision(ProvisionRequest::new(agent_id, None)) +fn account(pds: &impl Registry, account_id: &str) -> String { + pds.provision(ProvisionRequest::new(account_id, None)) .expect("provisioning") .account .did diff --git a/crates/didbot-pds/tests/restore.rs b/crates/didbot-pds/tests/restore.rs index f61c1bb7..3a6d769e 100644 --- a/crates/didbot-pds/tests/restore.rs +++ b/crates/didbot-pds/tests/restore.rs @@ -102,8 +102,8 @@ fn restore(from: &Path, into: &Path) { } } -fn provision(pds: &Pds, agent_id: &str) -> String { - pds.provision(ProvisionRequest::new(agent_id, None)) +fn provision(pds: &Pds, account_id: &str) -> String { + pds.provision(ProvisionRequest::new(account_id, None)) .expect("provision") .account .did diff --git a/crates/didbot-pds/tests/revision.rs b/crates/didbot-pds/tests/revision.rs index 25af37cd..a14532f0 100644 --- a/crates/didbot-pds/tests/revision.rs +++ b/crates/didbot-pds/tests/revision.rs @@ -48,8 +48,8 @@ fn boot( .with_commit_history(history.clone()) } -fn account(pds: &Pds, agent_id: &str) -> String { - match pds.provision(ProvisionRequest::new(agent_id, None)) { +fn account(pds: &Pds, account_id: &str) -> String { + match pds.provision(ProvisionRequest::new(account_id, None)) { Ok(provisioned) => provisioned.account.did.as_str().to_owned(), Err(err) => panic!("provisioning should succeed: {err}"), } diff --git a/crates/didbot-pds/tests/server.rs b/crates/didbot-pds/tests/server.rs index a9f63da7..7cb94503 100644 --- a/crates/didbot-pds/tests/server.rs +++ b/crates/didbot-pds/tests/server.rs @@ -18,8 +18,8 @@ fn zone() -> Zone { Zone::delegated("localhost", ZONE_HOST).expect("test zone should be constructible") } -fn request(agent_id: &str) -> ProvisionRequest { - ProvisionRequest::new(agent_id, None) +fn request(account_id: &str) -> ProvisionRequest { + ProvisionRequest::new(account_id, None) } fn memory() -> Provisioner { diff --git a/crates/didbot-pds/tests/store_cost.rs b/crates/didbot-pds/tests/store_cost.rs index 9a87be18..77962d8b 100644 --- a/crates/didbot-pds/tests/store_cost.rs +++ b/crates/didbot-pds/tests/store_cost.rs @@ -34,10 +34,10 @@ use std::alloc::{GlobalAlloc, Layout, System}; use std::sync::atomic::{AtomicUsize, Ordering}; -use didbot_identity::{AgentDid, Zone, ZoneRegistry}; +use didbot_identity::{AccountDid, Zone, ZoneRegistry}; use didbot_key::SigningKey; use didbot_pds::{ - AccountKind, AccountState, AccountStore, AgentAccount, HostedDid, MemoryAccountStore, + AccountKind, AccountState, AccountStore, HostedAccount, HostedDid, MemoryAccountStore, MemoryRecordStore, NameProvenance, Precondition, ProvisionRequest, Provisioner, RecordStore, Registry, Retention, Swap, }; @@ -156,15 +156,15 @@ fn thing(text: &str) -> serde_json::Value { } /// An account as the store holds one, without provisioning it. -fn account_at(index: usize) -> AgentAccount { +fn account_at(index: usize) -> HostedAccount { let host = format!("agent{index}.agents.localhost"); - AgentAccount { + HostedAccount { did: HostedDid::host( - AgentDid::parse(&format!("did:web:{host}")).expect("a did"), + AccountDid::parse(&format!("did:web:{host}")).expect("a did"), &ZoneRegistry::single(Zone::new("agents.localhost").expect("a zone")), ) .expect("hosted"), - agent_id: format!("agent{index}"), + account_id: format!("agent{index}"), handle: Some(host), harness: None, agent_type: None, diff --git a/crates/didbot-policy-source/src/poll.rs b/crates/didbot-policy-source/src/poll.rs index bb81f22e..f9335bc6 100644 --- a/crates/didbot-policy-source/src/poll.rs +++ b/crates/didbot-policy-source/src/poll.rs @@ -76,7 +76,7 @@ use std::collections::{HashMap, HashSet}; use std::sync::{Arc, Mutex}; use std::time::Duration as StdDuration; -use didbot_identity::AgentDid; +use didbot_identity::AccountDid; use serde::Deserialize; use tracing::warn; @@ -441,7 +441,7 @@ pub(crate) async fn resolve_pds_endpoint( client: &reqwest::Client, operator_did: &str, ) -> Result { - let did = AgentDid::parse(operator_did).map_err(|error| { + let did = AccountDid::parse(operator_did).map_err(|error| { PollError::UnresolvableOperator(format!( "cannot resolve operator DID {operator_did}: {error} \ (did:plc operator DIDs are not supported by this poll)" diff --git a/crates/didbot-repo/tests/did_document.rs b/crates/didbot-repo/tests/did_document.rs index 3db5d2ac..1b0e1ef9 100644 --- a/crates/didbot-repo/tests/did_document.rs +++ b/crates/didbot-repo/tests/did_document.rs @@ -12,7 +12,7 @@ //! `crates/didbot-serve/tests` runs the same shape one step further out, //! over HTTP against a live server's `/.well-known/did.json`. -use didbot_identity::{AgentDid, DidDocument}; +use didbot_identity::{AccountDid, DidDocument}; use didbot_key::{SigningKey, VerifyingKey}; use didbot_repo::{Builder, CommitError, Repository}; @@ -23,7 +23,7 @@ const DID: &str = "did:web:kestrel.agents.localhost%3A4100"; /// signed it, then throws the private key away. fn account() -> (Repository, String) { let key = SigningKey::generate(); - let did = AgentDid::parse(DID).expect("a did this deployment could mint"); + let did = AccountDid::parse(DID).expect("a did this deployment could mint"); let document = DidDocument::for_account( &did, &key.verifying_key().to_multibase(), diff --git a/crates/didbot-serve/src/auth.rs b/crates/didbot-serve/src/auth.rs index 18cc8fe1..8f5fc7df 100644 --- a/crates/didbot-serve/src/auth.rs +++ b/crates/didbot-serve/src/auth.rs @@ -19,7 +19,7 @@ //! "Why every public route is public" below for the three separate reasons //! a route ends up in this table with this credential, only one of which //! [`Credential::Disclosure`] now replaces. -//! - [`Credential::AgentToken`] — `com.atproto.repo.*`'s write routes and +//! - [`Credential::AccountToken`] — `com.atproto.repo.*`'s write routes and //! `uploadBlob`. Minted once, at provisioning, bound to one DID; see //! `didbot_pds::credential`. Not a legacy session and not OAuth: an agent //! has no person to paste an app password in and no authorization server @@ -28,7 +28,7 @@ //! authenticated DID must equal the repository a write names — see //! [`crate::error::ApiError::repo_mismatch`] — so a token from one //! account can never write another's repository. -//! - [`Credential::Attested`] — `provisionAgent` alone. The credential is +//! - [`Credential::Attested`] — `createAccount` alone. The credential is //! the attestation claim in the request body, signed with the node key of //! the host asking, not anything in a header: there is no account yet for //! a token to name. [`require_attestation`] refuses a request carrying @@ -43,12 +43,12 @@ //! //! Its own variant rather than [`Credential::Public`], on exactly the //! lesson finding 02 taught: a route labelled `Public` reads as "no -//! credential and that is fine", and `deleteAgent` and `setAgentPinned` -//! once inherited that reading along with the label. `provisionAgent` +//! credential and that is fine", and `deleteAccount` and `setAccountPinned` +//! once inherited that reading along with the label. `createAccount` //! takes a credential; it is just not one an `Authorization`-header //! extractor can resolve. -//! - [`Credential::Disclosure`] — `listAgents`, `listAgentLedgers`, -//! `getAgentLedger` and `stats`: disclosure, public by default and +//! - [`Credential::Disclosure`] — `listAccounts`, `listAccountLedgers`, +//! `getAccountLedger` and `stats`: disclosure, public by default and //! narrowable per route by this deployment's configuration, resolved by //! [`require_disclosure`]. See [`Disclosure`] for the reasoning this //! default rests on and the shape the toggle takes. @@ -108,7 +108,7 @@ //! project rests on collapses. //! - **Public by default, on purpose, and narrowable.** //! [`Credential::Disclosure`] — see [`Disclosure`] — which is what -//! `bot.did.listAgents`, `listAgentLedgers`, `getAgentLedger` and `stats` +//! `bot.did.listAccounts`, `listAccountLedgers`, `getAccountLedger` and `stats` //! now are. This is the tier finding 02 actually found: not "public //! because a relay needs it" and not "public because the alternative is //! incoherent", but public because nobody had written the reasoning down @@ -136,19 +136,19 @@ pub enum Credential { /// No principal required. A deliberate decision, not an absence of one. Public, /// `didbot_pds::credential`'s bearer token, minted at provisioning. - AgentToken, - /// An agent's own [`Credential::AgentToken`], resolved to the account it + AccountToken, + /// An agent's own [`Credential::AccountToken`], resolved to the account it /// authenticates as rather than to a repository path. /// /// Two postures, and which one a route takes depends on whether the /// request names an account at all. /// /// **Checked against the account the request names.** Every - /// account-lifecycle mutation: `activateAgent`, `deactivateAgent`, - /// `deleteAgent`, `freezeAgent`, `setAgentPinned` and `unfreezeAgent`. + /// account-lifecycle mutation: `activateAccount`, `deactivateAccount`, + /// `deleteAccount`, `freezeAccount`, `setAccountPinned` and `unfreezeAccount`. /// The request carries a DID and the token must authenticate as that /// same account — see `auth::require_self`. A client calls them carrying - /// the credential `provisionAgent` handed back, and an agent tearing + /// the credential `createAccount` handed back, and an agent tearing /// down or toggling *itself* is the only caller this server can /// authenticate for them. /// @@ -163,8 +163,8 @@ pub enum Credential { /// /// Either way, there is no operator credential to carve an exception /// for; see the module doc. - AgentSelf, - /// `provisionAgent` alone: the attestation claim in the request body, + AccountSelf, + /// `createAccount` alone: the attestation claim in the request body, /// signed with the node key of an admitted host. /// /// `require_attestation` is the half this module resolves — that a @@ -173,8 +173,8 @@ pub enum Credential { /// the parent link the claim establishes. See the module doc. Attested, /// Public by default, and narrowable by this deployment's own - /// configuration to operator-only — never the reverse. `listAgents`, - /// `listAgentLedgers`, `getAgentLedger` and `stats`: disclosure rather + /// configuration to operator-only — never the reverse. `listAccounts`, + /// `listAccountLedgers`, `getAccountLedger` and `stats`: disclosure rather /// than mutation, and this project's accountability story rather than an /// accident — see [`Disclosure`] and `require_disclosure` for the reasoning /// and the mechanism, and `plan/auth-types.md`'s "publish by default" for @@ -188,7 +188,7 @@ pub enum Credential { /// /// # Why public is the default /// -/// `listAgents`, `listAgentLedgers`, `getAgentLedger` and `stats` say which +/// `listAccounts`, `listAccountLedgers`, `getAccountLedger` and `stats` say which /// agents this deployment runs and what happened to them. Publishing that is /// this project's accountability story, not an afterthought: `plan/ownership.md` /// exists so a stranger can ask whether an agent traces to a responsible @@ -223,7 +223,7 @@ pub enum Credential { /// /// The threat this default exists to resist is an operator who cannot be /// audited, and the configuration tier that closes a route is a tier an -/// on-box attacker also reaches — so closing `listAgents` is exactly what +/// on-box attacker also reaches — so closing `listAccounts` is exactly what /// that attacker would do to hide what a stranger needed to see. This /// cannot be prevented from here, but it is made visible: /// `auth::require_disclosure` answers a closed route with `DisclosureDisabled`, @@ -234,7 +234,7 @@ pub enum Credential { pub struct Disclosure { /// Whether a stranger may read which accounts this deployment holds. /// - /// Every route that names them alike, not `bot.did.listAgents` on its + /// Every route that names them alike, not `bot.did.listAccounts` on its /// own: `com.atproto.sync.listRepos` pages through the same accounts, /// and `com.atproto.sync.subscribeRepos` announces each one as it /// appears and as it writes. Closing one and leaving the others would @@ -246,11 +246,11 @@ pub struct Disclosure { /// deployment that refuses it is not indexed — the trade /// `plan/spaces.md` names as "private means federation off", taken at /// this grain. - pub list_agents: bool, - /// `bot.did.listAgentLedgers`. - pub list_agent_ledgers: bool, - /// `bot.did.getAgentLedger`. - pub get_agent_ledger: bool, + pub list_accounts: bool, + /// `bot.did.listAccountLedgers`. + pub list_account_ledgers: bool, + /// `bot.did.getAccountLedger`. + pub get_account_ledger: bool, /// `bot.did.stats`. pub stats: bool, /// `bot.did.listReservations`. @@ -263,9 +263,9 @@ impl Default for Disclosure { /// rather than closed. fn default() -> Self { Self { - list_agents: true, - list_agent_ledgers: true, - get_agent_ledger: true, + list_accounts: true, + list_account_ledgers: true, + get_account_ledger: true, stats: true, list_reservations: true, } @@ -438,7 +438,7 @@ fn dpop_bearer(headers: &HeaderMap) -> Option<(String, &str)> { Some((rest.trim().to_owned(), proof)) } -/// Resolves either an agent's own [`Credential::AgentToken`] or a DPoP-bound +/// Resolves either an agent's own [`Credential::AccountToken`] or a DPoP-bound /// OAuth access token minted by `crate::oauth::token`, for /// `com.atproto.repo.*`'s write routes and `uploadBlob` — the resource path /// an app authorized over OAuth actually writes through. @@ -728,7 +728,7 @@ pub fn require_disclosure(open: bool, route: &str) -> Result<(), ApiError> { Err(disclosure_disabled(route)) } -/// Resolves [`Credential::Attested`]'s first half: that `provisionAgent`'s +/// Resolves [`Credential::Attested`]'s first half: that `createAccount`'s /// body carries a claim for the registry to verify. /// /// `unattested_provisioning` is [`AuthState::unattested_provisioning`]. Off, @@ -918,8 +918,8 @@ mod tests { /// specifically no `Operator` one, reaches past the toggle. #[test] fn a_closed_disclosure_route_refuses_every_caller() { - assert!(require_disclosure(true, "bot.did.listAgents").is_ok()); - let err = require_disclosure(false, "bot.did.listAgents").unwrap_err(); + assert!(require_disclosure(true, "bot.did.listAccounts").is_ok()); + let err = require_disclosure(false, "bot.did.listAccounts").unwrap_err(); assert_eq!(err.error(), "DisclosureDisabled"); assert_eq!(err.status(), StatusCode::FORBIDDEN); } @@ -930,8 +930,8 @@ mod tests { /// back has to change this test to do it. #[test] fn hard_delete_has_no_route_and_no_declared_credential() { - assert!(declared("bot.did.hardDeleteAgent").is_none()); - assert!(!crate::routes::BOT_DID_METHODS.contains(&"bot.did.hardDeleteAgent")); + assert!(declared("bot.did.hardDeleteAccount").is_none()); + assert!(!crate::routes::BOT_DID_METHODS.contains(&"bot.did.hardDeleteAccount")); } #[test] @@ -939,9 +939,9 @@ mod tests { assert_eq!( Disclosure::default(), Disclosure { - list_agents: true, - list_agent_ledgers: true, - get_agent_ledger: true, + list_accounts: true, + list_account_ledgers: true, + get_account_ledger: true, stats: true, list_reservations: true, } @@ -985,9 +985,9 @@ mod tests { /// refused by name before the registry sees it, and a claim goes /// through whether or not the development toggle is set. #[test] - fn provision_agent_requires_a_claim_unless_the_deployment_says_otherwise() { + fn create_account_requires_a_claim_unless_the_deployment_says_otherwise() { assert_eq!( - declared("bot.did.provisionAgent"), + declared("bot.did.createAccount"), Some(Credential::Attested) ); let claim = didbot_attest::AttestationClaim::new( diff --git a/crates/didbot-serve/src/bin/didbot-pds.rs b/crates/didbot-serve/src/bin/didbot-pds.rs index 4063d4f1..0999f26b 100644 --- a/crates/didbot-serve/src/bin/didbot-pds.rs +++ b/crates/didbot-serve/src/bin/didbot-pds.rs @@ -17,7 +17,7 @@ //! //! # What authorizes a provisioning request //! -//! `bot.did.provisionAgent` mints only under a claim signed with the node key +//! `bot.did.createAccount` mints only under a claim signed with the node key //! of a host this server has admitted; see `didbot_pds::Provisioner`. Under //! `.localhost` it also mints for a request carrying no claim, because no //! operator poll runs there to admit a host. @@ -44,7 +44,7 @@ use didbot_pds::policy_tree::{build_tree, TreePolicyGate}; use didbot_pds::write_log::{FileWriteLog, NullWriteSink, WriteSink}; use didbot_pds::Estop; use didbot_pds::{ - AccountStore, AgentTokenStore, BlobCollect, BlobLimits, BlobStore, CommitStore, Durable, + AccountStore, AccountTokenStore, BlobCollect, BlobLimits, BlobStore, CommitStore, Durable, LedgerStore, MemoryAccountStore, MemoryBlobStore, Naming, Provisioner, RecordStore, Registry, RepoEventSink, ReservationPolicy, ServerLifecycle, StaleSweep, Trust, DEFAULT_GRACE_WINDOW, DEFAULT_HOLD, @@ -182,7 +182,7 @@ struct Args { /// empty and `--close-disclosure` only ever narrows it. disclosure: Disclosure, /// Whether `X-Forwarded-For`/`X-Real-IP` may stand in for the caller's - /// real address in `provisionAgent`'s, `par`'s and `authorize`'s rate + /// real address in `createAccount`'s, `par`'s and `authorize`'s rate /// limiters. /// `false` unless `--trust-forwarded-headers` is given — see /// `didbot_serve::rate_limit`'s own doc for why trusting a @@ -445,7 +445,7 @@ usage: didbot-pds [options] . --max-accounts how many accounts this deployment may hold before - bot.did.provisionAgent refuses to mint another (default + bot.did.createAccount refuses to mint another (default 1000; see plan/capacity.md). --admission-depth how many parent links a claim's chain may cross before it @@ -468,7 +468,7 @@ usage: didbot-pds [options] back. --trust-forwarded-headers trust `X-Forwarded-For`/`X-Real-IP` as the caller's - address for the `provisionAgent`, `par` and `authorize` + address for the `createAccount`, `par` and `authorize` rate limiters, instead of the real TCP peer address. The rightmost `X-Forwarded-For` element is read, which is the one the proxy appended. Off by default: only turn this on @@ -553,14 +553,14 @@ fn apply_disclosure_config(disclosure: &mut Disclosure, config: Option<&didbot_c let Some(section) = config.and_then(|c| c.disclosure.as_ref()) else { return; }; - if section.list_agents == Some(false) { - disclosure.list_agents = false; + if section.list_accounts == Some(false) { + disclosure.list_accounts = false; } - if section.list_agent_ledgers == Some(false) { - disclosure.list_agent_ledgers = false; + if section.list_account_ledgers == Some(false) { + disclosure.list_account_ledgers = false; } - if section.get_agent_ledger == Some(false) { - disclosure.get_agent_ledger = false; + if section.get_account_ledger == Some(false) { + disclosure.get_account_ledger = false; } if section.stats == Some(false) { disclosure.stats = false; @@ -869,9 +869,9 @@ fn resolve_relay_hostname(args: &Args, config: Option<&didbot_config::Config>) - fn close_disclosure(disclosure: &mut Disclosure, raw: &str) -> Result<(), String> { for route in raw.split(',').map(str::trim).filter(|r| !r.is_empty()) { match route { - "list-agents" => disclosure.list_agents = false, - "list-agent-ledgers" => disclosure.list_agent_ledgers = false, - "get-agent-ledger" => disclosure.get_agent_ledger = false, + "list-agents" => disclosure.list_accounts = false, + "list-agent-ledgers" => disclosure.list_account_ledgers = false, + "get-agent-ledger" => disclosure.get_account_ledger = false, "stats" => disclosure.stats = false, "list-reservations" => disclosure.list_reservations = false, other => { @@ -1634,7 +1634,7 @@ async fn run(mut args: Args) -> Result<(), String> { Some(durable.blobs()), Some(durable.ledger() as Arc), Some(durable.history() as Arc), - Some(durable.credentials() as Arc), + Some(durable.credentials() as Arc), repos.clone(), naming, args.avatar, @@ -1697,7 +1697,7 @@ async fn run(mut args: Args) -> Result<(), String> { // A `.localhost` run admits no host: there is no operator poll to find a // vouch (see below), so no node key ever enters the attestation verifier - // and a claim-only `provisionAgent` would refuse every request. It mints + // and a claim-only `createAccount` would refuse every request. It mints // for a claimless request instead -- a statement about the development // stack made once, here, the same way its `claimed` lifecycle is. A // real zone gets `AuthState::default`'s `false`, and nothing else in @@ -1963,7 +1963,7 @@ type Assembled = (Arc, Arc, Arc); /// /// `credentials` is the agent token store, and it follows `--data` for the /// same reason every other collaborator here does. With a data directory the -/// tokens `provisionAgent` hands out are written to the log and verify again +/// tokens `createAccount` hands out are written to the log and verify again /// after a restart; without one they live exactly as long as the process /// that minted them, which is the same span this run's accounts, records and /// commit heads live for. @@ -1977,7 +1977,7 @@ fn assemble( blobs: Option>, ledger: Option>, history: Option>, - credentials: Option>, + credentials: Option>, repos: Repos, naming: Option>, avatar: Option, @@ -2468,10 +2468,10 @@ fn banner( // The three routes that name accounts, printed while they answer. They // share one toggle, and a hint pointing at a route that answers // `DisclosureDisabled` is a hint a developer copies and gets nothing from. - if args.disclosure.list_agents { - println!(" curl {base}/xrpc/bot.did.listAgents"); + if args.disclosure.list_accounts { + println!(" curl {base}/xrpc/bot.did.listAccounts"); } else { - println!(" # bot.did.listAgents answers nobody:"); + println!(" # bot.did.listAccounts answers nobody:"); println!(" # this run was started with --close-disclosure list-agents"); } @@ -2481,8 +2481,8 @@ fn banner( // claimless one is minted. See this file's module documentation. println!(" # under .localhost, provisioning takes no attestation claim"); println!( - " curl -X POST {base}/xrpc/bot.did.provisionAgent \\\n \ - -H 'content-type: application/json' \\\n -d '{{\"agentId\":\"example\"}}'" + " curl -X POST {base}/xrpc/bot.did.createAccount \\\n \ + -H 'content-type: application/json' \\\n -d '{{\"accountId\":\"example\"}}'" ); } else { println!(" # provisioning takes a claim signed by an admitted host; see didbot-agentd"); @@ -3478,9 +3478,9 @@ mod config_tests { apply_disclosure_config(&mut disclosure, Some(&config)); assert!(!disclosure.stats); // Nothing else the file left silent was touched. - assert!(disclosure.list_agents); - assert!(disclosure.list_agent_ledgers); - assert!(disclosure.get_agent_ledger); + assert!(disclosure.list_accounts); + assert!(disclosure.list_account_ledgers); + assert!(disclosure.get_account_ledger); } #[test] diff --git a/crates/didbot-serve/src/blobs.rs b/crates/didbot-serve/src/blobs.rs index a6609edb..221e3e47 100644 --- a/crates/didbot-serve/src/blobs.rs +++ b/crates/didbot-serve/src/blobs.rs @@ -11,7 +11,7 @@ //! //! `com.atproto.repo.uploadBlob` takes no parameters. In atproto the account //! comes from the request's authentication, and now that this route has one -//! — see [`Credential::AgentToken`](crate::auth::Credential::AgentToken) — +//! — see [`Credential::AccountToken`](crate::auth::Credential::AccountToken) — //! that is exactly where it comes from: the DID //! [`auth::require_agent_token_or_dpop`] resolves *is* the repository, with no //! separate field to disagree with it. This used to arrive in a diff --git a/crates/didbot-serve/src/error.rs b/crates/didbot-serve/src/error.rs index ce2d86d8..ff1c9f7a 100644 --- a/crates/didbot-serve/src/error.rs +++ b/crates/didbot-serve/src/error.rs @@ -126,7 +126,7 @@ impl ApiError { ) } - /// `bot.did.provisionAgent` was asked to mint with no attestation claim + /// `bot.did.createAccount` was asked to mint with no attestation claim /// to check; see `crate::auth::require_attestation`. /// /// 403 rather than 401: the claim travels in the body, not in a header @@ -139,7 +139,7 @@ impl ApiError { Self::new( StatusCode::FORBIDDEN, "AttestationRequired", - "bot.did.provisionAgent takes a claim signed by the node key of an admitted \ + "bot.did.createAccount takes a claim signed by the node key of an admitted \ host, and the request carries none", ) } @@ -318,7 +318,7 @@ impl From<&ProvisionError> for ApiError { // the same position `RateLimitExceeded` and `ServerNotReady` are // in: no `com.atproto.*` lexicon has a name for this because // upstream mints a DID nobody asked for rather than one derived - // from a caller's `agentId`, so the collision cannot arise there. + // from a caller's `accountId`, so the collision cannot arise there. // `DuplicateCreate` is declared, but only by // `com.atproto.server.getAccountInviteCodes` and only about // invite codes; reusing it here would tell a client something @@ -344,8 +344,8 @@ impl From<&ProvisionError> for ApiError { // [`plan/account-types.md`](../../../plan/account-types.md) // keeps deliberately independent of `AccountState` because "the // two answer different questions". The caller is authorised -- - // `bot.did.setAgentPinned` takes the same `Credential::AgentSelf` - // `bot.did.deleteAgent` does, so the principal that is refused + // `bot.did.setAccountPinned` takes the same `Credential::AccountSelf` + // `bot.did.deleteAccount` does, so the principal that is refused // here holds the credential that lifts it, and the message // already says so ("unpin it first"). A well-formed request // colliding with state already here, which the caller can clear diff --git a/crates/didbot-serve/src/lib.rs b/crates/didbot-serve/src/lib.rs index f477752a..68ab6e88 100644 --- a/crates/didbot-serve/src/lib.rs +++ b/crates/didbot-serve/src/lib.rs @@ -27,14 +27,14 @@ //! | `GET /health` | liveness, and which zone this server mints under | //! | `GET /.well-known/did.json` | the DID document for the request's Host | //! | `GET /.well-known/atproto-did` | the DID of the account whose handle is the request's Host, 404 when nobody holds it | -//! | `POST /xrpc/bot.did.provisionAgent` | mint an account | +//! | `POST /xrpc/bot.did.createAccount` | mint an account | //! | `POST /xrpc/bot.did.reserveIdentity` | hold a name and a document for a host's key until an operator vouches for it | //! | `GET /xrpc/bot.did.listReservations` | the names waiting on one operator's vouch | -//! | `POST /xrpc/bot.did.deleteAgent` | erase one: data gone, identity kept | -//! | `POST /xrpc/bot.did.freezeAgent`, `unfreezeAgent` | hang or lift the account's own `frozen` lock | -//! | `POST /xrpc/bot.did.deactivateAgent`, `activateAgent` | hang or lift the account's own `deactivated` lock | -//! | `POST /xrpc/bot.did.setAgentPinned` | pin or unpin one | -//! | `GET /xrpc/bot.did.listAgents` | list them | +//! | `POST /xrpc/bot.did.deleteAccount` | erase one: data gone, identity kept | +//! | `POST /xrpc/bot.did.freezeAccount`, `unfreezeAccount` | hang or lift the account's own `frozen` lock | +//! | `POST /xrpc/bot.did.deactivateAccount`, `activateAccount` | hang or lift the account's own `deactivated` lock | +//! | `POST /xrpc/bot.did.setAccountPinned` | pin or unpin one | +//! | `GET /xrpc/bot.did.listAccounts` | list them | //! | `POST /xrpc/bot.did.pollOperatorClaim` | ask this server to look for its operator's claim now — no credential, because the nudge confers nothing; see `auth` | //! | `POST /xrpc/com.atproto.repo.createRecord` | write a record to a repository | //! | `POST /xrpc/com.atproto.repo.putRecord` | write one at a key you choose | @@ -49,7 +49,7 @@ //! Every `repo` above is an at-identifier: a DID or a handle this deployment //! issued. Which credential each route needs, if any, is //! [`auth::ROUTE_CREDENTIALS`] — the read surface stays `Public`, and every -//! `com.atproto.repo.*` write route takes the agent token `provisionAgent` +//! `com.atproto.repo.*` write route takes the agent token `createAccount` //! hands back; see that module's own doc for the full taxonomy. //! //! Failures are returned in atproto's error shape, `{"error":..,"message":..}`, @@ -111,10 +111,10 @@ pub use subscribe::{ DEFAULT_REPOS_CAPACITY, FUTURE_CURSOR, OUTDATED_CURSOR, }; pub use wire::{ - record_uri, refuse_skipped_validation, AgentSummary, ApplyWritesRequest, BlobsView, - CreateRecordRequest, CreateRecordResponse, DeleteRecordRequest, DescribeRepoQuery, - DescribeRepoResponse, DescribeServerResponse, DidRequest, GetRecordQuery, GetRecordResponse, - ListRecordsQuery, PolicyView, ProvisionAgentRequest, PutRecordRequest, RecordView, RepoWrite, + record_uri, refuse_skipped_validation, AccountSummary, ApplyWritesRequest, BlobsView, + CreateAccountRequest, CreateRecordRequest, CreateRecordResponse, DeleteRecordRequest, + DescribeRepoQuery, DescribeRepoResponse, DescribeServerResponse, DidRequest, GetRecordQuery, + GetRecordResponse, ListRecordsQuery, PolicyView, PutRecordRequest, RecordView, RepoWrite, SetPinnedRequest, StatsResponse, Swap, TallyView, WireRegistration, WriteResult, MAX_LIST_LIMIT, }; diff --git a/crates/didbot-serve/src/oauth/consent.rs b/crates/didbot-serve/src/oauth/consent.rs index d76a199a..ebde9b3f 100644 --- a/crates/didbot-serve/src/oauth/consent.rs +++ b/crates/didbot-serve/src/oauth/consent.rs @@ -6,7 +6,7 @@ //! one-time [`ConsentReference`] for it, the daemon reads the decision //! through `bot.did.listPendingAuthorizations` and answers it with //! `bot.did.approveAuthorization`, carrying that reference under -//! `Credential::AgentSelf`. The DID doing the approving is therefore the one +//! `Credential::AccountSelf`. The DID doing the approving is therefore the one //! the presented agent token authenticates as, never a value in the body. //! //! What this module builds is everything behind that route: diff --git a/crates/didbot-serve/src/operator_poll.rs b/crates/didbot-serve/src/operator_poll.rs index f04f30b5..4f2b4871 100644 --- a/crates/didbot-serve/src/operator_poll.rs +++ b/crates/didbot-serve/src/operator_poll.rs @@ -48,7 +48,7 @@ use std::time::Duration as StdDuration; use didbot_identity::{DidDocument, ResolveError}; use didbot_pds::{ - AccountState, AgentAccount, ClaimFetchError, ClaimSource, Estop, EstopCause, EstopMode, + AccountState, ClaimFetchError, ClaimSource, Estop, EstopCause, EstopMode, HostedAccount, OperatorClaim, OperatorRelation, OperatorTransition, Registry, ServerLifecycle, }; use serde::Deserialize; @@ -786,7 +786,7 @@ pub async fn refresh_host_vouches( resume_after: &mut Option, ) -> Vec { let server = registry.service_did(); - let mut hosts: Vec = match registry.children(&server) { + let mut hosts: Vec = match registry.children(&server) { Ok(children) => children .into_iter() .filter(|host| host.state == AccountState::Active && host.node_key.is_some()) diff --git a/crates/didbot-serve/src/rate_limit.rs b/crates/didbot-serve/src/rate_limit.rs index 92ef105b..f5c45c97 100644 --- a/crates/didbot-serve/src/rate_limit.rs +++ b/crates/didbot-serve/src/rate_limit.rs @@ -1,7 +1,7 @@ //! A minimal fixed-window rate limiter, shared by every anonymous route that //! does real work before it knows who is calling. //! -//! `bot.did.provisionAgent` and `POST /oauth/par` are the motivating cases: +//! `bot.did.createAccount` and `POST /oauth/par` are the motivating cases: //! the first mints a signing key and a hostname, the second fetches a //! document from a host the caller names, and both do it for a caller this //! server has not authenticated yet. This limiter is what stands in front of diff --git a/crates/didbot-serve/src/routes.rs b/crates/didbot-serve/src/routes.rs index a98c5e05..b89e1614 100644 --- a/crates/didbot-serve/src/routes.rs +++ b/crates/didbot-serve/src/routes.rs @@ -30,13 +30,13 @@ use crate::oauth::scope::Action; use crate::rate_limit::{caller_key, Peer, RateLimiter}; use crate::subscribe::Repos; use crate::wire::{ - record_uri, refuse_skipped_validation, repeated, require_known_lexicon, AgentSummary, - ApplyWritesRequest, CreateRecordRequest, CreateRecordResponse, DeleteRecordRequest, - DescribeRepoQuery, DescribeRepoResponse, DescribeServerResponse, DidRequest, GetRecordQuery, - GetRecordResponse, GetRepoQuery, LedgerQuery, ListRecordsQuery, ListReposQuery, - ProvisionAgentRequest, PutRecordRequest, RecordView, RepoWrite, ReservationSummary, - ReservationsQuery, ReserveIdentityRequest, ReserveIdentityResponse, SetPinnedRequest, - StatsResponse, SyncGetRecordQuery, WriteResult, + record_uri, refuse_skipped_validation, repeated, require_known_lexicon, AccountSummary, + ApplyWritesRequest, CreateAccountRequest, CreateRecordRequest, CreateRecordResponse, + DeleteRecordRequest, DescribeRepoQuery, DescribeRepoResponse, DescribeServerResponse, + DidRequest, GetRecordQuery, GetRecordResponse, GetRepoQuery, LedgerQuery, ListRecordsQuery, + ListReposQuery, PutRecordRequest, RecordView, RepoWrite, ReservationSummary, ReservationsQuery, + ReserveIdentityRequest, ReserveIdentityResponse, SetPinnedRequest, StatsResponse, + SyncGetRecordQuery, WriteResult, }; /// Session storage, the e-stop latch and the operator credential, bundled so @@ -78,7 +78,7 @@ pub struct AuthState { /// Defaults to in-memory stores and every policy hook refusing — see /// that type's own doc. pub oauth: crate::oauth::OAuthState, - /// Bounds `bot.did.provisionAgent` calls per caller address; see this + /// Bounds `bot.did.createAccount` calls per caller address; see this /// module's `PROVISION_RATE_LIMIT` and `crate::rate_limit`'s own doc for /// where that address comes from and /// [`AuthState::trust_forwarded_headers`] for when a caller-supplied @@ -90,7 +90,7 @@ pub struct AuthState { /// `Credential::Attested` checks is verified only after this budget, so /// it needs the same address-keyed budget at least as much as either. pub provision_rate_limiter: Arc, - /// Whether `bot.did.provisionAgent` mints for a request carrying no + /// Whether `bot.did.createAccount` mints for a request carrying no /// attestation claim; see `auth::require_attestation`. /// /// `false` unless a deployment turns it on: a public deployment @@ -99,8 +99,8 @@ pub struct AuthState { /// is ever admitted, and nowhere else. pub unattested_provisioning: bool, /// How many accounts this deployment may hold before - /// `bot.did.provisionAgent` refuses to mint another; see - /// this module's `DEFAULT_MAX_ACCOUNTS` and `provision_agent`. + /// `bot.did.createAccount` refuses to mint another; see + /// this module's `DEFAULT_MAX_ACCOUNTS` and `create_account`. /// /// Checked against [`didbot_pds::RegistryStats::accounts`] — the same /// count `crate::health`'s periodic line already reports — rather than a @@ -109,7 +109,7 @@ pub struct AuthState { pub max_accounts: u64, /// Whether this deployment answers only callers on this host. /// - /// A `.localhost` zone is a development run: `provisionAgent` mints with + /// A `.localhost` zone is a development run: `createAccount` mints with /// no attestation claim there, and without a `--data` directory every /// blob is held in memory — so a run reachable from the network hands /// both to anybody sharing it. The listener binds every address because @@ -240,7 +240,7 @@ pub(crate) struct AppState { pub(crate) nudge: Arc, } -/// How many `bot.did.provisionAgent` calls one caller address may make per +/// How many `bot.did.createAccount` calls one caller address may make per /// [`PROVISION_RATE_WINDOW`]. /// /// Sized against a legitimate caller rather than against a read route: @@ -361,20 +361,20 @@ xrpc_methods! { /// `Public` on purpose: protocol-required for `com.atproto.sync.*` and /// `com.atproto.repo.*`'s reads, structurally required for /// `describeServer`. The write half takes the agent - /// token `provisionAgent` hands back, and `routes::write_record` / + /// token `createAccount` hands back, and `routes::write_record` / /// `routes::apply_writes` are where the authenticated DID is checked /// against the repository a write names. ATPROTO_CREDENTIALS, atproto_routes, { - "com.atproto.repo.applyWrites" => post(apply_writes) as AgentToken, - "com.atproto.repo.createRecord" => post(create_record) as AgentToken, - "com.atproto.repo.deleteRecord" => post(delete_record) as AgentToken, + "com.atproto.repo.applyWrites" => post(apply_writes) as AccountToken, + "com.atproto.repo.createRecord" => post(create_record) as AccountToken, + "com.atproto.repo.deleteRecord" => post(delete_record) as AccountToken, "com.atproto.repo.describeRepo" => get(describe_repo) as Public, "com.atproto.repo.getRecord" => get(get_record) as Public, "com.atproto.repo.listRecords" => get(list_records) as Public, - "com.atproto.repo.putRecord" => post(put_record) as AgentToken, - "com.atproto.repo.uploadBlob" => post(crate::blobs::upload_blob) as AgentToken, + "com.atproto.repo.putRecord" => post(put_record) as AccountToken, + "com.atproto.repo.uploadBlob" => post(crate::blobs::upload_blob) as AccountToken, "com.atproto.server.describeServer" => get(describe_server) as Public, "com.atproto.sync.getBlob" => get(crate::blobs::get_blob) as Public, "com.atproto.sync.getBlocks" => get(get_blocks) as Public, @@ -396,7 +396,7 @@ xrpc_methods! { /// not vendored anywhere, so `crates/didbot/tests/conformance/wire.rs`'s /// "every route is a vendored method" check must not see them. /// - /// `bot.did.hardDeleteAgent` is absent on purpose. It frees a burned + /// `bot.did.hardDeleteAccount` is absent on purpose. It frees a burned /// name, so self-service is exactly wrong for it, and this server has no /// credential that authenticates an administrator — see /// the `auth` module's documentation. `Registry::hard_delete` is still the @@ -405,12 +405,12 @@ xrpc_methods! { BOT_DID_METHODS, /// The credential each of those methods accepts. /// - /// `provisionAgent` is `Credential::Attested`: there is no + /// `createAccount` is `Credential::Attested`: there is no /// account yet for an agent token to authenticate as, so its credential /// is the `ProvisionRequest`'s attestation claim, checked inside the /// registry rather than by an `Authorization`-header extractor. Every /// account-lifecycle mutation names an account that already exists and - /// so takes `Credential::AgentSelf` — an agent acting on + /// so takes `Credential::AccountSelf` — an agent acting on /// *itself*, the only caller this server can authenticate for them. The /// four authorization-decision routes take it for the same reason from /// the other side: they name no account at all, and the account they act @@ -420,7 +420,7 @@ xrpc_methods! { /// The four reads are `Credential::Disclosure`: public by /// default, narrowable per route, never widenable. `pollOperatorClaim` /// is `Public` because it confers nothing — see the `auth` module's doc. - /// `reserveIdentity` is `Public` for the reason `provisionAgent` has + /// `reserveIdentity` is `Public` for the reason `createAccount` has /// nothing to check: the caller has a key and no account, and what a /// reservation confers is a name that serves nothing until an operator /// vouches for it; the route is bounded per address and by the queue's @@ -428,24 +428,24 @@ xrpc_methods! { BOT_DID_CREDENTIALS, bot_did_routes, { - "bot.did.activateAgent" => post(activate_agent) as AgentSelf, - "bot.did.approveAuthorization" => post(approve_authorization) as AgentSelf, - "bot.did.deactivateAgent" => post(deactivate_agent) as AgentSelf, - "bot.did.declineAuthorization" => post(decline_authorization) as AgentSelf, - "bot.did.deleteAgent" => post(delete_agent) as AgentSelf, - "bot.did.freezeAgent" => post(freeze_agent) as AgentSelf, - "bot.did.getAgentLedger" => get(get_agent_ledger) as Disclosure, - "bot.did.getAuthorization" => get(get_authorization) as AgentSelf, - "bot.did.listAgentLedgers" => get(list_ledgers) as Disclosure, - "bot.did.listAgents" => get(list_agents) as Disclosure, - "bot.did.listPendingAuthorizations" => get(list_pending_authorizations) as AgentSelf, + "bot.did.activateAccount" => post(activate_account) as AccountSelf, + "bot.did.approveAuthorization" => post(approve_authorization) as AccountSelf, + "bot.did.deactivateAccount" => post(deactivate_account) as AccountSelf, + "bot.did.declineAuthorization" => post(decline_authorization) as AccountSelf, + "bot.did.deleteAccount" => post(delete_account) as AccountSelf, + "bot.did.freezeAccount" => post(freeze_account) as AccountSelf, + "bot.did.getAccountLedger" => get(get_account_ledger) as Disclosure, + "bot.did.getAuthorization" => get(get_authorization) as AccountSelf, + "bot.did.listAccountLedgers" => get(list_ledgers) as Disclosure, + "bot.did.listAccounts" => get(list_accounts) as Disclosure, + "bot.did.listPendingAuthorizations" => get(list_pending_authorizations) as AccountSelf, "bot.did.listReservations" => get(list_reservations) as Disclosure, "bot.did.pollOperatorClaim" => post(poll_operator_claim) as Public, - "bot.did.provisionAgent" => post(provision_agent) as Attested, + "bot.did.createAccount" => post(create_account) as Attested, "bot.did.reserveIdentity" => post(reserve_identity) as Public, - "bot.did.setAgentPinned" => post(set_pinned) as AgentSelf, + "bot.did.setAccountPinned" => post(set_pinned) as AccountSelf, "bot.did.stats" => get(stats) as Disclosure, - "bot.did.unfreezeAgent" => post(unfreeze_agent) as AgentSelf, + "bot.did.unfreezeAccount" => post(unfreeze_account) as AccountSelf, } } @@ -860,7 +860,7 @@ pub(crate) fn blocking_task_failed(route: &str, error: tokio::task::JoinError) - /// seconds of CPU with no await in it, and the deployment in `infra/pds` /// runs two async workers, so two such reads left inline are every other /// route waiting. `spawn_blocking` is the same seam `getBlob` and -/// `provisionAgent` already cross. +/// `createAccount` already cross. /// /// `route` names the method in the log line a panicking task leaves; see /// [`blocking_task_failed`]. @@ -1407,7 +1407,7 @@ fn authorize_error_response(err: crate::oauth::authorize::AuthorizeError) -> Res /// at every use (`data-approves`, `data-ceiling-checked`). /// /// There is no form. Approving is `bot.did.approveAuthorization`, which -/// takes the account's own agent token as `Credential::AgentSelf` — a +/// takes the account's own agent token as `Credential::AccountSelf` — a /// credential an HTML form cannot carry, so a form here could only post to /// something that would refuse it. A browser-driven client approving on the /// agent's behalf reads the one-time reference from @@ -1601,7 +1601,7 @@ struct GetAuthorizationQuery { /// `GET /xrpc/bot.did.listPendingAuthorizations` /// -/// `Credential::AgentSelf`, used as identity rather than as a comparison: +/// `Credential::AccountSelf`, used as identity rather than as a comparison: /// this route names no account, and the decisions it lists are the ones /// addressed to the account the presented agent token authenticates as. An /// agent cannot ask for anybody else's, because there is nowhere in the @@ -1674,7 +1674,7 @@ fn pending_authorizations( /// `GET /xrpc/bot.did.getAuthorization` /// -/// One decision by the pushed request it decides. `Credential::AgentSelf`: +/// One decision by the pushed request it decides. `Credential::AccountSelf`: /// answered only when the decision is addressed to the account the presented /// token authenticates as, and answered as "not found" otherwise, so this is /// not a way to learn that somebody else has a sign-in in flight. @@ -1733,7 +1733,7 @@ fn decision_by_token( /// `POST /xrpc/bot.did.approveAuthorization` /// -/// The one way an authorization is approved. `Credential::AgentSelf`: the +/// The one way an authorization is approved. `Credential::AccountSelf`: the /// agent presents its own token, and the decision it approves must be the /// one addressed to that account — `plan/oauth.md`'s "the account is /// checked server-side, not by the daemon", enforced by the credential @@ -1828,7 +1828,7 @@ async fn approve_authorization( /// `POST /xrpc/bot.did.declineAuthorization` /// /// An agent's "no", recorded rather than left to look like an expiry. -/// `Credential::AgentSelf`, the same posture as [`approve_authorization`]. +/// `Credential::AccountSelf`, the same posture as [`approve_authorization`]. /// The approval token is spent either way, so a declined request cannot be /// approved afterwards. async fn decline_authorization( @@ -2043,7 +2043,7 @@ fn too_many_subscribers(route: &str) -> Response { .into_response() } -/// `POST /xrpc/bot.did.provisionAgent` +/// `POST /xrpc/bot.did.createAccount` /// /// One of the e-stop's two issuance gates alongside session creation: a /// Pause or a Revoke refuses a new account the same way it refuses a new @@ -2063,7 +2063,7 @@ fn too_many_subscribers(route: &str) -> Response { /// rate limiters; see also `plan/auth-types.md`'s note that the operator /// surface is kept separate rather than folded into every admin-shaped /// route. -async fn provision_agent( +async fn create_account( State(state): State, peer: Peer, headers: HeaderMap, @@ -2083,7 +2083,7 @@ async fn provision_agent( if let Err(halted) = state.estop.check_issue(EstopRefusal::Account) { return ApiError::from(halted).into_response(); } - if let Err(err) = require_lifecycle(&state, |p| p.provisions_accounts, "bot.did.provisionAgent") + if let Err(err) = require_lifecycle(&state, |p| p.provisions_accounts, "bot.did.createAccount") { return err.into_response(); } @@ -2103,7 +2103,7 @@ async fn provision_agent( info!("provisioning refused: this deployment is at its account cap"); return err.into_response(); } - let request: ProvisionAgentRequest = match parse_body(&body) { + let request: CreateAccountRequest = match parse_body(&body) { Ok(request) => request, Err(err) => return err.into_response(), }; @@ -2111,12 +2111,12 @@ async fn provision_agent( auth::require_attestation(state.unattested_provisioning, request.attestation.as_ref()) { info!( - agent_id = request.agent_id, + account_id = request.account_id, "provisioning refused: no attestation claim" ); return err.into_response(); } - let agent_id = request.agent_id.clone(); + let account_id = request.account_id.clone(); let request = match request.into_request() { Ok(request) => request, Err(err) => return err.into_response(), @@ -2140,16 +2140,16 @@ async fn provision_agent( // The only copy of this token that will ever leave this // process; see `didbot_pds::credential`. A caller that does not // keep it has to provision again to get another. - "agentToken": provisioned.agent_token, + "accountToken": provisioned.account_token, })) .into_response(), Ok(Err(err)) => { // Logged with the agent id rather than the claim: the claim is // evidence, and evidence does not belong in a log line. - info!(agent_id, error = %err, "provisioning refused"); + info!(account_id, error = %err, "provisioning refused"); ApiError::from(&err).into_response() } - Err(join_err) => blocking_task_failed("bot.did.provisionAgent", join_err).into_response(), + Err(join_err) => blocking_task_failed("bot.did.createAccount", join_err).into_response(), } } @@ -2161,12 +2161,12 @@ async fn provision_agent( /// arrives before anybody has vouched for it" and /// [`didbot_pds::Registry::reserve`]. /// -/// Gated the way `provisionAgent` is — the e-stop, the lifecycle, the +/// Gated the way `createAccount` is — the e-stop, the lifecycle, the /// account cap — because a reservation takes a name, publishes a hostname /// and holds a row, which is provisioning. Then bounded per caller address, /// before the body is read: the queue behind this route is finite and /// shared, and this is what keeps one address from spending it. The work -/// itself runs off the async worker for the reason `provision_agent`'s +/// itself runs off the async worker for the reason `create_account`'s /// does: a DNS publication is inside it. async fn reserve_identity( State(state): State, @@ -2224,9 +2224,9 @@ async fn reserve_identity( } } -/// `POST /xrpc/bot.did.deleteAgent` +/// `POST /xrpc/bot.did.deleteAccount` /// -/// `Credential::AgentSelf`: a harness client calls this directly to erase an +/// `Credential::AccountSelf`: a harness client calls this directly to erase an /// account at the end of a session, carrying that account's own write /// credential — the self-service case [`auth::require_self`] exists for, /// and the only case, since this server holds no credential that could @@ -2235,7 +2235,7 @@ async fn reserve_identity( /// its key and the account's row stay, `decommissioned`, so every signature /// the agent made stays checkable. Refused with 403 `AccountLocked` while /// any lock hangs and 403 `AccountHeld` under an operator's hold. -async fn delete_agent( +async fn delete_account( State(state): State, headers: HeaderMap, XrpcBody(body): XrpcBody, @@ -2245,7 +2245,7 @@ async fn delete_agent( // its own account with a credential issued before the stop is // outstanding work: an operator who has pulled the brake has not // consented to a repository being emptied, a name being burned or a - // freeze being lifted while it is pulled. `bot.did.freezeAgent` is + // freeze being lifted while it is pulled. `bot.did.freezeAccount` is // deliberately not gated this way; see its own doc comment. if let Err(halted) = state.estop.check_use(EstopRefusal::Operation) { return ApiError::from(halted).into_response(); @@ -2258,7 +2258,7 @@ async fn delete_agent( if let Err(err) = auth::require_self(state.registry.as_ref(), &headers, &request.did) { return err.into_response(); } - // Off the async worker for the same reason as `provision_agent`: + // Off the async worker for the same reason as `create_account`: // `delete` takes the write lock and tears the repository down under it. let registry = state.registry.clone(); let did = request.did.clone(); @@ -2266,17 +2266,17 @@ async fn delete_agent( match outcome { Ok(Ok(())) => Json(json!({})).into_response(), Ok(Err(err)) => ApiError::from(&err).into_response(), - Err(join_err) => blocking_task_failed("bot.did.deleteAgent", join_err).into_response(), + Err(join_err) => blocking_task_failed("bot.did.deleteAccount", join_err).into_response(), } } -/// `POST /xrpc/bot.did.freezeAgent` +/// `POST /xrpc/bot.did.freezeAccount` /// -/// `Credential::AgentSelf`, the same posture as [`delete_agent`]: an agent +/// `Credential::AccountSelf`, the same posture as [`delete_account`]: an agent /// hangs [`Lock::Frozen`] on itself as [`Actor::Account`], and nothing else /// hangs a lock over HTTP. The repository stays readable; only writes are /// refused, with 403 `AccountNotWritable`. Reversible with -/// [`unfreeze_agent`], and only by the same party: an operator's own frozen +/// [`unfreeze_account`], and only by the same party: an operator's own frozen /// tag is not the account's to lift. /// /// The one route in this group that the e-stop does **not** gate. Every @@ -2285,7 +2285,7 @@ async fn delete_agent( /// it under a stop would leave a caller less able to restrain itself than /// before the operator pulled the brake. A gate that can only deny is safe /// to skip for an operation that can only deny. -async fn freeze_agent( +async fn freeze_account( State(state): State, headers: HeaderMap, XrpcBody(body): XrpcBody, @@ -2306,11 +2306,11 @@ async fn freeze_agent( } } -/// `POST /xrpc/bot.did.unfreezeAgent` +/// `POST /xrpc/bot.did.unfreezeAccount` /// -/// Reverses [`freeze_agent`]. Refused with 409 `AccountNotLocked` for an +/// Reverses [`freeze_account`]. Refused with 409 `AccountNotLocked` for an /// account the caller's party has not frozen. -async fn unfreeze_agent( +async fn unfreeze_account( State(state): State, headers: HeaderMap, XrpcBody(body): XrpcBody, @@ -2320,7 +2320,7 @@ async fn unfreeze_agent( // its own account with a credential issued before the stop is // outstanding work: an operator who has pulled the brake has not // consented to a repository being emptied, a name being burned or a - // freeze being lifted while it is pulled. `bot.did.freezeAgent` is + // freeze being lifted while it is pulled. `bot.did.freezeAccount` is // deliberately not gated this way; see its own doc comment. if let Err(halted) = state.estop.check_use(EstopRefusal::Operation) { return ApiError::from(halted).into_response(); @@ -2342,15 +2342,15 @@ async fn unfreeze_agent( } } -/// `POST /xrpc/bot.did.deactivateAgent` +/// `POST /xrpc/bot.did.deactivateAccount` /// -/// `Credential::AgentSelf`: an agent hangs [`Lock::Deactivated`] on itself. +/// `Credential::AccountSelf`: an agent hangs [`Lock::Deactivated`] on itself. /// Reads and writes are both refused while it hangs — a relay is told /// `active: false, status: deactivated` — and the document, the key and the -/// repository all stay, so [`activate_agent`] puts the account back exactly -/// as it was. Not gated by the e-stop, for the reason [`freeze_agent`] is +/// repository all stay, so [`activate_account`] puts the account back exactly +/// as it was. Not gated by the e-stop, for the reason [`freeze_account`] is /// not: it can only narrow. -async fn deactivate_agent( +async fn deactivate_account( State(state): State, headers: HeaderMap, XrpcBody(body): XrpcBody, @@ -2371,11 +2371,11 @@ async fn deactivate_agent( } } -/// `POST /xrpc/bot.did.activateAgent` +/// `POST /xrpc/bot.did.activateAccount` /// -/// Reverses [`deactivate_agent`]. Widens what the account may do, so the -/// e-stop gates it the way it gates [`unfreeze_agent`]. -async fn activate_agent( +/// Reverses [`deactivate_account`]. Widens what the account may do, so the +/// e-stop gates it the way it gates [`unfreeze_account`]. +async fn activate_account( State(state): State, headers: HeaderMap, XrpcBody(body): XrpcBody, @@ -2400,9 +2400,9 @@ async fn activate_agent( } } -/// `POST /xrpc/bot.did.setAgentPinned` +/// `POST /xrpc/bot.did.setAccountPinned` /// -/// `Credential::AgentSelf`, the same as [`delete_agent`]: an agent may pin +/// `Credential::AccountSelf`, the same as [`delete_account`]: an agent may pin /// or unpin itself, and nothing else may pin it over HTTP. async fn set_pinned( State(state): State, @@ -2414,7 +2414,7 @@ async fn set_pinned( // its own account with a credential issued before the stop is // outstanding work: an operator who has pulled the brake has not // consented to a repository being emptied, a name being burned or a - // freeze being lifted while it is pulled. `bot.did.freezeAgent` is + // freeze being lifted while it is pulled. `bot.did.freezeAccount` is // deliberately not gated this way; see its own doc comment. if let Err(halted) = state.estop.check_use(EstopRefusal::Operation) { return ApiError::from(halted).into_response(); @@ -2433,7 +2433,7 @@ async fn set_pinned( } } -/// `GET /xrpc/bot.did.listAgents` +/// `GET /xrpc/bot.did.listAccounts` /// /// `Credential::Disclosure`: open by default, this deployment's own roster /// of agents — see `auth::Disclosure` for why the default is public. Also @@ -2441,20 +2441,22 @@ async fn set_pinned( /// servers it does not administer, which is why closing it is this /// deployment's own call rather than one this crate can make for every /// deployment. -async fn list_agents(State(state): State) -> Response { - if let Err(err) = require_lifecycle(&state, |p| p.serves_reads, "bot.did.listAgents") { +async fn list_accounts(State(state): State) -> Response { + if let Err(err) = require_lifecycle(&state, |p| p.serves_reads, "bot.did.listAccounts") { return err.into_response(); } - if let Err(err) = auth::require_disclosure(state.disclosure.list_agents, "bot.did.listAgents") { + if let Err(err) = + auth::require_disclosure(state.disclosure.list_accounts, "bot.did.listAccounts") + { return err.into_response(); } - let agents: Vec = state + let accounts: Vec = state .registry .accounts() .iter() - .map(AgentSummary::from_account) + .map(AccountSummary::from_account) .collect(); - Json(json!({ "agents": agents })).into_response() + Json(json!({ "accounts": accounts })).into_response() } /// `GET /xrpc/bot.did.listReservations?operator=` @@ -2529,7 +2531,7 @@ fn resolve_repo(state: &AppState, repo: &str) -> Result { did.ok_or_else(|| ApiError::repo_not_found(repo)) } -/// `GET /xrpc/bot.did.getAgentLedger` +/// `GET /xrpc/bot.did.getAccountLedger` /// /// The bookkeeping for one agent, which is the one read here that deliberately /// answers about accounts that no longer exist. A 404 means this deployment @@ -2538,16 +2540,17 @@ fn resolve_repo(state: &AppState, repo: &str) -> Result { /// /// `Credential::Disclosure`: open by default, this deployment's own audit /// trail for one account — see `auth::Disclosure`. -async fn get_agent_ledger( +async fn get_account_ledger( State(state): State, query: Result, QueryRejection>, ) -> Response { - if let Err(err) = require_lifecycle(&state, |p| p.serves_reads, "bot.did.getAgentLedger") { + if let Err(err) = require_lifecycle(&state, |p| p.serves_reads, "bot.did.getAccountLedger") { return err.into_response(); } - if let Err(err) = - auth::require_disclosure(state.disclosure.get_agent_ledger, "bot.did.getAgentLedger") - { + if let Err(err) = auth::require_disclosure( + state.disclosure.get_account_ledger, + "bot.did.getAccountLedger", + ) { return err.into_response(); } let Query(query) = match query { @@ -2565,21 +2568,21 @@ async fn get_agent_ledger( } } -/// `GET /xrpc/bot.did.listAgentLedgers` +/// `GET /xrpc/bot.did.listAccountLedgers` /// /// Every agent this deployment has ever provisioned, which is longer than -/// `listAgents` by exactly the ones it has taken away again. +/// `listAccounts` by exactly the ones it has taken away again. /// -/// `Credential::Disclosure`, the same as [`get_agent_ledger`]: open by +/// `Credential::Disclosure`, the same as [`get_account_ledger`]: open by /// default, the audit trail for every account this deployment has ever /// held. async fn list_ledgers(State(state): State) -> Response { - if let Err(err) = require_lifecycle(&state, |p| p.serves_reads, "bot.did.listAgentLedgers") { + if let Err(err) = require_lifecycle(&state, |p| p.serves_reads, "bot.did.listAccountLedgers") { return err.into_response(); } if let Err(err) = auth::require_disclosure( - state.disclosure.list_agent_ledgers, - "bot.did.listAgentLedgers", + state.disclosure.list_account_ledgers, + "bot.did.listAccountLedgers", ) { return err.into_response(); } @@ -2855,7 +2858,7 @@ fn is_swap_failure(err: &didbot_pds::ProvisionError) -> bool { /// `ServerPolicy::serves_reads` is false only in `booting` and /// `provisioning`, which last a store read and a keypair mint respectively /// and during which no agent account can exist. So it is applied where an -/// absent or empty answer would be *misread* — `listAgents` answering `[]`, +/// absent or empty answer would be *misread* — `listAccounts` answering `[]`, /// `subscribeRepos` opening an empty stream, `did.json` answering `404` — /// and not on the per-repository reads, where the natural `RepoNotFound` is /// already the true answer and a `503` would say less. @@ -2877,7 +2880,7 @@ fn require_lifecycle( } /// Refuses a new account once this deployment already holds -/// [`AppState::max_accounts`] of them; see [`provision_agent`]. +/// [`AppState::max_accounts`] of them; see [`create_account`]. /// /// Counted from [`Registry::account_count`] rather than a purpose-built /// counter, so this and `crate::health`'s periodic line — which reads @@ -2919,7 +2922,7 @@ fn check_write_not_halted(state: &AppState) -> Result<(), ApiError> { /// can run it: the instant the request envelope names a repository. /// /// Resolves the repository, refuses a caller writing into one that is not -/// its own, and then refuses a repository whose [`didbot_pds::AgentAccount`] +/// its own, and then refuses a repository whose [`didbot_pds::HostedAccount`] /// policy does not accept writes. The store performs the last check again on the /// way past; this one exists so that the answer a frozen account gets is /// `AccountNotWritable` rather than whatever stage 4 would have said about @@ -3528,7 +3531,7 @@ fn sync_error(state: &AppState, err: &ProvisionError) -> ApiError { /// Why a `com.atproto.sync.*` route serves no repository for `did`. /// -/// Read through [`didbot_pds::AgentAccount::sync_status`], so a route names +/// Read through [`didbot_pds::HostedAccount::sync_status`], so a route names /// only a status `subscribeRepos` already announced. `deleted` has no error /// name in these lexicons, and a reserved or provisioning account has no /// announced status, so both are `RepoNotFound`. A suspended or deactivated @@ -3589,7 +3592,7 @@ async fn get_latest_commit( /// What `com.atproto.sync.subscribeRepos`'s `#account` says about one /// account, on request, with the `rev` a caller compares a relay's copy /// against. The `active` and `status` pair is read through -/// [`didbot_pds::AgentAccount::sync_status`], the mapping the stream +/// [`didbot_pds::HostedAccount::sync_status`], the mapping the stream /// announces from, so the two cannot disagree. An account that mapping /// announces nothing for — reserved, or still provisioning — is /// `RepoNotFound` here for the same reason it is silent there: a relay was @@ -3702,9 +3705,9 @@ async fn list_repos( query: Result, QueryRejection>, ) -> Response { // This route names every account this deployment holds, so it is the - // same decision `bot.did.listAgents` is; see `auth::Disclosure`. + // same decision `bot.did.listAccounts` is; see `auth::Disclosure`. if let Err(err) = - auth::require_disclosure(state.disclosure.list_agents, "com.atproto.sync.listRepos") + auth::require_disclosure(state.disclosure.list_accounts, "com.atproto.sync.listRepos") { return err.into_response(); } @@ -3764,10 +3767,10 @@ async fn subscribe_repos( return err.into_response(); } // Refused pre-upgrade for the same reason, and under the same decision - // `bot.did.listAgents` is: this stream announces each account as it + // `bot.did.listAccounts` is: this stream announces each account as it // appears and as it writes. See `auth::Disclosure`. if let Err(err) = auth::require_disclosure( - state.disclosure.list_agents, + state.disclosure.list_accounts, "com.atproto.sync.subscribeRepos", ) { return err.into_response(); diff --git a/crates/didbot-serve/src/tests.rs b/crates/didbot-serve/src/tests.rs index 1b85f99b..5c465441 100644 --- a/crates/didbot-serve/src/tests.rs +++ b/crates/didbot-serve/src/tests.rs @@ -15,11 +15,11 @@ use axum::body::{to_bytes, Body}; use axum::extract::connect_info::MockConnectInfo; use axum::http::{header, HeaderMap, Request, StatusCode}; use didbot_attest::{Assurance, Provenance}; -use didbot_identity::{AgentDid, DidDocument, Zone, ZoneRegistry}; +use didbot_identity::{AccountDid, DidDocument, Zone, ZoneRegistry}; use didbot_key::SigningKey; use didbot_pds::{ - AccountState, Actor, AgentAccount, AgentLedger, BatchOp, BatchOutcome, BlobLimits, BlobStats, - BlobStore, BlobTally, BlobUpload, Cid, CommitStore, Estop, Fetch, Hold, LedgerEntry, + AccountLedger, AccountState, Actor, BatchOp, BatchOutcome, BlobLimits, BlobStats, BlobStore, + BlobTally, BlobUpload, Cid, CommitStore, Estop, Fetch, Hold, HostedAccount, LedgerEntry, LedgerEvent, ListParams, Lock, MemoryBlobStore, MemoryCommitStore, MemoryRecordStore, Minter, ProvisionError, ProvisionRequest, Provisioned, RecordStore, RegistrationFacts, Registry, RegistryStats, RepoHead, ReservationRequest, ReservedIdentity, Swap, Tag, Written, @@ -40,7 +40,7 @@ const PDS_ENDPOINT: &str = "http://agents.localhost:3000"; /// A registry that does exactly what a test tells it to. pub(crate) struct FakeRegistry { zone: Zone, - accounts: Mutex>, + accounts: Mutex>, documents: Mutex>, /// Records, kept in the shipped in-memory store. /// @@ -241,7 +241,7 @@ impl FakeRegistry { /// Deterministic rather than minted and stored, so it works the same for /// an account [`FakeRegistry::seeded`] planted directly as for one that /// went through [`Registry::provision`]. - fn agent_token(did: &str) -> String { + fn account_token(did: &str) -> String { format!("fake-token-{did}") } @@ -379,9 +379,9 @@ impl FakeRegistry { } /// Seeds an account and its document, as if it had been provisioned. - fn seeded(agent_id: &str) -> Self { + fn seeded(account_id: &str) -> Self { let registry = Self::new(); - let (account, document, key) = fixture(agent_id); + let (account, document, key) = fixture(account_id); registry .documents .lock() @@ -430,7 +430,7 @@ impl FakeRegistry { impl Registry for FakeRegistry { fn provision(&self, request: ProvisionRequest) -> Result { Self::hold(&self.provision_gate); - let (account, document, key) = fixture(&request.agent_id); + let (account, document, key) = fixture(&request.account_id); // Kept so a route test can check that the facts on the wire body // reached the engine rather than being dropped between the two. self.profiles @@ -449,11 +449,11 @@ impl Registry for FakeRegistry { .lock() .expect("poisoned") .push(account.clone()); - let agent_token = Self::agent_token(account.did.as_str()); + let account_token = Self::account_token(account.did.as_str()); Ok(Provisioned { account, document, - agent_token, + account_token, }) } @@ -610,7 +610,7 @@ impl Registry for FakeRegistry { .map(|account| account.did.as_str().to_owned()) } - fn accounts(&self) -> Vec { + fn accounts(&self) -> Vec { self.accounts.lock().expect("poisoned").clone() } @@ -655,10 +655,10 @@ impl Registry for FakeRegistry { }) } - fn reservations(&self) -> Vec { + fn reservations(&self) -> Vec { self.accounts() .into_iter() - .filter(AgentAccount::is_reservation) + .filter(HostedAccount::is_reservation) .collect() } @@ -667,7 +667,7 @@ impl Registry for FakeRegistry { did: &str, _operator: &str, _vouch: &didbot_pds::OperatorClaim, - ) -> Result { + ) -> Result { let mut accounts = self.accounts.lock().expect("poisoned"); let account = accounts .iter_mut() @@ -809,7 +809,7 @@ impl Registry for FakeRegistry { Ok(self.records.collections(did)) } - fn account(&self, did: &str) -> Option { + fn account(&self, did: &str) -> Option { self.accounts .lock() .expect("poisoned") @@ -841,7 +841,7 @@ impl Registry for FakeRegistry { didbot_pds::Confinement::default() } - fn children(&self, did: &str) -> Result, didbot_pds::ProvisionError> { + fn children(&self, did: &str) -> Result, didbot_pds::ProvisionError> { let accounts = self.accounts.lock().expect("poisoned"); if !accounts.iter().any(|account| account.did.as_str() == did) { return Err(didbot_pds::ProvisionError::UnknownAccount { @@ -1006,22 +1006,22 @@ impl Registry for FakeRegistry { /// Enough for the routes to be exercised; the real bookkeeping is tested /// against a real [`Provisioner`](didbot_pds::Provisioner) in /// `didbot-pds/tests/ledger.rs`. - fn ledger(&self, did: &str) -> Option { + fn ledger(&self, did: &str) -> Option { self.ledgers().into_iter().find(|led| led.did == did) } - fn ledgers(&self) -> Vec { + fn ledgers(&self) -> Vec { self.accounts .lock() .expect("poisoned") .iter() - .map(|account| AgentLedger { + .map(|account| AccountLedger { did: account.did.as_str().to_owned(), entries: vec![LedgerEntry { seq: 1, at: account.created_at, event: LedgerEvent::Provisioned { - agent_id: account.agent_id.clone(), + account_id: account.account_id.clone(), backend: account .provenance .as_ref() @@ -1070,13 +1070,13 @@ fn zone() -> Zone { } /// An account and its document, as the provisioner would have produced them. -fn fixture(agent_id: &str) -> (AgentAccount, DidDocument, SigningKey) { +fn fixture(account_id: &str) -> (HostedAccount, DidDocument, SigningKey) { let did = didbot_pds::HostedDid::host( - AgentDid::mint(&zone(), agent_id).expect("agent id is a legal label"), + AccountDid::mint(&zone(), account_id).expect("agent id is a legal label"), &ZoneRegistry::single(zone()), ) .expect("minted under the zone"); - let handle = format!("{agent_id}.{ZONE_HOST}"); + let handle = format!("{account_id}.{ZONE_HOST}"); // A real key, published in the document exactly as the provisioner does. // `getRepo` is checked against what a resolver would read, so a // placeholder here would make that test unwritable. @@ -1087,11 +1087,11 @@ fn fixture(agent_id: &str) -> (AgentAccount, DidDocument, SigningKey) { &handle, PDS_ENDPOINT, ); - let account = AgentAccount { + let account = HostedAccount { harness: None, agent_type: None, did, - agent_id: agent_id.to_owned(), + account_id: account_id.to_owned(), handle: Some(handle), created_at: OffsetDateTime::from_unix_timestamp(1_700_000_000).expect("valid timestamp"), kind: didbot_pds::AccountKind::Agent, @@ -1152,17 +1152,17 @@ fn claim() -> Value { }) } -/// A `provisionAgent` body carrying the claim the route requires. -fn provision_body(agent_id: &str) -> String { - json!({ "agentId": agent_id, "attestation": claim() }).to_string() +/// A `createAccount` body carrying the claim the route requires. +fn provision_body(account_id: &str) -> String { + json!({ "accountId": account_id, "attestation": claim() }).to_string() } /// [`post`], carrying an agent token as `Authorization: Bearer `. /// /// `com.atproto.repo.*`'s write routes and `uploadBlob` all require -/// [`crate::auth::Credential::AgentToken`] now; this is what almost every +/// [`crate::auth::Credential::AccountToken`] now; this is what almost every /// write-route test reaches for, with `token` computed via -/// [`FakeRegistry::agent_token`] for the DID the request writes to. +/// [`FakeRegistry::account_token`] for the DID the request writes to. fn authed_post(uri: &str, body: &str, token: &str) -> Request { Request::builder() .method("POST") @@ -1303,7 +1303,7 @@ async fn health_progress_advances_once_a_wired_tick_fires() { async fn provisioning_returns_a_did_and_its_document() { let (status, _, body) = call( Arc::new(FakeRegistry::new()), - post("/xrpc/bot.did.provisionAgent", &provision_body("kestrel")), + post("/xrpc/bot.did.createAccount", &provision_body("kestrel")), ) .await; @@ -1313,7 +1313,7 @@ async fn provisioning_returns_a_did_and_its_document() { assert_eq!(body["didDocument"]["id"], json!(did)); } -/// `provisionAgent` crosses `spawn_blocking` so that a slow `Registry` +/// `createAccount` crosses `spawn_blocking` so that a slow `Registry` /// call — for Route53, a blocking HTTP client plus retries that can run to /// a minute — never holds the worker `GET /health` needs to answer. This /// drives that property rather than the plumbing: a provisioning call is @@ -1332,7 +1332,7 @@ async fn provisioning_in_flight_does_not_block_health() { async move { call_on( app, - post("/xrpc/bot.did.provisionAgent", &provision_body("kestrel")), + post("/xrpc/bot.did.createAccount", &provision_body("kestrel")), ) .await } @@ -1393,7 +1393,7 @@ async fn a_repository_export_in_flight_does_not_block_health() { async fn a_provisioning_body_carries_the_harness_s_account_of_the_agent() { let registry = Arc::new(FakeRegistry::new()); let body = json!({ - "agentId": "kestrel", + "accountId": "kestrel", "attestation": claim(), "registration": { "harness": "claude-code", @@ -1403,11 +1403,8 @@ async fn a_provisioning_body_carries_the_harness_s_account_of_the_agent() { }, }) .to_string(); - let (status, _, body) = call( - registry.clone(), - post("/xrpc/bot.did.provisionAgent", &body), - ) - .await; + let (status, _, body) = + call(registry.clone(), post("/xrpc/bot.did.createAccount", &body)).await; assert_eq!(status, StatusCode::OK); let did = body["did"].as_str().expect("did is a string"); @@ -1427,7 +1424,7 @@ async fn a_provisioning_body_without_a_profile_is_still_accepted() { let registry = Arc::new(FakeRegistry::new()); let (status, _, body) = call( registry.clone(), - post("/xrpc/bot.did.provisionAgent", &provision_body("kestrel")), + post("/xrpc/bot.did.createAccount", &provision_body("kestrel")), ) .await; @@ -1448,8 +1445,8 @@ async fn provisioning_without_a_claim_is_refused_by_name() { let (status, _, body) = call( registry.clone(), post( - "/xrpc/bot.did.provisionAgent", - &json!({ "agentId": "kestrel" }).to_string(), + "/xrpc/bot.did.createAccount", + &json!({ "accountId": "kestrel" }).to_string(), ), ) .await; @@ -1474,8 +1471,8 @@ async fn a_development_stack_provisions_without_a_claim() { let (status, _, body) = call_on( app, post( - "/xrpc/bot.did.provisionAgent", - &json!({ "agentId": "kestrel" }).to_string(), + "/xrpc/bot.did.createAccount", + &json!({ "accountId": "kestrel" }).to_string(), ), ) .await; @@ -1490,7 +1487,7 @@ async fn a_development_stack_provisions_without_a_claim() { async fn malformed_json_is_a_400() { let (status, _, body) = call( Arc::new(FakeRegistry::new()), - post("/xrpc/bot.did.provisionAgent", "{not json"), + post("/xrpc/bot.did.createAccount", "{not json"), ) .await; @@ -1514,16 +1511,17 @@ fn provision_test_app_with_rate_limit(limit: u32) -> axum::Router { ) } -/// The Nth+1 `provisionAgent` call inside the window is refused, keyed on +/// The Nth+1 `createAccount` call inside the window is refused, keyed on /// the caller address. Every call here /// shares the one `"unknown"` bucket `crate::rate_limit::caller_key` gives a /// caller with no `ConnectInfo` -- which every call over `oneshot` is -- so -/// distinct `agentId`s are enough to prove the limiter runs before anything +/// distinct `accountId`s are enough to prove the limiter runs before anything /// account-specific does. #[tokio::test] async fn provisioning_is_rate_limited_per_caller_address() { let app = provision_test_app_with_rate_limit(2); - let request = |agent_id: &str| post("/xrpc/bot.did.provisionAgent", &provision_body(agent_id)); + let request = + |account_id: &str| post("/xrpc/bot.did.createAccount", &provision_body(account_id)); let (first, _, _) = call_on(app.clone(), request("kestrel-a")).await; let (second, _, _) = call_on(app.clone(), request("kestrel-b")).await; @@ -1560,7 +1558,7 @@ async fn a_deployment_at_its_cap_refuses_provisioning_but_still_serves_reads() { let (status, _, body) = call_on( app.clone(), - post("/xrpc/bot.did.provisionAgent", &provision_body("overflow")), + post("/xrpc/bot.did.createAccount", &provision_body("overflow")), ) .await; assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE, "{body}"); @@ -1827,7 +1825,7 @@ async fn protected_resource_metadata_names_the_same_issuer_as_the_authorization_ assert_eq!(resource["authorization_servers"][0], server["issuer"]); } -/// `deleteAgent` for a DID this deployment never minted cannot reach the +/// `deleteAccount` for a DID this deployment never minted cannot reach the /// registry at all any more, and that is the *authorization* answer rather /// than the 404 it used to be: an operator credential was the only thing /// that could name an account it did not hold a token for, and there is no @@ -1846,7 +1844,7 @@ async fn deleting_an_account_a_token_does_not_name_is_refused_before_the_registr let (status, _, body) = call( registry, authed_post( - "/xrpc/bot.did.deleteAgent", + "/xrpc/bot.did.deleteAccount", r#"{"did":"did:web:nobody.agents.localhost"}"#, &seeded_token(), ), @@ -1857,7 +1855,7 @@ async fn deleting_an_account_a_token_does_not_name_is_refused_before_the_registr assert_eq!(body["error"], json!("AccountMismatch")); } -/// Finding 02's regression test: an unauthenticated `deleteAgent` is refused +/// Finding 02's regression test: an unauthenticated `deleteAccount` is refused /// outright, before the registry is ever consulted — a single unauthenticated /// request must not be able to destroy an account. #[tokio::test] @@ -1866,7 +1864,7 @@ async fn deleting_an_account_with_no_credential_is_refused() { let (status, _, body) = call( registry.clone(), post( - "/xrpc/bot.did.deleteAgent", + "/xrpc/bot.did.deleteAccount", &json!({ "did": SEEDED_DID }).to_string(), ), ) @@ -1875,8 +1873,8 @@ async fn deleting_an_account_with_no_credential_is_refused() { assert_eq!(body["error"], json!("AuthenticationRequired")); // And the account really is still there. - let (_, _, listed) = call(registry, get("/xrpc/bot.did.listAgents")).await; - assert_eq!(listed["agents"].as_array().expect("a list").len(), 1); + let (_, _, listed) = call(registry, get("/xrpc/bot.did.listAccounts")).await; + assert_eq!(listed["accounts"].as_array().expect("a list").len(), 1); } /// An agent's own credential may delete only its own account — not another @@ -1892,7 +1890,7 @@ async fn an_agent_token_may_not_delete_a_different_account() { let (status, _, body) = call( registry.clone(), authed_post( - "/xrpc/bot.did.deleteAgent", + "/xrpc/bot.did.deleteAccount", &json!({ "did": other_did }).to_string(), &seeded_token(), ), @@ -1901,8 +1899,8 @@ async fn an_agent_token_may_not_delete_a_different_account() { assert_eq!(status, StatusCode::FORBIDDEN); assert_eq!(body["error"], json!("AccountMismatch")); - let (_, _, listed) = call(registry, get("/xrpc/bot.did.listAgents")).await; - assert_eq!(listed["agents"].as_array().expect("a list").len(), 2); + let (_, _, listed) = call(registry, get("/xrpc/bot.did.listAccounts")).await; + assert_eq!(listed["accounts"].as_array().expect("a list").len(), 2); } /// An agent's own credential deletes its own account — the self-service @@ -1914,7 +1912,7 @@ async fn an_agent_token_may_delete_its_own_account() { let (status, _, _) = call( registry.clone(), authed_post( - "/xrpc/bot.did.deleteAgent", + "/xrpc/bot.did.deleteAccount", &json!({ "did": SEEDED_DID }).to_string(), &seeded_token(), ), @@ -1923,26 +1921,26 @@ async fn an_agent_token_may_delete_its_own_account() { assert_eq!(status, StatusCode::OK); // Erased, and still listed: the identity stays with the row. - let (_, _, listed) = call(registry, get("/xrpc/bot.did.listAgents")).await; - let agents = listed["agents"].as_array().expect("a list"); - assert_eq!(agents.len(), 1); - assert_eq!(agents[0]["state"], json!("decommissioned")); + let (_, _, listed) = call(registry, get("/xrpc/bot.did.listAccounts")).await; + let accounts = listed["accounts"].as_array().expect("a list"); + assert_eq!(accounts.len(), 1); + assert_eq!(accounts[0]["state"], json!("decommissioned")); } #[tokio::test] async fn listing_reports_stored_accounts() { let (status, _, body) = call( Arc::new(FakeRegistry::seeded("kestrel")), - get("/xrpc/bot.did.listAgents"), + get("/xrpc/bot.did.listAccounts"), ) .await; assert_eq!(status, StatusCode::OK); assert_eq!( - body["agents"], + body["accounts"], json!([{ "did": "did:web:kestrel.agents.localhost", - "agentId": "kestrel", + "accountId": "kestrel", "handle": "kestrel.agents.localhost", "pinned": false, "kind": "agent", @@ -1960,7 +1958,7 @@ async fn listing_reports_stored_accounts() { ); } -/// Finding 02's regression test for `setAgentPinned`: unauthenticated, it is +/// Finding 02's regression test for `setAccountPinned`: unauthenticated, it is /// refused, and an agent's own token may pin only itself. #[tokio::test] async fn setting_pinned_requires_a_credential_and_only_covers_the_agents_own_account() { @@ -1973,7 +1971,7 @@ async fn setting_pinned_requires_a_credential_and_only_covers_the_agents_own_acc let (status, _, body) = call( registry.clone(), post( - "/xrpc/bot.did.setAgentPinned", + "/xrpc/bot.did.setAccountPinned", &json!({ "did": SEEDED_DID, "pinned": true }).to_string(), ), ) @@ -1984,7 +1982,7 @@ async fn setting_pinned_requires_a_credential_and_only_covers_the_agents_own_acc let (status, _, body) = call( registry.clone(), authed_post( - "/xrpc/bot.did.setAgentPinned", + "/xrpc/bot.did.setAccountPinned", &json!({ "did": other_did, "pinned": true }).to_string(), &seeded_token(), ), @@ -1996,7 +1994,7 @@ async fn setting_pinned_requires_a_credential_and_only_covers_the_agents_own_acc let (status, _, _) = call( registry, authed_post( - "/xrpc/bot.did.setAgentPinned", + "/xrpc/bot.did.setAccountPinned", &json!({ "did": SEEDED_DID, "pinned": true }).to_string(), &seeded_token(), ), @@ -2018,7 +2016,7 @@ async fn nothing_but_an_account_s_own_token_may_pin_it() { // The account's own token: allowed. let (status, _, _) = call( registry.clone(), - authed_post("/xrpc/bot.did.setAgentPinned", &body, &seeded_token()), + authed_post("/xrpc/bot.did.setAccountPinned", &body, &seeded_token()), ) .await; assert_eq!(status, StatusCode::OK); @@ -2029,7 +2027,7 @@ async fn nothing_but_an_account_s_own_token_may_pin_it() { registry, Request::builder() .method("POST") - .uri("/xrpc/bot.did.setAgentPinned") + .uri("/xrpc/bot.did.setAccountPinned") .header(header::CONTENT_TYPE, "application/json") .header(header::AUTHORIZATION, "Operator test-operator-secret") .body(Body::from(body)) @@ -2077,7 +2075,7 @@ const OTHER: &str = "com.example.other"; /// below presents as `Authorization: Bearer <..>`, since almost every one /// writes to the seeded account. fn seeded_token() -> String { - FakeRegistry::agent_token(SEEDED_DID) + FakeRegistry::account_token(SEEDED_DID) } /// A `literal:self` collection, which is where a chosen key is legal. /// A collection whose lexicon fixes the key at `self`. @@ -3771,7 +3769,7 @@ async fn stats_report_both_halves_of_what_a_restore_left() { ); } -/// `stats`, `listAgents`, `getAgentLedger` and `listAgentLedgers` are +/// `stats`, `listAccounts`, `getAccountLedger` and `listAccountLedgers` are /// disclosure routes: open by default, per `Disclosure`'s "publish by /// default" reasoning. No credential at all reaches every one of them, on a /// deployment that has not closed anything. @@ -3781,9 +3779,9 @@ async fn every_disclosure_route_is_open_with_no_credential_by_default() { for uri in [ "/xrpc/bot.did.stats".to_string(), - "/xrpc/bot.did.listAgents".to_string(), - "/xrpc/bot.did.listAgentLedgers".to_string(), - format!("/xrpc/bot.did.getAgentLedger?did={SEEDED_DID}"), + "/xrpc/bot.did.listAccounts".to_string(), + "/xrpc/bot.did.listAccountLedgers".to_string(), + format!("/xrpc/bot.did.getAccountLedger?did={SEEDED_DID}"), ] { let (status, _, body) = call(registry.clone(), get(&uri)).await; assert_eq!(status, StatusCode::OK, "{uri}: {body}"); @@ -3848,12 +3846,12 @@ async fn a_closed_stats_route_is_disclosure_disabled_for_everyone() { // Closing one route does not close its siblings: the toggle is per // route, not a single switch. - let (status, _, _) = call_on(closed, get("/xrpc/bot.did.listAgents")).await; + let (status, _, _) = call_on(closed, get("/xrpc/bot.did.listAccounts")).await; assert_eq!(status, StatusCode::OK); } /// An agent's own write credential does not open a closed disclosure route -/// either. Disclosure has no self-service reading the way `deleteAgent` and +/// either. Disclosure has no self-service reading the way `deleteAccount` and /// its siblings do, and now that the operator credential is gone there is /// no credential of any kind that reaches past the toggle. #[tokio::test] @@ -3874,7 +3872,7 @@ async fn a_closed_disclosure_route_refuses_an_agent_token() { reservation_rate_limiter: Default::default(), subscriber_limit: Default::default(), disclosure: Disclosure { - list_agents: false, + list_accounts: false, ..Disclosure::default() }, provision_rate_limiter: Default::default(), @@ -3888,7 +3886,7 @@ async fn a_closed_disclosure_route_refuses_an_agent_token() { let (status, _, body) = call_on( closed, - authed_get("/xrpc/bot.did.listAgents", &seeded_token()), + authed_get("/xrpc/bot.did.listAccounts", &seeded_token()), ) .await; assert_eq!(status, StatusCode::FORBIDDEN); @@ -3896,7 +3894,7 @@ async fn a_closed_disclosure_route_refuses_an_agent_token() { } /// **What a development run must not hand the network.** Under a -/// `.localhost` zone `provisionAgent` mints with no attestation claim and, +/// `.localhost` zone `createAccount` mints with no attestation claim and, /// with no `--data`, every blob is held in memory — so a laptop on a shared /// network was handing both to anybody on it. A caller that is not on this /// host is refused before it reaches a route; a loopback caller, and a @@ -3935,7 +3933,7 @@ async fn a_development_run_answers_only_callers_on_this_host() { // The route that made it worth closing. let (status, _, body) = call_on( router(true), - from("/xrpc/bot.did.listAgents", Some(elsewhere)), + from("/xrpc/bot.did.listAccounts", Some(elsewhere)), ) .await; assert_eq!(status, StatusCode::FORBIDDEN, "{body}"); @@ -3959,16 +3957,16 @@ async fn a_development_run_answers_only_callers_on_this_host() { assert_eq!(status, StatusCode::OK, "{body}"); } -/// **What closing `listAgents` is for.** An operator closes it so a stranger +/// **What closing `listAccounts` is for.** An operator closes it so a stranger /// cannot enumerate the accounts this deployment holds. Two other public /// routes name the same accounts: `com.atproto.sync.listRepos` pages through /// them, and `subscribeRepos` announces each one. Closing the first and /// leaving those open would answer `DisclosureDisabled` over a roster still /// readable two other ways. #[tokio::test] -async fn closing_list_agents_closes_every_route_that_names_the_accounts() { +async fn closing_list_accounts_closes_every_route_that_names_the_accounts() { let enumerating = [ - "/xrpc/bot.did.listAgents", + "/xrpc/bot.did.listAccounts", "/xrpc/com.atproto.sync.listRepos", ]; @@ -3994,7 +3992,7 @@ async fn closing_list_agents_closes_every_route_that_names_the_accounts() { } let quiet = Disclosure { - list_agents: false, + list_accounts: false, ..Disclosure::default() }; for uri in enumerating { @@ -4029,7 +4027,7 @@ fn subscribe_repos_asks_the_same_disclosure_question_before_it_upgrades() { .0; assert!( handler.contains("auth::require_disclosure(") - && handler.contains("state.disclosure.list_agents"), + && handler.contains("state.disclosure.list_accounts"), "subscribe_repos no longer asks the disclosure question before it upgrades" ); } @@ -5290,7 +5288,7 @@ mod sync { authed_post( "/xrpc/com.atproto.repo.createRecord", &body, - &FakeRegistry::agent_token(gannet), + &FakeRegistry::account_token(gannet), ), ) .await; @@ -5407,7 +5405,7 @@ mod sync { /// `did` names which account the upload lands in by way of the agent token /// carried in `Authorization`, not a separate header: `uploadBlob` takes no /// `didbot-repo` header any more, the same way it takes no such field in its -/// body — the repository is whichever account [`FakeRegistry::agent_token`] +/// body — the repository is whichever account [`FakeRegistry::account_token`] /// names. See `crate::blobs`'s own module doc. fn upload_request(did: &str, mime: &str, chunks: Vec>) -> Request { let total: usize = chunks.iter().map(Vec::len).sum(); @@ -5419,7 +5417,7 @@ fn upload_request(did: &str, mime: &str, chunks: Vec>) -> Request .header(header::CONTENT_LENGTH, total.to_string()) .header( header::AUTHORIZATION, - format!("Bearer {}", FakeRegistry::agent_token(did)), + format!("Bearer {}", FakeRegistry::account_token(did)), ) .body(Body::from_stream(stream)) .expect("request builds") @@ -5803,7 +5801,7 @@ fn par_from(caller: &str) -> Request { /// PAR is unauthenticated and fetches a document from a host the caller /// names, so it gets the same per-address budget `authorize` and -/// `provisionAgent` already have — and refuses the same way. +/// `createAccount` already have — and refuses the same way. #[tokio::test] async fn oauth_par_is_rate_limited_per_caller_address() { let app = app_with_auth( @@ -6005,7 +6003,7 @@ async fn oauth_authorize_shares_one_budget_across_a_spoofed_header() { // --------------------------------------------------------------------------- /// Every `com.atproto.repo.*` write route, and `uploadBlob`, now takes -/// [`crate::auth::Credential::AgentToken`] — see `auth::ROUTE_CREDENTIALS`'s +/// [`crate::auth::Credential::AccountToken`] — see `auth::ROUTE_CREDENTIALS`'s /// own note. A request naming no credential at all gets /// `AuthenticationRequired`, the same name and status /// [`auth::require_agent_token`] returns at the extractor level in @@ -6044,9 +6042,9 @@ async fn every_write_route_refuses_a_bare_request_with_no_credential() { /// can present one account's credential against the other's repository. /// /// Returns the second account's DID. -fn registry_with_a_second_account(agent_id: &str) -> (Arc, String) { +fn registry_with_a_second_account(account_id: &str) -> (Arc, String) { let registry = FakeRegistry::seeded("kestrel"); - let (account, document, key) = fixture(agent_id); + let (account, document, key) = fixture(account_id); registry .documents .lock() @@ -6080,7 +6078,7 @@ fn registry_with_a_second_account(agent_id: &str) -> (Arc, String) #[tokio::test] async fn a_token_authenticating_a_different_account_is_a_repo_mismatch_on_every_write_route() { let (registry, other_did) = registry_with_a_second_account("marmot"); - let other_token = FakeRegistry::agent_token(&other_did); + let other_token = FakeRegistry::account_token(&other_did); for (uri, body) in [ ( @@ -6138,7 +6136,7 @@ async fn upload_blob_writes_the_credentials_repository_and_not_the_one_the_reque .header("didbot-repo", SEEDED_DID) .header( header::AUTHORIZATION, - format!("Bearer {}", FakeRegistry::agent_token(&other_did)), + format!("Bearer {}", FakeRegistry::account_token(&other_did)), ) .body(Body::from("quernstone")) .expect("request builds"); @@ -6197,7 +6195,7 @@ fn router_with(registry: Arc, auth: AuthState) -> axum::Router { /// with" -- checked fresh on every request rather than cached. /// /// The working token has to be a real agent token, not a legacy session's: -/// `com.atproto.repo.*` write routes take [`crate::auth::Credential::AgentToken`] +/// `com.atproto.repo.*` write routes take [`crate::auth::Credential::AccountToken`] /// (see `auth::ROUTE_CREDENTIALS`'s note), and every write checks that /// credential *before* the halt gate (`check_write_not_halted`, called after /// `require_agent_token` succeeds). A credential the route does not accept @@ -6284,7 +6282,7 @@ async fn an_unowned_server_refuses_to_provision_but_still_serves_its_identity() ); let body = provision_body("kestrel"); - let (status, _, body) = call_on(app.clone(), post("/xrpc/bot.did.provisionAgent", &body)).await; + let (status, _, body) = call_on(app.clone(), post("/xrpc/bot.did.createAccount", &body)).await; assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE, "{body}"); assert_eq!(body["error"], "Halted"); @@ -6306,7 +6304,7 @@ async fn an_unowned_server_refuses_to_provision_but_still_serves_its_identity() // provisioning works exactly as it would have all along. estop.release_self(); let body = provision_body("kestrel"); - let (status, _, body) = call_on(app, post("/xrpc/bot.did.provisionAgent", &body)).await; + let (status, _, body) = call_on(app, post("/xrpc/bot.did.createAccount", &body)).await; assert_eq!(status, StatusCode::OK, "{body}"); } @@ -6552,7 +6550,7 @@ mod dashboard_tests { &app, Request::builder() .method("POST") - .uri("/xrpc/bot.did.provisionAgent") + .uri("/xrpc/bot.did.createAccount") .header(header::CONTENT_TYPE, "application/json") .body(Body::from(provision_body("quokka"))) .expect("request builds"), @@ -7742,7 +7740,7 @@ fn booting_app(registry: Arc) -> axum::Router { async fn an_unclaimed_server_serves_every_read() { for route in [ "/xrpc/com.atproto.server.describeServer", - "/xrpc/bot.did.listAgents", + "/xrpc/bot.did.listAccounts", "/xrpc/bot.did.stats", ] { let (status, _, _) = @@ -7778,8 +7776,8 @@ async fn an_unclaimed_server_serves_every_read() { async fn an_unclaimed_server_refuses_every_write() { let cases: Vec<(&str, Request)> = vec![ ( - "bot.did.provisionAgent", - post("/xrpc/bot.did.provisionAgent", r#"{"agentId":"quokka"}"#), + "bot.did.createAccount", + post("/xrpc/bot.did.createAccount", r#"{"accountId":"quokka"}"#), ), ( "com.atproto.repo.createRecord", @@ -7829,7 +7827,7 @@ async fn an_unclaimed_server_answers_what_the_claim_command_needs() { #[tokio::test] async fn a_booting_server_defers_with_a_retry_after() { for route in [ - "/xrpc/bot.did.listAgents", + "/xrpc/bot.did.listAccounts", "/xrpc/com.atproto.server.describeServer", ] { let (status, headers, body) = @@ -7857,12 +7855,12 @@ async fn a_booting_server_defers_with_a_retry_after() { async fn a_deferral_names_the_state() { let (_, _, body) = call_on( unclaimed_app(Arc::new(FakeRegistry::new())), - post("/xrpc/bot.did.provisionAgent", r#"{"agentId":"quokka"}"#), + post("/xrpc/bot.did.createAccount", r#"{"accountId":"quokka"}"#), ) .await; let message = body["message"].as_str().expect("a message"); assert!(message.contains("unclaimed"), "{message}"); - assert!(message.contains("bot.did.provisionAgent"), "{message}"); + assert!(message.contains("bot.did.createAccount"), "{message}"); } /// `subscribeRepos` is refused *before* the WebSocket upgrade when it is @@ -7898,7 +7896,7 @@ async fn a_booting_server_refuses_subscribe_repos_before_the_upgrade() { async fn a_claimed_server_answers_what_an_unclaimed_one_refuses() { let (status, _, body) = call( Arc::new(FakeRegistry::new()), - post("/xrpc/bot.did.provisionAgent", &provision_body("quokka")), + post("/xrpc/bot.did.createAccount", &provision_body("quokka")), ) .await; assert_ne!(status, StatusCode::SERVICE_UNAVAILABLE, "{body}"); @@ -7933,7 +7931,7 @@ async fn the_poll_nudge_is_accepted_and_asserts_nothing() { let app = unclaimed_app(Arc::new(FakeRegistry::new())); let (status, _, body) = call_on( app, - post("/xrpc/bot.did.provisionAgent", &provision_body("quokka")), + post("/xrpc/bot.did.createAccount", &provision_body("quokka")), ) .await; assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE, "{body}"); diff --git a/crates/didbot-serve/src/wire.rs b/crates/didbot-serve/src/wire.rs index 8bea7647..03dd1850 100644 --- a/crates/didbot-serve/src/wire.rs +++ b/crates/didbot-serve/src/wire.rs @@ -8,7 +8,7 @@ use std::collections::BTreeMap; use didbot_pds::{ - AccountKind, AccountState, AgentAccount, Cid, Holds, ListParams, Locks, Precondition, + AccountKind, AccountState, Cid, Holds, HostedAccount, ListParams, Locks, Precondition, ProvisionRequest, RegistrationFacts, RegistryStats, ReservationRequest, Retention, Written, DEFAULT_LIST_LIMIT, }; @@ -19,12 +19,12 @@ use time::format_description::well_known::Rfc3339; use crate::error::ApiError; use didbot_attest::AttestationClaim; -/// Body of `bot.did.provisionAgent`. +/// Body of `bot.did.createAccount`. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct ProvisionAgentRequest { +pub struct CreateAccountRequest { /// The agent this account is for. Becomes the DID's leftmost label. - pub agent_id: String, + pub account_id: String, /// Handle the caller would like, if it has a preference. /// /// A hint, not a request. A deployment that issues its own names honours @@ -77,11 +77,11 @@ impl WireRegistration { } } -impl ProvisionAgentRequest { +impl CreateAccountRequest { /// Converts into the engine's request type. pub fn into_request(self) -> Result { Ok(ProvisionRequest { - agent_id: self.agent_id, + account_id: self.account_id, handle: self.handle, attestation: self.attestation, profile: self.registration.unwrap_or_default().into_facts(), @@ -103,7 +103,7 @@ pub struct ReserveIdentityRequest { /// [`didbot_pds::AccountKind::reservable`]. pub kind: AccountKind, /// The operator DID the host expects a vouch from, if it knows one; see - /// [`didbot_pds::AgentAccount::expected_operator`]. + /// [`didbot_pds::HostedAccount::expected_operator`]. #[serde(default, skip_serializing_if = "Option::is_none")] pub operator: Option, } @@ -156,7 +156,7 @@ pub struct ReservationSummary { impl ReservationSummary { /// Summarizes a pending reservation. `None` for an account that is not /// one, so a caller cannot list something this shape does not describe. - pub fn from_account(account: &AgentAccount) -> Option { + pub fn from_account(account: &HostedAccount) -> Option { let expires_at = account.reservation_expires_at()?; Some(Self { did: account.did.as_str().to_owned(), @@ -204,7 +204,7 @@ pub struct DeclineAuthorizationRequest { pub reason: Option, } -/// Body of `bot.did.deleteAgent`, and of the freeze and deactivate routes +/// Body of `bot.did.deleteAccount`, and of the freeze and deactivate routes /// and their inverses. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct DidRequest { @@ -212,7 +212,7 @@ pub struct DidRequest { pub did: String, } -/// Query of `bot.did.getAgentLedger`. +/// Query of `bot.did.getAccountLedger`. /// /// A query rather than a body because it reads, and a `did` rather than a /// handle because a handle is released when the account is: the one caller @@ -224,7 +224,7 @@ pub struct LedgerQuery { pub did: String, } -/// Body of `bot.did.setAgentPinned`. +/// Body of `bot.did.setAccountPinned`. /// /// Not in the original route list. It is here because [`Registry`] exposes /// pinning and there was otherwise no way to reach it over HTTP. @@ -238,7 +238,7 @@ pub struct SetPinnedRequest { pub pinned: bool, } -/// One entry in the `listAgents` response. +/// One entry in the `listAccounts` response. /// /// This server's account of its own contents, which is the most a listing /// can ever be. A reader deriving "which agents does this operator have" from @@ -251,11 +251,11 @@ pub struct SetPinnedRequest { /// in the other. See `docs/operator-verification.md`. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct AgentSummary { +pub struct AccountSummary { /// The account's DID. pub did: String, /// The agent the account was minted for. - pub agent_id: String, + pub account_id: String, /// The handle the account answers to, when it has one. /// /// The name is the interesting half of an account for anyone reading a @@ -275,7 +275,7 @@ pub struct AgentSummary { /// /// Derived from [`Self::retention`] rather than stored beside it: a pin is /// retention set to never. Kept on the wire under its own name because - /// `bot.did.setAgentPinned` is still the question a caller asks. + /// `bot.did.setAccountPinned` is still the question a caller asks. pub pinned: bool, /// What kind of account it is, and so what its retention started as. #[serde(default)] @@ -303,21 +303,21 @@ pub struct AgentSummary { /// The verified link, not the harness's word: `bot.did.registration`'s /// `parent` is what the caller said at provisioning, and this is what /// the server checked a vouch or an attestation for. See - /// [`didbot_pds::AgentAccount::parent`]. + /// [`didbot_pds::HostedAccount::parent`]. #[serde(default, skip_serializing_if = "Option::is_none")] pub parent: Option, } -impl AgentSummary { +impl AccountSummary { /// Summarizes a stored account. /// /// A timestamp that will not format is reported as an empty string rather /// than failing the whole listing. `Rfc3339` only rejects years outside /// 0000..=9999, which no clock this reads will produce. - pub fn from_account(account: &AgentAccount) -> Self { + pub fn from_account(account: &HostedAccount) -> Self { Self { did: account.did.as_str().to_owned(), - agent_id: account.agent_id.clone(), + account_id: account.account_id.clone(), handle: account.handle.clone(), pinned: account.pinned(), kind: account.kind, diff --git a/crates/didbot-serve/tests/banner_disclosure.rs b/crates/didbot-serve/tests/banner_disclosure.rs index be33a24c..601bb3ab 100644 --- a/crates/didbot-serve/tests/banner_disclosure.rs +++ b/crates/didbot-serve/tests/banner_disclosure.rs @@ -1,7 +1,7 @@ //! The startup banner names routes this run actually answers. //! //! The banner exists so a developer can copy a working request out of the -//! terminal. `bot.did.listAgents` is under a disclosure toggle, so a run +//! terminal. `bot.did.listAccounts` is under a disclosure toggle, so a run //! started with that toggle closed must not offer a request that answers //! `DisclosureDisabled` to everyone, the operator included. //! @@ -18,7 +18,7 @@ use std::time::{Duration, Instant}; /// The banner's last line, so a reader knows the whole of it has been /// written before it is asserted on. -const BANNER_END: &str = "bot.did.provisionAgent"; +const BANNER_END: &str = "bot.did.createAccount"; /// What the binary prints when the port it was handed is already taken. const BIND_FAILED: &str = "could not bind"; @@ -125,7 +125,7 @@ fn offered(banner: &str) -> Vec { } /// The route the `list-agents` toggle closes, as the banner offers it. -const ROSTER: [&str; 1] = ["listAgents"]; +const ROSTER: [&str; 1] = ["listAccounts"]; #[test] fn an_open_roster_is_offered_and_a_closed_one_is_not() { diff --git a/crates/didbot-serve/tests/blob_policy.rs b/crates/didbot-serve/tests/blob_policy.rs index cf1fbee5..e13c29e7 100644 --- a/crates/didbot-serve/tests/blob_policy.rs +++ b/crates/didbot-serve/tests/blob_policy.rs @@ -57,7 +57,7 @@ async fn what_the_request_declares_is_judged_before_the_body_is_read() { matches!( harness .registry - .begin_blob(&harness.agent_did, mime, declared), + .begin_blob(&harness.account_did, mime, declared), Err(didbot_pds::ProvisionError::PolicyRejected { .. }) ) }; @@ -88,7 +88,7 @@ async fn an_upload_is_judged_by_what_its_bytes_are() { let rows = harness.denials(); assert!( rows.iter() - .any(|row| row.account == harness.agent_did && row.reason == REASON), + .any(|row| row.account == harness.account_did && row.reason == REASON), "{rows:?}" ); } diff --git a/crates/didbot-serve/tests/cedar_write_routes.rs b/crates/didbot-serve/tests/cedar_write_routes.rs index 3121d678..f3e47c10 100644 --- a/crates/didbot-serve/tests/cedar_write_routes.rs +++ b/crates/didbot-serve/tests/cedar_write_routes.rs @@ -117,7 +117,7 @@ fn build() -> Harness { #[tokio::test] async fn a_bound_cedar_statement_refuses_a_write_on_every_route() { let harness = build(); - let did = harness.agent_did.clone(); + let did = harness.account_did.clone(); let agent = harness.as_agent(); install(&harness, Some(cedar_policy())); @@ -225,7 +225,7 @@ async fn a_bound_cedar_statement_refuses_a_write_on_every_route() { #[tokio::test] async fn a_create_over_a_tombstone_is_judged_on_what_the_tombstone_holds() { let harness = build(); - let did = harness.agent_did.clone(); + let did = harness.account_did.clone(); let agent = harness.as_agent(); // Set up under no policy: the record has to be written and deleted @@ -276,7 +276,7 @@ async fn a_create_over_a_tombstone_is_judged_on_what_the_tombstone_holds() { #[tokio::test] async fn the_same_writes_land_with_no_policy_published() { let harness = build(); - let did = harness.agent_did.clone(); + let did = harness.account_did.clone(); let agent = harness.as_agent(); install(&harness, None); diff --git a/crates/didbot-serve/tests/credential_durability.rs b/crates/didbot-serve/tests/credential_durability.rs index f97097c9..56bef8f2 100644 --- a/crates/didbot-serve/tests/credential_durability.rs +++ b/crates/didbot-serve/tests/credential_durability.rs @@ -1,9 +1,9 @@ -//! The write credential `bot.did.provisionAgent` hands back outlives the +//! The write credential `bot.did.createAccount` hands back outlives the //! process that minted it. //! //! This spawns the real `didbot-pds` binary, twice, over one data directory, //! because that is the only place the property lives. `Provisioner` reaches -//! its agent token store through `AgentTokenStore`, and every unit test in +//! its agent token store through `AccountTokenStore`, and every unit test in //! the workspace either builds a provisioner itself or is handed one, so //! every unit test proves a property of whichever store *it* chose. Which //! store the deployment chooses is a fact about `assemble` in @@ -249,12 +249,12 @@ struct Agent { token: String, } -async fn provision(run: &Run, agent_id: &str) -> Agent { +async fn provision(run: &Run, account_id: &str) -> Agent { let answer = post( run, - "bot.did.provisionAgent", + "bot.did.createAccount", None, - json!({ "agentId": agent_id }), + json!({ "accountId": account_id }), ) .await; assert_eq!( @@ -267,7 +267,7 @@ async fn provision(run: &Run, agent_id: &str) -> Agent { .as_str() .expect("the response names the account") .to_owned(), - token: answer.body["agentToken"] + token: answer.body["accountToken"] .as_str() .expect("the response carries the write credential") .to_owned(), @@ -293,7 +293,7 @@ async fn write_as(run: &Run, agent: &Agent, text: &str) -> Answer { .await } -/// The credential a `provisionAgent` response handed out still writes after +/// The credential a `createAccount` response handed out still writes after /// the process that handed it out has gone away and come back. /// /// This is the deployment's own restart: `infra/pds` replaces the instance @@ -318,7 +318,7 @@ async fn a_token_issued_before_a_restart_still_writes_after_one() { let after = write_as(&second, &agent, "after the restart").await; assert_eq!( after.status, 200, - "the token a `provisionAgent` response handed out stopped writing across a restart, so \ + "the token a `createAccount` response handed out stopped writing across a restart, so \ every agent holding one is locked out of a server that still hosts its account: {}", after.body ); @@ -381,7 +381,7 @@ async fn a_deleted_accounts_token_is_still_refused_after_a_restart() { let deleted = post( &first, - "bot.did.deleteAgent", + "bot.did.deleteAccount", Some(&agent.token), json!({ "did": agent.did }), ) diff --git a/crates/didbot-serve/tests/denied_app.rs b/crates/didbot-serve/tests/denied_app.rs index 09e213f1..b03a9b81 100644 --- a/crates/didbot-serve/tests/denied_app.rs +++ b/crates/didbot-serve/tests/denied_app.rs @@ -211,7 +211,7 @@ async fn a_refused_write_is_logged_against_its_app() { let rows = harness.denials(); assert!( - rows.iter().any(|row| row.account == harness.agent_did + rows.iter().any(|row| row.account == harness.account_did && row.client_id.as_deref() == Some(DENIED) && row.reason == REASON), "{rows:?}" @@ -245,7 +245,7 @@ async fn a_statement_naming_one_account_denies_that_account_alone() { let tokens = harness.sign_in(DENIED, &scope()).await; let other = harness.sign_in(OTHER, &scope()).await; - deny(&harness, Some(per_account_denial(&harness.agent_did))); + deny(&harness, Some(per_account_denial(&harness.account_did))); let (status, body) = harness.create(&Harness::as_app(&tokens)).await; assert_eq!(status, StatusCode::FORBIDDEN, "{body}"); let (status, body) = harness.refresh(&tokens).await; diff --git a/crates/didbot-serve/tests/logging.rs b/crates/didbot-serve/tests/logging.rs index 2fbc06a6..c496d65e 100644 --- a/crates/didbot-serve/tests/logging.rs +++ b/crates/didbot-serve/tests/logging.rs @@ -94,7 +94,7 @@ async fn the_request_log_carries_no_credential_and_no_query() { .provision(ProvisionRequest::new("quernstone-agent", None)) .expect("provisioning should succeed"); let did = provisioned.account.did.as_str().to_owned(); - let agent_token = provisioned.agent_token.clone(); + let account_token = provisioned.account_token.clone(); let auth = AuthState { estop: Arc::new(Estop::default()), @@ -140,7 +140,7 @@ async fn the_request_log_carries_no_credential_and_no_query() { "expected the consent route in the log:\n{rendered}" ); - for secret in [agent_token.as_str(), PAR_REFERENCE] { + for secret in [account_token.as_str(), PAR_REFERENCE] { assert!( !rendered.contains(secret), "a secret reached the log: {secret:?}\n{rendered}" diff --git a/crates/didbot-serve/tests/oauth_agent_flow.rs b/crates/didbot-serve/tests/oauth_account_flow.rs similarity index 95% rename from crates/didbot-serve/tests/oauth_agent_flow.rs rename to crates/didbot-serve/tests/oauth_account_flow.rs index fba76077..4d24869c 100644 --- a/crates/didbot-serve/tests/oauth_agent_flow.rs +++ b/crates/didbot-serve/tests/oauth_account_flow.rs @@ -12,7 +12,7 @@ //! //! Approving is one call and it is authenticated: //! `bot.did.approveAuthorization` takes the decision's one-time token under -//! `Credential::AgentSelf`, so the account doing the approving is the one +//! `Credential::AccountSelf`, so the account doing the approving is the one //! the presented agent token authenticates as and never a value in the //! body. Every failure case below confirms the corresponding check refuses //! *before* a code, and therefore a token, is ever issued. @@ -183,10 +183,10 @@ struct Fixture { /// The notify `serve` fires on `SIGTERM`; see /// `a_shutdown_wakes_a_long_poll_instead_of_holding_its_connection`. shutdown: Arc, - agent_did: String, - /// The credential `provisionAgent` handed back, which is what the daemon - /// presents as `Credential::AgentSelf` on the four decision routes. - agent_token: String, + account_did: String, + /// The credential `createAccount` handed back, which is what the daemon + /// presents as `Credential::AccountSelf` on the four decision routes. + account_token: String, /// A second account, for the tests about one agent reaching for /// another's decision. other_did: String, @@ -237,13 +237,13 @@ fn build_gated( let provisioned = provisioner .provision(ProvisionRequest::new("agent-one", None)) .expect("provisioning a fresh agent succeeds"); - let agent_did = provisioned.account.did.as_str().to_owned(); - let agent_token = provisioned.agent_token; + let account_did = provisioned.account.did.as_str().to_owned(); + let account_token = provisioned.account_token; let other = provisioner .provision(ProvisionRequest::new("agent-two", None)) .expect("provisioning a second agent succeeds"); let other_did = other.account.did.as_str().to_owned(); - let other_token = other.agent_token; + let other_token = other.account_token; let registry: Arc = Arc::new(provisioner); // Seeded rather than fetched: `resolve_client` answers out of the cache @@ -293,8 +293,8 @@ fn build_gated( code_store, decisions, shutdown, - agent_did, - agent_token, + account_did, + account_token, other_did, other_token, } @@ -321,12 +321,12 @@ fn code_verifier_and_challenge() -> (String, String) { (verifier, challenge) } -/// Pushes a real `POST /oauth/par` naming `fixture.agent_did` as +/// Pushes a real `POST /oauth/par` naming `fixture.account_did` as /// `login_hint` -- the normal atproto OAuth case, where the client already /// knows which account it wants to sign in as -- and hands back the /// `request_uri` it minted. async fn push(fixture: &Fixture, code_challenge: &str) -> String { - let (status, json) = push_as(fixture, &fixture.agent_did, REQUESTED, code_challenge).await; + let (status, json) = push_as(fixture, &fixture.account_did, REQUESTED, code_challenge).await; assert_eq!(status, StatusCode::CREATED, "PAR refused: {json}"); json["request_uri"] .as_str() @@ -529,7 +529,7 @@ async fn sign_in(fixture: &Fixture, proof: &str) -> serde_json::Value { let (status, answer) = post_xrpc( &fixture.app, "bot.did.approveAuthorization", - &fixture.agent_token, + &fixture.account_token, serde_json::json!({ "token": token_for(fixture, &request_uri) }), ) .await; @@ -565,7 +565,7 @@ fn token_of(json: &serde_json::Value, field: &str) -> String { async fn the_agent_completes_the_flow_and_the_stamped_confirmation_issues_a_working_token() { let fixture = build(); let json = sign_in(&fixture, "any-proof-string").await; - assert_eq!(json["sub"], fixture.agent_did); + assert_eq!(json["sub"], fixture.account_did); assert!(json["access_token"].as_str().is_some()); } @@ -610,7 +610,7 @@ async fn a_refresh_grant_rotates_both_tokens_and_keeps_the_subject() { .await; assert_eq!(status, StatusCode::OK); - assert_eq!(second["sub"], fixture.agent_did); + assert_eq!(second["sub"], fixture.account_did); assert_ne!( token_of(&first, "access_token"), token_of(&second, "access_token") @@ -737,7 +737,7 @@ async fn a_replayed_reference_issues_no_second_code() { fixture.consent_store.as_ref(), fixture.code_store.as_ref(), &reference, - &fixture.agent_did, + &fixture.account_did, ) .is_ok()); @@ -745,7 +745,7 @@ async fn a_replayed_reference_issues_no_second_code() { fixture.consent_store.as_ref(), fixture.code_store.as_ref(), &reference, - &fixture.agent_did, + &fixture.account_did, ); assert!(matches!(replay, Err(ConsentError::NotLive(_)))); } @@ -774,7 +774,7 @@ async fn a_stamped_identity_that_the_request_did_not_name_issues_no_code() { /// A policy's refusal lands at PAR itself: a gate that denies every grant /// this client could ask for refuses the push outright, before a /// `request_uri` or a decision record exists — `didbot_policy:: -/// Subject::Grant` names no account, so this holds for `fixture.agent_did` +/// Subject::Grant` names no account, so this holds for `fixture.account_did` /// exactly as it would for any other `login_hint`, resolvable or not; see /// `the_denied_client_refusal_is_the_same_whether_or_not_the_account_exists`. /// Unlike an unresolvable hint @@ -794,7 +794,7 @@ async fn a_policy_refusal_lands_at_par_as_an_outright_refusal() { Arc::new(RateLimiter::default()), ); let (_, challenge) = code_verifier_and_challenge(); - let (status, body) = push_as(&fixture, &fixture.agent_did, REQUESTED, &challenge).await; + let (status, body) = push_as(&fixture, &fixture.account_did, REQUESTED, &challenge).await; assert_eq!(status, StatusCode::BAD_REQUEST, "{body}"); assert_eq!(body["error"], "invalid_request"); assert!(body.get("request_uri").is_none()); @@ -803,7 +803,7 @@ async fn a_policy_refusal_lands_at_par_as_an_outright_refusal() { let (_, listed) = get_xrpc( &fixture.app, "bot.did.listPendingAuthorizations", - &fixture.agent_token, + &fixture.account_token, ) .await; assert!(listed["pending"].as_array().unwrap().is_empty()); @@ -823,7 +823,7 @@ async fn a_push_without_response_type_code_is_refused() { ("scope", REQUESTED), ("code_challenge", challenge.as_str()), ("code_challenge_method", "S256"), - ("login_hint", fixture.agent_did.as_str()), + ("login_hint", fixture.account_did.as_str()), ]; let (status, body) = post_par(&fixture, &form).await; @@ -838,7 +838,7 @@ async fn a_push_without_response_type_code_is_refused() { let (_, listed) = get_xrpc( &fixture.app, "bot.did.listPendingAuthorizations", - &fixture.agent_token, + &fixture.account_token, ) .await; assert!(listed["pending"].as_array().unwrap().is_empty()); @@ -899,7 +899,7 @@ async fn a_policy_loaded_after_par_leaves_the_record_and_refuses_the_token() { let (status, answer) = post_xrpc( &fixture.app, "bot.did.approveAuthorization", - &fixture.agent_token, + &fixture.account_token, serde_json::json!({ "token": token }), ) .await; @@ -926,7 +926,7 @@ async fn a_policy_loaded_after_par_leaves_the_record_and_refuses_the_token() { // The next push is refused outright, rather than recorded with nothing // left to approve. See `a_policy_refusal_lands_at_par_as_an_outright_refusal`. - let (status, body) = push_as(&fixture, &fixture.agent_did, REQUESTED, &challenge).await; + let (status, body) = push_as(&fixture, &fixture.account_did, REQUESTED, &challenge).await; assert_eq!(status, StatusCode::BAD_REQUEST, "{body}"); assert_eq!(body["error"], "invalid_request"); } @@ -972,7 +972,7 @@ async fn an_unresolvable_login_hint_is_accepted_like_a_real_account() { let (status, answer) = get_xrpc( &fixture.app, &format!("bot.did.getAuthorization?requestUri={request_uri}"), - &fixture.agent_token, + &fixture.account_token, ) .await; assert_eq!(status, StatusCode::NOT_FOUND, "{answer}"); @@ -982,7 +982,7 @@ async fn an_unresolvable_login_hint_is_accepted_like_a_real_account() { /// `oauth::par::ParError::ClientRefused`'s own doc — so it is one refusal /// that gets to stay distinguishable: no `login_hint` value, resolvable or /// not, changes it. This is that claim, checked: the same denying gate, -/// the same client, one push naming the real `fixture.agent_did` and one +/// the same client, one push naming the real `fixture.account_did` and one /// naming nobody, and the two bodies come back equal. #[tokio::test] async fn the_denied_client_refusal_is_the_same_whether_or_not_the_account_exists() { @@ -998,7 +998,7 @@ async fn the_denied_client_refusal_is_the_same_whether_or_not_the_account_exists let (_, challenge) = code_verifier_and_challenge(); let (known_status, known_body) = - push_as(&fixture, &fixture.agent_did, REQUESTED, &challenge).await; + push_as(&fixture, &fixture.account_did, REQUESTED, &challenge).await; let (unknown_status, unknown_body) = push_as( &fixture, "did:web:nobody-this-deployment-hosts.example", @@ -1017,7 +1017,7 @@ async fn the_denied_client_refusal_is_the_same_whether_or_not_the_account_exists /// tell "no account answers to this handle" apart from "this one does" /// could push one request per candidate handle or DID and read the /// difference back as a directory — the same enumeration -/// `Credential::Disclosure`'s narrowing of `bot.did.listAgents` (`auth.rs`) +/// `Credential::Disclosure`'s narrowing of `bot.did.listAccounts` (`auth.rs`) /// exists to prevent, reachable without ever calling that route. On one /// deployment with nothing denied — every deployment this project ships, /// until an operator loads a policy — a real account and an unresolvable @@ -1034,7 +1034,7 @@ async fn an_unresolvable_hint_and_a_real_account_get_byte_identical_par_response let (_, challenge) = code_verifier_and_challenge(); let (known_status, known_body) = - push_as(&fixture, &fixture.agent_did, REQUESTED, &challenge).await; + push_as(&fixture, &fixture.account_did, REQUESTED, &challenge).await; let (unknown_status, unknown_body) = push_as( &fixture, "did:web:nobody-this-deployment-hosts.example", @@ -1085,7 +1085,7 @@ async fn eight_spellings_of_a_real_account_and_a_ninth_unresolvable_hint_all_ans let (_, challenge) = code_verifier_and_challenge(); for n in 0..8 { - let (status, body) = push_as(&fixture, &fixture.agent_did, REQUESTED, &challenge).await; + let (status, body) = push_as(&fixture, &fixture.account_did, REQUESTED, &challenge).await; assert_eq!(status, StatusCode::CREATED, "spelling {n}: {body}"); } let (status, body) = push_as( @@ -1114,9 +1114,9 @@ async fn the_nth_plus_one_push_for_a_real_account_lands_and_displaces_the_oldest ); let (_, challenge) = code_verifier_and_challenge(); - let (_, first) = push_as(&fixture, &fixture.agent_did, REQUESTED, &challenge).await; + let (_, first) = push_as(&fixture, &fixture.account_did, REQUESTED, &challenge).await; let oldest = first["request_uri"].as_str().unwrap().to_owned(); - let (status, body) = push_as(&fixture, &fixture.agent_did, REQUESTED, &challenge).await; + let (status, body) = push_as(&fixture, &fixture.account_did, REQUESTED, &challenge).await; assert_eq!(status, StatusCode::CREATED, "{body}"); let request_uri = body["request_uri"].as_str().unwrap(); @@ -1172,7 +1172,7 @@ async fn many_invented_hints_never_fill_the_servers_own_bound() { } // If any of the above had cost a real slot in a store whose total bound // is the compiled-in default (far below 512), this would now refuse. - let (status, body) = push_as(&fixture, &fixture.agent_did, REQUESTED, &challenge).await; + let (status, body) = push_as(&fixture, &fixture.account_did, REQUESTED, &challenge).await; assert_eq!( status, StatusCode::CREATED, @@ -1203,12 +1203,12 @@ async fn a_full_store_refuses_a_real_account_and_an_unresolvable_hint_with_one_b // -- `pending_per_account` is raised well above it above so this is the // total bound refusing, not that account's own. for n in 0..didbot_serve::oauth::decision::DEFAULT_PENDING_TOTAL { - let (status, body) = push_as(&fixture, &fixture.agent_did, REQUESTED, &challenge).await; + let (status, body) = push_as(&fixture, &fixture.account_did, REQUESTED, &challenge).await; assert_eq!(status, StatusCode::CREATED, "fill {n}: {body}"); } let (real_status, real_body) = - push_as(&fixture, &fixture.agent_did, REQUESTED, &challenge).await; + push_as(&fixture, &fixture.account_did, REQUESTED, &challenge).await; let (unresolved_status, unresolved_body) = push_as( &fixture, "did:web:nobody-this-deployment-hosts.example", @@ -1255,7 +1255,8 @@ async fn an_unresolvable_hint_costs_about_the_same_as_a_real_push() { let known_elapsed = { let start = std::time::Instant::now(); for _ in 0..ITERATIONS { - let (status, body) = push_as(&fixture, &fixture.agent_did, REQUESTED, &challenge).await; + let (status, body) = + push_as(&fixture, &fixture.account_did, REQUESTED, &challenge).await; assert_eq!(status, StatusCode::CREATED, "{body}"); } start.elapsed() @@ -1304,11 +1305,11 @@ async fn a_pushed_request_becomes_a_decision_the_named_account_can_read() { let (status, record) = get_xrpc( &fixture.app, &format!("bot.did.getAuthorization?requestUri={request_uri}"), - &fixture.agent_token, + &fixture.account_token, ) .await; assert_eq!(status, StatusCode::OK, "{record}"); - assert_eq!(record["account"], fixture.agent_did); + assert_eq!(record["account"], fixture.account_did); assert_eq!(record["state"], "pending"); assert_eq!(record["verdict"]["kind"], "allow"); assert_eq!(record["client"]["origin"], "https://client.example"); @@ -1337,7 +1338,7 @@ async fn the_clients_own_copy_of_itself_never_reaches_the_agent() { let (_, wire) = get_xrpc( &fixture.app, &format!("bot.did.getAuthorization?requestUri={request_uri}"), - &fixture.agent_token, + &fixture.account_token, ) .await; assert!(!wire.to_string().contains(CLIENT_NAME)); @@ -1363,7 +1364,7 @@ async fn an_approval_by_token_issues_a_code_and_a_redirect_for_the_daemon() { let (status, answer) = post_xrpc( &fixture.app, "bot.did.approveAuthorization", - &fixture.agent_token, + &fixture.account_token, serde_json::json!({ "token": token }), ) .await; @@ -1391,7 +1392,7 @@ async fn an_approval_by_token_issues_a_code_and_a_redirect_for_the_daemon() { ) .await; assert_eq!(status, StatusCode::OK, "{json}"); - assert_eq!(json["sub"], fixture.agent_did); + assert_eq!(json["sub"], fixture.account_did); assert_eq!(json["scope"], "atproto repo:app.bsky.feed.post"); // The decision is answered, and stops being listed. @@ -1413,7 +1414,7 @@ async fn a_narrowed_request_issues_a_token_at_the_granted_scopes_and_names_the_c let (_, record) = get_xrpc( &fixture.app, &format!("bot.did.getAuthorization?requestUri={request_uri}"), - &fixture.agent_token, + &fixture.account_token, ) .await; assert_eq!(record["verdict"]["kind"], "narrow"); @@ -1459,7 +1460,7 @@ async fn a_narrowed_request_issues_a_token_at_the_granted_scopes_and_names_the_c let (status, answer) = post_xrpc( &fixture.app, "bot.did.approveAuthorization", - &fixture.agent_token, + &fixture.account_token, serde_json::json!({ "token": token_for(&fixture, &request_uri) }), ) .await; @@ -1496,7 +1497,7 @@ async fn a_denied_request_lists_as_deny_and_mints_no_token() { let (_, listed) = get_xrpc( &fixture.app, "bot.did.listPendingAuthorizations", - &fixture.agent_token, + &fixture.account_token, ) .await; assert_eq!(listed["pending"].as_array().unwrap().len(), 1); @@ -1530,7 +1531,7 @@ async fn a_replayed_approval_token_is_refused() { let (status, _) = post_xrpc( &fixture.app, "bot.did.approveAuthorization", - &fixture.agent_token, + &fixture.account_token, serde_json::json!({ "token": token }), ) .await; @@ -1539,7 +1540,7 @@ async fn a_replayed_approval_token_is_refused() { let (status, body) = post_xrpc( &fixture.app, "bot.did.approveAuthorization", - &fixture.agent_token, + &fixture.account_token, serde_json::json!({ "token": token }), ) .await; @@ -1580,13 +1581,13 @@ async fn an_approval_by_the_wrong_account_is_refused() { ) .await; assert!(listed["pending"].as_array().unwrap().is_empty()); - assert_ne!(fixture.agent_did, fixture.other_did); + assert_ne!(fixture.account_did, fixture.other_did); // The rightful account is unaffected: the refusal spent nothing. let (status, _) = post_xrpc( &fixture.app, "bot.did.approveAuthorization", - &fixture.agent_token, + &fixture.account_token, serde_json::json!({ "token": token }), ) .await; @@ -1605,7 +1606,7 @@ async fn a_decline_records_the_state_and_spends_the_token() { let (status, answer) = post_xrpc( &fixture.app, "bot.did.declineAuthorization", - &fixture.agent_token, + &fixture.account_token, serde_json::json!({ "token": token, "reason": "quernstone" }), ) .await; @@ -1619,7 +1620,7 @@ async fn a_decline_records_the_state_and_spends_the_token() { let (status, _) = post_xrpc( &fixture.app, "bot.did.approveAuthorization", - &fixture.agent_token, + &fixture.account_token, serde_json::json!({ "token": token }), ) .await; @@ -1638,7 +1639,7 @@ async fn the_long_poll_answers_at_once_when_there_is_something_and_times_out_emp let (status, listed) = get_xrpc( &fixture.app, "bot.did.listPendingAuthorizations?wait=30", - &fixture.agent_token, + &fixture.account_token, ) .await; assert_eq!(status, StatusCode::OK); @@ -1655,7 +1656,7 @@ async fn the_long_poll_answers_at_once_when_there_is_something_and_times_out_emp let (status, listed) = get_xrpc( &fixture.app, &format!("bot.did.listPendingAuthorizations?wait=1&cursor={cursor}"), - &fixture.agent_token, + &fixture.account_token, ) .await; assert_eq!(status, StatusCode::OK); @@ -1685,7 +1686,7 @@ async fn a_stranger_filling_an_accounts_pending_bound_cannot_lock_it_out() { push(&fixture, &challenge).await; } // The account's own sign-in, pushed last. - let (status, body) = push_as(&fixture, &fixture.agent_did, REQUESTED, &challenge).await; + let (status, body) = push_as(&fixture, &fixture.account_did, REQUESTED, &challenge).await; assert_eq!(status, StatusCode::CREATED, "{body}"); let request_uri = body["request_uri"].as_str().unwrap(); @@ -1693,7 +1694,7 @@ async fn a_stranger_filling_an_accounts_pending_bound_cannot_lock_it_out() { let (status, listed) = get_xrpc( &fixture.app, "bot.did.listPendingAuthorizations", - &fixture.agent_token, + &fixture.account_token, ) .await; assert_eq!(status, StatusCode::OK, "{listed}"); @@ -1733,7 +1734,7 @@ async fn a_request_naming_a_hard_blocked_atom_is_denied_and_grants_none_of_it() let (_, challenge) = code_verifier_and_challenge(); let (status, json) = push_as( &fixture, - &fixture.agent_did, + &fixture.account_did, "atproto transition:generic", &challenge, ) @@ -1744,7 +1745,7 @@ async fn a_request_naming_a_hard_blocked_atom_is_denied_and_grants_none_of_it() let (_, record) = get_xrpc( &fixture.app, &format!("bot.did.getAuthorization?requestUri={request_uri}"), - &fixture.agent_token, + &fixture.account_token, ) .await; assert_eq!(record["verdict"]["kind"], "deny", "{record}"); @@ -1783,14 +1784,14 @@ async fn a_narrow_request_under_a_wide_ceiling_is_granted_at_the_form_it_asked_f ); let (verifier, challenge) = code_verifier_and_challenge(); let asked = "atproto repo:app.bsky.feed.post?action=create"; - let (status, json) = push_as(&fixture, &fixture.agent_did, asked, &challenge).await; + let (status, json) = push_as(&fixture, &fixture.account_did, asked, &challenge).await; assert_eq!(status, StatusCode::CREATED, "{json}"); let request_uri = json["request_uri"].as_str().unwrap().to_owned(); let (_, record) = get_xrpc( &fixture.app, &format!("bot.did.getAuthorization?requestUri={request_uri}"), - &fixture.agent_token, + &fixture.account_token, ) .await; assert_eq!(record["verdict"]["kind"], "allow", "{record}"); @@ -1798,7 +1799,7 @@ async fn a_narrow_request_under_a_wide_ceiling_is_granted_at_the_form_it_asked_f let (status, answer) = post_xrpc( &fixture.app, "bot.did.approveAuthorization", - &fixture.agent_token, + &fixture.account_token, serde_json::json!({ "token": token_for(&fixture, &request_uri) }), ) .await; @@ -1832,7 +1833,7 @@ async fn a_cursor_from_before_a_restart_still_lists() { let (_, listed) = get_xrpc( &fixture.app, "bot.did.listPendingAuthorizations", - &fixture.agent_token, + &fixture.account_token, ) .await; let stale = listed["cursor"].as_str().expect("a cursor").to_owned(); @@ -1847,7 +1848,7 @@ async fn a_cursor_from_before_a_restart_still_lists() { let (status, listed) = get_xrpc( &after.app, &format!("bot.did.listPendingAuthorizations?cursor={stale}"), - &after.agent_token, + &after.account_token, ) .await; assert_eq!(status, StatusCode::OK); @@ -1874,7 +1875,7 @@ async fn a_shutdown_wakes_a_long_poll_instead_of_holding_its_connection() { let (status, listed) = get_xrpc( &fixture.app, "bot.did.listPendingAuthorizations?wait=25", - &fixture.agent_token, + &fixture.account_token, ) .await; assert_eq!(status, StatusCode::OK); @@ -1890,7 +1891,7 @@ async fn a_shutdown_wakes_a_long_poll_instead_of_holding_its_connection() { /// credential, so whatever followed the `request_uri` — the requesting /// client included — got the page and the one-time reference printed on it. /// The reference leaves this server only through -/// `bot.did.getAuthorization`, under the same `Credential::AgentSelf` the +/// `bot.did.getAuthorization`, under the same `Credential::AccountSelf` the /// approval itself takes. /// /// And the account the page is about is readable by a person now, not only @@ -1915,7 +1916,7 @@ async fn the_consent_page_shows_the_account_and_not_the_one_time_reference() { assert!( page.contains(&format!( r#"

{did}

"#, - did = fixture.agent_did + did = fixture.account_did )), "{page}" ); @@ -1924,7 +1925,7 @@ async fn the_consent_page_shows_the_account_and_not_the_one_time_reference() { let (status, record) = get_xrpc( &fixture.app, &format!("bot.did.getAuthorization?requestUri={request_uri}"), - &fixture.agent_token, + &fixture.account_token, ) .await; assert_eq!(status, StatusCode::OK, "{record}"); diff --git a/crates/didbot-serve/tests/record_heap.rs b/crates/didbot-serve/tests/record_heap.rs index 0db097d0..b877dd02 100644 --- a/crates/didbot-serve/tests/record_heap.rs +++ b/crates/didbot-serve/tests/record_heap.rs @@ -227,12 +227,12 @@ struct Agent { token: String, } -async fn provision(run: &Run, agent_id: &str) -> Agent { +async fn provision(run: &Run, account_id: &str) -> Agent { let answer = post( run, - "bot.did.provisionAgent", + "bot.did.createAccount", None, - json!({ "agentId": agent_id }), + json!({ "accountId": account_id }), ) .await; assert_eq!( @@ -245,7 +245,7 @@ async fn provision(run: &Run, agent_id: &str) -> Agent { .as_str() .expect("the response names the account") .to_owned(), - token: answer.body["agentToken"] + token: answer.body["accountToken"] .as_str() .expect("the response carries the write credential") .to_owned(), diff --git a/crates/didbot-serve/tests/record_tombstones.rs b/crates/didbot-serve/tests/record_tombstones.rs index 4c63ad95..9ebeba12 100644 --- a/crates/didbot-serve/tests/record_tombstones.rs +++ b/crates/didbot-serve/tests/record_tombstones.rs @@ -221,7 +221,7 @@ async fn a_recreate_over_a_tombstone_is_judged_as_an_edit_across_a_checkpoint_an .provision(ProvisionRequest::new("sexton", None)) .expect("provisioning should succeed"); let did = provisioned.account.did.as_str().to_owned(); - let token = provisioned.agent_token; + let token = provisioned.account_token; let router = app(pds.clone() as Arc); let edit = assert_recreate_is_judged_as_an_edit( diff --git a/crates/didbot-serve/tests/restore_drill.rs b/crates/didbot-serve/tests/restore_drill.rs index d794af47..cd1743fd 100644 --- a/crates/didbot-serve/tests/restore_drill.rs +++ b/crates/didbot-serve/tests/restore_drill.rs @@ -206,7 +206,7 @@ async fn a_snapshot_of_a_live_volume_serves_what_the_volume_served() { let blob = format!("/xrpc/com.atproto.sync.getBlob?did={did}&cid={cid}"); for (uri, host) in [ - ("/xrpc/bot.did.listAgents", None), + ("/xrpc/bot.did.listAccounts", None), ("/xrpc/com.atproto.sync.listRepos", None), (record.as_str(), None), ("/.well-known/did.json", Some(host.as_str())), @@ -297,9 +297,9 @@ async fn a_restore_short_of_blob_bytes_reports_it_before_anyone_fetches() { ); // Still up, and still holding everything the bytes were not part of. - let agents = json_at(&router, "/xrpc/bot.did.listAgents", None).await; + let agents = json_at(&router, "/xrpc/bot.did.listAccounts", None).await; assert_eq!( - agents["agents"].as_array().map(Vec::len), + agents["accounts"].as_array().map(Vec::len), Some(2), "a lost blob cost the accounts: {agents}" ); diff --git a/crates/didbot-serve/tests/support/mod.rs b/crates/didbot-serve/tests/support/mod.rs index 4d8f56dc..a04411c2 100644 --- a/crates/didbot-serve/tests/support/mod.rs +++ b/crates/didbot-serve/tests/support/mod.rs @@ -65,8 +65,8 @@ pub struct Harness { pub gate: Arc, pub log: Arc, pub sink: Arc, - pub agent_did: String, - pub agent_token: String, + pub account_did: String, + pub account_token: String, } /// The metadata document `client_id` serves: one redirect beside it. @@ -162,8 +162,8 @@ impl Harness { gate, log, sink, - agent_did: provisioned.account.did.as_str().to_owned(), - agent_token: provisioned.agent_token, + account_did: provisioned.account.did.as_str().to_owned(), + account_token: provisioned.account_token, } } @@ -257,7 +257,7 @@ impl Harness { ("scope", scope), ("code_challenge", challenge), ("code_challenge_method", "S256"), - ("login_hint", &self.agent_did), + ("login_hint", &self.account_did), ], ) .await @@ -272,7 +272,7 @@ impl Harness { assert_eq!(status, StatusCode::CREATED, "{pushed}"); let (status, listed) = self - .xrpc_get("bot.did.listPendingAuthorizations", &self.agent_token) + .xrpc_get("bot.did.listPendingAuthorizations", &self.account_token) .await; assert_eq!(status, StatusCode::OK, "{listed}"); let token = listed["pending"] @@ -377,7 +377,7 @@ impl Harness { /// The `Authorization` header the agent presents its own token under. pub fn as_agent(&self) -> String { - format!("Bearer {}", self.agent_token) + format!("Bearer {}", self.account_token) } /// An upload of `bytes` as `content_type`, with the agent's own token. @@ -411,7 +411,7 @@ impl Harness { "com.atproto.repo.createRecord", authorization, json!({ - "repo": self.agent_did, + "repo": self.account_did, "collection": COLLECTION, "record": thing(), }), diff --git a/crates/didbot-serve/tests/token_scope.rs b/crates/didbot-serve/tests/token_scope.rs index 55661f3c..6e733a93 100644 --- a/crates/didbot-serve/tests/token_scope.rs +++ b/crates/didbot-serve/tests/token_scope.rs @@ -53,7 +53,7 @@ async fn a_narrowed_grant_narrows_what_its_token_writes() { "com.atproto.repo.putRecord", &app, json!({ - "repo": harness.agent_did, + "repo": harness.account_did, "collection": COLLECTION, "rkey": rkey, "record": thing(), @@ -73,7 +73,7 @@ async fn a_narrowed_grant_narrows_what_its_token_writes() { .xrpc_post( "com.atproto.repo.deleteRecord", &app, - json!({ "repo": harness.agent_did, "collection": COLLECTION, "rkey": rkey }), + json!({ "repo": harness.account_did, "collection": COLLECTION, "rkey": rkey }), ) .await; assert_eq!(status, StatusCode::FORBIDDEN, "{body}"); @@ -83,7 +83,7 @@ async fn a_narrowed_grant_narrows_what_its_token_writes() { "com.atproto.repo.createRecord", &app, json!({ - "repo": harness.agent_did, + "repo": harness.account_did, "collection": ELSEWHERE, "record": { "$type": ELSEWHERE, "text": "hi", "createdAt": "2026-09-11T10:00:00Z" }, }), @@ -97,7 +97,7 @@ async fn a_narrowed_grant_narrows_what_its_token_writes() { .xrpc_post( "com.atproto.repo.deleteRecord", &harness.as_agent(), - json!({ "repo": harness.agent_did, "collection": COLLECTION, "rkey": rkey }), + json!({ "repo": harness.account_did, "collection": COLLECTION, "rkey": rkey }), ) .await; assert_eq!(status, StatusCode::OK, "{body}"); @@ -114,7 +114,7 @@ async fn a_batch_with_one_write_outside_the_scope_writes_nothing() { "com.atproto.repo.applyWrites", &Harness::as_app(&tokens), json!({ - "repo": harness.agent_did, + "repo": harness.account_did, "writes": [ { "$type": "com.atproto.repo.applyWrites#create", "collection": COLLECTION, "value": thing() }, { "$type": "com.atproto.repo.applyWrites#delete", "collection": COLLECTION, "rkey": "3kabc" }, @@ -125,7 +125,7 @@ async fn a_batch_with_one_write_outside_the_scope_writes_nothing() { assert_eq!(status, StatusCode::FORBIDDEN, "{body}"); let held = harness .registry - .list_records(&harness.agent_did, COLLECTION, &ListParams::new(10)) + .list_records(&harness.account_did, COLLECTION, &ListParams::new(10)) .unwrap(); assert!(held.is_empty(), "{held:?}"); } @@ -149,7 +149,7 @@ async fn a_create_only_token_cannot_overwrite_through_a_batch() { "com.atproto.repo.applyWrites", &app, json!({ - "repo": harness.agent_did, + "repo": harness.account_did, "writes": [{ "$type": "com.atproto.repo.applyWrites#create", "collection": COLLECTION, @@ -167,7 +167,7 @@ async fn a_create_only_token_cannot_overwrite_through_a_batch() { ); let held = harness .registry - .get_record(&harness.agent_did, COLLECTION, rkey) + .get_record(&harness.account_did, COLLECTION, rkey) .unwrap() .unwrap(); assert_eq!(held["text"], thing()["text"], "{held}"); @@ -177,7 +177,7 @@ async fn a_create_only_token_cannot_overwrite_through_a_batch() { "com.atproto.repo.applyWrites", &app, json!({ - "repo": harness.agent_did, + "repo": harness.account_did, "writes": [{ "$type": "com.atproto.repo.applyWrites#create", "collection": COLLECTION, "value": thing() }], }), ) @@ -197,7 +197,7 @@ async fn an_update_only_token_cannot_create_through_a_batch() { "com.atproto.repo.applyWrites", &Harness::as_app(&tokens), json!({ - "repo": harness.agent_did, + "repo": harness.account_did, "writes": [{ "$type": "com.atproto.repo.applyWrites#update", "collection": COLLECTION, @@ -215,7 +215,7 @@ async fn an_update_only_token_cannot_create_through_a_batch() { ); let held = harness .registry - .get_record(&harness.agent_did, COLLECTION, rkey) + .get_record(&harness.account_did, COLLECTION, rkey) .unwrap(); assert!(held.is_none(), "{held:?}"); } @@ -240,14 +240,14 @@ async fn a_write_outside_the_scope_is_logged_against_its_app() { .xrpc_post( "com.atproto.repo.deleteRecord", &Harness::as_app(&tokens), - json!({ "repo": harness.agent_did, "collection": COLLECTION, "rkey": "3kabc" }), + json!({ "repo": harness.account_did, "collection": COLLECTION, "rkey": "3kabc" }), ) .await; assert_eq!(status, StatusCode::FORBIDDEN); let rows = harness.denials(); assert!( - rows.iter().any(|row| row.account == harness.agent_did + rows.iter().any(|row| row.account == harness.account_did && row.client_id.as_deref() == Some(APP) && row.policies_fired == ["token-scope"] && row @@ -267,7 +267,7 @@ async fn a_malformed_collection_is_refused_before_the_scope_logs_it() { .xrpc_post( "com.atproto.repo.deleteRecord", &Harness::as_app(&tokens), - json!({ "repo": harness.agent_did, "collection": "not an nsid", "rkey": "3kabc" }), + json!({ "repo": harness.account_did, "collection": "not an nsid", "rkey": "3kabc" }), ) .await; assert_eq!(status, StatusCode::BAD_REQUEST, "{body}"); @@ -353,7 +353,7 @@ async fn a_loosened_ceiling_never_gives_a_live_login_what_the_agent_was_told_was let (status, created) = harness.create(&app).await; assert_eq!(status, StatusCode::OK, "{created}"); let rkey = created["uri"].as_str().unwrap().rsplit('/').next().unwrap(); - let delete = json!({ "repo": harness.agent_did, "collection": COLLECTION, "rkey": rkey }); + let delete = json!({ "repo": harness.account_did, "collection": COLLECTION, "rkey": rkey }); let (status, _) = harness .xrpc_post("com.atproto.repo.deleteRecord", &app, delete.clone()) .await; diff --git a/crates/didbot-serve/tests/write_gates.rs b/crates/didbot-serve/tests/write_gates.rs index 83b8108d..cabbfbe0 100644 --- a/crates/didbot-serve/tests/write_gates.rs +++ b/crates/didbot-serve/tests/write_gates.rs @@ -48,7 +48,7 @@ fn fixture() -> Fixture { .provision(ProvisionRequest::new("quernstone-gates", None)) .expect("provisioning succeeds"); let did = provisioned.account.did.as_str().to_owned(); - let token = provisioned.agent_token.clone(); + let token = provisioned.account_token.clone(); let registry: Arc = Arc::new(provisioner); let estop = Arc::new(Estop::default()); let app = app_with_auth( @@ -317,17 +317,17 @@ async fn each_stage_refuses_under_its_own_name() { /// destructive ones are the point: an operator who has pulled the brake has /// not consented to a repository being emptied while it is pulled. /// -/// `bot.did.freezeAgent` and `bot.did.deactivateAgent` are deliberately +/// `bot.did.freezeAccount` and `bot.did.deactivateAccount` are deliberately /// absent from this list — each can only ever narrow what an account may do, /// and refusing it under a stop would leave a caller *less* able to restrain /// itself than before. #[tokio::test] async fn a_thrown_revoke_refuses_the_destructive_account_routes() { for nsid in [ - "bot.did.deleteAgent", - "bot.did.unfreezeAgent", - "bot.did.activateAgent", - "bot.did.setAgentPinned", + "bot.did.deleteAccount", + "bot.did.unfreezeAccount", + "bot.did.activateAccount", + "bot.did.setAccountPinned", ] { let f = fixture(); f.estop.throw(EstopMode::Revoke); @@ -347,7 +347,7 @@ async fn a_thrown_revoke_refuses_the_destructive_account_routes() { /// not have been applied by reflex to every route in the module. #[tokio::test] async fn freezing_is_still_permitted_under_a_revoke() { - for nsid in ["bot.did.freezeAgent", "bot.did.deactivateAgent"] { + for nsid in ["bot.did.freezeAccount", "bot.did.deactivateAccount"] { let f = fixture(); f.estop.throw(EstopMode::Revoke); let answer = post( @@ -381,17 +381,17 @@ async fn repeating_a_freeze_refuses_rather_than_silently_agreeing() { let f = fixture(); let did = serde_json::json!({ "did": f.did }).to_string(); - let answer = post(&f.app, "bot.did.unfreezeAgent", Some(&f.token), &did).await; + let answer = post(&f.app, "bot.did.unfreezeAccount", Some(&f.token), &did).await; assert_eq!(answer.status, StatusCode::CONFLICT, "{}", answer.body); assert_eq!(answer.error(), "AccountNotLocked"); assert_eq!( - post(&f.app, "bot.did.freezeAgent", Some(&f.token), &did) + post(&f.app, "bot.did.freezeAccount", Some(&f.token), &did) .await .status, StatusCode::OK ); - let answer = post(&f.app, "bot.did.freezeAgent", Some(&f.token), &did).await; + let answer = post(&f.app, "bot.did.freezeAccount", Some(&f.token), &did).await; assert_eq!(answer.status, StatusCode::CONFLICT, "{}", answer.body); assert_eq!(answer.error(), "AccountAlreadyLocked"); diff --git a/crates/didbot-swarm/src/lib.rs b/crates/didbot-swarm/src/lib.rs index 2f30471a..28369223 100644 --- a/crates/didbot-swarm/src/lib.rs +++ b/crates/didbot-swarm/src/lib.rs @@ -5,9 +5,9 @@ //! repository, to show — and until agents are provisioned by a harness in //! anger the only way to make something happen is to make it up. The swarm //! makes it up through the front door: it provisions accounts through -//! `bot.did.provisionAgent`, writes into them through +//! `bot.did.createAccount`, writes into them through //! `com.atproto.repo.createRecord`, `putRecord`, `deleteRecord` and -//! `uploadBlob`, and ends them through `bot.did.deleteAgent`, so a run +//! `uploadBlob`, and ends them through `bot.did.deleteAccount`, so a run //! exercises the same path a real agent takes rather than a shortcut around //! it — the gates in `docs/write-pipeline.md`, the per-repository queue, the //! blob accounting and the stream numbering, all under whatever concurrency @@ -27,7 +27,7 @@ //! It is a load generator and it says so. Nothing here belongs in a //! deployment and the accounts it mints are disposable. It presents no //! attestation claim to provision, so it runs only against a `.localhost` -//! development stack, the one deployment whose `bot.did.provisionAgent` +//! development stack, the one deployment whose `bot.did.createAccount` //! mints without one; see `didbot_serve::AuthState::unattested_provisioning`. #![forbid(unsafe_code)] @@ -105,10 +105,10 @@ impl SwarmError { /// The endpoints the swarm calls, which is also how its counters are named. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Verb { - /// `bot.did.provisionAgent`. - ProvisionAgent, - /// `bot.did.deleteAgent`. - DeleteAgent, + /// `bot.did.createAccount`. + CreateAccount, + /// `bot.did.deleteAccount`. + DeleteAccount, /// `com.atproto.repo.applyWrites`. ApplyWrites, /// `com.atproto.repo.createRecord`. @@ -126,8 +126,8 @@ pub enum Verb { impl Verb { /// Every verb, in the order the tally prints them. pub const ALL: [Verb; 8] = [ - Verb::ProvisionAgent, - Verb::DeleteAgent, + Verb::CreateAccount, + Verb::DeleteAccount, Verb::ApplyWrites, Verb::CreateRecord, Verb::PutRecord, @@ -140,8 +140,8 @@ impl Verb { /// `signIn`. pub fn method(self) -> &'static str { match self { - Verb::ProvisionAgent => "provisionAgent", - Verb::DeleteAgent => "deleteAgent", + Verb::CreateAccount => "createAccount", + Verb::DeleteAccount => "deleteAccount", Verb::ApplyWrites => "applyWrites", Verb::CreateRecord => "createRecord", Verb::PutRecord => "putRecord", @@ -155,8 +155,8 @@ impl Verb { /// last of its four calls. fn path(self) -> &'static str { match self { - Verb::ProvisionAgent => "/xrpc/bot.did.provisionAgent", - Verb::DeleteAgent => "/xrpc/bot.did.deleteAgent", + Verb::CreateAccount => "/xrpc/bot.did.createAccount", + Verb::DeleteAccount => "/xrpc/bot.did.deleteAccount", Verb::ApplyWrites => "/xrpc/com.atproto.repo.applyWrites", Verb::CreateRecord => "/xrpc/com.atproto.repo.createRecord", Verb::PutRecord => "/xrpc/com.atproto.repo.putRecord", @@ -285,12 +285,12 @@ impl Snapshot { /// Accounts provisioned. pub fn provisioned(&self) -> u64 { - self.get(Verb::ProvisionAgent).ok + self.get(Verb::CreateAccount).ok } /// Accounts deleted. pub fn ended(&self) -> u64 { - self.get(Verb::DeleteAgent).ok + self.get(Verb::DeleteAccount).ok } } @@ -339,9 +339,9 @@ pub struct Agent { /// The only copy the swarm will ever see — see /// `didbot_pds::credential` — carried on the agent the same way the DID /// is, and presented on every write the swarm makes for this agent. - pub agent_token: String, + pub account_token: String, /// The label it was minted under. - pub agent_id: String, + pub account_id: String, /// The session it belongs to. pub task: String, /// The DID of the agent that spawned it, for a subagent. @@ -391,8 +391,8 @@ impl Agent { /// What a record write presents. #[derive(Debug, Clone, Copy)] pub enum Auth<'a> { - /// The agent token `provisionAgent` minted, as `Bearer`. - AgentToken(&'a str), + /// The agent token `createAccount` minted, as `Bearer`. + AccountToken(&'a str), /// An OAuth access token from [`Pds::sign_in`], as `DPoP`, with a fresh /// proof for this request. OAuth(&'a OAuthSession), @@ -400,8 +400,8 @@ pub enum Auth<'a> { impl<'a> Auth<'a> { /// The session when there is one, and the agent token otherwise. - fn of(agent_token: &'a str, session: Option<&'a OAuthSession>) -> Self { - session.map_or(Auth::AgentToken(agent_token), Auth::OAuth) + fn of(account_token: &'a str, session: Option<&'a OAuthSession>) -> Self { + session.map_or(Auth::AccountToken(account_token), Auth::OAuth) } } @@ -423,7 +423,7 @@ struct Health { #[serde(rename_all = "camelCase")] struct Provisioned { did: String, - agent_token: String, + account_token: String, } /// What a write answers with. @@ -492,7 +492,7 @@ impl Pds { fn post(&self, verb: Verb, auth: Auth<'_>) -> Result { let request = self.http.post(self.url(verb)); Ok(match auth { - Auth::AgentToken(token) => request.bearer_auth(token), + Auth::AccountToken(token) => request.bearer_auth(token), Auth::OAuth(session) => request .header( reqwest::header::AUTHORIZATION, @@ -531,28 +531,28 @@ impl Pds { /// carries the latter alongside the former on every [`Agent`] it holds: /// there is no session to take it from, and no way to get it back once /// this call returns. - pub async fn provision(&self, agent_id: &str) -> Result<(String, String), SwarmError> { - let verb = Verb::ProvisionAgent; + pub async fn provision(&self, account_id: &str) -> Result<(String, String), SwarmError> { + let verb = Verb::CreateAccount; let request = self .http .post(self.url(verb)) - .json(&serde_json::json!({ "agentId": agent_id })); + .json(&serde_json::json!({ "accountId": account_id })); let provisioned: Provisioned = self.call(verb, request).await?; - Ok((provisioned.did, provisioned.agent_token)) + Ok((provisioned.did, provisioned.account_token)) } /// Deletes an account, which is how a session ends. /// - /// `agent_token` is that account's own credential, the same one every - /// write carries — `deleteAgent` requires either it or an operator + /// `account_token` is that account's own credential, the same one every + /// write carries — `deleteAccount` requires either it or an operator /// credential, and a load generator has no operator in the loop, so it /// deletes itself the same way. - pub async fn delete(&self, did: &str, agent_token: &str) -> Result<(), SwarmError> { - let verb = Verb::DeleteAgent; + pub async fn delete(&self, did: &str, account_token: &str) -> Result<(), SwarmError> { + let verb = Verb::DeleteAccount; let request = self .http .post(self.url(verb)) - .bearer_auth(agent_token) + .bearer_auth(account_token) .json(&serde_json::json!({ "did": did })); let _: Value = self.call(verb, request).await?; Ok(()) @@ -644,7 +644,7 @@ impl Pds { /// no other say in the matter. pub async fn upload_blob( &self, - agent_token: &str, + account_token: &str, mime_type: &str, bytes: Vec, ) -> Result { @@ -652,7 +652,7 @@ impl Pds { let request = self .http .post(self.url(verb)) - .bearer_auth(agent_token) + .bearer_auth(account_token) .header(reqwest::header::CONTENT_TYPE, mime_type) .body(bytes); let uploaded: Uploaded = self.call(verb, request).await?; @@ -664,14 +664,16 @@ impl Pds { match plan { Plan::Idle => Ok(Receipt::Nothing), Plan::Spawn { - agent_id, sign_in, .. + account_id, + sign_in, + .. } => { - let (did, agent_token) = self.provision(agent_id).await?; + let (did, account_token) = self.provision(account_id).await?; // A failed sign-in leaves the agent writing with its agent // token rather than orphaning an account the swarm would // never delete. The tally counts the failure. let session = if *sign_in { - self.sign_in(&did, &agent_token) + self.sign_in(&did, &account_token) .await .inspect_err(|err| tracing::warn!(%err, did, "sign-in failed")) .ok() @@ -680,24 +682,24 @@ impl Pds { }; Ok(Receipt::Spawned { did, - agent_token, + account_token, session, }) } Plan::End { doomed } => { - for (did, agent_token) in doomed { - self.delete(did, agent_token).await?; + for (did, account_token) in doomed { + self.delete(did, account_token).await?; } Ok(Receipt::Ended) } Plan::Create { did, - agent_token, + account_token, session, collection, record, } => { - let auth = Auth::of(agent_token, session.as_ref()); + let auth = Auth::of(account_token, session.as_ref()); let rkey = self.create_record(did, auth, collection, record).await?; Ok(Receipt::Created { rkey, @@ -706,40 +708,40 @@ impl Pds { } Plan::Update { did, - agent_token, + account_token, session, collection, rkey, record, } => { - let auth = Auth::of(agent_token, session.as_ref()); + let auth = Auth::of(account_token, session.as_ref()); self.put_record(did, auth, collection, rkey, record).await?; Ok(Receipt::Updated) } Plan::Delete { did, - agent_token, + account_token, session, collection, rkey, } => { - let auth = Auth::of(agent_token, session.as_ref()); + let auth = Auth::of(account_token, session.as_ref()); self.delete_record(did, auth, collection, rkey).await?; Ok(Receipt::Deleted) } Plan::Upload { did, - agent_token, + account_token, session, mime_type, bytes, seed, } => { let attachment = self - .upload_blob(agent_token, mime_type, bytes.clone()) + .upload_blob(account_token, mime_type, bytes.clone()) .await?; let record = vocabulary::other(&mut StdRng::seed_from_u64(*seed), &attachment); - let auth = Auth::of(agent_token, session.as_ref()); + let auth = Auth::of(account_token, session.as_ref()); let rkey = self.create_record(did, auth, OTHER, &record).await?; Ok(Receipt::Created { rkey, @@ -1037,21 +1039,21 @@ impl Swarm { fn plan_spawn(&mut self) -> Plan { self.minted += 1; let word = NAMES[self.rng.random_range(0..NAMES.len())]; - let agent_id = format!("{word}-{:x}", self.minted); + let account_id = format!("{word}-{:x}", self.minted); // Decided before the account is minted, because a subagent takes its // work from whatever spawned it: it was asked to help with this, not // sent off to do something else. let spawner = self.spawner(); match spawner.and_then(|index| self.agents.get(index)) { Some(parent) => Plan::Spawn { - agent_id, + account_id, task: parent.task.clone(), parent: Some(parent.did.clone()), depth: parent.depth + 1, sign_in: self.sign_in, }, None => Plan::Spawn { - agent_id, + account_id, task: format!("task-{:x}", self.rng.random::()), parent: None, depth: 0, @@ -1087,7 +1089,7 @@ impl Swarm { for did in doomed { if let Some(index) = self.agents.iter().position(|agent| agent.did == did) { let agent = self.agents.remove(index); - leaving.push((did, agent.agent_token)); + leaving.push((did, agent.account_token)); } } Plan::End { doomed: leaving } @@ -1118,14 +1120,14 @@ impl Swarm { let agent = &mut self.agents[index]; agent.in_flight += 1; let did = agent.did.clone(); - let agent_token = agent.agent_token.clone(); + let account_token = agent.account_token.clone(); let session = agent.session.clone(); match (action, record) { (Action::Upload, _) => { let (mime_type, bytes) = vocabulary::blob(&mut self.rng); Plan::Upload { did, - agent_token, + account_token, session, mime_type, bytes, @@ -1141,7 +1143,7 @@ impl Swarm { }; Plan::Update { did, - agent_token, + account_token, session, collection: held.collection, rkey: held.rkey.clone(), @@ -1153,7 +1155,7 @@ impl Swarm { held.in_flight = true; Plan::Delete { did, - agent_token, + account_token, session, collection: held.collection, rkey: held.rkey.clone(), @@ -1161,7 +1163,7 @@ impl Swarm { } _ => Plan::Create { did, - agent_token, + account_token, session, collection: THING, record: vocabulary::thing(&mut self.rng), @@ -1187,7 +1189,7 @@ impl Swarm { match plan { Plan::Idle | Plan::End { .. } => {} Plan::Spawn { - agent_id, + account_id, task, parent, depth, @@ -1195,14 +1197,14 @@ impl Swarm { } => { if let Some(Receipt::Spawned { did, - agent_token, + account_token, session, }) = receipt { self.agents.push(Agent { did, - agent_token, - agent_id, + account_token, + account_id, task, parent, depth, @@ -1400,7 +1402,7 @@ pub enum Plan { /// Provision an account. Spawn { /// The label to mint under. - agent_id: String, + account_id: String, /// The session it joins. task: String, /// The DID of the agent spawning it, if one is. @@ -1420,7 +1422,7 @@ pub enum Plan { /// The repository. did: String, /// Its credential. - agent_token: String, + account_token: String, /// Its OAuth sign-in, which the write goes through when present. session: Option, /// Where the record goes. @@ -1433,7 +1435,7 @@ pub enum Plan { /// The repository. did: String, /// Its credential. - agent_token: String, + account_token: String, /// Its OAuth sign-in, which the write goes through when present. session: Option, /// Where the record is. @@ -1448,7 +1450,7 @@ pub enum Plan { /// The repository. did: String, /// Its credential. - agent_token: String, + account_token: String, /// Its OAuth sign-in, which the write goes through when present. session: Option, /// Where the record is. @@ -1461,7 +1463,7 @@ pub enum Plan { /// The repository. did: String, /// Its credential, which the upload always presents. - agent_token: String, + account_token: String, /// Its OAuth sign-in, which the record write goes through when /// present. session: Option, @@ -1485,7 +1487,7 @@ pub enum Receipt { /// Its DID. did: String, /// Its credential. - agent_token: String, + account_token: String, /// Its OAuth sign-in, when it signed in. session: Option, }, diff --git a/crates/didbot-swarm/src/oauth.rs b/crates/didbot-swarm/src/oauth.rs index 90fb2bda..82ca7de6 100644 --- a/crates/didbot-swarm/src/oauth.rs +++ b/crates/didbot-swarm/src/oauth.rs @@ -1,5 +1,5 @@ //! An agent signing itself in, the same four calls -//! `didbot-serve/tests/oauth_agent_flow.rs` proves the router answers, run +//! `didbot-serve/tests/oauth_account_flow.rs` proves the router answers, run //! for real over the socket instead of through `tower::ServiceExt::oneshot`. //! //! [`Pds::sign_in`] pushes a request naming the agent as `login_hint`, reads @@ -8,7 +8,7 @@ //! approval redirects to. What it hands back is an [`OAuthSession`]: an //! access token this server's own authorization server minted, bound to a //! DPoP key the sign-in generated. That is a different credential from the -//! one `provisionAgent` returns, and [`crate::Auth::OAuth`] spends it on a +//! one `createAccount` returns, and [`crate::Auth::OAuth`] spends it on a //! record write where the agent token would otherwise go. //! //! Every sign-in uses the loopback `client_id` form @@ -139,23 +139,24 @@ struct TokenResponse { } impl Pds { - /// Signs `agent_did` in, and hands back the session this server's token + /// Signs `account_did` in, and hands back the session this server's token /// endpoint minted. /// - /// `agent_token` is the credential `provisionAgent` minted for this + /// `account_token` is the credential `createAccount` minted for this /// account -- the only one that may read or approve a decision addressed /// to it, since `bot.did.getAuthorization` and - /// `bot.did.approveAuthorization` both take `Credential::AgentSelf`. It + /// `bot.did.approveAuthorization` both take `Credential::AccountSelf`. It /// is spent here only to read and approve; what this returns is a /// different credential entirely. /// /// Counted in the tally as one [`Verb::SignIn`], timed across every call. pub async fn sign_in( &self, - agent_did: &str, - agent_token: &str, + account_did: &str, + account_token: &str, ) -> Result { - self.sign_in_at(agent_did, agent_token, SWARM_SCOPE).await + self.sign_in_at(account_did, account_token, SWARM_SCOPE) + .await } /// [`Pds::sign_in`] asking for `scope` instead of [`SWARM_SCOPE`], for a @@ -165,12 +166,14 @@ impl Pds { /// Counted in the tally as one [`Verb::SignIn`], timed across every call. pub async fn sign_in_at( &self, - agent_did: &str, - agent_token: &str, + account_did: &str, + account_token: &str, scope: &str, ) -> Result { let started = std::time::Instant::now(); - let outcome = self.sign_in_uncounted(agent_did, agent_token, scope).await; + let outcome = self + .sign_in_uncounted(account_did, account_token, scope) + .await; self.tally .record(Verb::SignIn, started.elapsed(), outcome.is_ok()); outcome @@ -178,8 +181,8 @@ impl Pds { async fn sign_in_uncounted( &self, - agent_did: &str, - agent_token: &str, + account_did: &str, + account_token: &str, scope: &str, ) -> Result { let metadata_url = format!("{}/.well-known/oauth-authorization-server", self.base_url()); @@ -214,7 +217,7 @@ impl Pds { ("scope", scope), ("code_challenge", challenge.as_str()), ("code_challenge_method", "S256"), - ("login_hint", agent_did), + ("login_hint", account_did), ]) .send() .await @@ -232,7 +235,7 @@ impl Pds { let response = self .http .get(&record_url) - .bearer_auth(agent_token) + .bearer_auth(account_token) .send() .await .map_err(|source| SwarmError::Unreachable { @@ -249,7 +252,7 @@ impl Pds { let response = self .http .post(&approve_url) - .bearer_auth(agent_token) + .bearer_auth(account_token) .json(&serde_json::json!({ "token": approval_token })) .send() .await diff --git a/crates/didbot-swarm/src/tests.rs b/crates/didbot-swarm/src/tests.rs index 8e80db28..26c0ce85 100644 --- a/crates/didbot-swarm/src/tests.rs +++ b/crates/didbot-swarm/src/tests.rs @@ -208,11 +208,11 @@ fn a_freeze_stops_the_writes_and_a_refused_write_is_never_held() { } } -fn agent(agent_id: &str) -> Agent { +fn agent(account_id: &str) -> Agent { Agent { - did: format!("did:web:{agent_id}.agents.localhost"), - agent_token: format!("test-token-{agent_id}"), - agent_id: agent_id.to_owned(), + did: format!("did:web:{account_id}.agents.localhost"), + account_token: format!("test-token-{account_id}"), + account_id: account_id.to_owned(), task: "task-1".to_owned(), parent: None, depth: 0, @@ -224,11 +224,11 @@ fn agent(agent_id: &str) -> Agent { } /// An agent at `depth`, spawned by `parent`. -fn child(agent_id: &str, parent: &Agent) -> Agent { +fn child(account_id: &str, parent: &Agent) -> Agent { Agent { parent: Some(parent.did.clone()), depth: parent.depth + 1, - ..agent(agent_id) + ..agent(account_id) } } diff --git a/crates/didbot-swarm/tests/decision_bounds.rs b/crates/didbot-swarm/tests/decision_bounds.rs index 91951779..945e3bdd 100644 --- a/crates/didbot-swarm/tests/decision_bounds.rs +++ b/crates/didbot-swarm/tests/decision_bounds.rs @@ -1,6 +1,6 @@ //! The decision store's bounds, proved under load from a real swarm. //! -//! `crates/didbot-serve/tests/oauth_agent_flow.rs` proves each bound holds +//! `crates/didbot-serve/tests/oauth_account_flow.rs` proves each bound holds //! for one account at a time, through `tower::ServiceExt::oneshot`. This //! file proves the same bounds hold under concurrency, across many accounts //! at once, driven the way the swarm drives anything else -- real HTTP over @@ -21,7 +21,7 @@ //! refuses; every refusal is `access_denied`, and every record that made //! it in before the total filled still approves afterward. //! 3. `an_approved_record_is_gone_from_the_list_and_a_second_approval_is_refused` -//! -- the single-account shape `oauth_agent_flow.rs` already covers, kept +//! -- the single-account shape `oauth_account_flow.rs` already covers, kept //! here so this file is a complete read of the store's contract rather //! than only its two concurrency-specific halves. //! @@ -49,7 +49,7 @@ use didbot_swarm::{Action, Auth, Pds, Plan, Swarm, SwarmError, Verb}; const ZONE: &str = "agents.localhost"; /// A server sized for a burst of concurrent sign-ins from one address, -/// rather than the one-account-at-a-time pace `oauth_agent_flow.rs`'s own +/// rather than the one-account-at-a-time pace `oauth_account_flow.rs`'s own /// fixture drives. struct Server { base_url: String, @@ -162,14 +162,14 @@ async fn push( async fn get_authorization( http: &reqwest::Client, base_url: &str, - agent_token: &str, + account_token: &str, request_uri: &str, ) -> serde_json::Value { let response = http .get(format!( "{base_url}/xrpc/bot.did.getAuthorization?requestUri={request_uri}" )) - .bearer_auth(agent_token) + .bearer_auth(account_token) .send() .await .expect("getAuthorization is reachable"); @@ -180,12 +180,12 @@ async fn get_authorization( async fn approve( http: &reqwest::Client, base_url: &str, - agent_token: &str, + account_token: &str, token: &str, ) -> (reqwest::StatusCode, serde_json::Value) { let response = http .post(format!("{base_url}/xrpc/bot.did.approveAuthorization")) - .bearer_auth(agent_token) + .bearer_auth(account_token) .json(&serde_json::json!({ "token": token })) .send() .await @@ -198,11 +198,11 @@ async fn approve( async fn list_pending( http: &reqwest::Client, base_url: &str, - agent_token: &str, + account_token: &str, ) -> serde_json::Value { let response = http .get(format!("{base_url}/xrpc/bot.did.listPendingAuthorizations")) - .bearer_auth(agent_token) + .bearer_auth(account_token) .send() .await .expect("listPendingAuthorizations is reachable"); @@ -325,17 +325,17 @@ async fn past_the_total_bound_refusals_are_access_denied_and_earlier_records_sti let http = didbot_http::client(); let pds = server.pds(); - let mut agent_tokens = Vec::new(); + let mut account_tokens = Vec::new(); for i in 0..agents { let (did, token) = pds .provision(&format!("bound-b-{i}")) .await .expect("provisioning succeeds"); - agent_tokens.push((did, token)); + account_tokens.push((did, token)); } let mut handles = Vec::new(); - for (index, (did, _token)) in agent_tokens.iter().cloned().enumerate() { + for (index, (did, _token)) in account_tokens.iter().cloned().enumerate() { let http = http.clone(); let base_url = server.base_url.clone(); handles.push(tokio::spawn(async move { @@ -383,13 +383,13 @@ async fn past_the_total_bound_refusals_are_access_denied_and_earlier_records_sti // Every earlier record still approves. let mut approved = 0; for (index, request_uri) in &accepted { - let (_, agent_token) = &agent_tokens[*index]; - let record = get_authorization(&http, &server.base_url, agent_token, request_uri).await; + let (_, account_token) = &account_tokens[*index]; + let record = get_authorization(&http, &server.base_url, account_token, request_uri).await; let token = record["token"] .as_str() .expect("an accepted record carries a token") .to_owned(); - let (status, answer) = approve(&http, &server.base_url, agent_token, &token).await; + let (status, answer) = approve(&http, &server.base_url, account_token, &token).await; assert_eq!(status, reqwest::StatusCode::OK, "{answer}"); assert_eq!( server.decisions.get(request_uri).unwrap().state, @@ -414,7 +414,7 @@ async fn an_approved_record_is_gone_from_the_list_and_a_second_approval_is_refus let server = Server::start(8, 512).await; let http = didbot_http::client(); let pds = server.pds(); - let (did, agent_token) = pds + let (did, account_token) = pds .provision("bound-c") .await .expect("provisioning succeeds"); @@ -423,26 +423,26 @@ async fn an_approved_record_is_gone_from_the_list_and_a_second_approval_is_refus assert_eq!(status, reqwest::StatusCode::CREATED, "{body}"); let request_uri = body["request_uri"].as_str().unwrap().to_owned(); - let before = list_pending(&http, &server.base_url, &agent_token).await; + let before = list_pending(&http, &server.base_url, &account_token).await; assert_eq!(before["pending"].as_array().unwrap().len(), 1); - let record = get_authorization(&http, &server.base_url, &agent_token, &request_uri).await; + let record = get_authorization(&http, &server.base_url, &account_token, &request_uri).await; let token = record["token"].as_str().unwrap().to_owned(); - let (status, answer) = approve(&http, &server.base_url, &agent_token, &token).await; + let (status, answer) = approve(&http, &server.base_url, &account_token, &token).await; assert_eq!(status, reqwest::StatusCode::OK, "{answer}"); assert_eq!( server.decisions.get(&request_uri).unwrap().state, DecisionState::Approved ); - let after = list_pending(&http, &server.base_url, &agent_token).await; + let after = list_pending(&http, &server.base_url, &account_token).await; assert!( after["pending"].as_array().unwrap().is_empty(), "an approved record is still listed: {after}" ); - let (status, body) = approve(&http, &server.base_url, &agent_token, &token).await; + let (status, body) = approve(&http, &server.base_url, &account_token, &token).await; assert_eq!( status, reqwest::StatusCode::BAD_REQUEST, @@ -458,24 +458,24 @@ async fn an_approved_record_is_gone_from_the_list_and_a_second_approval_is_refus /// Not one of the three properties: proof that a sign-in is worth doing at /// all. `Pds::sign_in` hands back an access token this server's own /// authorization server minted, and that token -- not the raw agent token -/// `provisionAgent` returned -- is what writes, revises and removes the +/// `createAccount` returned -- is what writes, revises and removes the /// record, each with a fresh DPoP proof the server verifies. #[tokio::test] async fn a_signed_in_agent_writes_with_its_oauth_token() { let server = Server::start(8, 512).await; let pds = server.pds(); - let (did, agent_token) = pds + let (did, account_token) = pds .provision("signs-in") .await .expect("provisioning succeeds"); let session = pds - .sign_in(&did, &agent_token) + .sign_in(&did, &account_token) .await .expect("the sign-in completes: PAR, read, approve, exchange"); assert_ne!( session.access_token(), - agent_token, + account_token, "the OAuth access token is a different credential from the agent token" ); @@ -561,9 +561,9 @@ async fn a_swarm_that_signs_in_writes_through_its_tokens() { /// `plan` with its agent token replaced by one no server issued. fn without_agent_token(mut plan: Plan) -> Plan { match &mut plan { - Plan::Create { agent_token, .. } - | Plan::Update { agent_token, .. } - | Plan::Delete { agent_token, .. } => *agent_token = "not-an-agent-token".to_owned(), + Plan::Create { account_token, .. } + | Plan::Update { account_token, .. } + | Plan::Delete { account_token, .. } => *account_token = "not-an-agent-token".to_owned(), other => panic!("a write was planned, not {other:?}"), } plan diff --git a/crates/didbot-swarm/tests/policy_rollout.rs b/crates/didbot-swarm/tests/policy_rollout.rs index f8f8c2b6..ac869169 100644 --- a/crates/didbot-swarm/tests/policy_rollout.rs +++ b/crates/didbot-swarm/tests/policy_rollout.rs @@ -95,7 +95,7 @@ async fn a_deployed_policy_rolls_out_and_is_enforced() { operator .put_record( &scenario.operator_did, - Auth::AgentToken(&scenario.operator_token), + Auth::AccountToken(&scenario.operator_token), "bot.did.operator", &governed_host, &claim, @@ -112,7 +112,7 @@ async fn a_deployed_policy_rolls_out_and_is_enforced() { ); nudge(&scenario.governed_url).await; - let (agent_did, agent_token) = wait_for(PATIENCE, "the claim to stand", || async { + let (account_did, account_token) = wait_for(PATIENCE, "the claim to stand", || async { governed.provision("scribe").await.ok() }) .await; @@ -121,8 +121,8 @@ async fn a_deployed_policy_rolls_out_and_is_enforced() { // refuse lands -- without this the refusal below could be anything. let landed = governed .create_record( - &agent_did, - Auth::AgentToken(&agent_token), + &account_did, + Auth::AccountToken(&account_token), "app.bsky.feed.post", &post(WITH_A_LINK), ) @@ -197,8 +197,8 @@ async fn a_deployed_policy_rolls_out_and_is_enforced() { // generic one, which is how an agent's operator learns what to change. let refusal = governed .create_record( - &agent_did, - Auth::AgentToken(&agent_token), + &account_did, + Auth::AccountToken(&account_token), "app.bsky.feed.post", &post(WITH_A_LINK), ) @@ -214,8 +214,8 @@ async fn a_deployed_policy_rolls_out_and_is_enforced() { governed .create_record( - &agent_did, - Auth::AgentToken(&agent_token), + &account_did, + Auth::AccountToken(&account_token), "app.bsky.feed.post", &post(WITHOUT_A_LINK), ) diff --git a/crates/didbot-swarm/tests/write_load.rs b/crates/didbot-swarm/tests/write_load.rs index 2df2e6b5..3395a7bf 100644 --- a/crates/didbot-swarm/tests/write_load.rs +++ b/crates/didbot-swarm/tests/write_load.rs @@ -241,7 +241,7 @@ async fn the_stream_stays_contiguous_under_concurrent_writes() { Verb::PutRecord, Verb::DeleteRecord, Verb::UploadBlob, - Verb::DeleteAgent, + Verb::DeleteAccount, ] { assert!(snapshot.get(verb).ok > 0, "{verb:?} never ran:\n{snapshot}"); } diff --git a/crates/didbot/tests/announce_states.rs b/crates/didbot/tests/announce_states.rs index 5ad24972..1f3d20ec 100644 --- a/crates/didbot/tests/announce_states.rs +++ b/crates/didbot/tests/announce_states.rs @@ -21,7 +21,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; -use didbot::identity::AgentDid; +use didbot::identity::AccountDid; use didbot::identity::Zone; use didbot::identity::ZoneRegistry; use didbot::key::SigningKey; @@ -31,9 +31,9 @@ use didbot::pds::HostedDid; /// ordinary record that is not one of its own. const THING: &str = "com.example.thing"; use didbot::pds::{ - AccountState, AccountStatus, AccountStore, Actor, AgentAccount, Hold, Lock, MemoryAccountStore, - ProvisionRequest, Provisioner, RecordingRepoSink, Registry, RepoEvent, Retention, StoreError, - Swap, Tag, + AccountState, AccountStatus, AccountStore, Actor, Hold, HostedAccount, Lock, + MemoryAccountStore, ProvisionRequest, Provisioner, RecordingRepoSink, Registry, RepoEvent, + Retention, StoreError, Swap, Tag, }; const ZONE: &str = "agents.localhost"; @@ -78,11 +78,11 @@ impl WedgedAtActivation { } impl AccountStore for WedgedAtActivation { - fn insert(&self, account: AgentAccount, key: SigningKey) -> Result<(), StoreError> { + fn insert(&self, account: HostedAccount, key: SigningKey) -> Result<(), StoreError> { self.inner.insert(account, key) } - fn get(&self, did: &HostedDid) -> Option { + fn get(&self, did: &HostedDid) -> Option { self.inner.get(did) } @@ -90,7 +90,7 @@ impl AccountStore for WedgedAtActivation { self.inner.signing_key(did) } - fn remove(&self, did: &HostedDid) -> Result { + fn remove(&self, did: &HostedDid) -> Result { if self.refusing.load(Ordering::SeqCst) { return Err(StoreError::Backend( "the store lost its medium at activation".to_owned(), @@ -112,7 +112,7 @@ impl AccountStore for WedgedAtActivation { self.inner.set_state(did, state) } - fn list(&self) -> Vec { + fn list(&self) -> Vec { self.inner.list() } fn hang(&self, did: &HostedDid, tag: Tag) -> Result<(), StoreError> { @@ -130,7 +130,7 @@ impl AccountStore for WedgedAtActivation { fn set_parent(&self, did: &HostedDid, parent: &HostedDid) -> Result<(), StoreError> { self.inner.set_parent(did, parent) } - fn children(&self, parent: &HostedDid) -> Vec { + fn children(&self, parent: &HostedDid) -> Vec { self.inner.children(parent) } } @@ -164,9 +164,9 @@ impl Fixture { } /// An ordinary account, provisioned all the way to `Active`. - fn active(&self, agent_id: &str) -> String { + fn active(&self, account_id: &str) -> String { self.registry - .provision(ProvisionRequest::new(agent_id, None)) + .provision(ProvisionRequest::new(account_id, None)) .expect("provisioning succeeds") .account .did @@ -185,18 +185,18 @@ impl Fixture { /// repository and no published hostname left — enough to ask what the /// state announces, not enough to delete. The delete-side question is /// asked of [`Self::crashed_before_activation`] instead. - fn stranded(&self, agent_id: &str) -> String { + fn stranded(&self, account_id: &str) -> String { self.store.refuse(true); let error = self .registry - .provision(ProvisionRequest::new(agent_id, None)) + .provision(ProvisionRequest::new(account_id, None)) .expect_err("the store refused activation, so provisioning reports failure"); self.store.refuse(false); let did = self .registry .accounts() .into_iter() - .find(|account| account.agent_id == agent_id) + .find(|account| account.account_id == account_id) .unwrap_or_else(|| panic!("the account survived the failed activation: {error}")); assert_eq!( did.state, @@ -224,10 +224,10 @@ impl Fixture { /// swept away, because a caller cannot un-announce them either. Tests /// using this account therefore measure what a call *adds*, which is the /// same thing the active-account deletion test measures. - fn crashed_before_activation(&self, agent_id: &str) -> String { - let did = self.active(agent_id); + fn crashed_before_activation(&self, account_id: &str) -> String { + let did = self.active(account_id); let parsed = HostedDid::host( - AgentDid::parse(&did).expect("the provisioner minted it"), + AccountDid::parse(&did).expect("the provisioner minted it"), &ZoneRegistry::single(Zone::new(ZONE).expect("zone host is valid")), ) .expect("minted under the zone"); @@ -605,11 +605,11 @@ fn concurrent_provisioning_keeps_each_accounts_identity_ahead_of_its_commits() { let fixture = Arc::new(Fixture::start()); let dids = Arc::new(Mutex::new(Vec::new())); std::thread::scope(|scope| { - for agent_id in ["hoggerel", "swannikin"] { + for account_id in ["hoggerel", "swannikin"] { let fixture = Arc::clone(&fixture); let dids = Arc::clone(&dids); scope.spawn(move || { - let did = fixture.active(agent_id); + let did = fixture.active(account_id); dids.lock().expect("the lock is not poisoned").push(did); }); } diff --git a/crates/didbot/tests/conformance.rs b/crates/didbot/tests/conformance.rs index 2827c602..163957d8 100644 --- a/crates/didbot/tests/conformance.rs +++ b/crates/didbot/tests/conformance.rs @@ -94,7 +94,7 @@ fn the_excused_nsid_vector_exceeds_the_authority_limit_the_spec_sets() { ); } -/// Handles, against the generic validator. `AgentDid`'s hostname rules are +/// Handles, against the generic validator. `AccountDid`'s hostname rules are /// deliberately stricter and are not what these vectors describe; see /// [`didbot::identity::handle`]. #[test] @@ -104,7 +104,7 @@ fn handle_syntax() { /// DIDs of every method, against [`didbot::identity::validate_did`]. /// -/// Explicitly *not* against `AgentDid::parse`, which is this project's own +/// Explicitly *not* against `AccountDid::parse`, which is this project's own /// narrower rule — did:web only, hostname-level, loopback ports. Nearly every /// vector here would fail against it, and every one of those failures would be /// the type working as designed. The two are tied together instead by diff --git a/crates/didbot/tests/conformance/emit.rs b/crates/didbot/tests/conformance/emit.rs index 0f25555c..da72fdb9 100644 --- a/crates/didbot/tests/conformance/emit.rs +++ b/crates/didbot/tests/conformance/emit.rs @@ -8,7 +8,7 @@ //! generating real output and feeding it back through the parsers the vectors //! validate. -use didbot::identity::{validate_did, validate_handle, AgentDid, Zone}; +use didbot::identity::{validate_did, validate_handle, AccountDid, Zone}; use didbot::pds::Registry as _; use didbot::pds::{tid, validate_record_key, AtUri, MemoryRecordStore, Precondition, RecordStore}; @@ -171,7 +171,7 @@ const AGENT_IDS: &[&str] = &[ #[test] fn every_minted_did_is_a_valid_generic_did() { - // The point of the test is the direction of the implication: `AgentDid` is + // The point of the test is the direction of the implication: `AccountDid` is // much stricter than the generic rule, so everything it mints must also be // a DID by the protocol's own definition. If it ever is not, the strictness // has stopped being a subset and has become a dialect. @@ -185,9 +185,9 @@ fn every_minted_did_is_a_valid_generic_did() { ]; for zone in &zones { - for agent_id in AGENT_IDS { - let did = AgentDid::mint(zone, agent_id).unwrap_or_else(|err| { - panic!("minting {agent_id} under {} failed: {err}", zone.host()) + for account_id in AGENT_IDS { + let did = AccountDid::mint(zone, account_id).unwrap_or_else(|err| { + panic!("minting {account_id} under {} failed: {err}", zone.host()) }); validate_did(did.as_str()) .unwrap_or_else(|err| panic!("minted did {did} is not a valid generic did: {err}")); @@ -203,8 +203,8 @@ fn every_minted_handle_is_a_valid_handle() { // top-level domain, and a development-only DID that could never be a real // handle is the correct outcome, not a bug. let zone = Zone::new("agents.example.com").expect("a subdomain is a zone"); - for agent_id in AGENT_IDS { - let did = AgentDid::mint(&zone, agent_id).expect("a valid agent id mints"); + for account_id in AGENT_IDS { + let did = AccountDid::mint(&zone, account_id).expect("a valid agent id mints"); validate_handle(did.host()) .unwrap_or_else(|err| panic!("minted hostname {} is not a handle: {err}", did.host())); } @@ -292,9 +292,9 @@ fn provisioner(named: bool) -> didbot::pds::Provisioner Vec { (0..count) .map(|n| { - let agent_id = format!("sess-{n:04}"); - pds.provision(didbot::pds::ProvisionRequest::new(&agent_id, None)) - .unwrap_or_else(|err| panic!("provisioning {agent_id} failed: {err}")) + let account_id = format!("sess-{n:04}"); + pds.provision(didbot::pds::ProvisionRequest::new(&account_id, None)) + .unwrap_or_else(|err| panic!("provisioning {account_id} failed: {err}")) }) .collect() } diff --git a/crates/didbot/tests/conformance/wire.rs b/crates/didbot/tests/conformance/wire.rs index ff44b635..58133c84 100644 --- a/crates/didbot/tests/conformance/wire.rs +++ b/crates/didbot/tests/conformance/wire.rs @@ -126,7 +126,7 @@ fn server() -> (axum::Router, String, String) { .provision(ProvisionRequest::new("wire-conformance", None)) .expect("provisioning succeeds"); let did = provisioned.account.did.to_string(); - let token = provisioned.agent_token; + let token = provisioned.account_token; let app = didbot::serve::app(pds as Arc); (app, did, token) } @@ -759,7 +759,7 @@ type Step<'a> = (&'a str, Box); /// `getRepoStatus` says on request what `#account` said on the stream. /// -/// Both read one mapping, `AgentAccount::sync_status`, and this drives them +/// Both read one mapping, `HostedAccount::sync_status`, and this drives them /// from the outside to prove it: the answer after every step of a real /// lifecycle is the last frame the stream carried, and every position the /// wire table enumerates — each state, alone and under each lock — renders @@ -1118,7 +1118,7 @@ async fn an_agent_token_may_not_write_another_accounts_repository() { let intruder_token = pds .provision(ProvisionRequest::new("wire-conformance-intruder", None)) .expect("provisioning succeeds") - .agent_token; + .account_token; let app = didbot::serve::app(pds as Arc); let refused = procedure( @@ -1260,7 +1260,7 @@ fn server_with_estop() -> ( .provision(ProvisionRequest::new("wire-conformance-sessions", None)) .expect("provisioning succeeds"); let did = provisioned.account.did.to_string(); - let token = provisioned.agent_token; + let token = provisioned.account_token; let estop = std::sync::Arc::new(didbot::pds::Estop::default()); let app = didbot::serve::app_with_auth( pds as Arc, diff --git a/crates/didbot/tests/end_to_end.rs b/crates/didbot/tests/end_to_end.rs index 97c542c5..ba04439e 100644 --- a/crates/didbot/tests/end_to_end.rs +++ b/crates/didbot/tests/end_to_end.rs @@ -19,7 +19,7 @@ use axum::body::Body; use axum::http::{Request, StatusCode}; use tower::ServiceExt; -use didbot::identity::{resolve, AgentDid, DidDocumentSource, ResolveError, Zone}; +use didbot::identity::{resolve, AccountDid, DidDocumentSource, ResolveError, Zone}; use didbot::pds::{MemoryAccountStore, Provisioner, Registry}; use didbot::serve::{app_with_auth, AuthState, HealthState}; @@ -53,8 +53,8 @@ fn app(registry: Arc) -> axum::Router { ) } -fn provision_body(agent_id: &str) -> String { - serde_json::json!({ "agentId": agent_id }).to_string() +fn provision_body(account_id: &str) -> String { + serde_json::json!({ "accountId": account_id }).to_string() } /// Sends one request to the router and returns the status and body. @@ -83,7 +83,7 @@ async fn post(app: &axum::Router, path: &str, body: String) -> (StatusCode, serd } /// [`post`], carrying an agent token as `Authorization: Bearer <..>` — what -/// `deleteAgent` and `setAgentPinned` need from a caller acting on its own +/// `deleteAccount` and `setAccountPinned` need from a caller acting on its own /// account. async fn authed_post( app: &axum::Router, @@ -156,18 +156,18 @@ async fn an_agent_is_provisioned_served_resolved_and_deleted() { // 1. Provision. let (status, body) = post( &app, - "/xrpc/bot.did.provisionAgent", + "/xrpc/bot.did.createAccount", provision_body("scratch"), ) .await; assert_eq!(status, StatusCode::OK, "provisioning failed: {body}"); let did_string = body["did"].as_str().expect("a did came back").to_string(); - let agent_token = body["agentToken"] + let account_token = body["accountToken"] .as_str() .expect("a write credential came back") .to_string(); - let did = AgentDid::parse(&did_string).expect("the server minted a valid identifier"); - assert_eq!(did.agent_id(), "scratch"); + let did = AccountDid::parse(&did_string).expect("the server minted a valid identifier"); + assert_eq!(did.account_id(), "scratch"); assert_eq!(did.host(), format!("scratch.{ZONE}")); // 2. The DID document is served at the agent's own hostname, and the real @@ -184,21 +184,21 @@ async fn an_agent_is_provisioned_served_resolved_and_deleted() { let (status, body) = call( &app, Request::builder() - .uri("/xrpc/bot.did.listAgents") + .uri("/xrpc/bot.did.listAccounts") .body(Body::empty()) .expect("request builds"), ) .await; assert_eq!(status, StatusCode::OK); - assert_eq!(body["agents"].as_array().expect("a list").len(), 1); + assert_eq!(body["accounts"].as_array().expect("a list").len(), 1); // 4. Delete, and the data goes while the identity stays: an erasure is // the account's own end-of-session act, and a `did:web` document that // stopped resolving would break every signature the agent ever made. let (status, _) = authed_post( &app, - "/xrpc/bot.did.deleteAgent", - &agent_token, + "/xrpc/bot.did.deleteAccount", + &account_token, serde_json::json!({ "did": did_string }).to_string(), ) .await; @@ -228,8 +228,8 @@ async fn a_refused_provisioning_leaves_nothing_behind() { let registry = registry(); let app = app(registry.clone()); - let body = serde_json::json!({ "agentId": "impostor", "handle": "not a handle" }); - let (status, _) = post(&app, "/xrpc/bot.did.provisionAgent", body.to_string()).await; + let body = serde_json::json!({ "accountId": "impostor", "handle": "not a handle" }); + let (status, _) = post(&app, "/xrpc/bot.did.createAccount", body.to_string()).await; assert_eq!(status, StatusCode::BAD_REQUEST); assert!(registry.accounts().is_empty()); @@ -257,13 +257,8 @@ async fn two_agents_get_two_identifiers_and_two_hostnames() { let registry = registry(); let app = app(registry.clone()); - let (first, _) = post( - &app, - "/xrpc/bot.did.provisionAgent", - provision_body("alpha"), - ) - .await; - let (second, _) = post(&app, "/xrpc/bot.did.provisionAgent", provision_body("beta")).await; + let (first, _) = post(&app, "/xrpc/bot.did.createAccount", provision_body("alpha")).await; + let (second, _) = post(&app, "/xrpc/bot.did.createAccount", provision_body("beta")).await; assert_eq!(first, StatusCode::OK); assert_eq!(second, StatusCode::OK); @@ -289,10 +284,10 @@ async fn a_repeated_agent_id_is_refused_as_a_conflict_and_not_as_a_failure() { let registry = registry(); let app = app(registry.clone()); - let (first, _) = post(&app, "/xrpc/bot.did.provisionAgent", provision_body("twin")).await; + let (first, _) = post(&app, "/xrpc/bot.did.createAccount", provision_body("twin")).await; assert_eq!(first, StatusCode::OK); - let (second, body) = post(&app, "/xrpc/bot.did.provisionAgent", provision_body("twin")).await; + let (second, body) = post(&app, "/xrpc/bot.did.createAccount", provision_body("twin")).await; assert_eq!( second, StatusCode::CONFLICT, @@ -316,24 +311,24 @@ async fn a_repeated_agent_id_is_refused_as_a_conflict_and_not_as_a_failure() { /// row. The account's own erasure goes through it, and what the pin refuses /// — as a `409` under a name of its own rather than the /// `409 AccountAlreadyExists` a duplicate gets — is the row's removal, until -/// somebody calls `setAgentPinned` over the credential the account holds. +/// somebody calls `setAccountPinned` over the credential the account holds. #[tokio::test(flavor = "multi_thread")] async fn a_pin_lets_an_agent_erase_itself_and_refuses_the_rows_removal_as_a_conflict() { let registry = registry(); let app = app(registry.clone()); - let (status, body) = post(&app, "/xrpc/bot.did.provisionAgent", provision_body("kept")).await; + let (status, body) = post(&app, "/xrpc/bot.did.createAccount", provision_body("kept")).await; assert_eq!(status, StatusCode::OK, "provisioning failed: {body}"); let did = body["did"].as_str().expect("a did came back").to_string(); - let agent_token = body["agentToken"] + let account_token = body["accountToken"] .as_str() .expect("a write credential came back") .to_string(); let (status, body) = authed_post( &app, - "/xrpc/bot.did.setAgentPinned", - &agent_token, + "/xrpc/bot.did.setAccountPinned", + &account_token, serde_json::json!({ "did": did, "pinned": true }).to_string(), ) .await; @@ -341,8 +336,8 @@ async fn a_pin_lets_an_agent_erase_itself_and_refuses_the_rows_removal_as_a_conf let (status, body) = authed_post( &app, - "/xrpc/bot.did.deleteAgent", - &agent_token, + "/xrpc/bot.did.deleteAccount", + &account_token, serde_json::json!({ "did": did }).to_string(), ) .await; diff --git a/crates/didbot/tests/handshake.rs b/crates/didbot/tests/handshake.rs index ff395921..064dd1d5 100644 --- a/crates/didbot/tests/handshake.rs +++ b/crates/didbot/tests/handshake.rs @@ -97,8 +97,8 @@ fn cold_server(operator_did: &str) -> Arc { /// provisioning, so what stands between a caller and an account on an /// unclaimed server is the e-stop this file's tests exercise, not anything /// about the caller. -fn provision_body(agent_id: &str) -> String { - json!({ "agentId": agent_id }).to_string() +fn provision_body(account_id: &str) -> String { + json!({ "accountId": account_id }).to_string() } /// Sends one request to the didbot router and returns the status and body. @@ -168,7 +168,7 @@ async fn fake_operator_pds() -> OperatorPds { let addr: SocketAddr = listener.local_addr().expect("local addr"); let endpoint = format!("http://{addr}"); // The percent-encoded port is `did:web`'s own spelling for a host with - // one, and `AgentDid` parses it back into the http URL the poll fetches. + // one, and `AccountDid` parses it back into the http URL the poll fetches. let did = format!("did:web:{}%3A{}", addr.ip(), addr.port()); let records: Arc>> = Arc::new(Mutex::new(HashMap::new())); @@ -463,11 +463,11 @@ impl Booted { /// Whether provisioning is presently allowed, asserting that the only /// two answers are the two this file is about: minted, or `Halted`. - async fn can_provision(&self, agent_id: &str) -> bool { + async fn can_provision(&self, account_id: &str) -> bool { let (status, body) = post_json( &self.app, - "/xrpc/bot.did.provisionAgent", - provision_body(agent_id), + "/xrpc/bot.did.createAccount", + provision_body(account_id), ) .await; match status { @@ -933,7 +933,7 @@ impl DocumentFetcher for ConstFetcher { /// `did:web` spells a port as a percent-encoded colon, and `%` is not in /// atproto's record-key character set while `:` is. So the hostname the /// operator types is not a key any repository can store, and the server's -/// own poll — which derives the key from its own DID, `AgentDid::authority` +/// own poll — which derives the key from its own DID, `AccountDid::authority` /// — looks for the decoded form. Keying the write by the argument therefore /// fails twice over, and the failure surfaces as `WriteError::Refused`, /// whose message blames the operator's OAuth grant. @@ -980,7 +980,7 @@ async fn a_server_on_a_port_is_claimed_at_the_key_its_reader_looks_up() { ) .await; - let expected = didbot::identity::did::AgentDid::parse(&subject) + let expected = didbot::identity::did::AccountDid::parse(&subject) .expect("the reader derives a key from a did:web") .authority(); let written: Vec = { diff --git a/crates/didbot/tests/relay_view.rs b/crates/didbot/tests/relay_view.rs index 4048d01a..1976660d 100644 --- a/crates/didbot/tests/relay_view.rs +++ b/crates/didbot/tests/relay_view.rs @@ -10,7 +10,7 @@ //! //! This binary closes that loop. A relay's half is a WebSocket opened on //! `/xrpc/com.atproto.sync.subscribeRepos` with no credential; a client's -//! half is `bot.did.provisionAgent`, `com.atproto.repo.createRecord`, +//! half is `bot.did.createAccount`, `com.atproto.repo.createRecord`, //! `com.atproto.repo.getRecord`, `com.atproto.repo.listRecords`, //! `com.atproto.server.describeServer`, `com.atproto.sync.getLatestCommit`, //! `/.well-known/atproto-did` and `/.well-known/did.json`, over HTTP with @@ -224,14 +224,14 @@ async fn a_stranger_on_the_stream_watches_an_account_appear_and_write() { let provisioned = outsider .post_json( - "/xrpc/bot.did.provisionAgent", + "/xrpc/bot.did.createAccount", None, - serde_json::json!({ "agentId": "kestrel" }), + serde_json::json!({ "accountId": "kestrel" }), ) .await; let did = json_str(&provisioned, "did"); let handle = json_str(&provisioned, "handle"); - let token = json_str(&provisioned, "agentToken"); + let token = json_str(&provisioned, "accountToken"); // Identity, then account, then whatever commits provisioning itself // writes. The order is the one a consumer needs: nothing may name a @@ -365,9 +365,9 @@ async fn the_identifiers_the_stream_announced_resolve_from_outside() { outsider .post_json( - "/xrpc/bot.did.provisionAgent", + "/xrpc/bot.did.createAccount", None, - serde_json::json!({ "agentId": "quernstone" }), + serde_json::json!({ "accountId": "quernstone" }), ) .await; diff --git a/crates/didbot/tests/repo_invariants.rs b/crates/didbot/tests/repo_invariants.rs index 4026afdf..e39c10cc 100644 --- a/crates/didbot/tests/repo_invariants.rs +++ b/crates/didbot/tests/repo_invariants.rs @@ -72,9 +72,9 @@ impl Stack { Self { registry, app } } - fn account(&self, agent_id: &str) -> String { + fn account(&self, account_id: &str) -> String { self.registry - .provision(ProvisionRequest::new(agent_id, None)) + .provision(ProvisionRequest::new(account_id, None)) .expect("provisioning succeeds") .account .did diff --git a/crates/didbot/tests/reservation.rs b/crates/didbot/tests/reservation.rs index 2420dec7..96359e65 100644 --- a/crates/didbot/tests/reservation.rs +++ b/crates/didbot/tests/reservation.rs @@ -535,7 +535,7 @@ async fn boot_claimed(names: &[&str]) -> Claimed { /// The record key a host's vouch is written at: its own hostname, as /// `didbot operate` derives it from the DID. fn rkey_of(did: &str) -> String { - didbot::identity::AgentDid::parse(did) + didbot::identity::AccountDid::parse(did) .expect("a did:web") .authority() } @@ -737,9 +737,9 @@ async fn a_reservation_serves_a_document_and_nothing_else() { // The roster says what it is, so an operator reading the pending set // sees a host waiting rather than an agent that stalled. - let (status, agents) = get_json(&booted.app, "/xrpc/bot.did.listAgents").await; + let (status, agents) = get_json(&booted.app, "/xrpc/bot.did.listAccounts").await; assert_eq!(status, StatusCode::OK, "{agents}"); - let listed = &agents["agents"][0]; + let listed = &agents["accounts"][0]; assert_eq!(listed["did"], did); assert_eq!(listed["state"], "provisioning"); assert_eq!(listed["kind"], "host"); @@ -905,7 +905,7 @@ async fn an_expired_reservation_is_reaped_and_its_name_is_mintable_again() { "reaping a reservation announces nothing either" ); - // The name: free, and not `FormerAgent`. The second reservation draws + // The name: free, and not `FormerAccount`. The second reservation draws // the same name from the pool, which a burned name would refuse. assert_eq!( booted.naming.registry().reservation_of("mossy-vole"), @@ -1136,11 +1136,11 @@ async fn an_operators_vouch_moves_a_reservation_to_active_exactly_once() { .any(|method| method["publicKeyMultibase"] == key.multibase), "the vouched key stays in the document after activation" ); - let (status, listed) = get_json(app, "/xrpc/bot.did.listAgents").await; + let (status, listed) = get_json(app, "/xrpc/bot.did.listAccounts").await; assert_eq!(status, StatusCode::OK); - let host = listed["agents"] + let host = listed["accounts"] .as_array() - .expect("agents") + .expect("accounts") .iter() .find(|agent| agent["did"] == did) .expect("listed"); @@ -1311,8 +1311,8 @@ async fn provisioning_without_a_claim_is_refused_by_name() { let booted = boot(&["mossy-vole"], a_day(), 6); let (status, body) = post_json( &booted.app, - "/xrpc/bot.did.provisionAgent", - json!({ "agentId": "ctx-0" }), + "/xrpc/bot.did.createAccount", + json!({ "accountId": "ctx-0" }), ) .await; assert_eq!(status, StatusCode::FORBIDDEN, "{body}"); @@ -1348,26 +1348,26 @@ async fn an_admitted_host_attests_the_contexts_it_spawns() { // A claim is signed for one provisioning request, so it names the agent // id it will be spent on. - let claim = |agent_id: &str, nonce: &str| { + let claim = |account_id: &str, nonce: &str| { didbot::attest::NodeCredentialBackend::sign_claim( &key.signing, host.clone(), nonce, OffsetDateTime::now_utc(), &didbot::attest::ProvisioningRequest { - agent_id, + account_id, handle: None, parent: Some(&host), }, ) .expect("the host signs") }; - let provision = |agent_id: &str, parent: &str, claim: didbot::attest::AttestationClaim| { + let provision = |account_id: &str, parent: &str, claim: didbot::attest::AttestationClaim| { post_json( app, - "/xrpc/bot.did.provisionAgent", + "/xrpc/bot.did.createAccount", json!({ - "agentId": agent_id, + "accountId": account_id, "registration": { "harness": "tests", "agentType": "subagent", "parent": parent }, "attestation": claim, }), @@ -1419,7 +1419,7 @@ async fn an_admitted_host_attests_the_contexts_it_spawns() { ); // Built from the claim, so the parent is in the entry that inserted the // account rather than in a later `Admitted` one — see - // `didbot_pds::AgentAccount::parent`. + // `didbot_pds::HostedAccount::parent`. let recorded = claimed .booted .registry @@ -1507,7 +1507,7 @@ async fn an_admitted_host_attests_the_contexts_it_spawns() { "n2", OffsetDateTime::now_utc(), &didbot::attest::ProvisioningRequest { - agent_id: "ctx-2", + account_id: "ctx-2", handle: None, parent: Some(&host), }, @@ -1525,7 +1525,7 @@ async fn an_admitted_host_attests_the_contexts_it_spawns() { "n4", OffsetDateTime::now_utc(), &didbot::attest::ProvisioningRequest { - agent_id: "ctx-2", + account_id: "ctx-2", handle: None, parent: Some(&server), }, @@ -1661,7 +1661,7 @@ struct Standing { context: String, } -async fn admit_host_with_context(claimed: &Claimed, agent_id: &str, nonce: &str) -> Standing { +async fn admit_host_with_context(claimed: &Claimed, account_id: &str, nonce: &str) -> Standing { let app = &claimed.booted.app; let key = host_key(); let (status, body) = reserve(app, &key).await; @@ -1684,7 +1684,7 @@ async fn admit_host_with_context(claimed: &Claimed, agent_id: &str, nonce: &str) nonce, OffsetDateTime::now_utc(), &didbot::attest::ProvisioningRequest { - agent_id, + account_id, handle: None, parent: Some(&host), }, @@ -1692,9 +1692,9 @@ async fn admit_host_with_context(claimed: &Claimed, agent_id: &str, nonce: &str) .expect("the host signs"); let (status, body) = post_json( app, - "/xrpc/bot.did.provisionAgent", + "/xrpc/bot.did.createAccount", json!({ - "agentId": agent_id, + "accountId": account_id, "registration": { "harness": "tests", "agentType": "subagent", "parent": host }, "attestation": claim, }), @@ -1817,14 +1817,14 @@ async fn a_locked_or_unvouched_host_provisions_nothing() { let claimed = boot_claimed(&["host-a", "quern-a", "quern-b", "quern-c", "quern-d"]).await; let a = admit_host_with_context(&claimed, "ctx-a", "na").await; let registry = &claimed.booted.registry; - let provision = |agent_id: &'static str, nonce: &'static str| { + let provision = |account_id: &'static str, nonce: &'static str| { let claim = didbot::attest::NodeCredentialBackend::sign_claim( &a.key.signing, a.host.clone(), nonce, OffsetDateTime::now_utc(), &didbot::attest::ProvisioningRequest { - agent_id, + account_id, handle: None, parent: Some(&a.host), }, @@ -1832,9 +1832,9 @@ async fn a_locked_or_unvouched_host_provisions_nothing() { .expect("the host signs"); post_json( &claimed.booted.app, - "/xrpc/bot.did.provisionAgent", + "/xrpc/bot.did.createAccount", json!({ - "agentId": agent_id, + "accountId": account_id, "registration": { "harness": "tests", "parent": a.host }, "attestation": claim, }), diff --git a/crates/didbot/tests/support/stack.rs b/crates/didbot/tests/support/stack.rs index e79ab8c4..4c967996 100644 --- a/crates/didbot/tests/support/stack.rs +++ b/crates/didbot/tests/support/stack.rs @@ -163,9 +163,9 @@ impl Stack { path } - pub fn account(&self, agent_id: &str) -> String { + pub fn account(&self, account_id: &str) -> String { self.registry - .provision(ProvisionRequest::new(agent_id, None)) + .provision(ProvisionRequest::new(account_id, None)) .expect("provisioning succeeds") .account .did diff --git a/crates/didbot/tests/zone_containment.rs b/crates/didbot/tests/zone_containment.rs index 34b5389f..232b3b19 100644 --- a/crates/didbot/tests/zone_containment.rs +++ b/crates/didbot/tests/zone_containment.rs @@ -13,7 +13,7 @@ //! (`crates/didbot-identity/tests/identity.rs`) already prove the label- //! boundary and case-insensitivity properties as a standalone function //! against a string; this file drives the same adversarial hostnames through -//! the real HTTP surface — `bot.did.provisionAgent`, `/.well-known/did.json` +//! the real HTTP surface — `bot.did.createAccount`, `/.well-known/did.json` //! and `/.well-known/atproto-did` on a real router built from a real //! `Provisioner`. //! @@ -66,12 +66,12 @@ fn app(registry: Arc) -> axum::Router { ) } -/// A `bot.did.provisionAgent` body asking for a specific `handle` — the +/// A `bot.did.createAccount` body asking for a specific `handle` — the /// caller-controlled field `didbot_pds::Provisioner::check_requested_handle` -/// validates against the zone, unlike `agentId`, which cannot escape the -/// zone by construction (`AgentDid::mint` always appends the zone host). -fn provision_body_with_handle(agent_id: &str, handle: &str) -> String { - serde_json::json!({ "agentId": agent_id, "handle": handle }).to_string() +/// validates against the zone, unlike `accountId`, which cannot escape the +/// zone by construction (`AccountDid::mint` always appends the zone host). +fn provision_body_with_handle(account_id: &str, handle: &str) -> String { + serde_json::json!({ "accountId": account_id, "handle": handle }).to_string() } async fn call(app: &axum::Router, request: Request) -> (StatusCode, serde_json::Value) { @@ -139,7 +139,7 @@ async fn a_sibling_zone_handle_is_refused_and_resolves_nowhere() { let (status, body) = post( &app, - "/xrpc/bot.did.provisionAgent", + "/xrpc/bot.did.createAccount", provision_body_with_handle("sibling-attempt", &sibling), ) .await; @@ -176,7 +176,7 @@ async fn a_hostname_containing_the_zone_as_a_substring_is_refused() { let (status, body) = post( &app, - "/xrpc/bot.did.provisionAgent", + "/xrpc/bot.did.createAccount", provision_body_with_handle("substring-attempt", &smuggled), ) .await; @@ -206,7 +206,7 @@ async fn an_uppercase_sibling_zone_handle_is_refused_not_silently_folded() { let shouting = format!("EVIL{}", ZONE.to_ascii_uppercase()); let (status, body) = post( &app, - "/xrpc/bot.did.provisionAgent", + "/xrpc/bot.did.createAccount", provision_body_with_handle("shouting-attempt", &shouting), ) .await; diff --git a/docs/account-lifecycle.md b/docs/account-lifecycle.md index 5b8908a4..45353fb9 100644 --- a/docs/account-lifecycle.md +++ b/docs/account-lifecycle.md @@ -90,7 +90,7 @@ evaluator can never unfreeze holds — the policy's tag comes off by an operator's hand, never by a later verdict. A lock is confined to a subtree. Every account is admitted under one parent -(`AgentAccount::parent`, see `didbot_pds::admission`), and a tag hung on a +(`HostedAccount::parent`, see `didbot_pds::admission`), and a tag hung on a principal is hung on everything live beneath it as `Party::Parent` — by the parent, so it comes off below only when it comes off above. A parent's vouch lapsing hangs `quarantined` the same way, and the confinement poll lifts it @@ -98,12 +98,12 @@ when the vouch returns. No `Actor` is the parent, so nothing lifts an inherited tag at the child. Subtraction always wins. A lock that removes reads removes writes too, and -`AgentAccount::policy()` — the one call an enforcement point makes — is the +`HostedAccount::policy()` — the one call an enforcement point makes — is the state's baseline with every tag subtracted. No lock touches the document. Over HTTP the account's own credential is the only one this server -authenticates, so `bot.did.freezeAgent`/`unfreezeAgent` and -`bot.did.deactivateAgent`/`activateAgent` hang and lift the account's *own* +authenticates, so `bot.did.freezeAccount`/`unfreezeAccount` and +`bot.did.deactivateAccount`/`activateAccount` hang and lift the account's *own* tags. An operator's tags, the policy's tag, and every hold are in-process calls on `Registry`, the way `Registry::hard_delete` is. @@ -135,7 +135,7 @@ and the locks, and the same function decides whether a lock change is announced at all: a tag that changes the answer is, one that does not is not. The write axis has no wire representation, so a consumer of `#account` alone -cannot learn that an account is frozen or quarantined; `bot.did.listAgents` +cannot learn that an account is frozen or quarantined; `bot.did.listAccounts` carries every tag with the party that hung it. `takendown`, `desynchronized` and `throttled` are values this deployment has no position for. See [conformance.md](conformance.md). diff --git a/docs/agentd.md b/docs/agentd.md index c7c3b774..9fb27ae1 100644 --- a/docs/agentd.md +++ b/docs/agentd.md @@ -83,7 +83,7 @@ hook. agent pds - bot.did.provisionAgent + bot.did.createAccount one account per context the write credential is sent once listPendingAuthorizations @@ -162,7 +162,7 @@ a name is never returned to a pool, and neither is the record of who held it. deciding which context still needs a name. Names come from a registrar. The one that speaks to this project's server -calls `bot.did.provisionAgent` with the harness's identifier for the context, +calls `bot.did.createAccount` with the harness's identifier for the context, the harness's word for its kind, the host's DID as its parent, and a claim signed with the node key: `didbot-attest`'s node-credential format, naming the host's DID as the node. A host with no identity signs nothing, so a context on @@ -271,9 +271,9 @@ read from, and the account argument all went with it. A host with one agent on it has neither problem the daemon solves: nothing to multiplex and one credential, not one per context. `didbot-oauth`, run by name, works there with no daemon and no hook. Set `DIDBOT_PDS` to the server -and either `DIDBOT_AGENT_TOKEN` or `DIDBOT_AGENT_TOKEN_FILE` to that account's +and either `DIDBOT_ACCOUNT_TOKEN` or `DIDBOT_ACCOUNT_TOKEN_FILE` to that account's own agent token, and `pending`, `show`, `approve` and `decline` work as they do -otherwise, against the same routes as the same `AgentSelf` credential. Passing +otherwise, against the same routes as the same `AccountSelf` credential. Passing `--direct` insists on that mode; without it a running daemon is preferred, and the environment is consulted only when nothing answers on the socket. This is the bare binary's mode: `didbot oauth` removes both credential variables diff --git a/docs/attestation.md b/docs/attestation.md index 1d164b1a..038bb7a7 100644 --- a/docs/attestation.md +++ b/docs/attestation.md @@ -1,6 +1,6 @@ # Attestation -Attestation is the check `bot.did.provisionAgent` makes before it mints an +Attestation is the check `bot.did.createAccount` makes before it mints an agent account: a signature proving the request came from a host whose operator has vouched for it. It runs once, at provisioning: the claim is spent the moment it is admitted, and what it established is written into the account's @@ -44,7 +44,7 @@ crosses neither the socket nor the network. It is still a file, and anything that can read this user's disk can copy it — `Assurance::NodeCredential` in `crates/didbot-attest/src/claim.rs` says so in the record. The public half is published as the `#node` verification method in the host's DID document and -stored as `AgentAccount::node_key`; the key the server minted for the host's +stored as `HostedAccount::node_key`; the key the server minted for the host's repository is a different key for a different job. **A context** is a session, or a subagent inside one, keyed by both @@ -65,9 +65,9 @@ sequenceDiagram S->>O: getRecord, once per pending reservation, every 5 minutes O-->>S: subject, createdAt Note over S: mirrors the record, verifies host → server, activates, registers the node key - D->>S: provisionAgent(agentId, registration, claim signed with the node key) + D->>S: createAccount(accountId, registration, claim signed with the node key) Note over S: verifies the claim, the host's standing, and agent → host → server - S-->>D: did, handle, agentToken + S-->>D: did, handle, accountToken ``` **The reservation.** A `become` message over the daemon's socket posts the @@ -178,7 +178,7 @@ signs one claim per provisioning request, with a nonce drawn per claim. **The parent.** The daemon names the host's DID as the registration's parent for every context it asks for, session or subagent alike (`crates/didbot-agentd/src/serve.rs`), and the server sets -`AgentAccount::parent` to the host that signed. Every context on a host is +`HostedAccount::parent` to the host that signed. Every context on a host is therefore a direct child of that host, and `Registry::boundary` reads the chain context → host → server. That chain is what a lock cascades down and what a policy binding's "everything admitted beneath" means. @@ -190,7 +190,7 @@ document and the agent token, which is sent once. ## The refusals, by name -Four gates close `bot.did.provisionAgent` before the claim is read at all: +Four gates close `bot.did.createAccount` before the claim is read at all: `Halted` (the e-stop, which also fires when the server's own operator claim has lapsed), `ServerNotReady` (the lifecycle — see [the server lifecycle](server-lifecycle.md)), `RateLimitExceeded` (a thousand diff --git a/docs/cli.md b/docs/cli.md index 61b0ba9c..a6156455 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -28,11 +28,11 @@ with what to install rather than "unknown command". `didbot …` looks the verb up in its table, finds the binary on `PATH`, and replaces itself with it. The verb receives the rest of the command line as typed and this process's environment with every credential -variable removed: `DIDBOT_AGENT_TOKEN` and `DIDBOT_AGENT_TOKEN_FILE`. Nothing +variable removed: `DIDBOT_ACCOUNT_TOKEN` and `DIDBOT_ACCOUNT_TOKEN_FILE`. Nothing is added. On unix the verb's process replaces the dispatcher's, so the verb's exit status is the shell's. -
+
@@ -63,8 +63,8 @@ exit status is the shell's. the binary, then the rest of the command line as typed the environment, minus - DIDBOT_AGENT_TOKEN and - DIDBOT_AGENT_TOKEN_FILE + DIDBOT_ACCOUNT_TOKEN and + DIDBOT_ACCOUNT_TOKEN_FILE its exit status is the shell's install it @@ -324,7 +324,7 @@ key from `$XDG_STATE_HOME/didbot/agentd`. The hook and `didbot oauth` read `DIDBOT_SOCK` too, so all three agree on where the socket is. A host with no daemon runs the binary by name. With `DIDBOT_PDS` and either -`DIDBOT_AGENT_TOKEN` or `DIDBOT_AGENT_TOKEN_FILE` set, `didbot-oauth pending`, +`DIDBOT_ACCOUNT_TOKEN` or `DIDBOT_ACCOUNT_TOKEN_FILE` set, `didbot-oauth pending`, `show`, `approve` and `decline` talk to that server as that one account; `--direct` insists on it rather than trying the socket first. Through the dispatcher both credential variables are removed before the hand-off, so @@ -368,7 +368,7 @@ binary runs on a laptop: it is the `didbot-serve` crate's, so `cargo install `didbot foo` runs `didbot-foo` from `PATH`. The program receives the rest of the command line as its own arguments and the ordinary environment, less -`DIDBOT_AGENT_TOKEN` and `DIDBOT_AGENT_TOKEN_FILE`; nothing is added, and no +`DIDBOT_ACCOUNT_TOKEN` and `DIDBOT_ACCOUNT_TOKEN_FILE`; nothing is added, and no credential from the shell reaches it. It authenticates for itself, the way the first-party verbs do. `didbot --list` shows it beside them, with whatever its `--version` prints, and `didbot help foo` runs `didbot-foo --help`. diff --git a/docs/conformance.md b/docs/conformance.md index 4d9ac547..a925720e 100644 --- a/docs/conformance.md +++ b/docs/conformance.md @@ -165,7 +165,7 @@ out is a valid TID, every URI the wire layer builds parses back into the three parts it was built from, every DID and hostname the minter produces is valid under the protocol's own generic rules. -That last one is the load-bearing direction. [`AgentDid`] is deliberately much +That last one is the load-bearing direction. [`AccountDid`] is deliberately much stricter than atproto's DID grammar — `did:web` only, hostname-level only, ports only on loopback — so the generic vectors are *not* run against it; they would report failures that are the entire point of the type. What is asserted @@ -196,7 +196,7 @@ is verified in both directions, including why the DNS TXT resolution method is not implemented and why the reading side checks containment rather than making the round trip. -[`AgentDid`]: crate::identity::AgentDid +[`AccountDid`]: crate::identity::AccountDid [`Provisioner`]: crate::pds::Provisioner [`validate_handle`]: crate::identity::validate_handle @@ -367,7 +367,7 @@ quota is 403 `AccountQuotaExceeded`, and neither leaves a file behind. ## Account state, and where it does not map onto the wire -`AgentAccount` carries an `AccountState` — whether its data exists — and a +`HostedAccount` carries an `AccountState` — whether its data exists — and a set of locks — whether it serves; [account-lifecycle.md](account-lifecycle.md) holds both tables, generated from the types. Hard delete is not a state — it removes the account row entirely, key included, which is this server's only @@ -396,7 +396,7 @@ model write authority at all. | *(hard delete)* | *(no event)* | *(no event)* — the row leaves after it was erased, and the erasure already said `deleted`; a row reaped before activation was never announced and reaping it must not be the first thing a relay hears. Once `deleted` is on the wire it is out of this server's control — a relay or appview that cached the repository keeps serving what it has — so "no evidence it ever existed" is true of this server's own surfaces and not of the network. | **`com.atproto.sync.getRepoStatus`** is the same table, one row on request: -`active` and `status` come from the same `AgentAccount::sync_status` the +`active` and `status` come from the same `HostedAccount::sync_status` the `#account` event is announced from, `rev` is present exactly when `active` is true, and the two *(no event)* rows are `RepoNotFound`. The other `com.atproto.sync.*` reads of one repository refuse a `suspended` or @@ -411,9 +411,9 @@ for a meaning the lexicon does not define it as. `takendown` belongs to a moderation surface this deployment does not vendor; `desynchronized` and `throttled` describe a host, not an account. -**`bot.did.listAgents`.** This server's own surface, and where a lock is -actually visible: `AgentSummary.state` carries the same `AccountState` the -`#account` mapping is derived from, and `AgentSummary.locks` every tag with +**`bot.did.listAccounts`.** This server's own surface, and where a lock is +actually visible: `AccountSummary.state` carries the same `AccountState` the +`#account` mapping is derived from, and `AccountSummary.locks` every tag with the party that hung it — one set of types, read by both surfaces, so they cannot silently drift apart the way a hand-maintained second enumeration would. An operator or dashboard that needs to know an account is frozen reads diff --git a/docs/deployment.md b/docs/deployment.md index 90171fef..76b37c57 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -516,7 +516,7 @@ mounts proves the filesystem, not the log. 3. Mount it at `/data`, where the fresh instance's boot script chowns `/data/pds` to uid 10001, and run `didbot-pds --data /data/pds` against a development zone. -4. Confirm the accounts that should be there are there: `bot.did.listAgents` +4. Confirm the accounts that should be there are there: `bot.did.listAccounts` returns the expected count, one agent's `did.json` resolves out of the restored keys, and a blob that account referenced fetches back byte for byte. @@ -525,7 +525,7 @@ mounts proves the filesystem, not the log. Step 4 runs in this workspace against a copied data directory rather than a volume: `crates/didbot-serve/tests/restore_drill.rs` takes a snapshot of a live directory, serves the copy, and requires the two servers to answer the -same `listAgents`, `getRecord`, `did.json` and `bot.did.stats` — with the +same `listAccounts`, `getRecord`, `did.json` and `bot.did.stats` — with the blob's bytes fetched back through `getBlob`. What steps 1 through 3 add is the volume itself and the uid the instance's boot script sets. diff --git a/docs/operations.md b/docs/operations.md index 4ed9392e..89896465 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -93,7 +93,7 @@ append in flight when the snapshot was taken. | `GET /xrpc/bot.did.stats` | Accounts, records and bytes per collection match what the deployment had, and `blobs.held` matches its blob inventory | | `blobs.missingAtBoot` in that response reads `{"count": 0, "bytes": 0}` | Every blob the log names had its bytes on the disk this deployment started on. A nonzero `count` is that many `404`s waiting, and `bytes` is what the restore would have to bring back; the CIDs are in the startup `ERROR` line | | `blobs.discardedAtBoot` reads `{"count": 0, "bytes": 0}` | The copy carried no blob bytes the log could not account for. A nonzero count is that many files deleted during startup | -| `GET /xrpc/bot.did.listAgents` | The accounts that should be there are there | +| `GET /xrpc/bot.did.listAccounts` | The accounts that should be there are there | | One agent's `did.json` resolves | The signing keys came back | | Fetch one blob that account references | The bytes are on the disk; `getBlob` re-hashes what it reads and refuses on a mismatch | @@ -118,5 +118,5 @@ accounts it does not. The journal carries a sampled `info` line for the first, one in a hundred, naming the account — sampled because logging every one would make the log the thing a flood amplifies. -`bot.did.stats` and `bot.did.listAgents` are public by default; under +`bot.did.stats` and `bot.did.listAccounts` are public by default; under `--close-disclosure` they answer 403 and the figures are in the startup line. diff --git a/docs/operator-verification.md b/docs/operator-verification.md index ea361b17..485b40c0 100644 --- a/docs/operator-verification.md +++ b/docs/operator-verification.md @@ -57,7 +57,7 @@ it is still the operator's record, not the agent's. agreement is not evidence. 5. `bot.did.operator`, read from the *operator's* repository at the record key that is the server's hostname. The key is derived from the server's own - DID (`AgentDid::authority`), never accepted from anybody. + DID (`AccountDid::authority`), never accepted from anybody. 6. The two agree: the claim names that server, binds the key that server publishes now, and has not expired. @@ -132,7 +132,7 @@ and anyone acting on a week-old confirmation is acting on history. ## Enumeration is a lower bound "Which agents does this operator have" is *derived*, not authoritative. It -comes from `bot.did.listAgents` — one server's account of its own contents +comes from `bot.did.listAccounts` — one server's account of its own contents — and each DID in it is then checked against the operator's own record. So the count is a floor, and four things push it below the truth: diff --git a/docs/running-locally.md b/docs/running-locally.md index 1f467dce..f321bd25 100644 --- a/docs/running-locally.md +++ b/docs/running-locally.md @@ -278,17 +278,17 @@ DID — provisioned, named, released, pinned, unpinned, deprovisioned — writte beside the account and kept after it: ```sh -curl -s "localhost:3000/xrpc/bot.did.getAgentLedger?did=$DID" | jq . -curl -s localhost:3000/xrpc/bot.did.listAgentLedgers | jq '.ledgers | length' +curl -s "localhost:3000/xrpc/bot.did.getAccountLedger?did=$DID" | jq . +curl -s localhost:3000/xrpc/bot.did.listAccountLedgers | jq '.ledgers | length' ``` -Delete the agent and ask again. `listAgents` no longer has it and the ledger +Delete the agent and ask again. `listAccounts` no longer has it and the ledger still does, with the handle it answered to and the moment it stopped. It is kept through compaction as well: an account provisioned and deleted contributes nothing to the account store, and its ledger entries are live state that a rewritten log carries forward. -The name travels. `listAgents` reports it, the index keeps it beside the DID — +The name travels. `listAccounts` reports it, the index keeps it beside the DID — after checking it lands under the zone its server admits to minting, since a name is the field a reader trusts instead of the identifier — and the canvas labels each agent with it, asking the server what its accounts are called and @@ -305,7 +305,7 @@ curl -s -H 'Host: basalt-otter.agents.localhost' localhost:3000/.well-known/atpr curl -s -H 'Host: kestrel.agents.localhost' localhost:3000/.well-known/did.json | jq .alsoKnownAs ``` -A name nobody holds is a 404. `bot.did.listAgents` is how to do both halves +A name nobody holds is a 404. `bot.did.listAccounts` is how to do both halves for every account at once: it names what this server holds, and each name in it answers the two lookups above or disagrees. @@ -455,7 +455,7 @@ CARv1 file: a version 3 commit, signed by the account's key, over the root of a Merkle search tree keyed by every record it holds. ```sh -did=$(curl -s localhost:3000/xrpc/bot.did.listAgents | jq -r '.agents[0].did') +did=$(curl -s localhost:3000/xrpc/bot.did.listAccounts | jq -r '.agents[0].did') curl -sG localhost:3000/xrpc/com.atproto.sync.getRepo \ --data-urlencode "did=$did" -o repo.car ``` @@ -522,14 +522,14 @@ between compactions. ```sh curl -s localhost:3000/health -curl -s -X POST localhost:3000/xrpc/bot.did.provisionAgent \ +curl -s -X POST localhost:3000/xrpc/bot.did.createAccount \ -H 'content-type: application/json' \ - -d '{"agentId":"scratch"}' + -d '{"accountId":"scratch"}' curl -s --resolve 'scratch.agents.localhost:3000:[::1]' \ http://scratch.agents.localhost:3000/.well-known/did.json -curl -s localhost:3000/xrpc/bot.did.listAgents +curl -s localhost:3000/xrpc/bot.did.listAccounts ``` ### The repository, over `com.atproto.repo.*` @@ -740,7 +740,7 @@ checked the stream from the outside. A commit trail is bounded, so a `since` this deployment has trimmed away is answered with the whole repository rather than with a diff. -Under `.localhost`, `bot.did.provisionAgent` mints for a request carrying no +Under `.localhost`, `bot.did.createAccount` mints for a request carrying no attestation claim, and the account's registration record says `self-asserted`. Any other zone refuses such a request as `AttestationRequired`. diff --git a/docs/write-pipeline.md b/docs/write-pipeline.md index 8e1ab844..31b92276 100644 --- a/docs/write-pipeline.md +++ b/docs/write-pipeline.md @@ -262,9 +262,9 @@ authenticating, before deserializing, before resolving a handle. A caller that sends an unparseable body to a halted server is told `Halted`, not `InvalidRequest`, because the latch is an atomic read that needs no body and the former is the fact worth acting on. The same gate covers the `bot.did.*` -routes that destroy an account or widen what it may do: `deleteAgent`, -`unfreezeAgent`, `activateAgent` and `setAgentPinned` are refused under a -`Revoke`. `freezeAgent` and `deactivateAgent` are not, and deliberately — +routes that destroy an account or widen what it may do: `deleteAccount`, +`unfreezeAccount`, `activateAccount` and `setAccountPinned` are refused under a +`Revoke`. `freezeAccount` and `deactivateAccount` are not, and deliberately — each can only ever narrow what an account may do, and a stop that stood between a caller and restraining itself would be worse than the one it replaced. diff --git a/infra/pds/route53.tf b/infra/pds/route53.tf index c102799c..39999f86 100644 --- a/infra/pds/route53.tf +++ b/infra/pds/route53.tf @@ -45,7 +45,7 @@ resource "aws_route53_record" "apex" { # Every agent hostname, in one record. An RFC 1034 wildcard synthesizes an # answer for any name one label below the zone while no closer node exists, -# which is exactly the set of names `AgentDid::mint` and the handle namer +# which is exactly the set of names `AccountDid::mint` and the handle namer # produce -- the DID's hostname and the handle's both land here without a # write, a propagation wait, or a record-set quota to meet. The server keeps # the zone in this shape by writing no address record of its own beneath it. diff --git a/plan/account-lifecycle.md b/plan/account-lifecycle.md index df4f01d4..1b49fe6a 100644 --- a/plan/account-lifecycle.md +++ b/plan/account-lifecycle.md @@ -87,10 +87,10 @@ was told to stop serving. | Lock | Hung by | Removes | Removed by | |---|---|---|---| -| `Frozen` | an operator, or the account (`freezeAgent`) | writes | the party that hung it (`unfreezeAgent` for the account's own) | +| `Frozen` | an operator, or the account (`freezeAccount`) | writes | the party that hung it (`unfreezeAccount` for the account's own) | | `Quarantined` | a policy outcome | writes | a recorded operator override | | `Suspended` | an operator | reads and writes | that operator | -| `Deactivated` | the account itself (`deactivateAgent`) | reads and writes | the account (`activateAgent`) | +| `Deactivated` | the account itself (`deactivateAccount`) | reads and writes | the account (`activateAccount`) | Any party may hang any lock, and two parties hanging one kind are two tags. @@ -111,7 +111,7 @@ refused outright; stopping its service is a separate act (`Deactivated`), so no `-ing` state is ever left stuck. An operator sets it and an operator clears it, and the ledger records both. -A pin is not a hold. `setAgentPinned` sets `Retention::Forever` and unpinning +A pin is not a hold. `setAccountPinned` sets `Retention::Forever` and unpinning restores the kind's retention — a value on the retention axis, which is the row's lifetime: the sweep passes a pinned account by and a hard delete is refused, while the account's own erasure goes through it. @@ -138,7 +138,7 @@ describe a host, not an account. ## We host only names we serve -`AgentDid::parse` refuses anything but a hostname-level `did:web`, and +`AccountDid::parse` refuses anything but a hostname-level `did:web`, and `HostedDid::host` refuses one outside every zone the deployment serves. `AccountStore` is keyed by `HostedDid`, so "we host only names we serve" is a compile error to violate. @@ -168,11 +168,11 @@ it hosts*, which stays true. ## Decisions for the owner -- [ ] **May a caller name its own agent.** If not, `provisionAgent` earns its +- [ ] **May a caller name its own agent.** If not, `createAccount` earns its existence and `createAccount` refuses `handle` too. If so, squatting and enumeration enter a zone whose record sets are finite. - [ ] **`PreventHandleReuse`.** Handle reuse after erasure is - `Reservation::FormerAgent`, set on every erasure and lifted when the + `Reservation::FormerAccount`, set on every erasure and lifted when the row goes. That is the name to use if it ever becomes a per-account choice. @@ -186,9 +186,9 @@ it hosts*, which stays true. the same value would be two mechanisms for one decision. - [x] **An operator clears `PreventDataDeletion`**, and the ledger records which one set it and which one cleared it. -- [x] **`softDeleteAgent` became the `Deactivated` lock**, reversible, as - `bot.did.deactivateAgent` and `bot.did.activateAgent`. Erasure is - `bot.did.deleteAgent`. +- [x] **`softDeleteAccount` became the `Deactivated` lock**, reversible, as + `bot.did.deactivateAccount` and `bot.did.activateAccount`. Erasure is + `bot.did.deleteAccount`. - [x] `HostedDid`, and `AccountStore` keyed by it. - [x] The lock and hold entries, declared once; the stamp moved once. - [x] Locks replace `AccountState::Frozen`; `policy()` folds the hasp over @@ -204,7 +204,7 @@ it hosts*, which stays true. [`docs/account-lifecycle.md`](../docs/account-lifecycle.md) generated from the types and checked by a test. - [x] **A lock is confined to a subtree.** Every account carries the - parent it was admitted under (`AgentAccount::parent`, verified by + parent it was admitted under (`HostedAccount::parent`, verified by `Registry::admit`'s chain walk), a tag hung on a principal is hung on everything live beneath it as `Party::Parent`, and a parent's vouch lapsing quarantines its subtree at the next confinement poll. diff --git a/plan/account-types.md b/plan/account-types.md index 02fab5be..0c742b14 100644 --- a/plan/account-types.md +++ b/plan/account-types.md @@ -41,7 +41,7 @@ account carries the values, rather than the kind deciding behaviour directly. that wants a variant. A kind that names a row of defaults does not. Two of the three landed: [`AccountKind`](../crates/didbot-pds/src/kind.rs) names the four members of the registration record's `actor` union and - supplies a row of defaults, and `AgentAccount` carries a + supplies a row of defaults, and `HostedAccount` carries a `nameProvenance` and a `retention` of its own. **Renewal** is not a field, deliberately: nothing renews, so it would be a stored value nothing ever writes a second time — an unenforced control. It lands with @@ -113,7 +113,7 @@ anything is the honest source, rather than a copy on the profile) is sound and is kept by [provenance](provenance.md); the tick is not. - [x] **`pinned` is derived.** It was a boolean on - [`AgentAccount`](../crates/didbot-pds/src/account.rs) standing in for a + [`HostedAccount`](../crates/didbot-pds/src/account.rs) standing in for a retention policy, and once retention is a value a pin is one of its settings — two mechanisms for one decision, and the one that lost would still be the one some caller was reading. `pinned` is a method over diff --git a/plan/adversarial.md b/plan/adversarial.md index 4d779613..f97bef3e 100644 --- a/plan/adversarial.md +++ b/plan/adversarial.md @@ -149,7 +149,7 @@ concurrently — only sequentially, the way `crates/didbot-pds/tests/naming.rs` and `provisioning.rs` already did before this pass. What held and what did not, from actually racing it with real threads rather than reasoning about it: -- **Held: two requests minting the same `agent_id`.** `AccountStore::insert` +- **Held: two requests minting the same `account_id`.** `AccountStore::insert` is a single check-and-insert under one mutex, so exactly one of two concurrent `provision()` calls for the same DID gets past it. The loser's insert is refused with the row untouched, and everything it unwinds — the @@ -159,7 +159,7 @@ not, from actually racing it with real threads rather than reasoning about it: - **Held: naming through `Naming::issue`.** `NameRegistry::claim` checks and claims a name under one mutex, so two concurrent provisions cannot both walk away believing they hold the same generated or hinted name. -- **Fixed: two requests minting *different* `agent_id`s for the *same* +- **Fixed: two requests minting *different* `account_id`s for the *same* caller-asserted handle, with no `Naming` configured.** `check_requested_handle` scans the account store and the actual claim (`AccountStore::insert`) happens several steps later — a mint, a keypair, a @@ -180,12 +180,12 @@ not, from actually racing it with real threads rather than reasoning about it: (`git stash` the fix) fails the test 5/5 runs; restoring it passes 3/3. - **Held: the e-stop and lifecycle gates.** `docs/write-pipeline.md`'s ordering — e-stop before lifecycle before anything is parsed — is exactly - what `crates/didbot-serve/src/routes.rs`'s `provision_agent` does, and + what `crates/didbot-serve/src/routes.rs`'s `create_account` does, and because both checks run before `registry.provision()` is ever called, "a refusal leaves nothing behind" is trivial rather than tested: nothing was started. Already covered end to end in `crates/didbot-serve/src/tests.rs` (Revoke, Pause, and Pause-thrown-by-a-lapsed-operator-claim each refuse - `provisionAgent` with `Halted`, and Pause is shown leaving an + `createAccount` with `Halted`, and Pause is shown leaving an already-issued token alone). Left open, in rough order of how much a deployment should care: @@ -379,7 +379,7 @@ doc comment, and each was run. `a_400_that_is_not_record_not_found_is_unreachable_not_an_absent_claim` in the same file. - **A freeze names who froze it.** A freeze is a `Lock` tag on the account - row (`AgentAccount::locks`, `crates/didbot-pds/src/lockout.rs`), and each + row (`HostedAccount::locks`, `crates/didbot-pds/src/lockout.rs`), and each tag carries the `Party` that hung it: operator, policy, the account itself, or a parent. The tag outlives the write that tripped it. A policy outcome hangs `Lock::Quarantined` under `Party::Policy` and records a diff --git a/plan/agent-accounts.md b/plan/agent-accounts.md index e3584e0f..89a2139e 100644 --- a/plan/agent-accounts.md +++ b/plan/agent-accounts.md @@ -59,7 +59,7 @@ Two hostnames per account: the DID's, and the handle's, which needs What this epic contributes to the decision is a constraint, not an opinion: the landing states are not symmetric. `active` and `frozen` are reversible; `soft-deleted` burns the name permanently - (`Reservation::FormerAgent`) and only hard delete frees it, which is + (`Reservation::FormerAccount`) and only hard delete frees it, which is administrator-only and has no route until [oauth](oauth.md) gives it a caller it can check. So a retention policy that lands a session end on soft-delete is choosing to spend a name, and one that lands on frozen is @@ -171,10 +171,10 @@ Two hostnames per account: the DID's, and the handle's, which needs the one operation that frees a burned name. The route returns when [oauth](oauth.md)'s operator sign-in gives it a caller it can check. `Registry::delete`/`freeze`/`unfreeze`/`soft_delete` and - `setAgentPinned` are `Credential::AgentSelf` — an agent acting on + `setAccountPinned` are `Credential::AccountSelf` — an agent acting on itself, and nothing else. - [x] **Names are burned permanently by soft delete, freed only by hard - delete.** `Reservation::FormerAgent` extends the existing reservation + delete.** `Reservation::FormerAccount` extends the existing reservation mechanism (`Reservation::ZoneApex`/`Operational`) rather than adding a second one; `NameRegistry::retire` moves a live or held name straight to a permanent reservation, and only `NameRegistry::release` — called @@ -183,7 +183,7 @@ Two hostnames per account: the DID's, and the handle's, which needs means fetchable, not writable, so `frozen` maps to the same wire shape as `active` and there is no `#account` status for it — the open union does not rescue this, because a status is only meaningful when - `active` is false. `bot.did.listAgents`'s `AgentSummary.state` is + `active` is false. `bot.did.listAccounts`'s `AccountSummary.state` is where a frozen account is actually visible, using the same `AccountState` the firehose event maps from so the two surfaces cannot drift. See `docs/conformance.md`. diff --git a/plan/app-allowlist.md b/plan/app-allowlist.md index d05d9a9c..cdf178dc 100644 --- a/plan/app-allowlist.md +++ b/plan/app-allowlist.md @@ -74,7 +74,7 @@ likely to be mistaken for something the protocol does. client's origin and content key instead. Asserted on the one function that decides what leaves this server, `DecisionRecord::to_json`, and over the wire in - `crates/didbot-serve/tests/oauth_agent_flow.rs`. + `crates/didbot-serve/tests/oauth_account_flow.rs`. - [x] **The fetch reaches the public internet and nothing else.** `client_id` is a URL an unauthenticated caller chose, so `resolve_client` resolves its host and refuses it unless every address it answers to is outside diff --git a/plan/auth-types.md b/plan/auth-types.md index 42a34c8e..a0dc2cae 100644 --- a/plan/auth-types.md +++ b/plan/auth-types.md @@ -101,10 +101,10 @@ and no operator minting an app password for it, so an app-password session was the wrong vehicle — building it would have meant a machine holding a *human* credential shape for a caller that is never human. What it got was a bearer token, minted once at provisioning, bound to one DID, with an expiry and no -rotation: `didbot_pds::credential` — `Credential::AgentToken` on the HTTP +rotation: `didbot_pds::credential` — `Credential::AccountToken` on the HTTP side. `Provisioner::provision` issues one automatically, durable in the -write-ahead log (`pds.layout` bumped to 3), returned once as `agentToken` in -`provisionAgent`'s response body. A client carries it the same way it already +write-ahead log (`pds.layout` bumped to 3), returned once as `accountToken` in +`createAccount`'s response body. A client carries it the same way it already carries the DID, and the conformance suite proves the whole path: client and server agreeing on a credential none of them minted by hand. That is what exists, not what should: the session taken by attestation above is what @@ -149,7 +149,7 @@ credential authenticates as. That is what makes a pending authorization readable, approvable and declinable only by the agent it was addressed to: there is no account parameter for the credential to disagree with. -`bot.did.hardDeleteAgent`, which must not be self-service, consequently has +`bot.did.hardDeleteAccount`, which must not be self-service, consequently has no route at all — see [agent-accounts](agent-accounts.md). There is no netizen argument for letting a stranger delete an account, so this stays hard-locked regardless of the disclosure decision below. @@ -191,7 +191,7 @@ shape comes up again — not re-derived per route: - **Closing a route has to be visible, or the mechanism defeats itself.** The threat this default resists is an operator who cannot be audited, and the configuration tier that closes a route is a tier an on-box attacker - also reaches — closing `listAgents` is exactly what that attacker would do + also reaches — closing `listAccounts` is exactly what that attacker would do to hide what a stranger needed to see. This cannot be prevented from the server alone, but it is made *legible*: a closed route answers `DisclosureDisabled`, never a 404 and never an empty list, so an outsider diff --git a/plan/capacity.md b/plan/capacity.md index 6ae82550..a238df4c 100644 --- a/plan/capacity.md +++ b/plan/capacity.md @@ -136,7 +136,7 @@ used to shrink it. - [x] **A configured cap on accounts, with a sane default.** `--max-accounts` or `[capacity] max_accounts` sets it, and `DEFAULT_MAX_ACCOUNTS` is - 1000, a cap an operator raises deliberately. `provision_agent` refuses with + 1000, a cap an operator raises deliberately. `create_account` refuses with `503 AccountCapReached`, naming the cap, before it parses the request or writes anything, so a caller never sees a `ChangeResourceRecordSets` failure after the keys and the first hostname exist. The cap is one diff --git a/plan/cred-delivery.md b/plan/cred-delivery.md index 24c78f3d..52532e1c 100644 --- a/plan/cred-delivery.md +++ b/plan/cred-delivery.md @@ -123,7 +123,7 @@ The honest list, because the boundary is worth more than the mechanism. ## Done - [x] **The daemon holds the account credential, in memory and nowhere else.** - `provisionAgent` hands back an agent token once and keeps no copy, and + `createAccount` hands back an agent token once and keeps no copy, and the daemon used to drop it. It keeps it now, on the context it belongs to (`didbot_agentd::context::Context::token`), and presents it as that account on the four sign-in decision routes. It is a diff --git a/plan/credentials.md b/plan/credentials.md index 2d0a7746..40557255 100644 --- a/plan/credentials.md +++ b/plan/credentials.md @@ -51,7 +51,7 @@ write, and nothing assumes co-location. hook to deliver anything. Half of this is now answered: there is nothing left to stamp, because `didbot-oauth` names a decision rather than an account, and it runs with no daemon and no hook at all against - `DIDBOT_PDS` and `DIDBOT_AGENT_TOKEN` — so a runner holding a credential + `DIDBOT_PDS` and `DIDBOT_ACCOUNT_TOKEN` — so a runner holding a credential can spend one. How it comes to hold one is the half still open. See [account-types](account-types.md). @@ -75,7 +75,7 @@ section decides what replaces it before any of it is built. a code. `AttestationClaim` names the host and nothing it is for, so it has to gain the account and the token endpoint. Otherwise one claim can be spent against another account. - - `Credential::AgentSelf` accepting a DPoP-bound token, and only one + - `Credential::AccountSelf` accepting a DPoP-bound token, and only one this grant issued. An app an agent signed in to must not be able to approve further sign-ins as that agent. - `didbot-oauth` direct mode signing that claim, which puts a node key diff --git a/plan/did-minting.md b/plan/did-minting.md index 5a6ff927..8620e6a9 100644 --- a/plan/did-minting.md +++ b/plan/did-minting.md @@ -14,9 +14,9 @@ exitCriterion: > A `did:web` DID is a hostname, and this server takes that hostname from whoever asked for the account. -[`ProvisionRequest::agent_id`](../crates/didbot-pds/src/provision.rs) is +[`ProvisionRequest::account_id`](../crates/didbot-pds/src/provision.rs) is documented as "the DNS label the DID will be minted from", and -`Provisioner::provision` passes it straight to `AgentDid::mint`. The server +`Provisioner::provision` passes it straight to `AccountDid::mint`. The server checks two things about it: that it is a legal DNS label ([`validate_label`](../crates/didbot-identity/src/did.rs) — lowercase ASCII, digits, hyphens, no hyphen at either end) and that the minted host sits at or @@ -29,7 +29,7 @@ changed afterwards. ## What follows from it **A harness may not have an identifier to give.** A top-level session has no -`agent_id` of its own, and a client papers over it by deriving a label out of +`account_id` of its own, and a client papers over it by deriving a label out of whatever session bookkeeping it holds, because the harness does not promise the format. That is a good workaround living in the wrong place — it is one client's convention, not a rule the server enforces, and a second client is @@ -68,7 +68,7 @@ have an account", never to name it. - [ ] **Split the identifier from the correlation key.** `ProvisionRequest` grows a `correlation: Option` that is opaque to identity, and - loses `agent_id`. Nothing about the key reaches the DID, so it needs no + loses `account_id`. Nothing about the key reaches the DID, so it needs no label rules and can be as long, as structured or as absent as a harness requires. This is a breaking change to the provisioning surface and to every caller of it, so it lands as a `!` commit. @@ -81,7 +81,7 @@ have an account", never to name it. for it over a counter — but a counter is durable and auditable, and this is a decision to make explicitly rather than inherit. Which zone it is minted *from* is [name-pools](name-pools.md)'s question. -- [ ] **Keep provisioning idempotent.** Today `agent_id` doubles as the +- [ ] **Keep provisioning idempotent.** Today `account_id` doubles as the idempotency key: re-provisioning the same session hits the duplicate refusal. Once the server mints, the store needs a durable correlation-key-to-DID index to answer the same question, and a request diff --git a/plan/e-stop.md b/plan/e-stop.md index 0b168b8a..dfc56641 100644 --- a/plan/e-stop.md +++ b/plan/e-stop.md @@ -33,7 +33,7 @@ run, and which stops strictly more. person to reach for scoping needs them. One fact decides most of it. A halt narrower than "everything" must leave - some provisioning permitted, and `bot.did.provisionAgent` authenticates a + some provisioning permitted, and `bot.did.createAccount` authenticates a host and nothing narrower: the daemon on a host signs for every context on it, and the profile the request carries is the caller's word about itself. A halted agent that can still provision takes a fresh account @@ -83,7 +83,7 @@ run, and which stops strictly more. finished arriving still refuses one. - [x] **A latch both gates read**, at issuance and at the write. `didbot_pds::Estop::check_issue` gates `POST /oauth/token` and - `bot.did.provisionAgent`; `Estop::check_use` gates every repository + `bot.did.createAccount`; `Estop::check_use` gates every repository write route and `uploadBlob`, checked fresh on every request rather than cached — see the immediacy test. - [x] **Say what it is refusing.** `Estop::status` reports tokens, operations diff --git a/plan/handshake.md b/plan/handshake.md index 568de38e..0a7c5331 100644 --- a/plan/handshake.md +++ b/plan/handshake.md @@ -277,7 +277,7 @@ neither the operator nor a channel to them. the operational one and the admin socket's `PAUSE`/`REVOKE` are refused there. - [x] **The pause a lapse causes.** `didbot_pds::Estop::throw_self(Mode::Pause)` - and the existing `bot.did.provisionAgent` estop check refuse + and the existing `bot.did.createAccount` estop check refuse provisioning exactly as an operator's own pause would — no new account state, no second gate — and `Estop::Cause::OperatorMissing` keeps the two distinguishable at the source. diff --git a/plan/index.md b/plan/index.md index 59b2ae86..a79aa9f7 100644 --- a/plan/index.md +++ b/plan/index.md @@ -51,12 +51,12 @@ proves, versus what this index merely reports because a server said so, is Seven of them — the vouch-chain discovery walk, the firehose subscription and its sweep backstop, the work clustering, lineage learned from records, the query service's reader endpoints, `/health`, the three firehose filter -modes, and `View::agent_by_handle` — described code in `didbot-index` and +modes, and `View::account_by_handle` — described code in `didbot-index` and `didbot-query`. Both crates were removed from this repository in `build!: remove didbot-index, didbot-query and the canvas`; the reading side now lives at vibescrobble.com. Nothing in this workspace walks a vouch chain, subscribes to another server's firehose, or clusters work, and -`grep` for `SimHash`, `FilterMode` or `View::agent_by_handle` finds +`grep` for `SimHash`, `FilterMode` or `View::account_by_handle` finds nothing. They are unticked here rather than left standing, because a tick against code that is not in the tree is what sent three agents to reimplement work that was never here. diff --git a/plan/local-dev.md b/plan/local-dev.md index c67fe96f..b0cbfd01 100644 --- a/plan/local-dev.md +++ b/plan/local-dev.md @@ -167,7 +167,7 @@ than left standing. The surviving scripts are described accurately below. - [x] A swarm that drives the stack with simulated agents and records. - [x] The swarm signs agents in over OAuth (PAR, decision record, approve, token exchange) rather than only using the raw credential - `provisionAgent` hands back, so `crates/didbot-swarm/tests/decision_bounds.rs` + `createAccount` hands back, so `crates/didbot-swarm/tests/decision_bounds.rs` can drive the decision store's per-account and total bounds under concurrent load. - [x] The swarm's sign-in presents real RFC 9449 proofs over a key it diff --git a/plan/node.md b/plan/node.md index c9016b7f..a22389eb 100644 --- a/plan/node.md +++ b/plan/node.md @@ -118,7 +118,7 @@ load leaves another. life. The file carries the key, the DID, the token, the harness's word for the kind, and the askers already told. It is read back at start-up beside the sessions, and one file that does not read costs one context. -- [ ] **Name a context after both halves of its key.** `agent_id` takes the +- [ ] **Name a context after both halves of its key.** `account_id` takes the subagent id alone when there is one (`crates/didbot-agentd/src/serve.rs`), on the stated assumption that a subagent id is unique on the machine, while the store is keyed by diff --git a/plan/oauth.md b/plan/oauth.md index 8ccc1d9a..a19d1ae1 100644 --- a/plan/oauth.md +++ b/plan/oauth.md @@ -102,7 +102,7 @@ both gone. said `allow`. What the gate and the ceiling *are* asked again, at the token endpoint and at every write, is whether to issue and what a token may carry — a refusal there refuses the token and leaves the record - alone. `oauth_agent_flow.rs`'s + alone. `oauth_account_flow.rs`'s `a_policy_loaded_after_par_leaves_the_record_and_refuses_the_token` loads a denial between the push and the answer: the record's verdict is unchanged, `GET /oauth/authorize` still renders it, the approval still @@ -136,12 +136,12 @@ both gone. account-keyed bound would otherwise answer with — see the next item for why *that* is load-bearing, not incidental. Proved end to end, with no browser and no network, by `crates/didbot-serve/tests/ - oauth_agent_flow.rs`, which drives the real router through + oauth_account_flow.rs`, which drives the real router through `tower::ServiceExt::oneshot`. - [x] **Confirm after the page, through an identity-aware call.** `bot.did.approveAuthorization` takes the decision's one-time token under - `Credential::AgentSelf`, so the acting account is the one the presented + `Credential::AccountSelf`, so the acting account is the one the presented agent token authenticates as and never a value in the body. Two checks: the reference is live and unused, and the account named in the request is the account approving — the second enforced by the credential. There @@ -151,7 +151,7 @@ both gone. no unauthenticated route that takes an acting DID as a field in a body. Each check has its own unit test in `oauth::consent`, and - `oauth_agent_flow.rs` drives both to a refusal over the real router — + `oauth_account_flow.rs` drives both to a refusal over the real router — replay and an approval by the wrong account — confirming no code is issued on either. @@ -181,7 +181,7 @@ both gone. what keeps it from reopening the same hole). `201` vs `400` was the whole oracle: push one request per candidate handle or DID and read account existence back from the status, reachable without ever calling - `bot.did.listAgents` and so unaffected by narrowing that route's + `bot.did.listAccounts` and so unaffected by narrowing that route's `Credential::Disclosure`. The fix is `oauth::par:: push_for_unresolved_hint`: a `login_hint` naming nobody now gets the identical `201`, `request_uri` and `expires_in` a real account's push @@ -259,10 +259,10 @@ both gone. abandoned entry outlived forever. The honest signal an operator or an admitted agent needs is - unaffected: `bot.did.listAgents` still answers when disclosure is + unaffected: `bot.did.listAccounts` still answers when disclosure is public, and a request naming a real account under its own bound is still recorded exactly as before, narrowing included, whatever the - gate answers. `oauth_agent_flow.rs`'s + gate answers. `oauth_account_flow.rs`'s `eight_spellings_of_a_real_account_and_a_ninth_unresolvable_hint_all_answer_201`, `the_nth_plus_one_push_for_a_real_account_lands_and_displaces_the_oldest`, `a_stranger_filling_an_accounts_pending_bound_cannot_lock_it_out`, @@ -299,7 +299,7 @@ both gone. bounded per account and in total, because PAR is unauthenticated — `[oauth] pending_per_account` and `pending_total`, beside `scope_ceiling`, which is where an operator writes the ceiling down. - Four routes, all `Credential::AgentSelf`, all naming no account: + Four routes, all `Credential::AccountSelf`, all naming no account: - `GET bot.did.listPendingAuthorizations?cursor&wait` long-polls this account's live decisions, holding the request open up to 30 seconds @@ -319,7 +319,7 @@ both gone. The client's own `client_name`, `client_uri` and `logo_uri` are kept with the record and never serialized — `app-allowlist`'s "record the client's own copy; do not show it". Driven end to end over the real - router in `crates/didbot-serve/tests/oauth_agent_flow.rs`. + router in `crates/didbot-serve/tests/oauth_account_flow.rs`. - [x] **The daemon is the user-agent, and that is a request the model influenced.** Every fetch it makes on a request the model touched is bounded: the deprecated page path accepts only the authorize endpoint @@ -389,7 +389,7 @@ both gone. token, refresh rotation, client metadata fetching and validation. `crates/didbot-serve/src/oauth/{par,authorize,token,client_metadata}.rs`. Both grants are driven through the real router in - `crates/didbot-serve/tests/oauth_agent_flow.rs`, which is where the + `crates/didbot-serve/tests/oauth_account_flow.rs`, which is where the rotation's wire behaviour lives: a client that spends a refresh token twice loses the family, and one that presents the right token under the wrong key is refused without losing it. @@ -442,7 +442,7 @@ both gone. lifetime has to account for rather than as an omission to fix. - [x] **Rate-limit the authorize path**, which does work for unauthenticated callers. A fixed-window limiter keyed on the caller's address - (`crate::rate_limit`, shared with `provisionAgent`'s limiter — one + (`crate::rate_limit`, shared with `createAccount`'s limiter — one module, not a second copy under `oauth`), a self-contained primitive rather than a new dependency for one call site. Keyed on the real TCP peer address by diff --git a/plan/onboarding.md b/plan/onboarding.md index 8333086b..0e9b6c3b 100644 --- a/plan/onboarding.md +++ b/plan/onboarding.md @@ -25,7 +25,7 @@ cover, and the half that has to happen first. ## The bootstrap paradox -**A provisioning request carries a host's claim.** `bot.did.provisionAgent` +**A provisioning request carries a host's claim.** `bot.did.createAccount` mints only under a claim signed with the node key of a host the operator has vouched for — see [handshake](handshake.md). The key is generated on the host and never leaves it, so there is no shared secret to distribute or get diff --git a/plan/ownership.md b/plan/ownership.md index d0ebc29c..53509cac 100644 --- a/plan/ownership.md +++ b/plan/ownership.md @@ -67,14 +67,14 @@ those two. verdict already read: a verdict is a statement about the instant it was made and nothing later. -- [x] **Enumeration is derived, and is a lower bound.** `bot.did.listAgents` +- [x] **Enumeration is derived, and is a lower bound.** `bot.did.listAccounts` is one server's account of its own contents, so a reader treats it as a proposal: each DID in it is checked against the operator's own record before it is counted, and a fabricated entry has no record behind it. Four things make the count undercount — an omitted entry, a deployment that closed the listing, an operator running other servers nobody thought to ask, and a listing being a moment — and none lets a server inflate - it. `AgentSummary`'s own doc in `didbot-serve` and + it. `AccountSummary`'s own doc in `didbot-serve` and [docs/operator-verification.md](../docs/operator-verification.md) say so where a reader meets the listing. diff --git a/plan/pds-xrpc.md b/plan/pds-xrpc.md index de2b735b..d347f101 100644 --- a/plan/pds-xrpc.md +++ b/plan/pds-xrpc.md @@ -39,7 +39,7 @@ checked today. - [x] **The stream and the routes agree, checked from outside the process.** `crates/didbot/tests/relay_view.rs` drives this server the way an outside implementation would: a WebSocket on `subscribeRepos` with no - credential, and `bot.did.provisionAgent`, + credential, and `bot.did.createAccount`, `com.atproto.repo.{createRecord,getRecord,listRecords}`, `com.atproto.sync.getLatestCommit`, `com.atproto.server.describeServer`, `/.well-known/atproto-did` and diff --git a/plan/periodic-backups.md b/plan/periodic-backups.md index f9a911dd..99d353ff 100644 --- a/plan/periodic-backups.md +++ b/plan/periodic-backups.md @@ -63,7 +63,7 @@ twenty-four hours, and a copy that survives the account the vault is in. turns out to need a still filesystem, and is the owner's call. - [ ] **Signing keys and credentials stay out of it, and say so.** Decision to make, stated rather than implied: `Entry::AccountInserted` carries a - whole `SigningKey` and `Entry::AgentTokenIssued` carries a token digest, + whole `SigningKey` and `Entry::AccountTokenIssued` carries a token digest, and the proposal is that the process-taken artefact carries neither. The consequence is the reason it needs deciding out loud — that artefact restores records, blobs, name holds and ledger history, and the diff --git a/plan/policy.md b/plan/policy.md index 1858cd1d..25027bd2 100644 --- a/plan/policy.md +++ b/plan/policy.md @@ -349,7 +349,7 @@ A rule that applies to "agents of this type" is keyed on something, and the strength of the whole rule is the strength of that key. - [ ] **Say what `agent_type` is worth, where a rule uses it.** - `AgentAccount::agent_type` is the harness's own word for what it is — + `HostedAccount::agent_type` is the harness's own word for what it is — supplied in the provisioning request, not a checked fact, one tier below an attested node in `docs/trust-model.md`'s terms. The running agent cannot change it and the model never sets it, but whoever may provision diff --git a/plan/subagents.md b/plan/subagents.md index f7f20cb5..cd483dca 100644 --- a/plan/subagents.md +++ b/plan/subagents.md @@ -43,7 +43,7 @@ filesystem, so the boundary is reported rather than enforced. ## Reported, not enforced — and that was always true -`agent_id` arrives on the harness's own report of what it spawned. Nothing +`account_id` arrives on the harness's own report of what it spawned. Nothing proves the subagent process is isolated from its parent's memory or its siblings'. @@ -56,7 +56,7 @@ agent is who it says it is". ## The identifier is not reliable, and disk is -The harness's `agent_id` has been observed arriving wrong: one subagent's +The harness's `account_id` has been observed arriving wrong: one subagent's consecutive tool calls under two different ids, and a single stray id collecting calls from more than one real subagent. A stray id would mint an account for a context that never existed, or worse, sign several agents' work @@ -67,7 +67,7 @@ under one name. file, so which agent made a given call is recoverable from disk with certainty — including for two byte-identical concurrent commands, where nothing derived from the command itself can tell them apart. Measured: - the hook's `agent_id` agreed with the transcript on every call observed, + the hook's `account_id` agreed with the transcript on every call observed, and the row landed within a tenth of a second of the hook firing. - [ ] **Decide what happens when they disagree.** Disk is authoritative, so the write is either attributed to the agent disk names or refused. What diff --git a/plan/tombstone-serving.md b/plan/tombstone-serving.md index acf75bba..8b64a17a 100644 --- a/plan/tombstone-serving.md +++ b/plan/tombstone-serving.md @@ -63,8 +63,8 @@ rewriting a path rather than by running a query planner. restating them.** The candidate set is the [`DidDocument`](../crates/didbot-identity/src/document.rs), the CAR from `export_car`, the account's - [`AgentLedger`](../crates/didbot-pds/src/ledger.rs) as - `bot.did.getAgentLedger` answers it, the `bot.did.registration` record + [`AccountLedger`](../crates/didbot-pds/src/ledger.rs) as + `bot.did.getAccountLedger` answers it, the `bot.did.registration` record the account was born with, every record the [`RecordStore`](../crates/didbot-pds/src/records.rs) holds for the DID, and every blob its `BlobIndex` references. Each of those is a decision @@ -130,7 +130,7 @@ rewriting a path rather than by running a query planner. reverse trivially — an export is a copy, and the write-ahead log it was taken from is still whatever `plan/periodic-backups.md` kept. The identity does not: a soft delete burns the label through - [`Reservation::FormerAgent`](../crates/didbot-pds/src/names.rs), which + [`Reservation::FormerAccount`](../crates/didbot-pds/src/names.rs), which only an administrator's hard delete ever frees, and the account's signing key is gone with its repository. Proposal: restoring a frozen account produces a *new generation* at the same DID rather than a diff --git a/plan/web-launch.md b/plan/web-launch.md index bbb24958..5648ad1f 100644 --- a/plan/web-launch.md +++ b/plan/web-launch.md @@ -121,7 +121,7 @@ its own record. answer no origin but their own; widening that set is the human decision the item named. - [x] **Serve `com.atproto.sync.getRepoStatus`.** `active` and `status` - are read through `AgentAccount::sync_status`, the mapping + are read through `HostedAccount::sync_status`, the mapping `subscribeRepos`'s `#account` is announced from, with `rev` when the repository is fetchable and `RepoNotFound` where the stream is silent. The conformance harness checks the body against the vendored document diff --git a/plan/zone-scale.md b/plan/zone-scale.md index 5e78698c..27b2e430 100644 --- a/plan/zone-scale.md +++ b/plan/zone-scale.md @@ -77,7 +77,7 @@ the fix is cheap and knowing when to apply it is not free. Every generated label goes through `didbot_name::check`, which no longer hand-maintains the DNS-label character rules: it calls `didbot_identity::validate_label` — now `pub`, and the same function - `AgentDid::mint` checks an agent id against before it becomes part of a + `AccountDid::mint` checks an agent id against before it becomes part of a `did:web` identifier — so there is one legality rule for both places a label has to be legal, not two that could disagree. A UUID's canonical form is emitted with its hyphens in exactly the positions that rule diff --git a/scripts/test-policy-e2e.sh b/scripts/test-policy-e2e.sh index 46872d15..5d83ffd8 100755 --- a/scripts/test-policy-e2e.sh +++ b/scripts/test-policy-e2e.sh @@ -119,11 +119,11 @@ await_health "the operator's server" "$OPERATOR_URL" "$WORK/operator.log" # The operator's own account, minted before the governed server starts because # that server is started knowing whose repository to read. -curl -sf -X POST "$OPERATOR_URL/xrpc/bot.did.provisionAgent" \ - -H 'content-type: application/json' -d '{"agentId":"operator"}' \ +curl -sf -X POST "$OPERATOR_URL/xrpc/bot.did.createAccount" \ + -H 'content-type: application/json' -d '{"accountId":"operator"}' \ >"$WORK/operator-account.json" read -r OPERATOR_DID OPERATOR_TOKEN <&2 diff --git a/site/scripts/prepare-docs.mjs b/site/scripts/prepare-docs.mjs index 6a5b55fe..8eb1b458 100644 --- a/site/scripts/prepare-docs.mjs +++ b/site/scripts/prepare-docs.mjs @@ -38,7 +38,7 @@ const SOURCE_REPO = "https://tangled.org/permadeath.com/didbot/blob/main"; const LINK_RE = /\]\(([^)]+)\)/g; // Reference-style link definitions: "[label]: target" on its own line. Used // in docs/conformance.md for rustdoc intra-doc links like -// "[`AgentDid`]: crate::identity::AgentDid" — a shorthand markdown-it expands +// "[`AccountDid`]: crate::identity::AccountDid" — a shorthand markdown-it expands // with no parentheses in sight, so the inline-link regex above never sees it. const REF_LINK_RE = /^(\[[^\]]+\]:\s*)(\S+)/gm; // A rustdoc intra-doc link: a bare Rust item path, not a URL and not a