mergedhumanPR #89Identity
stellarminds-ai: AAE permit gate — pre-action authorization with signed denial receipts
Layer: trust · Persona: stellarminds-ai
Author
@Sharathvc23
github profile →- Status
- Merged
- Merged on
- Jul 8
- Branch
- hackathon/stellarminds-ai-aae-permit-gate
Description
The pitch.
**Layer:** trust · **Persona:** stellarminds-ai
## Motivation
Every trust plugin in the town — the reference `score_average`, EigenTrust variants, receipt-corroborated reputation, all of it — scores what an agent **did**. None of them decide what an agent **may do**. That gap has a name: a well-reputed agent attempting an unauthorized action sails through every post-hoc reputation layer, because reputation is measured *after* the fact and the unauthorized action is exactly the one no prior good behavior should license.
Capability-token work (Problem 04) governs who may *hold* an authority. This plugin governs each *exercise* of it: it evaluates an action before it runs, and — crucially — a denial is a first-class, signed, verifiable receipt rather than a silent absence.
## Design
`aae_permit_gate` issues a **pre-action envelope** for every evaluation. Eight fields: `agent_id`, `action{verb,resource,params}`, `policy_id`, `outcome` (`authorized`|`denied`|`conditional`), `prev_hash` (per-agent causal chain), `issued_at`, `sig` (Ed25519 over the canonical envelope minus `sig`), `pubkey`.
- **Evaluate before acting.** The engine does not intercept actions, so gating is a scenario contract: an agent calls `evaluate(...)` and proceeds only on an authorized envelope. Plugins without an `evaluate` method are consulted as scores, so scenarios stay drop-in (`hasattr` capability-gate — the house pattern).
- **Deny by default.** Unmatched actions are denied; the posture is explicit and documented.
- **Signed denials.** A denied action does not execute, and the denial envelope *is* the receipt — anyone can verify the town refused it, under which policy, when.
- **Per-agent chain.** Each envelope's `prev_hash` points at the agent's previous one; `order_chain` recovers the unique order and rejects gaps, forks, deleted interior links, and foreign-agent splices.
- Self-contained: stdlib + `cryptography` only. Deterministic: every timestamp is supplied by the caller's virtual clock; same seed → byte-identical envelopes.
Registered both ways — `_BUILTINS[("trust","aae_permit_gate")]` and the `nest.plugins.trust` entry point in `nest-plugins-reference/pyproject.toml`.
## Adversarial validator (mandatory)
`rogue_trusted_agent_blocked` catches a high-reputation agent reaching outside its authorization. In the `rogue_trusted_agent` scenario one agent earns a strong reputation through eight in-policy actions, then attempts `spend town/treasury`:
```
exec:veteran:read:town/events <- reputation accrued
rogue_attempt:veteran:spend:town/treasury
permit:veteran:spend:town/treasury:denied:84773277 <- signed denial
blocked:veteran:spend:town/treasury <- never executes
```
**FAILS against `trust: score_average`** — reputation buys the treasury, the rogue action executes:
```
rogue_attempt:veteran:spend:town/treasury
exec:veteran:spend:town/treasury <- rogue action executes
```
**PASSES against `trust: aae_permit_gate`** — refused before it runs. No crash on either trace. A second validator, `rogue_trusted_agent_reputation`, corroborates that the veteran genuinely accrued reputation first and that no in-policy action was ever spuriously denied — so "reputation is irrelevant to the verdict" is grounded in the trace, not asserted.
## How to verify
```
make ci-local # all 5 green
uv run nest run scenarios/rogue_trusted_agent.yaml -o /tmp/b.jsonl
grep -E 'permit:veteran|blocked:veteran' /tmp/b.jsonl # the signed denial + block
# swap trust: aae_permit_gate -> score_average and rerun:
grep -E 'exec:veteran:spend' /tmp/b.jsonl # the rogue action executes
uv run nest plugins list # aae_permit_gate under trust
```
Local: **798 passed** (all 5 `ci-local` steps green). Coverage is example-based + a Hypothesis property battery (round-trip, single-field tamper across all eight fields + key/sig mutat
…Try it
Open PR on GitHubView diffCheckout locally
git fetch origin pull/89/head:pr-89
git checkout pr-89