initial openrpc server

This commit is contained in:
2025-01-31 21:53:29 +03:00
parent 99ecf1b0d8
commit 8322280066
15 changed files with 1848 additions and 0 deletions

1
lib/core/jobs/openrpc/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
server

View File

@@ -0,0 +1,183 @@
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.herolib.core.jobs.model
import net.websocket
import json
import rand
import time
import term
const ws_url = 'ws://localhost:8080'
// Helper function to send request and receive response
fn send_request(mut ws websocket.Client, request OpenRPCRequest) !OpenRPCResponse {
// Send request
request_json := json.encode(request)
println(request_json)
ws.write_string(request_json) or {
eprintln(term.red('Failed to send request: ${err}'))
return err
}
// Wait for response
mut msg := ws.read_next_message() or {
eprintln(term.red('Failed to read response: ${err}'))
return err
}
if msg.opcode != websocket.OPCode.text_frame {
return error('Invalid response type: expected text frame')
}
response_text := msg.payload.bytestr()
// Parse response
response := json.decode(OpenRPCResponse, response_text) or {
eprintln(term.red('Failed to decode response: ${err}'))
return err
}
return response
}
// OpenRPC request/response structures (copied from handler.v)
struct OpenRPCRequest {
jsonrpc string [required]
method string [required]
params []string
id int [required]
}
struct OpenRPCResponse {
jsonrpc string [required]
result string
error string
id int [required]
}
// Initialize and configure WebSocket client
fn init_client() !&websocket.Client {
mut ws := websocket.new_client(ws_url)!
ws.on_open(fn (mut ws websocket.Client) ! {
println(term.green('Connected to WebSocket server and ready...'))
})
ws.on_error(fn (mut ws websocket.Client, err string) ! {
eprintln(term.red('WebSocket error: ${err}'))
})
ws.on_close(fn (mut ws websocket.Client, code int, reason string) ! {
println(term.yellow('WebSocket connection closed: ${reason}'))
})
ws.on_message(fn (mut ws websocket.Client, msg &websocket.Message) ! {
if msg.payload.len > 0 {
println(term.blue('Received message: ${msg.payload.bytestr()}'))
}
})
ws.connect() or {
eprintln(term.red('Failed to connect: ${err}'))
return err
}
spawn ws.listen()
return ws
}
// Main client logic
mut ws := init_client()!
defer {
ws.close(1000, 'normal') or {
eprintln(term.red('Error closing connection: ${err}'))
}
}
println(term.green('Connected to ${ws_url}'))
// Create a new job
println(term.blue('\nCreating new job...'))
new_job := send_request(mut ws, OpenRPCRequest{
jsonrpc: '2.0'
method: 'job.new'
params: []string{}
id: rand.i32_in_range(1,10000000)!
}) or {
eprintln(term.red('Failed to create new job: ${err}'))
exit(1)
}
println(term.green('Created new job:'))
println(json.encode_pretty(new_job))
// Parse job from response
job := json.decode(model.Job, new_job.result) or {
eprintln(term.red('Failed to parse job: ${err}'))
exit(1)
}
// Set job properties
println(term.blue('\nSetting job properties...'))
mut updated_job := job
updated_job.guid = 'test-job-1'
updated_job.actor = 'vm_manager'
updated_job.action = 'start'
updated_job.params = {
'name': 'test-vm'
'memory': '2048'
}
// Save job
set_response := send_request(mut ws, OpenRPCRequest{
jsonrpc: '2.0'
method: 'job.set'
params: [json.encode(updated_job)]
id: rand.int()
}) or {
eprintln(term.red('Failed to save job: ${err}'))
exit(1)
}
println(term.green('Saved job:'))
println(json.encode_pretty(set_response))
// Update job status to running
println(term.blue('\nUpdating job status...'))
update_response := send_request(mut ws, OpenRPCRequest{
jsonrpc: '2.0'
method: 'job.update_status'
params: ['test-job-1', 'running']
id: rand.int()
}) or {
eprintln(term.red('Failed to update job status: ${err}'))
exit(1)
}
println(term.green('Updated job status:'))
println(json.encode_pretty(update_response))
// Get job to verify changes
println(term.blue('\nRetrieving job...'))
get_response := send_request(mut ws, OpenRPCRequest{
jsonrpc: '2.0'
method: 'job.get'
params: ['test-job-1']
id: rand.int()
}) or {
eprintln(term.red('Failed to retrieve job: ${err}'))
exit(1)
}
println(term.green('Retrieved job:'))
println(json.encode_pretty(get_response))
// List all jobs
println(term.blue('\nListing all jobs...'))
list_response := send_request(mut ws, OpenRPCRequest{
jsonrpc: '2.0'
method: 'job.list'
params: []string{}
id: rand.int()
}) or {
eprintln(term.red('Failed to list jobs: ${err}'))
exit(1)
}
println(term.green('All jobs:'))
println(json.encode_pretty(list_response))

View File

@@ -0,0 +1,41 @@
#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run
import freeflowuniverse.herolib.core.jobs.openrpc
import freeflowuniverse.herolib.core.jobs.model
import time
import sync
import os
fn start_rpc_server( mut wg sync.WaitGroup) ! {
defer { wg.done() }
// Create OpenRPC server
openrpc.server_start()!
}
fn start_ws_server( mut wg sync.WaitGroup) ! {
defer { wg.done() }
// Get port from environment variable or use default
port := if ws_port := os.getenv_opt('WS_PORT') {
ws_port.int()
} else {
8080
}
// Create and start WebSocket server
mut ws_server := openrpc.new_ws_server(port)!
ws_server.start()!
}
// Create wait group for servers
mut wg := sync.new_waitgroup()
wg.add(2)
// Start servers in separate threads
spawn start_rpc_server(mut wg)
spawn start_ws_server(mut wg)
// Wait for servers to finish (they run forever)
wg.wait()

View File

@@ -0,0 +1,27 @@
module openrpc
import freeflowuniverse.herolib.core.redisclient
import freeflowuniverse.herolib.core.jobs.model
// Generic OpenRPC server that handles all managers
pub struct OpenRPCServer {
mut:
redis &redisclient.Redis
queue &redisclient.RedisQueue
runner &model.HeroRunner
}
// Create new OpenRPC server with Redis connection
pub fn server_start() ! {
redis := redisclient.core_get()!
mut runner := model.new()!
mut s:= &OpenRPCServer{
redis: redis
queue: &redisclient.RedisQueue{
key: rpc_queue
redis: redis
}
runner:runner
}
s.start()!
}

View File

@@ -0,0 +1,71 @@
module openrpc
import freeflowuniverse.herolib.core.redisclient
import json
// Start the server and listen for requests
pub fn (mut s OpenRPCServer) start() ! {
println('Starting OpenRPC server.')
for {
// Get message from queue
msg := s.queue.get(5000)!
if msg.len==0{
println("queue '${rpc_queue}' empty")
continue
}
println("process '${msg}'")
// Parse OpenRPC request
request := json.decode(OpenRPCRequest, msg) or {
println('Error decoding request: ${err}')
continue
}
// Process request with appropriate handler
response := s.handle_request(request)!
// Send response back to Redis using response queue
response_json := json.encode(response)
key:='${rpc_queue}:${request.id}'
println("response: ${} put on return queue ${key} ")
mut response_queue := &redisclient.RedisQueue{
key: key
redis: s.redis
}
response_queue.add(response_json)!
}
}
// Get the handler for a specific method based on its prefix
fn (mut s OpenRPCServer) handle_request(request OpenRPCRequest) !OpenRPCResponse {
method := request.method.to_lower()
println("process: method: '${method}'")
if method.starts_with("job.") {
return s.handle_request_job(request) or {
return rpc_response_error(request.id,"error in request job:\n${err}")
}
}
if method.starts_with("agent.") {
return s.handle_request_agent(request) or {
return rpc_response_error(request.id,"error in request agent:\n${err}")
}
}
if method.starts_with("group.") {
return s.handle_request_group(request) or {
return rpc_response_error(request.id,"error in request group:\n${err}")
}
}
if method.starts_with("service.") {
return s.handle_request_service(request) or {
return rpc_response_error(request.id,"error in request service:\n${err}")
}
}
return rpc_response_error(request.id,"Could not find handler for ${method}")
}

View File

@@ -0,0 +1,69 @@
module openrpc
import freeflowuniverse.herolib.core.jobs.model
import json
pub fn (mut h OpenRPCServer) handle_request_agent(request OpenRPCRequest) !OpenRPCResponse {
mut response:=rpc_response_new(request.id)
match request.method {
'new' {
agent := h.runner.agents.new()
response.result = json.encode(agent)
}
'set' {
if request.params.len < 1 {
return error('Missing agent parameter')
}
agent := json.decode(model.Agent, request.params[0])!
h.runner.agents.set(agent)!
response.result = 'true'
}
'get' {
if request.params.len < 1 {
return error('Missing pubkey parameter')
}
agent := h.runner.agents.get(request.params[0])!
response.result = json.encode(agent)
}
'list' {
agents := h.runner.agents.list()!
response.result = json.encode(agents)
}
'delete' {
if request.params.len < 1 {
return error('Missing pubkey parameter')
}
h.runner.agents.delete(request.params[0])!
response.result = 'true'
}
'update_status' {
if request.params.len < 2 {
return error('Missing pubkey or status parameters')
}
status := match request.params[1] {
'ok' { model.AgentState.ok }
'down' { model.AgentState.down }
'error' { model.AgentState.error }
'halted' { model.AgentState.halted }
else { return error('Invalid status: ${request.params[1]}') }
}
h.runner.agents.update_status(request.params[0], status)!
response.result = 'true'
}
'get_by_service' {
if request.params.len < 2 {
return error('Missing actor or action parameters')
}
agents := h.runner.agents.get_by_service(request.params[0], request.params[1])!
response.result = json.encode(agents)
}
else {
return error('Unknown method: ${request.method}')
}
}
return response
}

View File

@@ -0,0 +1,66 @@
module openrpc
import freeflowuniverse.herolib.core.jobs.model
import json
pub fn (mut h OpenRPCServer) handle_request_group(request OpenRPCRequest) !OpenRPCResponse {
mut response:=rpc_response_new(request.id)
match request.method {
'new' {
group := h.runner.groups.new()
response.result = json.encode(group)
}
'set' {
if request.params.len < 1 {
return error('Missing group parameter')
}
group := json.decode(model.Group, request.params[0])!
h.runner.groups.set(group)!
response.result = 'true'
}
'get' {
if request.params.len < 1 {
return error('Missing guid parameter')
}
group := h.runner.groups.get(request.params[0])!
response.result = json.encode(group)
}
'list' {
groups := h.runner.groups.list()!
response.result = json.encode(groups)
}
'delete' {
if request.params.len < 1 {
return error('Missing guid parameter')
}
h.runner.groups.delete(request.params[0])!
response.result = 'true'
}
'add_member' {
if request.params.len < 2 {
return error('Missing guid or member parameters')
}
h.runner.groups.add_member(request.params[0], request.params[1])!
response.result = 'true'
}
'remove_member' {
if request.params.len < 2 {
return error('Missing guid or member parameters')
}
h.runner.groups.remove_member(request.params[0], request.params[1])!
response.result = 'true'
}
'get_user_groups' {
if request.params.len < 1 {
return error('Missing user_pubkey parameter')
}
groups := h.runner.groups.get_user_groups(request.params[0])!
response.result = json.encode(groups)
}
else {
return error('Unknown method: ${request.method}')
}
}
return response
}

View File

@@ -0,0 +1,63 @@
module openrpc
import freeflowuniverse.herolib.core.jobs.model
import json
pub fn (mut h OpenRPCServer) handle_request_job(request OpenRPCRequest) !OpenRPCResponse {
mut response:=rpc_response_new(request.id)
println(request)
match request.method {
'new' {
job := h.runner.jobs.new()
response.result = json.encode(job)
}
'set' {
if request.params.len < 1 {
return error('Missing job parameter')
}
job := json.decode(model.Job, request.params[0])!
h.runner.jobs.set(job)!
response.result = 'true'
}
'get' {
if request.params.len < 1 {
return error('Missing guid parameter')
}
job := h.runner.jobs.get(request.params[0])!
response.result = json.encode(job)
}
'list' {
jobs := h.runner.jobs.list()!
response.result = json.encode(jobs)
}
'delete' {
if request.params.len < 1 {
return error('Missing guid parameter')
}
h.runner.jobs.delete(request.params[0])!
response.result = 'true'
}
'update_status' {
if request.params.len < 2 {
return error('Missing guid or status parameters')
}
status := match request.params[1] {
'created' { model.Status.created }
'scheduled' { model.Status.scheduled }
'planned' { model.Status.planned }
'running' { model.Status.running }
'error' { model.Status.error }
'ok' { model.Status.ok }
else { return error('Invalid status: ${request.params[1]}') }
}
h.runner.jobs.update_status(request.params[0], status)!
job := h.runner.jobs.get(request.params[0])! // Get updated job to return
response.result = json.encode(job)
}
else {
return error('Unknown method: ${request.method}')
}
}
return response
}

View File

@@ -0,0 +1,81 @@
module openrpc
import freeflowuniverse.herolib.core.jobs.model
import json
pub fn (mut h OpenRPCServer) handle_request_service(request OpenRPCRequest) !OpenRPCResponse {
mut response:=rpc_response_new(request.id)
match request.method {
'new' {
service := h.runner.services.new()
response.result = json.encode(service)
}
'set' {
if request.params.len < 1 {
return error('Missing service parameter')
}
service := json.decode(model.Service, request.params[0])!
h.runner.services.set(service)!
response.result = 'true'
}
'get' {
if request.params.len < 1 {
return error('Missing actor parameter')
}
service := h.runner.services.get(request.params[0])!
response.result = json.encode(service)
}
'list' {
services := h.runner.services.list()!
response.result = json.encode(services)
}
'delete' {
if request.params.len < 1 {
return error('Missing actor parameter')
}
h.runner.services.delete(request.params[0])!
response.result = 'true'
}
'update_status' {
if request.params.len < 2 {
return error('Missing actor or status parameters')
}
status := match request.params[1] {
'ok' { model.ServiceState.ok }
'down' { model.ServiceState.down }
'error' { model.ServiceState.error }
'halted' { model.ServiceState.halted }
else { return error('Invalid status: ${request.params[1]}') }
}
h.runner.services.update_status(request.params[0], status)!
response.result = 'true'
}
'get_by_action' {
if request.params.len < 1 {
return error('Missing action parameter')
}
services := h.runner.services.get_by_action(request.params[0])!
response.result = json.encode(services)
}
'check_access' {
if request.params.len < 4 {
return error('Missing parameters: requires actor, action, user_pubkey, and groups')
}
// Parse groups array from JSON string
groups := json.decode([]string, request.params[3])!
has_access := h.runner.services.check_access(
request.params[0], // actor
request.params[1], // action
request.params[2], // user_pubkey
groups
)!
response.result = json.encode(has_access)
}
else {
return error('Unknown method: ${request.method}')
}
}
return response
}

View File

@@ -0,0 +1,39 @@
module openrpc
// Generic OpenRPC request/response structures
pub struct OpenRPCRequest {
pub mut:
jsonrpc string @[required]
method string @[required]
params []string
id int @[required]
}
pub struct OpenRPCResponse {
pub mut:
jsonrpc string @[required]
result string
error string
id int @[required]
}
fn rpc_response_new(id int)OpenRPCResponse {
mut response := OpenRPCResponse{
jsonrpc: '2.0'
id: id
}
return response
}
fn rpc_response_error(id int, errormsg string)OpenRPCResponse {
mut response := OpenRPCResponse{
jsonrpc: '2.0'
id: id
error:errormsg
}
return response
}
const rpc_queue = 'herorunner:q:rpc'

View File

@@ -0,0 +1,302 @@
{
"openrpc": "1.2.6",
"info": {
"title": "AgentManager Service",
"version": "1.0.0",
"description": "OpenRPC specification for the AgentManager module and its methods."
},
"methods": [
{
"name": "new",
"summary": "Create a new Agent instance",
"description": "Returns a new Agent with default or empty fields set. Caller can then fill in details.",
"params": [],
"result": {
"name": "Agent",
"description": "A freshly created Agent object.",
"schema": {
"$ref": "#/components/schemas/Agent"
}
}
},
{
"name": "set",
"summary": "Add or update an Agent in the system",
"description": "Stores an Agent in Redis by pubkey. Overwrites any previous entry with the same pubkey.",
"params": [
{
"name": "agent",
"description": "The Agent instance to be added or updated.",
"required": true,
"schema": {
"$ref": "#/components/schemas/Agent"
}
}
],
"result": {
"name": "success",
"description": "Indicates success. No data returned on success.",
"schema": {
"type": "boolean"
}
}
},
{
"name": "get",
"summary": "Retrieve an Agent by its public key",
"description": "Looks up a single Agent using its pubkey.",
"params": [
{
"name": "pubkey",
"description": "The public key to look up.",
"required": true,
"schema": {
"type": "string"
}
}
],
"result": {
"name": "Agent",
"description": "The Agent that was requested, if found.",
"schema": {
"$ref": "#/components/schemas/Agent"
}
}
},
{
"name": "list",
"summary": "List all Agents",
"description": "Returns an array of all known Agents.",
"params": [],
"result": {
"name": "Agents",
"description": "A list of all Agents in the system.",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Agent"
}
}
}
},
{
"name": "delete",
"summary": "Delete an Agent by its public key",
"description": "Removes an Agent from the system by pubkey.",
"params": [
{
"name": "pubkey",
"description": "The public key of the Agent to be deleted.",
"required": true,
"schema": {
"type": "string"
}
}
],
"result": {
"name": "success",
"description": "Indicates success. No data returned on success.",
"schema": {
"type": "boolean"
}
}
},
{
"name": "update_status",
"summary": "Update the status of an Agent",
"description": "Updates only the status field of the specified Agent.",
"params": [
{
"name": "pubkey",
"description": "Public key of the Agent to update.",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "status",
"description": "The new status to set for the Agent.",
"required": true,
"schema": {
"$ref": "#/components/schemas/AgentState"
}
}
],
"result": {
"name": "success",
"description": "Indicates success. No data returned on success.",
"schema": {
"type": "boolean"
}
}
},
{
"name": "get_by_service",
"summary": "Retrieve all Agents that provide a specific service action",
"description": "Filters Agents by matching actor and action in any of their declared services.",
"params": [
{
"name": "actor",
"description": "The actor name to match.",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "action",
"description": "The action name to match.",
"required": true,
"schema": {
"type": "string"
}
}
],
"result": {
"name": "Agents",
"description": "A list of Agents that match the specified service actor and action.",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Agent"
}
}
}
}
],
"components": {
"schemas": {
"Agent": {
"type": "object",
"properties": {
"pubkey": {
"type": "string",
"description": "Public key (ed25519) of the Agent."
},
"address": {
"type": "string",
"description": "Network address or domain where the Agent can be reached."
},
"port": {
"type": "integer",
"description": "Network port for the Agent (default: 9999)."
},
"description": {
"type": "string",
"description": "Optional human-readable description of the Agent."
},
"status": {
"$ref": "#/components/schemas/AgentStatus"
},
"services": {
"type": "array",
"items": {
"$ref": "#/components/schemas/AgentService"
},
"description": "List of public services provided by the Agent."
},
"signature": {
"type": "string",
"description": "Signature (by the Agent's private key) of address+port+description+status."
}
},
"required": ["pubkey", "status", "services"]
},
"AgentStatus": {
"type": "object",
"properties": {
"guid": {
"type": "string",
"description": "Unique ID for the job or session."
},
"timestamp_first": {
"$ref": "#/components/schemas/OurTime",
"description": "Timestamp when this Agent first came online."
},
"timestamp_last": {
"$ref": "#/components/schemas/OurTime",
"description": "Timestamp of the last heartbeat or update from the Agent."
},
"status": {
"$ref": "#/components/schemas/AgentState"
}
}
},
"AgentService": {
"type": "object",
"properties": {
"actor": {
"type": "string",
"description": "The actor name providing the service."
},
"actions": {
"type": "array",
"items": {
"$ref": "#/components/schemas/AgentServiceAction"
},
"description": "List of actions available for this service."
},
"description": {
"type": "string",
"description": "Optional human-readable description for the service."
},
"status": {
"$ref": "#/components/schemas/AgentServiceState"
}
},
"required": ["actor", "actions", "status"]
},
"AgentServiceAction": {
"type": "object",
"properties": {
"action": {
"type": "string",
"description": "Action name."
},
"description": {
"type": "string",
"description": "Optional description of this action."
},
"params": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "Dictionary of parameter names to parameter descriptions."
},
"params_example": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "Example values for the parameters."
},
"status": {
"$ref": "#/components/schemas/AgentServiceState"
},
"public": {
"type": "boolean",
"description": "Indicates if the action is publicly accessible to all or restricted."
}
},
"required": ["action", "status", "public"]
},
"AgentState": {
"type": "string",
"enum": ["ok", "down", "error", "halted"],
"description": "Possible states of an Agent."
},
"AgentServiceState": {
"type": "string",
"enum": ["ok", "down", "error", "halted"],
"description": "Possible states of an Agent service or action."
},
"OurTime": {
"type": "string",
"format": "date-time",
"description": "Represents a date/time or timestamp value."
}
}
}
}

View File

@@ -0,0 +1,218 @@
{
"openrpc": "1.2.6",
"info": {
"title": "Group Manager API",
"version": "1.0.0",
"description": "An OpenRPC specification for Group Manager methods"
},
"servers": [
{
"name": "Local",
"url": "http://localhost:8080"
}
],
"methods": [
{
"name": "GroupManager.new",
"summary": "Create a new (in-memory) Group instance",
"description": "Creates a new group object. Note that this does NOT store it in Redis. The caller must set the groups GUID and then call `GroupManager.set` if they wish to persist it.",
"params": [],
"result": {
"name": "group",
"description": "The newly-created group instance",
"schema": {
"$ref": "#/components/schemas/Group"
}
}
},
{
"name": "GroupManager.set",
"summary": "Add or update a Group in Redis",
"description": "Stores the specified group in Redis using the groups GUID as the key.",
"params": [
{
"name": "group",
"description": "The group object to store",
"schema": {
"$ref": "#/components/schemas/Group"
}
}
],
"result": {
"name": "result",
"description": "No return value",
"schema": {
"type": "null"
}
}
},
{
"name": "GroupManager.get",
"summary": "Retrieve a Group by GUID",
"description": "Fetches the group from Redis using the provided GUID.",
"params": [
{
"name": "guid",
"description": "The groups unique identifier",
"schema": {
"type": "string"
}
}
],
"result": {
"name": "group",
"description": "The requested group",
"schema": {
"$ref": "#/components/schemas/Group"
}
}
},
{
"name": "GroupManager.list",
"summary": "List all Groups",
"description": "Returns an array containing all groups stored in Redis.",
"params": [],
"result": {
"name": "groups",
"description": "All currently stored groups",
"schema": {
"$ref": "#/components/schemas/GroupList"
}
}
},
{
"name": "GroupManager.delete",
"summary": "Delete a Group by GUID",
"description": "Removes the specified group from Redis by its GUID.",
"params": [
{
"name": "guid",
"description": "The groups unique identifier",
"schema": {
"type": "string"
}
}
],
"result": {
"name": "result",
"description": "No return value",
"schema": {
"type": "null"
}
}
},
{
"name": "GroupManager.add_member",
"summary": "Add a member to a Group",
"description": "Adds a user pubkey or another groups GUID to the member list of the specified group. Does not add duplicates.",
"params": [
{
"name": "guid",
"description": "The target groups unique identifier",
"schema": {
"type": "string"
}
},
{
"name": "member",
"description": "Pubkey or group GUID to be added to the group",
"schema": {
"type": "string"
}
}
],
"result": {
"name": "result",
"description": "No return value",
"schema": {
"type": "null"
}
}
},
{
"name": "GroupManager.remove_member",
"summary": "Remove a member from a Group",
"description": "Removes a user pubkey or another groups GUID from the member list of the specified group.",
"params": [
{
"name": "guid",
"description": "The target groups unique identifier",
"schema": {
"type": "string"
}
},
{
"name": "member",
"description": "Pubkey or group GUID to be removed from the group",
"schema": {
"type": "string"
}
}
],
"result": {
"name": "result",
"description": "No return value",
"schema": {
"type": "null"
}
}
},
{
"name": "GroupManager.get_user_groups",
"summary": "List Groups that a user belongs to (directly or indirectly)",
"description": "Checks each group (and nested groups) to see if the user pubkey is a member, returning all groups in which the user is included (including membership through nested groups).",
"params": [
{
"name": "user_pubkey",
"description": "The pubkey of the user to check",
"schema": {
"type": "string"
}
}
],
"result": {
"name": "groups",
"description": "A list of groups to which the user belongs",
"schema": {
"$ref": "#/components/schemas/GroupList"
}
}
}
],
"components": {
"schemas": {
"Group": {
"type": "object",
"properties": {
"guid": {
"type": "string",
"description": "Unique ID for the group"
},
"name": {
"type": "string",
"description": "Name of the group"
},
"description": {
"type": "string",
"description": "Optional description of the group"
},
"members": {
"type": "array",
"description": "List of user pubkeys or other group GUIDs",
"items": {
"type": "string"
}
}
},
"required": ["guid", "members"]
},
"GroupList": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Group"
}
}
}
}
}

View File

@@ -0,0 +1,304 @@
{
"openrpc": "1.2.6",
"info": {
"title": "JobManager OpenRPC Specification",
"version": "1.0.0",
"description": "OpenRPC specification for the JobManager module which handles job operations."
},
"servers": [
{
"name": "Local",
"url": "http://localhost:8080/rpc"
}
],
"methods": [
{
"name": "newJob",
"summary": "Create a new Job instance",
"description": "Creates a new Job with default/empty values. The GUID is left empty for the caller to fill.",
"params": [],
"result": {
"name": "job",
"description": "A newly created Job object, not yet persisted.",
"schema": {
"$ref": "#/components/schemas/Job"
}
}
},
{
"name": "setJob",
"summary": "Add or update a Job in the system (Redis)",
"description": "Persists the given Job into the data store. If the GUID already exists, the existing job is overwritten.",
"params": [
{
"name": "job",
"description": "The Job object to store or update.",
"required": true,
"schema": {
"$ref": "#/components/schemas/Job"
}
}
],
"result": {
"name": "success",
"description": "Indicates if the operation was successful.",
"schema": {
"type": "boolean"
}
}
},
{
"name": "getJob",
"summary": "Retrieve a Job by its GUID",
"description": "Fetches an existing Job from the data store using its unique GUID.",
"params": [
{
"name": "guid",
"description": "The GUID of the Job to retrieve.",
"required": true,
"schema": {
"type": "string"
}
}
],
"result": {
"name": "job",
"description": "The retrieved Job object.",
"schema": {
"$ref": "#/components/schemas/Job"
}
}
},
{
"name": "listJobs",
"summary": "List all Jobs",
"description": "Returns an array of all Jobs present in the data store.",
"params": [],
"result": {
"name": "jobs",
"description": "Array of all Job objects found.",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Job"
}
}
}
},
{
"name": "deleteJob",
"summary": "Remove a Job by its GUID",
"description": "Deletes a specific Job from the data store by its GUID.",
"params": [
{
"name": "guid",
"description": "The GUID of the Job to delete.",
"required": true,
"schema": {
"type": "string"
}
}
],
"result": {
"name": "success",
"description": "Indicates if the job was successfully deleted.",
"schema": {
"type": "boolean"
}
}
},
{
"name": "updateJobStatus",
"summary": "Update the status of a Job",
"description": "Sets the status field of a Job in the data store.",
"params": [
{
"name": "guid",
"description": "The GUID of the Job to update.",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "status",
"description": "The new status for the Job.",
"required": true,
"schema": {
"$ref": "#/components/schemas/Status"
}
}
],
"result": {
"name": "job",
"description": "The updated Job object with new status applied.",
"schema": {
"$ref": "#/components/schemas/Job"
}
}
}
],
"components": {
"schemas": {
"Job": {
"type": "object",
"properties": {
"guid": {
"type": "string",
"description": "Unique ID for the Job."
},
"agents": {
"type": "array",
"description": "Public keys of the agent(s) which will execute the command.",
"items": {
"type": "string"
}
},
"source": {
"type": "string",
"description": "Pubkey of the agent who requested the job."
},
"circle": {
"type": "string",
"description": "Digital-life circle name this Job belongs to.",
"default": "default"
},
"context": {
"type": "string",
"description": "High-level context for the Job inside a circle.",
"default": "default"
},
"actor": {
"type": "string",
"description": "Actor name that will handle the Job (e.g. `vm_manager`)."
},
"action": {
"type": "string",
"description": "Action to be taken by the actor (e.g. `start`)."
},
"params": {
"type": "object",
"description": "Key-value parameters for the action to be performed.",
"additionalProperties": {
"type": "string"
}
},
"timeout_schedule": {
"type": "integer",
"description": "Timeout (in seconds) before the job is picked up by an agent.",
"default": 60
},
"timeout": {
"type": "integer",
"description": "Timeout (in seconds) for the job to complete.",
"default": 3600
},
"log": {
"type": "boolean",
"description": "Whether to log job details.",
"default": true
},
"ignore_error": {
"type": "boolean",
"description": "If true, job errors do not cause an exception to be raised."
},
"ignore_error_codes": {
"type": "array",
"description": "Array of error codes to ignore.",
"items": {
"type": "integer"
}
},
"debug": {
"type": "boolean",
"description": "If true, additional debug information is provided.",
"default": false
},
"retry": {
"type": "integer",
"description": "Number of retries allowed on error.",
"default": 0
},
"status": {
"$ref": "#/components/schemas/JobStatus"
},
"dependencies": {
"type": "array",
"description": "List of job dependencies that must complete before this job executes.",
"items": {
"$ref": "#/components/schemas/JobDependency"
}
}
},
"required": [
"guid",
"status"
]
},
"JobStatus": {
"type": "object",
"properties": {
"guid": {
"type": "string",
"description": "Unique ID for the Job (mirrors the parent job GUID)."
},
"created": {
"type": "string",
"format": "date-time",
"description": "When the job was created."
},
"start": {
"type": "string",
"format": "date-time",
"description": "When the job was picked up to start."
},
"end": {
"type": "string",
"format": "date-time",
"description": "When the job ended."
},
"status": {
"$ref": "#/components/schemas/Status"
}
},
"required": [
"guid",
"created",
"status"
]
},
"JobDependency": {
"type": "object",
"properties": {
"guid": {
"type": "string",
"description": "Unique ID of the Job this dependency points to."
},
"agents": {
"type": "array",
"description": "Possible agent(s) who can execute the dependency.",
"items": {
"type": "string"
}
}
},
"required": [
"guid"
]
},
"Status": {
"type": "string",
"enum": [
"created",
"scheduled",
"planned",
"running",
"error",
"ok"
],
"description": "Enumerates the possible states of a Job."
}
}
}
}

View File

@@ -0,0 +1,301 @@
{
"openrpc": "1.2.6",
"info": {
"title": "ServiceManager API",
"version": "1.0.0",
"description": "OpenRPC 2.0 spec for managing services with ServiceManager."
},
"servers": [
{
"name": "Local",
"url": "http://localhost:8080"
}
],
"methods": [
{
"name": "ServiceManager_new",
"summary": "Create a new Service instance (not saved to Redis yet).",
"description": "Creates and returns a new empty Service object with default values. The `actor` field remains empty until the caller sets it.",
"params": [],
"result": {
"name": "service",
"$ref": "#/components/schemas/Service"
}
},
{
"name": "ServiceManager_set",
"summary": "Add or update a Service in Redis.",
"description": "Stores the Service in Redis, identified by its `actor` property.",
"params": [
{
"name": "service",
"schema": {
"$ref": "#/components/schemas/Service"
}
}
],
"result": {
"name": "success",
"schema": {
"type": "boolean",
"description": "True if operation succeeds."
}
}
},
{
"name": "ServiceManager_get",
"summary": "Retrieve a Service by actor name.",
"description": "Gets the Service object from Redis using the given actor name.",
"params": [
{
"name": "actor",
"schema": {
"type": "string"
}
}
],
"result": {
"name": "service",
"$ref": "#/components/schemas/Service"
}
},
{
"name": "ServiceManager_list",
"summary": "List all Services.",
"description": "Returns an array of all Services stored in Redis.",
"params": [],
"result": {
"name": "services",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Service"
}
}
}
},
{
"name": "ServiceManager_delete",
"summary": "Delete a Service by actor name.",
"description": "Removes the Service from Redis using the given actor name.",
"params": [
{
"name": "actor",
"schema": {
"type": "string"
}
}
],
"result": {
"name": "success",
"schema": {
"type": "boolean"
}
}
},
{
"name": "ServiceManager_update_status",
"summary": "Update the status of a given Service.",
"description": "Updates only the `status` field of a Service specified by its actor name.",
"params": [
{
"name": "actor",
"schema": {
"type": "string"
}
},
{
"name": "status",
"schema": {
"$ref": "#/components/schemas/ServiceState"
}
}
],
"result": {
"name": "success",
"schema": {
"type": "boolean"
}
}
},
{
"name": "ServiceManager_get_by_action",
"summary": "Retrieve Services by action name.",
"description": "Returns all Services that provide the specified action.",
"params": [
{
"name": "action",
"schema": {
"type": "string"
}
}
],
"result": {
"name": "services",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Service"
}
}
}
},
{
"name": "ServiceManager_check_access",
"summary": "Check if a user has access to a Service action.",
"description": "Verifies if a user (and any groups they belong to) has the right to invoke a specified action on a given Service.",
"params": [
{
"name": "actor",
"schema": {
"type": "string"
}
},
{
"name": "action",
"schema": {
"type": "string"
}
},
{
"name": "user_pubkey",
"schema": {
"type": "string"
}
},
{
"name": "groups",
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
}
],
"result": {
"name": "hasAccess",
"schema": {
"type": "boolean"
}
}
}
],
"components": {
"schemas": {
"Service": {
"type": "object",
"properties": {
"actor": {
"type": "string",
"description": "The actor (unique name) providing the service."
},
"actions": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ServiceAction"
},
"description": "A list of actions available in this service."
},
"description": {
"type": "string",
"description": "Optional description of the service."
},
"status": {
"$ref": "#/components/schemas/ServiceState",
"description": "The current state of the service."
},
"acl": {
"$ref": "#/components/schemas/ACL",
"description": "An optional access control list for the entire service."
}
},
"required": ["actor", "actions", "status"]
},
"ServiceAction": {
"type": "object",
"properties": {
"action": {
"type": "string",
"description": "A unique identifier for the action."
},
"description": {
"type": "string",
"description": "Optional description of this action."
},
"params": {
"type": "object",
"description": "Parameter definitions for this action.",
"additionalProperties": {
"type": "string"
}
},
"params_example": {
"type": "object",
"description": "Example parameters for this action.",
"additionalProperties": {
"type": "string"
}
},
"acl": {
"$ref": "#/components/schemas/ACL",
"description": "Optional ACL specifically for this action."
}
},
"required": ["action"]
},
"ACL": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "A friendly name for the ACL."
},
"ace": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ACE"
},
"description": "A list of Access Control Entries."
}
},
"required": ["ace"]
},
"ACE": {
"type": "object",
"properties": {
"groups": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of group IDs that have this permission."
},
"users": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of user public keys that have this permission."
},
"right": {
"type": "string",
"description": "Permission type (e.g. 'read', 'write', 'admin', 'block')."
}
},
"required": ["right"]
},
"ServiceState": {
"type": "string",
"enum": [
"ok",
"down",
"error",
"halted"
],
"description": "Possible states of a service."
}
}
}
}

View File

@@ -0,0 +1,82 @@
module openrpc
import net.websocket
import freeflowuniverse.herolib.core.redisclient
import json
import rand
// WebSocket server that receives RPC requests
pub struct WSServer {
mut:
redis &redisclient.Redis
port int = 8080 // Default port, can be configured
}
// Create new WebSocket server
pub fn new_ws_server( port int) !&WSServer {
return &WSServer{
redis: redisclient.core_get()!
port: port
}
}
// Start the WebSocket server
pub fn (mut s WSServer) start() ! {
mut ws_server := websocket.new_server(.ip, s.port, '')
// Handle new WebSocket connections
ws_server.on_connect(fn (mut ws websocket.ServerClient) !bool {
println('New WebSocket client connected')
return true
})!
// Handle client disconnections
ws_server.on_close(fn (mut ws websocket.Client, code int, reason string) ! {
println('WebSocket client disconnected (code: ${code}, reason: ${reason})')
})
// Handle incoming messages
ws_server.on_message(fn [mut s] (mut ws websocket.Client, msg &websocket.Message) ! {
if msg.opcode != .text_frame {
println('WebSocket unknown msg opcode (code: ${msg.opcode})')
return
}
// Parse request
request := json.decode(OpenRPCRequest, msg.payload.bytestr()) or {
error_msg := '{"jsonrpc":"2.0","error":"Invalid JSON-RPC request","id":null}'
println(error_msg)
ws.write(error_msg.bytes(), websocket.OPCode.text_frame) or { panic(err) }
return
}
// Generate unique request ID if not provided
mut req_id := request.id
if req_id == 0 {
req_id = rand.i32_in_range(1,10000000)!
}
println('WebSocket put on queue: \'${rpc_queue}\' (msg: ${msg.payload.bytestr()})')
// Send request to Redis queue
s.redis.lpush(rpc_queue, msg.payload.bytestr())!
// Wait for response
response := s.redis.brpop(['${rpc_queue}:${req_id}'], 30)! // 30 second timeout
if response.len < 2 {
error_msg := '{"jsonrpc":"2.0","error":"Timeout waiting for response","id":${req_id}}'
println('WebSocket error response (err: ${response})')
ws.write(error_msg.bytes(), websocket.OPCode.text_frame) or { panic(err) }
return
}
println('WebSocket ok response (msg: ${response[1].bytes()})')
// Send response back to WebSocket client
ws.write(response[1].bytes(), websocket.OPCode.text_frame) or { panic(err) }
})
// Start server
println('WebSocket server listening on port ${s.port}')
ws_server.listen() or {
return error('Failed to start WebSocket server: ${err}')
}
}