Skip to main content

validate_link_pre_create

Function validate_link_pre_create 

Source
pub fn validate_link_pre_create(
    conn: &Connection,
    source_id: &str,
    target_id: &str,
    relation: &str,
    agent_id: &str,
    skip_governance: bool,
) -> Result<()>
Expand description

v0.7.0 fix-campaign A3 (LINK-PARITY) — shared pre-create validator invoked by every link-write entry point.

Closes the S5-H2 HIGH finding (#690): before A3 the L1-2 cycle check + K9 permission pipeline ran only in src/mcp/tools/link.rs::handle_link, so the HTTP POST /api/v1/links path and the federation-receive sync_push link loop could land reflects_on edges that the MCP path would have refused. The fix is defense-in-depth at the storage layer: every path — MCP, HTTP, SAL, federation — calls this helper, so the gates enforce no matter which entry point initiates the write.

Pipeline:

  1. Cycle check — invoked only when relation == "reflects_on". Calls crate::kg::cycle_check::would_create_reflection_cycle with the namespace-scoped effective_max_reflection_depth cap; on a would_cycle hit, returns an error prefixed with LINK_CYCLE_ERR_PREFIX so HTTP can surface 409 CONFLICT and signed-event emit can record the refusal. The walk fails CLOSED on SQL errors and on depth-ceiling truncation.
  2. K9 permission eval — runs the unified crate::permissions::Permissions::evaluate pipeline against the source memory’s namespace. On Deny, returns an error prefixed with LINK_PERMISSION_DENIED_ERR_PREFIX so HTTP surfaces 403. Ask is treated as Deny here because the storage-layer helper has no Ask-channel back to the operator; entry points that want interactive Ask handling (MCP) should invoke Permissions::evaluate directly BEFORE calling create_link.

skip_governance lets federation-receive bypass the K9 gate when the inbound link has already been cryptographically attested by an enrolled peer (attest_level == “peer_attested”). The cycle check always runs — even a trusted peer should not be able to extend a reflection cycle on the receiver. See create_link_inbound for the caller-side decision logic.

agent_id defaults to "system" when the caller cannot resolve a concrete claimant (federation receive path with no claim, etc.) — the permission rule matcher uses it for agent_pattern matching.