2026-03-13 00:18:14 +00:00
|
|
|
#[cfg(feature = "nats")]
|
|
|
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "nats")]
|
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 anyhow::Result;
|
2026-03-13 00:18:14 +00:00
|
|
|
#[cfg(feature = "nats")]
|
|
|
|
|
use bytes::Bytes;
|
|
|
|
|
#[cfg(feature = "nats")]
|
|
|
|
|
use platform_nats::{EventStream, NatsConnectionConfig, TopologyConfig};
|
|
|
|
|
#[cfg(feature = "nats")]
|
|
|
|
|
use serde_json::{json, Value};
|
|
|
|
|
#[cfg(feature = "nats")]
|
|
|
|
|
use tracing::{info, warn};
|
|
|
|
|
|
|
|
|
|
/// NATS JetStream publisher for daemon lifecycle events.
|
|
|
|
|
///
|
|
|
|
|
/// Uses platform-nats `connect_client()` — connection + auth only.
|
|
|
|
|
/// Stream/consumer topology comes entirely from `nats/streams.json` (or
|
|
|
|
|
/// equivalent), referenced by `nats_events.streams_config` in
|
|
|
|
|
/// `.ontoref/config.ncl`.
|
|
|
|
|
///
|
|
|
|
|
/// Gracefully degrades if NATS is unavailable or disabled in config.
|
|
|
|
|
#[cfg(feature = "nats")]
|
|
|
|
|
pub struct NatsPublisher {
|
|
|
|
|
stream: EventStream,
|
|
|
|
|
project: String,
|
|
|
|
|
port: u16,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(feature = "nats")]
|
|
|
|
|
impl NatsPublisher {
|
|
|
|
|
/// Connect to NATS JetStream, apply topology from config, bind consumer.
|
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
|
|
|
/// Returns `Ok(None)` when `cfg` is `None`, disabled, or unavailable.
|
2026-03-13 00:18:14 +00:00
|
|
|
pub async fn connect(
|
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
|
|
|
cfg: Option<&crate::config::NatsEventsConfig>,
|
2026-03-13 00:18:14 +00:00
|
|
|
project: String,
|
|
|
|
|
port: u16,
|
|
|
|
|
) -> Result<Option<Self>> {
|
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 Some(nats) = cfg else {
|
|
|
|
|
return Ok(None);
|
2026-03-13 00:18:14 +00:00
|
|
|
};
|
|
|
|
|
|
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
|
|
|
if !nats.enabled {
|
2026-03-13 00:18:14 +00:00
|
|
|
return Ok(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
|
|
|
info!("connecting to NATS...");
|
|
|
|
|
|
2026-03-13 00:18:14 +00:00
|
|
|
let conn_cfg = NatsConnectionConfig {
|
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
|
|
|
url: nats.url.clone(),
|
|
|
|
|
nkey_seed: nats.nkey_seed.clone(),
|
|
|
|
|
require_signed_messages: nats.require_signed_messages,
|
|
|
|
|
trusted_nkeys: nats.trusted_nkeys.clone(),
|
2026-03-13 00:18:14 +00:00
|
|
|
};
|
|
|
|
|
|
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 url = nats.url.clone();
|
|
|
|
|
|
2026-03-13 00:18:14 +00:00
|
|
|
let mut stream = match tokio::time::timeout(
|
|
|
|
|
std::time::Duration::from_secs(3),
|
|
|
|
|
EventStream::connect_client(&conn_cfg),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Ok(Ok(s)) => s,
|
|
|
|
|
Ok(Err(e)) => {
|
|
|
|
|
warn!(error = %e, url = %url, "NATS connection failed — running without events");
|
|
|
|
|
return Ok(None);
|
|
|
|
|
}
|
|
|
|
|
Err(_) => {
|
|
|
|
|
warn!(url = %url, "NATS connection timed out — running without events");
|
|
|
|
|
return Ok(None);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
info!(url = %url, "NATS connected");
|
|
|
|
|
|
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 topology_path = (!nats.streams_config.is_empty())
|
|
|
|
|
.then(|| std::path::PathBuf::from(&nats.streams_config));
|
2026-03-13 00:18:14 +00:00
|
|
|
|
|
|
|
|
let topology = match TopologyConfig::load(topology_path.as_deref()) {
|
|
|
|
|
Ok(Some(t)) => Some(t),
|
|
|
|
|
Ok(None) => {
|
|
|
|
|
warn!("no topology config found — publish-only mode (no consumer bound)");
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
warn!(error = %e, "topology config load failed — publish-only mode");
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if let Some(ref topo) = topology {
|
|
|
|
|
match stream.apply_topology(topo).await {
|
|
|
|
|
Ok(report) => info!(
|
|
|
|
|
streams = report.streams_applied,
|
|
|
|
|
consumers = report.consumers_applied,
|
|
|
|
|
"topology applied"
|
|
|
|
|
),
|
|
|
|
|
Err(e) => warn!(error = %e, "topology apply failed — publish-only mode"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Bind to daemon consumer on the first stream (convention: "daemon-{project}").
|
|
|
|
|
if let Some(ref topo) = topology {
|
|
|
|
|
if let Some(first_stream) = topo.streams.first() {
|
|
|
|
|
let consumer_name = format!("daemon-{project}");
|
|
|
|
|
if let Err(e) = stream
|
|
|
|
|
.bind_consumer(&first_stream.name, &consumer_name)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
warn!(error = %e, "failed to bind daemon consumer — pull_events disabled");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(Some(Self {
|
|
|
|
|
stream,
|
|
|
|
|
project,
|
|
|
|
|
port,
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn publish_started(&self) -> Result<()> {
|
|
|
|
|
let payload = json!({
|
|
|
|
|
"project": self.project,
|
|
|
|
|
"port": self.port,
|
|
|
|
|
"timestamp": iso8601_now(),
|
|
|
|
|
});
|
|
|
|
|
self.stream
|
|
|
|
|
.publish("ecosystem.daemon.started", Bytes::from(payload.to_string()))
|
|
|
|
|
.await?;
|
|
|
|
|
info!(port = self.port, "published daemon.started");
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn publish_stopped(&self, uptime_secs: u64) -> Result<()> {
|
|
|
|
|
let payload = json!({
|
|
|
|
|
"project": self.project,
|
|
|
|
|
"uptime_seconds": uptime_secs,
|
|
|
|
|
"timestamp": iso8601_now(),
|
|
|
|
|
});
|
|
|
|
|
self.stream
|
|
|
|
|
.publish("ecosystem.daemon.stopped", Bytes::from(payload.to_string()))
|
|
|
|
|
.await?;
|
|
|
|
|
info!(uptime = uptime_secs, "published daemon.stopped");
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Publish a file change notification for a specific project and event
|
|
|
|
|
/// type.
|
|
|
|
|
pub async fn publish_notification(
|
|
|
|
|
&self,
|
|
|
|
|
project: &str,
|
|
|
|
|
event: &crate::notifications::NotificationEvent,
|
|
|
|
|
files: &[String],
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
let subject = format!("ecosystem.{project}.{}", event.nats_suffix());
|
|
|
|
|
let payload = json!({
|
|
|
|
|
"project": project,
|
|
|
|
|
"event": format!("{event:?}"),
|
|
|
|
|
"files": files,
|
|
|
|
|
"timestamp": iso8601_now(),
|
|
|
|
|
});
|
|
|
|
|
self.stream
|
|
|
|
|
.publish(&subject, Bytes::from(payload.to_string()))
|
|
|
|
|
.await?;
|
|
|
|
|
info!(subject = %subject, files = files.len(), "published notification");
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Publish an actor registration event.
|
|
|
|
|
pub async fn publish_actor_registered(
|
|
|
|
|
&self,
|
|
|
|
|
token: &str,
|
|
|
|
|
actor_type: &str,
|
|
|
|
|
project: &str,
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
let payload = json!({
|
|
|
|
|
"token": token,
|
|
|
|
|
"actor_type": actor_type,
|
|
|
|
|
"project": project,
|
|
|
|
|
"timestamp": iso8601_now(),
|
|
|
|
|
});
|
|
|
|
|
self.stream
|
|
|
|
|
.publish(
|
|
|
|
|
"ecosystem.actor.registered",
|
|
|
|
|
Bytes::from(payload.to_string()),
|
|
|
|
|
)
|
|
|
|
|
.await?;
|
|
|
|
|
info!(token = %token, "published actor.registered");
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Publish an actor deregistration event.
|
|
|
|
|
pub async fn publish_actor_deregistered(&self, token: &str, reason: &str) -> Result<()> {
|
|
|
|
|
let payload = json!({
|
|
|
|
|
"token": token,
|
|
|
|
|
"reason": reason,
|
|
|
|
|
"timestamp": iso8601_now(),
|
|
|
|
|
});
|
|
|
|
|
self.stream
|
|
|
|
|
.publish(
|
|
|
|
|
"ecosystem.actor.deregistered",
|
|
|
|
|
Bytes::from(payload.to_string()),
|
|
|
|
|
)
|
|
|
|
|
.await?;
|
|
|
|
|
info!(token = %token, reason = %reason, "published actor.deregistered");
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Publish a file change event for a project (general, not requiring ack).
|
|
|
|
|
pub async fn publish_file_changed(&self, project: &str, files: &[String]) -> Result<()> {
|
|
|
|
|
let subject = format!("ecosystem.{project}.file.changed");
|
|
|
|
|
let payload = json!({
|
|
|
|
|
"project": project,
|
|
|
|
|
"files": files,
|
|
|
|
|
"timestamp": iso8601_now(),
|
|
|
|
|
});
|
|
|
|
|
self.stream
|
|
|
|
|
.publish(&subject, Bytes::from(payload.to_string()))
|
|
|
|
|
.await?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn publish_cache_invalidated(&self, reason: &str) -> Result<()> {
|
|
|
|
|
let payload = json!({
|
|
|
|
|
"project": self.project,
|
|
|
|
|
"reason": reason,
|
|
|
|
|
"affected_keys": [],
|
|
|
|
|
"timestamp": iso8601_now(),
|
|
|
|
|
});
|
|
|
|
|
self.stream
|
|
|
|
|
.publish(
|
|
|
|
|
"ecosystem.daemon.cache.invalidated",
|
|
|
|
|
Bytes::from(payload.to_string()),
|
|
|
|
|
)
|
|
|
|
|
.await?;
|
|
|
|
|
info!(reason = %reason, "published daemon.cache.invalidated");
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Pull pending messages from the bound JetStream consumer.
|
|
|
|
|
/// Returns (subject, parsed_json) for each valid message.
|
|
|
|
|
/// Returns empty vec if no consumer is bound (publish-only mode).
|
|
|
|
|
pub async fn pull_events(&self, max_msgs: usize) -> Result<Vec<(String, Value)>> {
|
|
|
|
|
let batch = match self.stream.pull_batch(max_msgs).await {
|
|
|
|
|
Ok(b) => b,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
let msg = e.to_string();
|
|
|
|
|
if msg.contains("no consumer bound") {
|
|
|
|
|
return Ok(Vec::new());
|
|
|
|
|
}
|
|
|
|
|
return Err(e);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut events = Vec::with_capacity(batch.len());
|
|
|
|
|
|
|
|
|
|
for (subject, payload_bytes, msg) in batch {
|
|
|
|
|
match serde_json::from_slice::<Value>(&payload_bytes) {
|
|
|
|
|
Ok(json) => events.push((subject, json)),
|
|
|
|
|
Err(e) => {
|
|
|
|
|
warn!(error = %e, subject = %subject, "invalid JSON in NATS message — skipping");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let _ = msg.ack().await;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(events)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Extract (mode_id, params) from a reflection.request payload.
|
|
|
|
|
pub fn parse_reflection_request(payload: &Value) -> Option<(String, Value)> {
|
|
|
|
|
let mode_id = payload.get("mode_id")?.as_str()?.to_string();
|
|
|
|
|
let params = payload
|
|
|
|
|
.get("params")
|
|
|
|
|
.cloned()
|
|
|
|
|
.unwrap_or(Value::Object(Default::default()));
|
|
|
|
|
Some((mode_id, params))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// ISO 8601 timestamp (UTC) without external dependency.
|
|
|
|
|
#[cfg(feature = "nats")]
|
|
|
|
|
fn iso8601_now() -> 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
|
|
|
iso8601_from_duration(
|
|
|
|
|
SystemTime::now()
|
|
|
|
|
.duration_since(UNIX_EPOCH)
|
|
|
|
|
.unwrap_or_default(),
|
|
|
|
|
)
|
|
|
|
|
}
|
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
|
|
|
fn iso8601_from_duration(now: std::time::Duration) -> String {
|
|
|
|
|
let total_secs = now.as_secs();
|
2026-03-13 00:18:14 +00:00
|
|
|
let micros = now.subsec_micros();
|
|
|
|
|
|
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 mut remaining_days = (total_secs / 86400) as u32;
|
|
|
|
|
let day_secs = (total_secs % 86400) as u32;
|
|
|
|
|
|
|
|
|
|
let mut year = 1970u32;
|
|
|
|
|
loop {
|
|
|
|
|
let diy = days_in_year(year);
|
|
|
|
|
if remaining_days < diy {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
remaining_days -= diy;
|
|
|
|
|
year += 1;
|
|
|
|
|
}
|
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 month_lengths: [u32; 12] = if is_leap_year(year) {
|
|
|
|
|
[31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
|
|
|
} else {
|
|
|
|
|
[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
|
|
|
};
|
|
|
|
|
let mut month = 1u32;
|
|
|
|
|
for &ml in &month_lengths {
|
|
|
|
|
if remaining_days < ml {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
remaining_days -= ml;
|
|
|
|
|
month += 1;
|
|
|
|
|
}
|
|
|
|
|
let day = remaining_days + 1;
|
2026-03-13 00:18:14 +00:00
|
|
|
|
|
|
|
|
format!(
|
|
|
|
|
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:06}Z",
|
|
|
|
|
year,
|
|
|
|
|
month,
|
|
|
|
|
day,
|
|
|
|
|
day_secs / 3600,
|
|
|
|
|
(day_secs % 3600) / 60,
|
|
|
|
|
day_secs % 60,
|
|
|
|
|
micros,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
fn is_leap_year(y: u32) -> bool {
|
|
|
|
|
(y.is_multiple_of(4) && !y.is_multiple_of(100)) || y.is_multiple_of(400)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn days_in_year(y: u32) -> u32 {
|
|
|
|
|
if is_leap_year(y) {
|
|
|
|
|
366
|
|
|
|
|
} else {
|
|
|
|
|
365
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use std::time::Duration;
|
|
|
|
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn epoch_zero_is_unix_origin() {
|
|
|
|
|
assert_eq!(
|
|
|
|
|
iso8601_from_duration(Duration::ZERO),
|
|
|
|
|
"1970-01-01T00:00:00.000000Z"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn known_date_non_leap() {
|
|
|
|
|
// 2001-03-01T00:00:00Z — first day of March in a non-leap year
|
|
|
|
|
// 2001-01-01 = 978307200; +59 days (Jan31 + Feb28) = 978307200 + 5097600 =
|
|
|
|
|
// 983404800
|
|
|
|
|
let secs = 978_307_200 + 31 * 86400 + 28 * 86400;
|
|
|
|
|
assert_eq!(
|
|
|
|
|
iso8601_from_duration(Duration::from_secs(secs)),
|
|
|
|
|
"2001-03-01T00:00:00.000000Z"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn leap_year_feb29() {
|
|
|
|
|
// 2000-02-29T00:00:00Z — only exists in a leap year
|
|
|
|
|
// 2000-01-01 = 946684800; +59 days (Jan31 + Feb29 is day 60, so +58 days from
|
|
|
|
|
// Jan1)
|
|
|
|
|
let secs = 946_684_800 + 31 * 86400 + 28 * 86400;
|
|
|
|
|
assert_eq!(
|
|
|
|
|
iso8601_from_duration(Duration::from_secs(secs)),
|
|
|
|
|
"2000-02-29T00:00:00.000000Z"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn century_non_leap_skips_feb29() {
|
|
|
|
|
// 1900 would be non-leap (div 100, not div 400); but our epoch starts at 1970
|
|
|
|
|
// Use 2100: it's div 100 but not div 400 → non-leap.
|
|
|
|
|
// 2100-02-28 exists; 2100-03-01 follows immediately (no Feb 29).
|
|
|
|
|
// Verify is_leap_year rejects 2100.
|
|
|
|
|
assert!(!is_leap_year(2100));
|
|
|
|
|
assert!(is_leap_year(2000));
|
|
|
|
|
assert!(is_leap_year(2024));
|
|
|
|
|
assert!(!is_leap_year(2023));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn subsecond_micros_preserved() {
|
|
|
|
|
let d = Duration::new(0, 123_456_000); // 123456 microseconds
|
|
|
|
|
let s = iso8601_from_duration(d);
|
|
|
|
|
assert!(s.ends_with(".123456Z"), "got: {s}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn time_components_correct() {
|
|
|
|
|
// 1970-01-01T01:02:03
|
|
|
|
|
let secs = 3600 + 2 * 60 + 3;
|
|
|
|
|
assert_eq!(
|
|
|
|
|
iso8601_from_duration(Duration::from_secs(secs)),
|
|
|
|
|
"1970-01-01T01:02:03.000000Z"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn iso8601_now_format() {
|
|
|
|
|
let s = iso8601_now();
|
|
|
|
|
assert_eq!(s.len(), 27, "unexpected length: {s}");
|
|
|
|
|
assert!(s.ends_with('Z'));
|
|
|
|
|
assert_eq!(&s[4..5], "-");
|
|
|
|
|
assert_eq!(&s[7..8], "-");
|
|
|
|
|
assert_eq!(&s[10..11], "T");
|
|
|
|
|
assert_eq!(&s[13..14], ":");
|
|
|
|
|
assert_eq!(&s[16..17], ":");
|
|
|
|
|
assert_eq!(&s[19..20], ".");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-13 00:18:14 +00:00
|
|
|
// ── No-op implementation when nats feature is disabled ────────────────
|
|
|
|
|
|
|
|
|
|
#[cfg(not(feature = "nats"))]
|
|
|
|
|
pub struct NatsPublisher;
|
|
|
|
|
|
|
|
|
|
#[cfg(not(feature = "nats"))]
|
|
|
|
|
impl NatsPublisher {
|
|
|
|
|
pub async fn connect(
|
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
|
|
|
_config: Option<&serde_json::Value>,
|
2026-03-13 00:18:14 +00:00
|
|
|
_project: String,
|
|
|
|
|
_port: u16,
|
|
|
|
|
) -> anyhow::Result<Option<Self>> {
|
|
|
|
|
Ok(None)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn publish_started(&self) -> anyhow::Result<()> {
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn publish_stopped(&self, _uptime_secs: u64) -> anyhow::Result<()> {
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn publish_notification(
|
|
|
|
|
&self,
|
|
|
|
|
_project: &str,
|
|
|
|
|
_event: &crate::notifications::NotificationEvent,
|
|
|
|
|
_files: &[String],
|
|
|
|
|
) -> anyhow::Result<()> {
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn publish_actor_registered(
|
|
|
|
|
&self,
|
|
|
|
|
_token: &str,
|
|
|
|
|
_actor_type: &str,
|
|
|
|
|
_project: &str,
|
|
|
|
|
) -> anyhow::Result<()> {
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn publish_actor_deregistered(
|
|
|
|
|
&self,
|
|
|
|
|
_token: &str,
|
|
|
|
|
_reason: &str,
|
|
|
|
|
) -> anyhow::Result<()> {
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn publish_file_changed(
|
|
|
|
|
&self,
|
|
|
|
|
_project: &str,
|
|
|
|
|
_files: &[String],
|
|
|
|
|
) -> anyhow::Result<()> {
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn publish_cache_invalidated(&self, _reason: &str) -> anyhow::Result<()> {
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|