2026-03-13 00:18:14 +00:00
|
|
|
use std::path::{Path, PathBuf};
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
use std::sync::atomic::AtomicU64;
|
|
|
|
|
use std::sync::{Arc, RwLock};
|
2026-03-13 00:18:14 +00:00
|
|
|
|
|
|
|
|
use dashmap::DashMap;
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
use tokio::sync::Semaphore;
|
2026-03-13 00:18:14 +00:00
|
|
|
use tracing::warn;
|
|
|
|
|
|
|
|
|
|
use crate::actors::ActorRegistry;
|
|
|
|
|
use crate::cache::NclCache;
|
|
|
|
|
use crate::notifications::NotificationStore;
|
|
|
|
|
|
feat: config surface, NCL contracts, override-layer mutation, on+re update
Config surface — per-project config introspection, coherence verification, and
audited mutation without destroying NCL structure (ADR-008):
- crates/ontoref-daemon/src/config.rs — typed DaemonNclConfig (parse-at-boundary
pattern); all section structs derive ConfigFields + config_section(id, ncl_file)
emitting inventory::submit!(ConfigFieldsEntry{...}) at link time
- crates/ontoref-derive/src/lib.rs — #[derive(ConfigFields)] proc-macro; serde
rename support; serde_rename_of() helper extracted to fix excessive_nesting
- crates/ontoref-daemon/src/main.rs — 3-tuple bootstrap block (nickel_import_path,
loaded_ncl_config: Option<DaemonNclConfig>, stdin_raw); apply_ui_config takes
&UiConfig; NATS call site typed; resolve_asset_dir cfg(feature = "ui")
- crates/ontoref-daemon/src/api.rs — config GET/PUT endpoints, quickref, coherence,
cross-project comparison; index_section_fields() extracted (excessive_nesting)
- crates/ontoref-daemon/src/config_coherence.rs — multi-consumer coherence;
merge_meta_into_section() extracted; and() replaces unnecessary and_then
NCL contracts for ontoref's own config:
- .ontoref/contracts.ncl — LogConfig (LogLevel, LogRotation, PositiveInt) and
DaemonConfig (Port, optional overrides); std.contract.from_validator throughout
- .ontoref/config.ncl — log | C.LogConfig applied
- .ontology/manifest.ncl — contracts_path, log/daemon contract refs, daemon section
with DaemonRuntimeConfig consumer and 7 declared fields
Protocol:
- adrs/adr-008-ncl-first-config-validation-and-override-layer.ncl — NCL contracts
as single validation gate; Rust structs are contract-trusted; override-layer
mutation writes {section}.overrides.ncl + _overrides_meta, never touches source
on+re update:
- .ontology/core.ncl — config-surface node (28 practices); adr-lifecycle extended
to adr-007 + adr-008; 6 new edges (ManifestsIn daemon, DependsOn ontology-crate,
Complements api-catalog-surface/dag-formalized/self-describing/adopt-ontoref)
- .ontology/state.ncl — protocol-maturity blocker and self-description-coverage
catalyst updated for session 2026-03-26
- README.md / CHANGELOG.md updated
2026-03-26 20:20:22 +00:00
|
|
|
// ── Config surface
|
|
|
|
|
// ────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/// Which process reads a config section.
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(rename_all = "snake_case")]
|
|
|
|
|
pub enum ConsumerKind {
|
|
|
|
|
RustStruct,
|
|
|
|
|
NuScript,
|
|
|
|
|
CiPipeline,
|
|
|
|
|
External,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A single process that reads fields from a config section.
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct ConfigConsumer {
|
|
|
|
|
/// Identifier for this consumer (e.g. "vapora-backend", "deploy-script").
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub kind: ConsumerKind,
|
|
|
|
|
/// Rust fully-qualified type or script path.
|
|
|
|
|
pub reference: String,
|
|
|
|
|
/// Fields this consumer reads. Empty = reads all fields.
|
|
|
|
|
pub fields: Vec<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Describes one NCL config file and all processes that read it.
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct ConfigSection {
|
|
|
|
|
pub id: String,
|
|
|
|
|
/// Path to the NCL file, relative to `config_root`.
|
|
|
|
|
pub file: String,
|
|
|
|
|
/// Path to the NCL contract file. Relative to `contracts_path` or project
|
|
|
|
|
/// root.
|
|
|
|
|
pub contract: String,
|
|
|
|
|
pub description: String,
|
|
|
|
|
/// Why this section exists and why current values were chosen.
|
|
|
|
|
pub rationale: String,
|
|
|
|
|
/// When false, ontoref will only read this section, never write.
|
|
|
|
|
pub mutable: bool,
|
|
|
|
|
pub consumers: Vec<ConfigConsumer>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// How the project's config files are organised.
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(rename_all = "snake_case")]
|
|
|
|
|
pub enum ConfigKind {
|
|
|
|
|
/// Multiple .ncl files merged via & operator.
|
|
|
|
|
NclMerge,
|
|
|
|
|
/// .typedialog/ structure with form.toml + validators + fragments.
|
|
|
|
|
TypeDialog,
|
|
|
|
|
/// Single monolithic .ncl file.
|
|
|
|
|
SingleFile,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Project-level config surface metadata — loaded once from manifest.ncl at
|
|
|
|
|
/// registration time and stored in `ProjectContext`.
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct ConfigSurface {
|
|
|
|
|
/// Directory containing config NCL files, relative to project root.
|
|
|
|
|
pub config_root: PathBuf,
|
|
|
|
|
/// Main NCL file (entry point for `nickel export`).
|
|
|
|
|
pub entry_point: String,
|
|
|
|
|
pub kind: ConfigKind,
|
|
|
|
|
/// Directory added to NICKEL_IMPORT_PATH when exporting config.
|
|
|
|
|
pub contracts_path: String,
|
|
|
|
|
/// Where ontoref writes `{section}.overrides.ncl` files.
|
|
|
|
|
/// Defaults to `config_root` when empty.
|
|
|
|
|
pub overrides_dir: PathBuf,
|
|
|
|
|
pub sections: Vec<ConfigSection>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ConfigSurface {
|
|
|
|
|
/// Resolve the directory where override files are written.
|
|
|
|
|
/// Returns `overrides_dir` if set, otherwise `config_root`.
|
|
|
|
|
pub fn resolved_overrides_dir(&self) -> &Path {
|
|
|
|
|
if self.overrides_dir == PathBuf::new() {
|
|
|
|
|
&self.config_root
|
|
|
|
|
} else {
|
|
|
|
|
&self.overrides_dir
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Resolve the absolute path to the config entry point given the project
|
|
|
|
|
/// root.
|
|
|
|
|
pub fn entry_point_path(&self, project_root: &Path) -> PathBuf {
|
|
|
|
|
project_root.join(&self.config_root).join(&self.entry_point)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Find a section by id.
|
|
|
|
|
pub fn section(&self, id: &str) -> Option<&ConfigSection> {
|
|
|
|
|
self.sections.iter().find(|s| s.id == id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Auth / keys
|
|
|
|
|
// ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
2026-03-13 00:18:14 +00:00
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(rename_all = "lowercase")]
|
|
|
|
|
pub enum Role {
|
|
|
|
|
Admin,
|
|
|
|
|
Viewer,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct KeyEntry {
|
|
|
|
|
pub role: Role,
|
|
|
|
|
/// Argon2id PHC string — produced by `ontoref-daemon --hash-password <pw>`
|
|
|
|
|
pub hash: String,
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
/// Human-readable label identifying which key this is (e.g. "developer",
|
|
|
|
|
/// "agent", "ci"). Recorded in sessions for audit tracing. Optional —
|
|
|
|
|
/// defaults to empty string for keys created before this field existed.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub label: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Result of a successful key verification — role and the label of the
|
|
|
|
|
/// matched key for audit trail purposes.
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct KeyMatch {
|
|
|
|
|
pub role: Role,
|
|
|
|
|
pub label: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Verify `password` against a slice of `KeyEntry`s using argon2id.
|
|
|
|
|
///
|
|
|
|
|
/// Returns the `KeyMatch` (role + label) of the first matching key, or `None`
|
|
|
|
|
/// if no key matches. Unparseable PHC hashes are logged and skipped.
|
|
|
|
|
pub fn verify_keys_list(keys: &[KeyEntry], password: &str) -> Option<KeyMatch> {
|
|
|
|
|
use argon2::{Argon2, PasswordHash, PasswordVerifier};
|
|
|
|
|
for key in keys {
|
|
|
|
|
let Ok(parsed) = PasswordHash::new(&key.hash) else {
|
|
|
|
|
warn!(role = ?key.role, label = %key.label, "skipping key entry with unparseable PHC hash — key is inactive");
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
if Argon2::default()
|
|
|
|
|
.verify_password(password.as_bytes(), &parsed)
|
|
|
|
|
.is_ok()
|
|
|
|
|
{
|
|
|
|
|
return Some(KeyMatch {
|
|
|
|
|
role: key.role,
|
|
|
|
|
label: key.label.clone(),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None
|
2026-03-13 00:18:14 +00:00
|
|
|
}
|
|
|
|
|
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
/// Entry as it arrives from the JSON config pipe (ADR-004).
|
2026-03-13 00:18:14 +00:00
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct RegistryEntry {
|
|
|
|
|
pub slug: String,
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
/// Absolute local path. Empty for push_only projects.
|
|
|
|
|
#[serde(default)]
|
2026-03-13 00:18:14 +00:00
|
|
|
pub root: PathBuf,
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
/// Import paths for nickel export (local projects only).
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub nickel_import_paths: Vec<String>,
|
2026-03-13 00:18:14 +00:00
|
|
|
#[serde(default)]
|
|
|
|
|
pub keys: Vec<KeyEntry>,
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
/// Git/SSH/HTTPS URL. Informational — daemon never fetches from it.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub remote_url: String,
|
|
|
|
|
/// When true: no local file watch, no NCL import. Project pushes via POST
|
|
|
|
|
/// /sync.
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub push_only: bool,
|
2026-03-13 00:18:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Per-project runtime state owned by the registry.
|
|
|
|
|
pub struct ProjectContext {
|
|
|
|
|
pub slug: String,
|
|
|
|
|
pub root: PathBuf,
|
|
|
|
|
pub import_path: Option<String>,
|
|
|
|
|
pub cache: Arc<NclCache>,
|
|
|
|
|
pub actors: Arc<ActorRegistry>,
|
|
|
|
|
pub notifications: Arc<NotificationStore>,
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
/// Mutable at runtime via `PUT /projects/{slug}/keys`.
|
|
|
|
|
pub keys: RwLock<Vec<KeyEntry>>,
|
|
|
|
|
/// Git/SSH/HTTPS remote URL. Informational.
|
|
|
|
|
pub remote_url: String,
|
|
|
|
|
/// No local file watch, no NCL import — project pushes via POST /sync.
|
|
|
|
|
pub push_only: bool,
|
|
|
|
|
/// Ensures only one `seed_ontology` runs at a time per project.
|
|
|
|
|
/// Prevents concurrent re-seeds caused by rapid filesystem events.
|
|
|
|
|
pub seed_lock: Arc<Semaphore>,
|
|
|
|
|
/// Incremented by 1 after each successful `seed_ontology` completion.
|
|
|
|
|
/// Clients can compare versions to detect stale local state.
|
|
|
|
|
pub ontology_version: Arc<AtomicU64>,
|
---
feat: API catalog surface, protocol v2 tooling, MCP expansion, on+re update
## Summary
Session 2026-03-23. Closes the loop between handler code and discoverability
across all three surfaces (browser, CLI, MCP agent) via compile-time inventory
registration. Adds protocol v2 update tooling, extends MCP from 21 to 29 tools,
and brings the self-description up to date.
## API Catalog Surface (#[onto_api] proc-macro)
- crates/ontoref-derive: new proc-macro crate; `#[onto_api(method, path,
description, auth, actors, params, tags)]` emits `inventory::submit!(ApiRouteEntry{...})`
at link time
- crates/ontoref-daemon/src/api_catalog.rs: `catalog()` — pure fn over
`inventory::iter::<ApiRouteEntry>()`, zero runtime allocation
- GET /api/catalog: returns full annotated HTTP surface as JSON
- templates/pages/api_catalog.html: new page with client-side filtering by
method, auth, path/description; detail panel per route (params table,
feature flag); linked from dashboard card and nav
- UI nav: "API" link (</> icon) added to mobile dropdown and desktop bar
- inventory = "0.3" added to workspace.dependencies (MIT, zero transitive deps)
## Protocol Update Mode
- reflection/modes/update_ontoref.ncl: 9-step DAG (5 detect parallel, 2 update
idempotent, 2 validate, 1 report) — brings any project from protocol v1 to v2
by adding manifest.ncl and connections.ncl if absent, scanning ADRs for
deprecated check_hint, validating with nickel export
- reflection/templates/update-ontology-prompt.md: 8-phase reusable prompt for
agent-driven ontology enrichment (infrastructure → audit → core.ncl →
state.ncl → manifest.ncl → connections.ncl → ADR migration → validation)
## CLI — describe group extensions
- reflection/bin/ontoref.nu: `describe diff [--fmt] [--file]` and
`describe api [--actor] [--tag] [--auth] [--fmt]` registered as canonical
subcommands with log-action; aliases `df` and `da` added; QUICK REFERENCE
and ALIASES sections updated
## MCP — two new tools (21 → 29 total)
- ontoref_api_catalog: filters catalog() output by actor/tag/auth; returns
{ routes, total } — no HTTP roundtrip, calls inventory directly
- ontoref_file_versions: reads ProjectContext.file_versions DashMap per slug;
returns BTreeMap<filename, u64> reload counters
- insert_mcp_ctx: audited and updated from 15 to 28 entries in 6 groups
- HelpTool JSON: 8 new entries (validate_adrs, validate, impact, guides,
bookmark_list, bookmark_add, api_catalog, file_versions)
- ServerHandler::get_info instructions updated to mention new tools
## Web UI — dashboard additions
- Dashboard: "API Catalog" card (9th); "Ontology File Versions" section showing
per-file reload counters from file_versions DashMap
- dashboard_mp: builds BTreeMap<String, u64> from ctx.file_versions and injects
into Tera context
## on+re update
- .ontology/core.ncl: describe-query-layer and adopt-ontoref-tooling descriptions
updated; ontoref-daemon updated ("11 pages", "29 tools", API catalog,
per-file versioning, #[onto_api]); new node api-catalog-surface (Yang/Practice)
with 3 edges; artifact_paths extended across 3 nodes
- .ontology/state.ncl: protocol-maturity blocker updated (protocol v2 complete);
self-description-coverage catalyst updated with session 2026-03-23 additions
- ADR-007: "API Surface Discoverability via #[onto_api] Proc-Macro" — Accepted
## Documentation
- README.md: crates table updated (11 pages, 29 MCP tools, ontoref-derive row);
MCP representative table expanded; API Catalog, Semantic Diff, Per-File
Versioning paragraphs added; update_ontoref onboarding section added
- CHANGELOG.md: [Unreleased] section with 4 change groups
- assets/web/src/index.html: tool counts 19→29 (EN+ES), page counts 12→11
(EN+ES), daemon description paragraph updated with API catalog + #[onto_api]
2026-03-23 00:58:27 +01:00
|
|
|
/// Per-file change counters. Keyed by canonical absolute path; incremented
|
|
|
|
|
/// on every cache invalidation for that file. Consumers compare snapshots
|
|
|
|
|
/// to detect which individual files changed between polls.
|
|
|
|
|
pub file_versions: Arc<DashMap<PathBuf, u64>>,
|
feat: config surface, NCL contracts, override-layer mutation, on+re update
Config surface — per-project config introspection, coherence verification, and
audited mutation without destroying NCL structure (ADR-008):
- crates/ontoref-daemon/src/config.rs — typed DaemonNclConfig (parse-at-boundary
pattern); all section structs derive ConfigFields + config_section(id, ncl_file)
emitting inventory::submit!(ConfigFieldsEntry{...}) at link time
- crates/ontoref-derive/src/lib.rs — #[derive(ConfigFields)] proc-macro; serde
rename support; serde_rename_of() helper extracted to fix excessive_nesting
- crates/ontoref-daemon/src/main.rs — 3-tuple bootstrap block (nickel_import_path,
loaded_ncl_config: Option<DaemonNclConfig>, stdin_raw); apply_ui_config takes
&UiConfig; NATS call site typed; resolve_asset_dir cfg(feature = "ui")
- crates/ontoref-daemon/src/api.rs — config GET/PUT endpoints, quickref, coherence,
cross-project comparison; index_section_fields() extracted (excessive_nesting)
- crates/ontoref-daemon/src/config_coherence.rs — multi-consumer coherence;
merge_meta_into_section() extracted; and() replaces unnecessary and_then
NCL contracts for ontoref's own config:
- .ontoref/contracts.ncl — LogConfig (LogLevel, LogRotation, PositiveInt) and
DaemonConfig (Port, optional overrides); std.contract.from_validator throughout
- .ontoref/config.ncl — log | C.LogConfig applied
- .ontology/manifest.ncl — contracts_path, log/daemon contract refs, daemon section
with DaemonRuntimeConfig consumer and 7 declared fields
Protocol:
- adrs/adr-008-ncl-first-config-validation-and-override-layer.ncl — NCL contracts
as single validation gate; Rust structs are contract-trusted; override-layer
mutation writes {section}.overrides.ncl + _overrides_meta, never touches source
on+re update:
- .ontology/core.ncl — config-surface node (28 practices); adr-lifecycle extended
to adr-007 + adr-008; 6 new edges (ManifestsIn daemon, DependsOn ontology-crate,
Complements api-catalog-surface/dag-formalized/self-describing/adopt-ontoref)
- .ontology/state.ncl — protocol-maturity blocker and self-description-coverage
catalyst updated for session 2026-03-26
- README.md / CHANGELOG.md updated
2026-03-26 20:20:22 +00:00
|
|
|
/// Config surface metadata loaded from the project's manifest.ncl at
|
|
|
|
|
/// registration time. `None` when the project's manifest has no
|
|
|
|
|
/// `config_surface` field or when the manifest can't be exported.
|
|
|
|
|
pub config_surface: Option<ConfigSurface>,
|
2026-03-13 00:18:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ProjectContext {
|
|
|
|
|
pub fn auth_enabled(&self) -> bool {
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
!self
|
|
|
|
|
.keys
|
|
|
|
|
.read()
|
|
|
|
|
.unwrap_or_else(|e| e.into_inner())
|
|
|
|
|
.is_empty()
|
2026-03-13 00:18:14 +00:00
|
|
|
}
|
|
|
|
|
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
pub fn verify_key(&self, password: &str) -> Option<KeyMatch> {
|
|
|
|
|
let keys = self.keys.read().unwrap_or_else(|e| e.into_inner());
|
|
|
|
|
verify_keys_list(&keys, password)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Replace the current key set. Returns the number of keys now active.
|
|
|
|
|
pub fn set_keys(&self, new_keys: Vec<KeyEntry>) -> usize {
|
|
|
|
|
let mut guard = self.keys.write().unwrap_or_else(|e| e.into_inner());
|
|
|
|
|
*guard = new_keys;
|
|
|
|
|
guard.len()
|
2026-03-13 00:18:14 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct ProjectRegistry {
|
|
|
|
|
contexts: DashMap<String, Arc<ProjectContext>>,
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
/// Slug of the primary (local) project — always present in `contexts`.
|
|
|
|
|
primary_slug: String,
|
2026-03-13 00:18:14 +00:00
|
|
|
stale_actor_timeout: u64,
|
|
|
|
|
max_notifications: usize,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ProjectRegistry {
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
/// Build a registry with a pre-constructed primary project context, plus
|
|
|
|
|
/// optional additional entries (service mode). The primary context is
|
|
|
|
|
/// inserted under `primary_slug` and accessible via `primary()`.
|
|
|
|
|
///
|
|
|
|
|
/// Returns `(registry, Arc<ProjectContext>)` so the caller can extract
|
|
|
|
|
/// shared `Arc` references (cache, actors, notifications) before consuming
|
|
|
|
|
/// the primary context into the registry.
|
|
|
|
|
pub fn with_primary(
|
|
|
|
|
primary_slug: String,
|
|
|
|
|
primary_ctx: Arc<ProjectContext>,
|
|
|
|
|
additional_entries: Vec<RegistryEntry>,
|
2026-03-13 00:18:14 +00:00
|
|
|
stale_actor_timeout: u64,
|
|
|
|
|
max_notifications: usize,
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
) -> Self {
|
2026-03-13 00:18:14 +00:00
|
|
|
let registry = Self {
|
|
|
|
|
contexts: DashMap::new(),
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
primary_slug: primary_slug.clone(),
|
2026-03-13 00:18:14 +00:00
|
|
|
stale_actor_timeout,
|
|
|
|
|
max_notifications,
|
|
|
|
|
};
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
registry.contexts.insert(primary_slug.clone(), primary_ctx);
|
2026-03-13 00:18:14 +00:00
|
|
|
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
for entry in additional_entries {
|
|
|
|
|
if entry.slug == primary_slug {
|
|
|
|
|
warn!(
|
|
|
|
|
slug = %entry.slug,
|
|
|
|
|
"additional registry entry collides with primary slug — skipping"
|
|
|
|
|
);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if entry.push_only {
|
|
|
|
|
let ctx = make_context(ContextSpec {
|
|
|
|
|
slug: entry.slug.clone(),
|
|
|
|
|
root: PathBuf::new(),
|
|
|
|
|
import_path: None,
|
|
|
|
|
keys: entry.keys,
|
|
|
|
|
remote_url: entry.remote_url,
|
|
|
|
|
push_only: true,
|
|
|
|
|
stale_actor_timeout,
|
|
|
|
|
max_notifications,
|
|
|
|
|
ack_required: vec![],
|
feat: config surface, NCL contracts, override-layer mutation, on+re update
Config surface — per-project config introspection, coherence verification, and
audited mutation without destroying NCL structure (ADR-008):
- crates/ontoref-daemon/src/config.rs — typed DaemonNclConfig (parse-at-boundary
pattern); all section structs derive ConfigFields + config_section(id, ncl_file)
emitting inventory::submit!(ConfigFieldsEntry{...}) at link time
- crates/ontoref-derive/src/lib.rs — #[derive(ConfigFields)] proc-macro; serde
rename support; serde_rename_of() helper extracted to fix excessive_nesting
- crates/ontoref-daemon/src/main.rs — 3-tuple bootstrap block (nickel_import_path,
loaded_ncl_config: Option<DaemonNclConfig>, stdin_raw); apply_ui_config takes
&UiConfig; NATS call site typed; resolve_asset_dir cfg(feature = "ui")
- crates/ontoref-daemon/src/api.rs — config GET/PUT endpoints, quickref, coherence,
cross-project comparison; index_section_fields() extracted (excessive_nesting)
- crates/ontoref-daemon/src/config_coherence.rs — multi-consumer coherence;
merge_meta_into_section() extracted; and() replaces unnecessary and_then
NCL contracts for ontoref's own config:
- .ontoref/contracts.ncl — LogConfig (LogLevel, LogRotation, PositiveInt) and
DaemonConfig (Port, optional overrides); std.contract.from_validator throughout
- .ontoref/config.ncl — log | C.LogConfig applied
- .ontology/manifest.ncl — contracts_path, log/daemon contract refs, daemon section
with DaemonRuntimeConfig consumer and 7 declared fields
Protocol:
- adrs/adr-008-ncl-first-config-validation-and-override-layer.ncl — NCL contracts
as single validation gate; Rust structs are contract-trusted; override-layer
mutation writes {section}.overrides.ncl + _overrides_meta, never touches source
on+re update:
- .ontology/core.ncl — config-surface node (28 practices); adr-lifecycle extended
to adr-007 + adr-008; 6 new edges (ManifestsIn daemon, DependsOn ontology-crate,
Complements api-catalog-surface/dag-formalized/self-describing/adopt-ontoref)
- .ontology/state.ncl — protocol-maturity blocker and self-description-coverage
catalyst updated for session 2026-03-26
- README.md / CHANGELOG.md updated
2026-03-26 20:20:22 +00:00
|
|
|
config_surface: None,
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
});
|
|
|
|
|
registry.contexts.insert(entry.slug, Arc::new(ctx));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-03-13 00:18:14 +00:00
|
|
|
|
2026-03-13 01:19:53 +00:00
|
|
|
let root = match entry.root.canonicalize() {
|
|
|
|
|
Ok(p) => p,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
warn!(
|
|
|
|
|
slug = %entry.slug,
|
|
|
|
|
path = %entry.root.display(),
|
|
|
|
|
error = %e,
|
|
|
|
|
"project root not found — skipping"
|
|
|
|
|
);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-03-13 00:18:14 +00:00
|
|
|
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
let import_path = resolve_import_path(&entry.nickel_import_paths, &root);
|
feat: config surface, NCL contracts, override-layer mutation, on+re update
Config surface — per-project config introspection, coherence verification, and
audited mutation without destroying NCL structure (ADR-008):
- crates/ontoref-daemon/src/config.rs — typed DaemonNclConfig (parse-at-boundary
pattern); all section structs derive ConfigFields + config_section(id, ncl_file)
emitting inventory::submit!(ConfigFieldsEntry{...}) at link time
- crates/ontoref-derive/src/lib.rs — #[derive(ConfigFields)] proc-macro; serde
rename support; serde_rename_of() helper extracted to fix excessive_nesting
- crates/ontoref-daemon/src/main.rs — 3-tuple bootstrap block (nickel_import_path,
loaded_ncl_config: Option<DaemonNclConfig>, stdin_raw); apply_ui_config takes
&UiConfig; NATS call site typed; resolve_asset_dir cfg(feature = "ui")
- crates/ontoref-daemon/src/api.rs — config GET/PUT endpoints, quickref, coherence,
cross-project comparison; index_section_fields() extracted (excessive_nesting)
- crates/ontoref-daemon/src/config_coherence.rs — multi-consumer coherence;
merge_meta_into_section() extracted; and() replaces unnecessary and_then
NCL contracts for ontoref's own config:
- .ontoref/contracts.ncl — LogConfig (LogLevel, LogRotation, PositiveInt) and
DaemonConfig (Port, optional overrides); std.contract.from_validator throughout
- .ontoref/config.ncl — log | C.LogConfig applied
- .ontology/manifest.ncl — contracts_path, log/daemon contract refs, daemon section
with DaemonRuntimeConfig consumer and 7 declared fields
Protocol:
- adrs/adr-008-ncl-first-config-validation-and-override-layer.ncl — NCL contracts
as single validation gate; Rust structs are contract-trusted; override-layer
mutation writes {section}.overrides.ncl + _overrides_meta, never touches source
on+re update:
- .ontology/core.ncl — config-surface node (28 practices); adr-lifecycle extended
to adr-007 + adr-008; 6 new edges (ManifestsIn daemon, DependsOn ontology-crate,
Complements api-catalog-surface/dag-formalized/self-describing/adopt-ontoref)
- .ontology/state.ncl — protocol-maturity blocker and self-description-coverage
catalyst updated for session 2026-03-26
- README.md / CHANGELOG.md updated
2026-03-26 20:20:22 +00:00
|
|
|
let config_surface = load_config_surface(&root, import_path.as_deref());
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
let ctx = make_context(ContextSpec {
|
|
|
|
|
slug: entry.slug.clone(),
|
2026-03-13 00:18:14 +00:00
|
|
|
root,
|
|
|
|
|
import_path,
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
keys: entry.keys,
|
|
|
|
|
remote_url: entry.remote_url,
|
|
|
|
|
push_only: false,
|
|
|
|
|
stale_actor_timeout,
|
|
|
|
|
max_notifications,
|
|
|
|
|
ack_required: vec![],
|
feat: config surface, NCL contracts, override-layer mutation, on+re update
Config surface — per-project config introspection, coherence verification, and
audited mutation without destroying NCL structure (ADR-008):
- crates/ontoref-daemon/src/config.rs — typed DaemonNclConfig (parse-at-boundary
pattern); all section structs derive ConfigFields + config_section(id, ncl_file)
emitting inventory::submit!(ConfigFieldsEntry{...}) at link time
- crates/ontoref-derive/src/lib.rs — #[derive(ConfigFields)] proc-macro; serde
rename support; serde_rename_of() helper extracted to fix excessive_nesting
- crates/ontoref-daemon/src/main.rs — 3-tuple bootstrap block (nickel_import_path,
loaded_ncl_config: Option<DaemonNclConfig>, stdin_raw); apply_ui_config takes
&UiConfig; NATS call site typed; resolve_asset_dir cfg(feature = "ui")
- crates/ontoref-daemon/src/api.rs — config GET/PUT endpoints, quickref, coherence,
cross-project comparison; index_section_fields() extracted (excessive_nesting)
- crates/ontoref-daemon/src/config_coherence.rs — multi-consumer coherence;
merge_meta_into_section() extracted; and() replaces unnecessary and_then
NCL contracts for ontoref's own config:
- .ontoref/contracts.ncl — LogConfig (LogLevel, LogRotation, PositiveInt) and
DaemonConfig (Port, optional overrides); std.contract.from_validator throughout
- .ontoref/config.ncl — log | C.LogConfig applied
- .ontology/manifest.ncl — contracts_path, log/daemon contract refs, daemon section
with DaemonRuntimeConfig consumer and 7 declared fields
Protocol:
- adrs/adr-008-ncl-first-config-validation-and-override-layer.ncl — NCL contracts
as single validation gate; Rust structs are contract-trusted; override-layer
mutation writes {section}.overrides.ncl + _overrides_meta, never touches source
on+re update:
- .ontology/core.ncl — config-surface node (28 practices); adr-lifecycle extended
to adr-007 + adr-008; 6 new edges (ManifestsIn daemon, DependsOn ontology-crate,
Complements api-catalog-surface/dag-formalized/self-describing/adopt-ontoref)
- .ontology/state.ncl — protocol-maturity blocker and self-description-coverage
catalyst updated for session 2026-03-26
- README.md / CHANGELOG.md updated
2026-03-26 20:20:22 +00:00
|
|
|
config_surface,
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
});
|
|
|
|
|
registry.contexts.insert(entry.slug, Arc::new(ctx));
|
2026-03-13 00:18:14 +00:00
|
|
|
}
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
|
|
|
|
|
registry
|
2026-03-13 00:18:14 +00:00
|
|
|
}
|
|
|
|
|
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
/// The slug of the primary (local) project.
|
|
|
|
|
pub fn primary_slug(&self) -> &str {
|
|
|
|
|
&self.primary_slug
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The primary project context. Panics if not found — invariant guaranteed
|
|
|
|
|
/// by `with_primary` constructor.
|
|
|
|
|
pub fn primary(&self) -> Arc<ProjectContext> {
|
|
|
|
|
self.contexts
|
|
|
|
|
.get(&self.primary_slug)
|
|
|
|
|
.map(|r| Arc::clone(&*r))
|
|
|
|
|
.expect("primary project context missing — registry invariant violated")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Add a project at runtime (UI-triggered, not from bootstrap).
|
2026-03-13 00:18:14 +00:00
|
|
|
pub fn add_project(&self, entry: RegistryEntry) -> anyhow::Result<()> {
|
|
|
|
|
if self.contexts.contains_key(&entry.slug) {
|
|
|
|
|
anyhow::bail!("project '{}' already exists", entry.slug);
|
|
|
|
|
}
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
let (root, import_path) = if entry.push_only {
|
|
|
|
|
(PathBuf::new(), None)
|
|
|
|
|
} else {
|
|
|
|
|
let r = entry
|
|
|
|
|
.root
|
|
|
|
|
.canonicalize()
|
|
|
|
|
.map_err(|e| anyhow::anyhow!("project '{}': root path error: {}", entry.slug, e))?;
|
|
|
|
|
let ip = resolve_import_path(&entry.nickel_import_paths, &r);
|
|
|
|
|
(r, ip)
|
|
|
|
|
};
|
feat: config surface, NCL contracts, override-layer mutation, on+re update
Config surface — per-project config introspection, coherence verification, and
audited mutation without destroying NCL structure (ADR-008):
- crates/ontoref-daemon/src/config.rs — typed DaemonNclConfig (parse-at-boundary
pattern); all section structs derive ConfigFields + config_section(id, ncl_file)
emitting inventory::submit!(ConfigFieldsEntry{...}) at link time
- crates/ontoref-derive/src/lib.rs — #[derive(ConfigFields)] proc-macro; serde
rename support; serde_rename_of() helper extracted to fix excessive_nesting
- crates/ontoref-daemon/src/main.rs — 3-tuple bootstrap block (nickel_import_path,
loaded_ncl_config: Option<DaemonNclConfig>, stdin_raw); apply_ui_config takes
&UiConfig; NATS call site typed; resolve_asset_dir cfg(feature = "ui")
- crates/ontoref-daemon/src/api.rs — config GET/PUT endpoints, quickref, coherence,
cross-project comparison; index_section_fields() extracted (excessive_nesting)
- crates/ontoref-daemon/src/config_coherence.rs — multi-consumer coherence;
merge_meta_into_section() extracted; and() replaces unnecessary and_then
NCL contracts for ontoref's own config:
- .ontoref/contracts.ncl — LogConfig (LogLevel, LogRotation, PositiveInt) and
DaemonConfig (Port, optional overrides); std.contract.from_validator throughout
- .ontoref/config.ncl — log | C.LogConfig applied
- .ontology/manifest.ncl — contracts_path, log/daemon contract refs, daemon section
with DaemonRuntimeConfig consumer and 7 declared fields
Protocol:
- adrs/adr-008-ncl-first-config-validation-and-override-layer.ncl — NCL contracts
as single validation gate; Rust structs are contract-trusted; override-layer
mutation writes {section}.overrides.ncl + _overrides_meta, never touches source
on+re update:
- .ontology/core.ncl — config-surface node (28 practices); adr-lifecycle extended
to adr-007 + adr-008; 6 new edges (ManifestsIn daemon, DependsOn ontology-crate,
Complements api-catalog-surface/dag-formalized/self-describing/adopt-ontoref)
- .ontology/state.ncl — protocol-maturity blocker and self-description-coverage
catalyst updated for session 2026-03-26
- README.md / CHANGELOG.md updated
2026-03-26 20:20:22 +00:00
|
|
|
let config_surface = if entry.push_only {
|
|
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
load_config_surface(&root, import_path.as_deref())
|
|
|
|
|
};
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
let ctx = make_context(ContextSpec {
|
|
|
|
|
slug: entry.slug.clone(),
|
2026-03-13 00:18:14 +00:00
|
|
|
root,
|
|
|
|
|
import_path,
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
keys: entry.keys,
|
|
|
|
|
remote_url: entry.remote_url,
|
|
|
|
|
push_only: entry.push_only,
|
|
|
|
|
stale_actor_timeout: self.stale_actor_timeout,
|
|
|
|
|
max_notifications: self.max_notifications,
|
|
|
|
|
ack_required: vec![],
|
feat: config surface, NCL contracts, override-layer mutation, on+re update
Config surface — per-project config introspection, coherence verification, and
audited mutation without destroying NCL structure (ADR-008):
- crates/ontoref-daemon/src/config.rs — typed DaemonNclConfig (parse-at-boundary
pattern); all section structs derive ConfigFields + config_section(id, ncl_file)
emitting inventory::submit!(ConfigFieldsEntry{...}) at link time
- crates/ontoref-derive/src/lib.rs — #[derive(ConfigFields)] proc-macro; serde
rename support; serde_rename_of() helper extracted to fix excessive_nesting
- crates/ontoref-daemon/src/main.rs — 3-tuple bootstrap block (nickel_import_path,
loaded_ncl_config: Option<DaemonNclConfig>, stdin_raw); apply_ui_config takes
&UiConfig; NATS call site typed; resolve_asset_dir cfg(feature = "ui")
- crates/ontoref-daemon/src/api.rs — config GET/PUT endpoints, quickref, coherence,
cross-project comparison; index_section_fields() extracted (excessive_nesting)
- crates/ontoref-daemon/src/config_coherence.rs — multi-consumer coherence;
merge_meta_into_section() extracted; and() replaces unnecessary and_then
NCL contracts for ontoref's own config:
- .ontoref/contracts.ncl — LogConfig (LogLevel, LogRotation, PositiveInt) and
DaemonConfig (Port, optional overrides); std.contract.from_validator throughout
- .ontoref/config.ncl — log | C.LogConfig applied
- .ontology/manifest.ncl — contracts_path, log/daemon contract refs, daemon section
with DaemonRuntimeConfig consumer and 7 declared fields
Protocol:
- adrs/adr-008-ncl-first-config-validation-and-override-layer.ncl — NCL contracts
as single validation gate; Rust structs are contract-trusted; override-layer
mutation writes {section}.overrides.ncl + _overrides_meta, never touches source
on+re update:
- .ontology/core.ncl — config-surface node (28 practices); adr-lifecycle extended
to adr-007 + adr-008; 6 new edges (ManifestsIn daemon, DependsOn ontology-crate,
Complements api-catalog-surface/dag-formalized/self-describing/adopt-ontoref)
- .ontology/state.ncl — protocol-maturity blocker and self-description-coverage
catalyst updated for session 2026-03-26
- README.md / CHANGELOG.md updated
2026-03-26 20:20:22 +00:00
|
|
|
config_surface,
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
});
|
2026-03-13 00:18:14 +00:00
|
|
|
self.contexts.insert(entry.slug, Arc::new(ctx));
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
Ok(())
|
2026-03-13 00:18:14 +00:00
|
|
|
}
|
|
|
|
|
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
/// Remove a project by slug.
|
|
|
|
|
pub fn remove_project(&self, slug: &str) {
|
2026-03-13 00:18:14 +00:00
|
|
|
self.contexts.remove(slug);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn get(&self, slug: &str) -> Option<Arc<ProjectContext>> {
|
|
|
|
|
self.contexts.get(slug).map(|r| Arc::clone(&*r))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn all(&self) -> Vec<Arc<ProjectContext>> {
|
|
|
|
|
let mut list: Vec<_> = self.contexts.iter().map(|r| Arc::clone(&*r)).collect();
|
|
|
|
|
list.sort_by(|a, b| a.slug.cmp(&b.slug));
|
|
|
|
|
list
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn count(&self) -> usize {
|
|
|
|
|
self.contexts.len()
|
|
|
|
|
}
|
|
|
|
|
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
/// Replace the key set for a registered project.
|
|
|
|
|
/// Returns `None` if the slug is not found.
|
|
|
|
|
pub fn update_keys(&self, slug: &str, keys: Vec<KeyEntry>) -> Option<usize> {
|
|
|
|
|
self.contexts.get(slug).map(|ctx| ctx.set_keys(keys))
|
2026-03-13 00:18:14 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
/// Resolve nickel_import_paths entries into a colon-separated
|
|
|
|
|
/// NICKEL_IMPORT_PATH string. Relative paths are resolved against the project
|
|
|
|
|
/// root.
|
|
|
|
|
fn resolve_import_path(paths: &[String], root: &Path) -> Option<String> {
|
2026-03-13 00:18:14 +00:00
|
|
|
let joined = paths
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|p| {
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
let candidate = Path::new(p);
|
2026-03-13 00:18:14 +00:00
|
|
|
if candidate.is_absolute() {
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
p.clone()
|
2026-03-13 00:18:14 +00:00
|
|
|
} else {
|
|
|
|
|
root.join(candidate).display().to_string()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
.join(":");
|
|
|
|
|
if joined.is_empty() {
|
|
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
Some(joined)
|
|
|
|
|
}
|
|
|
|
|
}
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
|
|
|
|
|
pub struct ContextSpec {
|
|
|
|
|
pub slug: String,
|
|
|
|
|
pub root: PathBuf,
|
|
|
|
|
pub import_path: Option<String>,
|
|
|
|
|
pub keys: Vec<KeyEntry>,
|
|
|
|
|
pub remote_url: String,
|
|
|
|
|
pub push_only: bool,
|
|
|
|
|
pub stale_actor_timeout: u64,
|
|
|
|
|
pub max_notifications: usize,
|
|
|
|
|
/// Directories that require notification acknowledgment.
|
|
|
|
|
pub ack_required: Vec<String>,
|
feat: config surface, NCL contracts, override-layer mutation, on+re update
Config surface — per-project config introspection, coherence verification, and
audited mutation without destroying NCL structure (ADR-008):
- crates/ontoref-daemon/src/config.rs — typed DaemonNclConfig (parse-at-boundary
pattern); all section structs derive ConfigFields + config_section(id, ncl_file)
emitting inventory::submit!(ConfigFieldsEntry{...}) at link time
- crates/ontoref-derive/src/lib.rs — #[derive(ConfigFields)] proc-macro; serde
rename support; serde_rename_of() helper extracted to fix excessive_nesting
- crates/ontoref-daemon/src/main.rs — 3-tuple bootstrap block (nickel_import_path,
loaded_ncl_config: Option<DaemonNclConfig>, stdin_raw); apply_ui_config takes
&UiConfig; NATS call site typed; resolve_asset_dir cfg(feature = "ui")
- crates/ontoref-daemon/src/api.rs — config GET/PUT endpoints, quickref, coherence,
cross-project comparison; index_section_fields() extracted (excessive_nesting)
- crates/ontoref-daemon/src/config_coherence.rs — multi-consumer coherence;
merge_meta_into_section() extracted; and() replaces unnecessary and_then
NCL contracts for ontoref's own config:
- .ontoref/contracts.ncl — LogConfig (LogLevel, LogRotation, PositiveInt) and
DaemonConfig (Port, optional overrides); std.contract.from_validator throughout
- .ontoref/config.ncl — log | C.LogConfig applied
- .ontology/manifest.ncl — contracts_path, log/daemon contract refs, daemon section
with DaemonRuntimeConfig consumer and 7 declared fields
Protocol:
- adrs/adr-008-ncl-first-config-validation-and-override-layer.ncl — NCL contracts
as single validation gate; Rust structs are contract-trusted; override-layer
mutation writes {section}.overrides.ncl + _overrides_meta, never touches source
on+re update:
- .ontology/core.ncl — config-surface node (28 practices); adr-lifecycle extended
to adr-007 + adr-008; 6 new edges (ManifestsIn daemon, DependsOn ontology-crate,
Complements api-catalog-surface/dag-formalized/self-describing/adopt-ontoref)
- .ontology/state.ncl — protocol-maturity blocker and self-description-coverage
catalyst updated for session 2026-03-26
- README.md / CHANGELOG.md updated
2026-03-26 20:20:22 +00:00
|
|
|
/// Pre-loaded config surface from the project's manifest.ncl.
|
|
|
|
|
/// Pass `None` for push_only projects or when the manifest has no
|
|
|
|
|
/// `config_surface` field.
|
|
|
|
|
pub config_surface: Option<ConfigSurface>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Attempt to load `ConfigSurface` from a project's `manifest.ncl`
|
|
|
|
|
/// synchronously.
|
|
|
|
|
///
|
|
|
|
|
/// Runs `nickel export` on `.ontology/manifest.ncl` and deserialises the
|
|
|
|
|
/// `config_surface` key. Returns `None` when:
|
|
|
|
|
/// - the manifest file doesn't exist
|
|
|
|
|
/// - the manifest has no `config_surface` field
|
|
|
|
|
/// - nickel export or deserialisation fails (logged at warn level)
|
|
|
|
|
pub fn load_config_surface(root: &Path, import_path: Option<&str>) -> Option<ConfigSurface> {
|
|
|
|
|
let manifest = root.join(".ontology").join("manifest.ncl");
|
|
|
|
|
if !manifest.exists() {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut cmd = std::process::Command::new("nickel");
|
|
|
|
|
cmd.args(["export", "--format", "json"])
|
|
|
|
|
.arg(&manifest)
|
|
|
|
|
.current_dir(root);
|
|
|
|
|
if let Some(ip) = import_path {
|
|
|
|
|
cmd.env("NICKEL_IMPORT_PATH", ip);
|
|
|
|
|
}
|
|
|
|
|
let output = cmd.output().ok()?;
|
|
|
|
|
|
|
|
|
|
if !output.status.success() {
|
|
|
|
|
warn!(
|
|
|
|
|
path = %manifest.display(),
|
|
|
|
|
stderr = %String::from_utf8_lossy(&output.stderr),
|
|
|
|
|
"nickel export of manifest.ncl failed — config_surface not loaded"
|
|
|
|
|
);
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let json: serde_json::Value = serde_json::from_slice(&output.stdout).ok()?;
|
|
|
|
|
let surface_val = json.get("config_surface")?;
|
|
|
|
|
|
|
|
|
|
// Deserialise into an intermediate form that matches the NCL schema,
|
|
|
|
|
// then convert to the canonical ConfigSurface.
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
struct NclConfigConsumer {
|
|
|
|
|
id: String,
|
|
|
|
|
kind: String,
|
|
|
|
|
#[serde(default, rename = "ref")]
|
|
|
|
|
reference: String,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
fields: Vec<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
struct NclConfigSection {
|
|
|
|
|
id: String,
|
|
|
|
|
file: String,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
contract: String,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
description: String,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
rationale: String,
|
|
|
|
|
#[serde(default = "default_true")]
|
|
|
|
|
mutable: bool,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
consumers: Vec<NclConfigConsumer>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_true() -> bool {
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
struct NclConfigSurface {
|
|
|
|
|
config_root: String,
|
|
|
|
|
#[serde(default = "default_config_ncl")]
|
|
|
|
|
entry_point: String,
|
|
|
|
|
#[serde(default = "default_ncl_merge")]
|
|
|
|
|
kind: String,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
contracts_path: String,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
overrides_dir: String,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
sections: Vec<NclConfigSection>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_config_ncl() -> String {
|
|
|
|
|
"config.ncl".to_string()
|
|
|
|
|
}
|
|
|
|
|
fn default_ncl_merge() -> String {
|
|
|
|
|
"NclMerge".to_string()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let ncl: NclConfigSurface = match serde_json::from_value(surface_val.clone()) {
|
|
|
|
|
Ok(v) => v,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
warn!(error = %e, "failed to deserialise config_surface from manifest.ncl");
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let kind = match ncl.kind.as_str() {
|
|
|
|
|
"TypeDialog" => ConfigKind::TypeDialog,
|
|
|
|
|
"SingleFile" => ConfigKind::SingleFile,
|
|
|
|
|
_ => ConfigKind::NclMerge,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let config_root = root.join(&ncl.config_root);
|
|
|
|
|
let overrides_dir = if ncl.overrides_dir.is_empty() {
|
|
|
|
|
PathBuf::new()
|
|
|
|
|
} else {
|
|
|
|
|
root.join(&ncl.overrides_dir)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let sections = ncl
|
|
|
|
|
.sections
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|s| {
|
|
|
|
|
let consumers = s
|
|
|
|
|
.consumers
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|c| {
|
|
|
|
|
let consumer_kind = match c.kind.as_str() {
|
|
|
|
|
"NuScript" => ConsumerKind::NuScript,
|
|
|
|
|
"CiPipeline" => ConsumerKind::CiPipeline,
|
|
|
|
|
"External" => ConsumerKind::External,
|
|
|
|
|
_ => ConsumerKind::RustStruct,
|
|
|
|
|
};
|
|
|
|
|
ConfigConsumer {
|
|
|
|
|
id: c.id,
|
|
|
|
|
kind: consumer_kind,
|
|
|
|
|
reference: c.reference,
|
|
|
|
|
fields: c.fields,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
ConfigSection {
|
|
|
|
|
id: s.id,
|
|
|
|
|
file: s.file,
|
|
|
|
|
contract: s.contract,
|
|
|
|
|
description: s.description,
|
|
|
|
|
rationale: s.rationale,
|
|
|
|
|
mutable: s.mutable,
|
|
|
|
|
consumers,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
Some(ConfigSurface {
|
|
|
|
|
config_root,
|
|
|
|
|
entry_point: ncl.entry_point,
|
|
|
|
|
kind,
|
|
|
|
|
contracts_path: ncl.contracts_path,
|
|
|
|
|
overrides_dir,
|
|
|
|
|
sections,
|
|
|
|
|
})
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn make_context(spec: ContextSpec) -> ProjectContext {
|
|
|
|
|
let sessions_dir = spec.root.join(".ontoref").join("sessions");
|
|
|
|
|
let actors =
|
|
|
|
|
Arc::new(ActorRegistry::new(spec.stale_actor_timeout).with_persist_dir(sessions_dir));
|
|
|
|
|
actors.load_persisted();
|
|
|
|
|
|
|
|
|
|
let ack_dirs = if spec.ack_required.is_empty() {
|
|
|
|
|
vec![".ontology".into(), "adrs".into()]
|
|
|
|
|
} else {
|
|
|
|
|
spec.ack_required
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ProjectContext {
|
|
|
|
|
slug: spec.slug,
|
|
|
|
|
root: spec.root,
|
|
|
|
|
import_path: spec.import_path,
|
|
|
|
|
cache: Arc::new(NclCache::new()),
|
|
|
|
|
actors,
|
|
|
|
|
notifications: Arc::new(NotificationStore::new(spec.max_notifications, ack_dirs)),
|
|
|
|
|
keys: RwLock::new(spec.keys),
|
|
|
|
|
remote_url: spec.remote_url,
|
|
|
|
|
push_only: spec.push_only,
|
|
|
|
|
seed_lock: Arc::new(Semaphore::new(1)),
|
|
|
|
|
ontology_version: Arc::new(AtomicU64::new(0)),
|
---
feat: API catalog surface, protocol v2 tooling, MCP expansion, on+re update
## Summary
Session 2026-03-23. Closes the loop between handler code and discoverability
across all three surfaces (browser, CLI, MCP agent) via compile-time inventory
registration. Adds protocol v2 update tooling, extends MCP from 21 to 29 tools,
and brings the self-description up to date.
## API Catalog Surface (#[onto_api] proc-macro)
- crates/ontoref-derive: new proc-macro crate; `#[onto_api(method, path,
description, auth, actors, params, tags)]` emits `inventory::submit!(ApiRouteEntry{...})`
at link time
- crates/ontoref-daemon/src/api_catalog.rs: `catalog()` — pure fn over
`inventory::iter::<ApiRouteEntry>()`, zero runtime allocation
- GET /api/catalog: returns full annotated HTTP surface as JSON
- templates/pages/api_catalog.html: new page with client-side filtering by
method, auth, path/description; detail panel per route (params table,
feature flag); linked from dashboard card and nav
- UI nav: "API" link (</> icon) added to mobile dropdown and desktop bar
- inventory = "0.3" added to workspace.dependencies (MIT, zero transitive deps)
## Protocol Update Mode
- reflection/modes/update_ontoref.ncl: 9-step DAG (5 detect parallel, 2 update
idempotent, 2 validate, 1 report) — brings any project from protocol v1 to v2
by adding manifest.ncl and connections.ncl if absent, scanning ADRs for
deprecated check_hint, validating with nickel export
- reflection/templates/update-ontology-prompt.md: 8-phase reusable prompt for
agent-driven ontology enrichment (infrastructure → audit → core.ncl →
state.ncl → manifest.ncl → connections.ncl → ADR migration → validation)
## CLI — describe group extensions
- reflection/bin/ontoref.nu: `describe diff [--fmt] [--file]` and
`describe api [--actor] [--tag] [--auth] [--fmt]` registered as canonical
subcommands with log-action; aliases `df` and `da` added; QUICK REFERENCE
and ALIASES sections updated
## MCP — two new tools (21 → 29 total)
- ontoref_api_catalog: filters catalog() output by actor/tag/auth; returns
{ routes, total } — no HTTP roundtrip, calls inventory directly
- ontoref_file_versions: reads ProjectContext.file_versions DashMap per slug;
returns BTreeMap<filename, u64> reload counters
- insert_mcp_ctx: audited and updated from 15 to 28 entries in 6 groups
- HelpTool JSON: 8 new entries (validate_adrs, validate, impact, guides,
bookmark_list, bookmark_add, api_catalog, file_versions)
- ServerHandler::get_info instructions updated to mention new tools
## Web UI — dashboard additions
- Dashboard: "API Catalog" card (9th); "Ontology File Versions" section showing
per-file reload counters from file_versions DashMap
- dashboard_mp: builds BTreeMap<String, u64> from ctx.file_versions and injects
into Tera context
## on+re update
- .ontology/core.ncl: describe-query-layer and adopt-ontoref-tooling descriptions
updated; ontoref-daemon updated ("11 pages", "29 tools", API catalog,
per-file versioning, #[onto_api]); new node api-catalog-surface (Yang/Practice)
with 3 edges; artifact_paths extended across 3 nodes
- .ontology/state.ncl: protocol-maturity blocker updated (protocol v2 complete);
self-description-coverage catalyst updated with session 2026-03-23 additions
- ADR-007: "API Surface Discoverability via #[onto_api] Proc-Macro" — Accepted
## Documentation
- README.md: crates table updated (11 pages, 29 MCP tools, ontoref-derive row);
MCP representative table expanded; API Catalog, Semantic Diff, Per-File
Versioning paragraphs added; update_ontoref onboarding section added
- CHANGELOG.md: [Unreleased] section with 4 change groups
- assets/web/src/index.html: tool counts 19→29 (EN+ES), page counts 12→11
(EN+ES), daemon description paragraph updated with API catalog + #[onto_api]
2026-03-23 00:58:27 +01:00
|
|
|
file_versions: Arc::new(DashMap::new()),
|
feat: config surface, NCL contracts, override-layer mutation, on+re update
Config surface — per-project config introspection, coherence verification, and
audited mutation without destroying NCL structure (ADR-008):
- crates/ontoref-daemon/src/config.rs — typed DaemonNclConfig (parse-at-boundary
pattern); all section structs derive ConfigFields + config_section(id, ncl_file)
emitting inventory::submit!(ConfigFieldsEntry{...}) at link time
- crates/ontoref-derive/src/lib.rs — #[derive(ConfigFields)] proc-macro; serde
rename support; serde_rename_of() helper extracted to fix excessive_nesting
- crates/ontoref-daemon/src/main.rs — 3-tuple bootstrap block (nickel_import_path,
loaded_ncl_config: Option<DaemonNclConfig>, stdin_raw); apply_ui_config takes
&UiConfig; NATS call site typed; resolve_asset_dir cfg(feature = "ui")
- crates/ontoref-daemon/src/api.rs — config GET/PUT endpoints, quickref, coherence,
cross-project comparison; index_section_fields() extracted (excessive_nesting)
- crates/ontoref-daemon/src/config_coherence.rs — multi-consumer coherence;
merge_meta_into_section() extracted; and() replaces unnecessary and_then
NCL contracts for ontoref's own config:
- .ontoref/contracts.ncl — LogConfig (LogLevel, LogRotation, PositiveInt) and
DaemonConfig (Port, optional overrides); std.contract.from_validator throughout
- .ontoref/config.ncl — log | C.LogConfig applied
- .ontology/manifest.ncl — contracts_path, log/daemon contract refs, daemon section
with DaemonRuntimeConfig consumer and 7 declared fields
Protocol:
- adrs/adr-008-ncl-first-config-validation-and-override-layer.ncl — NCL contracts
as single validation gate; Rust structs are contract-trusted; override-layer
mutation writes {section}.overrides.ncl + _overrides_meta, never touches source
on+re update:
- .ontology/core.ncl — config-surface node (28 practices); adr-lifecycle extended
to adr-007 + adr-008; 6 new edges (ManifestsIn daemon, DependsOn ontology-crate,
Complements api-catalog-surface/dag-formalized/self-describing/adopt-ontoref)
- .ontology/state.ncl — protocol-maturity blocker and self-description-coverage
catalyst updated for session 2026-03-26
- README.md / CHANGELOG.md updated
2026-03-26 20:20:22 +00:00
|
|
|
config_surface: spec.config_surface,
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
fn phc_hash(password: &str) -> String {
|
|
|
|
|
use argon2::password_hash::{rand_core::OsRng, SaltString};
|
|
|
|
|
use argon2::{Argon2, PasswordHasher};
|
|
|
|
|
let salt = SaltString::generate(&mut OsRng);
|
|
|
|
|
Argon2::default()
|
|
|
|
|
.hash_password(password.as_bytes(), &salt)
|
|
|
|
|
.unwrap()
|
|
|
|
|
.to_string()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn admin_entry(password: &str) -> KeyEntry {
|
|
|
|
|
KeyEntry {
|
|
|
|
|
role: Role::Admin,
|
|
|
|
|
hash: phc_hash(password),
|
|
|
|
|
label: String::new(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn viewer_entry(password: &str) -> KeyEntry {
|
|
|
|
|
KeyEntry {
|
|
|
|
|
role: Role::Viewer,
|
|
|
|
|
hash: phc_hash(password),
|
|
|
|
|
label: String::new(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── verify_keys_list ──────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn correct_password_returns_role() {
|
|
|
|
|
let keys = vec![admin_entry("secret")];
|
|
|
|
|
assert_eq!(
|
|
|
|
|
verify_keys_list(&keys, "secret").map(|m| m.role),
|
|
|
|
|
Some(Role::Admin)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn wrong_password_returns_none() {
|
|
|
|
|
let keys = vec![admin_entry("secret")];
|
|
|
|
|
assert!(verify_keys_list(&keys, "wrong").is_none());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn empty_keys_returns_none() {
|
|
|
|
|
assert!(verify_keys_list(&[], "anything").is_none());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn returns_first_matching_role() {
|
|
|
|
|
let keys = vec![viewer_entry("pw"), admin_entry("pw")];
|
|
|
|
|
// Viewer comes first — that role wins.
|
|
|
|
|
assert_eq!(
|
|
|
|
|
verify_keys_list(&keys, "pw").map(|m| m.role),
|
|
|
|
|
Some(Role::Viewer)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn invalid_phc_hash_is_skipped_not_panicked() {
|
|
|
|
|
let keys = vec![
|
|
|
|
|
KeyEntry {
|
|
|
|
|
role: Role::Admin,
|
|
|
|
|
hash: "not-a-phc-string".to_string(),
|
|
|
|
|
label: String::new(),
|
|
|
|
|
},
|
|
|
|
|
admin_entry("valid"),
|
|
|
|
|
];
|
|
|
|
|
// Bad entry skipped; valid entry matches.
|
|
|
|
|
assert_eq!(
|
|
|
|
|
verify_keys_list(&keys, "valid").map(|m| m.role),
|
|
|
|
|
Some(Role::Admin)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn invalid_phc_only_returns_none() {
|
|
|
|
|
let keys = vec![KeyEntry {
|
|
|
|
|
role: Role::Admin,
|
|
|
|
|
hash: "garbage".to_string(),
|
|
|
|
|
label: String::new(),
|
|
|
|
|
}];
|
|
|
|
|
assert!(verify_keys_list(&keys, "anything").is_none());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── resolve_import_path ───────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn empty_paths_returns_none() {
|
|
|
|
|
assert_eq!(resolve_import_path(&[], Path::new("/root")), None);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn absolute_path_returned_as_is() {
|
|
|
|
|
let result = resolve_import_path(&["/abs/path".to_string()], Path::new("/root"));
|
|
|
|
|
assert_eq!(result, Some("/abs/path".to_string()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn relative_path_joined_with_root() {
|
|
|
|
|
let result = resolve_import_path(&["lib".to_string()], Path::new("/root"));
|
|
|
|
|
assert_eq!(result, Some("/root/lib".to_string()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn multiple_paths_colon_separated() {
|
|
|
|
|
let result =
|
|
|
|
|
resolve_import_path(&["/abs".to_string(), "rel".to_string()], Path::new("/root"));
|
|
|
|
|
assert_eq!(result, Some("/abs:/root/rel".to_string()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── with_primary slug collision ───────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
fn minimal_ctx(slug: &str) -> Arc<ProjectContext> {
|
|
|
|
|
Arc::new(make_context(ContextSpec {
|
|
|
|
|
slug: slug.to_string(),
|
|
|
|
|
root: PathBuf::new(),
|
|
|
|
|
import_path: None,
|
|
|
|
|
keys: vec![],
|
|
|
|
|
remote_url: String::new(),
|
|
|
|
|
push_only: true,
|
|
|
|
|
stale_actor_timeout: 300,
|
|
|
|
|
max_notifications: 64,
|
|
|
|
|
ack_required: vec![],
|
feat: config surface, NCL contracts, override-layer mutation, on+re update
Config surface — per-project config introspection, coherence verification, and
audited mutation without destroying NCL structure (ADR-008):
- crates/ontoref-daemon/src/config.rs — typed DaemonNclConfig (parse-at-boundary
pattern); all section structs derive ConfigFields + config_section(id, ncl_file)
emitting inventory::submit!(ConfigFieldsEntry{...}) at link time
- crates/ontoref-derive/src/lib.rs — #[derive(ConfigFields)] proc-macro; serde
rename support; serde_rename_of() helper extracted to fix excessive_nesting
- crates/ontoref-daemon/src/main.rs — 3-tuple bootstrap block (nickel_import_path,
loaded_ncl_config: Option<DaemonNclConfig>, stdin_raw); apply_ui_config takes
&UiConfig; NATS call site typed; resolve_asset_dir cfg(feature = "ui")
- crates/ontoref-daemon/src/api.rs — config GET/PUT endpoints, quickref, coherence,
cross-project comparison; index_section_fields() extracted (excessive_nesting)
- crates/ontoref-daemon/src/config_coherence.rs — multi-consumer coherence;
merge_meta_into_section() extracted; and() replaces unnecessary and_then
NCL contracts for ontoref's own config:
- .ontoref/contracts.ncl — LogConfig (LogLevel, LogRotation, PositiveInt) and
DaemonConfig (Port, optional overrides); std.contract.from_validator throughout
- .ontoref/config.ncl — log | C.LogConfig applied
- .ontology/manifest.ncl — contracts_path, log/daemon contract refs, daemon section
with DaemonRuntimeConfig consumer and 7 declared fields
Protocol:
- adrs/adr-008-ncl-first-config-validation-and-override-layer.ncl — NCL contracts
as single validation gate; Rust structs are contract-trusted; override-layer
mutation writes {section}.overrides.ncl + _overrides_meta, never touches source
on+re update:
- .ontology/core.ncl — config-surface node (28 practices); adr-lifecycle extended
to adr-007 + adr-008; 6 new edges (ManifestsIn daemon, DependsOn ontology-crate,
Complements api-catalog-surface/dag-formalized/self-describing/adopt-ontoref)
- .ontology/state.ncl — protocol-maturity blocker and self-description-coverage
catalyst updated for session 2026-03-26
- README.md / CHANGELOG.md updated
2026-03-26 20:20:22 +00:00
|
|
|
config_surface: None,
|
feat: unified auth model, project onboarding, install pipeline, config management
The full scope across this batch: POST /sessions key→token exchange, SessionStore dual-index with revoke_by_id, CLI Bearer injection (ONTOREF_TOKEN), ontoref setup
--gen-keys, install scripts, daemon config form roundtrip, ADR-004/005, on+re self-description update (fully-self-described), and landing page refresh.
2026-03-13 20:56:31 +00:00
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn primary_is_accessible_after_construction() {
|
|
|
|
|
let registry =
|
|
|
|
|
ProjectRegistry::with_primary("main".to_string(), minimal_ctx("main"), vec![], 300, 64);
|
|
|
|
|
assert_eq!(registry.primary().slug, "main");
|
|
|
|
|
assert_eq!(registry.primary_slug(), "main");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn slug_collision_does_not_overwrite_primary() {
|
|
|
|
|
let primary = minimal_ctx("shared");
|
|
|
|
|
let primary_ptr = Arc::as_ptr(&primary);
|
|
|
|
|
|
|
|
|
|
let colliding = RegistryEntry {
|
|
|
|
|
slug: "shared".to_string(),
|
|
|
|
|
root: PathBuf::new(),
|
|
|
|
|
nickel_import_paths: vec![],
|
|
|
|
|
keys: vec![],
|
|
|
|
|
remote_url: String::new(),
|
|
|
|
|
push_only: true,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let registry =
|
|
|
|
|
ProjectRegistry::with_primary("shared".to_string(), primary, vec![colliding], 300, 64);
|
|
|
|
|
|
|
|
|
|
// The primary context pointer must survive — not replaced by the colliding
|
|
|
|
|
// entry.
|
|
|
|
|
assert_eq!(Arc::as_ptr(®istry.primary()), primary_ptr);
|
|
|
|
|
assert_eq!(registry.count(), 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn additional_non_colliding_entry_added() {
|
|
|
|
|
let registry = ProjectRegistry::with_primary(
|
|
|
|
|
"primary".to_string(),
|
|
|
|
|
minimal_ctx("primary"),
|
|
|
|
|
vec![RegistryEntry {
|
|
|
|
|
slug: "secondary".to_string(),
|
|
|
|
|
root: PathBuf::new(),
|
|
|
|
|
nickel_import_paths: vec![],
|
|
|
|
|
keys: vec![],
|
|
|
|
|
remote_url: String::new(),
|
|
|
|
|
push_only: true,
|
|
|
|
|
}],
|
|
|
|
|
300,
|
|
|
|
|
64,
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(registry.count(), 2);
|
|
|
|
|
assert!(registry.get("secondary").is_some());
|
|
|
|
|
}
|
|
|
|
|
}
|