317 lines
9.9 KiB
Nim
317 lines
9.9 KiB
Nim
# SPDX-License-Identifier: LSL-1.0
|
|
# Copyright (c) 2026 Markus Maiwald
|
|
# Stewardship: Self Sovereign Society Foundation
|
|
#
|
|
# This file is part of the Nexus Sovereign Core.
|
|
# See legal/LICENSE_SOVEREIGN.md for license terms.
|
|
|
|
## UTCP (Universal Tool Communication Protocol) Implementation
|
|
##
|
|
## This module implements the Universal Tool Communication Protocol for
|
|
## AI-addressable resources in NexusOS. UTCP enables seamless communication
|
|
## between:
|
|
## - nexus (system compiler)
|
|
## - nip (package manager)
|
|
## - Janus programming language
|
|
## - n8n AI agents
|
|
## - Local LLMs
|
|
## - SystemAdmin-AIs
|
|
## - Nippels (user application environments)
|
|
## - Nexters (system containers)
|
|
##
|
|
## UTCP provides a unified addressing scheme and request/response protocol
|
|
## for distributed system management and AI-driven automation.
|
|
|
|
import std/[tables, json, strutils, times, options, uri, sequtils, random]
|
|
import utils/resultutils as nipresult
|
|
when defined(posix):
|
|
import posix
|
|
|
|
# UTCP Protocol Types
|
|
|
|
type
|
|
UTCPScheme* = enum
|
|
## UTCP protocol schemes
|
|
UtcpPlain = "utcp" ## Plain UTCP (no encryption)
|
|
UtcpSecure = "utcps" ## Secure UTCP (TLS encryption)
|
|
|
|
UTCPResourceType* = enum
|
|
## Types of UTCP-addressable resources
|
|
Nippel = "nippel" ## User application environment
|
|
Nexter = "nexter" ## System container
|
|
Package = "package" ## Package resource
|
|
System = "system" ## System-level resource
|
|
Tool = "tool" ## Tool endpoint (nexus, nip, janus)
|
|
Agent = "agent" ## AI agent endpoint
|
|
LLM = "llm" ## Local LLM endpoint
|
|
|
|
UTCPAddress* = object
|
|
## Universal address for UTCP resources
|
|
scheme*: UTCPScheme ## Protocol scheme (utcp/utcps)
|
|
host*: string ## Hostname or IP address
|
|
port*: Option[int] ## Optional port (default: 7777)
|
|
resourceType*: UTCPResourceType ## Type of resource
|
|
resourceName*: string ## Name of the resource
|
|
path*: string ## Optional sub-path
|
|
query*: Table[string, string] ## Query parameters
|
|
|
|
UTCPMethod* = enum
|
|
## UTCP request methods
|
|
GET = "GET" ## Query resource state
|
|
POST = "POST" ## Modify resource state
|
|
PUT = "PUT" ## Create/replace resource
|
|
DELETE = "DELETE" ## Delete resource
|
|
EXEC = "EXEC" ## Execute command
|
|
SUBSCRIBE = "SUBSCRIBE" ## Subscribe to events
|
|
UNSUBSCRIBE = "UNSUBSCRIBE" ## Unsubscribe from events
|
|
|
|
UTCPRequest* = object
|
|
## UTCP request structure
|
|
address*: UTCPAddress ## Target address
|
|
meth*: UTCPMethod ## Request method (renamed from 'method' to avoid keyword)
|
|
headers*: Table[string, string] ## Request headers
|
|
payload*: JsonNode ## Request payload
|
|
timestamp*: DateTime ## Request timestamp
|
|
requestId*: string ## Unique request ID
|
|
|
|
UTCPStatus* = enum
|
|
## UTCP response status codes
|
|
Ok = 200 ## Success
|
|
Created = 201 ## Resource created
|
|
Accepted = 202 ## Request accepted
|
|
NoContent = 204 ## Success, no content
|
|
BadRequest = 400 ## Invalid request
|
|
Unauthorized = 401 ## Authentication required
|
|
Forbidden = 403 ## Access denied
|
|
NotFound = 404 ## Resource not found
|
|
MethodNotAllowed = 405 ## Method not supported
|
|
Conflict = 409 ## Resource conflict
|
|
InternalError = 500 ## Server error
|
|
NotImplemented = 501 ## Method not implemented
|
|
ServiceUnavailable = 503 ## Service unavailable
|
|
|
|
UTCPResponse* = object
|
|
## UTCP response structure
|
|
status*: UTCPStatus ## Response status
|
|
headers*: Table[string, string] ## Response headers
|
|
data*: JsonNode ## Response data
|
|
timestamp*: DateTime ## Response timestamp
|
|
requestId*: string ## Matching request ID
|
|
|
|
UTCPError* = object of CatchableError
|
|
## UTCP-specific errors
|
|
address*: string ## Address that caused error
|
|
meth*: string ## Method that failed (renamed from 'method' to avoid keyword)
|
|
|
|
UTCPHandler* = proc(request: UTCPRequest): Result[UTCPResponse, UTCPError] {.closure.}
|
|
## Handler function for UTCP requests
|
|
|
|
UTCPServer* = object
|
|
## UTCP server for handling requests
|
|
host*: string
|
|
port*: int
|
|
handlers*: Table[string, UTCPHandler] ## Route -> Handler mapping
|
|
running*: bool
|
|
|
|
# Constants
|
|
|
|
const
|
|
UTCP_DEFAULT_PORT* = 7777
|
|
UTCP_VERSION* = "1.0"
|
|
UTCP_USER_AGENT* = "NexusOS-UTCP/1.0"
|
|
|
|
# UTCP Address Functions
|
|
|
|
proc newUTCPAddress*(
|
|
host: string,
|
|
resourceType: UTCPResourceType,
|
|
resourceName: string,
|
|
scheme: UTCPScheme = UtcpPlain,
|
|
port: Option[int] = none(int),
|
|
path: string = "",
|
|
query: Table[string, string] = initTable[string, string]()
|
|
): UTCPAddress =
|
|
## Create a new UTCP address
|
|
result = UTCPAddress(
|
|
scheme: scheme,
|
|
host: host,
|
|
port: port,
|
|
resourceType: resourceType,
|
|
resourceName: resourceName,
|
|
path: path,
|
|
query: query
|
|
)
|
|
|
|
proc parseUTCPAddress*(address: string): Result[UTCPAddress, string] =
|
|
## Parse a UTCP address string
|
|
## Format: utcp://host[:port]/resourceType/resourceName[/path][?query]
|
|
try:
|
|
let uri = parseUri(address)
|
|
|
|
# Parse scheme
|
|
let scheme = case uri.scheme:
|
|
of "utcp": UtcpPlain
|
|
of "utcps": UtcpSecure
|
|
else:
|
|
return err[UTCPAddress]("Invalid UTCP scheme: " & uri.scheme)
|
|
|
|
# Parse host and port
|
|
let host = uri.hostname
|
|
let port = if uri.port != "": some(parseInt(uri.port)) else: none(int)
|
|
|
|
# Parse path components
|
|
let pathParts = uri.path.split('/').filterIt(it.len > 0)
|
|
if pathParts.len < 2:
|
|
return err[UTCPAddress]("Invalid UTCP path: must have resourceType/resourceName")
|
|
|
|
# Parse resource type
|
|
let resourceType = case pathParts[0]:
|
|
of "nippel": Nippel
|
|
of "nexter": Nexter
|
|
of "package": Package
|
|
of "system": System
|
|
of "tool": Tool
|
|
of "agent": Agent
|
|
of "llm": LLM
|
|
else:
|
|
return err[UTCPAddress]("Invalid resource type: " & pathParts[0])
|
|
|
|
let resourceName = pathParts[1]
|
|
let subPath = if pathParts.len > 2: "/" & pathParts[2..^1].join("/") else: ""
|
|
|
|
# Parse query parameters
|
|
var query = initTable[string, string]()
|
|
for (key, value) in uri.query.decodeQuery():
|
|
query[key] = value
|
|
|
|
return ok(UTCPAddress(
|
|
scheme: scheme,
|
|
host: host,
|
|
port: port,
|
|
resourceType: resourceType,
|
|
resourceName: resourceName,
|
|
path: subPath,
|
|
query: query
|
|
))
|
|
|
|
except Exception as e:
|
|
return err[UTCPAddress]("Failed to parse UTCP address: " & e.msg)
|
|
|
|
proc formatUTCPAddress*(address: UTCPAddress): string =
|
|
## Format a UTCP address as a string
|
|
result = $address.scheme & "://" & address.host
|
|
|
|
if address.port.isSome:
|
|
result.add(":" & $address.port.get())
|
|
|
|
result.add("/" & $address.resourceType & "/" & address.resourceName)
|
|
|
|
if address.path.len > 0:
|
|
result.add(address.path)
|
|
|
|
if address.query.len > 0:
|
|
result.add("?")
|
|
var first = true
|
|
for key, value in address.query:
|
|
if not first:
|
|
result.add("&")
|
|
result.add(encodeUrl(key) & "=" & encodeUrl(value))
|
|
first = false
|
|
|
|
proc assignUTCPAddress*(
|
|
resourceType: UTCPResourceType,
|
|
resourceName: string,
|
|
host: string = ""
|
|
): Result[UTCPAddress, string] =
|
|
## Assign a UTCP address to a resource
|
|
## If host is empty, uses local hostname
|
|
try:
|
|
let actualHost = if host.len > 0: host else:
|
|
when defined(posix):
|
|
var buf: array[256, char]
|
|
if gethostname(cast[cstring](addr buf[0]), 256) == 0:
|
|
$cast[cstring](addr buf[0])
|
|
else:
|
|
"localhost"
|
|
else:
|
|
"localhost"
|
|
|
|
let address = newUTCPAddress(
|
|
host = actualHost,
|
|
resourceType = resourceType,
|
|
resourceName = resourceName,
|
|
scheme = UtcpPlain,
|
|
port = none(int) # Use default port
|
|
)
|
|
|
|
return ok(address)
|
|
|
|
except Exception as e:
|
|
return err[UTCPAddress]("Failed to assign UTCP address: " & e.msg)
|
|
|
|
# UTCP Request/Response Functions
|
|
|
|
proc newUTCPRequest*(
|
|
address: UTCPAddress,
|
|
meth: UTCPMethod,
|
|
payload: JsonNode = newJNull(),
|
|
headers: Table[string, string] = initTable[string, string]()
|
|
): UTCPRequest =
|
|
## Create a new UTCP request
|
|
var actualHeaders = headers
|
|
if not actualHeaders.hasKey("User-Agent"):
|
|
actualHeaders["User-Agent"] = UTCP_USER_AGENT
|
|
if not actualHeaders.hasKey("UTCP-Version"):
|
|
actualHeaders["UTCP-Version"] = UTCP_VERSION
|
|
|
|
result = UTCPRequest(
|
|
address: address,
|
|
meth: meth,
|
|
headers: actualHeaders,
|
|
payload: payload,
|
|
timestamp: now(),
|
|
requestId: $now().toTime().toUnix() & "-" & $rand(1000000)
|
|
)
|
|
|
|
proc newUTCPResponse*(
|
|
status: UTCPStatus,
|
|
data: JsonNode = newJNull(),
|
|
requestId: string = "",
|
|
headers: Table[string, string] = initTable[string, string]()
|
|
): UTCPResponse =
|
|
## Create a new UTCP response
|
|
var actualHeaders = headers
|
|
if not actualHeaders.hasKey("UTCP-Version"):
|
|
actualHeaders["UTCP-Version"] = UTCP_VERSION
|
|
|
|
result = UTCPResponse(
|
|
status: status,
|
|
headers: actualHeaders,
|
|
data: data,
|
|
timestamp: now(),
|
|
requestId: requestId
|
|
)
|
|
|
|
# UTCP Method Handlers
|
|
# NOTE: Advanced UTCP protocol features (request routing, method handlers) are not yet implemented
|
|
# They will be added when full UTCP protocol support is needed
|
|
# For now, the CLI only uses parseUTCPAddress() and formatUTCPAddress()
|
|
|
|
# Utility Functions
|
|
|
|
proc isLocalAddress*(address: UTCPAddress): bool =
|
|
## Check if an address refers to the local host
|
|
let localNames = @["localhost", "127.0.0.1", "::1"]
|
|
when defined(posix):
|
|
var buf: array[256, char]
|
|
if gethostname(cast[cstring](addr buf[0]), 256) == 0:
|
|
let hostname = $cast[cstring](addr buf[0])
|
|
return address.host in localNames or address.host == hostname
|
|
return address.host in localNames
|
|
|
|
proc getDefaultPort*(scheme: UTCPScheme): int =
|
|
## Get the default port for a UTCP scheme
|
|
case scheme:
|
|
of UtcpPlain: UTCP_DEFAULT_PORT
|
|
of UtcpSecure: UTCP_DEFAULT_PORT + 1 # 7778 for secure
|