From 8fb61ac23634b00a6d8b2a6416f7b7d97e4a24ab Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 11 Aug 2026 22:07:02 +0200 Subject: [PATCH] Bind public Eyes evidence to exact source authority --- Cargo.lock | 1 + epiphany-core/src/coordinator_acceptance.rs | 52 ++ epiphany-core/src/coordinator_launch.rs | 20 +- epiphany-core/src/eyes_gateway.rs | 49 ++ epiphany-core/src/lib.rs | 3 + epiphany-core/src/operator_command.rs | 3 +- epiphany-core/src/runtime_spine.rs | 516 +++++++++++++++++- epiphany-core/src/substrate_gate.rs | 53 +- epiphany-model-adapter/src/lib.rs | 3 + epiphany-openai-runtime/src/lib.rs | 54 +- epiphany-tool-mcp-runtime/Cargo.toml | 1 + epiphany-tool-mcp-runtime/src/lib.rs | 2 + epiphany-tool-mcp-runtime/src/main.rs | 5 +- .../src/native_public.rs | 182 ++++++ notes/epiphany-current-algorithmic-map.md | 43 ++ notes/perfect-machine-audit-roadmap.md | 47 +- .../epiphany.model-request.schema.json | 35 +- ...untime.model-execution-binding.schema.json | 3 +- 18 files changed, 1037 insertions(+), 35 deletions(-) create mode 100644 epiphany-tool-mcp-runtime/src/native_public.rs diff --git a/Cargo.lock b/Cargo.lock index b3f1d25e..c71a3f96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2293,6 +2293,7 @@ dependencies = [ "rmcp 2.2.0", "serde", "serde_json", + "sha2 0.10.9", "tempfile", "tokio", "toml", diff --git a/epiphany-core/src/coordinator_acceptance.rs b/epiphany-core/src/coordinator_acceptance.rs index 5c1433f1..a7f47d77 100644 --- a/epiphany-core/src/coordinator_acceptance.rs +++ b/epiphany-core/src/coordinator_acceptance.rs @@ -201,14 +201,48 @@ pub fn accept_coordinator_role_finding( let mut available = vec![MIND_GATEWAY_REVIEW_TYPE.to_string()]; let mut prerequisites = Vec::new(); if role_id == EpiphanyRoleResultRoleId::Research { + let public_source_lookups = runtime_authenticated_public_source_lookups_for_worker( + store, + finding.runtime_job_id.as_deref().unwrap_or_default(), + )?; + for lookup in &public_source_lookups { + if !finding + .evidence_ids + .iter() + .any(|evidence_id| evidence_id == &lookup.receipt_id) + || !finding + .files_inspected + .iter() + .any(|source_ref| source_ref == &lookup.source_ref) + { + return Err(anyhow::anyhow!( + "Research finding did not cite its authenticated public source receipt and source ref" + )); + } + } + if finding + .evidence_ids + .iter() + .any(|evidence_id| evidence_id.starts_with("eyes-source-")) + && public_source_lookups.is_empty() + { + return Err(anyhow::anyhow!( + "Research finding cited public source evidence without an authenticated lookup" + )); + } let packet = eyes_evidence_packet_from_research_finding( format!("eyes-packet-{}", update.accepted_receipt_id), &finding, &update.applied_patch, + &public_source_lookups, accepted_at.clone(), ); available.push(EYES_EVIDENCE_PACKET_TYPE.to_string()); prerequisites.push(EpiphanyAcceptancePrerequisite::Eyes(packet)); + for lookup in public_source_lookups { + available.push(EYES_SOURCE_LOOKUP_RECEIPT_TYPE.to_string()); + prerequisites.push(EpiphanyAcceptancePrerequisite::EyesSourceLookup(lookup)); + } let grant_id = format!( "substrate-grant-{}", finding.runtime_job_id.as_deref().unwrap_or_default() @@ -609,6 +643,7 @@ pub fn read_accepted_coordinator_state( #[derive(Debug, Clone, PartialEq, Eq)] pub enum EpiphanyAcceptancePrerequisite { Eyes(EyesEvidencePacket), + EyesSourceLookup(EyesSourceLookupReceipt), SubstrateGate(SubstrateGateRepoAccessGrantReceipt), Soul(SoulVerdictReceipt), Continuity(ContinuityRecoveryReceipt), @@ -641,6 +676,19 @@ fn commit_state_with_mind_witness( store_path, expected_state, )?; + for prerequisite in prerequisites { + if let EpiphanyAcceptancePrerequisite::EyesSourceLookup(document) = prerequisite { + let authenticated = crate::runtime_spine::authenticated_public_source_lookups_for_worker( + &cache, + &document.source_job_id, + )?; + if !authenticated.iter().any(|candidate| candidate == document) { + return Err(anyhow::anyhow!( + "Eyes source lookup lost its authenticated authority before Mind commit" + )); + } + } + } let (review_envelope, _) = cache.prepare_entry(&mind_review.gateway_id, mind_review)?; let (commit_envelope, _) = cache.prepare_entry(&commit_receipt.receipt_id, commit_receipt)?; let mut batch = vec![review_envelope, commit_envelope]; @@ -649,6 +697,9 @@ fn commit_state_with_mind_witness( EpiphanyAcceptancePrerequisite::Eyes(document) => { cache.prepare_entry(&document.packet_id, document)?.0 } + EpiphanyAcceptancePrerequisite::EyesSourceLookup(document) => { + cache.prepare_entry(&document.receipt_id, document)?.0 + } EpiphanyAcceptancePrerequisite::SubstrateGate(document) => { cache.prepare_entry(&document.receipt_id, document)?.0 } @@ -1067,6 +1118,7 @@ mod tests { evidence_ids: Vec::new(), observation_ids: Vec::new(), source_refs: Vec::new(), + source_lookup_receipt_ids: Vec::new(), summary: "looked".to_string(), uncertainty: "none".to_string(), emitted_at: "2026-07-11T00:00:00Z".to_string(), diff --git a/epiphany-core/src/coordinator_launch.rs b/epiphany-core/src/coordinator_launch.rs index 2c05decc..04902bf6 100644 --- a/epiphany-core/src/coordinator_launch.rs +++ b/epiphany-core/src/coordinator_launch.rs @@ -532,7 +532,12 @@ fn commit_coordinator_job_launch_in_cache( } batch.push(cache.prepare_entry(&binding.binding_record_id, &binding)?.0); } - if request.binding_id == EPIPHANY_RESEARCH_ROLE_BINDING_ID { + if matches!( + request.binding_id.as_str(), + EPIPHANY_RESEARCH_ROLE_BINDING_ID + | crate::EPIPHANY_MODELING_ROLE_BINDING_ID + | crate::EPIPHANY_VERIFICATION_ROLE_BINDING_ID + ) { let grant = substrate_gate_repo_access_grant_for_launch( format!("substrate-grant-{}", plan.backend_job_id), plan.backend_job_id.clone(), @@ -3887,11 +3892,14 @@ pub(crate) mod tests { .get_all::()? .is_empty() ); - assert!( - cache - .get_all::()? - .is_empty() - ); + let grants = cache.get_all::()?; + assert_eq!(grants.len(), 1); + assert_eq!(grants[0].binding_id, crate::EPIPHANY_MODELING_ROLE_BINDING_ID); + assert_eq!(grants[0].granted_operations, vec!["read", "snapshot"]); + assert!(!grants[0] + .granted_operations + .iter() + .any(|operation| operation == "publicSourceRead")); Ok(()) } diff --git a/epiphany-core/src/eyes_gateway.rs b/epiphany-core/src/eyes_gateway.rs index 210ef64f..cd2a85c5 100644 --- a/epiphany-core/src/eyes_gateway.rs +++ b/epiphany-core/src/eyes_gateway.rs @@ -17,6 +17,44 @@ pub const EYES_EVIDENCE_PACKET_SCHEMA_VERSION: &str = "epiphany.eyes.evidence_pa pub const EYES_EVIDENCE_REFUSAL_RECEIPT_SCHEMA_VERSION: &str = "epiphany.eyes.evidence_refusal_receipt.v0"; +#[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)] +#[cultcache( + type = "epiphany.eyes.source_lookup_receipt", + schema = "EyesSourceLookupReceipt" +)] +pub struct EyesSourceLookupReceipt { + #[cultcache(key = 0)] + pub schema_version: String, + #[cultcache(key = 1)] + pub receipt_id: String, + #[cultcache(key = 2)] + pub source_job_id: String, + #[cultcache(key = 3)] + pub substrate_grant_receipt_id: String, + #[cultcache(key = 4)] + pub tool_intent_id: String, + #[cultcache(key = 5)] + pub tool_receipt_id: String, + #[cultcache(key = 6)] + pub provider: String, + #[cultcache(key = 7)] + pub repository: String, + #[cultcache(key = 8)] + pub revision: String, + #[cultcache(key = 9)] + pub path: String, + #[cultcache(key = 10)] + pub source_ref: String, + #[cultcache(key = 11)] + pub content_sha256: String, + #[cultcache(key = 12)] + pub byte_count: u64, + #[cultcache(key = 13)] + pub observed_at: String, + #[cultcache(key = 14)] + pub contract: String, +} + #[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)] #[cultcache(type = "epiphany.eyes.evidence_packet", schema = "EyesEvidencePacket")] pub struct EyesEvidencePacket { @@ -44,6 +82,8 @@ pub struct EyesEvidencePacket { pub emitted_at: String, #[cultcache(key = 11)] pub contract: String, + #[cultcache(key = 12, default)] + pub source_lookup_receipt_ids: Vec, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -131,6 +171,7 @@ pub fn eyes_evidence_packet_from_research_finding( packet_id: String, finding: &EpiphanyRoleFindingInterpretation, patch: &EpiphanyRoleStatePatchDocument, + source_lookups: &[EyesSourceLookupReceipt], emitted_at: String, ) -> EyesEvidencePacket { let evidence_ids = patch @@ -164,6 +205,9 @@ pub fn eyes_evidence_packet_from_research_finding( push_unique(&mut source_refs, rendered); } } + for lookup in source_lookups { + push_unique(&mut source_refs, lookup.source_ref.clone()); + } EyesEvidencePacket { schema_version: EYES_EVIDENCE_PACKET_SCHEMA_VERSION.to_string(), packet_id, @@ -187,6 +231,10 @@ pub fn eyes_evidence_packet_from_research_finding( }, emitted_at, contract: "Eyes packet emitted from a reviewed Research lane finding; it makes the source-gathering evidence claim citable before Mind admission.".to_string(), + source_lookup_receipt_ids: source_lookups + .iter() + .map(|lookup| lookup.receipt_id.clone()) + .collect(), } } @@ -286,6 +334,7 @@ mod tests { "eyes-packet-1".to_string(), &finding, &patch, + &[], "2026-05-30T00:00:00Z".to_string(), ); assert_eq!(packet.source_role_id, "research"); diff --git a/epiphany-core/src/lib.rs b/epiphany-core/src/lib.rs index a789c044..1010f165 100644 --- a/epiphany-core/src/lib.rs +++ b/epiphany-core/src/lib.rs @@ -618,6 +618,7 @@ pub use eyes_gateway::EYES_SOURCE_LOOKUP_RECEIPT_SCHEMA_VERSION; pub use eyes_gateway::EYES_SOURCE_LOOKUP_RECEIPT_TYPE; pub use eyes_gateway::EyesCultNetContract; pub use eyes_gateway::EyesEvidencePacket; +pub use eyes_gateway::EyesSourceLookupReceipt; pub use eyes_gateway::default_eyes_cultnet_contracts; pub use eyes_gateway::eyes_evidence_packet_from_research_finding; pub use hands_gateway::*; @@ -1111,6 +1112,7 @@ pub use runtime_spine::put_runtime_reorient_worker_result; pub use runtime_spine::put_runtime_role_worker_result; pub use runtime_spine::put_runtime_tool_execution_intent; pub use runtime_spine::put_runtime_tool_execution_receipt; +pub use runtime_spine::runtime_authenticated_public_source_lookups_for_worker; #[cfg(test)] pub(crate) use runtime_spine::put_soul_verdict_receipt; pub use runtime_spine::put_substrate_gate_repo_access_grant_receipt; @@ -1213,6 +1215,7 @@ pub use substrate_gate::SubstrateGateRepoAccessGrantReceipt; pub use substrate_gate::default_substrate_gate_cultnet_contracts; pub use substrate_gate::substrate_gate_coordinator_implementation_grant; pub use substrate_gate::substrate_gate_repo_access_grant_for_launch; +pub use substrate_gate::substrate_gate_operation_for_governed_tool; pub use substrate_gate::substrate_gate_repo_work_planning_grant; pub use surfaces::AdmittedModelDirectionConsiderationContextProjection; pub use surfaces::EpiphanyAcceptanceBundle; diff --git a/epiphany-core/src/operator_command.rs b/epiphany-core/src/operator_command.rs index 2da37cd2..db2f8189 100644 --- a/epiphany-core/src/operator_command.rs +++ b/epiphany-core/src/operator_command.rs @@ -1195,7 +1195,8 @@ mod tests { "heartbeat.scheduler", "coordinator.run", "persona.public_speech", - "daemon.tool_invocation" + "daemon.tool_invocation", + "daemon.lifecycle_poke" ] ); assert!( diff --git a/epiphany-core/src/runtime_spine.rs b/epiphany-core/src/runtime_spine.rs index 425cb396..9e6b811b 100644 --- a/epiphany-core/src/runtime_spine.rs +++ b/epiphany-core/src/runtime_spine.rs @@ -28,6 +28,7 @@ use crate::eyes_gateway::EYES_EVIDENCE_REVIEW_TYPE; use crate::eyes_gateway::EYES_SOURCE_LOOKUP_RECEIPT_SCHEMA_VERSION; use crate::eyes_gateway::EYES_SOURCE_LOOKUP_RECEIPT_TYPE; use crate::eyes_gateway::EyesEvidencePacket; +use crate::eyes_gateway::EyesSourceLookupReceipt; use crate::hands_gateway::*; use crate::heartbeat_state::HEARTBEAT_STATE_SCHEMA_VERSION; use crate::heartbeat_state::HEARTBEAT_STATE_TYPE; @@ -358,6 +359,8 @@ pub struct EpiphanyRuntimeModelExecutionBinding { pub provider: String, #[cultcache(key = 6)] pub bound_at: String, + #[cultcache(key = 7, default)] + pub source_worker_job_id: Option, } #[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] @@ -1154,6 +1157,7 @@ pub fn runtime_spine_cache(store_path: impl AsRef) -> Result { cache.register_entry_type::()?; cache.register_entry_type::()?; cache.register_entry_type::()?; + cache.register_entry_type::()?; cache.register_entry_type::()?; cache.register_entry_type::()?; cache.register_entry_type::()?; @@ -1675,6 +1679,47 @@ pub fn open_runtime_model_execution( require_runtime_identity_not_archived(&cache, "session", &session_options.session_id)?; require_runtime_identity_not_archived(&cache, "job", &job_options.job_id)?; require_runtime_identity_not_archived(&cache, "model-request", &model_request.request_id)?; + let source_worker_envelopes = if let Some(worker_job_id) = + model_request.source_worker_job_id.as_deref() + { + let launch = cache + .get::(worker_job_id)? + .ok_or_else(|| { + anyhow!( + "model execution source worker {worker_job_id:?} has no launch request" + ) + })?; + if launch.job_id != worker_job_id { + return Err(anyhow!( + "model execution source worker launch identity disagrees" + )); + } + let worker_job = cache + .get::(worker_job_id)? + .ok_or_else(|| anyhow!("model execution source worker has no runtime job"))?; + if worker_job.role != launch.role + || matches!( + worker_job.status, + EpiphanyRuntimeJobStatus::Completed + | EpiphanyRuntimeJobStatus::Failed + | EpiphanyRuntimeJobStatus::Cancelled + ) + { + return Err(anyhow!( + "model execution source worker job is foreign or terminal" + )); + } + vec![ + cache + .get_envelope::(worker_job_id)? + .ok_or_else(|| anyhow!("model execution lost source worker job envelope"))?, + cache + .get_envelope::(worker_job_id)? + .ok_or_else(|| anyhow!("model execution lost source worker launch envelope"))?, + ] + } else { + Vec::new() + }; let existing_session = cache.get::(&session_options.session_id)?; let session = existing_session .clone() @@ -1763,11 +1808,13 @@ pub fn open_runtime_model_execution( job_id: job.job_id.clone(), provider: model_request.provider.clone(), bound_at: bound_at.to_string(), + source_worker_job_id: model_request.source_worker_job_id.clone(), }; let identity_envelope = cache .get_envelope::(RUNTIME_IDENTITY_KEY)? .ok_or_else(|| anyhow!("model execution lost its exact runtime identity envelope"))?; let mut expected = vec![identity_envelope.clone()]; + expected.extend(source_worker_envelopes.iter().cloned()); if existing_session.is_some() { expected.push( cache @@ -1775,7 +1822,7 @@ pub fn open_runtime_model_execution( .ok_or_else(|| anyhow!("model execution lost its exact session envelope"))?, ); } - let replacements = vec![ + let mut replacements = vec![ identity_envelope, cache.prepare_entry(&session.session_id, &session)?.0, cache.prepare_entry(&job.job_id, &job)?.0, @@ -1790,6 +1837,7 @@ pub fn open_runtime_model_execution( .prepare_entry(&provider_request.request_id, provider_request)? .0, ]; + replacements.extend(source_worker_envelopes); if !runtime_spine_backing_store(store_path)?.compare_and_swap_batch(&expected, replacements)? { return Err(anyhow!( "model execution request publication lost its snapshot fence" @@ -1843,6 +1891,8 @@ pub fn put_runtime_tool_execution_intent( "tool execution job {job_id:?} is foreign or terminal" )); } + let governed_source_authority = + validate_governed_source_tool_intent(&cache, job_id, intent)?; if cache .get::(&intent.intent_id)? .is_some() @@ -1874,6 +1924,8 @@ pub fn put_runtime_tool_execution_intent( job_envelope.clone(), ]; let mut replacements = expected.clone(); + expected.extend(governed_source_authority.iter().cloned()); + replacements.extend(governed_source_authority); if let Some(model_request_id) = intent.model_request_id.as_deref() { let model_binding = cache .get::(model_request_id)? @@ -1892,6 +1944,15 @@ pub fn put_runtime_tool_execution_intent( intent.intent_id )); } + let persisted_model_request = cache + .get::(model_request_id)? + .ok_or_else(|| anyhow!("model-derived tool intent lost its model request"))?; + if persisted_model_request.source_worker_job_id != model_binding.source_worker_job_id { + return Err(anyhow!( + "model-derived tool intent {:?} has substituted worker provenance", + intent.intent_id + )); + } for envelope in [ cache.get_envelope::(model_request_id)?, cache.get_envelope::(model_request_id)?, @@ -1971,6 +2032,7 @@ pub fn require_runtime_tool_execution_binding( "tool execution binding {intent_id:?} has foreign or archived ownership" )); } + let _ = validate_governed_source_tool_intent(&cache, &binding.job_id, &intent)?; if let Some(model_request_id) = binding.model_request_id.as_deref() { let model_binding = cache .get::(model_request_id)? @@ -3527,6 +3589,260 @@ pub fn runtime_worker_process_claim( cache.get::(&worker_process_claim_id(job_id)) } +pub fn runtime_authenticated_public_source_lookups_for_worker( + store_path: impl AsRef, + worker_job_id: &str, +) -> Result> { + validate_non_empty(worker_job_id, "public source worker job id")?; + let mut cache = runtime_spine_cache(store_path)?; + cache.pull_all_backing_stores()?; + require_identity(&cache)?; + authenticated_public_source_lookups_for_worker(&cache, worker_job_id) +} + +pub(crate) fn authenticated_public_source_lookups_for_worker( + cache: &CultCache, + worker_job_id: &str, +) -> Result> { + validate_non_empty(worker_job_id, "public source worker job id")?; + require_identity(cache)?; + let launch = cache + .get::(worker_job_id)? + .ok_or_else(|| anyhow!("public source worker has no launch request"))?; + if launch.binding_id != crate::EPIPHANY_RESEARCH_ROLE_BINDING_ID + || launch.role != crate::EPIPHANY_RESEARCH_OWNER_ROLE + { + return Err(anyhow!("public source evidence is Eyes-owned")); + } + let grant_id = format!("substrate-grant-{worker_job_id}"); + let grant = cache + .get::(&grant_id)? + .ok_or_else(|| anyhow!("public source worker lost its launch grant"))?; + if grant.runtime_job_id != worker_job_id + || grant.binding_id != launch.binding_id + || grant.role != launch.role + || grant.authority_scope != launch.authority_scope + || !grant + .granted_operations + .iter() + .any(|operation| operation == crate::substrate_gate::SUBSTRATE_GATE_PUBLIC_SOURCE_READ_OPERATION) + { + return Err(anyhow!("public source worker grant is not exact")); + } + let model_request_ids = cache + .get_all::()? + .into_iter() + .filter(|binding| binding.source_worker_job_id.as_deref() == Some(worker_job_id)) + .map(|binding| binding.request_id) + .collect::>(); + let mut lookups = Vec::new(); + for binding in cache.get_all::()? { + let Some(model_request_id) = binding.model_request_id.as_deref() else { + continue; + }; + if !model_request_ids.contains(model_request_id) { + continue; + } + let intent = cache + .get::(&tool_invocation_intent_key( + &binding.intent_id, + ))? + .ok_or_else(|| anyhow!("public source tool binding lost its intent"))?; + if intent.server != "epiphany_public" || intent.tool_name != "github_file" { + continue; + } + let receipt = cache + .get::(&tool_invocation_receipt_key( + &intent.intent_id, + ))? + .ok_or_else(|| anyhow!("public source tool intent has no terminal receipt"))?; + let _ = validate_governed_source_tool_intent(&cache, &binding.job_id, &intent)?; + if binding.intent_id != intent.intent_id + || intent.model_request_id.as_deref() != Some(model_request_id) + || receipt.intent_id != intent.intent_id + || receipt.adapter != intent.adapter + || receipt.server != intent.server + || receipt.tool_name != intent.tool_name + || receipt.status != "completed" + || chrono::DateTime::parse_from_rfc3339(&receipt.completed_at).is_err() + { + return Err(anyhow!("public source tool family is not exact and terminal")); + } + let arguments: serde_json::Value = serde_json::from_str(&intent.arguments_json) + .context("public source intent arguments are invalid")?; + let result: serde_json::Value = serde_json::from_str( + receipt + .result_json + .as_deref() + .ok_or_else(|| anyhow!("public source receipt has no result"))?, + ) + .context("public source receipt result is invalid")?; + let owner = public_source_json_string(&arguments, "owner")?; + let repository_name = public_source_json_string(&arguments, "repository")?; + let revision = public_source_json_string(&arguments, "revision")?.to_ascii_lowercase(); + let path = public_source_json_string(&arguments, "path")?; + let content = public_source_json_string(&result, "content")?; + let repository = format!("github://{owner}/{repository_name}"); + let source_ref = format!("{repository}@{revision}/{path}"); + let content_sha256 = format!("{:x}", Sha256::digest(content.as_bytes())); + let byte_count = content.len() as u64; + let lookup_receipt_id = format!("eyes-source-{}", intent.intent_id); + if public_source_json_string(&result, "provider")? != "github" + || public_source_json_string(&result, "repository")? != repository + || public_source_json_string(&result, "revision")? != revision + || public_source_json_string(&result, "path")? != path + || public_source_json_string(&result, "sourceRef")? != source_ref + || public_source_json_string(&result, "contentSha256")? != content_sha256 + || result.get("byteCount").and_then(serde_json::Value::as_u64) + != Some(byte_count) + || public_source_json_string(&result, "evidenceReceiptId")? != lookup_receipt_id + { + return Err(anyhow!("public source result provenance is substituted")); + } + lookups.push(EyesSourceLookupReceipt { + schema_version: crate::EYES_SOURCE_LOOKUP_RECEIPT_SCHEMA_VERSION.to_string(), + receipt_id: lookup_receipt_id, + source_job_id: worker_job_id.to_string(), + substrate_grant_receipt_id: grant_id.clone(), + tool_intent_id: intent.intent_id, + tool_receipt_id: receipt.receipt_id, + provider: "github".to_string(), + repository, + revision, + path: path.to_string(), + source_ref, + content_sha256, + byte_count, + observed_at: receipt.completed_at, + contract: "Eyes admitted one bounded immutable public source only after authenticating its worker, model, tool, grant, provider identity, and content digest.".to_string(), + }); + } + lookups.sort_by(|left, right| left.receipt_id.cmp(&right.receipt_id)); + Ok(lookups) +} + +fn public_source_json_string<'a>(value: &'a serde_json::Value, field: &str) -> Result<&'a str> { + value + .get(field) + .and_then(serde_json::Value::as_str) + .filter(|value| !value.is_empty()) + .ok_or_else(|| anyhow!("public source cargo omitted {field:?}")) +} + +fn validate_governed_source_tool_intent( + cache: &CultCache, + job_id: &str, + intent: &EpiphanyToolInvocationIntent, +) -> Result> { + let required_operation = + crate::substrate_gate_operation_for_governed_tool(&intent.server, &intent.tool_name); + let source_worker_job_id = match intent.model_request_id.as_deref() { + Some(model_request_id) => { + let model_binding = cache + .get::(model_request_id)? + .ok_or_else(|| { + anyhow!( + "model-derived tool intent {:?} has no execution binding", + intent.intent_id + ) + })?; + match model_binding.source_worker_job_id { + Some(worker_job_id) => Some(worker_job_id), + None if required_operation.is_some() => { + return Err(anyhow!( + "governed source tool intent {:?} has no source worker authority", + intent.intent_id + )); + } + None => None, + } + } + None => { + if cache + .get::(job_id)? + .is_some_and(|launch| { + matches!( + launch.binding_id.as_str(), + crate::EPIPHANY_RESEARCH_ROLE_BINDING_ID + | crate::EPIPHANY_MODELING_ROLE_BINDING_ID + | crate::EPIPHANY_VERIFICATION_ROLE_BINDING_ID + ) + }) + { + return Err(anyhow!( + "governed source worker refuses a tool intent without model-call provenance" + )); + } + None + } + }; + let Some(source_worker_job_id) = source_worker_job_id else { + return Ok(Vec::new()); + }; + let launch = cache + .get::(&source_worker_job_id)? + .ok_or_else(|| anyhow!("governed source tool intent lost its source worker launch"))?; + let source_worker_job = cache + .get::(&source_worker_job_id)? + .ok_or_else(|| anyhow!("governed source tool intent lost its source worker job"))?; + if !matches!( + launch.binding_id.as_str(), + crate::EPIPHANY_RESEARCH_ROLE_BINDING_ID + | crate::EPIPHANY_MODELING_ROLE_BINDING_ID + | crate::EPIPHANY_VERIFICATION_ROLE_BINDING_ID + ) { + return Ok(Vec::new()); + } + let required_operation = required_operation.ok_or_else(|| { + anyhow!( + "governed source worker rejected unknown tool {}::{}", + intent.server, + intent.tool_name + ) + })?; + let grant_id = format!("substrate-grant-{source_worker_job_id}"); + let grant = cache + .get::(&grant_id)? + .ok_or_else(|| { + anyhow!( + "governed source tool intent {:?} has no exact launch grant", + intent.intent_id + ) + })?; + if source_worker_job.role != launch.role + || matches!( + source_worker_job.status, + EpiphanyRuntimeJobStatus::Completed + | EpiphanyRuntimeJobStatus::Failed + | EpiphanyRuntimeJobStatus::Cancelled + ) + || grant.runtime_job_id != source_worker_job_id + || grant.binding_id != launch.binding_id + || grant.role != launch.role + || grant.authority_scope != launch.authority_scope + || !grant + .granted_operations + .iter() + .any(|operation| operation == required_operation) + { + return Err(anyhow!( + "governed source tool intent {:?} is outside its launch grant", + intent.intent_id + )); + } + Ok(vec![ + cache + .get_envelope::(&source_worker_job_id)? + .ok_or_else(|| anyhow!("governed source tool lost worker job envelope"))?, + cache + .get_envelope::(&source_worker_job_id)? + .ok_or_else(|| anyhow!("governed source tool lost worker launch envelope"))?, + cache + .get_envelope::(&grant_id)? + .ok_or_else(|| anyhow!("governed source tool lost launch grant envelope"))?, + ]) +} + pub fn runtime_worker_process_claims( store_path: impl AsRef, ) -> Result> { @@ -16047,6 +16363,7 @@ pub(crate) mod tests { evidence_ids: vec![format!("evidence-{suffix}")], observation_ids: vec![format!("observation-{suffix}")], source_refs: vec!["epiphany-core/src/runtime_spine.rs:1".into()], + source_lookup_receipt_ids: Vec::new(), summary: "The exact claim is challenged by inspected source.".into(), uncertainty: "Bounded to the cited source.".into(), emitted_at: "2026-07-13T04:01:00Z".into(), @@ -19160,6 +19477,202 @@ pub(crate) mod tests { Ok(()) } + #[test] + fn governed_public_tool_intent_requires_exact_worker_launch_grant() -> Result<()> { + let temp = tempdir()?; + let store = temp.path().join("runtime.msgpack"); + initialize_runtime_spine( + &store, + RuntimeSpineInitOptions { + runtime_id: "epiphany-public-tool-test".to_string(), + display_name: "Epiphany Public Tool Test".to_string(), + created_at: "2026-08-11T00:00:00Z".to_string(), + }, + )?; + let state = crate::EpiphanyThreadState::default(); + let launch = crate::build_epiphany_role_launch_request( + "thread-public-tool", + crate::EpiphanyRoleResultRoleId::Research, + Some(state.revision), + Some(60), + &state, + ) + .map_err(anyhow::Error::msg)?; + let plan = crate::plan_coordinator_job_launch( + &state, + &launch, + &store, + "launcher-public-tool".to_string(), + "worker-public-tool".to_string(), + )?; + crate::commit_coordinator_job_launch( + &store, + "thread-public-tool", + &state, + &launch, + &plan, + "2026-08-11T00:00:01Z".to_string(), + )?; + + let mut model_request = EpiphanyModelRequest::new( + "request-public-tool", + "conversation-public-tool", + "openai-codex", + "gpt-test", + "Read one immutable public source.", + ); + model_request.source_worker_job_id = Some("worker-public-tool".to_string()); + let provider_request = EpiphanyOpenAiModelRequest::new( + "request-public-tool", + "conversation-public-tool", + "gpt-test", + "Read one immutable public source.", + ); + open_runtime_model_execution( + &store, + RuntimeSpineSessionOptions { + session_id: "model-session-public-tool".to_string(), + objective: "Read one immutable public source.".to_string(), + created_at: "2026-08-11T00:00:02Z".to_string(), + coordinator_note: "test".to_string(), + }, + RuntimeSpineJobOptions { + job_id: "model-job-public-tool".to_string(), + session_id: "model-session-public-tool".to_string(), + role: "openai-model-adapter".to_string(), + created_at: "2026-08-11T00:00:02Z".to_string(), + summary: "Bound public source model turn.".to_string(), + artifact_refs: Vec::new(), + }, + &model_request, + &provider_request, + "2026-08-11T00:00:02Z", + )?; + let exact = EpiphanyToolInvocationIntent::new( + "intent-public-tool-exact", + "epiphany-tools", + "epiphany_public", + "github_file", + r#"{"owner":"openai","repository":"openai-openapi","revision":"0123456789abcdef0123456789abcdef01234567","path":"openapi.yaml"}"#, + "model-runtime", + "Read immutable source.", + "2026-08-11T00:00:03Z", + ) + .with_model_call("call-public-tool-exact", "request-public-tool"); + put_runtime_tool_execution_intent( + &store, + "model-session-public-tool", + "model-job-public-tool", + &exact, + "2026-08-11T00:00:03Z", + )?; + let content = "openapi: 3.1.0\n"; + let content_sha256 = format!("{:x}", Sha256::digest(content.as_bytes())); + let mut exact_receipt = EpiphanyToolInvocationReceipt::new( + "receipt-public-tool-exact", + &exact.intent_id, + &exact.adapter, + &exact.server, + &exact.tool_name, + "completed", + "2026-08-11T00:00:04Z", + ); + exact_receipt.result_json = Some( + serde_json::json!({ + "provider": "github", + "repository": "github://openai/openai-openapi", + "revision": "0123456789abcdef0123456789abcdef01234567", + "path": "openapi.yaml", + "sourceRef": "github://openai/openai-openapi@0123456789abcdef0123456789abcdef01234567/openapi.yaml", + "contentSha256": content_sha256, + "byteCount": content.len(), + "content": content, + "evidenceReceiptId": "eyes-source-intent-public-tool-exact" + }) + .to_string(), + ); + put_runtime_tool_execution_receipt(&store, &exact_receipt)?; + let lookups = runtime_authenticated_public_source_lookups_for_worker( + &store, + "worker-public-tool", + )?; + assert_eq!(lookups.len(), 1); + assert_eq!( + lookups[0].receipt_id, + "eyes-source-intent-public-tool-exact" + ); + assert_eq!(lookups[0].content_sha256, content_sha256); + + let mut cache = runtime_spine_cache(&store)?; + cache.pull_all_backing_stores()?; + let receipt_key = tool_invocation_receipt_key(&exact.intent_id); + let mut substituted_receipt = cache + .get::(&receipt_key)? + .expect("exact public source receipt"); + let mut substituted_result: serde_json::Value = serde_json::from_str( + substituted_receipt.result_json.as_deref().unwrap(), + )?; + substituted_result["byteCount"] = serde_json::json!(1); + substituted_receipt.result_json = Some(substituted_result.to_string()); + cache.put(&receipt_key, &substituted_receipt)?; + let before_refusal = runtime_spine_backing_store(&store)?.pull_all()?; + assert!( + runtime_authenticated_public_source_lookups_for_worker( + &store, + "worker-public-tool" + ) + .is_err() + ); + assert_eq!(runtime_spine_backing_store(&store)?.pull_all()?, before_refusal); + cache.put(&receipt_key, &exact_receipt)?; + let grant_id = "substrate-grant-worker-public-tool"; + let mut grant = cache + .get::(grant_id)? + .expect("exact worker launch grant"); + grant + .granted_operations + .retain(|operation| operation != "publicSourceRead"); + cache.put(grant_id, &grant)?; + let hostile = EpiphanyToolInvocationIntent::new( + "intent-public-tool-hostile", + "epiphany-tools", + "epiphany_public", + "github_file", + "{}", + "model-runtime", + "Attempt public read without operation authority.", + "2026-08-11T00:00:04Z", + ) + .with_model_call("call-public-tool-hostile", "request-public-tool"); + let before = runtime_spine_backing_store(&store)?.pull_all()?; + assert!( + put_runtime_tool_execution_intent( + &store, + "model-session-public-tool", + "model-job-public-tool", + &hostile, + "2026-08-11T00:00:04Z", + ) + .is_err() + ); + assert_eq!(runtime_spine_backing_store(&store)?.pull_all()?, before); + let mut reloaded = runtime_spine_cache(&store)?; + reloaded.pull_all_backing_stores()?; + assert!( + reloaded + .get::(&hostile.intent_id)? + .is_none() + ); + assert!( + reloaded + .get::(&tool_invocation_intent_key( + &hostile.intent_id, + ))? + .is_none() + ); + Ok(()) + } + #[test] fn archived_model_session_is_exact_terminal_tombstone() -> Result<()> { let temp = tempdir()?; @@ -19599,6 +20112,7 @@ pub(crate) mod tests { evidence_ids: vec!["ev-1".to_string()], observation_ids: vec!["obs-1".to_string()], source_refs: vec!["src/lib.rs:1".to_string()], + source_lookup_receipt_ids: Vec::new(), summary: "Evidence packet.".to_string(), uncertainty: "none declared".to_string(), emitted_at: "2026-05-06T00:05:00Z".to_string(), diff --git a/epiphany-core/src/substrate_gate.rs b/epiphany-core/src/substrate_gate.rs index b719d356..63ece32e 100644 --- a/epiphany-core/src/substrate_gate.rs +++ b/epiphany-core/src/substrate_gate.rs @@ -27,6 +27,8 @@ pub const SUBSTRATE_GATE_REPO_SNAPSHOT_RECEIPT_SCHEMA_VERSION: &str = "epiphany.substrate_gate.repo_snapshot_receipt.v0"; pub const SUBSTRATE_GATE_REPO_MUTATION_RECEIPT_SCHEMA_VERSION: &str = "epiphany.substrate_gate.repo_mutation_receipt.v0"; +pub const SUBSTRATE_GATE_SOURCE_READ_OPERATION: &str = "read"; +pub const SUBSTRATE_GATE_PUBLIC_SOURCE_READ_OPERATION: &str = "publicSourceRead"; #[derive(Debug, Clone, PartialEq, Eq, DatabaseEntry)] #[cultcache( @@ -143,6 +145,13 @@ pub fn substrate_gate_repo_access_grant_for_launch( request: &EpiphanyJobLaunchRequest, granted_at: String, ) -> SubstrateGateRepoAccessGrantReceipt { + let mut granted_operations = vec![ + SUBSTRATE_GATE_SOURCE_READ_OPERATION.to_string(), + "snapshot".to_string(), + ]; + if request.binding_id == crate::EPIPHANY_RESEARCH_ROLE_BINDING_ID { + granted_operations.push(SUBSTRATE_GATE_PUBLIC_SOURCE_READ_OPERATION.to_string()); + } SubstrateGateRepoAccessGrantReceipt { schema_version: SUBSTRATE_GATE_REPO_ACCESS_GRANT_RECEIPT_SCHEMA_VERSION.to_string(), receipt_id, @@ -150,10 +159,29 @@ pub fn substrate_gate_repo_access_grant_for_launch( binding_id: request.binding_id.clone(), role: request.owner_role.clone(), authority_scope: request.authority_scope.clone(), - granted_operations: vec!["read".to_string(), "snapshot".to_string()], + granted_operations, granted_paths: vec![".".to_string()], granted_at, - contract: "Substrate Gate granted scoped repository read/snapshot access for this worker launch; mutation remains forbidden without a separate mutation receipt.".to_string(), + contract: "Substrate Gate granted the exact worker launch its named governed source operations; public-source reads are Eyes-only and mutation remains forbidden without a separate mutation receipt.".to_string(), + } +} + +pub fn substrate_gate_operation_for_governed_tool( + server: &str, + tool_name: &str, +) -> Option<&'static str> { + match (server, tool_name) { + ( + "epiphany_source", + "read_file" | "directory_inventory" | "git_show" | "read_hands_receipt", + ) + | ("epiphany_state", "resident_grant_lifecycle") => { + Some(SUBSTRATE_GATE_SOURCE_READ_OPERATION) + } + ("epiphany_public", "github_file") => { + Some(SUBSTRATE_GATE_PUBLIC_SOURCE_READ_OPERATION) + } + _ => None, } } @@ -294,10 +322,31 @@ mod tests { "2026-05-30T00:00:00Z".to_string(), ); assert!(grant.granted_operations.contains(&"read".to_string())); + assert!( + grant + .granted_operations + .contains(&"publicSourceRead".to_string()) + ); assert!(!grant.granted_operations.contains(&"write".to_string())); assert!(grant.contract.contains("mutation remains forbidden")); } + #[test] + fn governed_tools_map_to_fixed_grant_operations() { + assert_eq!( + substrate_gate_operation_for_governed_tool("epiphany_source", "read_file"), + Some("read") + ); + assert_eq!( + substrate_gate_operation_for_governed_tool("epiphany_public", "github_file"), + Some("publicSourceRead") + ); + assert_eq!( + substrate_gate_operation_for_governed_tool("epiphany_public", "arbitrary_url"), + None + ); + } + #[test] fn repo_work_and_implementation_grants_have_fixed_policies() { let planning = substrate_gate_repo_work_planning_grant( diff --git a/epiphany-model-adapter/src/lib.rs b/epiphany-model-adapter/src/lib.rs index 2e367a32..7b23b704 100644 --- a/epiphany-model-adapter/src/lib.rs +++ b/epiphany-model-adapter/src/lib.rs @@ -58,6 +58,8 @@ pub struct EpiphanyModelRequest { pub tools: Vec, #[cultcache(key = 13, default)] pub output_schema_json: Option, + #[cultcache(key = 14, default)] + pub source_worker_job_id: Option, } impl EpiphanyModelRequest { @@ -83,6 +85,7 @@ impl EpiphanyModelRequest { previous_response_id: None, tools: Vec::new(), output_schema_json: None, + source_worker_job_id: None, } } } diff --git a/epiphany-openai-runtime/src/lib.rs b/epiphany-openai-runtime/src/lib.rs index d7db336f..85e9e9cd 100644 --- a/epiphany-openai-runtime/src/lib.rs +++ b/epiphany-openai-runtime/src/lib.rs @@ -595,7 +595,7 @@ pub fn build_worker_model_request( if launch_request.binding_id == epiphany_core::EPIPHANY_VERIFICATION_ROLE_BINDING_ID { instructions.push_str("\n\nTool mandate: before returning `needs-evidence` because source files, artifact directories, command artifacts, commit diffs, Hands receipt bodies, or resident grant lifecycle are not inspectable, call the governed read-only tools available on this request. Use `mcp__epiphany_source__read_file` for cited source/artifact files, `mcp__epiphany_source__directory_inventory` for bounded workspace directory counts and bytes, `mcp__epiphany_source__git_show` for commit diffs, `mcp__epiphany_source__read_hands_receipt` for Hands patch/command/commit receipts, and `mcp__epiphany_state__resident_grant_lifecycle` for exact or bounded recent grant-owned lifecycle state. Directory totals are authoritative only when the tool reports `complete=true`. Grant launchability is authoritative only from the typed state projection, never artifact names or acknowledgement presence. If a tool fails, cite that failed tool result and the exact remaining blocker."); } else if launch_request.binding_id == epiphany_core::EPIPHANY_RESEARCH_ROLE_BINDING_ID { - instructions.push_str("\n\nTool mandate: Eyes must inspect current repository sources or typed resident state before emitting evidence. Call the bounded read-only file, directory-inventory, Git, or `mcp__epiphany_state__resident_grant_lifecycle` tool appropriate to the claim; cite exact inspected paths/revisions or grant identities in filesInspected and evidence, and report a source gap if the required body cannot be observed. Directory totals are authoritative only when the inventory reports `complete=true`; grant launchability is authoritative only from the typed lifecycle projection."); + instructions.push_str("\n\nTool mandate: Eyes must inspect current repository sources, immutable public GitHub source, or typed resident state before emitting evidence. Call the bounded governed tool appropriate to the claim. Public evidence uses `mcp__epiphany_public__github_file` with an exact 40-hex commit, repository, and path; put its exact sourceRef in filesInspected and its evidenceReceiptId in evidence, and preserve its contentSha256 in the finding. Never substitute a branch, tag, arbitrary URL, or model memory for immutable public evidence. Cite exact inspected paths/revisions or grant identities in filesInspected and evidence, and report a source gap if the required body cannot be observed. Directory totals are authoritative only when the inventory reports `complete=true`; grant launchability is authoritative only from the typed lifecycle projection."); } else if launch_request.binding_id == epiphany_core::EPIPHANY_MODELING_ROLE_BINDING_ID { instructions.push_str("\n\nTool mandate: Modeling must inspect current repository sources or typed resident state before proposing repository anatomy. Call the bounded read-only file, directory-inventory, Git, or `mcp__epiphany_state__resident_grant_lifecycle` tool appropriate to the claim; cite exact inspected paths/revisions or grant identities in filesInspected and evidence, and emit regather-needed instead of inventing unobserved structure. Directory totals are authoritative only when the inventory reports `complete=true`; grant launchability is authoritative only from the typed lifecycle projection."); } @@ -615,13 +615,16 @@ pub fn build_worker_model_request( request.reasoning_summary = Some("concise".to_string()); request.output_contract_id = Some(launch_request.output_contract_id.clone()); request.output_schema_json = Some(output_schema_json); + request.source_worker_job_id = Some(launch_request.job_id.clone()); if matches!( launch_request.binding_id.as_str(), epiphany_core::EPIPHANY_RESEARCH_ROLE_BINDING_ID | epiphany_core::EPIPHANY_MODELING_ROLE_BINDING_ID | epiphany_core::EPIPHANY_VERIFICATION_ROLE_BINDING_ID ) { - request.tools = repository_source_tools(); + request.tools = repository_source_tools( + launch_request.binding_id == epiphany_core::EPIPHANY_RESEARCH_ROLE_BINDING_ID, + ); } Ok(request) } @@ -1047,6 +1050,7 @@ pub fn model_request_from_openai_request( output_contract_id: request.output_contract_id.clone(), previous_response_id: request.previous_response_id.clone(), output_schema_json: request.output_schema_json.clone(), + source_worker_job_id: None, tools: request .tools .iter() @@ -1091,8 +1095,8 @@ pub fn openai_request_from_model_request( } } -fn repository_source_tools() -> Vec { - vec![ +fn repository_source_tools(include_public_source: bool) -> Vec { + let mut tools = vec![ EpiphanyModelToolDefinition { name: "mcp__epiphany_source__read_file".to_string(), description: "Read a bounded UTF-8 text slice from the current workspace for source-grounded Eyes, Modeling, or Soul work. Use only for repository sources and operator-safe artifacts in scope.".to_string(), @@ -1166,7 +1170,27 @@ fn repository_source_tools() -> Vec { }) .to_string(), }, - ] + ]; + if include_public_source { + tools.push(EpiphanyModelToolDefinition { + name: "mcp__epiphany_public__github_file".to_string(), + description: "Read one bounded UTF-8 file from a public GitHub repository at an exact immutable 40-hex commit. Returns provider/repository/revision/path/sourceRef/contentSha256 provenance. Branches, tags, redirects, arbitrary hosts, and mutable URLs are refused.".to_string(), + parameters_json: serde_json::json!({ + "type": "object", + "additionalProperties": false, + "properties": { + "owner": {"type": "string"}, + "repository": {"type": "string"}, + "revision": {"type": "string", "pattern": "^[0-9a-fA-F]{40}$"}, + "path": {"type": "string"}, + "maxBytes": {"type": "integer", "minimum": 512, "maximum": 65536} + }, + "required": ["owner", "repository", "revision", "path"] + }) + .to_string(), + }); + } + tools } fn model_input_from_openai_input(input: &EpiphanyOpenAiInputItem) -> EpiphanyModelInputItem { @@ -3186,6 +3210,10 @@ mod tests { .tools .iter() .any(|tool| tool.name == "mcp__epiphany_state__resident_grant_lifecycle")); + assert!(!model_request + .tools + .iter() + .any(|tool| tool.name == "mcp__epiphany_public__github_file")); assert!(model_request.instructions.contains("Modeling must inspect")); let openai_summary = EpiphanyOpenAiRuntimeRunSummary { store: store.display().to_string(), @@ -3338,6 +3366,7 @@ mod tests { assert!(tool_names.contains(&"mcp__epiphany_source__git_show")); assert!(tool_names.contains(&"mcp__epiphany_source__read_hands_receipt")); assert!(tool_names.contains(&"mcp__epiphany_state__resident_grant_lifecycle")); + assert!(!tool_names.contains(&"mcp__epiphany_public__github_file")); assert!(model_request .instructions .contains("mcp__epiphany_source__read_file")); @@ -3347,6 +3376,21 @@ mod tests { assert!(model_request .instructions .contains("mcp__epiphany_state__resident_grant_lifecycle")); + + let mut research_launch = launch_request; + research_launch.binding_id = + epiphany_core::EPIPHANY_RESEARCH_ROLE_BINDING_ID.to_string(); + research_launch.role = epiphany_core::EPIPHANY_RESEARCH_OWNER_ROLE.to_string(); + research_launch.authority_scope = "epiphany.role.research".to_string(); + let research_request = + build_worker_model_request(&research_launch, DEFAULT_MODEL_PROVIDER, "gpt-5.4")?; + assert!(research_request + .tools + .iter() + .any(|tool| tool.name == "mcp__epiphany_public__github_file")); + assert!(research_request + .instructions + .contains("exact 40-hex commit")); Ok(()) } diff --git a/epiphany-tool-mcp-runtime/Cargo.toml b/epiphany-tool-mcp-runtime/Cargo.toml index c8c8f7ac..298250cc 100644 --- a/epiphany-tool-mcp-runtime/Cargo.toml +++ b/epiphany-tool-mcp-runtime/Cargo.toml @@ -12,6 +12,7 @@ epiphany-core = { path = "../epiphany-core" } cultcache-rs = { path = "../vendor/cultcache-rs" } http = "1" reqwest = { version = "0.13.2", default-features = false, features = ["json", "rustls"] } +sha2 = "0.10" rmcp = { version = "2.2.0", default-features = false, features = ["client", "transport-child-process", "transport-streamable-http-client-reqwest"] } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/epiphany-tool-mcp-runtime/src/lib.rs b/epiphany-tool-mcp-runtime/src/lib.rs index eead1f44..1fd3285d 100644 --- a/epiphany-tool-mcp-runtime/src/lib.rs +++ b/epiphany-tool-mcp-runtime/src/lib.rs @@ -4,9 +4,11 @@ //! endpoint policy and translates between MCP cargo and typed tool receipts. mod config; +mod native_public; mod native_source; pub use config::{McpRuntimeConfig, McpServerConfig, McpTransportConfig}; +pub use native_public::execute_epiphany_public; pub use native_source::execute_epiphany_source; use anyhow::{Context, Result, anyhow}; diff --git a/epiphany-tool-mcp-runtime/src/main.rs b/epiphany-tool-mcp-runtime/src/main.rs index 3b0268a4..4099efec 100644 --- a/epiphany-tool-mcp-runtime/src/main.rs +++ b/epiphany-tool-mcp-runtime/src/main.rs @@ -5,7 +5,8 @@ use epiphany_tool_adapter::{ TOOL_ADAPTER_INVOCATION_RECEIPT_SCHEMA_ID, tool_invocation_intent_key, }; use epiphany_tool_mcp_runtime::{ - McpRuntimeConfig, execute_epiphany_source, invoke, validate_intent, + McpRuntimeConfig, execute_epiphany_public, execute_epiphany_source, invoke, + validate_intent, }; use serde::Serialize; use std::collections::BTreeMap; @@ -95,6 +96,8 @@ async fn execute_to_receipt( None => std::env::current_dir()?, }; execute_epiphany_source(intent, &options.store, &cwd) + } else if intent.server == "epiphany_public" { + execute_epiphany_public(intent).await } else if intent.server == "epiphany_state" { let resident_store = options.resident_store.as_ref().ok_or_else(|| { anyhow!("epiphany_state requires an explicitly bound --resident-store") diff --git a/epiphany-tool-mcp-runtime/src/native_public.rs b/epiphany-tool-mcp-runtime/src/native_public.rs new file mode 100644 index 00000000..2275a79b --- /dev/null +++ b/epiphany-tool-mcp-runtime/src/native_public.rs @@ -0,0 +1,182 @@ +use anyhow::{Context, Result, anyhow}; +use epiphany_tool_adapter::EpiphanyToolInvocationIntent; +use reqwest::redirect::Policy; +use serde_json::{Value, json}; +use sha2::{Digest, Sha256}; +use std::time::Duration; + +const MAX_PUBLIC_SOURCE_BYTES: usize = 64 * 1024; + +pub async fn execute_epiphany_public(intent: &EpiphanyToolInvocationIntent) -> Result { + let arguments: Value = + serde_json::from_str(&intent.arguments_json).context("arguments_json is not valid JSON")?; + if !arguments.is_object() { + return Err(anyhow!("epiphany_public arguments must be an object")); + } + match intent.tool_name.as_str() { + "github_file" => github_file(intent, &arguments).await, + other => Err(anyhow!("unknown epiphany_public tool {other:?}")), + } +} + +async fn github_file(intent: &EpiphanyToolInvocationIntent, arguments: &Value) -> Result { + let owner = repository_component(arguments, "owner")?; + let repository = repository_component(arguments, "repository")?; + let revision = required_string(arguments, "revision")?; + if revision.len() != 40 || !revision.bytes().all(|byte| byte.is_ascii_hexdigit()) { + return Err(anyhow!( + "github_file revision must be an immutable 40-hex commit id" + )); + } + let path = required_string(arguments, "path")?; + validate_repository_path(path)?; + let maximum = arguments + .get("maxBytes") + .map(|value| { + value + .as_u64() + .ok_or_else(|| anyhow!("github_file maxBytes must be unsigned")) + }) + .transpose()? + .unwrap_or(32_768) + .clamp(512, MAX_PUBLIC_SOURCE_BYTES as u64) as usize; + let revision = revision.to_ascii_lowercase(); + let url = format!("https://raw.githubusercontent.com/{owner}/{repository}/{revision}/{path}"); + let client = reqwest::Client::builder() + .redirect(Policy::none()) + .no_proxy() + .timeout(Duration::from_secs(15)) + .user_agent("epiphany-public-source/0") + .build() + .context("building bounded public-source client")?; + let mut response = client + .get(&url) + .send() + .await + .context("reading immutable GitHub source")?; + if !response.status().is_success() { + return Err(anyhow!( + "immutable GitHub source returned HTTP {}", + response.status() + )); + } + if response + .content_length() + .is_some_and(|length| length > maximum as u64) + { + return Err(anyhow!("immutable GitHub source exceeds maxBytes")); + } + let mut bytes = Vec::with_capacity(maximum.min(8_192)); + while let Some(chunk) = response + .chunk() + .await + .context("reading immutable GitHub source body")? + { + if bytes.len() + chunk.len() > maximum { + return Err(anyhow!("immutable GitHub source exceeds maxBytes")); + } + bytes.extend_from_slice(&chunk); + } + let content = String::from_utf8(bytes.clone()) + .map_err(|_| anyhow!("immutable GitHub source is not UTF-8 text"))?; + let content_sha256 = format!("{:x}", Sha256::digest(&bytes)); + let evidence_receipt_id = format!("eyes-source-{}", intent.intent_id); + Ok(json!({ + "provider": "github", + "repository": format!("github://{owner}/{repository}"), + "revision": revision, + "path": path, + "sourceRef": format!("github://{owner}/{repository}@{revision}/{path}"), + "contentSha256": content_sha256, + "evidenceReceiptId": evidence_receipt_id, + "byteCount": bytes.len(), + "content": content, + })) +} + +fn repository_component<'a>(arguments: &'a Value, name: &str) -> Result<&'a str> { + let value = required_string(arguments, name)?; + if value.len() > 100 + || !value + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.')) + { + return Err(anyhow!("github_file {name} is invalid")); + } + Ok(value) +} + +fn validate_repository_path(path: &str) -> Result<()> { + if path.len() > 512 + || path.starts_with('/') + || path.split('/').any(|segment| { + segment.is_empty() + || matches!(segment, "." | "..") + || !segment + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.')) + }) + { + return Err(anyhow!("github_file path is invalid")); + } + Ok(()) +} + +fn required_string<'a>(value: &'a Value, name: &str) -> Result<&'a str> { + value + .get(name) + .and_then(Value::as_str) + .filter(|value| !value.trim().is_empty()) + .ok_or_else(|| anyhow!("missing required string argument {name:?}")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn public_source_identity_requires_immutable_github_shape() { + let valid = json!({ + "owner": "openai", + "repository": "openai-openapi", + "revision": "0123456789abcdef0123456789abcdef01234567", + "path": "openapi.yaml" + }); + assert_eq!(repository_component(&valid, "owner").unwrap(), "openai"); + assert!(validate_repository_path("docs/source_file.rs").is_ok()); + assert!(validate_repository_path("../secret").is_err()); + assert!(validate_repository_path("path with spaces").is_err()); + } + + #[tokio::test] + #[ignore = "live immutable GitHub source proof"] + async fn reads_exact_public_epiphany_source_with_digest_provenance() -> Result<()> { + let intent = EpiphanyToolInvocationIntent::new( + "live-public-source", + epiphany_tool_adapter::EPIPHANY_TOOL_RUNTIME_ADAPTER_ID, + "epiphany_public", + "github_file", + r#"{"owner":"GameCult","repository":"Epiphany","revision":"43dc865e0f332b82de4b95292d5c600f8b901706","path":"README.md","maxBytes":65536}"#, + "live-test", + "Prove exact public source access.", + "2026-08-11T00:00:00Z", + ); + let result = execute_epiphany_public(&intent).await?; + assert_eq!( + result["sourceRef"], + "github://GameCult/Epiphany@43dc865e0f332b82de4b95292d5c600f8b901706/README.md" + ); + assert_eq!(result["contentSha256"].as_str().map(str::len), Some(64)); + assert!(result["byteCount"].as_u64().is_some_and(|count| count > 0)); + assert!( + result["content"] + .as_str() + .is_some_and(|content| content.contains("Epiphany")) + ); + println!( + "sourceRef={} contentSha256={} byteCount={}", + result["sourceRef"], result["contentSha256"], result["byteCount"] + ); + Ok(()) + } +} diff --git a/notes/epiphany-current-algorithmic-map.md b/notes/epiphany-current-algorithmic-map.md index ea61f792..699fdb74 100644 --- a/notes/epiphany-current-algorithmic-map.md +++ b/notes/epiphany-current-algorithmic-map.md @@ -4327,6 +4327,49 @@ Soul binding, and split-admission replay. routing excludes manual regather. c010 remains alive, braked, and without an active lease after native Imagination produced no admitted work. +## Governed source-tool authority gap (2026-08-11) + +The transport body is broader than the admitted source faculty. The generic +MCP runtime can call configured subprocess or HTTP providers, while live Eyes, +Modeling, and Soul requests advertise only workspace-confined `read_file`, +`directory_inventory`, `git_show`, typed Hands-receipt reads, and the exact +resident-grant projection. No public-source tool is currently model-visible. + +- Owner: the immutable Substrate Gate grant created with an exact worker launch + must own which governed source operations that job may invoke. +- Inputs: exact runtime job, launch binding and role; exact tool server/name; + and a fixed tool-to-operation classification owned by the source boundary. +- Outputs: runtime admission accepts a model-derived tool intent only when the + exact launch grant names its required operation. Public evidence additionally + carries provider, repository, immutable revision, path, and content digest. +- Derived state: MCP configuration, model-visible tool definitions, tool-call + JSON, and transport reach are capability projections or protocol cargo. None + grants source access by existing. +- Forbidden writers: a model-selected tool name, generic MCP endpoint, prompt + instruction, runtime binding, or Eyes packet may not synthesize Substrate + authority. Arbitrary URLs and redirects are not public-source identity. +- Shared paths: Eyes, Modeling, and Soul must receive source tools only through + the same launch-grant and runtime-intent admission primitive. Local and public + source operations differ in provider policy but share the grant check. +- Cut line: publish an immutable grant for every launch that advertises source + tools; reject an intent before adapter/network execution when its exact grant + lacks the mapped operation. Then add one bounded provider-shaped public source + operation rather than exposing the generic MCP transport as evidence authority. +- Verification layer: focused grant/intent tests must refuse absent, foreign, + role-swapped, job-swapped, and operation-missing grants byte-identically. The + packaged Linux circuit must admit one immutable public source through Eyes and + Mind while an ungranted public request produces no network consequence. + +Current source enforces this map. Research, Modeling, and Verification launches +publish exact read grants; Research alone receives `publicSourceRead` and the +model-visible `epiphany_public.github_file` tool. Runtime admission follows the +inner model binding back to the outer typed worker launch and exact grant before +persisting an intent or executing an adapter. Public GitHub results become +`EyesSourceLookupReceipt` only after the full worker/model/tool/grant family and +the provider/repository/commit/path/content digest are reauthenticated inside +the same full-snapshot CAS that admits the Eyes packet into Mind. Generic MCP +transport and result JSON remain transport cargo, not evidence authority. + ## Resident PID 1 owns adopted worker reaping (2026-08-11) - Owner: resident Self owns OS cleanup for model-worker grandchildren that its diff --git a/notes/perfect-machine-audit-roadmap.md b/notes/perfect-machine-audit-roadmap.md index 18f1f103..7a991573 100644 --- a/notes/perfect-machine-audit-roadmap.md +++ b/notes/perfect-machine-audit-roadmap.md @@ -12,17 +12,18 @@ the actual braked resident body are the live gates. ## Current body - Source branch: `codex/epiphany-shakedown-live`. -- Current pushed source: exact `a07f6541`. -- Accepted live runtime implementation: exact `a07f6541` through resident - directive settlement; autonomous promotion remains failed closed. +- Current pushed source: exact `43dc865e`. +- Accepted live runtime implementation: exact `465af24d` through resident PID 1 + reaping across genuine Modeling and Imagination worker families. - Audited/live release: - `sha256-ba51cbac5fe2400c27497c5df92fe128ce083a29c45e9ccf5eb95d21ec807b26`; + `sha256-089e000511f0420bfaab1136952b8cf94677a72d5c30a3fb3e6a26996bd3726f`; witness - `sha256-01a6d5d74129ffb3096a3aa3f4ea7cd98d13af38b27d5322a8ab1d8f458d7a03`. -- Actual c008b: both owners stopped. The operator retry settled once at - revision 320; later autonomous turnover killed resident Self on a promotion - companion collision and left an active lease. Heartbeat stopped exit zero; - the exact cognitive brake is engaged. + `sha256-20ca293589b33d3b2e6ea5a7411db544741fe18360ecbd4423cdfa000d1ad7c6`. +- Actual c011: both owners are alive under the engaged cognitive brake at + resident revision 384, with no active lease, no admitted work, and zero + defunct children. Exact claimed worker PIDs 152 and 275 terminalized through + Modeling and Imagination respectively, settled once, and were reaped by the + resident PID 1 owner. - Build body: Starfire. Yggdrasil is the small public crossing and never builds this graph. @@ -31,8 +32,8 @@ the actual braked resident body are the live gates. | Faculty | Owner | Accepted proof | Remaining live gate | |---|---|---|---| | Self | resident Self state machine | single-consumption Heartbeat grants, typed fulfillment, exact recovery/requeue, cooldown and brake behavior; actual c007h superseded one overtaken unlaunched continuation and accepted the recovered proposal-Modeling result exactly once | autonomous turnover from a genuinely new typed frontier; current `awaitFrontierProposal` is correctly idle | -| Substrate Gate | `substrate_gate` | scoped read/mutation grants on Eyes and Hands paths; canonical brake covers direct daemon lifecycle actuation; non-repository process utilities retain explicit owners | one broader foreign/public Verse evidence adoption path with Eyes | -| Eyes | Eyes gateway and authenticated source tools | challenged-claim Windows circuit and governed Linux tool circuit accepted into Mind | one broader foreign/public Verse evidence adoption path | +| Substrate Gate | `substrate_gate` | immutable scoped grants now bind every advertised Eyes, Modeling, and Soul source operation through the outer worker launch; canonical brake covers direct daemon lifecycle actuation; non-repository process utilities retain explicit owners | packaged negative proof that an ungranted public request creates no network consequence | +| Eyes | Eyes gateway and authenticated source tools | challenged-claim Windows circuit, governed Linux tool circuit, and exact immutable GitHub reader with typed receipt derivation | package and adopt one broader public source through Eyes into Mind | | Modeling | runtime-owned typed Modeling contracts | causal Eyes-to-Modeling acceptance, direct proposal Modeling, archived fulfillment | broader Body-map coverage across a changing live repository | | Imagination | typed proposal/consideration producers | genuine packaged multi-option generation, proposal-only candidates, exact fulfillment, no Hands adoption leak | autonomous turnover from the next genuine eligible frontier | | Hands | Hands gateway | bounded patch, command, commit, push, and changed-path receipts; Idunn, crossing, forge, IDE, repo-birth, and model-runtime processes retain separate owners; Unity method override is structurally refused | audit the next Soul verifier boundary rather than adding Hands gates around non-Hands owners | @@ -41,7 +42,7 @@ the actual braked resident body are the live gates. | Persona | Persona turn, speech audit, public mouth | cross-host request/permit/receipt, signed failure and unknown no-repost behavior, conversation retention | one completed public consequence; currently blocked by absent Yggdrasil Discord credential | | Continuity | runtime/resident recovery and retention | graceful shutdown, exact process-death recovery, session closure/archive, lifecycle retention, non-resurrection, bounded endurance, and receipt-derived recovery on the current body | later deployed restart supervision | | Nervous system | Heartbeat, runtime spine, Idunn, CultMesh | detached scheduling, typed jobs, provider readiness, active/warming transitions across PID namespaces, bounded endurance, narrow local-Verse decision reads, bounded Docker logs, and causal grant turnover | audit non-repository actuator publication and command ownership | -| Body | Rust binaries, named-volume CultCache stores, model transport | deterministic 24-binary packaging, fast warm replay, named-volume interruption proof | deployment-sized endurance and remaining inspection surfaces | +| Body | Rust binaries, named-volume CultCache stores, model transport | deterministic 24-binary packaging, fast warm replay, named-volume interruption proof, and exact PID 1 reaping across two adopted worker kinds | deployment-sized endurance and remaining inspection surfaces | ## Accepted coordinated circuits @@ -118,11 +119,23 @@ these legitimate aggregates into ad hoc query piles. ## Selected next bounded cut -Keep c008b stopped. Inspect the partial autonomous proposal companion family, -map the promotion transaction owner, and prove a copied-state repair that is -idempotent without accepting substituted companions. Then recover the exact -missing active process through Continuity before restarting the resident pair. -Do not build on Yggdrasil. +Keep c011 alive and braked. Source now binds every governed model-derived tool +intent to its outer worker launch and exact Substrate Gate grant. Eyes alone can +see the bounded `github_file` provider, which accepts only one GitHub repository, +exact 40-hex commit, repository path, 64 KiB UTF-8 body, no redirects, and no +proxy. Runtime recomputes its identity and bytes before a typed +`EyesSourceLookupReceipt` can share the Mind acceptance transaction. Focused +hostile substitution is byte-identical and the exact upstream README reader is +already proven from Starfire. + +Package this source once on Starfire. On copied named-volume state, launch an +actual Research lane that cites the returned `sourceRef` and +`evidenceReceiptId`, accept it into Mind, and inspect the typed lookup plus Eyes +packet in the committed snapshot. Then exercise an otherwise exact worker whose +grant lacks `publicSourceRead`; require refusal before adapter/network execution +and byte-identical runtime state. Only after both gates pass may c011 be replaced. +Generic MCP transport remains transport and arbitrary URL fetching remains +absent. Do not build on Yggdrasil. ## Completion standard diff --git a/schemas/cultnet/epiphany.model-request.schema.json b/schemas/cultnet/epiphany.model-request.schema.json index ebd1a697..8474b826 100644 --- a/schemas/cultnet/epiphany.model-request.schema.json +++ b/schemas/cultnet/epiphany.model-request.schema.json @@ -18,12 +18,45 @@ "reasoning_effort": { "type": ["string", "null"] }, "reasoning_summary": { "type": ["string", "null"] }, "service_tier": { "type": ["string", "null"] }, - "output_contract_id": { "type": ["string", "null"] } + "output_contract_id": { "type": ["string", "null"] }, + "previous_response_id": { "type": ["string", "null"] }, + "tools": { + "type": "array", + "items": { + "type": "object", + "required": ["name", "description", "parameters_json"], + "properties": { + "name": { "type": "string" }, + "description": { "type": "string" }, + "parameters_json": { "type": "string" } + }, + "additionalProperties": false + } + }, + "output_schema_json": { "type": ["string", "null"] }, + "source_worker_job_id": { "type": ["string", "null"] } }, "additionalProperties": false, "$defs": { "input_item": { "oneOf": [ + { + "type": "object", + "required": ["ToolCall"], + "properties": { + "ToolCall": { + "type": "object", + "required": ["call_id", "name", "arguments"], + "properties": { + "call_id": { "type": "string" }, + "name": { "type": "string" }, + "arguments": { "type": "string" } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": ["UserText"], diff --git a/schemas/cultnet/epiphany.runtime.model-execution-binding.schema.json b/schemas/cultnet/epiphany.runtime.model-execution-binding.schema.json index c826a8fd..03067621 100644 --- a/schemas/cultnet/epiphany.runtime.model-execution-binding.schema.json +++ b/schemas/cultnet/epiphany.runtime.model-execution-binding.schema.json @@ -12,7 +12,8 @@ "sessionId": { "type": "string", "minLength": 1 }, "jobId": { "type": "string", "minLength": 1 }, "provider": { "type": "string", "minLength": 1 }, - "boundAt": { "type": "string", "format": "date-time" } + "boundAt": { "type": "string", "format": "date-time" }, + "sourceWorkerJobId": { "type": ["string", "null"] } }, "required": [ "schemaVersion",