Bind operator run receipts to result evidence

This commit is contained in:
Meta Vens 2026-07-12 20:33:30 +01:00
commit 2a797370f6
7 changed files with 95 additions and 16 deletions

View file

@ -1,11 +1,6 @@
use anyhow::Context;
use anyhow::Result;
use chrono::Utc;
use epiphany_core::EPIPHANY_CULTMESH_INTERNAL_VERSE_ID;
use epiphany_core::EPIPHANY_CULTMESH_OPERATOR_RUN_INTENT_SCHEMA_VERSION;
use epiphany_core::EPIPHANY_CULTMESH_OPERATOR_RUN_RECEIPT_SCHEMA_VERSION;
use epiphany_core::EpiphanyCultMeshOperatorRunIntentEntry;
use epiphany_core::EpiphanyCultMeshOperatorRunReceiptEntry;
use epiphany_core::epiphany_cultmesh_coordinator_run_receipt_from_summary_json;
use epiphany_core::epiphany_cultmesh_hands_action_gate_from_summary_json;
use epiphany_core::epiphany_cultmesh_role_review_event_from_summary_json;
@ -16,8 +11,13 @@ use epiphany_core::write_epiphany_cultmesh_hands_action_gate;
use epiphany_core::write_epiphany_cultmesh_operator_run_intent;
use epiphany_core::write_epiphany_cultmesh_operator_run_receipt;
use epiphany_core::write_epiphany_cultmesh_role_review_event;
use serde_json::Value;
use epiphany_core::EpiphanyCultMeshOperatorRunIntentEntry;
use epiphany_core::EpiphanyCultMeshOperatorRunReceiptEntry;
use epiphany_core::EPIPHANY_CULTMESH_INTERNAL_VERSE_ID;
use epiphany_core::EPIPHANY_CULTMESH_OPERATOR_RUN_INTENT_SCHEMA_VERSION;
use epiphany_core::EPIPHANY_CULTMESH_OPERATOR_RUN_RECEIPT_SCHEMA_VERSION;
use serde_json::json;
use serde_json::Value;
use std::env;
use std::fs;
use std::path::PathBuf;
@ -49,6 +49,51 @@ fn main() -> Result<()> {
print_json(json!({"status": "written", "store": args.store, "intent": written}))?;
}
"receipt" => {
let intent =
load_latest_epiphany_cultmesh_operator_run_intent(&args.store, &args.runtime_id)?
.context("operator-run receipt requires a persisted run intent")?;
if intent.run_id != args.run_id || intent.mode != args.mode {
anyhow::bail!(
"operator-run receipt does not match latest intent: requested run={} mode={}, latest run={} mode={}",
args.run_id,
args.mode,
intent.run_id,
intent.mode
);
}
let result_path = canonical_file(&args.result_path, "--result-path")?;
let artifact_root = canonical_directory(&args.artifact_root, "--artifact-root")?;
if !result_path.starts_with(&artifact_root) {
anyhow::bail!(
"operator-run result {} is outside artifact root {}",
result_path.display(),
artifact_root.display()
);
}
let result_source = fs::read_to_string(&result_path).with_context(|| {
format!("failed to read result artifact {}", result_path.display())
})?;
let _: Value = serde_json::from_str(result_source.trim_start_matches('\u{feff}'))
.with_context(|| {
format!(
"operator-run result is not valid JSON: {}",
result_path.display()
)
})?;
let requested_at = chrono::DateTime::parse_from_rfc3339(&intent.requested_at_utc)
.context("operator-run intent has invalid requested_at_utc")?
.with_timezone(&Utc);
let modified_at: chrono::DateTime<Utc> = fs::metadata(&result_path)?
.modified()
.context("operator-run result has no modification time")?
.into();
if modified_at < requested_at {
anyhow::bail!(
"operator-run result predates its intent: result={} intent={}",
modified_at.to_rfc3339(),
requested_at.to_rfc3339()
);
}
let mut artifact_refs = Vec::new();
push_non_empty(&mut artifact_refs, &args.result_path);
push_non_empty(&mut artifact_refs, &args.artifact_root);
@ -60,7 +105,7 @@ fn main() -> Result<()> {
run_id: args.run_id.clone(),
completed_at_utc: Utc::now().to_rfc3339(),
mode: args.mode.clone(),
status: args.status.clone(),
status: "completed".to_string(),
result_path: args.result_path.clone(),
artifact_root: args.artifact_root.clone(),
dogfood_root: args.dogfood_root.clone(),
@ -212,7 +257,6 @@ struct Args {
artifact_root: String,
dogfood_root: String,
result_path: String,
status: String,
operator_snapshot_store: String,
operator_snapshot_id: String,
coordinator_summary: Option<PathBuf>,
@ -241,7 +285,6 @@ impl Args {
artifact_root: String::new(),
dogfood_root: String::new(),
result_path: String::new(),
status: "completed".to_string(),
operator_snapshot_store: String::new(),
operator_snapshot_id: String::new(),
coordinator_summary: None,
@ -268,7 +311,6 @@ impl Args {
"--artifact-root" => args.artifact_root = next(&mut values, "--artifact-root")?,
"--dogfood-root" => args.dogfood_root = next(&mut values, "--dogfood-root")?,
"--result-path" => args.result_path = next(&mut values, "--result-path")?,
"--status" => args.status = next(&mut values, "--status")?,
"--operator-snapshot-store" => {
args.operator_snapshot_store = next(&mut values, "--operator-snapshot-store")?
}
@ -319,6 +361,30 @@ fn push_non_empty(items: &mut Vec<String>, value: &str) {
}
}
fn canonical_file(value: &str, name: &str) -> Result<PathBuf> {
if value.trim().is_empty() {
anyhow::bail!("operator-run receipt requires {name}");
}
let path =
fs::canonicalize(value).with_context(|| format!("{name} does not exist: {value}"))?;
if !path.is_file() {
anyhow::bail!("{name} is not a file: {}", path.display());
}
Ok(path)
}
fn canonical_directory(value: &str, name: &str) -> Result<PathBuf> {
if value.trim().is_empty() {
anyhow::bail!("operator-run receipt requires {name}");
}
let path =
fs::canonicalize(value).with_context(|| format!("{name} does not exist: {value}"))?;
if !path.is_dir() {
anyhow::bail!("{name} is not a directory: {}", path.display());
}
Ok(path)
}
fn print_json(value: Value) -> Result<()> {
println!("{}", serde_json::to_string_pretty(&value)?);
Ok(())

View file

@ -86,6 +86,13 @@
- Negative proof: targeting `state/local-verse.ccmp` was rejected and its SHA-256 did not change.
- Positive proof: the built-in quarantined smoke completes successfully and reports its quarantine coordinates.
## Operator-run completion receipt boundary (2026-07-12)
- Intent owner: operator orchestration writes run id, mode, roots, limits, and artifact coordinates before execution.
- Completion evidence: matching latest intent plus a valid JSON result inside the canonical artifact root, modified at or after intent creation.
- Receipt status is derived as `completed`; callers cannot submit status.
- Forbidden evidence: missing, non-JSON, pre-intent, out-of-root, or mismatched-run artifacts.
This is the source-grounded map of the live machine. Historical route and
bridge anatomy belongs in git history and evidence ledgers, not here.

View file

@ -40,6 +40,10 @@ The first call-site audit found a new canonical-promotion substitution in `epiph
The monolithic `epiphany-verse-query smoke` fixture writer can no longer accept `--store` or `--runtime-id`; it is hard-bound to `.epiphany-smoke/verse-query-default/local-verse.ccmp` and `verse-query-default-smoke`. A direct canonical-store attempt was rejected before mutation and `state/local-verse.ccmp` retained the same SHA-256. The quarantined smoke now explicitly seeds fixture daemon statuses, expects intent-only batch poke rows, and completes `status=ok` within quarantine. Next inspect operator-run/supervisor provenance and quarantine standalone smoke binaries that accept arbitrary roots.
## 2026-07-12 operator-run completion evidence binding
`epiphany-operator-run receipt` no longer accepts `--status`. It loads the latest operator intent and requires matching run id/mode, canonicalizes the result and artifact-root paths, requires the result to be a file contained under the artifact root, parses it as JSON, and rejects it if its modification time predates the intent. Only then does it derive `status=completed`. The wrapper no longer passes a completion assertion. Negative proof rejects caller status and out-of-root artifacts; positive proof uses a post-intent JSON artifact. Next audit daemon-supervisor planning/rehearsal receipts against actual service/process evidence.
This is the re-entry rite for `E:\Projects\EpiphanyAgent`: the waking chant for
the local machine-spirit before it touches the forge.

View file

@ -19,11 +19,14 @@ A forged operator-status artifact containing `intentId=forged-intent`, `receiptI
## Remaining scrutiny
1. Inspect `epiphany-operator-run` receipt construction against actual subprocess/runtime evidence rather than command arguments.
2. Inspect daemon-supervisor plan/rehearsal receipts separately from execution receipts so planning cannot masquerade as service mutation.
3. Extend the same store quarantine invariant to standalone smoke binaries that accept output paths.
4. Distill or remove dead public receipt constructors once every named provider executable exists.
1. Inspect daemon-supervisor plan/rehearsal receipts separately from execution receipts so planning cannot masquerade as service mutation.
2. Extend the same store quarantine invariant to standalone smoke binaries that accept output paths.
3. Distill or remove dead public receipt constructors once every named provider executable exists.
## Smoke quarantine update
`epiphany-verse-query smoke` is now hard-bound to `.epiphany-smoke/verse-query-default/local-verse.ccmp` and runtime `verse-query-default-smoke`. Store or runtime overrides are rejected before the fixture body runs. A direct attempt against `state/local-verse.ccmp` was refused and its SHA-256 remained unchanged. The quarantined smoke was repaired to seed fixture liveness explicitly and to expect requester-only poke output; it completes successfully inside the quarantine.
## Operator-run evidence binding update
The standalone `receipt` command previously accepted caller status and path strings, then wrote `completed` evidence without inspecting the run. It now requires the latest persisted intent to match run id and mode, derives status internally as `completed`, requires an existing valid JSON result contained by the canonical artifact root, and requires the result modification time to be no earlier than the intent request time. `--status` no longer exists. The PowerShell orchestrator supplies only the result coordinates after its checked subprocess completes.

Binary file not shown.

View file

@ -70,7 +70,7 @@ data_flow:
- move historical proof to git, commits, smoke artifacts, or distilled evidence rather than plan prose
active_subgoals:
- Use `notes/conceptual-substitution-audit-2026-07-12.md` plus `notes/receipt-writer-provenance-audit-2026-07-12.md` as the current authority-corrosion maps. Operator-snapshot cannot promote edge JSON into canonical tool receipts. `epiphany-verse-query smoke` is now hard-bound to its `.epiphany-smoke` store/runtime; overrides are refused before fixture writes, and canonical-store hash proof passed. Next audit operator-run and daemon-supervisor evidence provenance, then extend quarantine to standalone smoke binaries with configurable paths.
- Use `notes/conceptual-substitution-audit-2026-07-12.md` plus `notes/receipt-writer-provenance-audit-2026-07-12.md` as the current authority-corrosion maps. Operator-snapshot promotion is cut; verse-query smoke is quarantined. Operator-run completion now binds to a matching persisted intent and a fresh valid JSON result contained under the canonical artifact root; callers cannot supply completion status. Next audit daemon-supervisor plan/rehearsal versus execution provenance, then extend quarantine to standalone smoke binaries.
- `swarm-overview` is an Epiphany-local, renderer-neutral diagnostic projection over provider-owned typed state. It owns no lifecycle, discovery, composition, or mutation authority. Epiphany publishes clean provider-owned Eve surfaces; Odin may discover them; downstream lowerers and compositors remain outside Epiphany's concern.
- Idunn owns daemon survival and service lifecycle physiology. Codex/operator sessions may inspect, generate runbooks, and route explicit operator authority, but keeping daemons alive belongs to Idunn-owned service lifecycle receipts, scheduler policy, audits, and aftercare. Do not let Self, diagnostic projections, or ad hoc wrapper summaries become a second daemon-keeper. Global tool readbacks for Self `service-health`, `service-policy-directory`, and `swarm-online-runbook` state `lifecycleOwner=Idunn` and `hostedBody=Epiphany` without service mutation or private-state exposure.
- Use `notes/epiphany-cultmesh-dreaming-roadmap.md` as the design authority for distributed Epiphany dreaming. Public dreams are separately authored typed documents distributed through CultMesh/CultNet; private state remains local and cannot be shared by flipping a visibility flag. Foreign dreams are external thought weather until a reviewed local adoption receipt digests them into memory, planning, doctrine, or governance.

View file

@ -3379,7 +3379,6 @@ $operatorRunReceiptArgs = @(
"--runtime-id", $LocalVerseRuntimeId,
"--run-id", $runId,
"--mode", $Mode,
"--status", "completed",
"--result-path", $resultPath,
"--artifact-root", $artifactRoot,
"--dogfood-root", $dogfoodRoot,