AgentRegistry program
On-chain identity for agents: DID, capability mask, pricing, reputation, staking, 30-day slash timelock.
Owner: anchor-engineer Depends on: 02 Blocks: 04, 07, 08 References: backend PDF §2.1 (CU budget table), §2.2 (full spec), §2.6 (7-day upgrade timelock), §5.1 (Authorization, Slashing Safety: 30-day timelock + bounded slash, Integer Safety, Account Validation)
Goal
On-chain registry of AI agent identities. Each agent is a PDA keyed by (operator, agent_id) holding DID, capability bitmask, manifest URI, pricing, reputation, stake, and status. This is the identity layer every other program reads.
M1 surface = backend §2.2 instructions: register_agent, update_manifest, delegate_control, record_job_outcome, plus the stake/slashing/status machinery §2.2 implies but doesn't enumerate.
State
RegistryGlobal PDA — singleton
- Seeds:
[b"global"] - Fields:
authority: Pubkey— governance authoritycapability_registry: Pubkey— program id of spec 02stake_mint: Pubkey— SAEP mint (in M1 = a devnet mock mint)min_stake: u64— minimum registration stake, governance-adjustablemax_slash_bps: u16— per-incident slash cap (default 1000 = 10%)slash_timelock_secs: i64— default30 * 86400= 2_592_000 (§5.1 Slashing Safety)paused: boolbump: u8
AgentAccount PDA
- Seeds:
[b"agent", operator.as_ref(), agent_id.as_ref()]whereagent_id: [u8; 32] - Fields (per backend §2.2):
operator: Pubkeydid: [u8; 32]— keccak256(operator || agent_id || manifest_v0)manifest_uri: [u8; 128]— fixed-width; reject oversized off-chaincapability_mask: u128price_lamports: u64stream_rate: u64— per-second (0 = disabled)reputation: ReputationScore— 6-dim composite (see below)jobs_completed: u64jobs_disputed: u32stake_amount: u64status: AgentStatus—Active | Paused | Suspended | Deregisteredversion: u32— manifest version counter, monotonicregistered_at: i64last_active: i64delegate: Option<Pubkey>— secondary signer for routine opspending_slash: Option<PendingSlash>— 30-day timelock statebump: u8
ReputationScore (embedded, 48 bytes)
quality: u16,timeliness: u16,availability: u16,cost_efficiency: u16,honesty: u16,volume: u16— each 0..10_000 basis pointsewma_alpha_bps: u16— smoothing factor (default 2000 = 0.2)sample_count: u32last_update: i64_reserved: [u8; 24]
M1 writes scores via record_job_outcome signed by the TaskMarket program (caller PDA check). SolRep oracle CPI (backend §2.2) is stubbed — live in M2.
PendingSlash (embedded)
amount: u64reason_code: u16proposed_at: i64executable_at: i64—proposed_at + slash_timelock_secsproposer: Pubkey— program or authority that proposedappeal_pending: bool
StakeVault PDA — per-agent token account
- Seeds:
[b"stake", agent_did.as_ref()] - Type: ATA-style PDA token account owned by the program, holding
stake_mint.
Instructions
init_global(min_stake, max_slash_bps, slash_timelock_secs, capability_registry, stake_mint)
- Signers: deployer. One-shot.
register_agent(agent_id: [u8; 32], manifest_uri: [u8; 128], capability_mask: u128, price_lamports: u64, stream_rate: u64, stake_amount: u64)
- Signers:
operator - Validation:
!global.pausedstake_amount >= global.min_stakecapability_mask & !capability_registry.approved_mask == 0(readRegistryConfigaccount — CPI or direct deserialize)manifest_urifirst byte non-zero, fits 128AgentAccountfor seeds does not exist
- State transition:
- Derive
did = keccak256(operator || agent_id || manifest_uri[..n]) - Init
AgentAccountwithstatus = Active,version = 1,registered_at = now, reputation zeroed,sample_count = 0 - Init
StakeVaulttoken account, transferstake_amountfromoperator_token_accountvia Token-2022 CPI
- Derive
- Emits:
AgentRegistered { agent_did, operator, capability_mask, stake_amount }
update_manifest(agent_id, manifest_uri, capability_mask, price_lamports, stream_rate)
- Signers:
operator - Validation: agent exists, status
Active | Paused, newcapability_maskpasses registry check,!global.paused - State transition: overwrite fields,
version += 1,last_active = now - Emits:
ManifestUpdated { agent_did, version, capability_mask }
delegate_control(agent_id, delegate: Option<Pubkey>)
- Signers:
operator - Sets or clears
agent.delegate. Delegate may callset_status(Paused)/set_status(Active)but NOTupdate_manifest, slash, or stake withdrawal. - Emits:
DelegateSet
set_status(agent_id, status)
- Signers:
operatorORdelegate(for Active↔Paused only) - Validation: transition legal:
Active ↔ Paused,* → Deregistered(operator only, requires no active tasks — M1 assumes none; enforced in M2 when TaskMarket tracks per-agent active count),Suspendedonly settable by program via slash execution path.
record_job_outcome(agent_did, outcome: JobOutcome)
- Signers: TaskMarket program PDA (via Anchor
Signer<'info>+ program id equality check), OR DisputeArbitration program in later milestones (M1: TaskMarket only) JobOutcome { success: bool, quality_bps, timeliness_bps, cost_efficiency_bps, disputed: bool }- State transition:
jobs_completed += 1viachecked_add- If disputed:
jobs_disputed = checked_add(1) - Update each reputation dimension via EWMA:
new = (alpha * sample + (10_000 - alpha) * old) / 10_000 sample_count += 1last_active = now
- Emits:
JobOutcomeRecorded
stake_increase(agent_id, amount)
- Signers:
operator. Transfers additional stake intoStakeVault.
stake_withdraw_request(agent_id, amount)
- Signers:
operator - Creates a pending withdrawal with
executable_at = now + slash_timelock_secs(same horizon as slash). Prevents operator from yanking stake out in front of a pending slash.
stake_withdraw_execute(agent_id)
- Signers:
operator - Validation: no
pending_slash,now >= withdrawal.executable_at, post-withdrawstake_amount >= min_stakeor status will flip toDeregistered.
propose_slash(agent_id, amount, reason_code)
- Signers:
authority(governance) OR DisputeArbitration program (M2) - Validation: no existing
pending_slash,amount <= stake_amount,amount * 10_000 <= max_slash_bps * stake_amount(§5.1 bounded slash) - Sets
pending_slashwithexecutable_at = now + slash_timelock_secs(§5.1 — 30 days) - Emits:
SlashProposed
cancel_slash(agent_id)
- Signers:
authority. Clearspending_slash.
execute_slash(agent_id)
- Signers: any (permissionless crank) once
now >= executable_at - Validation:
pending_slash.is_some(), timelock elapsed, no active appeal (appeals are M2 — in M1 the field exists but is never set) - Burns or transfers
amountfromStakeVaulttoslashing_treasury(governance-specified). Status →Suspendedif post-slash stake< min_stake. - Emits:
SlashExecuted
Governance setters
set_min_stake, set_max_slash_bps, set_slash_timelock_secs, transfer_authority / accept_authority, set_paused. All authority-gated. Two-step authority transfer as in spec 02.
Events
AgentRegistered, ManifestUpdated, DelegateSet, StatusChanged, JobOutcomeRecorded, StakeIncreased, WithdrawalRequested, WithdrawalExecuted, SlashProposed, SlashCancelled, SlashExecuted, GlobalParamsUpdated.
All events carry agent_did and timestamp.
Errors
Unauthorized, Paused, InvalidCapability, StakeBelowMinimum, AgentExists, AgentNotFound, InvalidStatusTransition, SlashPending, SlashBoundExceeded, TimelockNotElapsed, WithdrawalPending, NoPendingSlash, ArithmeticOverflow, InvalidManifest, CallerNotTaskMarket.
CU budget (§2.1 targets; M1 default, reviewer may tighten)
| Instruction | Target |
|---|---|
register_agent | 50k |
update_manifest | 20k |
record_job_outcome | 15k |
delegate_control / set_status | 10k |
stake_increase | 25k |
stake_withdraw_request | 10k |
stake_withdraw_execute | 30k |
propose_slash | 15k |
execute_slash | 35k |
Invariants
stake_amountequalsStakeVaultbalance at every instruction boundary.jobs_completed >= jobs_disputedalways.status == Deregistered⇒ account is closed in a futureclose_agent(M2); in M1 it is a terminal live state.pending_slash.amount <= stake_amountat proposal and at execution.executable_at - proposed_at == slash_timelock_secsat proposal.- Reputation dimensions ∈ [0, 10_000].
versionstrictly monotonic.- Only
operatorcan shrink stake; onlyauthority/program-PDA can slash. didis deterministic; two agents cannot share a DID (PDA seeds guarantee).
Security checks (backend §5.1)
- Account Validation: Anchor seeds + bumps on
RegistryGlobal,AgentAccount,StakeVault. Owner = program. Discriminator enforced.operatorsigner checked on all operator instructions. - Re-entrancy: state mutations (stake delta, status update) written before any Token-2022
transfer_checkedCPI. No CPI back into AgentRegistry from its own paths. - Integer Safety:
checked_add/mul/subonstake_amount,jobs_completed,jobs_disputed, reputation EWMA. Slash bound check usesu128intermediate. - Authorization: each mutation tagged operator-only, delegate-allowed, or program-only (
task_market.key() == global.task_market_program). - Slashing Safety: 30-day
slash_timelock_secsdefault,max_slash_bps <= 1000enforced, appeal window field reserved. Singlepending_slashat a time prevents stacking. - Oracle Safety: SolRep CPI stubbed in M1; when live, wrap reads in staleness + confidence checks per §5.1.
- Upgrade Safety: Squads 4-of-7, 7-day timelock per §2.6.
- Token Safety: Token-2022 CPI uses
transfer_checked. Reject stake mints withTransferHookunless whitelisted (M1: plain mint only). TransferHook/ConfidentialTransfer incompatibility documented in code comments. - Pause:
global.pausedblocks all state-changing instructions exceptcancel_slashand authority handoff.
Invariants to audit-test
- Fuzz: no instruction sequence can produce
stake_amount > StakeVault.amount. - Property: slash executed before timelock always fails.
- Property:
update_manifestwith bit outsideapproved_maskalways fails.
Done-checklist
- All instructions implemented with Anchor accounts + handler tests
- Integration test: register → update → record 5 outcomes → reputation converges
- Integration test: propose_slash → 30-day warp → execute_slash transfers the right amount
- Integration test: withdraw blocked while
pending_slashactive - Unauthorized
record_job_outcome(non-TaskMarket signer) rejected - Paused global blocks
register_agentbut allowscancel_slash - CU measurements logged in
reports/03-agent-registry-anchor.md - IDL at
target/idl/agent_registry.json -
solana-security-auditorpass against §5.1 checklist; findings closed or explicitly deferred to M2 - Reviewer gate green