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).
678 lines
30 KiB
Text
678 lines
30 KiB
Text
#!/usr/bin/env nu
|
||
# domains/provisioning/commands.nu — Provisioning domain CLI for ontoref.
|
||
# Dispatched by the ontoref bash wrapper when repo_kind in {DevWorkspace, Mixed}:
|
||
# ore provisioning <command> [args]
|
||
#
|
||
# Works for both DevWorkspace (workspace-level) and Mixed (platform-level) projects.
|
||
# Commands that require project-specific files (card.ncl, reflection/backlog.ncl)
|
||
# degrade gracefully when those files are absent.
|
||
|
||
def project-root [] {
|
||
$env.ONTOREF_PROJECT_ROOT? | default (pwd | path expand)
|
||
}
|
||
|
||
# ─── Loaders ──────────────────────────────────────────────────────────────────
|
||
|
||
def load-state [] { ^nickel export $"(project-root)/.ontology/state.ncl" | from json }
|
||
def load-core [] { ^nickel export $"(project-root)/.ontology/core.ncl" | from json }
|
||
def load-gate [] { ^nickel export $"(project-root)/.ontology/gate.ncl" | from json }
|
||
def load-connections [] { ^nickel export $"(project-root)/.ontology/connections.ncl" | from json }
|
||
def load-manifest [] { ^nickel export $"(project-root)/.ontology/manifest.ncl" | from json }
|
||
|
||
def load-card [] {
|
||
let path = $"(project-root)/card.ncl"
|
||
if ($path | path exists) { ^nickel export $path | from json } else { null }
|
||
}
|
||
|
||
def load-backlog [] {
|
||
let path = $"(project-root)/reflection/backlog.ncl"
|
||
if ($path | path exists) { ^nickel export $path | from json } else { { items: [] } }
|
||
}
|
||
|
||
# ─── state / next / validate ──────────────────────────────────────────────────
|
||
|
||
export def "state" [] {
|
||
load-state | get dimensions | each { |dim|
|
||
{ dimension: $dim.id, current: $dim.current_state, desired: $dim.desired_state, horizon: $dim.horizon }
|
||
}
|
||
}
|
||
|
||
export def "next" [] {
|
||
load-state | get dimensions | each { |dim|
|
||
let active = $dim.transitions | where from == $dim.current_state
|
||
if ($active | is-empty) {
|
||
{ dimension: $dim.id, from: $dim.current_state, to: "–", condition: "no active transition", blocker: "–", catalyst: "–" }
|
||
} else {
|
||
let tr = $active | first
|
||
{ dimension: $dim.id, from: $tr.from, to: $tr.to, condition: $tr.condition, blocker: $tr.blocker, catalyst: $tr.catalyst }
|
||
}
|
||
}
|
||
}
|
||
|
||
export def "validate" [decision: string] {
|
||
let core = load-core
|
||
let words = ($decision | str downcase | split row " " | where { |w| ($w | str length) > 3 })
|
||
let affected = ($core.nodes | each { |n|
|
||
let text = $"($n.id) ($n.name) ($n.description)" | str downcase
|
||
if ($words | any { |w| $text =~ $w }) { $n } else { null }
|
||
} | compact)
|
||
{
|
||
decision: $decision,
|
||
invariants_at_risk: ($affected | where invariant == true | select id level name),
|
||
tensions_touched: ($affected | where level == "Tension" | select id name),
|
||
practices_touched: ($affected | where level == "Practice" | select id name),
|
||
verdict: "manual review required — if any invariant is contradicted, justification is required",
|
||
}
|
||
}
|
||
|
||
# ─── connections / gates ──────────────────────────────────────────────────────
|
||
|
||
export def "connections" [] {
|
||
let c = load-connections
|
||
if ($c.upstream? | default [] | is-not-empty) {
|
||
print ""
|
||
print "UPSTREAM"
|
||
$c.upstream | each { |u|
|
||
print $" ($u.project) kind=($u.kind) via=($u.via)"
|
||
if ($u.node? | default "" | is-not-empty) { print $" node: ($u.node)" }
|
||
print $" ($u.note)"
|
||
}
|
||
}
|
||
if ($c.downstream? | default [] | is-not-empty) {
|
||
print ""
|
||
print "DOWNSTREAM"
|
||
$c.downstream | each { |d|
|
||
print $" ($d.project) kind=($d.kind) via=($d.via)"
|
||
if ($d.node? | default "" | is-not-empty) { print $" node: ($d.node)" }
|
||
print $" ($d.note)"
|
||
}
|
||
}
|
||
if ($c.peers? | default [] | is-not-empty) {
|
||
print ""
|
||
print "PEERS"
|
||
$c.peers | each { |p| print $" ($p.project) kind=($p.kind) via=($p.via)" }
|
||
}
|
||
}
|
||
|
||
export def "gates" [] {
|
||
load-gate | get membranes | each { |m|
|
||
{
|
||
membrane: $m.id,
|
||
name: $m.name,
|
||
active: $m.active,
|
||
permeability: $m.permeability,
|
||
condition: ($m.opening_condition.description? | default ""),
|
||
}
|
||
}
|
||
}
|
||
|
||
# ─── card (DevWorkspace only) ─────────────────────────────────────────────────
|
||
|
||
export def "card" [] {
|
||
let c = load-card
|
||
if ($c | is-empty) {
|
||
print "No card.ncl found — this command is only available for DevWorkspace projects."
|
||
return
|
||
}
|
||
print $"id: ($c.id)"
|
||
print $"name: ($c.name)"
|
||
print $"tagline: ($c.tagline)"
|
||
print $"status: ($c.status)"
|
||
print $"version: ($c.version)"
|
||
print $"started: ($c.started_at)"
|
||
if ($c.tags? | default [] | is-not-empty) { print $"tags: ($c.tags | str join ', ')" }
|
||
if ($c.tools? | default [] | is-not-empty) { print $"tools: ($c.tools | str join ', ')" }
|
||
if ($c.features? | default [] | is-not-empty) {
|
||
print ""
|
||
print "features:"
|
||
$c.features | each { |f| print $" - ($f)" }
|
||
}
|
||
}
|
||
|
||
# ─── capabilities (manifest.capabilities) ────────────────────────────────────
|
||
|
||
export def "capabilities" [] {
|
||
let caps = load-manifest | get capabilities? | default []
|
||
if ($caps | is-empty) { print "No capabilities declared in manifest."; return }
|
||
$caps | select id name summary
|
||
}
|
||
|
||
# ─── backlog (platform only) ──────────────────────────────────────────────────
|
||
|
||
export def "backlog" [--priority: string = ""] {
|
||
let items = load-backlog | get items
|
||
if ($items | is-empty) {
|
||
print "No backlog.ncl found — this command is only available for platform projects."
|
||
return
|
||
}
|
||
let filtered = if ($priority | is-empty) { $items } else { $items | where priority == $priority }
|
||
$filtered | each { |i|
|
||
{
|
||
id: $i.id,
|
||
priority: $i.priority,
|
||
blocked_by: ($i.blocked_by | str join ", "),
|
||
}
|
||
}
|
||
}
|
||
|
||
export def "backlog show" [id: string] {
|
||
let items = load-backlog | get items
|
||
if ($items | is-empty) { error make { msg: "No backlog.ncl found for this project." } }
|
||
let item = $items | where id == $id
|
||
if ($item | is-empty) { error make { msg: $"Backlog item not found: ($id)" } }
|
||
let r = $item | first
|
||
print $"id: ($r.id)"
|
||
print $"priority: ($r.priority)"
|
||
print ""
|
||
print "description:"
|
||
print $" ($r.description)"
|
||
if ($r.blocked_by? | default [] | is-not-empty) {
|
||
print ""
|
||
print $"blocked_by: ($r.blocked_by | str join ', ')"
|
||
}
|
||
if ($r.related_nodes? | default [] | is-not-empty) {
|
||
print $"nodes: ($r.related_nodes | str join ', ')"
|
||
}
|
||
}
|
||
|
||
# ─── install (Mixed/platform only) ───────────────────────────────────────────
|
||
|
||
def detect-platform []: nothing -> string {
|
||
if (do { ^which docker } | complete | get exit_code) == 0 { "docker" }
|
||
else if (do { ^which podman } | complete | get exit_code) == 0 { "podman" }
|
||
else if (do { ^which kubectl } | complete | get exit_code) == 0 { "kubernetes" }
|
||
else { "" }
|
||
}
|
||
|
||
export def "install" [
|
||
--mode: string = "solo" # Deployment mode: solo|multi-user|cicd|enterprise
|
||
--platform: string = "" # Container platform: docker|podman|kubernetes (auto-detected)
|
||
--dry-run # Print what would be run without executing
|
||
]: nothing -> nothing {
|
||
let root = (project-root)
|
||
|
||
let installer_bin = [$root, "platform", "target", "release", "provisioning-installer"] | path join
|
||
let installer_src = [$root, "platform", "installer"] | path join
|
||
let just_available = (do { ^which just } | complete | get exit_code) == 0
|
||
|
||
let resolved_platform = if ($platform | is-not-empty) { $platform } else { detect-platform }
|
||
|
||
print $"Platform install mode=($mode) platform=(if ($resolved_platform | is-empty) { '?' } else { $resolved_platform })"
|
||
print ""
|
||
|
||
if ($resolved_platform | is-empty) {
|
||
print "No supported container platform found (docker, podman, kubectl)."
|
||
print "Install one and retry."
|
||
return
|
||
}
|
||
|
||
if ($installer_bin | path exists) {
|
||
print $"installer binary: ($installer_bin)"
|
||
if $dry_run {
|
||
print $"[dry-run] would run: ($installer_bin) --headless --mode ($mode) --platform ($resolved_platform) --yes"
|
||
return
|
||
}
|
||
do { ^$installer_bin --headless --mode $mode --platform $resolved_platform --yes } | complete | ignore
|
||
return
|
||
}
|
||
|
||
if ($installer_src | path exists) {
|
||
print "installer binary not built — building from source..."
|
||
if $dry_run {
|
||
print $"[dry-run] would run: cargo build --release --manifest-path ($installer_src)/Cargo.toml"
|
||
return
|
||
}
|
||
let r = do { ^cargo build --release --manifest-path $"($installer_src)/Cargo.toml" } | complete
|
||
if $r.exit_code != 0 {
|
||
error make { msg: $"installer build failed:\n($r.stderr)" }
|
||
}
|
||
do { ^$installer_bin --headless --mode $mode --platform $resolved_platform --yes } | complete | ignore
|
||
return
|
||
}
|
||
|
||
if $just_available {
|
||
print "No installer binary or source found — delegating to: just install"
|
||
if $dry_run {
|
||
print $"[dry-run] would run: just install mode=($mode)"
|
||
return
|
||
}
|
||
let r = do { ^just install $"mode=($mode)" } | complete
|
||
if $r.exit_code != 0 {
|
||
error make { msg: $"just install failed:\n($r.stderr)" }
|
||
}
|
||
return
|
||
}
|
||
|
||
print "installer not found and 'just' is not available."
|
||
print $"Build the installer first: cargo build --release --manifest-path ($installer_src)/Cargo.toml"
|
||
}
|
||
|
||
# ─── ops (ADR-037: NATS ops contract) ────────────────────────────────────────
|
||
|
||
def workspace-id []: nothing -> string {
|
||
let card = load-card
|
||
if ($card | is-empty) { "unknown" } else { $card.id }
|
||
}
|
||
|
||
def nats-available []: nothing -> bool {
|
||
(do { ^which nats } | complete | get exit_code) == 0
|
||
}
|
||
|
||
def keeper-cli-available []: nothing -> bool {
|
||
(do { ^which keeper-cli } | complete | get exit_code) == 0
|
||
}
|
||
|
||
def rad-available []: nothing -> bool {
|
||
(do { ^which rad } | complete | get exit_code) == 0
|
||
}
|
||
|
||
export def "ops list" []: nothing -> nothing {
|
||
let ws = workspace-id
|
||
if not (nats-available) {
|
||
print "nats CLI not found — install nats-io/natscli"
|
||
return
|
||
}
|
||
let stream = $"OPS_PENDING_(($ws | str upcase))"
|
||
let r = do { ^nats stream info $stream --json } | complete
|
||
if $r.exit_code != 0 {
|
||
print $"Stream ($stream) not found or NATS unreachable: ($r.stderr | str trim)"
|
||
return
|
||
}
|
||
let info = $r.stdout | from json
|
||
print $"stream: ($stream)"
|
||
print $"messages: ($info.state.messages)"
|
||
print $"first_seq: ($info.state.first_seq)"
|
||
print $"last_seq: ($info.state.last_seq)"
|
||
}
|
||
|
||
export def "ops describe" [id: string]: nothing -> nothing {
|
||
let ws = workspace-id
|
||
if not (nats-available) { print "nats CLI not found"; return }
|
||
let subject = $"ops.pending.($ws).>"
|
||
let r = do { ^nats req $subject "" --count 50 --json } | complete
|
||
if $r.exit_code != 0 { print $"NATS request failed: ($r.stderr | str trim)"; return }
|
||
let msgs = $r.stdout | lines | each { |l| $l | from json | get data? | default {} }
|
||
let found = $msgs | where { |m| ($m.jti? | default "") == $id }
|
||
if ($found | is-empty) { print $"Op not found: ($id)"; return }
|
||
let op = $found | first
|
||
print $"jti: ($op.jti)"
|
||
print $"op_type: ($op.op_type? | default 'unknown')"
|
||
print $"target: ($op.target? | default 'unknown')"
|
||
print $"expected_version: ($op.expected_state_version? | default 'none')"
|
||
print $"scopes: ($op.scopes? | default [] | str join ', ')"
|
||
print $"issued_at: ($op.iat? | default 'unknown')"
|
||
}
|
||
|
||
export def "ops sign" [id: string]: nothing -> nothing {
|
||
if not (keeper-cli-available) { print "keeper-cli not found — build from platform/crates/ops-keeper"; return }
|
||
let ws = workspace-id
|
||
let r = do { ^keeper-cli --workspace $ws sign $id } | complete
|
||
if $r.exit_code != 0 {
|
||
error make { msg: $"keeper-cli sign failed: ($r.stderr | str trim)" }
|
||
}
|
||
print $r.stdout
|
||
}
|
||
|
||
export def "ops history" [--workspace: string = ""]: nothing -> nothing {
|
||
let ws = if ($workspace | is-empty) { workspace-id } else { $workspace }
|
||
let repo_path = $env.HOME | path join ".local/share/radicle/repos" $"state-($ws)"
|
||
if not ($repo_path | path exists) {
|
||
print $"Radicle state repo not found at ($repo_path)"
|
||
print "Run: rad clone rad:<rid> state-($ws)"
|
||
return
|
||
}
|
||
let r = do { ^git -C $repo_path log --oneline --no-decorate -20 } | complete
|
||
if $r.exit_code != 0 { print $"git log failed: ($r.stderr | str trim)"; return }
|
||
$r.stdout | lines | each { |l|
|
||
let parts = $l | split row " " | enumerate
|
||
let hash = $parts | where index == 0 | get item.0
|
||
let msg = $parts | skip 1 | get item | str join " "
|
||
{ commit: $hash, message: $msg }
|
||
}
|
||
}
|
||
|
||
# ─── playbook (extensions/playbooks/) ────────────────────────────────────────
|
||
|
||
def playbooks-root []: nothing -> string {
|
||
let root = project-root
|
||
[$root, "..", "..", "provisioning", "extensions", "playbooks"] | path join | path expand
|
||
}
|
||
|
||
export def "playbook list" []: nothing -> nothing {
|
||
let root = playbooks-root
|
||
if not ($root | path exists) { print "No extensions/playbooks/ directory found."; return }
|
||
^ls $root | where type == dir | get name | each { |d|
|
||
let playbook_path = [$d, "playbook.ncl"] | path join
|
||
let has_run = ([$d, "run.nu"] | path join | path exists)
|
||
{ name: ($d | path basename), has_run: $has_run, has_playbook: ($playbook_path | path exists) }
|
||
}
|
||
}
|
||
|
||
export def "playbook describe" [name: string]: nothing -> nothing {
|
||
let root = playbooks-root
|
||
let playbook_path = [$root, $name, "playbook.ncl"] | path join
|
||
if not ($playbook_path | path exists) {
|
||
print $"Playbook not found: ($name)"
|
||
print $"Expected at: ($playbook_path)"
|
||
return
|
||
}
|
||
let r = do { ^nickel export $playbook_path } | complete
|
||
if $r.exit_code != 0 { print $"Failed to load playbook: ($r.stderr | str trim)"; return }
|
||
let p = $r.stdout | from json
|
||
print $"name: ($p.name)"
|
||
print $"description: ($p.description)"
|
||
print $"version: ($p.version)"
|
||
print ""
|
||
print "preconditions:"
|
||
$p.preconditions? | default [] | each { |c| print $" - ($c)" }
|
||
print ""
|
||
print "steps:"
|
||
$p.steps? | default [] | enumerate | each { |s|
|
||
print $" ($s.index + 1). ($s.item.id): ($s.item.description)"
|
||
}
|
||
if ($p.rollback_strategy? | default "none") != "none" {
|
||
print $"\nrollback: ($p.rollback_strategy)"
|
||
}
|
||
}
|
||
|
||
export def "playbook run" [name: string, --dry-run]: nothing -> nothing {
|
||
let root = playbooks-root
|
||
let run_path = [$root, $name, "run.nu"] | path join
|
||
if not ($run_path | path exists) {
|
||
print $"run.nu not found for playbook: ($name)"
|
||
return
|
||
}
|
||
let ws = workspace-id
|
||
if $dry_run {
|
||
let dry_path = [$root, $name, "tests", "dry_run.nu"] | path join
|
||
if ($dry_path | path exists) {
|
||
let r = do { ^nu $dry_path --workspace $ws } | complete
|
||
if $r.exit_code != 0 { error make { msg: $"dry-run failed: ($r.stderr | str trim)" } }
|
||
print $r.stdout
|
||
} else {
|
||
print $"[dry-run] would execute: nu ($run_path) --workspace ($ws)"
|
||
print "No dry_run.nu found — simulating step listing only."
|
||
playbook describe $name
|
||
}
|
||
return
|
||
}
|
||
let r = do { ^nu $run_path --workspace $ws } | complete
|
||
if $r.exit_code != 0 {
|
||
error make { msg: $"playbook run failed: ($r.stderr | str trim)" }
|
||
}
|
||
print $r.stdout
|
||
}
|
||
|
||
export def "playbook history" []: nothing -> nothing {
|
||
let ws = workspace-id
|
||
let repo_path = $env.HOME | path join ".local/share/radicle/repos" $"state-($ws)"
|
||
if not ($repo_path | path exists) {
|
||
print $"Radicle state repo not found at ($repo_path)"
|
||
return
|
||
}
|
||
let r = do { ^git -C $repo_path log --oneline --no-decorate --grep="playbook:" -20 } | complete
|
||
if $r.exit_code != 0 { print $"git log failed: ($r.stderr | str trim)"; return }
|
||
if ($r.stdout | is-empty) { print "No playbook audit entries found."; return }
|
||
$r.stdout | lines | each { |l|
|
||
let parts = $l | split row " " | enumerate
|
||
let hash = $parts | where index == 0 | get item.0
|
||
let msg = $parts | skip 1 | get item | str join " "
|
||
{ commit: $hash, message: $msg }
|
||
}
|
||
}
|
||
|
||
# ─── keeper (ADR-037 dual-mode signing) ──────────────────────────────────────
|
||
|
||
export def "keeper status" []: nothing -> nothing {
|
||
let ws = workspace-id
|
||
if not (keeper-cli-available) { print "keeper-cli not found"; return }
|
||
let r = do { ^keeper-cli --workspace $ws status } | complete
|
||
if $r.exit_code != 0 {
|
||
print $"keeper-cli unavailable: ($r.stderr | str trim)"
|
||
return
|
||
}
|
||
print $r.stdout
|
||
}
|
||
|
||
export def "keeper policy" []: nothing -> nothing {
|
||
let ws = workspace-id
|
||
let policy_repo = $env.HOME | path join ".local/share/radicle/repos" $"policy-($ws)"
|
||
if not ($policy_repo | path exists) {
|
||
print $"Policy repo not cloned locally: policy-($ws)"
|
||
print "Run: rad clone rad:<rid> policy-($ws)"
|
||
return
|
||
}
|
||
let policy_path = [$policy_repo, "policy.ncl"] | path join
|
||
if not ($policy_path | path exists) { print "policy.ncl not found in policy repo"; return }
|
||
let r = do { ^nickel export $policy_path } | complete
|
||
if $r.exit_code != 0 { print $"Failed to load policy: ($r.stderr | str trim)"; return }
|
||
let p = $r.stdout | from json
|
||
print $"workspace: ($p.workspace)"
|
||
print $"version: ($p.version)"
|
||
print ""
|
||
print "auto_sign rules:"
|
||
$p.auto_sign? | default [] | each { |rule|
|
||
print $" op_type=($rule.op_type) target=($rule.target? | default '*') scope=($rule.scope? | default 'any')"
|
||
}
|
||
print ""
|
||
print "require_manual rules:"
|
||
$p.require_manual? | default [] | each { |rule|
|
||
print $" op_type=($rule.op_type) target=($rule.target? | default '*')"
|
||
}
|
||
}
|
||
|
||
export def "keeper switch" [mode: string]: nothing -> nothing {
|
||
let valid_modes = ["auto", "operator-only"]
|
||
if not ($valid_modes | any { |m| $m == $mode }) {
|
||
error make { msg: $"Invalid mode '($mode)'. Valid modes: ($valid_modes | str join ', ')" }
|
||
}
|
||
let playbook = if $mode == "auto" { "switch_to_vm_ops" } else { "switch_to_operator_only" }
|
||
print $"Delegating to playbook: ($playbook)"
|
||
playbook run $playbook
|
||
}
|
||
|
||
# ─── governance (ADR-038 Radicle delegation) ─────────────────────────────────
|
||
|
||
export def "governance delegations" []: nothing -> nothing {
|
||
let ws = workspace-id
|
||
let repos = ["policy", "desired", "state"] | each { |r| $"($r)-($ws)" }
|
||
$repos | each { |repo_name|
|
||
let r = do { ^rad id --repo $repo_name --json } | complete
|
||
if $r.exit_code != 0 {
|
||
{ repo: $repo_name, error: ($r.stderr | str trim), delegates: [] }
|
||
} else {
|
||
let info = $r.stdout | from json
|
||
{ repo: $repo_name, threshold: ($info.threshold? | default 1), delegates: ($info.delegates? | default []) }
|
||
}
|
||
}
|
||
}
|
||
|
||
export def "governance signers" []: nothing -> nothing {
|
||
let ws = workspace-id
|
||
if not (nats-available) { print "nats CLI not found"; return }
|
||
let r = do { ^nats auth account ls --json } | complete
|
||
if $r.exit_code != 0 {
|
||
print $"NATS auth query failed: ($r.stderr | str trim)"
|
||
return
|
||
}
|
||
let accounts = $r.stdout | from json
|
||
let ops_accounts = $accounts | where { |a| ($a.name? | default "") | str contains $ws }
|
||
if ($ops_accounts | is-empty) { print $"No NATS accounts found for workspace: ($ws)"; return }
|
||
$ops_accounts | each { |a|
|
||
{ account: $a.name, subject: ($a.subject? | default ""), signers: ($a.signing_keys? | default []) }
|
||
}
|
||
}
|
||
|
||
# ─── help + main ──────────────────────────────────────────────────────────────
|
||
|
||
# ─── registry (i sub-domain) ──────────────────────────────────────────────────
|
||
|
||
const DEFAULT_REGISTRY = "reg.librecloud.online"
|
||
|
||
def _find-capabilities-ncl []: nothing -> string {
|
||
let ws_path = ($env | get -o PROVISIONING_WORKSPACE_PATH | default "")
|
||
if ($ws_path | is-not-empty) {
|
||
let candidates = (do { glob $"($ws_path)/infra/*/capabilities.ncl" } | default [])
|
||
if ($candidates | is-not-empty) { return ($candidates | first) }
|
||
}
|
||
mut dir = $env.PWD
|
||
for _ in 0..6 {
|
||
let cur = $dir # immutable copy for closure capture in glob
|
||
let candidates = (do { glob $"($cur)/infra/*/capabilities.ncl" } | default [])
|
||
if ($candidates | is-not-empty) { return ($candidates | first) }
|
||
let parent = ($dir | path dirname)
|
||
if $parent == $dir { break }
|
||
$dir = $parent
|
||
}
|
||
""
|
||
}
|
||
|
||
export def "i resolve-registry" []: nothing -> string {
|
||
let from_env = ($env | get -o PROVISIONING_REGISTRY | default "")
|
||
if ($from_env | is-not-empty) { return $from_env }
|
||
let caps = (_find-capabilities-ncl)
|
||
if ($caps | is-not-empty) {
|
||
let ip = ($env.NICKEL_IMPORT_PATH? | default ($env.PROVISIONING? | default ""))
|
||
let ncl = (do { ^nickel export --import-path $ip $caps } | complete)
|
||
if $ncl.exit_code == 0 {
|
||
let data = ($ncl.stdout | from json)
|
||
let reg_default = ($data | get -o provides.registries.default | default "")
|
||
let registries = ($data | get -o provides.registries.registries | default [])
|
||
if ($reg_default | is-not-empty) and ($registries | is-not-empty) {
|
||
let entry = ($registries | where { |e| $e.id == $reg_default } | first | default null)
|
||
if ($entry != null) and ($entry.endpoint? | default "" | is-not-empty) {
|
||
return $entry.endpoint
|
||
}
|
||
}
|
||
}
|
||
}
|
||
$DEFAULT_REGISTRY
|
||
}
|
||
|
||
export def "i list" [--live]: nothing -> nothing {
|
||
let reg = (i resolve-registry)
|
||
if not $live {
|
||
print $"registry : ($reg)"
|
||
print "Use --live to query the live catalog."
|
||
return
|
||
}
|
||
let url = $"https://($reg)/v2/_catalog"
|
||
let body = try { http get $url } catch {
|
||
print $"Registry catalog unavailable at ($url)"
|
||
return
|
||
}
|
||
let repos = ($body | get -o repositories | default [])
|
||
|
||
let domains = ($repos | where { |r| $r | str starts-with "domains/" } | each { |r|
|
||
let parts = ($r | str replace "domains/" "" | split row "/")
|
||
{ kind: "domain", participant: ($parts | first), id: ($parts | skip 1 | str join "/"), ref: $"($reg)/($r)" }
|
||
})
|
||
let modes = ($repos | where { |r| $r | str starts-with "modes/" } | each { |r|
|
||
let parts = ($r | str replace "modes/" "" | split row "/")
|
||
{ kind: "mode", participant: ($parts | first), id: ($parts | skip 1 | str join "/"), ref: $"($reg)/($r)" }
|
||
})
|
||
|
||
let all = ($domains | append $modes)
|
||
if ($all | is-empty) {
|
||
print $"No domains/ or modes/ artifacts found in ($reg)"
|
||
return
|
||
}
|
||
print $"registry: ($reg) artifacts: ($all | length)"
|
||
print ""
|
||
$all | each { |a|
|
||
print $" ($a.kind) ($a.participant)/($a.id) → ($a.ref)"
|
||
}
|
||
null
|
||
}
|
||
|
||
def show-help [] {
|
||
print "Provisioning — ontoref domain extension"
|
||
print ""
|
||
print "USAGE"
|
||
print " ore provisioning <command> [args]"
|
||
print ""
|
||
print "COMMANDS (all repo_kinds)"
|
||
print " state Current FSM position across all dimensions"
|
||
print " next Next valid transitions with blockers/catalysts"
|
||
print " validate <decision> Check against ontological invariants"
|
||
print " connections Upstream/downstream project dependency graph"
|
||
print " gates Membrane status and opening conditions"
|
||
print " capabilities Platform capabilities from manifest"
|
||
print ""
|
||
print "COMMANDS (DevWorkspace only)"
|
||
print " card Workspace card: identity, clusters, status"
|
||
print ""
|
||
print "COMMANDS (Mixed/platform only)"
|
||
print " backlog [--priority] Backlog items filtered by High|Medium|Low"
|
||
print " backlog show <id> Full detail of a backlog item"
|
||
print " install [--mode] [--platform] Install provisioning platform services"
|
||
print ""
|
||
print "COMMANDS (DevWorkspace — ops contract, ADR-037)"
|
||
print " ops list Pending ops queue depth for this workspace"
|
||
print " ops describe <id> Full op detail: JWT claims, scopes, expected_version"
|
||
print " ops sign <id> Operator signs a pending op via keeper-cli"
|
||
print " ops history [<workspace>] Applied ops from <workspace>-state Radicle ledger"
|
||
print ""
|
||
print "COMMANDS (DevWorkspace — playbooks)"
|
||
print " playbook list Available playbooks for this workspace"
|
||
print " playbook describe <name> Steps, params, preconditions of a playbook"
|
||
print " playbook run <name> Execute a playbook (use --dry-run first)"
|
||
print " playbook history Past playbook executions from audit ledger"
|
||
print ""
|
||
print "COMMANDS (DevWorkspace — keeper daemon, ADR-037)"
|
||
print " keeper status Keeper mode (auto|operator-only|down) + policy version"
|
||
print " keeper policy Active keeper policy from policy-<workspace> Radicle repo"
|
||
print " keeper switch <mode> Switch keeper mode (delegates to switch_to_<mode> playbook)"
|
||
print ""
|
||
print "COMMANDS (DevWorkspace — governance, ADR-038)"
|
||
print " governance delegations Radicle delegation sets for policy/desired/state repos"
|
||
print " governance signers Active NATS JWT signers and M-of-N quorum status"
|
||
print ""
|
||
print "COMMANDS (all repo_kinds — registry)"
|
||
print " i resolve-registry Resolved OCI registry endpoint (env → capabilities.ncl → default)"
|
||
print " i list Show resolved registry endpoint"
|
||
print " i list --live Query _catalog and list live domains/ and modes/ artifacts"
|
||
}
|
||
|
||
def main [
|
||
...args: string
|
||
--priority: string = ""
|
||
--mode: string = "solo"
|
||
--platform: string = ""
|
||
--dry-run
|
||
--live
|
||
] {
|
||
if ($args | is-empty) or ($args | first) == "help" { show-help; return }
|
||
let sub = ($args | str join " ")
|
||
match $sub {
|
||
"state" => { state }
|
||
"next" => { next }
|
||
"connections" => { connections }
|
||
"gates" => { gates }
|
||
"card" => { card }
|
||
"capabilities" => { capabilities }
|
||
"install" => { if $dry_run { install --mode $mode --platform $platform --dry-run } else { install --mode $mode --platform $platform } }
|
||
"backlog" => { backlog --priority $priority }
|
||
"ops list" => { ops list }
|
||
"ops history" => { ops history }
|
||
"keeper status" => { keeper status }
|
||
"keeper policy" => { keeper policy }
|
||
"governance delegations" => { governance delegations }
|
||
"governance signers" => { governance signers }
|
||
"playbook list" => { playbook list }
|
||
"playbook history" => { playbook history }
|
||
_ if ($sub | str starts-with "backlog show ") => { backlog show ($args | get 2) }
|
||
_ if ($sub | str starts-with "validate ") => { validate ($args | skip 1 | str join " ") }
|
||
_ if ($sub | str starts-with "ops describe ") => { ops describe ($args | get 2) }
|
||
_ if ($sub | str starts-with "ops sign ") => { ops sign ($args | get 2) }
|
||
_ if ($sub | str starts-with "playbook describe ") => { playbook describe ($args | get 2) }
|
||
_ if ($sub | str starts-with "playbook run ") => {
|
||
if $dry_run { playbook run ($args | get 2) --dry-run } else { playbook run ($args | get 2) }
|
||
}
|
||
_ if ($sub | str starts-with "keeper switch ") => { keeper switch ($args | get 2) }
|
||
_ if ($sub | str starts-with "ops history ") => { ops history --workspace ($args | get 2) }
|
||
"i resolve-registry" => { i resolve-registry | print }
|
||
"i list" => { if $live { i list --live } else { i list } }
|
||
_ => { print $"Unknown command: ($sub)"; show-help }
|
||
}
|
||
}
|