ai-registry 0.1.0

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

/// 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_KNOWLEDGE_REFERENCE_LENGTH {
            return false;
        }
        if s.starts_with("bafy") {
            return s.chars().all(|c| matches!(c, 'a'..='z' | '2'..='7'));
        } else if s.starts_with("Qm") {
            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 configure knowledge instruction
pub fn process_configure_knowledge(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult {
    let args = ConfigureKnowledge::try_from_bytes(data)?;

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

    // Validate accounts
    agent_info.is_writable()?.has_seeds(
        &[
            AGENT,
            agent_info
                .as_account::<Agent>(&ai_registry_api::ID)?
                .creator
                .as_ref(),
        ],
        &ai_registry_api::ID,
    )?;
    controller_info.is_signer()?;

    // Load agent and verify controller and check if knowledge is finalized
    let agent = agent_info.as_account::<Agent>(&ai_registry_api::ID)?;
    if controller_info.key != &agent.controller {
        return Err(AgentError::UnauthorizedController.into());
    }

    if agent.state_flags & FLAG_KNOWLEDGE_FINALIZED != 0 {
        return Err(AgentError::KnowledgeFinalized.into());
    }

    // Validate knowledge reference based on type
    let ref_len = args
        .reference
        .iter()
        .position(|&b| b == 0)
        .unwrap_or(MAX_KNOWLEDGE_REFERENCE_LENGTH);
    let ref_bytes = &args.reference[..ref_len];
    let zero_hash = [0u8; 32];

    match args.reference_type {
        0 => {
            // None - must be empty and no hash
            if !ref_bytes.is_empty() || args.content_hash != zero_hash {
                return Err(AgentError::InvalidKnowledgeFields.into());
            }
        }
        1 => {
            // IPFS CID - no hash required
            if ref_bytes.is_empty() {
                return Err(AgentError::InvalidKnowledgeFields.into());
            }
            if !is_valid_cid(ref_bytes) {
                return Err(AgentError::InvalidKnowledgeFields.into());
            }
            if args.content_hash != zero_hash {
                return Err(AgentError::InvalidKnowledgeFields.into());
            }
        }
        2 => {
            // URL - must be https:// and require hash
            if ref_bytes.is_empty() {
                return Err(AgentError::InvalidKnowledgeFields.into());
            }
            let url_str =
                core::str::from_utf8(ref_bytes).map_err(|_| AgentError::InvalidKnowledgeFields)?;
            if !url_str.starts_with("https://") {
                return Err(AgentError::InsecureUri.into());
            }
            if args.content_hash == zero_hash {
                return Err(AgentError::InvalidKnowledgeFields.into());
            }
        }
        3 => {
            // IPNS - require hash
            if ref_bytes.is_empty() {
                return Err(AgentError::InvalidKnowledgeFields.into());
            }
            if args.content_hash == zero_hash {
                return Err(AgentError::InvalidKnowledgeFields.into());
            }
        }
        4 => {
            // Manifest - require hash
            if ref_bytes.is_empty() {
                return Err(AgentError::InvalidKnowledgeFields.into());
            }
            if args.content_hash == zero_hash {
                return Err(AgentError::InvalidKnowledgeFields.into());
            }
        }
        _ => return Err(AgentError::InvalidKnowledgeFields.into()),
    }

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

    agent.knowledge_reference_type = args.reference_type;
    if args.reference_type == 0 {
        agent.knowledge_reference = [0u8; 128];
        agent.knowledge_reference_len = 0;
        agent.knowledge_content_hash = zero_hash;
    } else {
        agent.knowledge_reference = args.reference;
        agent.knowledge_reference_len = ref_len as u8;
        agent.knowledge_content_hash = args.content_hash;
    }
    agent.updated_at = clock.unix_timestamp;

    // Create reference preview (first 32 bytes)
    let mut ref_preview = [0u8; 32];
    let preview_len = core::cmp::min(32, ref_len);
    if preview_len > 0 {
        ref_preview[..preview_len].copy_from_slice(&ref_bytes[..preview_len]);
    }

    // Emit event
    KnowledgeConfiguredEvent {
        creator: agent.creator,
        agent: *agent_info.key,
        reference_type: args.reference_type,
        reference_preview: ref_preview,
        content_hash: args.content_hash,
        timestamp: clock.unix_timestamp,
        _padding: [0; 7],
    }
    .log();

    Ok(())
}