Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions crates/khive-pack-brain/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 != "*" {
Expand Down Expand Up @@ -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;

Expand Down
6 changes: 3 additions & 3 deletions crates/khive-pack-code/src/source_ingest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}
Expand Down
26 changes: 13 additions & 13 deletions crates/khive-pack-knowledge/src/knowledge/crud.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")?;
}
}

Expand Down Expand Up @@ -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.
Expand All @@ -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<String> = domain_in.tags.clone().unwrap_or_default();
Expand Down
28 changes: 15 additions & 13 deletions crates/khive-pack-knowledge/src/knowledge/sections.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -924,13 +925,14 @@ impl KnowledgeHandlers {
let mut upserted = 0usize;
let mut section_results: Vec<Value> = 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);
Expand Down
4 changes: 2 additions & 2 deletions crates/khive-runtime/src/curation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion crates/khive-runtime/src/note_write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 3 additions & 2 deletions crates/khive-runtime/src/portability.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,15 +259,16 @@ 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
// metadata is in ADR-115 Amendment 1 §3's unchanged blocking-scanner
// 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")?;
}
}

Expand Down
23 changes: 12 additions & 11 deletions crates/khive-runtime/src/secret_gate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(result: RuntimeResult<T>, record: &str, field: &str) -> RuntimeResult<T> {
result.map_err(|error| match error {
RuntimeError::SecretDetected(matched) => RuntimeError::SecretDetected(SecretMatch {
Expand All @@ -163,21 +164,21 @@ pub fn locate<T>(result: RuntimeResult<T>, 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) ────────────────────────────
Expand Down
14 changes: 11 additions & 3 deletions crates/khive-runtime/src/streams.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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(_) => {}
Expand Down
25 changes: 16 additions & 9 deletions crates/kkernel/src/code_ingest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(&note.content).map_err(|e| anyhow::anyhow!("{e}"))?;
for (index, note) in batch.notes.iter().enumerate() {
let record = format!("note[{index}]");
secret_gate::check_at(&note.content, &record, "content")
.map_err(|e| anyhow::anyhow!("{e}"))?;
if let Some(name) = &note.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) = &note.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}"))?;
Expand Down
Loading