diff --git a/notes/fresh-workspace-handoff.md b/notes/fresh-workspace-handoff.md index 4c5bc0b9..bccaf788 100644 --- a/notes/fresh-workspace-handoff.md +++ b/notes/fresh-workspace-handoff.md @@ -396,12 +396,16 @@ inventory and ledger now exist: `epiphany-core` owns `EpiphanyRoleStatePatchDocument`, policy checks read typed fields directly, and the app-server maps the typed document to the legacy `ThreadEpiphanyUpdatePatch` without JSON round-tripping. +- Fifth cut landed: `ThreadEpiphanyJobLaunchParams` no longer exposes + `input_json` / `output_schema_json`. The legacy app-server protocol launch + edge now carries a typed `ThreadEpiphanyWorkerLaunchDocument` and + `output_contract_id`, then maps that document into the native + `EpiphanyWorkerLaunchDocument` for core. -Continue at the protocol edge. Replace -`ThreadEpiphanyJobLaunchParams.input_json` / `output_schema_json` with CultNet -typed launch-intent messages, then seal MCP JSON behind a CultNet MCP adapter. -Do not resume Rider, Unity, Aquarium, Face, dogfood, planning, app, skill, -marketplace, or bridge expansion until this organ is being cut cleanly. +Continue by sealing MCP JSON behind a CultNet MCP adapter, then audit heartbeat +cognition quarantine values for typing or explicit expiration. Do not resume +Rider, Unity, Aquarium, Face, dogfood, planning, app, skill, marketplace, or +bridge expansion until this organ is being cut cleanly. The Phase 6 freshness slice is landed. It exposes read-only `thread/epiphany/freshness` from live retrieval summaries plus graph diff --git a/notes/json-contamination-ledger.md b/notes/json-contamination-ledger.md index 6d62b906..bbed94ca 100644 --- a/notes/json-contamination-ledger.md +++ b/notes/json-contamination-ledger.md @@ -33,11 +33,11 @@ Rule: JSON is allowed at external schema/wire boundaries. Inside Epiphany, live - Simpler architecture: role interpretation stores `AgentSelfPatch`, and only the protocol projection serializes JSON for the existing app-server surface. - Files: `epiphany-core/src/agent_memory.rs`, `epiphany-core/src/surfaces/role_result.rs`, `vendor/codex/codex-rs/app-server/src/codex_message_processor.rs`, `vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs`. - `vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs` - - Current mechanism: `ThreadEpiphanyRoleFinding.self_patch: Option`, `ThreadEpiphanyJobLaunchRequest.input_json`, and `output_schema_json`. + - Current mechanism: `ThreadEpiphanyRoleFinding.self_patch: Option` remains for the legacy projection. Launch params now use typed worker launch documents and `output_contract_id`. - Real need: app-server compatibility while Epiphany is still hosted inside Codex. - What breaks if deleted: frontend/server protocol compilation. - Essential or bad ownership: temporary compatibility reliquary, not a final contract. - - Simpler architecture: CultNet schema messages generated from typed document structs. + - Simpler architecture: CultNet schema messages generated from typed document structs; keep app-server protocol only as a temporary compatibility reliquary. ## Quarantine JSON @@ -61,5 +61,6 @@ Rule: JSON is allowed at external schema/wire boundaries. Inside Epiphany, live 2. Partly done: core worker launch `input_json` and `output_schema_json` are replaced by typed launch documents and output contract ids. Remaining cut is the protocol edge. 3. Done: replace role `statePatch` JSON with typed map/planning/graph patch documents. `EpiphanyRoleFindingInterpretation` now carries `EpiphanyRoleStatePatchDocument`; app-server maps it to the legacy protocol patch without JSON round-tripping. 4. Done: replace runtime job result JSON projections. Runtime-spine results now project through typed role/reorient interpreters instead of `runtime_job_result_to_role_json` and `runtime_job_result_to_reorient_json`. -5. Seal MCP JSON behind a CultNet MCP adapter. -6. Audit heartbeat cognition values and either type them or explicitly expire them. +5. Done: replace app-server launch protocol `input_json` / `output_schema_json` with typed launch documents and `output_contract_id`. +6. Seal MCP JSON behind a CultNet MCP adapter. +7. Audit heartbeat cognition values and either type them or explicitly expire them. diff --git a/state/ledgers.msgpack b/state/ledgers.msgpack index 7d85c681..dcc02a5c 100644 Binary files a/state/ledgers.msgpack and b/state/ledgers.msgpack differ diff --git a/state/map.yaml b/state/map.yaml index 5e16b647..6be95e76 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -278,5 +278,5 @@ rejected_paths: reason: making Epiphany cleaner inside Codex still preserves the wrong organ; Codex must be starved down to OpenAI subscription auth/model transport, while Epiphany data and subsystem communication move to typed CultCache/CultNet. current_status: - summary: The Codex starvation pass has four receipts. `notes/codex-auth-spine-inventory.md` maps the minimal vendored OpenAI auth/model-call spine; `notes/json-contamination-ledger.md` classifies the major Epiphany JSON uses by allowed edge, quarantine, or internal contamination. Role-result `selfPatch` and `statePatch` now use typed Epiphany documents internally. Core worker launch now uses `EpiphanyWorkerLaunchDocument` plus `output_contract_id` instead of `input_json` / `output_schema_json`. Runtime job results now project through typed role/reorient interpreters instead of JSON round-tripping. Legacy app-server JSON remains only at hostile ingress/protocol quarantine pending CultNet replacement. - next_action: Continue the liberation pass at the protocol edge: replace `ThreadEpiphanyJobLaunchParams.input_json` / `output_schema_json` with CultNet typed launch-intent messages, then seal MCP JSON behind a CultNet MCP adapter instead of letting Codex app/tool plumbing own it. + summary: The Codex starvation pass has five receipts. `notes/codex-auth-spine-inventory.md` maps the minimal vendored OpenAI auth/model-call spine; `notes/json-contamination-ledger.md` classifies the major Epiphany JSON uses by allowed edge, quarantine, or internal contamination. Role-result `selfPatch` and `statePatch` now use typed Epiphany documents internally. Core worker launch and app-server launch params now use typed worker launch documents plus `output_contract_id` instead of `input_json` / `output_schema_json`. Runtime job results now project through typed role/reorient interpreters instead of JSON round-tripping. Legacy app-server JSON remains in smaller compatibility pockets pending CultNet replacement. + next_action: Continue the liberation pass by sealing MCP JSON behind a CultNet MCP adapter instead of letting Codex app/tool plumbing own it, then audit heartbeat cognition quarantine values for typing or explicit expiration. diff --git a/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs b/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs index dab9b11e..99bf2ee4 100644 --- a/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/vendor/codex/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -5492,16 +5492,118 @@ pub struct ThreadEpiphanyJobLaunchParams { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub linked_graph_node_ids: Vec, pub instruction: String, - #[ts(type = "unknown")] - pub input_json: serde_json::Value, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional = nullable, type = "unknown | null")] - pub output_schema_json: Option, + pub launch_document: ThreadEpiphanyWorkerLaunchDocument, + pub output_contract_id: String, #[serde(default)] #[ts(optional = nullable)] pub max_runtime_seconds: Option, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase", tag = "documentKind")] +#[ts(export_to = "v2/")] +pub enum ThreadEpiphanyWorkerLaunchDocument { + Role(ThreadEpiphanyRoleWorkerLaunchDocument), + Reorient(ThreadEpiphanyReorientWorkerLaunchDocument), +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadEpiphanyRoleWorkerLaunchDocument { + pub thread_id: String, + pub role_id: String, + pub state_revision: u64, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub objective: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub active_subgoal_id: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub active_subgoals: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub active_graph_node_ids: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub investigation_checkpoint: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub scratch: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub invariants: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub graphs: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub recent_evidence: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub recent_observations: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub graph_frontier: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub graph_checkpoint: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub planning: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub churn: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ThreadEpiphanyReorientWorkerLaunchDocument { + pub thread_id: String, + pub mode: String, + pub checkpoint_id: String, + pub checkpoint_kind: String, + pub checkpoint_disposition: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub checkpoint_focus: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub checkpoint_summary: Option, + pub checkpoint_next_action: String, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub checkpoint_open_questions: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub checkpoint_evidence_ids: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub checkpoint_code_refs: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub decision_reasons: Vec, + pub decision_note: String, + pub pressure_level: String, + pub retrieval_status: String, + pub graph_status: String, + pub watcher_status: String, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub checkpoint_dirty_paths: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub checkpoint_changed_paths: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub scratch: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional = nullable)] + pub graphs: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub recent_evidence: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub recent_observations: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub active_frontier_node_ids: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub linked_subgoal_ids: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub linked_graph_node_ids: Vec, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] diff --git a/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs b/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs index 2e427373..042a695c 100644 --- a/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs +++ b/vendor/codex/codex-rs/app-server/src/codex_message_processor.rs @@ -207,6 +207,7 @@ use codex_app_server_protocol::ThreadEpiphanyReorientResultResponse; use codex_app_server_protocol::ThreadEpiphanyReorientResultStatus; use codex_app_server_protocol::ThreadEpiphanyReorientSource; use codex_app_server_protocol::ThreadEpiphanyReorientStateStatus; +use codex_app_server_protocol::ThreadEpiphanyReorientWorkerLaunchDocument; use codex_app_server_protocol::ThreadEpiphanyRetrievalFreshness; use codex_app_server_protocol::ThreadEpiphanyRetrievalFreshnessStatus; use codex_app_server_protocol::ThreadEpiphanyRetrieveIndexSummary; @@ -228,6 +229,7 @@ use codex_app_server_protocol::ThreadEpiphanyRoleResultStatus; use codex_app_server_protocol::ThreadEpiphanyRoleSelfPersistenceReview; use codex_app_server_protocol::ThreadEpiphanyRoleSelfPersistenceStatus; use codex_app_server_protocol::ThreadEpiphanyRoleStatus; +use codex_app_server_protocol::ThreadEpiphanyRoleWorkerLaunchDocument; use codex_app_server_protocol::ThreadEpiphanyRolesSource; use codex_app_server_protocol::ThreadEpiphanyScene; use codex_app_server_protocol::ThreadEpiphanySceneAction; @@ -255,6 +257,7 @@ use codex_app_server_protocol::ThreadEpiphanyViewPlanning; use codex_app_server_protocol::ThreadEpiphanyViewReorient; use codex_app_server_protocol::ThreadEpiphanyViewResponse; use codex_app_server_protocol::ThreadEpiphanyViewRoles; +use codex_app_server_protocol::ThreadEpiphanyWorkerLaunchDocument; use codex_app_server_protocol::ThreadForkParams; use codex_app_server_protocol::ThreadForkResponse; use codex_app_server_protocol::ThreadIncrementElicitationParams; @@ -6385,8 +6388,8 @@ impl CodexMessageProcessor { linked_subgoal_ids, linked_graph_node_ids, instruction, - input_json, - output_schema_json: _, + launch_document, + output_contract_id, max_runtime_seconds, } = params; @@ -6411,19 +6414,7 @@ impl CodexMessageProcessor { } }; - let launch_document = - match serde_json::from_value::(input_json) { - Ok(document) => document, - Err(err) => { - self.send_invalid_request_error( - request_id, - format!("invalid Epiphany job launch document: {err}"), - ) - .await; - return; - } - }; - let output_contract_id = launch_document.output_contract_id().to_string(); + let launch_document = map_core_worker_launch_document(launch_document); let changed_fields = epiphany_job_launch_changed_fields(); let launched = match thread .epiphany_launch_job(EpiphanyJobLaunchRequest { @@ -13412,6 +13403,78 @@ fn map_protocol_update_patch(patch: EpiphanyRoleStatePatchDocument) -> ThreadEpi } } +fn map_core_worker_launch_document( + document: ThreadEpiphanyWorkerLaunchDocument, +) -> EpiphanyWorkerLaunchDocument { + match document { + ThreadEpiphanyWorkerLaunchDocument::Role(document) => { + EpiphanyWorkerLaunchDocument::Role(map_core_role_worker_launch_document(document)) + } + ThreadEpiphanyWorkerLaunchDocument::Reorient(document) => { + EpiphanyWorkerLaunchDocument::Reorient(map_core_reorient_worker_launch_document( + document, + )) + } + } +} + +fn map_core_role_worker_launch_document( + document: ThreadEpiphanyRoleWorkerLaunchDocument, +) -> EpiphanyRoleWorkerLaunchDocument { + EpiphanyRoleWorkerLaunchDocument { + thread_id: document.thread_id, + role_id: document.role_id, + state_revision: document.state_revision, + objective: document.objective, + active_subgoal_id: document.active_subgoal_id, + active_subgoals: document.active_subgoals, + active_graph_node_ids: document.active_graph_node_ids, + investigation_checkpoint: document.investigation_checkpoint, + scratch: document.scratch, + invariants: document.invariants, + graphs: document.graphs, + recent_evidence: document.recent_evidence, + recent_observations: document.recent_observations, + graph_frontier: document.graph_frontier, + graph_checkpoint: document.graph_checkpoint, + planning: document.planning, + churn: document.churn, + } +} + +fn map_core_reorient_worker_launch_document( + document: ThreadEpiphanyReorientWorkerLaunchDocument, +) -> EpiphanyReorientWorkerLaunchDocument { + EpiphanyReorientWorkerLaunchDocument { + thread_id: document.thread_id, + mode: document.mode, + checkpoint_id: document.checkpoint_id, + checkpoint_kind: document.checkpoint_kind, + checkpoint_disposition: document.checkpoint_disposition, + checkpoint_focus: document.checkpoint_focus, + checkpoint_summary: document.checkpoint_summary, + checkpoint_next_action: document.checkpoint_next_action, + checkpoint_open_questions: document.checkpoint_open_questions, + checkpoint_evidence_ids: document.checkpoint_evidence_ids, + checkpoint_code_refs: document.checkpoint_code_refs, + decision_reasons: document.decision_reasons, + decision_note: document.decision_note, + pressure_level: document.pressure_level, + retrieval_status: document.retrieval_status, + graph_status: document.graph_status, + watcher_status: document.watcher_status, + checkpoint_dirty_paths: document.checkpoint_dirty_paths, + checkpoint_changed_paths: document.checkpoint_changed_paths, + scratch: document.scratch, + graphs: document.graphs, + recent_evidence: document.recent_evidence, + recent_observations: document.recent_observations, + active_frontier_node_ids: document.active_frontier_node_ids, + linked_subgoal_ids: document.linked_subgoal_ids, + linked_graph_node_ids: document.linked_graph_node_ids, + } +} + fn core_state_patch_from_protocol( patch: &ThreadEpiphanyUpdatePatch, ) -> EpiphanyRoleStatePatchDocument {