ontoref/reflection/modules/services.nu
Jesús Pérez 82a358f18d
Some checks failed
Nickel Type Check / Nickel Type Checking (push) Has been cancelled
Rust CI / Security Audit (push) Has been cancelled
Rust CI / Check + Test + Lint (push) Has been cancelled
feat: #[onto_mcp_tool] catalog, OCI credential vault layer, validate ADR-018 mode hierarchy
ontoref-derive: #[onto_mcp_tool] attribute macro registers MCP tool unit-structs in
  the catalog at link time via inventory::submit!; annotated item is emitted unchanged,
  ToolBase/AsyncTool impls stay on the struct. All 34 tools migrated from manual wiring
  (net +5: ontoref_list_projects, ontoref_search, ontoref_describe,
  ontoref_list_ontology_extensions, ontoref_get_ontology_extension).

  validate modes (ADR-018): reads level_hierarchy from workflow.ncl and checks every
  .ncl mode for level declared, strategy declared, delegate chain coherent, compose
  extends valid. mode resolve <id> shows which hierarchy level handles a mode and why.
  --self-test generates synthetic fixtures in a temp dir for CI smoke-testing.

  validate run-cargo: two-step Cargo.toml resolution — workspace layout first
  (crates/<check.crate>/Cargo.toml), single-crate fallback by package name or repo
  basename. Lets the same ADR constraint shape apply to workspace and single-crate repos.

  ontology/schemas/manifest.ncl: registry_topology_type contract — multi-registry
  coordination, push targets, participant scopes, per-namespace capability.

  reflection/requirements/base.ncl: oras ≥1.2.0, cosign ≥2.0.0, sops ≥3.9.0, age
  ≥1.1.0, restic declared as Hard/Soft requirements with version_min, check_cmd, and
  install_hint (ADR-017 toolchain surface).

  ADR-019: per-file recipient routing for tenant isolation without multi-vault. Schema
  additions: sops.recipient_groups + sops.recipient_rules in ontoref-project.ncl.
  secrets-bootstrap generates .sops.yaml from project.ncl in declarative mode. Three
  new secrets-audit checks: recipient-routing-coherent, recipient-routing-coverage,
  no-multi-vault. Adoption templates: single-team/, multi-tenant/, agent-first/.
  Integration templates: domain-producer/, mode-producer/, mode-consumer/.

  UI: project_picker surfaces registry badge (⟳ participant) and vault badge
  (⛁ vault_id · N, green=declarative / amber=legacy) per project card. Expanded panel
  adds collapsible Registry section with namespace, endpoint, and push/pull capability.
  manage.html gains Runtime Services card — MCP and GraphQL toggleable without restart
  via HTMX POST /ui/manage/services/{service}/toggle.

  describe.nu: capabilities JSON includes registry_topology and vault_state per project.
  sync.nu: drift check extended to detect //! absence on newly registered crates.
  qa.ncl: six entries — credential-vault-best-practice (layered data-flow diagram),
  credential-vault-templates (paths A/B/C), credential-vault-troubleshooting (15 named
  errors), integration-what-and-why (ADR-042 OCI federation), integration-how-to-implement,
  integration-troubleshooting.

  on+re: core.ncl + manifest.ncl updated to reflect OCI, MCP, and mode-hierarchy nodes.
  Deleted stale presentation assets (2026-02 slides + voice notes).
2026-05-12 04:46:15 +01:00

347 lines
10 KiB
Text

#!/usr/bin/env nu
# services.nu - Config-driven service lifecycle management with dependency resolution.
#
# Services configured in .ontoref/config.ncl:
# services.services[] - array of {id, enabled, depends_on[], config}
# services.startup_order - explicit startup order (optional)
# services.shutdown_order - explicit shutdown order (optional)
#
# Usage:
# ontoref services # Show all services status
# ontoref services start [id] # Start service (and dependencies)
# ontoref services stop [id] # Stop service
# ontoref services restart [id] # Restart service
# ontoref services status [id] # Show service status
# ontoref services health [id] # Health check service
use ../modules/store.nu *
# -- Configuration ------------------------------------------------------------
def get-project-config []: nothing -> record {
let root = ($env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT)
let config_path = $"($root)/.ontoref/config.ncl"
if not ($config_path | path exists) {
return { services: { services: [] }, daemon: {} }
}
let result = (do { ^nickel export $config_path } | complete)
if $result.exit_code == 0 {
$result.stdout | from json
} else {
{ services: { services: [] }, daemon: {} }
}
}
def get-internal-services []: nothing -> list {
# Only daemon is managed by onref. DB is external.
[
{ id: "daemon", enabled: false, depends_on: [], managed: true }
]
}
def get-services-list []: nothing -> list {
let config = (get-project-config)
let all = ($config.services.services? | default [])
# Filter to only managed services (daemon)
$all | where { |s| $s.id == "daemon" }
}
def get-service [id: string]: nothing -> record {
let services = (get-services-list)
let service = ($services | where id == $id | first)
if ($service | is-empty) {
error make { msg: $"Service not found: ($id)" }
}
$service
}
def get-startup-order []: nothing -> list {
let config = (get-project-config)
let explicit = ($config.services.startup_order? | default null)
if ($explicit | is-not-empty) {
$explicit
} else {
# Default order: daemon first, then db
["daemon", "db"]
}
}
def get-shutdown-order []: nothing -> list {
let config = (get-project-config)
let explicit = ($config.services.shutdown_order? | default null)
if ($explicit | is-not-empty) {
$explicit
} else {
# Reverse order: db first, then daemon
["db", "daemon"]
}
}
def daemon-pid-file []: nothing -> string {
$"($env.HOME)/.ontoref/daemon.pid"
}
def daemon-running? []: nothing -> bool {
let pid_file = (daemon-pid-file)
if not ($pid_file | path exists) { return false }
let pid = (open $pid_file | str trim)
let result = (do { ^kill -0 $pid } | complete)
$result.exit_code == 0
}
# -- Status -------------------------------------------------------------------
def "services" [action?: string, id?: string]: nothing -> nothing {
match ($action | default "") {
"" => { services overview },
"start" => { services start $id },
"stop" => { services stop $id },
"restart" => { services restart $id },
"status" => { services status $id },
"health" => { services health $id },
_ => { print $"Unknown action: ($action). Use: start, stop, restart, status, health" }
}
}
export def "main" [action?: string, id?: string]: nothing -> nothing {
services $action $id
}
export def "services overview" []: nothing -> nothing {
let config = (get-project-config)
let services = (get-services-list)
# Color codes (defined once)
let cyan = (ansi cyan)
let blue = (ansi blue)
let green = (ansi green)
let red = (ansi red)
let gray = (ansi dark_gray)
let yellow = (ansi yellow)
let reset = (ansi reset)
print ""
print ($cyan + " ontoref services (managed by ontoref)" + $reset)
print ($gray + " ----------------------------------------" + $reset)
print ""
# Show managed services (daemon)
for service in $services {
let status_text = if (daemon-running?) { "running" } else { "stopped" }
let status_icon = if ($status_text == "running") { "✓" } else { "✗" }
if $service.enabled {
let port = ($config.daemon.port? | default 7890)
let status_color = if ($status_text == "running") { $green } else { $red }
let line = $blue + " " + $service.id + $reset + " [" + $status_color + $status_icon + $reset + "] " + $status_text + " - port " + $yellow + ($port | into string) + $reset
print $line
} else {
let line = $blue + " " + $service.id + $reset + " [" + $gray + "-" + $reset + "] " + $gray + "disabled" + $reset
print $line
}
}
print ""
print ($cyan + " External services (monitored only)" + $reset)
print ($gray + " ----------------------------------------" + $reset)
print ""
# Show external services: DB and NATS
let db_config = ($config.services.services? | default [] | where id == "db" | first)
if ($db_config | is-not-empty) {
if ($db_config.enabled | default false) {
let db_url = ($config.db.url? | default "")
if ($db_url | is-not-empty) {
let line = $blue + " db" + $reset + " " + $gray + "[status check only]" + $reset + " " + $yellow + $db_url + $reset
print $line
} else {
let line = $blue + " db" + $reset + " " + $gray + "[disabled]" + $reset + " no URL configured"
print $line
}
} else {
let line = $blue + " db" + $reset + " [" + $gray + "-" + $reset + "] " + $gray + "disabled" + $reset
print $line
}
}
# NATS status
let nats_config = ($config.nats_events? | default { enabled: false, url: "" })
if ($nats_config.enabled | default false) {
let nats_url = ($nats_config.url? | default "nats://localhost:4222")
let line = $blue + " nats" + $reset + " " + $gray + "[event system]" + $reset + " " + $yellow + $nats_url + $reset
print $line
} else {
let line = $blue + " nats" + $reset + " [" + $gray + "-" + $reset + "] " + $gray + "disabled" + $reset
print $line
}
print ""
print ($gray + " Manage: ontoref services <start|stop|restart> daemon" + $reset)
print ($gray + " Monitor: ontoref services <status|health> [daemon|db]" + $reset)
print ($gray + " Events: strat nats <status|listen|emit>" + $reset)
print ""
}
# -- Lifecycle Management -----------------------------------------------------
export def "services start" [id?: string]: nothing -> nothing {
let requested = ($id | default "daemon")
match $requested {
"daemon" => {
if (daemon-running?) {
print " ✓ daemon already running"
} else {
daemon-start
}
},
"db" => { print " [ext] database is external - manage separately" },
_ => { print $" [warn] unknown service: ($requested)" }
}
}
export def "services stop" [id?: string]: nothing -> nothing {
let requested = ($id | default "daemon")
match $requested {
"daemon" => { daemon-stop },
"db" => { print " [ext] database must be stopped externally" },
_ => { print $" [warn] unknown service: ($requested)" }
}
}
export def "services restart" [id?: string]: nothing -> nothing {
let requested = ($id | default "daemon")
match $requested {
"daemon" => {
print " Restarting daemon..."
daemon-stop
sleep 500ms
daemon-start
},
"db" => { print " [ext] database must be restarted externally" },
_ => { print $" [warn] unknown service: ($requested)" }
}
}
export def "services status" [id?: string]: nothing -> nothing {
if ($id | is-not-empty) {
match $id {
"daemon" => {
print ""
if (daemon-running?) {
let pid_file = (daemon-pid-file)
let pid = (open $pid_file | str trim)
print $" daemon: running (PID $pid)"
} else {
print " daemon: stopped"
}
print ""
},
"db" => { print " database: check externally" },
_ => { print $" unknown service: ($id)" }
}
} else {
services overview
}
}
export def "services health" [id?: string]: nothing -> nothing {
if ($id | is-not-empty) {
match $id {
"daemon" => { daemon-health },
"db" => { db-health },
_ => { print $" unknown service: ($id)" }
}
} else {
let services = (get-services-list | get id)
for svc_id in $services {
match $svc_id {
"daemon" => { daemon-health },
"db" => { db-health },
_ => {}
}
}
}
}
# -- Built-in Service Handlers ------------------------------------------------
def daemon-start []: nothing -> nothing {
let root = ($env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT)
let pid_file = (daemon-pid-file)
print " Starting daemon..."
mkdir ($pid_file | path dirname)
do { \^ontoref-daemon --project-root $root --pid-file $pid_file } &
sleep 500ms
if (daemon-running?) {
print " ✓ daemon started"
} else {
print " ✗ failed to start daemon"
}
}
def daemon-stop []: nothing -> nothing {
if not (daemon-running?) {
print " ✓ daemon not running"
return
}
let pid_file = (daemon-pid-file)
let pid = (open $pid_file | str trim)
print " Stopping daemon..."
do { ^kill $pid } | complete
sleep 500ms
if not (daemon-running?) {
print " ✓ daemon stopped"
rm -f $pid_file
} else {
do { ^kill -9 $pid } | complete
rm -f $pid_file
}
}
def daemon-health []: nothing -> nothing {
if not (daemon-running?) {
print " ✗ daemon not running"
return
}
let url = (daemon-url)
let result = (do { ^curl -sf $"($url)/health" } | complete)
if $result.exit_code != 0 {
print " ✗ daemon health check failed"
return
}
let health = ($result.stdout | from json)
print $" ✓ daemon healthy (uptime: ($health.uptime_secs)s, cache: ($health.cache_entries) entries)"
}
def db-health []: nothing -> nothing {
let config = (get-project-config)
let db_config = ($config.services.services? | default [] | where id == "db" | first)
if ($db_config.config.url? | default "" | is-empty) {
print " ⚠ database URL not configured"
return
}
let url = $db_config.config.url
let result = (do { ^curl -sf $url } | complete)
if $result.exit_code == 0 {
print $" ✓ database healthy ($url)"
} else {
print $" ✗ database check failed ($url)"
}
}