feat(identity): add user and fleet ownership - #1470
Conversation
Signed-off-by: Joshua J. Bouw <jjb@unicity-labs.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Suppressed comments (1)
crates/astrid-core/src/identity/ownership.rs:326
- This role documentation understates the restriction: the implementation rejects an administrator changing or removing any owner, not only the last owner. Describe owner membership as entirely owner-controlled so API consumers do not rely on authority they do not have.
/// Manages members and principal assignment but cannot remove the last owner.
Signed-off-by: Joshua J. Bouw <jjb@unicity-labs.com>
Signed-off-by: Joshua J. Bouw <jjb@unicity-labs.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
crates/astrid-core/src/identity/ownership.rs:475
- These durability tests only assert formatting and self-consistency, so an accidental change to the domain separator or canonical byte layout would still pass while changing every persisted UID. Pin exact known digests for both
UserUidandFleetUid, as the analogousPrincipalUidtest does incrates/astrid-core/src/identity/principal.rs:294-296.
fn user_uid_is_stable_and_canonical() {
let identity = user();
assert_eq!(identity.uid.to_string().len(), 64);
assert_eq!(identity.uid.to_string().parse(), Ok(identity.uid));
assert_eq!(identity.validate(), Ok(()));
Signed-off-by: Joshua J. Bouw <jjb@unicity-labs.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.
Suppressed comments (2)
crates/astrid-core/src/identity/ownership.rs:475
- This only checks output length and round-trips through the same implementation, so changing the domain key, field order, or endianness would still pass while changing every durable
UserUid. Pin literal expected-hex vectors for both user and fleet derivations, as the existingPrincipalUidtest does incrates/astrid-core/src/identity/principal.rs:291-296.
fn user_uid_is_stable_and_canonical() {
let identity = user();
assert_eq!(identity.uid.to_string().len(), 64);
assert_eq!(identity.uid.to_string().parse(), Ok(identity.uid));
assert_eq!(identity.validate(), Ok(()));
crates/astrid-storage/src/ownership.rs:877
- Both writers are clones of the same
OwnershipStore, so they sharemutation_lockand are serialized before either reads the graph. This test would still pass if the CAS retry logic were removed, so it does not cover the concurrent-writer/no-lost-update acceptance criterion. Construct independent stores over the same backend and shared principal directory, ideally with a test backend/barrier that forces both to read the same version before CAS.
let first_store = store.clone();
let second_store = store.clone();
Signed-off-by: Joshua J. Bouw <jjb@unicity-labs.com>
Signed-off-by: Joshua J. Bouw <jjb@unicity-labs.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
crates/astrid-storage/src/ownership.rs:265
- This guard only excludes writers sharing this process-local mutex. An independently coordinated writer can read the unowned graph, wait until this guard has deleted the durable identity, and then successfully CAS its assignment because deletion never changes
GRAPH_KEY; that leaves a principal edge which immediately failsload(). The CAS test below explicitly models writers that bypass this mutex, so deletion needs a durable graph reservation/tombstone (or one transaction spanning the graph and identity record), not only an in-memory barrier.
let guard = Arc::clone(&self.mutation_lock).lock_owned().await;
if let Some(ownership) = self.load().await?.principal_owner(principal_uid) {
Signed-off-by: Joshua J. Bouw <jjb@unicity-labs.com>
Signed-off-by: Joshua J. Bouw <jjb@unicity-labs.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
crates/astrid-storage/src/ownership.rs:385
- This check only runs when starting another deletion; it does not reject rebinding the alias.
ownership_tests.rs:532successfully registers the reserved alias to a second UID, which can then be assigned and used normally until its own deletion fails withDeletionAliasReserved. Enforce the reservation in the identity alias registration/create path and make the test assert that the rebind itself fails, as promised by the PR description.
if let Some(requested) = &alias
&& let Some((reserved_uid, _)) = graph
.principal_deletions
.iter()
.find(|(_, reservation)| reservation.alias.as_ref() == Some(requested))
Signed-off-by: Joshua J. Bouw <jjb@unicity-labs.com>
Signed-off-by: Joshua J. Bouw <jjb@unicity-labs.com>
Linked Issue
Closes #1469.
Summary
Add Astrid-native human and fleet ownership identities without conflating
ownership with capability groups. The kernel now owns a fail-closed, atomically
persisted ownership graph while preserving existing CLI, HTTP, profile, group,
and
StateOwnerCodecV1behavior.Changes
UserUidandFleetUidgenesis identitiescapability groups
creation and its initial owner commit together
authorization
removing/demoting owners
the durable graph while identity deletion is in progress
frontend CLI link was already removed
recoverable after the durable user and live directory entry are both gone
deletion
an active deletion reservation
state
defaultCLIoperator into one user, one default fleet, and one owned principal
defaultprincipal left by an interruptedfirst boot instead of attempting to create a duplicate
the repository source-size policy
non-root assignment, and AOS work
Verification
cargo test -p astrid-core identity::ownership -- --nocapture— 4 passedcargo test -p astrid-storage --lib -- --quiet— 270 passed; 6 ignoredcargo test -p astrid-kernel --lib -- --quiet— 294 passedcargo test -p astrid-kernel legacy_root_ownership_bootstrap_is_deterministic_and_idempotent -- --nocapture— passedcargo test -p astrid-kernel cli_root_bootstrap_recovers_a_durable_principal_without_its_link -- --nocapture— passedcargo test -p astrid-storage ownership::tests -- --quiet— 12 passedcargo test -p astrid-storage deletion_reservation -- --nocapture— 3 passedcargo test -p astrid-storage stale_assignment_retries_and_observes_deletion_reservation -- --nocapture— passedcargo test -p astrid-storage deletion_guard_serializes_assignment_with_directory_removal -- --nocapture— passedcargo test -p astrid-storage concurrent_principal_assignments_do_not_lose_updates -- --nocapture— passedcargo test -p astrid-kernel agent_delete_rejects_a_fleet_owned_principal_without_partial_deletion -- --nocapture— passedcargo test -p astrid-kernel agent_delete_retry_clears_reservation_after_identity_was_removed -- --nocapture— passedcargo fmt --all -- --checkcargo clippy --workspace --all-targets --all-features -- -D warningsgit diff --checkGitHub Actions may currently be unavailable. Local verification above is
complete; a missing hosted run is not represented as a passing CI result.
AI / Tool Assistance
Assisted-by: Codex: GPT-5
Codex assisted with implementation, adversarial invariant review,
regression-test design, documentation, local validation, and PR preparation.
After reviewing the staged changes, the human maintainer explicitly authorized
Codex to create the GPG-signed, signed-off commit using the configured
maintainer identity and accepts the DCO attestation and ownership of the work.
Checklist
[Unreleased]and validation.
this PR.
Signed-off-bytrailer.