diff --git a/crates/khive-pack-brain/src/handlers.rs b/crates/khive-pack-brain/src/handlers.rs index 1661008c8..076e9bd1c 100644 --- a/crates/khive-pack-brain/src/handlers.rs +++ b/crates/khive-pack-brain/src/handlers.rs @@ -2383,7 +2383,7 @@ impl BrainPack { let p: MarkTurnParams = serde_json::from_value(params) .map_err(|e| RuntimeError::InvalidInput(e.to_string()))?; if let Some(label) = p.label.as_deref() { - khive_runtime::secret_gate::check(label)?; + khive_runtime::secret_gate::check_at(label, "turn", "label")?; } let phase = p.label.unwrap_or_else(|| "actor_turn".to_string()); let actor = format!("{}:{}", token.actor().kind, token.actor().id); @@ -2459,13 +2459,13 @@ impl BrainPack { // Secret gate: scan arbitrary text fields before writing. // Wildcard sentinel `*` is safe; real values are scanned. if actor != "*" { - khive_runtime::secret_gate::check(&actor)?; + khive_runtime::secret_gate::check_at(&actor, "binding", "actor")?; } if namespace != "*" { - khive_runtime::secret_gate::check(&namespace)?; + khive_runtime::secret_gate::check_at(&namespace, "binding", "namespace")?; } if consumer_kind != "*" { - khive_runtime::secret_gate::check(&consumer_kind)?; + khive_runtime::secret_gate::check_at(&consumer_kind, "binding", "consumer_kind")?; } if consumer_kind != "*" { @@ -2730,10 +2730,10 @@ impl BrainPack { // Secret gate: scan caller-supplied text before any write. // `p_name` is already constrained to [a-zA-Z0-9-]+ and cannot carry a secret. - khive_runtime::secret_gate::check(&description)?; - khive_runtime::secret_gate::check(&consumer_kind)?; + khive_runtime::secret_gate::check_at(&description, "profile", "description")?; + khive_runtime::secret_gate::check_at(&consumer_kind, "profile", "consumer_kind")?; if let Some(ref seed) = p.seed_priors { - khive_runtime::secret_gate::check_json(seed)?; + khive_runtime::secret_gate::check_json_at(seed, "profile", "seed_priors")?; } let seed_priors = p.seed_priors; diff --git a/crates/khive-pack-code/src/source_ingest.rs b/crates/khive-pack-code/src/source_ingest.rs index 2dbb91fbc..e07a32f2e 100644 --- a/crates/khive-pack-code/src/source_ingest.rs +++ b/crates/khive-pack-code/src/source_ingest.rs @@ -632,12 +632,12 @@ async fn get_entity_opt( /// never set `description`, so this is additive and does not change their /// gate coverage. fn gate_check(entity: &Entity) -> Result<(), RuntimeError> { - secret_gate::check(&entity.name)?; + secret_gate::check_at(&entity.name, "entity", "name")?; if let Some(description) = &entity.description { - secret_gate::check(description)?; + secret_gate::check_at(description, "entity", "description")?; } if let Some(properties) = &entity.properties { - secret_gate::check_json(properties)?; + secret_gate::check_json_at(properties, "entity", "properties")?; } Ok(()) } diff --git a/crates/khive-pack-knowledge/src/knowledge/crud.rs b/crates/khive-pack-knowledge/src/knowledge/crud.rs index 76f3f75bf..7710320fc 100644 --- a/crates/khive-pack-knowledge/src/knowledge/crud.rs +++ b/crates/khive-pack-knowledge/src/knowledge/crud.rs @@ -272,23 +272,23 @@ impl KnowledgeHandlers { // atom that produced it by POSITION, never by slug: this loop returns ONE // error for the whole batch, the slug is itself a scanned field, and two // atoms may share a slug within one payload (#2605). - use khive_runtime::secret_gate::{self, locate}; + use khive_runtime::secret_gate; let record = format!("atoms[{index}]"); - locate(secret_gate::check(&slug), &record, "slug")?; - locate(secret_gate::check(&atom_in.name), &record, "name")?; - locate(secret_gate::check(&content), &record, "content")?; + secret_gate::check_at(&slug, &record, "slug")?; + secret_gate::check_at(&atom_in.name, &record, "name")?; + secret_gate::check_at(&content, &record, "content")?; if let Some(ref tags_vec) = atom_in.tags { - locate(secret_gate::check_tags(tags_vec), &record, "tags")?; + secret_gate::check_tags_at(tags_vec, &record, "tags")?; } if let Some(ref props) = atom_in.properties { - locate(secret_gate::check_json(props), &record, "properties")?; + secret_gate::check_json_at(props, &record, "properties")?; } secret_gate::reject_reserved_secret_gate_property(atom_in.properties.as_ref())?; if let Some(Some(uri)) = &atom_in.source_uri { - locate(secret_gate::check(uri), &record, "source_uri")?; + secret_gate::check_at(uri, &record, "source_uri")?; } if let Some(Some(st)) = &atom_in.source_type { - locate(secret_gate::check(st), &record, "source_type")?; + secret_gate::check_at(st, &record, "source_type")?; } } @@ -487,8 +487,8 @@ impl KnowledgeHandlers { } // Secret gate: scan slug and name first (before content-length validation) // so security violations short-circuit before business logic errors. - khive_runtime::secret_gate::check(&slug)?; - khive_runtime::secret_gate::check(&name)?; + khive_runtime::secret_gate::check_at(&slug, "domain", "slug")?; + khive_runtime::secret_gate::check_at(&name, "domain", "name")?; // Domain mirror atoms are written to knowledge_atoms with the description // as content. Enforce the same 20-word minimum that normal atoms must satisfy // so the FTS and embedding surfaces receive adequate content. @@ -497,12 +497,12 @@ impl KnowledgeHandlers { RuntimeError::InvalidInput(format!("domain {slug:?}: description {e}")) })?; // Secret gate: scan remaining caller-supplied text. - khive_runtime::secret_gate::check(mirror_content)?; + khive_runtime::secret_gate::check_at(mirror_content, "domain", "description")?; if let Some(ref tags_vec) = domain_in.tags { - khive_runtime::secret_gate::check_tags(tags_vec)?; + khive_runtime::secret_gate::check_tags_at(tags_vec, "domain", "tags")?; } if let Some(ref members_vec) = domain_in.members { - khive_runtime::secret_gate::check_tags(members_vec)?; + khive_runtime::secret_gate::check_tags_at(members_vec, "domain", "members")?; } let mut tags: Vec = domain_in.tags.clone().unwrap_or_default(); diff --git a/crates/khive-pack-knowledge/src/knowledge/sections.rs b/crates/khive-pack-knowledge/src/knowledge/sections.rs index fe455f315..8c589120a 100644 --- a/crates/khive-pack-knowledge/src/knowledge/sections.rs +++ b/crates/khive-pack-knowledge/src/knowledge/sections.rs @@ -677,14 +677,15 @@ fn prepare_import_file( let (sections, sections_skipped) = if chunk_strategy == "section" { let mut prepared = Vec::new(); let mut skipped = 0usize; - for (section_type, heading, content) in parsed_sections { + for (index, (section_type, heading, content)) in parsed_sections.into_iter().enumerate() { + let record = format!("section[{index}]"); if content.len() < super::util::MIN_SECTION_CONTENT_LEN { skipped = skipped.saturating_add(1); continue; } validate_section_content(&content)?; - khive_runtime::secret_gate::check(&heading)?; - khive_runtime::secret_gate::check(&content)?; + khive_runtime::secret_gate::check_at(&heading, &record, "heading")?; + khive_runtime::secret_gate::check_at(&content, &record, "content")?; prepared.push(PreparedSection { section_type, heading, @@ -696,15 +697,15 @@ fn prepare_import_file( (Vec::new(), 0) }; - khive_runtime::secret_gate::check(&slug)?; - khive_runtime::secret_gate::check(&name)?; - khive_runtime::secret_gate::check_tags(&frontmatter.tags)?; - khive_runtime::secret_gate::check(&atom_content)?; + khive_runtime::secret_gate::check_at(&slug, "file", "slug")?; + khive_runtime::secret_gate::check_at(&name, "file", "name")?; + khive_runtime::secret_gate::check_tags_at(&frontmatter.tags, "file", "tags")?; + khive_runtime::secret_gate::check_at(&atom_content, "file", "content")?; let properties_value = Value::Object(properties.clone()); - khive_runtime::secret_gate::check_json(&properties_value)?; + khive_runtime::secret_gate::check_json_at(&properties_value, "file", "properties")?; khive_runtime::secret_gate::reject_reserved_secret_gate_property(Some(&properties_value))?; - khive_runtime::secret_gate::check(&source_uri)?; - khive_runtime::secret_gate::check(source_type)?; + khive_runtime::secret_gate::check_at(&source_uri, "file", "source_uri")?; + khive_runtime::secret_gate::check_at(source_type, "file", "source_type")?; Ok(PreparedImportFile { slug, @@ -924,13 +925,14 @@ impl KnowledgeHandlers { let mut upserted = 0usize; let mut section_results: Vec = Vec::with_capacity(p.sections.len()); - for su in &p.sections { + for (index, su) in p.sections.iter().enumerate() { + let record = format!("section[{index}]"); let stype = parse_section_type(&su.section_type)?; validate_section_content(&su.content)?; // Secret gate: scan section content and heading before any write. - khive_runtime::secret_gate::check(&su.content)?; + khive_runtime::secret_gate::check_at(&su.content, &record, "content")?; if let Some(ref h) = su.heading { - khive_runtime::secret_gate::check(h)?; + khive_runtime::secret_gate::check_at(h, &record, "heading")?; } let heading = su.heading.as_deref().unwrap_or(stype.as_str()).to_string(); let tokens = count_tokens(&su.content); diff --git a/crates/khive-runtime/src/curation.rs b/crates/khive-runtime/src/curation.rs index 4cf9a2311..89b775e65 100644 --- a/crates/khive-runtime/src/curation.rs +++ b/crates/khive-runtime/src/curation.rs @@ -2184,7 +2184,7 @@ impl KhiveRuntime { "transport_message_id": &transport_message_id, }), "message", - "delivery", + "delivered", )?; let snapshot = self.outbound_message(token, id).await?; if Self::outbound_delivery_is_terminal( @@ -2233,7 +2233,7 @@ impl KhiveRuntime { "last_error": &last_error, }), "message", - "delivery", + "failed", )?; let snapshot = self.outbound_message(token, id).await?; if Self::outbound_delivery_is_terminal( diff --git a/crates/khive-runtime/src/note_write.rs b/crates/khive-runtime/src/note_write.rs index 5b321d22b..6a5811aea 100644 --- a/crates/khive-runtime/src/note_write.rs +++ b/crates/khive-runtime/src/note_write.rs @@ -648,7 +648,7 @@ impl KhiveRuntime { "embedding_content must be a non-empty proper prefix of content".into(), )); } - crate::secret_gate::check(prefix)?; + crate::secret_gate::check_at(prefix, "note", "embedding_content")?; } let mut candidate = khive_storage::note::Note::new(token.namespace().as_str(), kind, content); diff --git a/crates/khive-runtime/src/portability.rs b/crates/khive-runtime/src/portability.rs index 4ca6201a4..e2c07646e 100644 --- a/crates/khive-runtime/src/portability.rs +++ b/crates/khive-runtime/src/portability.rs @@ -259,7 +259,8 @@ impl KhiveRuntime { ))); } } - for edge in &archive.edges { + for (index, edge) in archive.edges.iter().enumerate() { + let record = format!("edge[{index}]"); crate::operations::validate_edge_weight(edge.weight)?; // Edge properties are caller-controlled input: the runtime-owned // `khive:secret_gate` key is reservation-only on import, and edge @@ -267,7 +268,7 @@ impl KhiveRuntime { // class, so credential-shaped values are rejected here as well. crate::secret_gate::reject_reserved_secret_gate_property(edge.properties.as_ref())?; if let Some(p) = edge.properties.as_ref() { - crate::secret_gate::check_json(p)?; + crate::secret_gate::check_json_at(p, &record, "properties")?; } } diff --git a/crates/khive-runtime/src/secret_gate.rs b/crates/khive-runtime/src/secret_gate.rs index ea8f10203..1bd31d132 100644 --- a/crates/khive-runtime/src/secret_gate.rs +++ b/crates/khive-runtime/src/secret_gate.rs @@ -150,9 +150,10 @@ pub fn check_tags(tags: &[String]) -> RuntimeResult<()> { /// rejected with it (khive #2605). Pass through anything that is not a gate /// refusal unchanged — this adds identity, it does not reclassify. /// -/// `scope` is a record label for a batch (`notes[2]`) and the verb for a -/// single-record write (`comm.send`). Both answer the same question, which is -/// where in the submitted payload the writer should look. +/// `record` names the record the field belongs to, as the caller submitted it: +/// `entity`, `note`, `task`, `proposal`, `message`, indexed when it came from a +/// batch (`note[2]`). It answers where in the submitted payload the writer +/// should look, which is the question a refused writer actually asks. pub fn locate(result: RuntimeResult, record: &str, field: &str) -> RuntimeResult { result.map_err(|error| match error { RuntimeError::SecretDetected(matched) => RuntimeError::SecretDetected(SecretMatch { @@ -163,21 +164,21 @@ pub fn locate(result: RuntimeResult, record: &str, field: &str) -> Runtime }) } -/// `check` that names where it looked. `scope` is the verb for a single-record -/// write, a record label inside a batch. -pub fn check_at(content: &str, scope: &str, field: &str) -> RuntimeResult<()> { - locate(check(content), scope, field) +/// `check` that names where it looked. `record` is the record noun the caller +/// submitted, indexed inside a batch; `field` is the field of it that was scanned. +pub fn check_at(content: &str, record: &str, field: &str) -> RuntimeResult<()> { + locate(check(content), record, field) } /// `check_json` that names where it looked. The location is the field holding /// the JSON, not the path of the string leaf that matched inside it. -pub fn check_json_at(value: &serde_json::Value, scope: &str, field: &str) -> RuntimeResult<()> { - locate(check_json(value), scope, field) +pub fn check_json_at(value: &serde_json::Value, record: &str, field: &str) -> RuntimeResult<()> { + locate(check_json(value), record, field) } /// `check_tags` that names where it looked. -pub fn check_tags_at(tags: &[String], scope: &str, field: &str) -> RuntimeResult<()> { - locate(check_tags(tags), scope, field) +pub fn check_tags_at(tags: &[String], record: &str, field: &str) -> RuntimeResult<()> { + locate(check_tags(tags), record, field) } // ─── Reserved property key (ADR-115 Amendment 1) ──────────────────────────── diff --git a/crates/khive-runtime/src/streams.rs b/crates/khive-runtime/src/streams.rs index b5664988b..494a43b16 100644 --- a/crates/khive-runtime/src/streams.rs +++ b/crates/khive-runtime/src/streams.rs @@ -1050,9 +1050,11 @@ impl KhiveRuntime { self.validate_note_kind(&fence.kind)?; } } - crate::secret_gate::check( + crate::secret_gate::check_at( &serde_json::to_string(&spec.record) .map_err(|error| RuntimeError::InvalidInput(error.to_string()))?, + &format!("member[{index}]"), + "record", )?; } StreamBatchMember::Write(spec) => { @@ -1075,12 +1077,18 @@ impl KhiveRuntime { spec.key, spec.kind, ))); } - crate::secret_gate::check( + crate::secret_gate::check_at( &serde_json::to_string(&spec.doc) .map_err(|error| RuntimeError::InvalidInput(error.to_string()))?, + &format!("member[{index}]"), + "doc", )?; if let Some(tags) = &spec.tags { - crate::secret_gate::check_json(&json!({"tags": tags}))?; + crate::secret_gate::check_json_at( + &json!({"tags": tags}), + &format!("member[{index}]"), + "tags", + )?; } } StreamBatchMember::Refused(_) => {} diff --git a/crates/kkernel/src/code_ingest.rs b/crates/kkernel/src/code_ingest.rs index 61401259e..a03d81c4c 100644 --- a/crates/kkernel/src/code_ingest.rs +++ b/crates/kkernel/src/code_ingest.rs @@ -552,25 +552,32 @@ async fn settle_writer_drain( /// evidence is rejected here exactly as it would be on the shared `create` /// verb path, rather than persisting verbatim. fn preflight_secret_gate(batch: &CodeIngestBatch) -> Result<()> { - for entity in &batch.entities { - secret_gate::check(&entity.name).map_err(|e| anyhow::anyhow!("{e}"))?; + for (index, entity) in batch.entities.iter().enumerate() { + let record = format!("entity[{index}]"); + secret_gate::check_at(&entity.name, &record, "name").map_err(|e| anyhow::anyhow!("{e}"))?; if let Some(description) = &entity.description { - secret_gate::check(description).map_err(|e| anyhow::anyhow!("{e}"))?; + secret_gate::check_at(description, &record, "description") + .map_err(|e| anyhow::anyhow!("{e}"))?; } if let Some(properties) = &entity.properties { - secret_gate::check_json(properties).map_err(|e| anyhow::anyhow!("{e}"))?; + secret_gate::check_json_at(properties, &record, "properties") + .map_err(|e| anyhow::anyhow!("{e}"))?; } secret_gate::reject_reserved_secret_gate_property(entity.properties.as_ref()) .map_err(|e| anyhow::anyhow!("{e}"))?; - secret_gate::check_tags(&entity.tags).map_err(|e| anyhow::anyhow!("{e}"))?; + secret_gate::check_tags_at(&entity.tags, &record, "tags") + .map_err(|e| anyhow::anyhow!("{e}"))?; } - for note in &batch.notes { - secret_gate::check(¬e.content).map_err(|e| anyhow::anyhow!("{e}"))?; + for (index, note) in batch.notes.iter().enumerate() { + let record = format!("note[{index}]"); + secret_gate::check_at(¬e.content, &record, "content") + .map_err(|e| anyhow::anyhow!("{e}"))?; if let Some(name) = ¬e.name { - secret_gate::check(name).map_err(|e| anyhow::anyhow!("{e}"))?; + secret_gate::check_at(name, &record, "name").map_err(|e| anyhow::anyhow!("{e}"))?; } if let Some(properties) = ¬e.properties { - secret_gate::check_json(properties).map_err(|e| anyhow::anyhow!("{e}"))?; + secret_gate::check_json_at(properties, &record, "properties") + .map_err(|e| anyhow::anyhow!("{e}"))?; } secret_gate::reject_reserved_secret_gate_property(note.properties.as_ref()) .map_err(|e| anyhow::anyhow!("{e}"))?;