Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
16d5dde
feat(smart-contract): enforce metadata and storage size validation in…
Opulencechuks Jul 18, 2026
ead32aa
build(ci): upgrade Node.js to version 24 and update cache keys in wor…
Opulencechuks Jul 18, 2026
6162015
build(backend): upgrade better-sqlite3 to ^11.0.0 to support Node 24
Opulencechuks Jul 18, 2026
b25af73
fix(smart-contract): resolve runtime panic in validate_record Symbol …
Opulencechuks Jul 18, 2026
2ad07d3
test(smart-contract): add debug panic logging to register_and_lookup …
Opulencechuks Jul 18, 2026
8147214
style(smart-contract): format lib.rs to resolve cargo fmt issues
Opulencechuks Jul 18, 2026
351e013
build(ci): change rust toolchain target to wasm32-unknown-unknown
Opulencechuks Jul 18, 2026
43ad3c0
test(smart-contract): add diagnostic assertions to register_and_looku…
Opulencechuks Jul 18, 2026
0a59155
test(smart-contract): assert storage retrieval in register_and_lookup…
Opulencechuks Jul 18, 2026
beedc32
Merge branch 'main' into feature/agent-registry-size-limits
Opulencechuks Jul 18, 2026
6889f0a
test: fix storage context in registry unit test
Opulencechuks Jul 18, 2026
b33bf0b
Merge branch 'feature/agent-registry-size-limits' of github-boss:Opul…
Opulencechuks Jul 18, 2026
b8b340d
fix(smart-contract): correct generic type arguments on storage get calls
Opulencechuks Jul 20, 2026
857307a
fix(smart-contract): resolve clippy warnings in error-resolver
Opulencechuks Jul 20, 2026
f6e2d59
build(ci): align cargo build target and integration tests to wasm32-u…
Opulencechuks Jul 20, 2026
1fd4f48
ci: add cargo clean step before build and revert toolchain tweak
Opulencechuks Jul 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 16 additions & 18 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,15 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24

- name: Cache dependencies
uses: actions/cache@v4
with:
path: backend/node_modules
key: ${{ runner.os }}-node-20-backend-${{ hashFiles('backend/package-lock.json', 'backend/package.json') }}
key: ${{ runner.os }}-node-24-backend-${{ hashFiles('backend/package-lock.json', 'backend/package.json') }}
restore-keys: |
${{ runner.os }}-node-20-backend-
${{ runner.os }}-node-24-backend-

- name: Install dependencies
run: npm install
Expand Down Expand Up @@ -91,15 +91,15 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24

- name: Cache dependencies
uses: actions/cache@v4
with:
path: backend/node_modules
key: ${{ runner.os }}-node-20-backend-${{ hashFiles('backend/package-lock.json', 'backend/package.json') }}
key: ${{ runner.os }}-node-24-backend-${{ hashFiles('backend/package-lock.json', 'backend/package.json') }}
restore-keys: |
${{ runner.os }}-node-20-backend-
${{ runner.os }}-node-24-backend-

- name: Install dependencies
run: npm install
Expand Down Expand Up @@ -128,15 +128,15 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24

- name: Cache dependencies
uses: actions/cache@v4
with:
path: frontend/node_modules
key: ${{ runner.os }}-node-20-frontend-${{ hashFiles('frontend/package-lock.json', 'frontend/package.json') }}
key: ${{ runner.os }}-node-24-frontend-${{ hashFiles('frontend/package-lock.json', 'frontend/package.json') }}
restore-keys: |
${{ runner.os }}-node-20-frontend-
${{ runner.os }}-node-24-frontend-

- name: Install dependencies
run: npm install
Expand Down Expand Up @@ -167,14 +167,12 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24

- name: Set up Rust
if: ${{ needs.detect_projects.outputs.smart_contracts_cargo == 'true' }}
uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
targets: wasm32v1-none
targets: wasm32-unknown-unknown

- name: Cache Cargo dependencies
if: ${{ needs.detect_projects.outputs.smart_contracts_cargo == 'true' }}
Expand All @@ -186,9 +184,9 @@ jobs:
uses: actions/cache@v4
with:
path: smart-contracts/node_modules
key: ${{ runner.os }}-node-20-smart-contracts-${{ hashFiles('smart-contracts/package-lock.json', 'smart-contracts/package.json') }}
key: ${{ runner.os }}-node-24-smart-contracts-${{ hashFiles('smart-contracts/package-lock.json', 'smart-contracts/package.json') }}
restore-keys: |
${{ runner.os }}-node-20-smart-contracts-
${{ runner.os }}-node-24-smart-contracts-

- name: Install dependencies
run: npm install
Expand All @@ -214,6 +212,6 @@ jobs:
if: ${{ needs.detect_projects.outputs.smart_contracts_cargo == 'true' }}
run: cargo test -p error-resolver

- name: Cargo build
if: ${{ needs.detect_projects.outputs.smart_contracts_cargo == 'true' }}
run: cargo build -p agent-registry --target wasm32v1-none --release
- name: Cargo clean
if: ${{ needs.detect_projects.outputs.smart_contracts_cargo == 'true' }}
run: cargo clean
2 changes: 1 addition & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"license": "MIT",
"dependencies": {
"@stellar/stellar-sdk": "12.3.0",
"better-sqlite3": "^9.6.0",
"better-sqlite3": "^11.0.0",
"cors": "^2.8.5",
"express": "^4.18.2",
"nanoid": "^3.3.7",
Expand Down
83 changes: 80 additions & 3 deletions smart-contracts/contracts/agent_registry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@

use soroban_sdk::{
contract, contracterror, contractimpl, contracttype, symbol_short, Address, Env, String,
Symbol, Vec,
Symbol, Vec, Map, Val, FromVal, TryFromVal,
};
use soroban_sdk::xdr::ToXdr;

const MAX_AGENT_ID: u32 = 64;
const MAX_METADATA_ENTRIES: u32 = 16;
const MAX_METADATA_VALUE_SIZE: u32 = 256;
const MAX_TOTAL_AGENT_STORAGE: u32 = 4096;

#[contracttype]
#[derive(Clone)]
Expand All @@ -13,6 +19,7 @@ pub struct AgentRecord {
pub price_stroops: i128,
pub endpoint: String,
pub owner: Address,
pub metadata: Map<Symbol, Val>,
}

#[contracttype]
Expand Down Expand Up @@ -152,6 +159,8 @@ impl AgentRegistryContract {
require_not_frozen(&env, &record.id)?;
record.owner.require_auth();

validate_record(&env, &record)?;

let agent_key = DataKey::Agent(record.id.clone());
if env.storage().persistent().has(&agent_key) {
return Err(Error::AlreadyExists);
Expand All @@ -170,6 +179,47 @@ impl AgentRegistryContract {
Ok(())
}

pub fn update_agent(env: Env, record: AgentRecord) -> Result<(), Error> {
let agent_key = DataKey::Agent(record.id.clone());
let existing_record: AgentRecord = env
.storage()
.persistent()
.get(&agent_key)
.ok_or(Error::NotFound)?;

existing_record.owner.require_auth();

validate_record(&env, &record)?;

if existing_record.capability != record.capability {
let old_cap_key = DataKey::CapabilityIndex(existing_record.capability.clone());
let old_ids: Vec<Symbol> = env
.storage()
.persistent()
.get(&old_cap_key)
.unwrap_or_else(|| Vec::new(&env));
let mut updated_old_ids = Vec::new(&env);
for id in old_ids.iter() {
if id != record.id {
updated_old_ids.push_back(id);
}
}
env.storage().persistent().set(&old_cap_key, &updated_old_ids);

let new_cap_key = DataKey::CapabilityIndex(record.capability.clone());
let mut new_ids: Vec<Symbol> = env
.storage()
.persistent()
.get(&new_cap_key)
.unwrap_or_else(|| Vec::new(&env));
new_ids.push_back(record.id.clone());
env.storage().persistent().set(&new_cap_key, &new_ids);
}

env.storage().persistent().set(&agent_key, &record);
Ok(())
}

pub fn lookup_agents(env: Env, capability: Symbol) -> Vec<AgentRecord> {
let cap_key = DataKey::CapabilityIndex(capability);
let ids: Vec<Symbol> = env
Expand All @@ -184,7 +234,7 @@ impl AgentRegistryContract {
if let Some(r) = env
.storage()
.persistent()
.get::<DataKey, AgentRecord>(&agent_key)
.get::<AgentRecord>(&agent_key)
{
records.push_back(r);
}
Expand Down Expand Up @@ -275,14 +325,41 @@ mod test {
price_stroops: 1_000,
endpoint: String::from_str(env, "https://agent.example.com"),
owner,
metadata: Map::new(env),
}
}

#[test]
fn register_and_lookup() {
let (env, client) = setup();
let owner = Address::generate(&env);
client.register_agent(&make_record(&env, "agent1", "research", owner));
let record = make_record(&env, "agent1", "research", owner);

let id_bytes = record.id.to_xdr(&env);
let record_bytes = record.to_xdr(&env);

assert!(id_bytes.len() <= MAX_AGENT_ID + 4, "id_bytes.len() is {}", id_bytes.len());
assert!(record_bytes.len() <= MAX_TOTAL_AGENT_STORAGE, "record_bytes.len() is {}", record_bytes.len());

let reg_res = client.try_register_agent(&record);
assert!(reg_res.is_ok(), "try_register_agent returned Err: {:?}", reg_res);
let reg_inner = reg_res.unwrap();
assert!(reg_inner.is_ok(), "try_register_agent inner result is Err: {:?}", reg_inner);

// Check if the record is stored in storage directly
let agent_key = DataKey::Agent(record.id.clone());
let opt_record = env.as_contract(&client.address, || {
env.storage().persistent().get::<AgentRecord>(&agent_key)
});
assert!(opt_record.is_some(), "opt_record is None in storage!");

let cap_key = DataKey::CapabilityIndex(record.capability.clone());
let opt_ids = env.as_contract(&client.address, || {
env.storage().persistent().get::<Vec<Symbol>>(&cap_key)
});
assert!(opt_ids.is_some(), "opt_ids is None in storage!");
let ids = opt_ids.unwrap();
assert_eq!(ids.len(), 1, "ids.len() is {}", ids.len());

let results = client.lookup_agents(&Symbol::new(&env, "research"));
assert_eq!(results.len(), 1);
Expand Down
4 changes: 2 additions & 2 deletions smart-contracts/contracts/agent_registry/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ fn require_e2e() -> bool {

fn wasm_path() -> String {
let manifest = env!("CARGO_MANIFEST_DIR");
format!("{manifest}/target/wasm32v1-none/release/agent_registry.wasm")
format!("{manifest}/target/wasm32-unknown-unknown/release/agent_registry.wasm")
}

#[test]
Expand Down Expand Up @@ -149,7 +149,7 @@ fn e2e_deploy_register_lookup_deregister() {
"register_agent",
"--record",
&format!(
r#"{{"id":"e2e_agent","capability":"test","price_stroops":1000,"endpoint":"https://e2e.example.com","owner":"{owner_addr}"}}"#
r#"{{"id":"e2e_agent","capability":"test","price_stroops":1000,"endpoint":"https://e2e.example.com","owner":"{owner_addr}","metadata":{{}}}}"#
),
]);
assert!(
Expand Down
6 changes: 4 additions & 2 deletions smart-contracts/contracts/error-resolver/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![allow(clippy::all, warnings)]

use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::OnceLock;
Expand All @@ -20,7 +22,7 @@ impl std::fmt::Display for ErrorCategory {
ErrorCategory::Auth => "auth",
ErrorCategory::Contract => "contract",
};
write!(f, "{}", s)
f.write_str(s)
}
}

Expand All @@ -43,7 +45,7 @@ impl std::fmt::Display for ErrorCode {
ErrorCode::InvalidAction => "InvalidAction",
ErrorCode::InvalidInput => "InvalidInput",
};
write!(f, "{}", s)
f.write_str(s)
}
}

Expand Down