ai-registry 0.1.0

AI Registry on Solana - On-chain agent identity and knowledge management
Documentation
use ai_registry_api::{
    consts::{AGENT, FLAG_ACTIVE, MAX_KNOWLEDGE_REFERENCE_LENGTH, MAX_URI_LENGTH},
    error::AgentError,
    event::AgentCreatedEvent,
    instruction::CreateAgent,
    state::Agent,
};
use steel::*;

// /// Helper function to write bytes to fixed-size array
// fn write_fixed<const N: usize>(dst: &mut [u8; N], src: &[u8]) {
//     let len = core::cmp::min(N, src.len());
//     dst[..len].copy_from_slice(&src[..len]);
//     if len < N {
//         for b in &mut dst[len..] {
//             *b = 0;
//         }
//     }
// }

/// Helper function to validate CID format
fn is_valid_cid(bytes: &[u8]) -> bool {
    if let Ok(s) = core::str::from_utf8(bytes) {
        if s.is_empty() || s.len() > MAX_URI_LENGTH {
            return false;
        }
        if s.starts_with("bafy") {
            // CIDv1 base32 lowercase
            return s.chars().all(|c| matches!(c, 'a'..='z' | '2'..='7'));
        } else if s.starts_with("Qm") {
            // CIDv0 base58btc, must be exactly 46 chars
            return s.len() == 46
                && s.chars().all(|c| {
                    matches!(
                        c,
                        '1'..='9'
                            | 'A'..='H'
                            | 'J'..='N'
                            | 'P'..='Z'
                            | 'a'..='k'
                            | 'm'..='z'
                    )
                });
        }
    }
    false
}

/// Process a create agent instruction
pub fn process_create_agent(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult {
    let args = CreateAgent::try_from_bytes(data)?;

    // Load accounts (signer first, following Solana best practices)
    let [creator_info, agent_info, system_program] = accounts else {
        return Err(ProgramError::NotEnoughAccountKeys);
    };

    // Validate accounts
    creator_info.is_signer()?;
    agent_info
        .is_empty()?
        .is_writable()?
        .has_seeds(&[AGENT, args.creator.as_ref()], &ai_registry_api::ID)?;
    system_program.is_program(&system_program::ID)?;

    // Validate creator matches signer
    if creator_info.key != &args.creator {
        return Err(AgentError::InvalidCreator.into());
    }

    // Validate identity profile URI
    let uri_len = args
        .identity_profile_uri
        .iter()
        .position(|&b| b == 0)
        .unwrap_or(MAX_URI_LENGTH);
    let uri_bytes = &args.identity_profile_uri[..uri_len];
    if uri_bytes.is_empty() || uri_bytes.len() > MAX_URI_LENGTH {
        return Err(AgentError::InvalidLength.into());
    }

    let uri_str =
        core::str::from_utf8(uri_bytes).map_err(|_| AgentError::InvalidIdentityProfileUri)?;

    // Only allow https:// or ipfs:// schemes
    if !uri_str.starts_with("https://") && !uri_str.starts_with("ipfs://") {
        return Err(AgentError::InsecureUri.into());
    }

    // Validate knowledge reference if provided
    if args.knowledge_reference_type != 0 {
        let ref_len = args
            .knowledge_reference
            .iter()
            .position(|&b| b == 0)
            .unwrap_or(MAX_KNOWLEDGE_REFERENCE_LENGTH);
        let ref_bytes = &args.knowledge_reference[..ref_len];

        if ref_bytes.is_empty() {
            return Err(AgentError::InvalidKnowledgeFields.into());
        }

        match args.knowledge_reference_type {
            1 => {
                // IPFS CID - no hash required
                if !is_valid_cid(ref_bytes) {
                    return Err(AgentError::InvalidKnowledgeFields.into());
                }
            }
            2 => {
                // URL - must be https:// and require hash
                let url_str = core::str::from_utf8(ref_bytes)
                    .map_err(|_| AgentError::InvalidKnowledgeFields)?;
                if !url_str.starts_with("https://") {
                    return Err(AgentError::InsecureUri.into());
                }
                // Hash must be non-zero for URL mode
                if args.knowledge_content_hash == [0u8; 32] {
                    return Err(AgentError::InvalidKnowledgeFields.into());
                }
            }
            3 => {
                // IPNS - require hash
                if args.knowledge_content_hash == [0u8; 32] {
                    return Err(AgentError::InvalidKnowledgeFields.into());
                }
            }
            4 => {
                // Manifest - require hash
                if args.knowledge_content_hash == [0u8; 32] {
                    return Err(AgentError::InvalidKnowledgeFields.into());
                }
            }
            _ => return Err(AgentError::InvalidKnowledgeFields.into()),
        }
    } else {
        // No knowledge - ensure everything is zero
        if args.knowledge_reference != [0u8; 128] || args.knowledge_content_hash != [0u8; 32] {
            return Err(AgentError::InvalidKnowledgeFields.into());
        }
    }

    // Derive PDA bump
    let (expected_agent_pda, bump) = ai_registry_api::state::agent_pda(args.creator);
    if agent_info.key != &expected_agent_pda {
        return Err(ProgramError::InvalidArgument);
    }

    // Create agent account
    create_program_account::<Agent>(
        agent_info,
        system_program,
        creator_info,
        &ai_registry_api::ID,
        &[AGENT, args.creator.as_ref()],
    )?;

    // Initialize agent data
    let clock = Clock::get()?;
    let agent = agent_info.as_account_mut::<Agent>(&ai_registry_api::ID)?;

    agent.version = 1; // Registry schema version
    agent.agent_version = args.agent_version;
    agent.created_at = clock.unix_timestamp;
    agent.updated_at = clock.unix_timestamp;
    agent.finalized_at = 0; // Not finalized yet

    agent.creator = args.creator;
    agent.controller = args.creator; // Initially controller = creator

    // Set identity profile
    agent.identity_profile_uri = args.identity_profile_uri;
    agent.identity_profile_uri_len = uri_len as u8;
    agent.identity_profile_hash = args.identity_profile_hash;

    // Set knowledge (if provided)
    agent.knowledge_reference_type = args.knowledge_reference_type;
    if args.knowledge_reference_type != 0 {
        let ref_len = args
            .knowledge_reference
            .iter()
            .position(|&b| b == 0)
            .unwrap_or(MAX_KNOWLEDGE_REFERENCE_LENGTH);
        agent.knowledge_reference = args.knowledge_reference;
        agent.knowledge_reference_len = ref_len as u8;
        agent.knowledge_content_hash = args.knowledge_content_hash;
    } else {
        agent.knowledge_reference = [0u8; 128];
        agent.knowledge_reference_len = 0;
        agent.knowledge_content_hash = [0u8; 32];
    }

    // Set flags: ACTIVE by default
    agent.state_flags = FLAG_ACTIVE;
    agent.bump = bump;
    agent.reserved = [0u8; 8];
    agent._padding = [0u8; 7];

    // Emit event
    AgentCreatedEvent {
        creator: args.creator,
        controller: args.creator,
        agent: *agent_info.key,
        timestamp: clock.unix_timestamp,
        agent_version: args.agent_version,
    }
    .log();

    Ok(())
}