This commit is contained in:
2025-01-31 17:13:56 +03:00
parent d6a13f81e0
commit fb0754b3b2
14 changed files with 1053 additions and 0 deletions

60
lib/core/jobs/agent.v Normal file
View File

@@ -0,0 +1,60 @@
module jobs
import freeflowuniverse.herolib.data.ourtime
// Agent represents a service provider that can execute jobs
pub struct Agent {
pub mut:
pubkey string // pubkey using ed25519
address string // where we can find 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
}
// AgentStatus represents the current state of an agent
pub struct AgentStatus {
pub mut:
guid string // unique id for the job
timestamp_first ourtime.Time // when agent came online
timestamp_last ourtime.Time // last time agent let us know that he is working
status AgentState // current state of the agent
}
// AgentService represents a service provided by an agent
pub struct AgentService {
pub mut:
actor string // name of the actor providing the service
actions []AgentServiceAction // available actions for this service
description string // optional description
status AgentServiceState // current state of the service
}
// AgentServiceAction represents an action that can be performed by a service
pub struct AgentServiceAction {
pub mut:
action string // which action
description string // optional description
params map[string]string // e.g. name:'name of the vm' ...
params_example map[string]string // e.g. name:'myvm'
status AgentServiceState // current state of the action
public bool // if everyone can use then true, if restricted means only certain people can use
}
// AgentState represents the possible states of an agent
pub enum AgentState {
ok // agent is functioning normally
down // agent is not responding
error // agent encountered an error
halted // agent has been manually stopped
}
// AgentServiceState represents the possible states of an agent service or action
pub enum AgentServiceState {
ok // service/action is functioning normally
down // service/action is not available
error // service/action encountered an error
halted // service/action has been manually stopped
}

View File

@@ -0,0 +1,93 @@
module jobs
import freeflowuniverse.herolib.core.redisclient
import freeflowuniverse.herolib.data.ourtime
import json
const (
agents_key = 'herorunner:agents' // Redis key for storing agents
)
// AgentManager handles all agent-related operations
pub struct AgentManager {
mut:
redis &redisclient.Redis
}
// 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.Time{}
timestamp_last: ourtime.Time{}
status: .ok
}
services: []AgentService{}
}
}
// add adds a new agent to Redis
pub fn (mut m AgentManager) set(agent Agent) ! {
// Store agent in Redis hash where key is agent.pubkey and value is JSON of agent
agent_json := json.encode(agent)
m.redis.hset(agents_key, agent.pubkey, agent_json)!
}
// get retrieves an agent by its public key
pub fn (mut m AgentManager) get(pubkey string) !Agent {
agent_json := m.redis.hget(agents_key, pubkey)!
return json.decode(Agent, agent_json)
}
// list returns all agents
pub fn (mut m AgentManager) list() ![]Agent {
mut agents := []Agent{}
// Get all agents from Redis hash
agents_map := m.redis.hgetall(agents_key)!
// Convert each JSON value to Agent struct
for _, agent_json in agents_map {
agent := json.decode(Agent, agent_json)!
agents << agent
}
return agents
}
// delete removes an agent by its public key
pub fn (mut m AgentManager) delete(pubkey string) ! {
m.redis.hdel(agents_key, pubkey)!
}
// update_status updates just the status of an agent
pub fn (mut m AgentManager) update_status(pubkey string, status AgentState) ! {
mut agent := m.get(pubkey)!
agent.status.status = status
m.update(agent)!
}
// 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{}
agents := m.list()!
for agent in agents {
for service in agent.services {
if service.actor != actor {
continue
}
for act in service.actions {
if act.action == action {
matching_agents << agent
break
}
}
}
}
return matching_agents
}

View File

@@ -0,0 +1,75 @@
module jobs
import freeflowuniverse.herolib.core.redisclient
import freeflowuniverse.herolib.data.ourtime
fn test_runner.agents() {
mut runner:=new()!
// Create a new agent using the manager
mut agent := runner.agents.new()
agent.pubkey = 'test-agent-1'
agent.address = '127.0.0.1'
agent.description = 'Test Agent'
// Create a service action
mut action := AgentServiceAction{
action: 'start'
description: 'Start a VM'
params: {
'name': 'string'
}
params_example: {
'name': 'myvm'
}
status: .ok
public: true
}
// Create a service
mut service := AgentService{
actor: 'vm_manager'
actions: [action]
description: 'VM Management Service'
status: .ok
}
agent.services = [service]
// Add the agent
runner.agents.set(agent)!
// Get the agent and verify fields
retrieved_agent := runner.agents.get(agent.pubkey)!
assert retrieved_agent.pubkey == agent.pubkey
assert retrieved_agent.address == agent.address
assert retrieved_agent.description == agent.description
assert retrieved_agent.services.len == 1
assert retrieved_agent.services[0].actor == 'vm_manager'
assert retrieved_agent.status.status == .ok
// Update agent status
runner.agents.update_status(agent.pubkey, .down)!
updated_agent := runner.agents.get(agent.pubkey)!
assert updated_agent.status.status == .down
// Test get_by_service
agents := runner.agents.get_by_service('vm_manager', 'start')!
assert agents.len > 0
assert agents[0].pubkey == agent.pubkey
// List all agents
all_agents := runner.agents.list()!
assert all_agents.len > 0
assert all_agents[0].pubkey == agent.pubkey
// Delete the agent
runner.agents.delete(agent.pubkey)!
// Verify deletion
agents_after := runner.agents.list()!
for a in agents_after {
assert a.pubkey != agent.pubkey
}
}

37
lib/core/jobs/factory.v Normal file
View File

@@ -0,0 +1,37 @@
module jobs
import freeflowuniverse.herolib.core.redisclient
// HeroRunner is the main factory for managing jobs, agents, services and groups
pub struct HeroRunner {
mut:
redis &redisclient.Redis
pub mut:
jobs &JobManager
agents &AgentManager
services &ServiceManager
groups &GroupManager
}
// new creates a new HeroRunner instance
pub fn new() !&HeroRunner {
mut redis := redisclient.core_get()!
mut hr := &HeroRunner{
redis: redis
jobs: &JobManager{
redis: redis
}
agents: &AgentManager{
redis: redis
}
services: &ServiceManager{
redis: redis
}
groups: &GroupManager{
redis: redis
}
}
return hr
}

10
lib/core/jobs/group.v Normal file
View File

@@ -0,0 +1,10 @@
module jobs
// Group represents a collection of members (users or other groups)
pub struct Group {
pub mut:
guid string // unique id
name string // name of the group
description string // optional description
members []string // can be other group or member which is defined by pubkey
}

View File

@@ -0,0 +1,104 @@
module jobs
import freeflowuniverse.herolib.core.redisclient
import json
const (
groups_key = 'herorunner:groups' // Redis key for storing groups
)
// GroupManager handles all group-related operations
pub struct GroupManager {
mut:
redis &redisclient.Redis
}
// new creates a new Group instance
pub fn (mut m GroupManager) new() Group {
return Group{
guid: '' // Empty GUID to be filled by caller
members: []string{}
}
}
// add adds a new group to Redis
pub fn (mut m GroupManager) set(group Group) ! {
// Store group in Redis hash where key is group.guid and value is JSON of group
group_json := json.encode(group)
m.redis.hset(groups_key, group.guid, group_json)!
}
// get retrieves a group by its GUID
pub fn (mut m GroupManager) get(guid string) !Group {
group_json := m.redis.hget(groups_key, guid)!
return json.decode(Group, group_json)
}
// list returns all groups
pub fn (mut m GroupManager) list() ![]Group {
mut groups := []Group{}
// Get all groups from Redis hash
groups_map := m.redis.hgetall(groups_key)!
// Convert each JSON value to Group struct
for _, group_json in groups_map {
group := json.decode(Group, group_json)!
groups << group
}
return groups
}
// delete removes a group by its GUID
pub fn (mut m GroupManager) delete(guid string) ! {
m.redis.hdel(groups_key, guid)!
}
// add_member adds a member (user pubkey or group GUID) to a group
pub fn (mut m GroupManager) add_member(guid string, member string) ! {
mut group := m.get(guid)!
if member !in group.members {
group.members << member
m.update(group)!
}
}
// remove_member removes a member from a group
pub fn (mut m GroupManager) remove_member(guid string, member string) ! {
mut group := m.get(guid)!
group.members = group.members.filter(it != member)
m.update(group)!
}
// get_user_groups returns all groups that a user is a member of (directly or indirectly)
pub fn (mut m GroupManager) get_user_groups(user_pubkey string) ![]Group {
mut user_groups := []Group{}
mut checked_groups := map[string]bool{}
groups := m.list()!
// Recursive function to check group membership
fn check_group_membership(group Group, user string, groups []Group, mut checked map[string]bool, mut result []Group) {
if group.guid in checked {
return
}
checked[group.guid] = true
if user in group.members {
result << group
// Check parent groups
for parent_group in groups {
if group.guid in parent_group.members {
check_group_membership(parent_group, user, groups, mut checked, mut result)
}
}
}
}
// Check each group
for group in groups {
check_group_membership(group, user_pubkey, groups, mut checked_groups, mut user_groups)
}
return user_groups
}

View File

@@ -0,0 +1,67 @@
module jobs
import freeflowuniverse.herolib.core.redisclient
fn test_runner.groups() {
mut runner:=new()!
// Create a new group using the manager
mut group := runner.groups.new()
group.guid = 'admin-group'
group.name = 'Administrators'
group.description = 'Administrator group with full access'
// Add the group
runner.groups.set(group)!
// Create a subgroup
mut subgroup := runner.groups.new()
subgroup.guid = 'vm-admins'
subgroup.name = 'VM Administrators'
subgroup.description = 'VM management administrators'
runner.groups.add(subgroup)!
// Add subgroup to main group
runner.groups.add_member(group.guid, subgroup.guid)!
// Add a user to the subgroup
runner.groups.add_member(subgroup.guid, 'user-1-pubkey')!
// Get the groups and verify fields
retrieved_group := runner.groups.get(group.guid)!
assert retrieved_group.guid == group.guid
assert retrieved_group.name == group.name
assert retrieved_group.description == group.description
assert retrieved_group.members.len == 1
assert retrieved_group.members[0] == subgroup.guid
retrieved_subgroup := runner.groups.get(subgroup.guid)!
assert retrieved_subgroup.members.len == 1
assert retrieved_subgroup.members[0] == 'user-1-pubkey'
// Test recursive group membership
user_groups := runner.groups.get_user_groups('user-1-pubkey')!
assert user_groups.len == 1
assert user_groups[0].guid == subgroup.guid
// Remove member from subgroup
runner.groups.remove_member(subgroup.guid, 'user-1-pubkey')!
updated_subgroup := runner.groups.get(subgroup.guid)!
assert updated_subgroup.members.len == 0
// List all groups
groups := runner.groups.list()!
assert groups.len == 2
// Delete the groups
runner.groups.delete(subgroup.guid)!
runner.groups.delete(group.guid)!
// Verify deletion
groups_after := runner.groups.list()!
for g in groups_after {
assert g.guid != group.guid
assert g.guid != subgroup.guid
}
}

52
lib/core/jobs/job.v Normal file
View File

@@ -0,0 +1,52 @@
module jobs
import freeflowuniverse.herolib.data.ourtime
// Job represents a task to be executed by an agent
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
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 until other jobs are done
}
// JobStatus represents the current state of a job
pub struct JobStatus {
pub mut:
guid string // unique id for the job
created ourtime.Time // when we created the job
start ourtime.Time // when the job needs to start
end ourtime.Time // when the job ended, can be in error
status Status // current status of the job
}
// JobDependency represents a dependency on another job
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
}
// Status represents the possible states of a job
pub enum Status {
created // initial state
scheduled // job has been scheduled
planned // arrived where actor will execute the job
running // job is currently running
error // job encountered an error
ok // job completed successfully
}

View File

@@ -0,0 +1,70 @@
module jobs
import freeflowuniverse.herolib.core.redisclient
import freeflowuniverse.herolib.data.ourtime
import json
const (
jobs_key = 'herorunner:jobs' // Redis key for storing jobs
)
// JobManager handles all job-related operations
pub struct JobManager {
mut:
redis &redisclient.Redis
}
// 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.Time{}
start: ourtime.Time{}
end: ourtime.Time{}
status: .created
}
}
}
// add adds a new job to Redis
pub fn (mut m JobManager) set(job Job) ! {
// Store job in Redis hash where key is job.guid and value is JSON of job
job_json := json.encode(job)
m.redis.hset(jobs_key, job.guid, job_json)!
}
// get retrieves a job by its GUID
pub fn (mut m JobManager) get(guid string) !Job {
job_json := m.redis.hget(jobs_key, guid)!
return json.decode(Job, job_json)
}
// list returns all jobs
pub fn (mut m JobManager) list() ![]Job {
mut jobs := []Job{}
// Get all jobs from Redis hash
jobs_map := m.redis.hgetall(jobs_key)!
// Convert each JSON value to Job struct
for _, job_json in jobs_map {
job := json.decode(Job, job_json)!
jobs << job
}
return jobs
}
// delete removes a job by its GUID
pub fn (mut m JobManager) delete(guid string) ! {
m.redis.hdel(jobs_key, guid)!
}
// update_status updates just the status of a job
pub fn (mut m JobManager) update_status(guid string, status Status) ! {
mut job := m.get(guid)!
job.status.status = status
m.update(job)!
}

View File

@@ -0,0 +1,47 @@
module jobs
import freeflowuniverse.herolib.core.redisclient
import freeflowuniverse.herolib.data.ourtime
fn test_runner.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
}
}

44
lib/core/jobs/service.v Normal file
View File

@@ -0,0 +1,44 @@
module jobs
// Service represents a service that can be provided by agents
pub struct Service {
pub mut:
actor string // name of the actor providing the service
actions []ServiceAction // available actions for this service
description string // optional description
status ServiceState // current state of the service
acl ?ACL // access control list for the service
}
// ServiceAction represents an action that can be performed by a service
pub struct ServiceAction {
pub mut:
action string // which action
description string // optional description
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
}
// ACL represents an access control list
pub struct ACL {
pub mut:
name string
ace []ACE
}
// ACE represents an access control entry
pub struct ACE {
pub mut:
groups []string // guid's of the groups who have access
users []string // in case groups are not used then is users
right string // e.g. read, write, admin, block
}
// ServiceState represents the possible states of a service
pub enum ServiceState {
ok // service is functioning normally
down // service is not available
error // service encountered an error
halted // service has been manually stopped
}

View File

@@ -0,0 +1,124 @@
module jobs
import freeflowuniverse.herolib.core.redisclient
import json
const (
services_key = 'herorunner:services' // Redis key for storing services
)
// ServiceManager handles all service-related operations
pub struct ServiceManager {
mut:
redis &redisclient.Redis
}
// new creates a new Service instance
pub fn (mut m ServiceManager) new() Service {
return Service{
actor: '' // Empty actor name to be filled by caller
actions: []ServiceAction{}
status: .ok
}
}
// add adds a new service to Redis
pub fn (mut m ServiceManager) set(service Service) ! {
// Store service in Redis hash where key is service.actor and value is JSON of service
service_json := json.encode(service)
m.redis.hset(services_key, service.actor, service_json)!
}
// get retrieves a service by its actor name
pub fn (mut m ServiceManager) get(actor string) !Service {
service_json := m.redis.hget(services_key, actor)!
return json.decode(Service, service_json)
}
// list returns all services
pub fn (mut m ServiceManager) list() ![]Service {
mut services := []Service{}
// Get all services from Redis hash
services_map := m.redis.hgetall(services_key)!
// Convert each JSON value to Service struct
for _, service_json in services_map {
service := json.decode(Service, service_json)!
services << service
}
return services
}
// delete removes a service by its actor name
pub fn (mut m ServiceManager) delete(actor string) ! {
m.redis.hdel(services_key, actor)!
}
// update_status updates just the status of a service
pub fn (mut m ServiceManager) update_status(actor string, status ServiceState) ! {
mut service := m.get(actor)!
service.status = status
m.update(service)!
}
// 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{}
services := m.list()!
for service in services {
for act in service.actions {
if act.action == action {
matching_services << service
break
}
}
}
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, groups []string) !bool {
service := m.get(actor)!
// Find the specific action
mut service_action := ServiceAction{}
mut found := false
for act in service.actions {
if act.action == action {
service_action = act
found = true
break
}
}
if !found {
return error('Action ${action} not found in service ${actor}')
}
// If no ACL is defined, access is granted
if service_action.acl == none {
return true
}
acl := service_action.acl or { return true }
// Check each ACE in the ACL
for ace in acl.ace {
// Check if user is directly listed
if user_pubkey in ace.users {
return ace.right != 'block'
}
// Check if any of user's groups are listed
for group in groups {
if group in ace.groups {
return ace.right != 'block'
}
}
}
return false
}

View File

@@ -0,0 +1,84 @@
module jobs
import freeflowuniverse.herolib.core.redisclient
fn test_runner.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{
groups: ['admin-group']
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_group_access := runner.services.check_access(service.actor, 'start', 'user-2-pubkey', ['admin-group'])!
assert has_group_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
}
}

186
lib/core/jobs/specs.md Normal file
View File

@@ -0,0 +1,186 @@
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 Group {
pub mut:
guid string //unique id
name string
description string
members []string //can be other group or member which is defined by pubkey
}
```
this info is stored in in redis on herorunner:groups
```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:
groups []string //guid's of the groups who have access
users []string //in case groups 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