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).
258 lines
9.8 KiB
Text
258 lines
9.8 KiB
Text
#!/usr/bin/env nu
|
|
# Vault backend abstraction for src-vault snapshot operations (ADR-017).
|
|
# All functions accept the decrypted vault_key value — they never read sops.
|
|
# Switching backends: change sops.vault_backend in project.ncl, re-init the local repo.
|
|
|
|
use ./secrets.nu [resolve-vault-access]
|
|
|
|
const VAULT_BACKENDS = ["restic" "kopia"]
|
|
|
|
def require-tool [tool: string]: nothing -> string {
|
|
let rows = (which $tool)
|
|
if ($rows | is-empty) {
|
|
error make --unspanned { msg: $"vault backend '($tool)' not found in PATH" }
|
|
}
|
|
$rows | get path | first | into string
|
|
}
|
|
|
|
def assert-backend [tool: string]: nothing -> nothing {
|
|
if $tool not-in $VAULT_BACKENDS {
|
|
error make --unspanned { msg: $"Unknown vault backend: ($tool). Valid: ($VAULT_BACKENDS | str join ', ')" }
|
|
}
|
|
}
|
|
|
|
# Initialize a new vault repository at `repo_path`.
|
|
export def vault-init [
|
|
repo_path: string
|
|
vault_key: string
|
|
--tool: string = "restic"
|
|
]: nothing -> nothing {
|
|
assert-backend $tool
|
|
let bin = (require-tool $tool)
|
|
let result = match $tool {
|
|
"restic" => (with-env { RESTIC_PASSWORD: $vault_key } {
|
|
do { ^$bin -r $repo_path init } | complete
|
|
}),
|
|
"kopia" => (with-env { KOPIA_PASSWORD: $vault_key } {
|
|
do { ^$bin repository create filesystem --path $repo_path } | complete
|
|
}),
|
|
}
|
|
if $result.exit_code != 0 {
|
|
error make --unspanned { msg: $"vault init failed:\n($result.stderr)" }
|
|
}
|
|
}
|
|
|
|
# Create a snapshot of `source_path`. Returns the snapshot short-id.
|
|
export def vault-backup [
|
|
repo_path: string
|
|
source_path: string
|
|
vault_key: string
|
|
--tool: string = "restic"
|
|
--tags: list<string> = []
|
|
]: nothing -> string {
|
|
assert-backend $tool
|
|
let bin = (require-tool $tool)
|
|
let result = match $tool {
|
|
"restic" => {
|
|
let tag_args = ($tags | each { |t| ["--tag" $t] } | flatten)
|
|
with-env { RESTIC_PASSWORD: $vault_key } {
|
|
do { ^$bin -r $repo_path backup $source_path ...$tag_args } | complete
|
|
}
|
|
},
|
|
"kopia" => {
|
|
with-env { KOPIA_PASSWORD: $vault_key } {
|
|
do { ^$bin --config-file ($repo_path | path join "kopia.config") snapshot create $source_path } | complete
|
|
}
|
|
},
|
|
}
|
|
if $result.exit_code != 0 {
|
|
error make --unspanned { msg: $"vault backup failed:\n($result.stderr)" }
|
|
}
|
|
$result.stdout | lines
|
|
| where { |l| $l | str contains "snapshot " }
|
|
| first
|
|
| default ""
|
|
| parse --regex 'snapshot\s+([0-9a-f]+)'
|
|
| get -o capture0
|
|
| first
|
|
| default ""
|
|
}
|
|
|
|
# Restore snapshot `snapshot_id` (or "latest") to `dest_path`.
|
|
export def vault-restore [
|
|
repo_path: string
|
|
dest_path: string
|
|
vault_key: string
|
|
snapshot_id: string = "latest"
|
|
--tool: string = "restic"
|
|
]: nothing -> nothing {
|
|
assert-backend $tool
|
|
let bin = (require-tool $tool)
|
|
let result = match $tool {
|
|
"restic" => (with-env { RESTIC_PASSWORD: $vault_key } {
|
|
do { ^$bin -r $repo_path restore $snapshot_id --target $dest_path } | complete
|
|
}),
|
|
"kopia" => (with-env { KOPIA_PASSWORD: $vault_key } {
|
|
do { ^$bin --config-file ($repo_path | path join "kopia.config") snapshot restore $snapshot_id $dest_path } | complete
|
|
}),
|
|
}
|
|
if $result.exit_code != 0 {
|
|
error make --unspanned { msg: $"vault restore failed:\n($result.stderr)" }
|
|
}
|
|
}
|
|
|
|
# List snapshots. Returns list of {id, time, tags} records.
|
|
export def vault-snapshots [
|
|
repo_path: string
|
|
vault_key: string
|
|
--tool: string = "restic"
|
|
]: nothing -> list {
|
|
assert-backend $tool
|
|
let bin = (require-tool $tool)
|
|
let result = match $tool {
|
|
"restic" => (with-env { RESTIC_PASSWORD: $vault_key } {
|
|
do { ^$bin -r $repo_path snapshots --json } | complete
|
|
}),
|
|
"kopia" => (with-env { KOPIA_PASSWORD: $vault_key } {
|
|
do { ^$bin --config-file ($repo_path | path join "kopia.config") snapshot list --json } | complete
|
|
}),
|
|
}
|
|
if $result.exit_code != 0 {
|
|
error make --unspanned { msg: $"vault snapshots failed:\n($result.stderr)" }
|
|
}
|
|
let raw = ($result.stdout | str trim)
|
|
if ($raw | is-empty) { return [] }
|
|
match $tool {
|
|
"restic" => {
|
|
$raw | from json | each { |s|
|
|
{ id: ($s | get -o short_id | default ""), time: ($s | get -o time | default ""), tags: ($s | get -o tags | default []) }
|
|
}
|
|
},
|
|
"kopia" => {
|
|
$raw | from json | each { |s|
|
|
{ id: ($s | get -o id | default ""), time: ($s | get -o startTime | default ""), tags: [] }
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
# Verify vault repository integrity.
|
|
export def vault-check [
|
|
repo_path: string
|
|
vault_key: string
|
|
--tool: string = "restic"
|
|
]: nothing -> bool {
|
|
assert-backend $tool
|
|
let bin = (require-tool $tool)
|
|
let result = match $tool {
|
|
"restic" => (with-env { RESTIC_PASSWORD: $vault_key } {
|
|
do { ^$bin -r $repo_path check } | complete
|
|
}),
|
|
"kopia" => (with-env { KOPIA_PASSWORD: $vault_key } {
|
|
do { ^$bin --config-file ($repo_path | path join "kopia.config") repository verify-object-integrity } | complete
|
|
}),
|
|
}
|
|
$result.exit_code == 0
|
|
}
|
|
|
|
# ── CLI dispatch — `ore vault X` ──────────────────────────────────────────────
|
|
# High-level wrappers that resolve vault context (project.ncl → vault_id, backend,
|
|
# repo_path, vault_key) and call the primitives above.
|
|
|
|
def _project-root-cli []: nothing -> string {
|
|
let from_env = ($env.ONTOREF_PROJECT_ROOT? | default "")
|
|
if ($from_env | is-not-empty) { return $from_env }
|
|
pwd
|
|
}
|
|
|
|
def _resolve-vault-context [project_root: string]: nothing -> record {
|
|
let project_ncl = ($project_root | path join ".ontoref" "project.ncl")
|
|
if not ($project_ncl | path exists) {
|
|
error make --unspanned { msg: $"No project.ncl at ($project_ncl)" }
|
|
}
|
|
let import_path = ($env.NICKEL_IMPORT_PATH? | default ($env.PROVISIONING? | default ""))
|
|
let r = (do { ^nickel export --import-path $import_path $project_ncl } | complete)
|
|
if $r.exit_code != 0 {
|
|
error make --unspanned { msg: $"Failed to evaluate project.ncl: ($r.stderr)" }
|
|
}
|
|
let project = ($r.stdout | from json)
|
|
let sops_cfg = ($project | get -o sops | default {})
|
|
let vault_id = ($sops_cfg | get -o vault_id | default ($project | get -o slug | default ""))
|
|
if ($vault_id | is-empty) {
|
|
error make --unspanned { msg: "Cannot determine vault_id (slug empty and sops.vault_id absent)" }
|
|
}
|
|
let backend = ($sops_cfg | get -o vault_backend | default "restic")
|
|
let backend_str = if (($backend | describe) == "string") { $backend } else { ($backend | to text) }
|
|
{
|
|
vault_id: $vault_id,
|
|
backend: $backend_str,
|
|
repo_path: ($env.HOME | path join ".config" "ontoref" "vaults" $vault_id "repo"),
|
|
}
|
|
}
|
|
|
|
# List snapshots in the vault repository for the active project.
|
|
export def "vault snapshots" []: nothing -> nothing {
|
|
let project_root = (_project-root-cli)
|
|
let ctx = (_resolve-vault-context $project_root)
|
|
let access = (resolve-vault-access $project_root)
|
|
if ($access.vault_key | is-empty) {
|
|
error make --unspanned {
|
|
msg: "vault_key empty — run: ore secrets sync, then retry"
|
|
}
|
|
}
|
|
let snaps = (vault-snapshots $ctx.repo_path $access.vault_key --tool $ctx.backend)
|
|
if ($snaps | is-empty) {
|
|
print $" No snapshots in ($ctx.repo_path)"
|
|
return
|
|
}
|
|
print $" (ansi white_bold)Vault snapshots(ansi reset) — ($ctx.vault_id) ((($ctx.backend)))"
|
|
for s in $snaps {
|
|
print $" ($s.id) ($s.time) tags=[($s.tags | str join ', ')]"
|
|
}
|
|
}
|
|
|
|
# Run integrity check on the vault repository.
|
|
export def "vault check" []: nothing -> nothing {
|
|
let project_root = (_project-root-cli)
|
|
let ctx = (_resolve-vault-context $project_root)
|
|
let access = (resolve-vault-access $project_root)
|
|
if ($access.vault_key | is-empty) {
|
|
error make --unspanned {
|
|
msg: "vault_key empty — run: ore secrets sync, then retry"
|
|
}
|
|
}
|
|
let ok = (vault-check $ctx.repo_path $access.vault_key --tool $ctx.backend)
|
|
if $ok {
|
|
print $" (ansi green)OK(ansi reset) ($ctx.vault_id) (($ctx.backend)) repository integrity verified"
|
|
} else {
|
|
print $" (ansi red)FAIL(ansi reset) ($ctx.vault_id) (($ctx.backend)) check failed"
|
|
}
|
|
}
|
|
|
|
# Summary: vault context resolved + last snapshot.
|
|
export def "vault status" []: nothing -> nothing {
|
|
let project_root = (_project-root-cli)
|
|
let ctx = (_resolve-vault-context $project_root)
|
|
print $" (ansi white_bold)Vault(ansi reset) ($ctx.vault_id)"
|
|
print $" (ansi dark_gray)backend(ansi reset) ($ctx.backend)"
|
|
print $" (ansi dark_gray)repo(ansi reset) ($ctx.repo_path) (if ($ctx.repo_path | path exists) { '(present)' } else { '(absent)' })"
|
|
if not ($ctx.repo_path | path exists) {
|
|
print " Run: ore secrets bootstrap (admin) or ore secrets sync"
|
|
return
|
|
}
|
|
let access = (try { resolve-vault-access $project_root } catch { |e|
|
|
print $" vault access (ansi red)error(ansi reset): ($e.msg)"
|
|
return
|
|
})
|
|
if ($access.vault_key | is-empty) { return }
|
|
let snaps = (try { vault-snapshots $ctx.repo_path $access.vault_key --tool $ctx.backend } catch { |e|
|
|
print $" snapshots (ansi red)error(ansi reset): ($e.msg)"
|
|
return
|
|
})
|
|
if ($snaps | is-empty) {
|
|
print " snapshots (none)"
|
|
} else {
|
|
let last = ($snaps | last)
|
|
print $" (ansi dark_gray)snapshots(ansi reset) ($snaps | length) total — last: ($last.id) @ ($last.time)"
|
|
}
|
|
}
|