nip/src/nimpak/utcp_protocol.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