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

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

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

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

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

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

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

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

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

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