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).
355 lines
17 KiB
Text
355 lines
17 KiB
Text
#!/usr/bin/env nu
|
|
# Tests for reflection/modules/secrets.nu — the 15 named errors that form the
|
|
# helper contract. Each test builds a minimal fixture (project_root with
|
|
# .ontoref/project.ncl and/or .ontology/manifest.ncl) and asserts the helper
|
|
# raises the expected `[<code>]` prefix.
|
|
#
|
|
# Run: nu reflection/tests/test_secrets.nu
|
|
# Exit: 0 if all pass, 1 if any fail.
|
|
|
|
use ../modules/secrets.nu *
|
|
|
|
const ONTOREF_ROOT = "/Users/Akasha/Development/ontoref"
|
|
|
|
# Self-contained import path — the secrets module reads $env.NICKEL_IMPORT_PATH
|
|
# when invoking `nickel export`. Each fixture's manifest.ncl imports
|
|
# `ontology/defaults/manifest.ncl`, which lives under $ONTOREF_ROOT/ontology.
|
|
# We append (rather than overwrite) so a caller's existing path still applies.
|
|
$env.NICKEL_IMPORT_PATH = (
|
|
[
|
|
($env.NICKEL_IMPORT_PATH? | default "")
|
|
$ONTOREF_ROOT
|
|
$"($ONTOREF_ROOT)/ontology"
|
|
($env.HOME | path join ".config" "ontoref" "schemas")
|
|
] | where { |s| ($s | str length) > 0 } | str join (char esep)
|
|
)
|
|
|
|
# ── Test runner ──────────────────────────────────────────────────────────────
|
|
|
|
mut TOTAL = 0
|
|
mut PASSED = 0
|
|
mut FAILED = []
|
|
|
|
def expect-error [
|
|
name: string
|
|
code: string # e.g. "kage-not-resolvable"
|
|
body: closure
|
|
]: nothing -> record {
|
|
let r = (try {
|
|
do $body
|
|
{ kind: "no-error", msg: "" }
|
|
} catch { |e|
|
|
{ kind: "error", msg: $e.msg }
|
|
})
|
|
let prefix = $"[($code)]"
|
|
if $r.kind == "no-error" {
|
|
{ ok: false, msg: $"expected error ($prefix) but body returned without error" }
|
|
} else if ($r.msg | str starts-with $prefix) {
|
|
{ ok: true, msg: "" }
|
|
} else {
|
|
let snippet = ($r.msg | str substring 0..200)
|
|
{ ok: false, msg: $"expected ($prefix) got: ($snippet)" }
|
|
}
|
|
}
|
|
|
|
# ── Fixture builders ─────────────────────────────────────────────────────────
|
|
|
|
def write-project-ncl [
|
|
root: string
|
|
body: string # the body of make_project { ... }
|
|
]: nothing -> nothing {
|
|
mkdir ($root | path join ".ontoref")
|
|
let schema_dir = ($env.HOME | path join ".config" "ontoref" "schemas")
|
|
let import = $'let s = import "ontoref-project.ncl" in
|
|
'
|
|
let content = $"($import)
|
|
s.make_project {
|
|
($body)
|
|
}"
|
|
$content | save --force ($root | path join ".ontoref" "project.ncl")
|
|
}
|
|
|
|
def write-manifest-ncl [
|
|
root: string
|
|
body: string # additional fields beyond the required boilerplate
|
|
]: nothing -> nothing {
|
|
mkdir ($root | path join ".ontology")
|
|
# Minimum required fields by ontology/defaults/manifest.ncl::make_manifest.
|
|
let boilerplate = ([
|
|
' project = "fixture",',
|
|
(" repo_kind = " + (char single_quote) + "Mixed,"),
|
|
' description = "test fixture",',
|
|
' consumption_modes = [],',
|
|
] | str join (char newline))
|
|
let content = ([
|
|
'let m = import "ontology/defaults/manifest.ncl" in',
|
|
'',
|
|
'm.make_manifest {',
|
|
$boilerplate,
|
|
$body,
|
|
'}',
|
|
] | str join (char newline))
|
|
$content | save --force ($root | path join ".ontology" "manifest.ncl")
|
|
}
|
|
|
|
def fixture-base-project [root: string, sops_block: string]: nothing -> nothing {
|
|
write-project-ncl $root $' slug = "fixture",
|
|
root = "($root)",
|
|
nickel_import_paths = ["($root)", "($ONTOREF_ROOT)", "($ONTOREF_ROOT)/ontology"],
|
|
keys = [],
|
|
($sops_block)'
|
|
}
|
|
|
|
# ── Tests ────────────────────────────────────────────────────────────────────
|
|
|
|
let TMP = (mktemp -d -t ontoref-tests.XXXXXX)
|
|
print $"# fixtures in ($TMP)"
|
|
print ""
|
|
|
|
# Each test runs in its own subdir of $TMP so fixtures don't collide.
|
|
def run-one [name: string, code: string, body: closure]: nothing -> record {
|
|
let r = (expect-error $name $code $body)
|
|
if $r.ok {
|
|
print $" (ansi green)✓(ansi reset) ($name) [($code)]"
|
|
} else {
|
|
print $" (ansi red)✗(ansi reset) ($name) [($code)] → ($r.msg)"
|
|
}
|
|
$r
|
|
}
|
|
|
|
# --- 1. [invalid-op] ------------------------------------------------------------
|
|
let r1 = (run-one "invalid op" "invalid-op" {
|
|
let f = ($TMP | path join "t1")
|
|
fixture-base-project $f 'sops = { enabled = false }'
|
|
write-manifest-ncl $f ''
|
|
resolve-registry-credential $f primary --op badname
|
|
})
|
|
$TOTAL = $TOTAL + 1; if $r1.ok { $PASSED = $PASSED + 1 } else { $FAILED = ($FAILED | append "invalid-op") }
|
|
|
|
# --- 2. [project-ncl-missing] ---------------------------------------------------
|
|
# Use assert-actor-authorized which reads project.ncl first;
|
|
# resolve-registry-credential reads manifest first and would shadow this case.
|
|
let r2 = (run-one "project-ncl missing" "project-ncl-missing" {
|
|
let f = ($TMP | path join "t2-empty")
|
|
mkdir $f
|
|
assert-actor-authorized $f pull
|
|
})
|
|
$TOTAL = $TOTAL + 1; if $r2.ok { $PASSED = $PASSED + 1 } else { $FAILED = ($FAILED | append "project-ncl-missing") }
|
|
|
|
# --- 3. [manifest-ncl-missing] --------------------------------------------------
|
|
let r3 = (run-one "manifest.ncl missing" "manifest-ncl-missing" {
|
|
let f = ($TMP | path join "t3")
|
|
fixture-base-project $f 'sops = { enabled = true, vault_id = "fixture", master_key_path = "/dev/null" }'
|
|
resolve-registry-credential $f primary --op pull
|
|
})
|
|
$TOTAL = $TOTAL + 1; if $r3.ok { $PASSED = $PASSED + 1 } else { $FAILED = ($FAILED | append "manifest-ncl-missing") }
|
|
|
|
# --- 4. [registry-provides-missing] --------------------------------------------
|
|
let r4 = (run-one "registry_provides missing" "registry-provides-missing" {
|
|
let f = ($TMP | path join "t4")
|
|
fixture-base-project $f 'sops = { enabled = true, vault_id = "fixture", master_key_path = "/dev/null" }'
|
|
write-manifest-ncl $f ''
|
|
resolve-registry-credential $f primary --op pull
|
|
})
|
|
$TOTAL = $TOTAL + 1; if $r4.ok { $PASSED = $PASSED + 1 } else { $FAILED = ($FAILED | append "registry-provides-missing") }
|
|
|
|
# --- 5. [registry-id-unknown] --------------------------------------------------
|
|
let r5 = (run-one "registry id unknown" "registry-id-unknown" {
|
|
let f = ($TMP | path join "t5")
|
|
fixture-base-project $f 'sops = { enabled = true, vault_id = "fixture", master_key_path = "/dev/null" }'
|
|
write-manifest-ncl $f ' registry_provides = m.make_registry_provides {
|
|
participant = "fixture",
|
|
registries = m.make_registries_config {
|
|
default = "primary",
|
|
registries = [ m.make_registry_entry { id = "primary", endpoint = "x", credential_sops = "registry/ro.sops.yaml" } ],
|
|
},
|
|
},'
|
|
resolve-registry-credential $f does-not-exist --op pull
|
|
})
|
|
$TOTAL = $TOTAL + 1; if $r5.ok { $PASSED = $PASSED + 1 } else { $FAILED = ($FAILED | append "registry-id-unknown") }
|
|
|
|
# --- 6. [credential-sops-missing] ----------------------------------------------
|
|
let r6 = (run-one "credential_sops missing for op" "credential-sops-missing" {
|
|
let f = ($TMP | path join "t6")
|
|
fixture-base-project $f 'sops = { enabled = true, vault_id = "fixture", master_key_path = "/dev/null" }'
|
|
write-manifest-ncl $f ' registry_provides = m.make_registry_provides {
|
|
participant = "fixture",
|
|
registries = m.make_registries_config {
|
|
default = "primary",
|
|
registries = [ m.make_registry_entry { id = "primary", endpoint = "x", credential_sops = "registry/ro.sops.yaml" } ],
|
|
},
|
|
},'
|
|
# credential_sops_rw is absent, op=push → should fail
|
|
resolve-registry-credential $f primary --op push
|
|
})
|
|
$TOTAL = $TOTAL + 1; if $r6.ok { $PASSED = $PASSED + 1 } else { $FAILED = ($FAILED | append "credential-sops-missing") }
|
|
|
|
# --- 7. [sops-file-not-found] --------------------------------------------------
|
|
let r7 = (run-one "sops file not on disk" "sops-file-not-found" {
|
|
let f = ($TMP | path join "t7")
|
|
fixture-base-project $f $'sops = { enabled = true, vault_id = "fixture-t7-($r6.ok)", master_key_path = "/dev/null" }'
|
|
write-manifest-ncl $f ' registry_provides = m.make_registry_provides {
|
|
participant = "fixture",
|
|
registries = m.make_registries_config {
|
|
default = "primary",
|
|
registries = [ m.make_registry_entry { id = "primary", endpoint = "x", credential_sops = "registry/ro.sops.yaml" } ],
|
|
},
|
|
},'
|
|
# Vault dir for "fixture-t7-..." does not exist → sops file not found
|
|
resolve-registry-credential $f primary --op pull
|
|
})
|
|
$TOTAL = $TOTAL + 1; if $r7.ok { $PASSED = $PASSED + 1 } else { $FAILED = ($FAILED | append "sops-file-not-found") }
|
|
|
|
# --- 8. [kage-not-resolvable] (master_key_path file missing) -------------------
|
|
let r8 = (run-one "kage not resolvable" "kage-not-resolvable" {
|
|
let f = ($TMP | path join "t8")
|
|
let bogus_kage = ($TMP | path join "non-existent.kage")
|
|
fixture-base-project $f $'sops = { enabled = true, vault_id = "fixture-t8", master_key_path = "($bogus_kage)" }'
|
|
write-manifest-ncl $f ' registry_provides = m.make_registry_provides {
|
|
participant = "fixture",
|
|
registries = m.make_registries_config {
|
|
default = "primary",
|
|
registries = [ m.make_registry_entry { id = "primary", endpoint = "x", credential_sops = "registry/ro.sops.yaml" } ],
|
|
},
|
|
},'
|
|
# Pre-create the sops file so we get past sops-file-not-found.
|
|
let vault_dir = ($env.HOME | path join ".config" "ontoref" "vaults" "fixture-t8" "src-vault" "registry")
|
|
mkdir $vault_dir
|
|
"fake" | save --force ($vault_dir | path join "ro.sops.yaml")
|
|
resolve-registry-credential $f primary --op pull
|
|
})
|
|
$TOTAL = $TOTAL + 1; if $r8.ok { $PASSED = $PASSED + 1 } else { $FAILED = ($FAILED | append "kage-not-resolvable") }
|
|
# Cleanup
|
|
rm -rf ($env.HOME | path join ".config" "ontoref" "vaults" "fixture-t8")
|
|
|
|
# --- 9. [actor-bindings-missing] -----------------------------------------------
|
|
let r9 = (run-one "actor_key_bindings empty" "actor-bindings-missing" {
|
|
let f = ($TMP | path join "t9")
|
|
fixture-base-project $f 'sops = { enabled = true, vault_id = "fixture-t9", master_key_path = "/dev/null", actor_key_bindings = {} }'
|
|
write-manifest-ncl $f ''
|
|
assert-actor-authorized $f pull
|
|
})
|
|
$TOTAL = $TOTAL + 1; if $r9.ok { $PASSED = $PASSED + 1 } else { $FAILED = ($FAILED | append "actor-bindings-missing") }
|
|
|
|
# --- 10. [actor-not-bound] -----------------------------------------------------
|
|
let r10 = (run-one "actor not in bindings" "actor-not-bound" {
|
|
let f = ($TMP | path join "t10")
|
|
fixture-base-project $f 'sops = { enabled = true, vault_id = "fixture-t10", master_key_path = "/dev/null", actor_key_bindings = { admin = "admin" } }'
|
|
write-manifest-ncl $f ''
|
|
with-env { ONTOREF_ACTOR: "alien" } {
|
|
assert-actor-authorized $f pull
|
|
}
|
|
})
|
|
$TOTAL = $TOTAL + 1; if $r10.ok { $PASSED = $PASSED + 1 } else { $FAILED = ($FAILED | append "actor-not-bound") }
|
|
|
|
# --- 11. [scope-not-loaded] ----------------------------------------------------
|
|
let r11 = (run-one "scope file missing" "scope-not-loaded" {
|
|
let f = ($TMP | path join "t11")
|
|
fixture-base-project $f 'sops = { enabled = true, vault_id = "fixture-t11", master_key_path = "/dev/null", actor_key_bindings = { developer = "developer" } }'
|
|
write-manifest-ncl $f ''
|
|
# No scopes/developer.ncl in vault dir
|
|
with-env { ONTOREF_ACTOR: "developer" } {
|
|
assert-actor-authorized $f pull
|
|
}
|
|
})
|
|
$TOTAL = $TOTAL + 1; if $r11.ok { $PASSED = $PASSED + 1 } else { $FAILED = ($FAILED | append "scope-not-loaded") }
|
|
|
|
# --- 12. [op-not-in-scope] -----------------------------------------------------
|
|
let r12 = (run-one "op not in scope.ops" "op-not-in-scope" {
|
|
let f = ($TMP | path join "t12")
|
|
fixture-base-project $f 'sops = { enabled = true, vault_id = "fixture-t12", master_key_path = "/dev/null", actor_key_bindings = { developer = "developer" } }'
|
|
write-manifest-ncl $f ''
|
|
let scope_dir = ($env.HOME | path join ".config" "ontoref" "vaults" "fixture-t12" "src-vault" "scopes")
|
|
mkdir $scope_dir
|
|
("{ role = \"developer\", access = 'ro, bound_actor = [], namespaces = [\"domains/*/\"], ops = ['pull, 'verify] }"
|
|
| save --force ($scope_dir | path join "developer.ncl"))
|
|
with-env { ONTOREF_ACTOR: "developer" } {
|
|
assert-actor-authorized $f push
|
|
}
|
|
})
|
|
$TOTAL = $TOTAL + 1; if $r12.ok { $PASSED = $PASSED + 1 } else { $FAILED = ($FAILED | append "op-not-in-scope") }
|
|
rm -rf ($env.HOME | path join ".config" "ontoref" "vaults" "fixture-t12")
|
|
|
|
# --- 13. [actor-not-in-bound-actor] --------------------------------------------
|
|
let r13 = (run-one "scope.bound_actor excludes actor" "actor-not-in-bound-actor" {
|
|
let f = ($TMP | path join "t13")
|
|
fixture-base-project $f 'sops = { enabled = true, vault_id = "fixture-t13", master_key_path = "/dev/null", actor_key_bindings = { developer = "developer" } }'
|
|
write-manifest-ncl $f ''
|
|
let scope_dir = ($env.HOME | path join ".config" "ontoref" "vaults" "fixture-t13" "src-vault" "scopes")
|
|
mkdir $scope_dir
|
|
("{ role = \"developer\", access = 'rw, bound_actor = [\"admin\"], namespaces = [\"domains/*/\"], ops = ['pull, 'push] }"
|
|
| save --force ($scope_dir | path join "developer.ncl"))
|
|
with-env { ONTOREF_ACTOR: "developer" } {
|
|
assert-actor-authorized $f pull
|
|
}
|
|
})
|
|
$TOTAL = $TOTAL + 1; if $r13.ok { $PASSED = $PASSED + 1 } else { $FAILED = ($FAILED | append "actor-not-in-bound-actor") }
|
|
rm -rf ($env.HOME | path join ".config" "ontoref" "vaults" "fixture-t13")
|
|
|
|
# --- 14. [target-not-in-scope] -------------------------------------------------
|
|
let r14 = (run-one "target not in scope.namespaces" "target-not-in-scope" {
|
|
let f = ($TMP | path join "t14")
|
|
fixture-base-project $f 'sops = { enabled = true, vault_id = "fixture-t14", master_key_path = "/dev/null", actor_key_bindings = { developer = "developer" } }'
|
|
write-manifest-ncl $f ''
|
|
let scope_dir = ($env.HOME | path join ".config" "ontoref" "vaults" "fixture-t14" "src-vault" "scopes")
|
|
mkdir $scope_dir
|
|
("{ role = \"developer\", access = 'rw, bound_actor = [], namespaces = [\"domains/foo/\"], ops = ['pull, 'push] }"
|
|
| save --force ($scope_dir | path join "developer.ncl"))
|
|
with-env { ONTOREF_ACTOR: "developer" } {
|
|
assert-target-in-scope $f "modes/lian-build/"
|
|
}
|
|
})
|
|
$TOTAL = $TOTAL + 1; if $r14.ok { $PASSED = $PASSED + 1 } else { $FAILED = ($FAILED | append "target-not-in-scope") }
|
|
rm -rf ($env.HOME | path join ".config" "ontoref" "vaults" "fixture-t14")
|
|
|
|
# --- 15. [sops-decrypt-failed] ------------------------------------------------
|
|
# Generate a real age keypair (key-A), encrypt a sops file with key-A's pubkey,
|
|
# then attempt resolve-registry-credential with a DIFFERENT master key (key-B).
|
|
# sops returns non-zero because key-B is not a recipient.
|
|
let r15 = (run-one "sops decrypt failed (wrong master_key)" "sops-decrypt-failed" {
|
|
let f = ($TMP | path join "t15")
|
|
let kage_a = ($TMP | path join "key-a.kage")
|
|
let kage_b = ($TMP | path join "key-b.kage")
|
|
^age-keygen -o $kage_a out+err> /dev/null
|
|
^age-keygen -o $kage_b out+err> /dev/null
|
|
chmod 0400 $kage_a
|
|
chmod 0400 $kage_b
|
|
let pub_a = (open --raw $kage_a | lines | where { |l| $l | str contains "public key" } | first | split row " " | last)
|
|
|
|
fixture-base-project $f $'sops = { enabled = true, vault_id = "fixture-t15", master_key_path = "($kage_b)" }'
|
|
write-manifest-ncl $f ' registry_provides = m.make_registry_provides {
|
|
participant = "fixture",
|
|
registries = m.make_registries_config {
|
|
default = "primary",
|
|
registries = [ m.make_registry_entry { id = "primary", endpoint = "x", credential_sops = "registry/ro.sops.yaml" } ],
|
|
},
|
|
},'
|
|
# Pre-create the sops file encrypted ONLY for key-A (key-B cannot decrypt).
|
|
let vault_dir = ($env.HOME | path join ".config" "ontoref" "vaults" "fixture-t15" "src-vault" "registry")
|
|
mkdir $vault_dir
|
|
let sops_path = ($vault_dir | path join "ro.sops.yaml")
|
|
let plain = ($vault_dir | path join "_plain.yaml")
|
|
"username: u\npassword: p\n" | save --force $plain
|
|
let enc = (^sops --age $pub_a --input-type yaml --encrypt $plain)
|
|
$enc | save --force $sops_path
|
|
rm $plain
|
|
|
|
# Now resolve with key-B as master — sops decrypt must fail.
|
|
resolve-registry-credential $f primary --op pull
|
|
})
|
|
$TOTAL = $TOTAL + 1; if $r15.ok { $PASSED = $PASSED + 1 } else { $FAILED = ($FAILED | append "sops-decrypt-failed") }
|
|
rm -rf ($env.HOME | path join ".config" "ontoref" "vaults" "fixture-t15")
|
|
|
|
# ── Report ───────────────────────────────────────────────────────────────────
|
|
|
|
print ""
|
|
let bar = if ($FAILED | is-empty) { (ansi green) } else { (ansi red) }
|
|
print $"($bar)── ($PASSED)/($TOTAL) tests passed(ansi reset)"
|
|
if not ($FAILED | is-empty) {
|
|
print $" failed: ($FAILED | str join ', ')"
|
|
rm -rf $TMP
|
|
exit 1
|
|
}
|
|
|
|
# Cleanup fixtures
|
|
rm -rf $TMP
|