Bind operator run receipts to result evidence
This commit is contained in:
parent
fe6d6df701
commit
2a797370f6
7 changed files with 95 additions and 16 deletions
|
|
@ -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(())
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -3379,7 +3379,6 @@ $operatorRunReceiptArgs = @(
|
|||
"--runtime-id", $LocalVerseRuntimeId,
|
||||
"--run-id", $runId,
|
||||
"--mode", $Mode,
|
||||
"--status", "completed",
|
||||
"--result-path", $resultPath,
|
||||
"--artifact-root", $artifactRoot,
|
||||
"--dogfood-root", $dogfoodRoot,
|
||||
|
|
|
|||
Reference in a new issue