From 7970347af129bf2eb4e32d6ac13b1a6032207da2 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 03:46:31 +0100 Subject: [PATCH] Bind deployment to one witnessed release --- epiphany-core/Cargo.toml | 4 + .../src/bin/epiphany-daemon-supervisor.rs | 158 +++-- .../bin/epiphany-memory-semantic-projector.rs | 3 +- .../src/bin/epiphany-memory-semantic.rs | 1 - epiphany-core/src/bin/epiphany-release.rs | 74 ++ epiphany-core/src/bin/epiphany-verse-query.rs | 4 +- epiphany-core/src/cultmesh_integration.rs | 3 + epiphany-core/src/lib.rs | 11 + epiphany-core/src/packaged_release.rs | 647 ++++++++++++++++++ notes/epiphany-swarm-readiness-plan.md | 133 ++-- notes/fresh-workspace-handoff.md | 29 + notes/semantic-projector-service-runbook.md | 51 +- state/ledgers.msgpack | Bin 521939 -> 522543 bytes state/map.yaml | 3 +- state/scratch.md | 10 + 15 files changed, 995 insertions(+), 136 deletions(-) create mode 100644 epiphany-core/src/bin/epiphany-release.rs create mode 100644 epiphany-core/src/packaged_release.rs diff --git a/epiphany-core/Cargo.toml b/epiphany-core/Cargo.toml index 6d1b923b..86e798ad 100644 --- a/epiphany-core/Cargo.toml +++ b/epiphany-core/Cargo.toml @@ -23,6 +23,10 @@ name = "epiphany-workspace-coverage-recovery-smoke" path = "src/bin/epiphany-workspace-coverage-recovery-smoke.rs" required-features = ["workspace-coverage-recovery-smoke"] +[[bin]] +name = "epiphany-release" +path = "src/bin/epiphany-release.rs" + [dependencies] anyhow = "1" bm25 = "2.3.2" diff --git a/epiphany-core/src/bin/epiphany-daemon-supervisor.rs b/epiphany-core/src/bin/epiphany-daemon-supervisor.rs index a1f4b445..9af3b871 100644 --- a/epiphany-core/src/bin/epiphany-daemon-supervisor.rs +++ b/epiphany-core/src/bin/epiphany-daemon-supervisor.rs @@ -29,6 +29,7 @@ use epiphany_core::load_epiphany_cultmesh_managed_service_policy; use epiphany_core::load_epiphany_cultmesh_managed_service_policy_with_digest; use epiphany_core::load_epiphany_cultmesh_status; use epiphany_core::load_epiphany_cultmesh_swarm_brake; +use epiphany_core::load_epiphany_packaged_release; use epiphany_core::load_latest_epiphany_cultmesh_daemon_heartbeat; use epiphany_core::load_latest_epiphany_cultmesh_daemon_service_lifecycle_receipt_for_service; use epiphany_core::migrate_memory_semantic_projection_attempts_v0; @@ -47,6 +48,7 @@ use epiphany_core::write_epiphany_cultmesh_daemon_service_lifecycle_receipt; use epiphany_core::write_epiphany_cultmesh_managed_service_policy; use epiphany_core::write_epiphany_cultmesh_semantic_projector_service_policy; use epiphany_core::write_epiphany_cultmesh_workspace_coverage_projector_service_policy; +use epiphany_core::{EpiphanyPackagedReleaseEntry, authenticate_epiphany_packaged_release}; use epiphany_core::{ WORKSPACE_COVERAGE_PROCESS_LAUNCH_SCHEMA_VERSION, WorkspaceCoverageManagedProcessLaunchEntry, WorkspaceCoverageProcessBootstrap, authenticate_workspace_coverage_provider_heartbeat, @@ -60,6 +62,9 @@ use epiphany_core::{ write_workspace_coverage_managed_process_launch, write_workspace_coverage_process_bootstrap, write_workspace_coverage_process_termination_observation, }; +use epiphany_core::{ + epiphany_packaged_release_binary_path, epiphany_packaged_release_witness_sha256, +}; use rand_core::{OsRng, RngCore}; use serde_json::Value; use serde_json::json; @@ -200,9 +205,40 @@ fn managed_service_task_name(args: &Args) -> String { .unwrap_or_else(|| "Epiphany-Idunn-Managed-Service-Reconciler".to_string()) } +fn pinned_packaged_release( + args: &Args, + require_digest: bool, +) -> Result<(EpiphanyPackagedReleaseEntry, String)> { + let release_id = args + .release_id + .as_deref() + .context("managed-service deployment requires --release-id")?; + let witness = load_epiphany_packaged_release(&args.store, &args.runtime_id, release_id)? + .context("pinned packaged release is absent")?; + let digest = match args.release_witness_sha256.as_deref() { + Some(expected) => expected.to_string(), + None if require_digest => { + anyhow::bail!("managed-service runtime requires --release-witness-sha256") + } + None => epiphany_packaged_release_witness_sha256(&witness)?, + }; + let authenticated = + authenticate_epiphany_packaged_release(&args.store, &args.runtime_id, release_id, &digest)?; + Ok((authenticated, digest)) +} + fn managed_service_task_action(args: &Args) -> Result<(PathBuf, PathBuf, Vec)> { - let command = fs::canonicalize(service_command_path(args)?) - .context("managed-service task command must be an existing absolute executable")?; + if args.service_command.is_some() { + anyhow::bail!( + "managed-service task command comes from --release-id; --service-command is forbidden" + ); + } + let (release, witness_digest) = pinned_packaged_release(args, false)?; + let command = fs::canonicalize(epiphany_packaged_release_binary_path( + &release, + "supervisor", + )?) + .context("witnessed managed-service supervisor is absent")?; let cwd = fs::canonicalize( args.cwd .clone() @@ -220,6 +256,10 @@ fn managed_service_task_action(args: &Args) -> Result<(PathBuf, PathBuf, Vec Result<()> { "providerIncarnation": heartbeat.provider_incarnation, "heartbeatStatus": heartbeat.status, "startupCorrelationMatches": correlation_matches, - "readinessAuthority": "semantic-query-admission", - "semanticReadiness": "not-asserted-by-this-status", "privateStateExposed": false, "authoritative": false, }))? @@ -736,8 +774,17 @@ fn serve(args: Args) -> Result<()> { } fn managed_service_serve(args: Args) -> Result<()> { + let (release, _) = pinned_packaged_release(&args, true)?; + let expected_supervisor = fs::canonicalize(epiphany_packaged_release_binary_path( + &release, + "supervisor", + )?)?; + if fs::canonicalize(env::current_exe()?)? != expected_supervisor { + anyhow::bail!("managed-service reconciler executable is not the pinned release supervisor"); + } let mut iteration = 0_u64; loop { + pinned_packaged_release(&args, true)?; iteration = iteration.saturating_add(1); let policies = load_epiphany_cultmesh_managed_service_policies(&args.store, args.runtime_id.clone())?; @@ -1260,7 +1307,7 @@ fn semantic_projector_service_policy(mut args: Args) -> Result<()> { "managed-service-policy-{SEMANTIC_PROJECTOR_SERVICE_ID}" )); args.restart_mode = "always".to_string(); - args.service_command = Some(semantic_projector_command_path()?); + args.service_command = Some(packaged_role_command_path(&args, "semantic-projector")?); args.service_args = semantic_projector_service_args( agent_store, runtime_store, @@ -1300,7 +1347,10 @@ fn workspace_coverage_projector_service_policy(mut args: Args) -> Result<()> { "managed-service-policy-{WORKSPACE_COVERAGE_PROJECTOR_SERVICE_ID}" )); args.restart_mode = "always".to_string(); - args.service_command = Some(workspace_coverage_projector_command_path()?); + args.service_command = Some(packaged_role_command_path( + &args, + "workspace-coverage-projector", + )?); args.service_args = workspace_coverage_projector_service_args( runtime_store, &args.store, @@ -1318,22 +1368,28 @@ fn workspace_coverage_projector_service_policy(mut args: Args) -> Result<()> { write_managed_service_policy(args) } -fn workspace_coverage_projector_command_path() -> Result { - let binary = if cfg!(windows) { - "epiphany-workspace-coverage-projector.exe" - } else { - "epiphany-workspace-coverage-projector" - }; - let path = env::current_exe() - .map(|path| path.with_file_name(binary)) - .context("failed to derive packaged workspace coverage projector executable")?; - if !path.is_file() { - anyhow::bail!( - "packaged workspace coverage projector executable is missing at {}; build both supervisor and projector together", - path.display() - ); +fn packaged_role_command_path(args: &Args, role: &str) -> Result { + #[cfg(feature = "workspace-coverage-recovery-smoke")] + if args.release_id.is_none() + && std::env::var_os("EPIPHANY_WORKSPACE_COVERAGE_SMOKE_DIAGNOSTICS").is_some() + { + let file_name = match role { + "semantic-projector" if cfg!(windows) => "epiphany-memory-semantic-projector.exe", + "semantic-projector" => "epiphany-memory-semantic-projector", + "workspace-coverage-projector" if cfg!(windows) => { + "epiphany-workspace-coverage-projector.exe" + } + "workspace-coverage-projector" => "epiphany-workspace-coverage-projector", + _ => anyhow::bail!("unknown smoke-only packaged role {role}"), + }; + let path = env::current_exe()?.with_file_name(file_name); + if !path.is_file() { + anyhow::bail!("smoke-only sibling is absent: {}", path.display()); + } + return Ok(path); } - Ok(path) + let (release, _) = pinned_packaged_release(args, true)?; + epiphany_packaged_release_binary_path(&release, role) } fn workspace_coverage_projector_service_args( @@ -1364,24 +1420,6 @@ fn workspace_coverage_projector_service_args( ] } -fn semantic_projector_command_path() -> Result { - let binary = if cfg!(windows) { - "epiphany-memory-semantic-projector.exe" - } else { - "epiphany-memory-semantic-projector" - }; - let path = env::current_exe() - .map(|path| path.with_file_name(binary)) - .context("failed to derive packaged semantic projector executable")?; - if !path.is_file() { - anyhow::bail!( - "packaged semantic projector executable is missing at {}; build both supervisor and projector together", - path.display() - ); - } - Ok(path) -} - fn semantic_projector_service_args( agent_store: &Path, runtime_store: &Path, @@ -1465,12 +1503,32 @@ fn managed_service_read(args: Args) -> Result<()> { } fn managed_service_reconcile(mut args: Args) -> Result<()> { + let pinned_release = if args.release_id.is_some() { + Some(pinned_packaged_release(&args, true)?.0) + } else { + None + }; let policy = load_epiphany_cultmesh_managed_service_policy( &args.store, args.runtime_id.clone(), &args.service_id, )? .with_context(|| format!("managed service policy missing for {}", args.service_id))?; + if let Some(release) = &pinned_release { + let expected_role = match policy.service_id.as_str() { + SEMANTIC_PROJECTOR_SERVICE_ID => Some("semantic-projector"), + WORKSPACE_COVERAGE_PROJECTOR_SERVICE_ID => Some("workspace-coverage-projector"), + _ => None, + }; + if let Some(role) = expected_role + && fs::canonicalize(&policy.command)? + != fs::canonicalize(epiphany_packaged_release_binary_path(release, role)?)? + { + anyhow::bail!( + "reserved managed-service policy command is outside pinned release role {role}" + ); + } + } if policy.service_id == WORKSPACE_COVERAGE_PROJECTOR_SERVICE_ID { return reconcile_workspace_coverage_projector(args, policy); } @@ -3315,7 +3373,7 @@ mod semantic_projector_authority_tests { let specialized_end = specialized_tail.find("\nfn ").unwrap(); let specialized = &specialized_tail[..specialized_end]; assert!(specialized.contains("args.service_command.is_some()")); - assert!(specialized.contains("semantic_projector_command_path()")); + assert!(specialized.contains("packaged_role_command_path(&args, \"semantic-projector\")")); assert!(specialized.contains("write_managed_service_policy(args)")); let legacy_cli = include_str!("epiphany-memory-semantic.rs"); @@ -3367,6 +3425,9 @@ mod semantic_projector_authority_tests { let body = &tail[..end]; for required in [ "managed-service-serve", + "--release-id", + "--release-witness-sha256", + "pinned_packaged_release", "New-ScheduledTaskTrigger -AtLogOn", "LogonType Interactive", "RunLevel Limited", @@ -3385,6 +3446,7 @@ mod semantic_projector_authority_tests { ); } assert!(!body.contains("sc.exe")); + assert!(body.contains("--service-command is forbidden")); assert!(source.contains("let mut task_restart_count = 999_u32")); assert!(source.contains("task_readback_has_drift")); assert!(source.contains("\"install-drift\"")); @@ -3461,6 +3523,8 @@ struct Args { task_logon_delay_seconds: u64, task_restart_interval_seconds: u64, task_restart_count: u32, + release_id: Option, + release_witness_sha256: Option, } impl Args { @@ -3509,6 +3573,8 @@ impl Args { let mut task_restart_interval_seconds = 60_u64; let mut task_restart_count = 999_u32; let mut fatal_log = None; + let mut release_id = None; + let mut release_witness_sha256 = None; while let Some(arg) = values.next() { match arg.as_str() { @@ -3677,6 +3743,16 @@ impl Args { values.next().context("missing --fatal-log value")?, )); } + "--release-id" => { + release_id = Some(values.next().context("missing --release-id value")?); + } + "--release-witness-sha256" => { + release_witness_sha256 = Some( + values + .next() + .context("missing --release-witness-sha256 value")?, + ); + } other => anyhow::bail!("unknown argument {other:?}"), } } @@ -3825,6 +3901,8 @@ impl Args { task_logon_delay_seconds, task_restart_interval_seconds, task_restart_count, + release_id, + release_witness_sha256, }) } } diff --git a/epiphany-core/src/bin/epiphany-memory-semantic-projector.rs b/epiphany-core/src/bin/epiphany-memory-semantic-projector.rs index 2e7d6524..95ec6896 100644 --- a/epiphany-core/src/bin/epiphany-memory-semantic-projector.rs +++ b/epiphany-core/src/bin/epiphany-memory-semantic-projector.rs @@ -123,8 +123,7 @@ fn main() -> Result<()> { "healthPublicationFaults": health_publication_faults, "selectedScopeId": outcome.selected_scope_id, "privateStateExposed": false, - "authoritative": false, - "queryAdmission": false + "authoritative": false }))? ); if args.max_iterations != 0 && sequence >= args.max_iterations { diff --git a/epiphany-core/src/bin/epiphany-memory-semantic.rs b/epiphany-core/src/bin/epiphany-memory-semantic.rs index 5aee8935..67d3b251 100644 --- a/epiphany-core/src/bin/epiphany-memory-semantic.rs +++ b/epiphany-core/src/bin/epiphany-memory-semantic.rs @@ -254,7 +254,6 @@ fn print_semantic_health( "authoritative": health.authoritative, "nonAuthoritative": !health.authoritative, "authority": "sight-only", - "queryAdmission": false, "queryEligibleDisplayOnly": health.query_eligible_display_only, "privateStateExposed": health.private_state_exposed, })) diff --git a/epiphany-core/src/bin/epiphany-release.rs b/epiphany-core/src/bin/epiphany-release.rs new file mode 100644 index 00000000..12e60395 --- /dev/null +++ b/epiphany-core/src/bin/epiphany-release.rs @@ -0,0 +1,74 @@ +use anyhow::{Context, Result, bail}; +use epiphany_core::{ + PackageReleaseRequest, epiphany_packaged_release_witness_sha256, + package_and_publish_epiphany_release, +}; +use std::env; +use std::path::PathBuf; +use std::process::Command; + +fn main() -> Result<()> { + let mut args = env::args().skip(1); + if args.next().as_deref() != Some("package") { + bail!( + "usage: epiphany-release package --repo PATH --destination PATH --store PATH --runtime-id ID [--target-triple TRIPLE]" + ); + } + let mut repo = None; + let mut destination = None; + let mut store = None; + let mut runtime = None; + let mut target = None; + while let Some(flag) = args.next() { + let value = args + .next() + .with_context(|| format!("missing value for {flag}"))?; + match flag.as_str() { + "--repo" => repo = Some(PathBuf::from(value)), + "--destination" => destination = Some(PathBuf::from(value)), + "--store" => store = Some(PathBuf::from(value)), + "--runtime-id" => runtime = Some(value), + "--target-triple" => target = Some(value), + _ => bail!("unknown argument {flag}"), + } + } + let repo = repo.context("missing --repo")?; + let destination = destination.context("missing --destination")?; + let store = store.context("missing --store")?; + let runtime = runtime.context("missing --runtime-id")?; + let rustc = command_output("rustc", &["-vV"])?; + let target = target + .or_else(|| { + rustc + .lines() + .find_map(|line| line.strip_prefix("host: ").map(str::to_string)) + }) + .context("rustc did not report host target")?; + let witness = package_and_publish_epiphany_release(PackageReleaseRequest { + repo: &repo, + destination: &destination, + store: &store, + runtime_id: &runtime, + target_triple: &target, + toolchain_fingerprint: &rustc, + })?; + let witness_sha256 = epiphany_packaged_release_witness_sha256(&witness)?; + println!( + "{}", + serde_json::to_string_pretty( + &serde_json::json!({"schemaVersion":witness.schema_version,"status":"published","releaseId":witness.release_id,"witnessSha256":witness_sha256,"sourceCommit":witness.source_commit_sha,"packageRoot":witness.package_root,"binaryCount":witness.binaries.len(),"privateStateExposed":false}) + )? + ); + Ok(()) +} + +fn command_output(command: &str, args: &[&str]) -> Result { + let output = Command::new(command).args(args).output()?; + if !output.status.success() { + bail!( + "{command} failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + Ok(String::from_utf8(output.stdout)?.trim().to_string()) +} diff --git a/epiphany-core/src/bin/epiphany-verse-query.rs b/epiphany-core/src/bin/epiphany-verse-query.rs index c93906ab..61491dc6 100644 --- a/epiphany-core/src/bin/epiphany-verse-query.rs +++ b/epiphany-core/src/bin/epiphany-verse-query.rs @@ -291,7 +291,6 @@ fn run_cli() -> Result<()> { "privateStateExposed": false, "authoritative": false, "authority": "sight-only", - "queryAdmission": false, "rows": report_rows, "tuiRows": tui_rows, }))? @@ -2218,7 +2217,7 @@ fn semantic_projection_health_tui_row( row: &EpiphanyCultMeshSemanticProjectionHealthEntry, ) -> String { format!( - "SEMANTIC-HEALTH swarm={} partition={} generation={} status=observed-{} documents={} dimensions={} provider={} incarnation={} authority=sight-only query-admission=false private=false", + "SEMANTIC-HEALTH swarm={} partition={} generation={} status=observed-{} documents={} dimensions={} provider={} incarnation={} authority=sight-only private=false", row.swarm_id, row.partition, row.source_generation, @@ -2253,7 +2252,6 @@ fn semantic_projection_health_report_row( "authoritative": row.authoritative, "nonAuthoritative": !row.authoritative, "authority": "sight-only", - "queryAdmission": false, "queryEligibleDisplayOnly": row.query_eligible_display_only, "privateStateExposed": row.private_state_exposed, }) diff --git a/epiphany-core/src/cultmesh_integration.rs b/epiphany-core/src/cultmesh_integration.rs index 76afb80a..92db26ff 100644 --- a/epiphany-core/src/cultmesh_integration.rs +++ b/epiphany-core/src/cultmesh_integration.rs @@ -5,6 +5,7 @@ use crate::default_hands_cultnet_contracts; use crate::default_mind_cultnet_contracts; use crate::default_soul_cultnet_contracts; use crate::default_substrate_gate_cultnet_contracts; +use crate::packaged_release::{EpiphanyPackagedReleaseEntry, EpiphanyPackagedReleaseHead}; use crate::workspace_coverage_process_documents::{ WORKSPACE_COVERAGE_PROCESS_EVIDENCE_HEAD_SCHEMA_VERSION, WORKSPACE_COVERAGE_PROCESS_LAUNCH_SCHEMA_VERSION, @@ -2650,6 +2651,8 @@ cultmesh_documents!(EpiphanyCultMeshDocuments { WorkspaceCoverageProcessEvidenceHead => WORKSPACE_COVERAGE_PROCESS_EVIDENCE_HEAD_SCHEMA_VERSION, WorkspaceCoverageProviderHeartbeatEntry => WORKSPACE_COVERAGE_PROVIDER_HEARTBEAT_SCHEMA_VERSION, WorkspaceCoverageProcessTerminationObservationEntry => WORKSPACE_COVERAGE_PROCESS_TERMINATION_SCHEMA_VERSION, + EpiphanyPackagedReleaseEntry => crate::packaged_release::EPIPHANY_PACKAGED_RELEASE_SCHEMA_VERSION, + EpiphanyPackagedReleaseHead => crate::packaged_release::EPIPHANY_PACKAGED_RELEASE_HEAD_SCHEMA_VERSION, EpiphanyCultMeshIdunnDeploymentReceiptEntry => EPIPHANY_CULTMESH_IDUNN_DEPLOYMENT_RECEIPT_SCHEMA_VERSION, EpiphanyCultMeshIdunnAftercareAuditReceiptEntry => EPIPHANY_CULTMESH_IDUNN_AFTERCARE_AUDIT_RECEIPT_SCHEMA_VERSION, EpiphanyCultMeshSwarmBrakeEntry => EPIPHANY_CULTMESH_SWARM_BRAKE_SCHEMA_VERSION, diff --git a/epiphany-core/src/lib.rs b/epiphany-core/src/lib.rs index 3355ccf5..6280d05f 100644 --- a/epiphany-core/src/lib.rs +++ b/epiphany-core/src/lib.rs @@ -19,6 +19,7 @@ mod host_identity; mod memory_graph; mod mind_gateway; mod organ_dependencies; +mod packaged_release; mod persona_turn; mod process_observation; mod promotion; @@ -792,6 +793,16 @@ pub use organ_dependencies::evaluate_receipt_proof_profiles; pub use organ_dependencies::receipt_proof_evaluation_errors; pub use organ_dependencies::render_organ_dependencies; pub use organ_dependencies::render_organ_dependency; +pub use packaged_release::{ + EPIPHANY_PACKAGED_RELEASE_HEAD_SCHEMA_VERSION, EPIPHANY_PACKAGED_RELEASE_SCHEMA_VERSION, + EpiphanyPackagedReleaseBinary, EpiphanyPackagedReleaseEntry, EpiphanyPackagedReleaseHead, + PackageReleaseRequest, authenticate_epiphany_packaged_release, + epiphany_packaged_release_binary_path, epiphany_packaged_release_witness_sha256, + load_epiphany_packaged_release, load_epiphany_packaged_release_head, + package_and_publish_epiphany_release, publish_epiphany_packaged_release, + required_packaged_release_binaries, validate_epiphany_packaged_release, + verify_epiphany_packaged_release_files, +}; pub use persona_turn::PERSONA_INTERPRETER_PROMPT_SCHEMA_VERSION; pub use persona_turn::PERSONA_PROJECTOR_PROMPT_SCHEMA_VERSION; pub use persona_turn::PERSONA_TURN_PROMPT_SCHEMA_VERSION; diff --git a/epiphany-core/src/packaged_release.rs b/epiphany-core/src/packaged_release.rs new file mode 100644 index 00000000..3d3c513e --- /dev/null +++ b/epiphany-core/src/packaged_release.rs @@ -0,0 +1,647 @@ +use crate::open_epiphany_cultmesh_node; +use anyhow::{Context, Result, anyhow, bail}; +use chrono::{DateTime, Utc}; +use cultcache_rs::{DatabaseEntry, SingleFileMessagePackBackingStore}; +use sha2::{Digest, Sha256}; +use std::collections::BTreeSet; +use std::fs; +use std::path::{Path, PathBuf}; +use uuid::Uuid; + +pub const EPIPHANY_PACKAGED_RELEASE_SCHEMA_VERSION: &str = "epiphany.packaged_release.v0"; +pub const EPIPHANY_PACKAGED_RELEASE_HEAD_SCHEMA_VERSION: &str = "epiphany.packaged_release_head.v0"; +const RELEASE_KEY_PREFIX: &str = "epiphany-local/packaged-release/by-id/"; +const RELEASE_HEAD_KEY: &str = "epiphany-local/packaged-release/current"; + +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct EpiphanyPackagedReleaseBinary { + pub role: String, + pub file_name: String, + pub canonical_path: String, + pub sha256: String, + pub byte_len: u64, +} + +#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.packaged_release", + schema = "EpiphanyPackagedReleaseEntry" +)] +pub struct EpiphanyPackagedReleaseEntry { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub release_id: String, + #[cultcache(key = 2)] + pub runtime_id: String, + #[cultcache(key = 3)] + pub source_commit_sha: String, + #[cultcache(key = 4)] + pub target_triple: String, + #[cultcache(key = 5)] + pub cargo_profile: String, + #[cultcache(key = 6)] + pub toolchain_fingerprint: String, + #[cultcache(key = 7)] + pub created_at_utc: String, + #[cultcache(key = 8)] + pub package_root: String, + #[cultcache(key = 9)] + pub binaries: Vec, + #[cultcache(key = 10)] + pub private_state_exposed: bool, +} + +#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.packaged_release_head", + schema = "EpiphanyPackagedReleaseHead" +)] +pub struct EpiphanyPackagedReleaseHead { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub runtime_id: String, + #[cultcache(key = 2)] + pub release_id: String, + #[cultcache(key = 3)] + pub witness_sha256: String, + #[cultcache(key = 4)] + pub published_at_utc: String, +} + +pub struct PackageReleaseRequest<'a> { + pub repo: &'a Path, + pub destination: &'a Path, + pub store: &'a Path, + pub runtime_id: &'a str, + pub target_triple: &'a str, + pub toolchain_fingerprint: &'a str, +} + +pub fn required_packaged_release_binaries() -> Vec<(&'static str, &'static str)> { + vec![ + ( + "supervisor", + if cfg!(windows) { + "epiphany-daemon-supervisor.exe" + } else { + "epiphany-daemon-supervisor" + }, + ), + ( + "semantic-projector", + if cfg!(windows) { + "epiphany-memory-semantic-projector.exe" + } else { + "epiphany-memory-semantic-projector" + }, + ), + ( + "workspace-coverage-projector", + if cfg!(windows) { + "epiphany-workspace-coverage-projector.exe" + } else { + "epiphany-workspace-coverage-projector" + }, + ), + ( + "semantic-query", + if cfg!(windows) { + "epiphany-memory-semantic.exe" + } else { + "epiphany-memory-semantic" + }, + ), + ] +} + +pub fn package_and_publish_epiphany_release( + request: PackageReleaseRequest<'_>, +) -> Result { + let (source_commit_sha, source_commit_time) = clean_source_commit(request.repo)?; + require_nonempty("runtime id", request.runtime_id)?; + require_nonempty("target triple", request.target_triple)?; + require_nonempty("toolchain fingerprint", request.toolchain_fingerprint)?; + fs::create_dir_all(request.destination)?; + let destination = fs::canonicalize(request.destination)?; + let source_root = destination.join(format!(".source-{}", Uuid::new_v4())); + let source_guard = GitWorktreeGuard::create(request.repo, &source_root, &source_commit_sha)?; + let build_root = destination.join(format!(".build-{}", Uuid::new_v4())); + let _build_guard = DirectoryCleanup(build_root.clone()); + build_required_release_siblings(&source_guard.path, &build_root, request.target_triple)?; + let built_dir = build_root.join(request.target_triple).join("release"); + let staging = destination.join(format!(".staging-{}", Uuid::new_v4())); + fs::create_dir(&staging)?; + let result = (|| { + let mut binaries = Vec::new(); + for (role, file_name) in required_packaged_release_binaries() { + let source = built_dir.join(file_name); + if !source.is_file() { + bail!("required packaged sibling is absent: {}", source.display()); + } + let target = staging.join(file_name); + fs::copy(&source, &target)?; + binaries.push(binary_record(role, file_name, &target)?); + } + binaries.sort_by(|left, right| left.role.cmp(&right.role)); + let release_id = release_id( + request.runtime_id, + &source_commit_sha, + request.target_triple, + request.toolchain_fingerprint, + &binaries, + ); + let final_root = destination.join(&source_commit_sha).join(&release_id); + fs::create_dir_all(final_root.parent().expect("release root has parent"))?; + for binary in &mut binaries { + binary.canonical_path = final_root.join(&binary.file_name).display().to_string(); + } + let witness = EpiphanyPackagedReleaseEntry { + schema_version: EPIPHANY_PACKAGED_RELEASE_SCHEMA_VERSION.into(), + release_id, + runtime_id: request.runtime_id.into(), + source_commit_sha, + target_triple: request.target_triple.into(), + cargo_profile: "release".into(), + toolchain_fingerprint: request.toolchain_fingerprint.into(), + created_at_utc: source_commit_time, + package_root: final_root.display().to_string(), + binaries, + private_state_exposed: false, + }; + validate_epiphany_packaged_release(&witness)?; + if final_root.exists() { + verify_epiphany_packaged_release_files(&witness)?; + fs::remove_dir_all(&staging)?; + } else { + fs::rename(&staging, &final_root) + .context("failed to atomically publish packaged release directory")?; + verify_epiphany_packaged_release_files(&witness)?; + } + publish_epiphany_packaged_release(request.store, request.runtime_id, witness) + })(); + if staging.exists() { + let _ = fs::remove_dir_all(staging); + } + result +} + +struct DirectoryCleanup(PathBuf); + +impl Drop for DirectoryCleanup { + fn drop(&mut self) { + if self.0.exists() { + let _ = fs::remove_dir_all(&self.0); + } + } +} + +struct GitWorktreeGuard { + repo: PathBuf, + path: PathBuf, +} + +impl GitWorktreeGuard { + fn create(repo: &Path, path: &Path, commit: &str) -> Result { + let output = std::process::Command::new("git") + .args(["worktree", "add", "--detach"]) + .arg(path) + .arg(commit) + .current_dir(repo) + .output()?; + if !output.status.success() { + bail!( + "failed to create exact-source release worktree: {}", + String::from_utf8_lossy(&output.stderr) + ); + } + Ok(Self { + repo: repo.to_path_buf(), + path: path.to_path_buf(), + }) + } +} + +impl Drop for GitWorktreeGuard { + fn drop(&mut self) { + let removed = std::process::Command::new("git") + .args(["worktree", "remove", "--force"]) + .arg(&self.path) + .current_dir(&self.repo) + .status() + .is_ok_and(|status| status.success()); + if !removed && self.path.exists() { + let _ = fs::remove_dir_all(&self.path); + } + } +} + +fn build_required_release_siblings(repo: &Path, target_dir: &Path, target: &str) -> Result<()> { + let manifest = repo.join("epiphany-core").join("Cargo.toml"); + if !manifest.is_file() { + bail!( + "Epiphany release manifest is absent: {}", + manifest.display() + ); + } + let mut command = std::process::Command::new("cargo"); + command + .arg("build") + .arg("--release") + .arg("--manifest-path") + .arg(&manifest) + .arg("--target-dir") + .arg(target_dir) + .arg("--target") + .arg(target); + command.arg("--locked"); + for (role, _) in required_packaged_release_binaries() { + let binary = match role { + "supervisor" => "epiphany-daemon-supervisor", + "semantic-projector" => "epiphany-memory-semantic-projector", + "workspace-coverage-projector" => "epiphany-workspace-coverage-projector", + "semantic-query" => "epiphany-memory-semantic", + _ => bail!("unknown required release role {role}"), + }; + command.arg("--bin").arg(binary); + } + let status = command + .status() + .context("failed to start owned release build")?; + if !status.success() { + bail!("owned Epiphany release build failed"); + } + Ok(()) +} + +pub fn validate_epiphany_packaged_release(entry: &EpiphanyPackagedReleaseEntry) -> Result<()> { + if entry.schema_version != EPIPHANY_PACKAGED_RELEASE_SCHEMA_VERSION { + bail!("unsupported packaged release schema"); + } + require_nonempty("runtime id", &entry.runtime_id)?; + validate_commit(&entry.source_commit_sha)?; + require_nonempty("target triple", &entry.target_triple)?; + if entry.cargo_profile != "release" { + bail!("packaged release profile must be release"); + } + require_nonempty("toolchain fingerprint", &entry.toolchain_fingerprint)?; + DateTime::parse_from_rfc3339(&entry.created_at_utc)?; + if entry.private_state_exposed { + bail!("packaged release may not expose private state"); + } + let root = Path::new(&entry.package_root); + if !root.is_absolute() { + bail!("packaged release root must be absolute"); + } + let required = required_packaged_release_binaries(); + if entry.binaries.len() != required.len() { + bail!("packaged release sibling set is incomplete"); + } + let mut roles = BTreeSet::new(); + let mut names = BTreeSet::new(); + for binary in &entry.binaries { + if !roles.insert(binary.role.as_str()) || !names.insert(binary.file_name.as_str()) { + bail!("packaged release contains duplicate role or filename"); + } + if !required + .iter() + .any(|pair| pair.0 == binary.role && pair.1 == binary.file_name) + { + bail!("packaged release contains an unknown or swapped sibling role"); + } + validate_sha256(&binary.sha256)?; + if binary.byte_len == 0 { + bail!("packaged release binary is empty"); + } + if Path::new(&binary.canonical_path) != root.join(&binary.file_name) { + bail!("packaged release binary path escapes or disagrees with package root"); + } + } + let mut identity_binaries = entry.binaries.clone(); + identity_binaries.sort_by(|left, right| left.role.cmp(&right.role)); + let expected = release_id( + &entry.runtime_id, + &entry.source_commit_sha, + &entry.target_triple, + &entry.toolchain_fingerprint, + &identity_binaries, + ); + if entry.release_id != expected { + bail!("packaged release id does not authenticate sibling set"); + } + Ok(()) +} + +pub fn verify_epiphany_packaged_release_files(entry: &EpiphanyPackagedReleaseEntry) -> Result<()> { + validate_epiphany_packaged_release(entry)?; + let root = fs::canonicalize(&entry.package_root).context("packaged release root is absent")?; + if root != PathBuf::from(&entry.package_root) { + bail!("packaged release root is not canonical"); + } + let actual_names = fs::read_dir(&root)? + .map(|item| Ok(item?.file_name().to_string_lossy().into_owned())) + .collect::>>()?; + let expected_names = entry + .binaries + .iter() + .map(|binary| binary.file_name.clone()) + .collect::>(); + if actual_names != expected_names { + bail!("packaged release directory is not the exact witnessed sibling set"); + } + for binary in &entry.binaries { + let path = root.join(&binary.file_name); + if fs::canonicalize(&path)? != PathBuf::from(&binary.canonical_path) { + bail!("packaged sibling path aliases another file"); + } + let metadata = fs::metadata(&path)?; + if !metadata.is_file() + || metadata.len() != binary.byte_len + || file_sha256(&path)? != binary.sha256 + { + bail!( + "packaged sibling bytes disagree with witness: {}", + binary.role + ); + } + } + Ok(()) +} + +pub fn publish_epiphany_packaged_release( + store: &Path, + runtime_id: &str, + entry: EpiphanyPackagedReleaseEntry, +) -> Result { + validate_epiphany_packaged_release(&entry)?; + verify_epiphany_packaged_release_files(&entry)?; + if entry.runtime_id != runtime_id { + bail!("release witness runtime disagrees with target Verse"); + } + let node = open_epiphany_cultmesh_node(store, runtime_id.to_string())?; + let identity_key = format!("{RELEASE_KEY_PREFIX}{}", entry.release_id); + let existing = node.get::(&identity_key)?; + if existing.as_ref().is_some_and(|current| current != &entry) { + bail!("immutable packaged release identity collision"); + } + let head = EpiphanyPackagedReleaseHead { + schema_version: EPIPHANY_PACKAGED_RELEASE_HEAD_SCHEMA_VERSION.into(), + runtime_id: runtime_id.into(), + release_id: entry.release_id.clone(), + witness_sha256: witness_sha256(&entry)?, + published_at_utc: Utc::now().to_rfc3339(), + }; + let prior_head = node + .cache() + .get_envelope::(RELEASE_HEAD_KEY)?; + let mut expected = Vec::new(); + if let Some(envelope) = prior_head { + expected.push(envelope); + } + let mut replacements = Vec::new(); + if existing.is_none() { + replacements.push(node.cache().prepare_entry(&identity_key, &entry)?.0); + } + replacements.push(node.cache().prepare_entry(RELEASE_HEAD_KEY, &head)?.0); + if !SingleFileMessagePackBackingStore::new(store) + .compare_and_swap_batch(&expected, replacements)? + { + bail!("packaged release publication lost current-head compare-and-swap"); + } + Ok(entry) +} + +pub fn load_epiphany_packaged_release( + store: &Path, + runtime_id: &str, + release_id: &str, +) -> Result> { + open_epiphany_cultmesh_node(store, runtime_id.to_string())? + .get(&format!("{RELEASE_KEY_PREFIX}{release_id}")) +} + +pub fn load_epiphany_packaged_release_head( + store: &Path, + runtime_id: &str, +) -> Result> { + open_epiphany_cultmesh_node(store, runtime_id.to_string())?.get(RELEASE_HEAD_KEY) +} + +pub fn authenticate_epiphany_packaged_release( + store: &Path, + runtime_id: &str, + release_id: &str, + expected_witness_sha256: &str, +) -> Result { + validate_sha256(expected_witness_sha256)?; + let entry = load_epiphany_packaged_release(store, runtime_id, release_id)? + .context("packaged release witness is absent")?; + validate_epiphany_packaged_release(&entry)?; + verify_epiphany_packaged_release_files(&entry)?; + if witness_sha256(&entry)? != expected_witness_sha256 { + bail!("packaged release witness digest disagrees with pinned task authority"); + } + Ok(entry) +} + +pub fn epiphany_packaged_release_witness_sha256( + entry: &EpiphanyPackagedReleaseEntry, +) -> Result { + witness_sha256(entry) +} + +pub fn epiphany_packaged_release_binary_path( + entry: &EpiphanyPackagedReleaseEntry, + role: &str, +) -> Result { + entry + .binaries + .iter() + .find(|binary| binary.role == role) + .map(|binary| PathBuf::from(&binary.canonical_path)) + .with_context(|| format!("packaged release lacks required role {role}")) +} + +fn clean_source_commit(repo: &Path) -> Result<(String, String)> { + let head = git(repo, &["rev-parse", "HEAD"])?; + validate_commit(&head)?; + if !git(repo, &["status", "--porcelain=v1", "--untracked-files=no"])?.is_empty() { + bail!("packaged release requires a clean tracked source commit"); + } + if git(repo, &["submodule", "status", "--recursive"])? + .lines() + .any(|line| line.starts_with(['-', '+', 'U'])) + { + bail!("packaged release requires clean initialized submodules"); + } + let committed_at = git(repo, &["show", "-s", "--format=%cI", "HEAD"])?; + DateTime::parse_from_rfc3339(&committed_at).context("Git commit time is not RFC3339")?; + Ok((head, committed_at)) +} + +fn git(repo: &Path, args: &[&str]) -> Result { + let output = std::process::Command::new("git") + .args(args) + .current_dir(repo) + .output()?; + if !output.status.success() { + bail!( + "git {} failed: {}", + args.join(" "), + String::from_utf8_lossy(&output.stderr) + ); + } + Ok(String::from_utf8(output.stdout)?.trim().to_string()) +} + +fn binary_record( + role: &str, + file_name: &str, + path: &Path, +) -> Result { + let canonical = fs::canonicalize(path)?; + let len = fs::metadata(&canonical)?.len(); + if len == 0 { + bail!("required packaged sibling is empty: {file_name}"); + } + Ok(EpiphanyPackagedReleaseBinary { + role: role.into(), + file_name: file_name.into(), + canonical_path: canonical.display().to_string(), + sha256: file_sha256(&canonical)?, + byte_len: len, + }) +} + +fn release_id( + runtime: &str, + commit: &str, + target: &str, + toolchain: &str, + binaries: &[EpiphanyPackagedReleaseBinary], +) -> String { + let mut hash = Sha256::new(); + for value in [ + "epiphany.packaged-release.identity.v0", + runtime, + commit, + target, + "release", + toolchain, + ] { + hash.update((value.len() as u64).to_be_bytes()); + hash.update(value.as_bytes()); + } + for binary in binaries { + for value in [ + &binary.role, + &binary.file_name, + &binary.sha256, + &binary.byte_len.to_string(), + ] { + hash.update((value.len() as u64).to_be_bytes()); + hash.update(value.as_bytes()); + } + } + format!("sha256-{:x}", hash.finalize()) +} + +fn witness_sha256(entry: &EpiphanyPackagedReleaseEntry) -> Result { + Ok(format!( + "sha256-{:x}", + Sha256::digest(rmp_serde::to_vec(entry)?) + )) +} +fn file_sha256(path: &Path) -> Result { + Ok(format!("sha256-{:x}", Sha256::digest(fs::read(path)?))) +} +fn validate_sha256(value: &str) -> Result<()> { + if value.len() != 71 + || !value.starts_with("sha256-") + || !value[7..] + .bytes() + .all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase()) + { + bail!("invalid SHA-256 digest"); + } + Ok(()) +} +fn validate_commit(value: &str) -> Result<()> { + if value.len() != 40 || !value.bytes().all(|b| b.is_ascii_hexdigit()) { + bail!("source commit must be a full 40-hex Git object id"); + } + Ok(()) +} +fn require_nonempty(label: &str, value: &str) -> Result<()> { + if value.trim().is_empty() { + return Err(anyhow!("{label} must not be empty")); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + fn fixture() -> (TempDir, EpiphanyPackagedReleaseEntry) { + let dir = TempDir::new().unwrap(); + let root = dir.path().join("release"); + fs::create_dir(&root).unwrap(); + let mut binaries = Vec::new(); + for (index, (role, name)) in required_packaged_release_binaries().into_iter().enumerate() { + let path = root.join(name); + fs::write(&path, format!("binary-{index}")).unwrap(); + binaries.push(binary_record(role, name, &path).unwrap()); + } + binaries.sort_by(|a, b| a.role.cmp(&b.role)); + let id = release_id("runtime", &"a".repeat(40), "target", "rustc", &binaries); + let entry = EpiphanyPackagedReleaseEntry { + schema_version: EPIPHANY_PACKAGED_RELEASE_SCHEMA_VERSION.into(), + release_id: id, + runtime_id: "runtime".into(), + source_commit_sha: "a".repeat(40), + target_triple: "target".into(), + cargo_profile: "release".into(), + toolchain_fingerprint: "rustc".into(), + created_at_utc: Utc::now().to_rfc3339(), + package_root: fs::canonicalize(root).unwrap().display().to_string(), + binaries, + private_state_exposed: false, + }; + (dir, entry) + } + + #[test] + fn exact_fixture_verifies() { + let (_d, e) = fixture(); + verify_epiphany_packaged_release_files(&e).unwrap(); + } + #[test] + fn one_byte_replacement_is_rejected() { + let (_d, e) = fixture(); + fs::write(&e.binaries[0].canonical_path, "hostile").unwrap(); + assert!(verify_epiphany_packaged_release_files(&e).is_err()); + } + #[test] + fn extra_sibling_is_rejected() { + let (_d, e) = fixture(); + fs::write(Path::new(&e.package_root).join("stowaway.exe"), "x").unwrap(); + assert!(verify_epiphany_packaged_release_files(&e).is_err()); + } + #[test] + fn swapped_role_is_rejected() { + let (_d, mut e) = fixture(); + let role = e.binaries[0].role.clone(); + e.binaries[0].role = e.binaries[1].role.clone(); + e.binaries[1].role = role; + assert!(validate_epiphany_packaged_release(&e).is_err()); + } + #[test] + fn counterfeit_release_id_is_rejected() { + let (_d, mut e) = fixture(); + e.release_id = format!("sha256-{}", "0".repeat(64)); + assert!(validate_epiphany_packaged_release(&e).is_err()); + } +} diff --git a/notes/epiphany-swarm-readiness-plan.md b/notes/epiphany-swarm-readiness-plan.md index bcef83f9..56ebf15a 100644 --- a/notes/epiphany-swarm-readiness-plan.md +++ b/notes/epiphany-swarm-readiness-plan.md @@ -59,23 +59,31 @@ VoidBot's ordinary spine is not the rotten part. Its durable lessons are: ## Current Mechanism -Epiphany currently has the right ingredients, but not yet the final live swarm -shape: +Epiphany owns its state, organ scheduler, workers, process policy, and local +operator surfaces in native Rust and typed CultCache/CultMesh documents. Codex +survives only as the retained OpenAI subscription-auth/model-transport bridge; +it no longer owns Epiphany routes, prompts, scheduling, or state mutation. -- `tools/epiphany_local_run.ps1` can build and smoke the compatibility shell. -- `epiphany-runtime-spine` owns typed runtime identity/session/job/result/event - documents and advertises CultNet contracts. -- heartbeat state owns role dossiers, initiative, heat, active-turn freeze, - sleep, rumination, appraisals, and derived reactions. -- role memory accepts reviewed `selfPatch` petitions into typed Ghostlight-like - dossiers. -- coordinator/CRRC/role/status surfaces exist, but the practical local runner - still goes through Codex app-server JSON-RPC for operator reads/actions. -- Persona, Discord, Rider, Unity, Void memory, and repo birth bridges are typed - surfaces or artifacts, but they are not yet an integrated live swarm loop. +- Runtime spine, Mind gateway, Substrate Gate, Eyes, Hands, Soul, Modeling, + Continuity, heartbeat, and Persona operations have typed intent/review/receipt + families. +- One heartbeat scheduler owns initiative, heat, active-turn freeze, + completion-gated cooldown, stale-turn recovery, sleep, and rumination. +- The local CultMesh swarm brake is typed and enforced at runner/action edges. +- Persona public speech has parent-side repetition/saturation eligibility and + Bifrost/Heimdall crossing gates. +- Memory has reviewed revise/retire/crystallize/prune/merge operations; Qdrant + remains rebuildable semantic sight rather than canonical Mind. +- Idunn owns managed projector lifecycle and exact abandoned-claim recovery. + Task Scheduler owns only current-user after-login Idunn presence. +- Mind and Modeling semantic query admission reauthenticate canonical CultCache + authority at query time against the Yggdrasil-hosted Qdrant/Ollama substrate. -The machine is pointed toward live use. It is not cleared for unattended swarm -operation. +The installed host is operational after login, but its mutable release directory +does not yet prove that supervisor, projectors, and query gate are one source +generation. Whole-repository readiness also lacks its Mind-owned race-bounded +join. Those are the remaining non-permission architecture gates. A real reboot/ +logon survival proof remains a separate operator-authorized boundary. ## Invariants @@ -96,66 +104,27 @@ operation. - No broad live scheduler until the local runner can show the typed surfaces that explain what it would do and why. -## Readiness Gate +## Readiness Evidence Matrix -Before live fire, Epiphany needs these gates closed: +This matrix is the live gate. A green row names its authority and proof; it is +not permission for another row to borrow the result. -1. **Operator Run Path** - - Keep `tools/epiphany_local_run.ps1` as the human entrypoint for now. - - It must keep printing a concise verdict. - - It must keep artifacts operator-safe by default. - - It must make Codex app-server dependency obvious as compatibility, not - architecture. +| Gate | Owner | Current evidence | State | +| --- | --- | --- | --- | +| Typed brake and initiative physiology | heartbeat/CultMesh | brake, completion cooldown, and stale-turn recovery tests and receipts | closed | +| Reviewed Mind and organ-state mutation | Mind gateway and organ services | typed review/commit/lifecycle families; no whole-state model write path | closed | +| Public speech/crossing eligibility | Persona + Bifrost/Heimdall | speech-audit receipts and fail-closed identity/capability checks | closed | +| Branch-local autonomous action | Substrate Gate + Hands + Soul + Mind | scoped grants and patch/command/commit/verdict/admission chains | closed | +| Semantic projection/query recovery | canonical admission + projector + query gate + Idunn | live Mind/Modeling semantic ranking and GUID-scoped exact process-recovery smokes | closed | +| Atomic installed sibling generation | release packager + Idunn | no immutable commit-addressed package witness yet; mutable sibling directory can mix builds | open | +| Whole-repository observed readiness | Mind | required Body R1/R2 + admitted model + live semantic + live workspace-coverage join does not yet exist | open | +| Current host package | release deployment | installed supervisor/projectors predate current recovery repairs | open | +| After-login recurrence | Task Scheduler + Idunn + ops tunnel | task recurrence and exact child recovery observed on current boot | closed for current boot | +| Reboot/logon survival | Task Scheduler + Idunn + ops tunnel | requires fresh post-boot lineage and query proof | permission-bound | -2. **Typed Swarm Brake** - - Add an Epiphany-owned pause document for swarm/heartbeat/live Persona - operation. - - Runners must fail closed when the document is missing, malformed, or - paused for a teardown reason, according to the surface being protected. - - The brake must be inspectable through Aquarium/status. - -3. **Initiative Scheduler Boundary** - - Keep one heartbeat scheduler for lanes/Personas/system organs. - - Heat is a multiplier on recovery pressure, not a time machine. - - Active-turn freeze and completion-gated cooldown are hard invariants. - - Stale active-turn recovery must write typed receipts. - -4. **Memory Lifecycle** - - Keep role-local memory writes as reviewed `selfPatch` operations. - - Add explicit lifecycle operations where missing: revise, retire, - crystallize, prune short-term residue, and merge incubation support. - - Sleep/maintenance may propose lifecycle operations; it must not silently - rewrite role identity. - -5. **Public Persona Safety** - - Persona speech eligibility must be parent-side state policy, not prompt hope. - - Track recent outputs, repeated topics, repeated openings, target/channel - saturation, and action kind. - - Work-shaped asks route to coordinator/Bifrost-style governance; banter - does not secretly become work dispatch. - -6. **CultNet Operator Surface** - - Move the local status/coordinator loop toward native CultNet snapshots, - intents, and receipts. - - Codex JSON-RPC may remain as the compatibility projection while the wall is - being evacuated. - - The next run path should consume typed documents first and only use - app-server JSON-RPC where no native route exists yet. - -7. **Audit And Review** - - Every automatic or semi-automatic action must produce a typed receipt. - - Semantic findings remain review-gated. - - Operator artifacts must seal direct thought, raw transcripts, and long - prompt-shaped notes by default. - -8. **Autonomous Branch Work** - - A repo swarm must own a git branch/worktree as its sacrificial workbench. - - Hands may edit, run checks, and commit autonomously inside that branch when - Substrate Gate, Hands, Soul, and Mind receipts support the work. - - Bifrost gates PR/upstream publication and credit; it does not gate every - local branch edit. - - Cross-body collaboration must travel through Odin/Eve/CultMesh/Bifrost - contracts instead of direct workspace rummaging. +`providerStatus`, heartbeats, Qdrant collection existence, task state, watcher +silence, legacy retrieval Ready, Eve/Gjallar rows, and empty dirty-path lists are +never substitutes for the Mind readiness join or query admission. ## Ranked Next Cuts @@ -195,16 +164,16 @@ Rebuild: ## Immediate Work Queue -0. Land the repo-swarm MVP contract and make repo initialization create a - branch-local autonomous workbench by default. -1. Add typed swarm pause/brake state and status projection. -2. Add stale active-turn recovery receipts to heartbeat state if the current - heartbeat store cannot prove recovery cleanly. -3. Add a Persona output audit surface before increasing public speech cadence. -4. Add memory lifecycle operations that match VoidBot's useful pattern without - porting its TypeScript state shape wholesale. -5. Move one local-run read path from Codex JSON-RPC compatibility to CultNet - snapshot/receipt consumption. +1. Build and authenticate one immutable, commit-addressed sibling release. Task + installation and Idunn must consume that witness and reject mixed binaries. +2. Implement Mind's race-bounded `RepositoryReadinessProjection`, including a + live workspace-coverage Qdrant evidence reader; stored `Current` is not enough. +3. Deploy the witnessed release, replace the stale running reconciler/projector, + and repeat exact recurrence plus Mind/Modeling query admission proofs. +4. With explicit live operator approval, reboot and prove the fresh Task + Scheduler -> Idunn -> exactly-one-projector lineage plus tunnel and query + admission. Do not silently widen after-login authority into boot service + authority. The correct posture is patient pressure. We are not rushing to live fire. We are aiming the barrel, checking the chamber, and making sure the thing does not try diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index d64b5b77..b7daaf9c 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -1964,3 +1964,32 @@ projection test passed before the recovery smoke. Next: audit deployability against the swarm readiness plan and live host state. Do not conflate code-complete physiology with the permission-bound reboot/logon proof; no reboot without explicit live operator approval. + +## Fresh deployability audit (2026-07-16) + +The host is operational after current-user login, not yet deployable as one +proven artifact generation. `GameCult-Yggdrasil-Tunnel` and +`Epiphany-Idunn-Managed-Service-Reconciler` are enabled/running; the Yggdrasil +Qdrant tunnel and Ollama endpoint are live; packaged Mind and Modeling queries +return semantic ranking. Local retired Qdrant remains stopped. This is real +current-boot physiology. + +Four non-permission wounds remain. Installed supervisor/projector binaries +predate recovery commit `34802407`; Task Scheduler pins one mutable supervisor +path while Idunn discovers siblings by filename/existence, so mixed generations +are possible; Mind has no race-bounded whole-repository readiness join; and the +old readiness plan described already-landed June work as future work. The plan +now contains the live evidence matrix. Packaging must become an immutable typed +commit-addressed release witness consumed by Task Scheduler and Idunn. Mind +readiness must join fresh Body R1/R2, exact Body-grounded RepoModel admission, +live Modeling semantic eligibility, and live workspace-coverage Qdrant evidence. +Stored coverage `Current` is not sufficient. + +The constant `queryAdmission=false` and supervisor fields naming an external +readiness authority are removed from sight-only projector/health/status +projections. Those surfaces never evaluated query admission and therefore have +no opinion. Query-time reauthentication remains the semantic admission owner. + +After the witnessed release is deployed, prove exact current-boot recurrence +and Mind/Modeling query ranking. A reboot/logon proof remains the sole +permission-bound host gate; do not reboot without explicit live approval. diff --git a/notes/semantic-projector-service-runbook.md b/notes/semantic-projector-service-runbook.md index f3cb2cce..0d51642c 100644 --- a/notes/semantic-projector-service-runbook.md +++ b/notes/semantic-projector-service-runbook.md @@ -21,13 +21,26 @@ embedding work over WireGuard, but moving only the projector to Yggdrasil would split local store authority. Do not install a second projector on Yggdrasil or one process per partition. -## Build +## Package one source generation ```powershell $env:CARGO_TARGET_DIR='C:\Users\Meta\.cargo-target-codex' -cargo build --release --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-memory-semantic-projector --bin epiphany-memory-semantic --bin epiphany-daemon-supervisor +cargo build --release --manifest-path .\epiphany-core\Cargo.toml --bin epiphany-release +& "$env:CARGO_TARGET_DIR\release\epiphany-release.exe" package ` + --repo . ` + --destination .\.epiphany-run\releases ` + --store .\.epiphany-run\cultmesh\local-verse.ccmp ` + --runtime-id epiphany-local ``` +The packager refuses dirty tracked source and owns a fresh isolated Cargo release +build of the supervisor, semantic projector, workspace-coverage projector, and +semantic query gate. It copies exactly those four siblings into an immutable +commit-addressed directory, verifies every byte, then atomically publishes a +typed CultCache release witness and current head. Keep the returned `releaseId`, +`witnessSha256`, and `packageRoot`; Task Scheduler and Idunn pin them. A mutable +`target\release` directory is build output, not deployment authority. + ## Transport configuration The reserved typed service policy binds the non-secret transport coordinates: @@ -65,10 +78,15 @@ REST/gRPC ports locally as `16333`/`16334`; do not restart the retired node. ## Publish the typed service policy ```powershell -$supervisor = '.\target\release\epiphany-daemon-supervisor.exe' +$releaseRoot = '' +$releaseId = '' +$releaseDigest = '' +$supervisor = Join-Path $releaseRoot 'epiphany-daemon-supervisor.exe' & $supervisor semantic-projector-service-policy ` --store .\.epiphany-run\cultmesh\local-verse.ccmp ` --runtime-id epiphany-local ` + --release-id $releaseId ` + --release-witness-sha256 $releaseDigest ` --agent-store .\state\agents.msgpack ` --runtime-store .\state\runtime-spine.msgpack ` --qdrant-url http://127.0.0.1:16333 ` @@ -80,8 +98,8 @@ $supervisor = '.\target\release\epiphany-daemon-supervisor.exe' This command writes the fixed `epiphany-memory-semantic-projector-service` managed-service policy with `restartMode=always`. Its generated child command contains both canonical -stores. The projector executable is derived as the packaged sibling of the -running supervisor and must already exist. Caller-supplied executable paths, +stores. The projector executable is resolved from the authenticated +`semantic-projector` role in the pinned release witness. Caller-supplied executable paths, service IDs, restart modes, arbitrary child arguments, and finite iteration limits do not own this service shape. The generic managed-service writer refuses the reserved service id. @@ -101,6 +119,25 @@ publishing a pulse or heartbeat. A launch receipt proves process creation; it does not claim semantic readiness. The first heartbeat comes later, after the projector holds the OS singleton and validates both canonical inputs. +## Install the pinned after-login reconciler + +```powershell +& $supervisor managed-service-task-install ` + --store .\.epiphany-run\cultmesh\local-verse.ccmp ` + --runtime-id epiphany-local ` + --release-id $releaseId ` + --cwd . ` + --loop-interval-seconds 60 +``` + +Task installation resolves the supervisor path and witness digest from the +typed release. The scheduled action pins both `--release-id` and +`--release-witness-sha256`; `managed-service-serve` authenticates the complete +four-sibling set and its own path before reconciliation, then revalidates the +set on every pulse. Reserved projector policies must name the exact witnessed +role paths. A task pointing at the right supervisor with a missing/wrong witness +argument, or a release directory with one changed sibling, is drift/failure. + ## Recovery Use `semantic-recover` only with the exact abandoned claim, the immutable launch @@ -142,7 +179,7 @@ curl.exe -fsS http://127.0.0.1:16333/collections Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match 'managed-service-serve|memory-semantic-projector' } | Select-Object ProcessId, ParentProcessId, CreationDate, CommandLine -C:\Users\Meta\.cargo-target-codex\release\epiphany-daemon-supervisor.exe ` +& $supervisor ` semantic-projector-service-status ` --store .\.epiphany-run\cultmesh\local-verse.ccmp ` --runtime-id epiphany-local @@ -181,7 +218,7 @@ BM25, which is a failed deployment proof rather than semantic readiness: ```powershell $env:EPIPHANY_QDRANT_URL='http://127.0.0.1:16333' $env:EPIPHANY_OLLAMA_BASE_URL='http://10.77.0.1:11435' -$semantic='C:\Users\Meta\.cargo-target-codex\release\epiphany-memory-semantic.exe' +$semantic=Join-Path $releaseRoot 'epiphany-memory-semantic.exe' & $semantic context --agent-store .\state\agents.msgpack --partition mind ` --text 'current Epiphany doctrine and memory' --query-id reboot-proof-mind --budget 8 & $semantic context --runtime-store .\state\runtime-spine.msgpack --partition modeling ` diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index a98f5a9a5537be26c09504b7a27a5c566e86f582..833c661cc4dfb8343c46fd56c8ef8ece9080ac91 100644 GIT binary patch delta 628 zcmX|-zlsz=5XPPL&jy;99U@$T>CWvs94lNP$Q_CX8h99(^-R~!6x-81+g-hT6D?wJ zFL28}hJt~Z8u$VRg0CPZVqj?XE*caKUsZqg)$i-i{dd3i-|uBN52r^(@%qb^;h$R{ z?q!QC`}TG-INuC1sq|izx+Zm9>c+>-VEFL&?Tb&vq?qQD2l@T!>7+P1ynORHySx5A z7@c5eT(3$p#675}iNTAeD%8LYLSSVOo<)nID@_(dfJMZJup(|iS->S}v=u_D*wdLv z8mh=Ja~`R-Y1;RXbW0(Sv*0?i@&;JXylWv)-Ei)#=^;cvlf~fxo-3v$LZ_;^s!``2 z4XS`*8LgI{*jTrMGLq3Dsb118B6&$2s5E42k(0dwjjXCh8hEBE&MVipa^w@@HLRSU zvquE7?9wR|oKc)kSH4CbK^Mg=G#IwswvDVx;jyzSdrnk#(X=eD*!dnh69aJHiOq2| ze4Mr?SP9*@5s0HLqSLhsb9kAeMT36)R7Yz;;B31BI|_fxR7s%!TS$*J)fjj;