provisioning-core/nulib/cli/integration.nu

1198 lines
51 KiB
Text
Raw Normal View History

#!/usr/bin/env nu
# prvng integration — OCI domain artifact management for federated integration modes.
# Ruta β: this surface lives entirely in provisioning per ADR-042.
#
# Subcommand tree:
# integration domain publish <path> [--ref <ref>] [--registry <reg>]
# integration domain pull <id> <version> [--registry <reg>]
# integration domain describe <id> <version> [--registry <reg>]
# integration domain verify <id> <version> [--registry <reg>]
# integration domain diff <id>:<v1> <id>:<v2>
# integration ecosystem domains [--registry <reg>]
export-env {
let lib_dirs_raw = ($env.NU_LIB_DIRS? | default "")
let current_lib_dirs = if ($lib_dirs_raw | type) == "string" {
if ($lib_dirs_raw | is-empty) { [] } else { ($lib_dirs_raw | split row ":") }
} else {
$lib_dirs_raw
}
let dynamic = ($env.PROVISIONING? | default "" | path join "core" "nulib")
$env.NU_LIB_DIRS = ([
"/opt/provisioning/core/nulib"
"/usr/local/provisioning/core/nulib"
] | append $current_lib_dirs | append (if ($dynamic | is-not-empty) { [$dynamic] } else { [] }))
}
use platform/oci/domain_client.nu [
domain-push
domain-pull
domain-describe
domain-verify
ecosystem-list-domains
ecosystem-list-modes
resolve-registry
mode-push
mode-pull
]
use platform/integrations/context_assembler.nu [
assemble-context
build-secret-delivery-context
]
def int-err [msg: string] {
error make --unspanned { msg: $msg }
}
# Return the value after --flag in args, or "" when absent.
def parse-flag [flag: string, args: list<string>]: nothing -> string {
let hits = ($args | enumerate | where { |e| $e.item == $flag })
if ($hits | is-empty) {
return ""
}
let next = ($hits | first | get index) + 1
if $next < ($args | length) { $args | get $next } else { "" }
}
def print-check [label: string, pass: bool]: nothing -> nothing {
let icon = if $pass { "✓" } else { "✗" }
let color = if $pass { "green_bold" } else { "red_bold" }
print $" ($icon | ansi $color) ($label)"
}
# ── domain subcommands ────────────────────────────────────────────────────────
def resolve-workspace-participant []: nothing -> string {
let manifest = ($env.PWD | path join ".ontology" "manifest.ncl")
if not ($manifest | path exists) { return "" }
let ip = ($env.NICKEL_IMPORT_PATH? | default ($env.PROVISIONING? | default ""))
let ncl = (do { ^nickel export --import-path $ip $manifest } | complete)
if $ncl.exit_code != 0 { return "" }
$ncl.stdout | from json | get -o registry_provides.participant | default ""
}
def cmd-domain-publish [args: list<string>]: nothing -> nothing {
let path = $args | get 0? | default ""
if ($path | is-empty) {
int-err "Usage: integration domain publish <path> <participant> [--ref <ref>] [--registry <registry>]"
}
let flag_ref = (parse-flag "--ref" $args)
let flag_registry = (parse-flag "--registry" $args)
let flag_participant = (parse-flag "--participant" $args)
# participant: explicit flag → second positional arg → workspace manifest → error
let participant = if ($flag_participant | is-not-empty) {
$flag_participant
} else {
let pos = $args | get 1? | default ""
if ($pos | is-not-empty) and not ($pos | str starts-with "-") {
$pos
} else {
let ws = (resolve-workspace-participant)
if ($ws | is-not-empty) { $ws } else {
int-err "participant required: pass as second arg or --participant <id> (or declare registry_provides in .ontology/manifest.ncl)"
""
}
}
}
let result = if ($flag_ref | is-not-empty) {
if ($flag_registry | is-not-empty) {
domain-push $path $participant --ref $flag_ref --registry $flag_registry
} else {
domain-push $path $participant --ref $flag_ref
}
} else {
if ($flag_registry | is-not-empty) {
domain-push $path $participant --registry $flag_registry
} else {
domain-push $path $participant
}
}
print $"Pushed ($result.ref)"
print $"Digest: ($result.digest)"
}
# Split "<participant>/<id>" into {participant, id}. Errors when no "/" present.
def split-domain-ref [ref: string]: nothing -> record {
let parts = ($ref | split row "/" | filter { |p| $p | is-not-empty })
if ($parts | length) < 2 {
error make --unspanned { msg: $"Expected <participant>/<id>, got: ($ref)" }
}
{ participant: ($parts | first), id: ($parts | skip 1 | str join "/") }
}
def cmd-domain-pull [args: list<string>]: nothing -> nothing {
let ref_arg = $args | get 0? | default ""
let version = $args | get 1? | default ""
if ($ref_arg | is-empty) or ($version | is-empty) {
int-err "Usage: integration domain pull <participant>/<id> <version> [--registry <registry>]"
}
let parts = (split-domain-ref $ref_arg)
let flag_registry = (parse-flag "--registry" $args)
let cache = if ($flag_registry | is-not-empty) {
domain-pull $parts.participant $parts.id $version --registry $flag_registry
} else {
domain-pull $parts.participant $parts.id $version
}
print $"Pulled ($ref_arg):($version) → ($cache)"
}
def cmd-domain-describe [args: list<string>]: nothing -> nothing {
let ref_arg = $args | get 0? | default ""
let version = $args | get 1? | default ""
if ($ref_arg | is-empty) or ($version | is-empty) {
int-err "Usage: integration domain describe <participant>/<id> <version>"
}
let parts = (split-domain-ref $ref_arg)
let info = (domain-describe $parts.participant $parts.id $version)
print $"Ref: ($info.ref)"
print $"ArtifactType: ($info.artifact_type)"
print $"Digest: ($info.digest)"
print "Layers:"
for layer in $info.layers {
let mt = $layer | get -o mediaType | default ""
let dg = $layer | get -o digest | default ""
let dg_short = if ($dg | str length) > 20 { $dg | str substring 0..20 } else { $dg }
print $" ($mt) ($dg_short)..."
}
}
def cmd-domain-verify [args: list<string>]: nothing -> nothing {
let ref_arg = $args | get 0? | default ""
let version = $args | get 1? | default ""
let cosign_key = (parse-flag "--cosign-key" $args)
if ($ref_arg | is-empty) or ($version | is-empty) {
int-err "Usage: integration domain verify <participant>/<id> <version> [--cosign-key <path>]"
}
let parts = (split-domain-ref $ref_arg)
let resolved_key = if ($cosign_key | is-not-empty) { $cosign_key } else { resolve-cosign-pubkey }
let result = if ($resolved_key | is-not-empty) {
domain-verify $parts.participant $parts.id $version --cosign-key $resolved_key
} else {
domain-verify $parts.participant $parts.id $version
}
print $"Verifying ($result.ref)..."
print-check "artifact-type" $result.checks.artifact_type
print-check "contract.ncl present" $result.checks.contract_present
print-check "contract.ncl typechecks" $result.checks.contract_typechecks
print-check "digest consistent" $result.checks.digest_ok
print-check "cosign signature" $result.checks.cosign_signature
if ($result.cosign_key | is-empty) {
print " (cosign key not configured — signature check skipped)"
}
if not $result.pass {
print ""
for e in $result.errors { print $" Error: ($e)" }
exit 1
}
print ""
print $"(ansi green_bold)OK(ansi reset)"
}
# Parse <participant>/<id>:<version> into its parts.
def split-domain-versioned-ref [ref: string]: nothing -> record {
let head_tail = ($ref | split row ":")
let head = ($head_tail | get 0? | default "")
let version = ($head_tail | get 1? | default "")
let pid = ($head | split row "/")
{
participant: ($pid | get 0? | default ""),
id: ($pid | skip 1 | str join "/"),
version: $version,
}
}
def cmd-domain-diff [args: list<string>]: nothing -> nothing {
let a = $args | get 0? | default ""
let b = $args | get 1? | default ""
if ($a | is-empty) or ($b | is-empty) {
int-err "Usage: integration domain diff <participant>/<id>:<v1> <participant>/<id>:<v2>"
}
let pa = (split-domain-versioned-ref $a)
let pb = (split-domain-versioned-ref $b)
if ($pa.participant | is-empty) or ($pa.id | is-empty) or ($pa.version | is-empty) {
int-err $"Malformed ref: ($a) — expected <participant>/<id>:<version>"
}
if ($pb.participant | is-empty) or ($pb.id | is-empty) or ($pb.version | is-empty) {
int-err $"Malformed ref: ($b) — expected <participant>/<id>:<version>"
}
let flag_registry = (parse-flag "--registry" $args)
let cache_a = if ($flag_registry | is-not-empty) {
domain-pull $pa.participant $pa.id $pa.version --registry $flag_registry
} else {
domain-pull $pa.participant $pa.id $pa.version
}
let cache_b = if ($flag_registry | is-not-empty) {
domain-pull $pb.participant $pb.id $pb.version --registry $flag_registry
} else {
domain-pull $pb.participant $pb.id $pb.version
}
let info_a = if ($flag_registry | is-not-empty) {
domain-describe $pa.participant $pa.id $pa.version --registry $flag_registry
} else {
domain-describe $pa.participant $pa.id $pa.version
}
let info_b = if ($flag_registry | is-not-empty) {
domain-describe $pb.participant $pb.id $pb.version --registry $flag_registry
} else {
domain-describe $pb.participant $pb.id $pb.version
}
print ""
print $"(ansi white_bold)Domain diff(ansi reset) ($a) ↔ ($b)"
print ""
# 1. Identity
let same_identity = ($pa.participant == $pb.participant) and ($pa.id == $pb.id)
if not $same_identity {
let id_a = $"($pa.participant)/($pa.id)"
let id_b = $"($pb.participant)/($pb.id)"
print $" (ansi yellow)note(ansi reset) comparing across different domain identities — A=($id_a) B=($id_b)"
print ""
}
# 2. Digest
let same_digest = ($info_a.raw == $info_b.raw)
if $same_digest {
print $" (ansi green)=(ansi reset) manifest identical (no structural change)"
return
}
print $" (ansi cyan)≠(ansi reset) manifests differ"
print $" ($a): digest ($info_a.digest)"
print $" ($b): digest ($info_b.digest)"
print ""
# 3. Artifact type
if $info_a.artifact_type != $info_b.artifact_type {
print $" (ansi red)Δ artifactType(ansi reset) ($info_a.artifact_type) → ($info_b.artifact_type)"
}
# 4. Layer set comparison (mediaType + size + digest by layer index)
let layers_a = ($info_a.layers | each { |l| { mt: ($l.mediaType? | default ""), size: ($l.size? | default 0), digest: ($l.digest? | default "") } })
let layers_b = ($info_b.layers | each { |l| { mt: ($l.mediaType? | default ""), size: ($l.size? | default 0), digest: ($l.digest? | default "") } })
print ""
print " layers:"
print $" A: ($layers_a | length) B: ($layers_b | length)"
let max_layers = (if (($layers_a | length) > ($layers_b | length)) { $layers_a | length } else { $layers_b | length })
for i in 0..($max_layers - 1) {
let la = ($layers_a | get -i $i | default null)
let lb = ($layers_b | get -i $i | default null)
if $la == null {
print $" (ansi green)+ #($i)(ansi reset) ($lb.mt) ($lb.digest)"
} else if $lb == null {
print $" (ansi red)- #($i)(ansi reset) ($la.mt) ($la.digest)"
} else if $la.digest == $lb.digest {
print $" (ansi dark_gray)= #($i)(ansi reset) ($la.mt) unchanged"
} else {
print $" (ansi cyan)Δ #($i)(ansi reset) ($la.mt)"
print $" A: ($la.digest)"
print $" B: ($lb.digest)"
}
}
# 5. Contract content diff (text — line-level, system 'diff' if available)
let contract_a = ($cache_a | path join "contract.ncl")
let contract_b = ($cache_b | path join "contract.ncl")
if ($contract_a | path exists) and ($contract_b | path exists) {
print ""
print " contract.ncl:"
let diff_bin = (which diff)
if ($diff_bin | is-empty) {
let same = ((open --raw $contract_a) == (open --raw $contract_b))
if $same {
print " (ansi green)= identical(ansi reset)"
} else {
print " (ansi cyan)≠ differs(ansi reset) (install diff for line-level output)"
}
} else {
let r = (do { ^diff -u $contract_a $contract_b } | complete)
if $r.exit_code == 0 {
print " (ansi green)= identical(ansi reset)"
} else {
# Indent diff output for visual nesting
let lines = ($r.stdout | lines)
if ($lines | is-empty) {
print " (ansi cyan)≠ differs(ansi reset) (no readable diff)"
} else {
for ln in $lines {
print $" ($ln)"
}
}
}
}
}
print ""
}
def cmd-domain [args: list<string>]: nothing -> nothing {
let sub = $args | get 0? | default ""
let rest = if ($args | length) > 1 { $args | skip 1 } else { [] }
match $sub {
"publish" => { cmd-domain-publish $rest }
"pull" => { cmd-domain-pull $rest }
"describe" | "desc" => { cmd-domain-describe $rest }
"verify" => { cmd-domain-verify $rest }
"diff" => { cmd-domain-diff $rest }
_ => {
print "integration domain <subcommand>"
print ""
print "Subcommands:"
print " publish <path> Push domain artifact to registry"
print " pull <participant>/<id> <version> Pull domain artifact to local cache"
print " describe <participant>/<id> <version> Show OCI manifest details"
print " verify <participant>/<id> <version> Verify artifact integrity and contract"
print " diff <participant>/<id>:<v1> <participant>/<id>:<v2> Structural diff between two versions"
}
}
}
# ── mode subcommand ───────────────────────────────────────────────────────────
def cmd-mode-publish [args: list<string>]: nothing -> nothing {
let path = $args | get 0? | default ""
let participant = $args | get 1? | default ""
let mode_id = $args | get 2? | default ""
let version = $args | get 3? | default ""
let flag_registry = (parse-flag "--registry" $args)
if ($path | is-empty) or ($participant | is-empty) or ($mode_id | is-empty) or ($version | is-empty) {
int-err "Usage: integration mode publish <path> <participant> <mode-id> <version> [--registry <reg>]"
}
let result = if ($flag_registry | is-not-empty) {
mode-push $path $participant $mode_id $version --registry $flag_registry
} else {
mode-push $path $participant $mode_id $version
}
print $"Published mode artifact:"
print $" ref: ($result.ref)"
print $" digest: ($result.digest)"
let cosign_key = (resolve-cosign-pubkey)
if ($cosign_key | is-not-empty) {
let priv_key = ($env | get -o "COSIGN_KEY_PATH" | default "")
if ($priv_key | is-not-empty) {
print $"Signing ($result.ref) with ($cosign_key)..."
let image_ref = ($result.ref | str replace --regex ':.*$' '' | $"($in)@($result.digest)")
let sign = (do { COSIGN_PASSWORD="" ^cosign sign --key $priv_key --yes $image_ref } | complete)
if $sign.exit_code == 0 {
print $" (ansi green_bold)✓(ansi reset) signed"
} else {
let sign_err = ($sign.stderr | str trim | lines | last | default "sign failed")
print $" (ansi yellow_bold)⚠(ansi reset) signing failed: ($sign_err)"
}
} else {
print $" (ansi yellow_bold)⚠(ansi reset) COSIGN_KEY_PATH not set — artifact not signed"
}
}
}
# Parse <participant>/<mode-id> into a record. Caller validates mode_id present.
def split-mode-ref [ref: string]: nothing -> record {
let parts = ($ref | split row "/")
let participant = ($parts | first | default "")
let mode_id = ($parts | skip 1 | str join "/")
{ participant: $participant, mode_id: $mode_id }
}
def cmd-mode-pull [args: list<string>]: nothing -> nothing {
let pm = $args | get 0? | default ""
let version = $args | get 1? | default ""
if ($pm | is-empty) or ($version | is-empty) {
int-err "Usage: integration mode pull <participant>/<mode-id> <version> [--registry <reg>]"
}
let parts = (split-mode-ref $pm)
if ($parts.mode_id | is-empty) {
int-err "Mode ref must be <participant>/<mode-id>"
}
let flag_registry = (parse-flag "--registry" $args)
let cache = if ($flag_registry | is-not-empty) {
mode-pull $parts.participant $parts.mode_id $version --registry $flag_registry
} else {
mode-pull $parts.participant $parts.mode_id $version
}
print $"Mode artifact pulled to: ($cache)"
}
def cmd-mode-describe [args: list<string>]: nothing -> nothing {
let pm = $args | get 0? | default ""
let version = $args | get 1? | default ""
if ($pm | is-empty) or ($version | is-empty) {
int-err "Usage: integration mode describe <participant>/<mode-id> <version> [--registry <reg>]"
}
let parts = (split-mode-ref $pm)
if ($parts.mode_id | is-empty) {
int-err "Mode ref must be <participant>/<mode-id>"
}
let flag_registry = (parse-flag "--registry" $args)
let cache = if ($flag_registry | is-not-empty) {
mode-pull $parts.participant $parts.mode_id $version --registry $flag_registry
} else {
mode-pull $parts.participant $parts.mode_id $version
}
let decl_path = ($cache | path join "provisioning.ncl")
if not ($decl_path | path exists) {
int-err $"provisioning.ncl absent in pulled artifact at ($cache)"
}
let import_path = ($env.NICKEL_IMPORT_PATH? | default ($env.PROVISIONING? | default ""))
let ncl = (do { ^nickel export --import-path $import_path $decl_path } | complete)
if $ncl.exit_code != 0 {
int-err $"Failed to evaluate provisioning.ncl: ($ncl.stderr)"
}
let mode = ($ncl.stdout | from json)
print ""
print $"(ansi white_bold)Mode(ansi reset) ($parts.participant)/($parts.mode_id):($version)"
print $"(ansi dark_gray)id(ansi reset) ($mode | get -o id | default '(unknown)')"
print $"(ansi dark_gray)participant(ansi reset) ($mode | get -o participant | default $parts.participant)"
print $"(ansi dark_gray)direction(ansi reset) ($mode | get -o direction | default '(unspecified)')"
print $"(ansi dark_gray)trigger(ansi reset) ($mode | get -o trigger | default '(unspecified)')"
let domains_used = ($mode | get -o domains_used | default [])
print $"(ansi dark_gray)domains_used(ansi reset) ($domains_used | length) entries"
let steps = ($mode | get -o steps | default [])
print $"(ansi dark_gray)steps(ansi reset) ($steps | length) defined"
print ""
}
def cmd-mode-verify [args: list<string>]: nothing -> nothing {
let pm = $args | get 0? | default ""
let version = $args | get 1? | default ""
if ($pm | is-empty) or ($version | is-empty) {
int-err "Usage: integration mode verify <participant>/<mode-id> <version> [--registry <reg>]"
}
let parts = (split-mode-ref $pm)
if ($parts.mode_id | is-empty) {
int-err "Mode ref must be <participant>/<mode-id>"
}
let flag_registry = (parse-flag "--registry" $args)
let cache = if ($flag_registry | is-not-empty) {
mode-pull $parts.participant $parts.mode_id $version --registry $flag_registry
} else {
mode-pull $parts.participant $parts.mode_id $version
}
mut errors = []
let decl_path = ($cache | path join "provisioning.ncl")
let lock_path = ($cache | path join "domains.lock.ncl")
let decl_present = ($decl_path | path exists)
let lock_present = ($lock_path | path exists)
if not $decl_present { $errors = ($errors | append "provisioning.ncl missing") }
if not $lock_present { $errors = ($errors | append "domains.lock.ncl missing") }
let import_path = ($env.NICKEL_IMPORT_PATH? | default ($env.PROVISIONING? | default ""))
mut decl_typecheck_ok = false
if $decl_present {
let r = (do { ^nickel typecheck --import-path $import_path $decl_path } | complete)
if $r.exit_code == 0 { $decl_typecheck_ok = true } else {
$errors = ($errors | append $"provisioning.ncl typecheck failed: ($r.stderr | str trim)")
}
}
let manifest_path = ($cache | path join "oci-manifest.json")
let artifact_type_ok = if ($manifest_path | path exists) {
let m = (open --raw $manifest_path | from json)
let at = ($m | get -o artifactType | default "")
$at == "application/vnd.ontoref.mode.v1"
} else { false }
if not $artifact_type_ok { $errors = ($errors | append "artifactType != application/vnd.ontoref.mode.v1") }
let pass = $decl_present and $lock_present and $decl_typecheck_ok and $artifact_type_ok
print ""
print-check "provisioning.ncl present" $decl_present
print-check "domains.lock.ncl present" $lock_present
print-check "provisioning.ncl typechecks" $decl_typecheck_ok
print-check "artifactType vnd.ontoref.mode.v1" $artifact_type_ok
print ""
if $pass {
print $"(ansi green_bold)OK(ansi reset)"
} else {
for e in $errors { print $" (ansi red)·(ansi reset) ($e)" }
exit 1
}
}
def cmd-mode [args: list<string>]: nothing -> nothing {
let sub = $args | get 0? | default ""
let rest = if ($args | length) > 1 { $args | skip 1 } else { [] }
match $sub {
"publish" => { cmd-mode-publish $rest }
"pull" => { cmd-mode-pull $rest }
"describe" | "desc" => { cmd-mode-describe $rest }
"verify" => { cmd-mode-verify $rest }
_ => {
print "integration mode <subcommand>"
print ""
print "Subcommands:"
print " publish <path> <participant> <mode-id> <version> Push mode artifact"
print " pull <participant>/<mode-id> <version> Pull mode artifact to local cache"
print " describe <participant>/<mode-id> <version> Show mode metadata + steps"
print " verify <participant>/<mode-id> <version> Verify integrity + typecheck"
}
}
}
# ── invoke subcommand ────────────────────────────────────────────────────────
def cmd-invoke [args: list<string>]: nothing -> nothing {
let mode_id = $args | get 0? | default ""
if ($mode_id | is-empty) {
int-err "Usage: integration invoke <mode-id> --binary <name> [--workspace-root <path>] [--ncl-import <path>] [--dry-run]"
}
let flag_binary = (parse-flag "--binary" $args)
let flag_ws_root = (parse-flag "--workspace-root" $args)
let flag_ncl_import = (parse-flag "--ncl-import" $args)
let dry_run = ($args | any { |a| $a == "--dry-run" })
let binary = if ($flag_binary | is-not-empty) { $flag_binary } else {
# Derive participant name from mode-id convention: strip trailing "-<noun>"
$mode_id | split row "-" | drop | str join "-"
}
let ws_root = if ($flag_ws_root | is-not-empty) { $flag_ws_root } else { $env.PWD }
# Cabling file convention: infra/<workspace>/integrations/<mode-id>.ncl
# Look relative to workspace root; fall back to scanning infra/
let direct = ($ws_root | path join "integrations" $"($mode_id).ncl")
let cabling_path = if ($direct | path exists) {
$direct
} else {
let infra = ($ws_root | path join "infra")
if ($infra | path exists) {
let candidates = (
ls $infra
| where type == "dir"
| get name
| each { |d| $d | path join "integrations" $"($mode_id).ncl" }
| where { |p| $p | path exists }
)
if ($candidates | is-empty) {
error make --unspanned { msg: $"No cabling file found for mode ($mode_id) under ($ws_root)" }
}
$candidates | first
} else {
error make --unspanned { msg: $"No cabling file found for mode ($mode_id) under ($ws_root)" }
}
}
print $"Assembling context for ($mode_id) from ($cabling_path)..."
let ctx = if ($flag_ncl_import | is-not-empty) {
assemble-context $cabling_path --workspace-root $ws_root --ncl-import $flag_ncl_import
} else {
assemble-context $cabling_path --workspace-root $ws_root
}
let envelope = (build-secret-delivery-context $ctx)
if $dry_run {
print "-- dry-run: context envelope (secrets redacted) --"
let parsed = ($envelope | from json)
let redacted = ($parsed | update secrets {
|r| $r.secrets | transpose key entry | reduce --fold {} { |row, acc|
$acc | upsert $row.key { value: "<redacted>", source: $row.entry.source }
}
})
print ($redacted | to json --indent 2)
return
}
print $"Invoking ($binary)..."
let result = (do { $envelope | ^$binary } | complete)
if $result.exit_code != 0 {
print $"($binary) exited ($result.exit_code)"
if ($result.stderr | is-not-empty) { print $result.stderr }
exit $result.exit_code
}
print $result.stdout
}
# ── ecosystem subcommands ─────────────────────────────────────────────────────
def cmd-ecosystem [args: list<string>]: nothing -> nothing {
let sub = $args | get 0? | default ""
let rest = if ($args | length) > 1 { $args | skip 1 } else { [] }
match $sub {
"domains" => {
let flag_registry = (parse-flag "--registry" $rest)
let domains = if ($flag_registry | is-not-empty) {
ecosystem-list-domains --registry $flag_registry
} else {
ecosystem-list-domains
}
if ($domains | is-empty) {
print "No domain artifacts found (catalog API may be unavailable)"
} else {
for d in $domains {
let versions = ($d.versions | str join ", ")
print $"($d.participant)/($d.id) ($versions)"
}
}
}
"modes" => {
let flag_registry = (parse-flag "--registry" $rest)
let modes = if ($flag_registry | is-not-empty) {
ecosystem-list-modes --registry $flag_registry
} else {
ecosystem-list-modes
}
if ($modes | is-empty) {
print "No mode artifacts found (catalog API may be unavailable)"
} else {
for m in $modes {
let versions = ($m.versions | str join ", ")
print $"($m.participant)/($m.id) ($versions)"
}
}
}
_ => {
print "integration ecosystem <subcommand>"
print ""
print "Subcommands:"
print " domains [--registry <reg>] List all published domain artifacts"
print " modes [--registry <reg>] List all published mode artifacts"
}
}
}
# ── list subcommand ──────────────────────────────────────────────────────────
def cmd-list [args: list<string>]: nothing -> nothing {
let live = ($args | any { |a| $a == "--live" })
let flag_registry = (parse-flag "--registry" $args)
let flag_ws_dir = (parse-flag "--workspace-dir" $args)
if $live {
let reg = if ($flag_registry | is-not-empty) { $flag_registry } else { resolve-registry }
print $"Registry: ($reg)"
print ""
let domains = if ($flag_registry | is-not-empty) {
ecosystem-list-domains --registry $flag_registry
} else {
ecosystem-list-domains
}
if ($domains | is-not-empty) {
print "Domains:"
for d in $domains {
let versions = ($d.versions | str join ", ")
print $" ($d.participant)/($d.id) ($versions)"
}
} else {
print "Domains: (none)"
}
print ""
let modes = if ($flag_registry | is-not-empty) {
ecosystem-list-modes --registry $flag_registry
} else {
ecosystem-list-modes
}
if ($modes | is-not-empty) {
print "Modes:"
for m in $modes {
let versions = ($m.versions | str join ", ")
print $" ($m.participant)/($m.id) ($versions)"
}
} else {
print "Modes: (none)"
}
return
}
let root = if ($flag_ws_dir | is-not-empty) { $flag_ws_dir } else { $env.PWD }
# Collect cabling files from infra/*/integrations/*.ncl
let infra = ($root | path join "infra")
let cablings = if ($infra | path exists) {
ls $infra
| where type == "dir"
| get name
| each { |ws_dir|
let int_dir = $ws_dir | path join "integrations"
if ($int_dir | path exists) {
ls $int_dir | where name =~ '\.ncl$' | get name
} else {
[]
}
}
| flatten
} else {
[]
}
# Collect cached domains. Cache layout: ~/.cache/ontoref/domains/<participant>/<id>/<version>/
# T5: extract participant from the directory hierarchy; fallback for legacy
# caches without the participant level (versions live directly under id/).
let domain_cache = ($env.HOME | path join ".cache" "ontoref" "domains")
let cached_domains = if ($domain_cache | path exists) {
try {
ls $domain_cache
| where type == "dir"
| get name
| each { |participant_dir|
let participant = ($participant_dir | path basename)
# Heuristic: if the participant_dir contains version-shaped subdirs directly,
# this is the legacy flat layout (id only) — treat it as { id: <basename>, no participant }.
let inner = (ls $participant_dir | where type == "dir" | get name)
let inner_has_subdir_with_versions = ($inner | any { |d| (ls $d | where type == "dir" | length) > 0 })
if $inner_has_subdir_with_versions {
# New layout: <participant>/<id>/<version>/
$inner | each { |id_dir|
let versions = (ls $id_dir | where type == "dir" | get name
| each { |v| $v | path basename } | str join ", ")
{ participant: $participant, id: ($id_dir | path basename), versions: $versions }
}
} else {
# Legacy flat layout: <id>/<version>/
let versions = ($inner | each { |v| $v | path basename } | str join ", ")
[{ participant: "(legacy)", id: $participant, versions: $versions }]
}
}
| flatten
} catch { [] }
} else {
[]
}
if ($cablings | is-empty) and ($cached_domains | is-empty) {
print "No integration modes or cached domains found."
print "Use: prvng integration subscribe <mode-id> --mode-file <path> --workspace-dir <path>"
return
}
if ($cablings | is-not-empty) {
print "Integration modes (cabling files):"
for c in $cablings {
let mode_id = ($c | path basename | str replace ".ncl" "")
let ws = ($c | path dirname | path dirname | path basename)
print $" ($mode_id) [workspace: ($ws)] ($c)"
}
}
if ($cached_domains | is-not-empty) {
print ""
print "Cached domains:"
for d in $cached_domains {
print $" ($d.participant)/($d.id) ($d.versions)"
}
}
}
# ── describe subcommand ───────────────────────────────────────────────────────
def cmd-describe [args: list<string>]: nothing -> nothing {
let mode_id = $args | get 0? | default ""
let flag_mode_file = (parse-flag "--mode-file" $args)
let flag_ncl_import = (parse-flag "--ncl-import" $args)
if ($mode_id | is-empty) or ($flag_mode_file | is-empty) {
int-err "Usage: integration describe <mode-id> --mode-file <path> [--ncl-import <path>]"
}
let import_path = if ($flag_ncl_import | is-not-empty) { $flag_ncl_import } else {
$env.PROVISIONING? | default "/opt/provisioning"
}
let r = (do { ^nickel export --import-path $import_path $flag_mode_file } | complete)
if $r.exit_code != 0 {
int-err $"Failed to evaluate mode file: ($r.stderr)"
}
let mode = ($r.stdout | from json | get mode)
print $"Mode: ($mode.id)"
print $"Participant: ($mode.participant)"
print $"Direction: ($mode.direction)"
print $"Trigger: ($mode.trigger)"
print ""
print "Domains used:"
for d in ($mode | get -o domains_used | default []) {
print $" ($d.id) ($d.version)"
}
print ""
print "Steps:"
for s in ($mode | get -o steps | default []) {
let deps = ($s | get -o depends_on | default [] | each { |d| $d.step } | str join ", ")
let dep_str = if ($deps | is-not-empty) { $" ← ($deps)" } else { "" }
print $" ($s.id)($dep_str) — ($s.action)"
}
}
# ── subscribe subcommand ──────────────────────────────────────────────────────
def resolve-cosign-pubkey []: nothing -> string {
let from_env = ($env | get -o "ONTOREF_COSIGN_PUBKEY" | default "")
if ($from_env | is-not-empty) { return $from_env }
let prov = ($env.PROVISIONING? | default "")
let catalog_root = ($env.PROVISIONING_CATALOG_PATH? | default (if ($prov | is-not-empty) { $prov | path join "catalog" } else { "" }))
if ($catalog_root | is-not-empty) {
let catalog_key = ($catalog_root | path join "domains/_signing/cosign.pub")
if ($catalog_key | path exists) { return $catalog_key }
}
""
}
def resolve-semver-range [range: string]: nothing -> string {
let m = ($range | parse --regex '(\d+\.\d+\.\d+)')
if ($m | is-empty) {
$range | str trim
} else {
$m | first | get capture0
}
}
def domain-bindings-for-scaffold [participant: string, domain_id: string, version: string]: nothing -> list<string> {
let cache = ($env.HOME | path join ".cache" "ontoref" "domains" $participant $domain_id $version)
let ex_path = ($cache | path join "example.json")
if ($ex_path | path exists) {
let ex = (open $ex_path)
let secrets = ($ex | get -o secrets | default {})
if ($secrets | describe | str starts-with "record") and (($secrets | columns | length) > 0) {
return ($secrets | columns | each { |fld|
let source = ($secrets | get $fld | get -o source | default "sops")
if $source == "sops" {
$" # REQUIRED — SOPS-encrypted\n \"($domain_id).($fld)\" = { kind = \"sops\", path = \"secrets/<edit-me>.sops.yaml\", key = \"<KEY>\" },"
} else if $source == "component_output" {
$" # DEFAULT — resolved from component output\n \"($domain_id).($fld)\" = { kind = \"component\", name = \"<component-name>\", field = \"<dotted.field.path>\" },"
} else {
$" # DEFAULT — literal value\n \"($domain_id).($fld)\" = { kind = \"literal\", value = \"<value>\" },"
}
})
}
}
# Domain-aware fallbacks for core domains without example.json
match $domain_id {
"event-emission" => {
[
$" # REQUIRED — stable subject prefix for this workspace + mode\n \"event-emission.subject_prefix\" = { kind = \"literal\", value = \"ws.<workspace>.<mode-id>\" },",
]
}
"result-reporting" => {
[ " # result-reporting is output-only — no bindings required" ]
}
_ => {
[ $" # REQUIRED — fill bindings for ($domain_id)\n # \"($domain_id).<field>\" = { kind = \"literal\", value = \"<value>\" }," ]
}
}
}
def build-cabling-scaffold [mode: record, workspace: string, registry: string, ncl_import: string]: nothing -> string {
let mode_id = $mode.id
let domains = ($mode | get -o domains_used | default [])
mut binding_blocks = []
for d in $domains {
let version = (resolve-semver-range $d.version)
let participant = ($d | get -o participant | default ($mode | get -o participant | default "unknown"))
let lines = (domain-bindings-for-scaffold $participant $d.id $version)
$binding_blocks = ($binding_blocks | append $" # ──── ($d.id) ──────────────────────────")
$binding_blocks = ($binding_blocks | append $lines)
$binding_blocks = ($binding_blocks | append "")
}
let bindings_body = ($binding_blocks | str join "\n")
$"# Cabling for mode ($mode_id) in workspace ($workspace).
# Generated skeleton — operator must fill in REQUIRED bindings before invoking.
# Validate: nickel export --import-path <provisioning> infra/($workspace)/integrations/($mode_id).ncl
let cabling = import \"schemas/lib/integration/cabling.ncl\" in
{
config = {
mode_id = \"($mode_id)\",
workspace = \"($workspace)\",
bindings = {
($bindings_body)
},
} | cabling.Cabling,
}
"
}
def cmd-subscribe [args: list<string>]: nothing -> nothing {
let mode_id = $args | get 0? | default ""
if ($mode_id | is-empty) {
int-err "Usage: integration subscribe <mode-id> --mode-file <path> --workspace-dir <path> [--registry <reg>] [--ncl-import <path>]"
}
let flag_mode_file = (parse-flag "--mode-file" $args)
let flag_ws_dir = (parse-flag "--workspace-dir" $args)
let flag_registry = (parse-flag "--registry" $args)
let flag_ncl_import = (parse-flag "--ncl-import" $args)
if ($flag_mode_file | is-empty) or ($flag_ws_dir | is-empty) {
int-err "subscribe requires --mode-file and --workspace-dir"
}
let registry = if ($flag_registry | is-not-empty) { $flag_registry } else { resolve-registry }
let import_path = if ($flag_ncl_import | is-not-empty) { $flag_ncl_import } else {
$env.PROVISIONING? | default "/opt/provisioning"
}
let r = (do { ^nickel export --import-path $import_path $flag_mode_file } | complete)
if $r.exit_code != 0 {
int-err $"Failed to evaluate mode file ($flag_mode_file): ($r.stderr)"
}
let mode = ($r.stdout | from json | get mode)
print $"Mode: ($mode.id) participant: ($mode.participant)"
print $"Registry: ($registry)"
print ""
# Pull domains and verify signatures when a cosign key is resolvable.
# A resolved key turns unsigned artifacts into hard errors; absent key skips silently.
let cosign_key = (resolve-cosign-pubkey)
for d in ($mode | get -o domains_used | default []) {
let version = (resolve-semver-range $d.version)
# participant field in domains_used takes precedence; fall back to mode.participant
let participant = ($d | get -o participant | default ($mode | get -o participant | default ""))
if ($participant | is-empty) {
int-err $"Domain ($d.id) has no participant — add participant field to domains_used entry or declare mode.participant"
}
print $"Pulling ($participant)/($d.id):($version)..."
let cache = (domain-pull $participant $d.id $version --registry $registry)
print $" cached → ($cache)"
let verify_result = if ($cosign_key | is-not-empty) {
domain-verify $participant $d.id $version --registry $registry --cosign-key $cosign_key
} else {
null
}
if $verify_result != null {
if $verify_result.pass {
print $" (ansi green_bold)✓(ansi reset) signature verified"
} else {
let errs = ($verify_result.errors | str join "; ")
int-err $"Domain ($d.id):($version) failed verification: ($errs)"
}
}
}
# Resolve workspace name from infra/ directory structure
let infra = ($flag_ws_dir | path join "infra")
let ws_name = if ($infra | path exists) {
let dirs = (ls $infra | where type == "dir" | get name)
if ($dirs | is-empty) { $flag_ws_dir | path basename } else { $dirs | first | path basename }
} else {
$flag_ws_dir | path basename
}
let integrations_dir = if ($infra | path exists) {
$infra | path join $ws_name | path join "integrations"
} else {
$flag_ws_dir | path join "integrations"
}
mkdir $integrations_dir
let cabling_path = ($integrations_dir | path join $"($mode_id).ncl")
if ($cabling_path | path exists) {
print ""
print $"Cabling already exists: ($cabling_path)"
print "Skipping scaffold. Delete the file to regenerate."
} else {
let scaffold = (build-cabling-scaffold $mode $ws_name $registry $import_path)
$scaffold | save $cabling_path
print ""
print $"Scaffolded cabling → ($cabling_path)"
print " Edit REQUIRED bindings before running invoke."
}
}
# ── validate subcommand ───────────────────────────────────────────────────────
def check-resolver [resolver: record, ws_root: string]: nothing -> record {
match $resolver.kind {
"sops" => {
let path = if ($resolver.path | str starts-with "/") {
$resolver.path
} else {
$ws_root | path join $resolver.path
}
if ($path | path exists) {
{ ok: true, msg: $"($path) ✓" }
} else {
{ ok: false, msg: $"SOPS file not found: ($path)" }
}
}
"component" => {
let infra = ($ws_root | path join "infra")
let comp_ncl = if ($infra | path exists) {
let dirs = (ls $infra | where type == "dir" | get name)
if ($dirs | is-empty) {
($ws_root | path join "components" $"($resolver.name).ncl")
} else {
$dirs | first | path join "components" $"($resolver.name).ncl"
}
} else {
$ws_root | path join "components" $"($resolver.name).ncl"
}
if ($comp_ncl | path exists) {
{ ok: true, msg: $"($comp_ncl) ✓" }
} else {
{ ok: false, msg: $"Component NCL not found: ($comp_ncl)" }
}
}
"literal" => {
let v = ($resolver.value | into string)
if ($v | is-not-empty) and ($v != "<value>") and (not ($v | str starts-with "<")) {
{ ok: true, msg: $"literal: ($v)" }
} else {
{ ok: false, msg: $"Literal value looks like an unfilled placeholder: ($v)" }
}
}
"env" => {
let val = ($env | get -o $resolver.env_var | default "")
if ($val | is-not-empty) {
{ ok: true, msg: $"$($resolver.env_var) is set" }
} else {
{ ok: false, msg: $"env var ($resolver.env_var) is not set in current environment" }
}
}
_ => { ok: false, msg: $"Unknown resolver kind: ($resolver.kind)" }
}
}
def cmd-validate [args: list<string>]: nothing -> nothing {
let mode_id = $args | get 0? | default ""
let flag_ws_dir = (parse-flag "--workspace-dir" $args)
let flag_ncl_import = (parse-flag "--ncl-import" $args)
if ($mode_id | is-empty) or ($flag_ws_dir | is-empty) {
int-err "Usage: integration validate <mode-id> --workspace-dir <path> [--ncl-import <path>]"
}
let import_path = if ($flag_ncl_import | is-not-empty) { $flag_ncl_import } else {
$env.PROVISIONING? | default "/opt/provisioning"
}
# Find cabling
let infra = ($flag_ws_dir | path join "infra")
let candidates = if ($infra | path exists) {
ls $infra
| where type == "dir"
| get name
| each { |d| $d | path join "integrations" $"($mode_id).ncl" }
| where { |p| $p | path exists }
} else {
let p = ($flag_ws_dir | path join "integrations" $"($mode_id).ncl")
if ($p | path exists) { [$p] } else { [] }
}
if ($candidates | is-empty) {
int-err $"No cabling file found for ($mode_id) under ($flag_ws_dir)"
}
let cabling_path = ($candidates | first)
print $"Validating cabling: ($cabling_path)"
print ""
let r = (do { ^nickel export --import-path $import_path $cabling_path } | complete)
if $r.exit_code != 0 {
print $" ✗ Schema validation failed:"
print $r.stderr
exit 1
}
let cabling = ($r.stdout | from json | get config)
print $" ✓ Nickel schema valid (mode: ($cabling.mode_id), workspace: ($cabling.workspace))"
print ""
print "Bindings:"
mut all_ok = true
for entry in ($cabling.bindings | transpose key resolver) {
let result = (check-resolver $entry.resolver $flag_ws_dir)
let icon = if $result.ok {
$"(ansi green_bold)✓(ansi reset)"
} else {
$"(ansi red_bold)✗(ansi reset)"
}
print $" ($icon) ($entry.key) [($entry.resolver.kind)] ($result.msg)"
if not $result.ok { $all_ok = false }
}
print ""
if $all_ok {
print $"(ansi green_bold)OK — all bindings resolve(ansi reset)"
} else {
print $"(ansi red_bold)FAIL — one or more bindings cannot be resolved(ansi reset)"
exit 1
}
}
# ── help ─────────────────────────────────────────────────────────────────────
def _print-integration-help []: nothing -> nothing {
print "prvng integration (i) — federated OCI integration modes (ADR-042)"
print ""
print " Domain artifacts — OCI layers, cosign-signed, stored as domains/<participant>/<id>:"
print " domain publish <path> <participant> Push domain artifact"
print " domain pull <participant>/<id> <version> Cache domain locally"
print " domain describe <participant>/<id> <version> Show OCI manifest"
print " domain verify <participant>/<id> <version> Check integrity + signature"
print " domain diff <p>/<id>:<v1> <p>/<id>:<v2> Layer diff"
print " ecosystem domains [--registry <reg>] List all domain artifacts"
print " ecosystem modes [--registry <reg>] List all mode artifacts"
print ""
print " Mode lifecycle — subscribe, invoke, validate:"
print " mode publish <path> <participant> <mode-id> <ver> Push mode artifact"
print " subscribe <mode-id> --mode-file <path> --workspace-dir <path>"
print " [--registry <reg>] [--ncl-import <path>]"
print " Pull domain deps, verify signatures, scaffold cabling.ncl"
print " invoke <mode-id> [--binary <name>] [--workspace-root <path>]"
print " [--ncl-import <path>] [--dry-run]"
print " Assemble context and pipe to mode binary stdin"
print " validate <mode-id> --workspace-dir <path> [--ncl-import <path>]"
print " Typecheck cabling.ncl and check all bindings resolve"
print " describe <mode-id> --mode-file <path> [--ncl-import <path>]"
print " Show mode metadata: domains, steps, direction"
print " list [--workspace-dir <path>]"
print " List cabling files and cached domains"
print " list --live [--registry <reg>]"
print " Query live registry: domains/ + modes/ present in the registry"
print ""
print " Examples:"
print " prvng i domain verify secret-delivery 0.1.0"
print " prvng i domain publish catalog/domains/secret-delivery"
print " prvng i ecosystem domains"
print " prvng i subscribe lian-build --mode-file infra/modes/lian-build.ncl --workspace-dir ."
print " prvng i invoke lian-build --dry-run"
print " prvng i validate lian-build --workspace-dir ."
print " prvng i list"
print ""
print " Flags:"
print " --registry <host> Override default registry (reg.librecloud.online)"
print " --ncl-import <path> Extra Nickel import path (defaults to \$PROVISIONING)"
print " --workspace-root <p> Workspace root for cabling resolution (defaults to \$PWD)"
print " --dry-run Print assembled context envelope without invoking binary"
}
# ── entry point ───────────────────────────────────────────────────────────────
def main [...args: string]: nothing -> nothing {
let cmd = $args | get 0? | default ""
let rest = if ($args | length) > 1 { $args | skip 1 } else { [] }
match $cmd {
"integration" | "i" => {
let sub = $rest | get 0? | default ""
let sub_rest = if ($rest | length) > 1 { $rest | skip 1 } else { [] }
match $sub {
"domain" => { cmd-domain $sub_rest }
"mode" => { cmd-mode $sub_rest }
"ecosystem" => { cmd-ecosystem $sub_rest }
"invoke" => { cmd-invoke $sub_rest }
"list" => { cmd-list $sub_rest }
"describe" => { cmd-describe $sub_rest }
"subscribe" => { cmd-subscribe $sub_rest }
"validate" => { cmd-validate $sub_rest }
_ => { _print-integration-help }
}
}
_ => { _print-integration-help }
}
}