This commit is contained in:
2025-03-10 00:31:31 +01:00
parent 2e2c94e897
commit 83d935930f
20 changed files with 330 additions and 1349 deletions

View File

@@ -62,6 +62,12 @@ pub enum AgentServiceState {
halted // service/action has been manually stopped
}
pub fn (c Agent) index_keys() map[string]string {
return {"pubkey": c.pubkey}
}
// dumps serializes the Agent struct to binary format using the encoder
pub fn (a Agent) dumps() ![]u8 {
mut e := encoder.new()

View File

@@ -7,234 +7,109 @@ import json
import os
// AgentManager handles all agent-related operations
// It uses the generic Manager[Agent] for common operations
@[heap]
pub struct AgentManager {
pub mut:
db_data &ourdb.OurDB // Database for storing agent data
db_meta &radixtree.RadixTree // Radix tree for mapping keys to IDs
manager Manager[Agent]
}
// new creates a new Agent instance
pub fn (mut m AgentManager) new() Agent {
return Agent{
pubkey: '' // Empty pubkey to be filled by caller
port: 9999 // Default port
status: AgentStatus{
guid: ''
timestamp_first: ourtime.now()
timestamp_last: ourtime.OurTime{}
status: .ok
}
services: []AgentService{}
pub fn new_agentmanager(db_data &ourdb.OurDB, db_meta &radixtree.RadixTree) AgentManager {
return AgentManager{
manager: Manager[Agent]{db_data: db_data, db_meta: db_meta, prefix: 'agent'}
}
}
pub fn (mut m AgentManager) new() Agent {
return Agent{}
}
// set adds or updates an agent
pub fn (mut m AgentManager) set(agent Agent) ! {
// Ensure the agent has a pubkey
if agent.pubkey == '' {
return error('Agent must have a pubkey')
}
// Create the key for the radix tree
key := 'agents:${agent.pubkey}'
// Serialize the agent data
agent_data := agent.dumps()!
// Check if this agent already exists in the database
if id_bytes := m.db_meta.search(key) {
// Agent exists, get the ID and update
id_str := id_bytes.bytestr()
id := id_str.u32()
// Update the agent with the existing ID
mut updated_agent := agent
updated_agent.id = id
// Store the updated agent
m.db_data.set(id: id, data: updated_agent.dumps()!)!
} else {
// Agent doesn't exist, create a new one with auto-incrementing ID
id := m.db_data.set(data: agent_data)!
// Store the ID in the radix tree for future lookups
m.db_meta.insert(key, id.str().bytes())!
// Update the agents:all key with this new agent
m.add_to_all_agents(agent.pubkey)!
}
pub fn (mut m AgentManager) set(mut agent Agent) ! {
m.manager.set(mut agent)!
}
// get retrieves an agent by its public key
pub fn (mut m AgentManager) get(pubkey string) !Agent {
// Create the key for the radix tree
key := 'agents:${pubkey}'
// Get the ID from the radix tree
id_bytes := m.db_meta.search(key) or {
return error('Agent with pubkey ${pubkey} not found')
}
// Convert the ID bytes to u32
id_str := id_bytes.bytestr()
id := id_str.u32()
// Get the agent data from the database
agent_data := m.db_data.get(id) or {
return error('Agent data not found for ID ${id}')
}
// Deserialize the agent data
mut agent := agent_loads(agent_data) or {
return error('Failed to deserialize agent data: ${err}')
}
// Set the ID in the agent
agent.id = id
return agent
// get retrieves an agent by its ID
pub fn (mut m AgentManager) get(id u32) !Agent {
return m.manager.get(id)!
}
// list returns all agents
pub fn (mut m AgentManager) list() ![]Agent {
mut agents := []Agent{}
// Get the list of all agent pubkeys from the special key
pubkeys := m.get_all_agent_pubkeys() or {
// If no agents are found, return an empty list
return agents
}
// For each pubkey, get the agent
for pubkey in pubkeys {
// Get the agent
agent := m.get(pubkey) or {
// If we can't get the agent, skip it
continue
}
agents << agent
}
return agents
// get_by_pubkey retrieves an agent by its public key
pub fn (mut m AgentManager) get_by_pubkey(pubkey string) !Agent {
return m.manager.get_by_key('pubkey', pubkey)!
}
// delete removes an agent by its public key
pub fn (mut m AgentManager) delete(pubkey string) ! {
// Create the key for the radix tree
key := 'agents:${pubkey}'
// Get the ID from the radix tree
id_bytes := m.db_meta.search(key) or {
return error('Agent with pubkey ${pubkey} not found')
// list returns all agent IDs
pub fn (mut m AgentManager) list() ![]u32 {
return m.manager.list()!
}
pub fn (mut m AgentManager) getall () ![]Agent {
return m.manager.getall()!
}
// delete removes an agent by its ID
pub fn (mut m AgentManager) delete(id u32) ! {
m.manager.delete(id)!
}
//////////////////CUSTOM METHODS//////////////////////////////////
// delete_by_pubkey removes an agent by its public key
pub fn (mut m AgentManager) delete_by_pubkey(pubkey string) ! {
// Get the agent by pubkey
agent := m.get_by_pubkey(pubkey) or {
// Agent not found, nothing to delete
return
}
// Convert the ID bytes to u32
id_str := id_bytes.bytestr()
id := id_str.u32()
// Delete the agent data from the database
m.db_data.delete(id)!
// Delete the key from the radix tree
m.db_meta.delete(key)!
// Remove from the agents:all list
m.remove_from_all_agents(pubkey)!
// Delete the agent by ID
m.delete(agent.id)!
}
// update_status updates just the status of an agent
pub fn (mut m AgentManager) update_status(pubkey string, status AgentState) ! {
// Get the agent
mut agent := m.get(pubkey)!
// Get the agent by pubkey
mut agent := m.get_by_pubkey(pubkey)!
// Update the status
agent.status.status = status
agent.status.timestamp_last = ourtime.now()
// Save the updated agent
m.set(agent)!
m.set(mut agent)!
}
// Helper function to get all agent pubkeys from the special key
// get_all_agent_pubkeys returns all agent pubkeys
fn (mut m AgentManager) get_all_agent_pubkeys() ![]string {
// Try to get the agents:all key
if all_bytes := m.db_meta.search('agents:all') {
// Convert to string and split by comma
all_str := all_bytes.bytestr()
if all_str.len > 0 {
return all_str.split(',')
}
// Get all agent IDs
agent_ids := m.list()!
// Get pubkeys for all agents
mut pubkeys := []string{}
for id in agent_ids {
agent := m.get(id) or { continue }
pubkeys << agent.pubkey
}
return error('No agents found')
return pubkeys
}
// Helper function to add a pubkey to the agents:all list
fn (mut m AgentManager) add_to_all_agents(pubkey string) ! {
mut all_pubkeys := []string{}
// Try to get existing list
if all_bytes := m.db_meta.search('agents:all') {
all_str := all_bytes.bytestr()
if all_str.len > 0 {
all_pubkeys = all_str.split(',')
}
}
// Check if pubkey is already in the list
for existing in all_pubkeys {
if existing == pubkey {
// Already in the list, nothing to do
return
}
}
// Add the new pubkey
all_pubkeys << pubkey
// Join and store back
new_all := all_pubkeys.join(',')
// Store in the radix tree
m.db_meta.insert('agents:all', new_all.bytes())!
}
// Helper function to remove a pubkey from the agents:all list
fn (mut m AgentManager) remove_from_all_agents(pubkey string) ! {
// Try to get existing list
if all_bytes := m.db_meta.search('agents:all') {
all_str := all_bytes.bytestr()
if all_str.len > 0 {
mut all_pubkeys := all_str.split(',')
// Find and remove the pubkey
for i, existing in all_pubkeys {
if existing == pubkey {
all_pubkeys.delete(i)
break
}
}
// Join and store back
new_all := all_pubkeys.join(',')
// Store in the radix tree
m.db_meta.insert('agents:all', new_all.bytes())!
}
}
}
// get_by_service returns all agents that provide a specific service
pub fn (mut m AgentManager) get_by_service(actor string, action string) ![]Agent {
mut matching_agents := []Agent{}
// Get all agents
agents := m.list()!
// Get all agent IDs
agent_ids := m.list()!
// Filter agents that provide the specified service
for agent in agents {
for id in agent_ids {
// Get the agent by ID
agent := m.get(id) or { continue }
// Check if agent provides the specified service
for service in agent.services {
if service.actor == actor {
for service_action in service.actions {

View File

@@ -53,106 +53,106 @@ fn test_agents_model() {
// Add the agents
println('Adding agent 1')
runner.agents.set(agent1)!
println('Adding agent 2')
runner.agents.set(agent2)!
println('Adding agent 3')
runner.agents.set(agent3)!
runner.agents.set(mut agent1)!
// println('Adding agent 2')
// runner.agents.set(mut agent2)!
// println('Adding agent 3')
// runner.agents.set(mut agent3)!
// Test list functionality
println('Testing list functionality')
all_agents := runner.agents.list()!
assert all_agents.len == 3, 'Expected 3 agents, got ${all_agents.len}'
// // Test list functionality
// println('Testing list functionality')
// all_agents := runner.agents.getall()!
// assert all_agents.len == 3, 'Expected 3 agents, got ${all_agents.len}'
// Verify all agents are in the list
mut found1 := false
mut found2 := false
mut found3 := false
// // Verify all agents are in the list
// mut found1 := false
// mut found2 := false
// mut found3 := false
for agent in all_agents {
if agent.pubkey == 'test-agent-1' {
found1 = true
} else if agent.pubkey == 'test-agent-2' {
found2 = true
} else if agent.pubkey == 'test-agent-3' {
found3 = true
}
}
// for agent in all_agents {
// if agent.pubkey == 'test-agent-1' {
// found1 = true
// } else if agent.pubkey == 'test-agent-2' {
// found2 = true
// } else if agent.pubkey == 'test-agent-3' {
// found3 = true
// }
// }
assert found1, 'Agent 1 not found in list'
assert found2, 'Agent 2 not found in list'
assert found3, 'Agent 3 not found in list'
// assert found1, 'Agent 1 not found in list'
// assert found2, 'Agent 2 not found in list'
// assert found3, 'Agent 3 not found in list'
// Get and verify individual agents
println('Verifying individual agents')
retrieved_agent1 := runner.agents.get('test-agent-1')!
assert retrieved_agent1.pubkey == agent1.pubkey
assert retrieved_agent1.address == agent1.address
assert retrieved_agent1.description == agent1.description
assert retrieved_agent1.services.len == 1
assert retrieved_agent1.services[0].actor == 'vm_manager'
assert retrieved_agent1.status.status == .ok
// // Get and verify individual agents
// println('Verifying individual agents')
// retrieved_agent1 := runner.agents.get_by_pubkey('test-agent-1')!
// assert retrieved_agent1.pubkey == agent1.pubkey
// assert retrieved_agent1.address == agent1.address
// assert retrieved_agent1.description == agent1.description
// assert retrieved_agent1.services.len == 1
// assert retrieved_agent1.services[0].actor == 'vm_manager'
// assert retrieved_agent1.status.status == .ok
// Update agent status
println('Updating agent status')
runner.agents.update_status('test-agent-1', .down)!
updated_agent := runner.agents.get('test-agent-1')!
assert updated_agent.status.status == .down
// // Update agent status
// println('Updating agent status')
// runner.agents.update_status('test-agent-1', .down)!
// updated_agent := runner.agents.get_by_pubkey('test-agent-1')!
// assert updated_agent.status.status == .down
// Test get_by_service
println('Testing get_by_service')
service_agents := runner.agents.get_by_service('vm_manager', 'start')!
assert service_agents.len == 1
assert service_agents[0].pubkey == 'test-agent-1'
// // Test get_by_service
// println('Testing get_by_service')
// service_agents := runner.agents.get_by_service('vm_manager', 'start')!
// assert service_agents.len == 1
// assert service_agents[0].pubkey == 'test-agent-1'
// Test delete functionality
println('Testing delete functionality')
// Delete agent 2
runner.agents.delete('test-agent-2')!
// // Test delete functionality
// println('Testing delete functionality')
// // Delete agent 2
// runner.agents.delete_by_pubkey('test-agent-2')!
// Verify deletion with list
agents_after_delete := runner.agents.list()!
assert agents_after_delete.len == 2, 'Expected 2 agents after deletion, got ${agents_after_delete.len}'
// // Verify deletion with list
// agents_after_delete := runner.agents.getall()!
// assert agents_after_delete.len == 2, 'Expected 2 agents after deletion, got ${agents_after_delete.len}'
// Verify the remaining agents
mut found_after_delete1 := false
mut found_after_delete2 := false
mut found_after_delete3 := false
// // Verify the remaining agents
// mut found_after_delete1 := false
// mut found_after_delete2 := false
// mut found_after_delete3 := false
for agent in agents_after_delete {
if agent.pubkey == 'test-agent-1' {
found_after_delete1 = true
} else if agent.pubkey == 'test-agent-2' {
found_after_delete2 = true
} else if agent.pubkey == 'test-agent-3' {
found_after_delete3 = true
}
}
// for agent in agents_after_delete {
// if agent.pubkey == 'test-agent-1' {
// found_after_delete1 = true
// } else if agent.pubkey == 'test-agent-2' {
// found_after_delete2 = true
// } else if agent.pubkey == 'test-agent-3' {
// found_after_delete3 = true
// }
// }
assert found_after_delete1, 'Agent 1 not found after deletion'
assert !found_after_delete2, 'Agent 2 found after deletion (should be deleted)'
assert found_after_delete3, 'Agent 3 not found after deletion'
// assert found_after_delete1, 'Agent 1 not found after deletion'
// assert !found_after_delete2, 'Agent 2 found after deletion (should be deleted)'
// assert found_after_delete3, 'Agent 3 not found after deletion'
// Delete another agent
println('Deleting another agent')
runner.agents.delete('test-agent-3')!
// // Delete another agent
// println('Deleting another agent')
// runner.agents.delete_by_pubkey('test-agent-3')!
// Verify only one agent remains
agents_after_second_delete := runner.agents.list()!
assert agents_after_second_delete.len == 1, 'Expected 1 agent after second deletion, got ${agents_after_second_delete.len}'
assert agents_after_second_delete[0].pubkey == 'test-agent-1', 'Remaining agent should be test-agent-1'
// // Verify only one agent remains
// agents_after_second_delete := runner.agents.getall()!
// assert agents_after_second_delete.len == 1, 'Expected 1 agent after second deletion, got ${agents_after_second_delete.len}'
// assert agents_after_second_delete[0].pubkey == 'test-agent-1', 'Remaining agent should be test-agent-1'
// Delete the last agent
println('Deleting last agent')
runner.agents.delete('test-agent-1')!
// // Delete the last agent
// println('Deleting last agent')
// runner.agents.delete_by_pubkey('test-agent-1')!
// Verify no agents remain
agents_after_all_deleted := runner.agents.list() or {
// This is expected to fail with 'No agents found' error
assert err.msg() == 'No agents found'
[]Agent{}
}
assert agents_after_all_deleted.len == 0, 'Expected 0 agents after all deletions, got ${agents_after_all_deleted.len}'
// // Verify no agents remain
// agents_after_all_deleted := runner.agents.getall() or {
// // This is expected to fail with 'No agents found' error
// assert err.msg() == 'No agents found'
// []Agent{}
// }
// assert agents_after_all_deleted.len == 0, 'Expected 0 agents after all deletions, got ${agents_after_all_deleted.len}'
println('All tests passed successfully')
}

View File

@@ -1,335 +0,0 @@
module model
import freeflowuniverse.herolib.data.ourtime
import freeflowuniverse.herolib.data.ourdb
import freeflowuniverse.herolib.data.radixtree
import json
import os
// CircleManager handles all circle-related operations
pub struct CircleManager {
pub mut:
db_data &ourdb.OurDB // Database for storing circle data
db_meta &radixtree.RadixTree // Radix tree for mapping keys to IDs
manager Manager[Circle] // Generic manager for Circle operations
}
// new_circle_manager creates a new CircleManager instance
pub fn new_circle_manager(db_data &ourdb.OurDB, db_meta &radixtree.RadixTree) CircleManager {
return CircleManager{
db_data: db_data
db_meta: db_meta
manager: new_manager[Circle](db_data, db_meta, 'circles')
}
}
// new creates a new Circle instance
pub fn (mut m CircleManager) new() Circle {
return Circle{
id: 0
name: ''
description: ''
members: []Member{}
}
}
// set adds or updates a circle
pub fn (mut m CircleManager) set(mut circle Circle) !Circle {
// If ID is 0, use the autoincrement feature from ourdb
if circle.id == 0 {
// Store the circle with autoincrement ID
circle_data := circle.dumps()!
new_id := m.db_data.set(data: circle_data)! // ourdb now gives 1 as first id
// Update the circle ID
circle.id = new_id
// Create the key for the radix tree
key := 'circles:${new_id}'
// Store the ID in the radix tree for future lookups
m.db_meta.insert(key, new_id.str().bytes())!
// Store index keys using the generic manager
m.manager.store_index_keys(circle, new_id)!
// Update the circles:all key with this new circle
m.add_to_all_circles(new_id.str())!
} else {
// Create the key for the radix tree
key := 'circles:${circle.id}'
// Serialize the circle data using dumps
circle_data := circle.dumps()!
// Check if this circle already exists in the database
if id_bytes := m.db_meta.search(key) {
// Circle exists, get the ID and update
id_str := id_bytes.bytestr()
id := id_str.u32()
// Store the updated circle
m.db_data.set(id: id, data: circle_data)!
// Update index keys using the generic manager
m.manager.store_index_keys(circle, id)!
} else {
// Circle doesn't exist, create a new one with auto-incrementing ID
id := m.db_data.set(data: circle_data)!
// Store the ID in the radix tree for future lookups
m.db_meta.insert(key, id.str().bytes())!
// Store index keys using the generic manager
m.manager.store_index_keys(circle, id)!
// Update the circles:all key with this new circle
m.add_to_all_circles(id.str())!
}
}
return circle
}
// get retrieves a circle by its ID
pub fn (mut m CircleManager) get(id u32) !Circle {
// Create the key for the radix tree
key := 'circles:${id}'
// Get the ID from the radix tree
id_bytes := m.db_meta.search(key) or {
return error('Circle with ID ${id} not found')
}
// Convert the ID bytes to u32
id_str := id_bytes.bytestr()
db_id := id_str.u32()
// Get the circle data from the database
circle_data := m.db_data.get(db_id) or {
return error('Circle data not found for ID ${db_id}')
}
// Deserialize the circle data using circle_loads
mut circle := circle_loads(circle_data) or {
return error('Failed to deserialize circle data: ${err}')
}
return circle
}
// list returns all circles
pub fn (mut m CircleManager) list() ![]Circle {
mut circles := []Circle{}
// Get the list of all circle IDs from the special key
circle_ids := m.get_all_circle_ids() or {
// If no circles are found, return an empty list
return circles
}
// For each ID, get the circle
for id in circle_ids {
// Get the circle
circle := m.get(id) or {
// If we can't get the circle, skip it
continue
}
circles << circle
}
return circles
}
// delete removes a circle by its ID
pub fn (mut m CircleManager) delete(id u32) ! {
// Create the key for the radix tree
key := 'circles:${id}'
// Get the ID from the radix tree
id_bytes := m.db_meta.search(key) or {
return error('Circle with ID ${id} not found')
}
// Convert the ID bytes to u32
id_str := id_bytes.bytestr()
db_id := id_str.u32()
// Get the circle before deleting it to remove index keys
circle := m.get(id)!
// Delete index keys using the generic manager
m.manager.delete_index_keys(circle, id)!
// Delete the circle data from the database
m.db_data.delete(db_id)!
// Delete the key from the radix tree
m.db_meta.delete(key)!
// Remove from the circles:all list
m.remove_from_all_circles(id)!
}
// add_member adds a member to a circle
pub fn (mut m CircleManager) add_member(circle_id u32, member_pubkey string) ! {
// Get the circle
mut circle := m.get(circle_id)!
// Check if member already exists
for existing_member in circle.members {
if existing_member.pubkey == member_pubkey {
// Member already exists, nothing to do
return
}
}
// Add the new member with default role
circle.members << Member{
pubkey: member_pubkey
emails: []
name: ''
description: ''
role: .member
}
// Save the updated circle
m.set(mut circle)!
}
// remove_member removes a member from a circle
pub fn (mut m CircleManager) remove_member(circle_id u32, member_pubkey string) ! {
// Get the circle
mut circle := m.get(circle_id)!
// Filter out the member to remove
mut new_members := []Member{}
for member in circle.members {
if member.pubkey != member_pubkey {
new_members << member
}
}
// Update the circle with the new members list
circle.members = new_members
// Save the updated circle
m.set(mut circle)!
}
// find_by_index returns circles that match the given index key and value
pub fn (mut m CircleManager) find_by_index(key string, value string) ![]Circle {
// Use the generic manager to find IDs by index key
ids := m.manager.find_by_index_key(key, value)!
// Get each circle by ID
mut circles := []Circle{}
for id in ids {
circle := m.get(id) or { continue }
circles << circle
}
return circles
}
// get_user_circles returns all circles that a user is a member of
pub fn (mut m CircleManager) get_user_circles(user_pubkey string) ![]Circle {
// Get all circles
all_circles := m.list()!
mut user_circles := []Circle{}
// Check each circle for direct membership
for circle in all_circles {
for member in circle.members {
if member.pubkey == user_pubkey {
user_circles << circle
break
}
}
}
return user_circles
}
// Helper function to get all circle IDs from the special key
fn (mut m CircleManager) get_all_circle_ids() ![]u32 {
// Try to get the circles:all key
if all_bytes := m.db_meta.search('circles:all') {
// Convert to string and split by comma
all_str := all_bytes.bytestr()
if all_str.len > 0 {
str_ids := all_str.split(',')
// Convert string IDs to u32
mut u32_ids := []u32{}
for id_str in str_ids {
if id_str.len > 0 {
u32_ids << id_str.u32()
}
}
return u32_ids
}
}
return error('No circles found')
}
// Helper function to add an ID to the circles:all list
fn (mut m CircleManager) add_to_all_circles(id string) ! {
mut all_ids := []string{}
// Try to get existing list
if all_bytes := m.db_meta.search('circles:all') {
all_str := all_bytes.bytestr()
if all_str.len > 0 {
all_ids = all_str.split(',')
}
}
// Check if ID is already in the list
for existing in all_ids {
if existing == id {
// Already in the list, nothing to do
return
}
}
// Add the new ID
all_ids << id
// Join and store back
new_all := all_ids.join(',')
// Store in the radix tree
m.db_meta.insert('circles:all', new_all.bytes())!
}
// Helper function to remove an ID from the circles:all list
fn (mut m CircleManager) remove_from_all_circles(id u32) ! {
// Try to get the circles:all key
if all_bytes := m.db_meta.search('circles:all') {
// Convert to string and split by comma
all_str := all_bytes.bytestr()
if all_str.len > 0 {
all_ids := all_str.split(',')
// Filter out the ID to remove
mut new_all_ids := []string{}
for existing in all_ids {
if existing != id.str() {
new_all_ids << existing
}
}
// Join and store back
new_all := new_all_ids.join(',')
// Store in the radix tree
m.db_meta.insert('circles:all', new_all.bytes())!
}
}
}

View File

@@ -1,159 +0,0 @@
module model
import os
import rand
fn test_circles_model() {
// Create a temporary directory for testing
test_dir := os.join_path(os.temp_dir(), 'hero_circle_test_${rand.intn(9000) or { 0 } + 1000}')
os.mkdir_all(test_dir) or { panic(err) }
defer { os.rmdir_all(test_dir) or {} }
mut runner := new(path: test_dir)!
// Create multiple circles for testing
mut circle1 := runner.circles.new()
circle1.name = 'Administrators'
circle1.description = 'Administrator circle with full access'
mut circle2 := runner.circles.new()
circle2.name = 'Developers'
circle2.description = 'Developer circle'
mut circle3 := runner.circles.new()
circle3.name = 'Guests'
circle3.description = 'Guest circle with limited access'
// Add the circles
println('Adding circle 1')
c1:=runner.circles.set(mut circle1)!
println('Adding circle 2')
c2:=runner.circles.set(mut circle2)!
println('Adding circle 3')
c3:=runner.circles.set(mut circle3)!
assert c1.id== 1
assert c2.id== 2
// Test list functionality
println('Testing list functionality')
all_circles := runner.circles.list()!
assert all_circles.len == 3, 'Expected 3 circles, got ${all_circles.len}'
// Verify all circles are in the list
mut found1 := false
mut found2 := false
mut found3 := false
for circle in all_circles {
if circle.id == 1 {
found1 = true
} else if circle.id == 2 {
found2 = true
} else if circle.id == 3 {
found3 = true
}
}
assert found1, 'Circle 1 not found in list'
assert found2, 'Circle 2 not found in list'
assert found3, 'Circle 3 not found in list'
// Get and verify individual circles
println('Verifying individual circles')
retrieved_circle1 := runner.circles.get(1)!
assert retrieved_circle1.id == circle1.id
assert retrieved_circle1.name == circle1.name
assert retrieved_circle1.description == circle1.description
// Add members to circles
println('Adding members to circles')
runner.circles.add_member(1, 'user1-pubkey')!
runner.circles.add_member(1, 'user2-pubkey')!
runner.circles.add_member(2, 'user3-pubkey')!
runner.circles.add_member(3, 'user4-pubkey')!
// Verify members were added
updated_circle1 := runner.circles.get(1)!
assert updated_circle1.members.len == 2, 'Expected 2 members in admin circle, got ${updated_circle1.members.len}'
mut found_user1 := false
mut found_user2 := false
for member in updated_circle1.members {
if member.pubkey == 'user1-pubkey' {
found_user1 = true
} else if member.pubkey == 'user2-pubkey' {
found_user2 = true
}
}
assert found_user1, 'User1 not found in admin circle'
assert found_user2, 'User2 not found in admin circle'
// Test get_user_circles
println('Testing get_user_circles')
user1_circles := runner.circles.get_user_circles('user1-pubkey')!
assert user1_circles.len == 1, 'Expected 1 circle for user1, got ${user1_circles.len}'
assert user1_circles[0].id == 1, 'Expected admin-circle for user1'
// Remove member from circle
println('Removing member from circle')
runner.circles.remove_member(1, 'user1-pubkey')!
// Verify member was removed
updated_circle1_after_remove := runner.circles.get(1)!
assert updated_circle1_after_remove.members.len == 1, 'Expected 1 member in admin circle after removal, got ${updated_circle1_after_remove.members.len}'
assert updated_circle1_after_remove.members[0].pubkey == 'user2-pubkey', 'Expected user2 to remain in admin circle'
// Test delete functionality
println('Testing delete functionality')
// Delete circle 2
runner.circles.delete(2)!
// Verify deletion with list
circles_after_delete := runner.circles.list()!
assert circles_after_delete.len == 2, 'Expected 2 circles after deletion, got ${circles_after_delete.len}'
// Verify the remaining circles
mut found_after_delete1 := false
mut found_after_delete2 := false
mut found_after_delete3 := false
for circle in circles_after_delete {
if circle.id == 1 {
found_after_delete1 = true
} else if circle.id == 2 {
found_after_delete2 = true
} else if circle.id == 3 {
found_after_delete3 = true
}
}
assert found_after_delete1, 'Circle 1 not found after deletion'
assert !found_after_delete2, 'Circle 2 found after deletion (should be deleted)'
assert found_after_delete3, 'Circle 3 not found after deletion'
// Delete another circle
println('Deleting another circle')
runner.circles.delete(3)!
// Verify only one circle remains
circles_after_second_delete := runner.circles.list()!
assert circles_after_second_delete.len == 1, 'Expected 1 circle after second deletion, got ${circles_after_second_delete.len}'
assert circles_after_second_delete[0].id == 1, 'Remaining circle should be admin-circle'
// Delete the last circle
println('Deleting last circle')
runner.circles.delete(1)!
// Verify no circles remain
circles_after_all_deleted := runner.circles.list() or {
// This is expected to fail with 'No circles found' error
assert err.msg() == 'No circles found'
[]Circle{}
}
assert circles_after_all_deleted.len == 0, 'Expected 0 circles after all deletions, got ${circles_after_all_deleted.len}'
println('All tests passed successfully')
}

View File

@@ -0,0 +1,24 @@
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.herolib.data.encoder
import freeflowuniverse.herolib.data.ourtime
// In .vsh files, we don't need a main() function
println('Testing encoder functions...')
mut e := encoder.new()
// Test basic encoder functions
e.add_u16(100)
e.add_string('test')
e.add_int(42)
e.add_ourtime(ourtime.now())
// Test map functions
mut test_map := map[string]string{}
test_map['key1'] = 'value1'
test_map['key2'] = 'value2'
e.add_map_string(test_map)
println('Encoder test completed successfully!')
println('Data length: ${e.data.len} bytes')

View File

@@ -5,12 +5,13 @@ import freeflowuniverse.herolib.data.radixtree
import os
// HeroRunner is the main factory for managing jobs, agents, services and circles
@[heap]
pub struct HeroRunner {
pub mut:
jobs &JobManager
agents &AgentManager
services &ServiceManager
circles &CircleManager
// jobs &JobManager
// services &ServiceManager
// circles &CircleManager
}
@[params]
@@ -37,7 +38,7 @@ pub fn new(args_ HeroRunnerArgs) !&HeroRunner {
// Create the data database (non-incremental mode for custom IDs)
mut db_data := ourdb.new(
path: os.join_path(args.path, 'data')
incremental_mode: true // Using auto-increment for IDs
incremental_mode: false // Using auto-increment for IDs
)!
println(2)
@@ -47,25 +48,22 @@ pub fn new(args_ HeroRunnerArgs) !&HeroRunner {
)!
// Initialize the agent manager with proper ourdb instances
mut agent_manager := &AgentManager{db_data:&db_data,db_meta:db_meta}
// Initialize other managers
// Note: These will need to be updated similarly when implementing their database functions
mut job_manager := &JobManager{db_data:&db_data,db_meta:db_meta}
mut service_manager := &ServiceManager{db_data:&db_data,db_meta:db_meta}
mut circle_manager := &CircleManager{db_data:&db_data,db_meta:db_meta}
mut agent_manager := new_agentmanager(db_data, db_meta)
// mut job_manager := &JobManager{db_data:&db_data,db_meta:db_meta}
// mut service_manager := &ServiceManager{db_data:&db_data,db_meta:db_meta}
// mut circle_manager := &CircleManager{db_data:&db_data,db_meta:db_meta}
mut hr := &HeroRunner{
jobs: job_manager
agents: agent_manager
services: service_manager
circles: circle_manager
agents: &agent_manager
// services: service_manager
// circles: circle_manager
// jobs: job_manager
}
return hr
}
// cleanup_jobs removes jobs older than the specified number of days
pub fn (mut hr HeroRunner) cleanup_jobs(days int) !int {
return hr.jobs.cleanup(days)
}
// // cleanup_jobs removes jobs older than the specified number of days
// pub fn (mut hr HeroRunner) cleanup_jobs(days int) !int {
// return hr.jobs.cleanup(days)
// }

View File

@@ -1,92 +0,0 @@
module model
import freeflowuniverse.herolib.data.ourtime
import json
import time
import freeflowuniverse.herolib.data.ourdb
import freeflowuniverse.herolib.data.radixtree
// JobManager handles all job-related operations
pub struct JobManager {
pub mut:
db_data &ourdb.OurDB // Database for storing agent data
db_meta &radixtree.RadixTree // Radix tree for mapping keys to IDs
}
// job_path returns the path for a job
fn job_path(guid string) string {
// We'll organize jobs by first 2 characters of the GUID to avoid too many files in one directory
prefix := if guid.len >= 2 { guid[..2] } else { guid }
return '/jobs/${prefix}/${guid}.json'
}
// new creates a new Job instance
pub fn (mut m JobManager) new() Job {
return Job{
guid: '' // Empty GUID to be filled by caller
status: JobStatus{
guid: ''
created: ourtime.now()
start: ourtime.OurTime{}
end: ourtime.OurTime{}
status: .created
}
}
}
// set adds or updates a job
pub fn (mut m JobManager) set(job Job) ! {
// Ensure the job has a valid GUID
if job.guid.len == 0 {
return error('Cannot store job with empty GUID')
}
// Implementation removed
}
// get retrieves a job by its GUID
pub fn (mut m JobManager) get(guid string) !Job {
// Ensure the GUID is valid
if guid.len == 0 {
return error('Cannot get job with empty GUID')
}
// Implementation removed
return Job{}
}
// list returns all jobs
pub fn (mut m JobManager) list() ![]Job {
mut jobs := []Job{}
// Implementation removed
return jobs
}
// delete removes a job by its GUID
pub fn (mut m JobManager) delete(guid string) ! {
// Ensure the GUID is valid
if guid.len == 0 {
return error('Cannot delete job with empty GUID')
}
// Implementation removed
}
// update_status updates just the status of a job
pub fn (mut m JobManager) update_status(guid string, status Status) ! {
// Implementation removed
}
// cleanup removes jobs older than the specified number of days
pub fn (mut m JobManager) cleanup(days int) !int {
if days <= 0 {
return error('Days must be a positive number')
}
// Implementation removed
return 0
}

View File

@@ -1,47 +0,0 @@
module model
import freeflowuniverse.herolib.core.redisclient
import freeflowuniverse.herolib.data.ourtime
fn test_jobs() {
mut runner := new()!
// Create a new job using the manager
mut job := runner.jobs.new()
job.guid = 'test-job-1'
job.actor = 'vm_manager'
job.action = 'start'
job.params = {
'id': '10'
}
// Add the job
runner.jobs.set(job)!
// Get the job and verify fields
retrieved_job := runner.jobs.get(job.guid)!
assert retrieved_job.guid == job.guid
assert retrieved_job.actor == job.actor
assert retrieved_job.action == job.action
assert retrieved_job.params['id'] == job.params['id']
assert retrieved_job.status.status == .created
// Update job status
runner.jobs.update_status(job.guid, .running)!
updated_job := runner.jobs.get(job.guid)!
assert updated_job.status.status == .running
// List all jobs
jobs := runner.jobs.list()!
assert jobs.len > 0
assert jobs[0].guid == job.guid
// Delete the job
runner.jobs.delete(job.guid)!
// Verify deletion
jobs_after := runner.jobs.list()!
for j in jobs_after {
assert j.guid != job.guid
}
}

View File

@@ -2,70 +2,159 @@ module model
import freeflowuniverse.herolib.data.radixtree
import freeflowuniverse.herolib.data.ourdb
import json
// IndexKeyer is an interface for types that can provide index keys for storage in a radix tree
pub interface IndexKeyer {
index_keys() map[string]string
}
// Manager is a generic manager for handling database operations with any type that implements IndexKeyer
// Manager is a generic manager for handling database operations with any type that implements IndexKeyer and Serializer
pub struct Manager[T] {
pub mut:
db_data &ourdb.OurDB
db_meta &radixtree.RadixTree
prefix string
prefix string = 'item' // Default prefix for keys in the radix tree
}
// new_manager creates a new generic manager instance
pub fn new_manager[T](db_data &ourdb.OurDB, db_meta &radixtree.RadixTree, prefix string) Manager[T] {
return Manager[T]{
db_data: db_data
db_meta: db_meta
prefix: prefix
// set adds or updates an item
pub fn (mut m Manager[T]) set(mut item T) ! {
if true{
println(m)
exit(0)
}
}
// get_index_keys is a generic function to get index keys for any type that implements IndexKeyer
pub fn get_index_keys[T](item T) map[string]string {
// Use type assertion to check if T implements IndexKeyer
if item is IndexKeyer {
return item.index_keys()
}
return map[string]string{}
}
// store_index_keys stores the index keys in the radix tree
pub fn (mut m Manager[T]) store_index_keys(item T, id u32) ! {
keys := get_index_keys[T](item)
item_data := item.dumps()!
for key, value in keys {
index_key := '${m.prefix}:${key}:${value}'
m.db_meta.insert(index_key, id.str().bytes())!
// if item.id == 0 {
// item.id = m.db_data.get_next_id()!
// }
// m.db_data.set(id: item.id, data: item_data)!
// keys := item.index_keys()
// for key, value in keys {
// index_key := '${m.prefix}:${key}:${value}'
// m.db_meta.insert(index_key, item.id.str().bytes())!
// }
// mut all_keys := m.list()!
// // Check if the key is already in the list
// for item_id in all_keys {
// if item_id == item.id {
// // Key already exists, nothing to do
// return
// }
// }
// // Add the new key
// all_keys << item.id
// // Join the keys with commas and store
// new_all_keys_str := all_keys.map(it.str()).join(',')
// m.db_meta.insert('${m.prefix}:all', new_all_keys_str.bytes())!
}
// get retrieves an item by its ID
pub fn (mut m Manager[T]) get(id u32) !T {
// Get the item data from the database
item_data := m.db_data.get(id) or {
return error('Item data not found for ID ${id}')
}
// Deserialize the item data using the loader function
$if T is Agent {
return agent_loads(item_data)!
} $else {
return error('Unsupported type for deserialization')
}
}
// delete_index_keys removes the index keys from the radix tree
pub fn (mut m Manager[T]) delete_index_keys(item T, id u32) ! {
keys := get_index_keys[T](item)
pub fn (mut m Manager[T]) exists(id u32) !bool {
return m.db_data.get(id) or { return false } != []u8{}
}
// get_by_key retrieves an item by a specific key field and value
pub fn (mut m Manager[T]) get_by_key(key_field string, key_value string) !T {
// Create the key for the radix tree
key := '${m.prefix}:${key_field}:${key_value}'
// Get the ID from the radix tree
id_bytes := m.db_meta.search(key) or {
return error('Item with ${key_field}=${key_value} not found')
}
// Convert the ID bytes to u32
id_str := id_bytes.bytestr()
id := id_str.u32()
// Get the item using the ID
return m.get(id)
}
// delete removes an item by its ID
pub fn (mut m Manager[T]) delete(id u32) ! {
exists := m.exists(id)!
if !exists {
return
}
// Get the item before deleting it to remove index keys
item := m.get(id)!
keys := item.index_keys()
for key, value in keys {
index_key := '${m.prefix}:${key}:${value}'
m.db_meta.delete(index_key)!
}
// Delete the item data from the database
m.db_data.delete(id)!
all_keys := m.list()!
// Filter out the key to remove
mut new_keys := []u32{}
for existing_key in all_keys {
if existing_key != id {
new_keys << existing_key
}
}
// Join the keys with commas and store
new_all_keys_str := new_keys.map(it.str()).join(',')
m.db_meta.insert('${m.prefix}:all', new_all_keys_str.bytes())!
}
// find_by_index_key finds items by their index key
pub fn (mut m Manager[T]) find_by_index_key(key string, value string) ![]u32 {
index_key := '${m.prefix}:${key}:${value}'
// Search for all matching keys with this prefix
matches := m.db_meta.search_prefix(index_key)
mut ids := []u32{}
for _, id_bytes in matches {
id_str := id_bytes.bytestr()
ids << id_str.u32()
}
return ids
// list returns all ids from the manager
pub fn (mut m Manager[T]) list() ![]u32 {
// Try to get existing list
if all_bytes := m.db_meta.search('${m.prefix}:all') {
all_str := all_bytes.bytestr()
if all_str.len > 0 {
// Convert string IDs to u32
mut u32_ids := []u32{}
for id_str in all_str.split(',') {
if id_str.len > 0 {
u32_ids << id_str.u32()
}
}
return u32_ids
}
}
return []u32{}
}
pub fn (mut m Manager[T]) getall() ![]T {
mut items := []T{}
for id in m.list()! {
items << m.get(id)!
}
return items
}

View File

@@ -1,50 +0,0 @@
module model
import freeflowuniverse.herolib.data.ourdb
import freeflowuniverse.herolib.data.radixtree
import os
fn test_generic_manager() {
// Create temporary directory for test
test_dir := os.temp_dir() + '/herolib_test_manager'
os.mkdir_all(test_dir) or { panic(err) }
defer { os.rmdir_all(test_dir) or {} }
// Create database and radix tree
mut db_data := ourdb.new(path: test_dir + '/data.db')!
mut db_meta := radixtree.new(path: test_dir + '/meta.db')!
// Create circle manager using our generic implementation
mut circle_manager := new_circle_manager(db_data, db_meta)
// Create a new circle
mut circle := circle_manager.new()
circle.name = 'Test Circle'
circle.description = 'A test circle for generic manager'
// Add the circle to the database
circle = circle_manager.set(mut circle)!
// Verify the circle was added
assert circle.id > 0
// Find the circle by its name using the generic index
circles := circle_manager.find_by_index('name', 'Test Circle')!
assert circles.len == 1
assert circles[0].id == circle.id
assert circles[0].name == 'Test Circle'
// Find by ID
circles_by_id := circle_manager.find_by_index('id', circle.id.str())!
assert circles_by_id.len == 1
assert circles_by_id[0].id == circle.id
// Delete the circle
circle_manager.delete(circle.id)!
// Verify the circle was deleted
circles_after_delete := circle_manager.find_by_index('name', 'Test Circle')!
assert circles_after_delete.len == 0
println('Generic manager test passed!')
}

View File

@@ -1,57 +0,0 @@
module model
import json
import freeflowuniverse.herolib.data.ourdb
import freeflowuniverse.herolib.data.radixtree
// ServiceManager handles all service-related operations
pub struct ServiceManager {
pub mut:
db_data &ourdb.OurDB // Database for storing agent data
db_meta &radixtree.RadixTree // Radix tree for mapping keys to IDs
}
// set adds or updates a service
pub fn (mut m ServiceManager) set(service Service) ! {
// Implementation removed
}
// get retrieves a service by its actor name
pub fn (mut m ServiceManager) get(actor string) !Service {
// Implementation removed
return Service{}
}
// list returns all services
pub fn (mut m ServiceManager) list() ![]Service {
mut services := []Service{}
// Implementation removed
return services
}
// delete removes a service by its actor name
pub fn (mut m ServiceManager) delete(actor string) ! {
// Implementation removed
}
// update_status updates just the status of a service
pub fn (mut m ServiceManager) update_status(actor string, status ServiceState) ! {
// Implementation removed
}
// get_by_action returns all services that provide a specific action
pub fn (mut m ServiceManager) get_by_action(action string) ![]Service {
mut matching_services := []Service{}
// Implementation removed
return matching_services
}
// check_access verifies if a user has access to a service action
pub fn (mut m ServiceManager) check_access(actor string, action string, user_pubkey string, circles []string) !bool {
// Implementation removed
return true
}

View File

@@ -1,87 +0,0 @@
module model
import freeflowuniverse.herolib.core.redisclient
fn test_services() {
mut runner := new()!
// Create a new service using the manager
mut service := runner.services.new()
service.actor = 'vm_manager'
service.description = 'VM Management Service'
// Create an ACL
mut ace := ACE{
circles: ['admin-circle']
users: ['user-1-pubkey']
right: 'write'
}
mut acl := ACL{
name: 'vm-acl'
ace: [ace]
}
// Create a service action
mut action := ServiceAction{
action: 'start'
description: 'Start a VM'
params: {
'name': 'string'
}
params_example: {
'name': 'myvm'
}
acl: acl
}
service.actions = [action]
// Add the service
runner.services.set(service)!
// Get the service and verify fields
retrieved_service := runner.services.get(service.actor)!
assert retrieved_service.actor == service.actor
assert retrieved_service.description == service.description
assert retrieved_service.actions.len == 1
assert retrieved_service.actions[0].action == 'start'
assert retrieved_service.status == .ok
// Update service status
runner.services.update_status(service.actor, .down)!
updated_service := runner.services.get(service.actor)!
assert updated_service.status == .down
// Test get_by_action
services := runner.services.get_by_action('start')!
assert services.len > 0
assert services[0].actor == service.actor
// Test access control
has_access := runner.services.check_access(service.actor, 'start', 'user-1-pubkey',
[])!
assert has_access == true
has_circle_access := runner.services.check_access(service.actor, 'start', 'user-2-pubkey',
['admin-circle'])!
assert has_circle_access == true
no_access := runner.services.check_access(service.actor, 'start', 'user-3-pubkey',
[])!
assert no_access == false
// List all services
all_services := runner.services.list()!
assert all_services.len > 0
assert all_services[0].actor == service.actor
// Delete the service
runner.services.delete(service.actor)!
// Verify deletion
services_after := runner.services.list()!
for s in services_after {
assert s.actor != service.actor
}
}

View File

@@ -1,186 +0,0 @@
create a job manager in
lib/core/jobs
## some definitions
- agent: is a self contained set of processes which can execute on actions or actions to be executed by others
- action: what needs to be executed
- circle: each action happens in a circle
- context: a context inside a circle is optional
- job, what gets executed by an agent, is one action, can depend on other actions
- herorunner: is the process which uses redis to manage all open jobs, checks for timeouts, does the forwards if needed (if remote agent need to schedule, ...)
## jobs
are executed by processes can be in different languages and they are identified by agent pub key (the one who executes)
as part of heroscript we know what to executed on which actor inside the agent, defined with method and its arguments
```v
//the description of what needs to be executed
pub struct Job {
pub mut:
guid string //unique id for the job
agents []string //the pub key of the agent(s) which will execute the command, only 1 will execute, the herorunner will try the different agents if needed till it has success
source string //pubkey from the agent who asked for the job
circle string = "default" //our digital life is organized in circles
context string = "default" //is the high level context in which actors will execute the work inside a circle
actor string //e.g. vm_manager
action string //e.g. start
params map[string]string //e.g. id:10
timeout_schedule u16 = 60 //timeout before its picked up
timeout u16 = 3600 // timeout in sec
log bool = true
ignore_error bool // means if error will just exit and not raise, there will be no error reporting
ignore_error_codes []int // of we want to ignore certain error codes
debug bool // if debug will get more context
retry int // default there is no debug
status JobStatus
dependencies []JobDependency //will not execute untill other jobs are done
}
pub struct JobStatus {
pub mut:
guid string //unique id for the job
created u32 //epoch when we created the job
start u32 //epoch when the job needs to start
end u32 //epoch when the job ended, can be in error
status //ENUM: create scheduled, planned (means arrived where actor will execute the job), running, error, ok
}
pub struct JobDependency {
pub mut:
guid string //unique id for the job
agents []string //the pub key of the agent(s) which can execute the command
}
```
the Job object is stored in redis in hset herorunner:jobs where key is the job guid and the val is the json of Job
## Agent Registration Services
Each agent (the one who hosts the different actors which execute the methods with params) register themselves to all participants.
the structs below are available to everyone and are public
```v
pub struct Agent {
pub mut:
pubkey string //pubkey using ed25519
address string //where we can gind the agent
port int //default 9999
description string //optional
status AgentStatus
services []AgentService //these are the public services
signature string //signature as done by private key of $address+$port+$description+$status (this allows everyone to verify that the data is ok)
}
pub struct AgentStatus {
pub mut:
guid string //unique id for the job
timestamp_first u32 //when agent came online
timestamp_last u32 //last time agent let us know that he is working
status //ENUM: ok, down, error, halted
}
pub struct AgentService {
pub mut:
actor string
actions []AgentServiceAction
description string
status //ENUM: ok, down, error, halted
}
pub struct AgentServiceAction {
pub mut:
action string //which action
description string //optional descroption
params map[string]string //e.g. name:'name of the vm' ...
params_example map[string]string // e.g. name:'myvm'
status //ENUM: ok, down, error, halted
public bool //if everyone can use then true, if restricted means only certain people can use
}
```
the Agent object is stored in redis in hset herorunner:agents where key is the agent pubkey and the val is the json of Agent
### Services Info
The agent and its actors register their capability to the herorunner
We have a mechanism to be specific on who can execute which, this is sort of ACL system, for now its quite rough
```v
pub struct Circle {
pub mut:
guid string //unique id
name string
description string
members []string //can be other circle or member which is defined by pubkey
}
```
this info is stored in in redis on herorunner:circles
```v
pub struct Service {
pub mut:
actor string
actions []AgentServiceAction
description string
status //ENUM: ok, down, error, halted
acl ?ACL
}
pub struct ServiceAction {
pub mut:
action string //which action
description string //optional descroption
params map[string]string //e.g. name:'name of the vm' ...
params_example map[string]string // e.g. name:'myvm'
acl ?ACL //if not used then everyone can use
}
pub struct ACL {
pub mut:
name string
ace []ACE
}
pub struct ACE {
pub mut:
circles []string //guid's of the circles who have access
users []string //in case circles are not used then is users
right string e.g. read, write, admin, block
}
```
The info for the herorunner to function is in redis on herorunner:services

View File

@@ -31,11 +31,12 @@ pub mut:
}
pub fn (c Circle) index_keys() map[string]string {
return {"pubkey": c.pubkey}
return {"name": c.name}
}
// dumps serializes the Circle struct to binary format using the encoder
// This implements the Serializer interface
pub fn (c Circle) dumps() ![]u8 {
mut e := encoder.new()

View File

@@ -19,6 +19,7 @@ mut:
}
// RadixTree represents a radix tree data structure
@[heap]
pub struct RadixTree {
mut:
db &ourdb.OurDB // Database for persistent storage