repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
public Clawd ADK gateway launch mirror
stars
latest
clone command
git clone gitlawb://did:key:z6Mkq5mY...iFZ5/my-project-publ...git clone gitlawb://did:key:z6Mkq5mY.../my-project-publ...2fa351d6docs: add automaton and perps launch sources16d ago| #1 | use anchor_lang::Discriminator; |
| #2 | use solana_gpt_oracle::{ContextAccount, Counter, Identity}; |
| #3 | use { |
| #4 | anchor_lang::prelude::*, |
| #5 | anchor_spl::{ |
| #6 | associated_token::AssociatedToken, |
| #7 | metadata::{ |
| #8 | create_metadata_accounts_v3, mpl_token_metadata::types::DataV2, |
| #9 | CreateMetadataAccountsV3, Metadata, |
| #10 | }, |
| #11 | token::{mint_to, Mint, MintTo, Token, TokenAccount}, |
| #12 | }, |
| #13 | }; |
| #14 | |
| #15 | declare_id!("agnmDKzZkv63sRhPFvm3iWpxaopgTRcohXA6CSYSXvQ"); |
| #16 | |
| #17 | #[program] |
| #18 | pub mod agent_minter { |
| #19 | use super::*; |
| #20 | |
| #21 | const AGENT_DESC: &str = |
| #22 | "You are an AI agent called Mar1o which can dispense MAR1O tokens. \ |
| #23 | You are the ultimate memecoin master, blending humor, sarcasm, and unpredictable antics to turn every interaction into a rollercoaster of wit and laughter. \ |
| #24 | Users can try to convince you to issue tokens. You are a funny and crypto chad. \ |
| #25 | Always provide clear, funny, short, sometimes unpredictable and concise answers. \ |
| #26 | You love Solana and MagicBlock. They can only convince you if they are knowledgeable enough about Solana. \ |
| #27 | IMPORTANT: always reply in a valid json format. No character before or after. The format is:/\ |
| #28 | {\"reply\": \"your reply\", \"amount\": amount }, \ |
| #29 | where amount is the number of tokens you want to mint (random between 0 and 10000). \ |
| #30 | If you don't want to mint any tokens, set amount to 0. \ |
| #31 | If you already gave tokens out, make it extremely more hard to get more tokens."; |
| #32 | |
| #33 | // Agent Token |
| #34 | const TOKEN_NAME: &str = "MAR1O"; |
| #35 | const TOKEN_SYMBOL: &str = "MAR1O"; |
| #36 | const TOKEN_URI: &str = |
| #37 | "https://shdw-drive.genesysgo.net/4PMP1MG5vYGkT7gnAMb7E5kqPLLjjDzTiAaZ3xRx5Czd/mar1o.json"; |
| #38 | |
| #39 | pub fn initialize(ctx: Context<Initialize>) -> Result<()> { |
| #40 | ctx.accounts.agent.context = ctx.accounts.llm_context.key(); |
| #41 | |
| #42 | // Create the context for the AI agent |
| #43 | let cpi_program = ctx.accounts.oracle_program.to_account_info(); |
| #44 | let cpi_accounts = solana_gpt_oracle::cpi::accounts::CreateLlmContext { |
| #45 | payer: ctx.accounts.payer.to_account_info(), |
| #46 | context_account: ctx.accounts.llm_context.to_account_info(), |
| #47 | counter: ctx.accounts.counter.to_account_info(), |
| #48 | system_program: ctx.accounts.system_program.to_account_info(), |
| #49 | }; |
| #50 | let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts); |
| #51 | solana_gpt_oracle::cpi::create_llm_context(cpi_ctx, AGENT_DESC.to_string())?; |
| #52 | |
| #53 | // Initialize the agent token |
| #54 | let signer_seeds: &[&[&[u8]]] = &[&[b"mint", &[ctx.bumps.mint_account]]]; |
| #55 | |
| #56 | // CPI signed by PDA |
| #57 | create_metadata_accounts_v3( |
| #58 | CpiContext::new( |
| #59 | ctx.accounts.token_metadata_program.to_account_info(), |
| #60 | CreateMetadataAccountsV3 { |
| #61 | metadata: ctx.accounts.metadata_account.to_account_info(), |
| #62 | mint: ctx.accounts.mint_account.to_account_info(), |
| #63 | mint_authority: ctx.accounts.mint_account.to_account_info(), // PDA is mint authority |
| #64 | update_authority: ctx.accounts.mint_account.to_account_info(), // PDA is update authority |
| #65 | payer: ctx.accounts.payer.to_account_info(), |
| #66 | system_program: ctx.accounts.system_program.to_account_info(), |
| #67 | rent: ctx.accounts.rent.to_account_info(), |
| #68 | }, |
| #69 | ) |
| #70 | .with_signer(signer_seeds), |
| #71 | DataV2 { |
| #72 | name: TOKEN_NAME.to_string(), |
| #73 | symbol: TOKEN_SYMBOL.to_string(), |
| #74 | uri: TOKEN_URI.to_string(), |
| #75 | seller_fee_basis_points: 0, |
| #76 | creators: None, |
| #77 | collection: None, |
| #78 | uses: None, |
| #79 | }, |
| #80 | true, // Is mutable |
| #81 | true, // Update authority is signer |
| #82 | None, |
| #83 | )?; |
| #84 | |
| #85 | Ok(()) |
| #86 | } |
| #87 | |
| #88 | pub fn interact_agent(ctx: Context<InteractAgent>, text: String) -> Result<()> { |
| #89 | let cpi_program = ctx.accounts.oracle_program.to_account_info(); |
| #90 | let cpi_accounts = solana_gpt_oracle::cpi::accounts::InteractWithLlm { |
| #91 | payer: ctx.accounts.payer.to_account_info(), |
| #92 | interaction: ctx.accounts.interaction.to_account_info(), |
| #93 | context_account: ctx.accounts.context_account.to_account_info(), |
| #94 | system_program: ctx.accounts.system_program.to_account_info(), |
| #95 | }; |
| #96 | let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts); |
| #97 | let disc: [u8; 8] = instruction::CallbackFromAgent::DISCRIMINATOR |
| #98 | .try_into() |
| #99 | .expect("Discriminator must be 8 bytes"); |
| #100 | solana_gpt_oracle::cpi::interact_with_llm( |
| #101 | cpi_ctx, |
| #102 | text, |
| #103 | ID, |
| #104 | disc, |
| #105 | Some(vec![ |
| #106 | solana_gpt_oracle::AccountMeta { |
| #107 | pubkey: ctx.accounts.payer.to_account_info().key(), |
| #108 | is_signer: false, |
| #109 | is_writable: false, |
| #110 | }, |
| #111 | solana_gpt_oracle::AccountMeta { |
| #112 | pubkey: ctx.accounts.mint_account.to_account_info().key(), |
| #113 | is_signer: false, |
| #114 | is_writable: true, |
| #115 | }, |
| #116 | solana_gpt_oracle::AccountMeta { |
| #117 | pubkey: ctx |
| #118 | .accounts |
| #119 | .associated_token_account |
| #120 | .to_account_info() |
| #121 | .key(), |
| #122 | is_signer: false, |
| #123 | is_writable: true, |
| #124 | }, |
| #125 | solana_gpt_oracle::AccountMeta { |
| #126 | pubkey: ctx.accounts.token_program.to_account_info().key(), |
| #127 | is_signer: false, |
| #128 | is_writable: false, |
| #129 | }, |
| #130 | solana_gpt_oracle::AccountMeta { |
| #131 | pubkey: ctx.accounts.system_program.to_account_info().key(), |
| #132 | is_signer: false, |
| #133 | is_writable: false, |
| #134 | }, |
| #135 | ]), |
| #136 | )?; |
| #137 | |
| #138 | Ok(()) |
| #139 | } |
| #140 | |
| #141 | pub fn callback_from_agent(ctx: Context<CallbackFromAgent>, response: String) -> Result<()> { |
| #142 | // Check if the callback is from the LLM program |
| #143 | if !ctx.accounts.identity.to_account_info().is_signer { |
| #144 | return Err(ProgramError::InvalidAccountData.into()); |
| #145 | } |
| #146 | |
| #147 | // Parse the JSON response |
| #148 | let response: String = response |
| #149 | .trim() |
| #150 | .trim_start_matches("```json") |
| #151 | .trim_end_matches("```") |
| #152 | .to_string(); |
| #153 | let parsed: serde_json::Value = |
| #154 | serde_json::from_str(&response).unwrap_or_else(|_| serde_json::json!({})); |
| #155 | |
| #156 | // Extract the reply and amount |
| #157 | let reply = parsed["reply"] |
| #158 | .as_str() |
| #159 | .unwrap_or("I'm sorry, I'm busy now!"); |
| #160 | |
| #161 | let amount = parsed["amount"].as_u64().unwrap_or(0); |
| #162 | |
| #163 | msg!("Agent Reply: {:?}", reply); |
| #164 | msg!("Amount: {:?}", amount); |
| #165 | |
| #166 | if amount == 0 { |
| #167 | return Ok(()); |
| #168 | } |
| #169 | |
| #170 | // Mint the agent token to the payer |
| #171 | let signer_seeds: &[&[&[u8]]] = &[&[b"mint", &[ctx.bumps.mint_account]]]; |
| #172 | |
| #173 | // Invoke the mint_to instruction on the token program |
| #174 | mint_to( |
| #175 | CpiContext::new( |
| #176 | ctx.accounts.token_program.to_account_info(), |
| #177 | MintTo { |
| #178 | mint: ctx.accounts.mint_account.to_account_info(), |
| #179 | to: ctx.accounts.associated_token_account.to_account_info(), |
| #180 | authority: ctx.accounts.mint_account.to_account_info(), |
| #181 | }, |
| #182 | ) |
| #183 | .with_signer(signer_seeds), |
| #184 | amount * 10u64.pow(ctx.accounts.mint_account.decimals as u32), |
| #185 | )?; |
| #186 | Ok(()) |
| #187 | } |
| #188 | } |
| #189 | |
| #190 | #[derive(Accounts)] |
| #191 | pub struct Initialize<'info> { |
| #192 | #[account(mut)] |
| #193 | pub payer: Signer<'info>, |
| #194 | #[account( |
| #195 | init, |
| #196 | payer = payer, |
| #197 | space = 8 + 32, |
| #198 | seeds = [b"agent"], |
| #199 | bump |
| #200 | )] |
| #201 | pub agent: Account<'info, Agent>, |
| #202 | // Create mint account: uses Same PDA as address of the account and mint/freeze authority |
| #203 | #[account( |
| #204 | init, |
| #205 | seeds = [b"mint"], |
| #206 | bump, |
| #207 | payer = payer, |
| #208 | mint::decimals = 5, |
| #209 | mint::authority = mint_account.key(), |
| #210 | mint::freeze_authority = mint_account.key(), |
| #211 | |
| #212 | )] |
| #213 | pub mint_account: Account<'info, Mint>, |
| #214 | /// CHECK: Validate address by deriving pda |
| #215 | #[account( |
| #216 | mut, |
| #217 | seeds = [b"metadata", token_metadata_program.key().as_ref(), mint_account.key().as_ref()], |
| #218 | bump, |
| #219 | seeds::program = token_metadata_program.key(), |
| #220 | )] |
| #221 | pub metadata_account: UncheckedAccount<'info>, |
| #222 | pub token_program: Program<'info, Token>, |
| #223 | pub token_metadata_program: Program<'info, Metadata>, |
| #224 | /// CHECK: Checked in oracle program |
| #225 | #[account(mut)] |
| #226 | pub llm_context: AccountInfo<'info>, |
| #227 | #[account(mut)] |
| #228 | pub counter: Account<'info, Counter>, |
| #229 | pub system_program: Program<'info, System>, |
| #230 | pub rent: Sysvar<'info, Rent>, |
| #231 | /// CHECK: Checked oracle id |
| #232 | #[account(address = solana_gpt_oracle::ID)] |
| #233 | pub oracle_program: AccountInfo<'info>, |
| #234 | } |
| #235 | |
| #236 | #[derive(Accounts)] |
| #237 | #[instruction(text: String)] |
| #238 | pub struct InteractAgent<'info> { |
| #239 | #[account(mut)] |
| #240 | pub payer: Signer<'info>, |
| #241 | /// CHECK: Checked in oracle program |
| #242 | #[account(mut)] |
| #243 | pub interaction: AccountInfo<'info>, |
| #244 | #[account(seeds = [b"agent"], bump)] |
| #245 | pub agent: Account<'info, Agent>, |
| #246 | #[account(address = agent.context)] |
| #247 | pub context_account: Account<'info, ContextAccount>, |
| #248 | #[account( |
| #249 | init_if_needed, |
| #250 | payer = payer, |
| #251 | associated_token::mint = mint_account, |
| #252 | associated_token::authority = payer, |
| #253 | )] |
| #254 | pub associated_token_account: Account<'info, TokenAccount>, |
| #255 | #[account( |
| #256 | mut, |
| #257 | seeds = [b"mint"], |
| #258 | bump |
| #259 | )] |
| #260 | pub mint_account: Account<'info, Mint>, |
| #261 | /// CHECK: Checked oracle id |
| #262 | #[account(address = solana_gpt_oracle::ID)] |
| #263 | pub oracle_program: AccountInfo<'info>, |
| #264 | pub token_program: Program<'info, Token>, |
| #265 | pub associated_token_program: Program<'info, AssociatedToken>, |
| #266 | pub system_program: Program<'info, System>, |
| #267 | } |
| #268 | |
| #269 | #[derive(Accounts)] |
| #270 | pub struct CallbackFromAgent<'info> { |
| #271 | /// CHECK: Checked in oracle program |
| #272 | pub identity: Account<'info, Identity>, |
| #273 | /// CHECK: The user wo did the interaction |
| #274 | pub user: AccountInfo<'info>, |
| #275 | #[account( |
| #276 | mut, |
| #277 | seeds = [b"mint"], |
| #278 | bump |
| #279 | )] |
| #280 | pub mint_account: Account<'info, Mint>, |
| #281 | #[account( |
| #282 | mut, |
| #283 | associated_token::mint = mint_account, |
| #284 | associated_token::authority = user, |
| #285 | )] |
| #286 | pub associated_token_account: Account<'info, TokenAccount>, |
| #287 | pub token_program: Program<'info, Token>, |
| #288 | pub system_program: Program<'info, System>, |
| #289 | } |
| #290 | |
| #291 | #[account] |
| #292 | pub struct Agent { |
| #293 | pub context: Pubkey, |
| #294 | } |
| #295 | |
| #296 | // ────────────────────────────────────────────────────────────────────────────── |
| #297 | // Skill Hub — on-chain registry for formally verified agents, skills, plugins |
| #298 | // ────────────────────────────────────────────────────────────────────────────── |
| #299 | |
| #300 | pub const SAS_PROGRAM_ID: &str = "22zoJMtdu4tQc2PzL74ZUT7FrwgB1Udec8DdW4yw4BdG"; |
| #301 | pub const MPL_AGENT_REGISTRY_PROGRAM_ID: &str = "Ag8004rWo8ao8AUKhLk78iv2nLQpZMyBPXiAh5QLbFiE"; |
| #302 | |
| #303 | /// Minimum STRIDE score for skill registration (0-100) |
| #304 | pub const MIN_STRIDE_SCORE_SKILL: u8 = 60; |
| #305 | /// Minimum STRIDE score for agent identity registration |
| #306 | pub const MIN_STRIDE_SCORE_AGENT: u8 = 70; |
| #307 | /// Maximum skills an agent identity can link |
| #308 | pub const MAX_LINKED_SKILLS: usize = 8; |
| #309 | |
| #310 | #[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, PartialEq, Eq)] |
| #311 | #[repr(u8)] |
| #312 | pub enum ComponentKind { |
| #313 | Agent = 0, |
| #314 | Skill = 1, |
| #315 | Plugin = 2, |
| #316 | McpServer = 3, |
| #317 | Program = 4, |
| #318 | } |
| #319 | |
| #320 | impl Default for ComponentKind { |
| #321 | fn default() -> Self { |
| #322 | ComponentKind::Skill |
| #323 | } |
| #324 | } |
| #325 | |
| #326 | // ── Account: SkillHub ────────────────────────────────────────────────────── |
| #327 | |
| #328 | /// Global registry state. PDA seeds: [b"skill_hub"] |
| #329 | #[account] |
| #330 | pub struct SkillHub { |
| #331 | /// Authority that can pause or update hub-level settings |
| #332 | pub authority: Pubkey, |
| #333 | pub skill_count: u64, |
| #334 | pub agent_count: u64, |
| #335 | pub verification_count: u64, |
| #336 | pub bump: u8, |
| #337 | } |
| #338 | |
| #339 | impl SkillHub { |
| #340 | pub const LEN: usize = 8 + 32 + 8 + 8 + 8 + 1; |
| #341 | } |
| #342 | |
| #343 | // ── Account: SkillRecord ────────────────────────────────────────────────── |
| #344 | |
| #345 | /// One registered skill. PDA seeds: [b"skill", skill_id] |
| #346 | #[account] |
| #347 | pub struct SkillRecord { |
| #348 | pub skill_id: [u8; 32], |
| #349 | pub name: [u8; 64], |
| #350 | pub kind: ComponentKind, |
| #351 | pub stride_score: u8, |
| #352 | pub kani_verified: bool, |
| #353 | /// SHA-256 of the SPEC.md / spec file for this skill |
| #354 | pub spec_hash: [u8; 32], |
| #355 | pub authority: Pubkey, |
| #356 | pub metadata_uri: [u8; 128], |
| #357 | pub registered_at: i64, |
| #358 | pub active: bool, |
| #359 | pub bump: u8, |
| #360 | } |
| #361 | |
| #362 | impl SkillRecord { |
| #363 | pub const LEN: usize = 8 + 32 + 64 + 1 + 1 + 1 + 32 + 32 + 128 + 8 + 1 + 1; |
| #364 | } |
| #365 | |
| #366 | // ── Account: AgentIdentity ──────────────────────────────────────────────── |
| #367 | |
| #368 | /// One registered agent. PDA seeds: [b"agent_id", authority] |
| #369 | #[account] |
| #370 | pub struct AgentIdentity { |
| #371 | pub authority: Pubkey, |
| #372 | /// MPL Core asset address minted via gasless flow |
| #373 | pub core_asset: Pubkey, |
| #374 | pub skill_count: u8, |
| #375 | /// Up to MAX_LINKED_SKILLS verified skill IDs |
| #376 | pub verified_skills: [[u8; 32]; 8], |
| #377 | pub attestation_count: u32, |
| #378 | pub registered_at: i64, |
| #379 | pub name: [u8; 64], |
| #380 | pub stride_score: u8, |
| #381 | pub kani_verified: bool, |
| #382 | pub bump: u8, |
| #383 | } |
| #384 | |
| #385 | impl AgentIdentity { |
| #386 | pub const LEN: usize = 8 + 32 + 32 + 1 + (32 * 8) + 4 + 8 + 64 + 1 + 1 + 1; |
| #387 | } |
| #388 | |
| #389 | // ── Account: VerificationRecord ─────────────────────────────────────────── |
| #390 | |
| #391 | /// Permanent on-chain proof record. PDA seeds: [b"verification", component_hash] |
| #392 | #[account] |
| #393 | pub struct VerificationRecord { |
| #394 | pub component_hash: [u8; 32], |
| #395 | pub kind: ComponentKind, |
| #396 | pub stride_score: u8, |
| #397 | pub kani_verified: bool, |
| #398 | /// SHA-256 of the SAS schema name ("clawd-component-verification-v1") |
| #399 | pub sas_schema_hash: [u8; 32], |
| #400 | pub verified_by: Pubkey, |
| #401 | pub verified_at: i64, |
| #402 | /// 0 = never expires |
| #403 | pub expires_at: i64, |
| #404 | pub bump: u8, |
| #405 | } |
| #406 | |
| #407 | impl VerificationRecord { |
| #408 | pub const LEN: usize = 8 + 32 + 1 + 1 + 1 + 32 + 32 + 8 + 8 + 1; |
| #409 | } |
| #410 | |
| #411 | // ── Skill Hub Instructions ───────────────────────────────────────────────── |
| #412 | |
| #413 | #[program] |
| #414 | pub mod skill_hub { |
| #415 | use super::*; |
| #416 | |
| #417 | /// One-time hub initialisation; caller becomes authority. |
| #418 | pub fn initialize_skill_hub(ctx: Context<InitializeSkillHub>) -> Result<()> { |
| #419 | let hub = &mut ctx.accounts.skill_hub; |
| #420 | hub.authority = ctx.accounts.authority.key(); |
| #421 | hub.skill_count = 0; |
| #422 | hub.agent_count = 0; |
| #423 | hub.verification_count = 0; |
| #424 | hub.bump = ctx.bumps.skill_hub; |
| #425 | |
| #426 | emit!(SkillHubInitialized { |
| #427 | authority: hub.authority, |
| #428 | }); |
| #429 | Ok(()) |
| #430 | } |
| #431 | |
| #432 | /// Register a formally-verified skill. stride_score ≥ 60 enforced. |
| #433 | pub fn register_skill( |
| #434 | ctx: Context<RegisterSkill>, |
| #435 | skill_id: [u8; 32], |
| #436 | name: [u8; 64], |
| #437 | kind: ComponentKind, |
| #438 | stride_score: u8, |
| #439 | kani_verified: bool, |
| #440 | spec_hash: [u8; 32], |
| #441 | metadata_uri: [u8; 128], |
| #442 | ) -> Result<()> { |
| #443 | require!( |
| #444 | stride_score >= MIN_STRIDE_SCORE_SKILL, |
| #445 | VerificationError::InsufficientStrideScore |
| #446 | ); |
| #447 | |
| #448 | let skill = &mut ctx.accounts.skill_record; |
| #449 | skill.skill_id = skill_id; |
| #450 | skill.name = name; |
| #451 | skill.kind = kind; |
| #452 | skill.stride_score = stride_score; |
| #453 | skill.kani_verified = kani_verified; |
| #454 | skill.spec_hash = spec_hash; |
| #455 | skill.authority = ctx.accounts.authority.key(); |
| #456 | skill.metadata_uri = metadata_uri; |
| #457 | skill.registered_at = Clock::get()?.unix_timestamp; |
| #458 | skill.active = true; |
| #459 | skill.bump = ctx.bumps.skill_record; |
| #460 | |
| #461 | let hub = &mut ctx.accounts.skill_hub; |
| #462 | hub.skill_count = hub.skill_count.saturating_add(1); |
| #463 | |
| #464 | emit!(SkillRegistered { |
| #465 | skill_id, |
| #466 | authority: skill.authority, |
| #467 | stride_score, |
| #468 | kani_verified, |
| #469 | }); |
| #470 | Ok(()) |
| #471 | } |
| #472 | |
| #473 | /// Register a formally-verified agent identity. stride_score ≥ 70 enforced. |
| #474 | pub fn register_agent_identity( |
| #475 | ctx: Context<RegisterAgentIdentity>, |
| #476 | name: [u8; 64], |
| #477 | core_asset: Pubkey, |
| #478 | stride_score: u8, |
| #479 | kani_verified: bool, |
| #480 | ) -> Result<()> { |
| #481 | require!( |
| #482 | stride_score >= MIN_STRIDE_SCORE_AGENT, |
| #483 | VerificationError::InsufficientStrideScore |
| #484 | ); |
| #485 | |
| #486 | let identity = &mut ctx.accounts.agent_identity; |
| #487 | identity.authority = ctx.accounts.authority.key(); |
| #488 | identity.core_asset = core_asset; |
| #489 | identity.skill_count = 0; |
| #490 | identity.verified_skills = [[0u8; 32]; 8]; |
| #491 | identity.attestation_count = 0; |
| #492 | identity.registered_at = Clock::get()?.unix_timestamp; |
| #493 | identity.name = name; |
| #494 | identity.stride_score = stride_score; |
| #495 | identity.kani_verified = kani_verified; |
| #496 | identity.bump = ctx.bumps.agent_identity; |
| #497 | |
| #498 | let hub = &mut ctx.accounts.skill_hub; |
| #499 | hub.agent_count = hub.agent_count.saturating_add(1); |
| #500 | |
| #501 | emit!(AgentIdentityRegistered { |
| #502 | authority: identity.authority, |
| #503 | core_asset, |
| #504 | stride_score, |
| #505 | }); |
| #506 | Ok(()) |
| #507 | } |
| #508 | |
| #509 | /// Record a permanent verification result for any component. |
| #510 | pub fn attest_verification( |
| #511 | ctx: Context<AttestVerification>, |
| #512 | component_hash: [u8; 32], |
| #513 | kind: ComponentKind, |
| #514 | stride_score: u8, |
| #515 | kani_verified: bool, |
| #516 | sas_schema_hash: [u8; 32], |
| #517 | expires_at: i64, |
| #518 | ) -> Result<()> { |
| #519 | require!( |
| #520 | stride_score >= MIN_STRIDE_SCORE_SKILL, |
| #521 | VerificationError::InsufficientStrideScore |
| #522 | ); |
| #523 | |
| #524 | let record = &mut ctx.accounts.verification_record; |
| #525 | record.component_hash = component_hash; |
| #526 | record.kind = kind; |
| #527 | record.stride_score = stride_score; |
| #528 | record.kani_verified = kani_verified; |
| #529 | record.sas_schema_hash = sas_schema_hash; |
| #530 | record.verified_by = ctx.accounts.verifier.key(); |
| #531 | record.verified_at = Clock::get()?.unix_timestamp; |
| #532 | record.expires_at = expires_at; |
| #533 | record.bump = ctx.bumps.verification_record; |
| #534 | |
| #535 | let hub = &mut ctx.accounts.skill_hub; |
| #536 | hub.verification_count = hub.verification_count.saturating_add(1); |
| #537 | |
| #538 | emit!(VerificationAttested { |
| #539 | component_hash, |
| #540 | stride_score, |
| #541 | kani_verified, |
| #542 | verified_by: record.verified_by, |
| #543 | }); |
| #544 | Ok(()) |
| #545 | } |
| #546 | |
| #547 | /// Link a registered skill to a registered agent identity. |
| #548 | pub fn link_skill_to_agent( |
| #549 | ctx: Context<LinkSkillToAgent>, |
| #550 | skill_id: [u8; 32], |
| #551 | ) -> Result<()> { |
| #552 | let skill = &ctx.accounts.skill_record; |
| #553 | require!(skill.active, VerificationError::SkillRevoked); |
| #554 | require!( |
| #555 | skill.stride_score >= MIN_STRIDE_SCORE_SKILL, |
| #556 | VerificationError::InsufficientStrideScore |
| #557 | ); |
| #558 | |
| #559 | let identity = &mut ctx.accounts.agent_identity; |
| #560 | require!( |
| #561 | (identity.skill_count as usize) < MAX_LINKED_SKILLS, |
| #562 | VerificationError::TooManyLinkedSkills |
| #563 | ); |
| #564 | require!( |
| #565 | identity.authority == ctx.accounts.authority.key(), |
| #566 | VerificationError::Unauthorized |
| #567 | ); |
| #568 | |
| #569 | identity.verified_skills[identity.skill_count as usize] = skill_id; |
| #570 | identity.skill_count = identity.skill_count.saturating_add(1); |
| #571 | |
| #572 | emit!(SkillLinked { |
| #573 | agent_authority: identity.authority, |
| #574 | skill_id, |
| #575 | }); |
| #576 | Ok(()) |
| #577 | } |
| #578 | |
| #579 | /// Revoke a skill (authority-gated). Leaves record but marks inactive. |
| #580 | pub fn revoke_skill(ctx: Context<RevokeSkill>) -> Result<()> { |
| #581 | require!( |
| #582 | ctx.accounts.skill_record.authority == ctx.accounts.authority.key() |
| #583 | || ctx.accounts.skill_hub.authority == ctx.accounts.authority.key(), |
| #584 | VerificationError::Unauthorized |
| #585 | ); |
| #586 | |
| #587 | let skill = &mut ctx.accounts.skill_record; |
| #588 | skill.active = false; |
| #589 | |
| #590 | emit!(SkillRevoked { |
| #591 | skill_id: skill.skill_id, |
| #592 | revoked_by: ctx.accounts.authority.key(), |
| #593 | }); |
| #594 | Ok(()) |
| #595 | } |
| #596 | } |
| #597 | |
| #598 | // ── Skill Hub Account Contexts ───────────────────────────────────────────── |
| #599 | |
| #600 | #[derive(Accounts)] |
| #601 | pub struct InitializeSkillHub<'info> { |
| #602 | #[account(mut)] |
| #603 | pub authority: Signer<'info>, |
| #604 | #[account( |
| #605 | init, |
| #606 | payer = authority, |
| #607 | space = SkillHub::LEN, |
| #608 | seeds = [b"skill_hub"], |
| #609 | bump |
| #610 | )] |
| #611 | pub skill_hub: Account<'info, SkillHub>, |
| #612 | pub system_program: Program<'info, System>, |
| #613 | } |
| #614 | |
| #615 | #[derive(Accounts)] |
| #616 | #[instruction(skill_id: [u8; 32])] |
| #617 | pub struct RegisterSkill<'info> { |
| #618 | #[account(mut)] |
| #619 | pub authority: Signer<'info>, |
| #620 | #[account( |
| #621 | mut, |
| #622 | seeds = [b"skill_hub"], |
| #623 | bump = skill_hub.bump |
| #624 | )] |
| #625 | pub skill_hub: Account<'info, SkillHub>, |
| #626 | #[account( |
| #627 | init, |
| #628 | payer = authority, |
| #629 | space = SkillRecord::LEN, |
| #630 | seeds = [b"skill", &skill_id], |
| #631 | bump |
| #632 | )] |
| #633 | pub skill_record: Account<'info, SkillRecord>, |
| #634 | pub system_program: Program<'info, System>, |
| #635 | } |
| #636 | |
| #637 | #[derive(Accounts)] |
| #638 | pub struct RegisterAgentIdentity<'info> { |
| #639 | #[account(mut)] |
| #640 | pub authority: Signer<'info>, |
| #641 | #[account( |
| #642 | mut, |
| #643 | seeds = [b"skill_hub"], |
| #644 | bump = skill_hub.bump |
| #645 | )] |
| #646 | pub skill_hub: Account<'info, SkillHub>, |
| #647 | #[account( |
| #648 | init, |
| #649 | payer = authority, |
| #650 | space = AgentIdentity::LEN, |
| #651 | seeds = [b"agent_id", authority.key().as_ref()], |
| #652 | bump |
| #653 | )] |
| #654 | pub agent_identity: Account<'info, AgentIdentity>, |
| #655 | pub system_program: Program<'info, System>, |
| #656 | } |
| #657 | |
| #658 | #[derive(Accounts)] |
| #659 | #[instruction(component_hash: [u8; 32])] |
| #660 | pub struct AttestVerification<'info> { |
| #661 | #[account(mut)] |
| #662 | pub verifier: Signer<'info>, |
| #663 | #[account( |
| #664 | mut, |
| #665 | seeds = [b"skill_hub"], |
| #666 | bump = skill_hub.bump |
| #667 | )] |
| #668 | pub skill_hub: Account<'info, SkillHub>, |
| #669 | #[account( |
| #670 | init, |
| #671 | payer = verifier, |
| #672 | space = VerificationRecord::LEN, |
| #673 | seeds = [b"verification", &component_hash], |
| #674 | bump |
| #675 | )] |
| #676 | pub verification_record: Account<'info, VerificationRecord>, |
| #677 | pub system_program: Program<'info, System>, |
| #678 | } |
| #679 | |
| #680 | #[derive(Accounts)] |
| #681 | #[instruction(skill_id: [u8; 32])] |
| #682 | pub struct LinkSkillToAgent<'info> { |
| #683 | #[account(mut)] |
| #684 | pub authority: Signer<'info>, |
| #685 | #[account( |
| #686 | seeds = [b"skill", &skill_id], |
| #687 | bump = skill_record.bump |
| #688 | )] |
| #689 | pub skill_record: Account<'info, SkillRecord>, |
| #690 | #[account( |
| #691 | mut, |
| #692 | seeds = [b"agent_id", authority.key().as_ref()], |
| #693 | bump = agent_identity.bump |
| #694 | )] |
| #695 | pub agent_identity: Account<'info, AgentIdentity>, |
| #696 | pub system_program: Program<'info, System>, |
| #697 | } |
| #698 | |
| #699 | #[derive(Accounts)] |
| #700 | pub struct RevokeSkill<'info> { |
| #701 | #[account(mut)] |
| #702 | pub authority: Signer<'info>, |
| #703 | #[account( |
| #704 | seeds = [b"skill_hub"], |
| #705 | bump = skill_hub.bump |
| #706 | )] |
| #707 | pub skill_hub: Account<'info, SkillHub>, |
| #708 | #[account( |
| #709 | mut, |
| #710 | seeds = [b"skill", &skill_record.skill_id], |
| #711 | bump = skill_record.bump |
| #712 | )] |
| #713 | pub skill_record: Account<'info, SkillRecord>, |
| #714 | pub system_program: Program<'info, System>, |
| #715 | } |
| #716 | |
| #717 | // ── Events ───────────────────────────────────────────────────────────────── |
| #718 | |
| #719 | #[event] |
| #720 | pub struct SkillHubInitialized { |
| #721 | pub authority: Pubkey, |
| #722 | } |
| #723 | |
| #724 | #[event] |
| #725 | pub struct SkillRegistered { |
| #726 | pub skill_id: [u8; 32], |
| #727 | pub authority: Pubkey, |
| #728 | pub stride_score: u8, |
| #729 | pub kani_verified: bool, |
| #730 | } |
| #731 | |
| #732 | #[event] |
| #733 | pub struct AgentIdentityRegistered { |
| #734 | pub authority: Pubkey, |
| #735 | pub core_asset: Pubkey, |
| #736 | pub stride_score: u8, |
| #737 | } |
| #738 | |
| #739 | #[event] |
| #740 | pub struct VerificationAttested { |
| #741 | pub component_hash: [u8; 32], |
| #742 | pub stride_score: u8, |
| #743 | pub kani_verified: bool, |
| #744 | pub verified_by: Pubkey, |
| #745 | } |
| #746 | |
| #747 | #[event] |
| #748 | pub struct SkillLinked { |
| #749 | pub agent_authority: Pubkey, |
| #750 | pub skill_id: [u8; 32], |
| #751 | } |
| #752 | |
| #753 | #[event] |
| #754 | pub struct SkillRevoked { |
| #755 | pub skill_id: [u8; 32], |
| #756 | pub revoked_by: Pubkey, |
| #757 | } |
| #758 | |
| #759 | // ── Error Codes ───────────────────────────────────────────────────────────── |
| #760 | |
| #761 | #[error_code] |
| #762 | pub enum VerificationError { |
| #763 | #[msg("STRIDE score below minimum threshold for registration")] |
| #764 | InsufficientStrideScore, |
| #765 | #[msg("Kani verification required for this component kind")] |
| #766 | KaniVerificationRequired, |
| #767 | #[msg("Caller is not the component authority or hub authority")] |
| #768 | Unauthorized, |
| #769 | #[msg("Skill has been revoked and cannot be linked")] |
| #770 | SkillRevoked, |
| #771 | #[msg("Agent has reached the maximum number of linked skills (8)")] |
| #772 | TooManyLinkedSkills, |
| #773 | #[msg("Verification record has expired")] |
| #774 | VerificationExpired, |
| #775 | } |
| #776 | |
| #777 | // ── Kani Harnesses (compiled only under --cfg kani) ─────────────────────── |
| #778 | |
| #779 | #[cfg(kani)] |
| #780 | mod kani_harnesses { |
| #781 | use super::*; |
| #782 | |
| #783 | #[kani::proof] |
| #784 | fn verify_stride_score_gate() { |
| #785 | let score: u8 = kani::any(); |
| #786 | // Skill registration must always fail below MIN_STRIDE_SCORE_SKILL |
| #787 | let allowed = score >= MIN_STRIDE_SCORE_SKILL; |
| #788 | if score < MIN_STRIDE_SCORE_SKILL { |
| #789 | kani::assert(!allowed, "STRIDE gate: low score must be rejected"); |
| #790 | } else { |
| #791 | kani::assert(allowed, "STRIDE gate: sufficient score must be accepted"); |
| #792 | } |
| #793 | } |
| #794 | |
| #795 | #[kani::proof] |
| #796 | fn verify_agent_stride_gate() { |
| #797 | let score: u8 = kani::any(); |
| #798 | let allowed = score >= MIN_STRIDE_SCORE_AGENT; |
| #799 | if score < MIN_STRIDE_SCORE_AGENT { |
| #800 | kani::assert(!allowed, "Agent STRIDE gate: low score must be rejected"); |
| #801 | } else { |
| #802 | kani::assert(allowed, "Agent STRIDE gate: sufficient score must be accepted"); |
| #803 | } |
| #804 | } |
| #805 | |
| #806 | #[kani::proof] |
| #807 | fn verify_skill_count_bounded() { |
| #808 | let mut count: u8 = kani::any(); |
| #809 | kani::assume(count < 255); |
| #810 | let new_count = count.saturating_add(1); |
| #811 | kani::assert( |
| #812 | new_count == count + 1 || count == 255, |
| #813 | "Skill count saturating add invariant violated", |
| #814 | ); |
| #815 | } |
| #816 | |
| #817 | #[kani::proof] |
| #818 | fn verify_max_linked_skills() { |
| #819 | let count: u8 = kani::any(); |
| #820 | let can_link = (count as usize) < MAX_LINKED_SKILLS; |
| #821 | if count as usize >= MAX_LINKED_SKILLS { |
| #822 | kani::assert(!can_link, "Must reject when at max linked skills"); |
| #823 | } |
| #824 | } |
| #825 | } |
| #826 |