in reviewhumanPR #131Data Facts
audit-engineer: intent-gated datafacts — pre-publication intent gate (problem 08)
Layer: datafacts · Problem: 08 · Persona: audit-engineer · Author: Midhun Raj Charles (midhunrajcharles) — same name as the Phase 2 skills submission ([AgentIntent in the registry](https://nandatown.projectnanda.org/skills), entry…
Author
@midhunrajcharles
github profile →- Status
- In review
- Opened on
- Jul 9
- Branch
- hackathon/agentintent-intent-gated-datafacts
Description
The pitch.
**Layer:** `datafacts` · **Problem:** 08 · **Persona:** audit-engineer · **Author:** Midhun Raj Charles (`midhunrajcharles`) — same name as the Phase 2 skills submission ([AgentIntent in the registry](https://nandatown.projectnanda.org/skills), entry `f9e242fd-4cd8-4f71-9240-b12ff98447e5`, SKILL.md: https://agentintent.onrender.com/SKILL.md).
**The persona, in the code:** an auditor assumes every actor will lie *after the fact*, so the only usable evidence is a commitment recorded *before* the action. That is the design axis of the whole diff: intents are declared before publication and consumed exactly once; the ownership check runs before the intent is burned so a failed spoof leaves the evidence trail intact; expired and fulfilled intents are never deleted — `intent_log()` keeps every record with its status transition for post-hoc trace analysis; and the validators grade the *trace*, not the plugin's own claims. The same declare→verify→complete→audit model is what the Phase 2 AgentIntent service exposes over HTTP.
## Problem
**Problem 08 — datafacts.** This PR extends the merged CidFacts core (#31) with an attack class it cannot express: **pre-publication intent**. CidFacts proves *what* was published (content addressing) and *when/by whom* it was signed (signed freshness), but places no constraint on *whether* an agent may publish at all — any agent can call `publish()` at any moment with no prior announcement, so surprise publications are invisible in a trace. `IntentGatedFacts` adds a mandatory declare-before-publish gate on top of the merged base class (extension, not fork): a one-time, TTL-bound intent on the shared logical clock, consumed at publish time, plus a publish-time owner check so a publisher cannot claim another agent's identity.
## What
- `nest_plugins_reference/datafacts/intent_facts.py` — `IntentGatedFacts` extends the merged `CidFacts` with a pre-publication intent registry (one-time intents, TTL on the shared logical clock, per-instance owner binding, `IntentError` on violation). Registered as `("datafacts", "intent_facts")` in the nest-core builtins **and** under `[project.entry-points."nest.plugins.datafacts"]`.
- Builtin scenario `intent_gated_datafacts` (`scenarios/intent_gated_datafacts.yaml` + factory): three honest supply-chain publishers declare an intent then publish; an attacker publishes with no declared intent.
- Two trace validators in `nest_core/validators.py` — `validate_intent_no_surprise_publication` and `validate_intent_gate_blocks_attacker` — the discriminator pair: both **FAIL** on `datafacts_v1` (baseline) and **PASS** on `intent_facts`, byte-deterministic across seeds 42 / 7 / 1337.
## Attacks blocked (that #31's CidFacts cannot)
1. **Surprise publication** — publish with no prior declared intent → `IntentError`.
2. **Expired-intent replay** — intent TTL elapsed on the logical clock → `IntentError`.
3. **Owner-spoof / intent-hijack** — agent B declares its own intent, then publishes a dataset whose `owner` field claims agent A: `publish()` compares `dataset.owner` against the instance owner *before* consuming the intent, so the spoofed dataset never reaches the shared store. CidFacts alone only catches the signer mismatch later, at `verify_freshness` time, after the store is already polluted.
## Review follow-ups (per @dhve)
- Rebased onto current main; conflicts in `scenarios.py` / `test_validators.py` resolved. Purely additive again.
- Dropped `integration/agent_intent_client.py` and its 33 mocked-transport tests from this PR (and with them the undeclared transitive `httpx` dependency). The protocol client lives in the external repo linked below.
- Added the `dataset.owner` ownership check in `publish()` plus two adversarial owner-spoof tests (spoof blocked; blocked spoof does not burn the live intent).
- Owner id now comes from an explicit `owner=` constructor argument or the public `agent_id` property (verified against `Ed25519RotatingIdentity`); the `identity._agent_i
…Try it
Open PR on GitHubView diffCheckout locally
git fetch origin pull/131/head:pr-131
git checkout pr-131