Skip to content

feat(registry): the agent register — every agent that has acted - #77

Merged
ExposureGuard merged 2 commits into
mainfrom
feat/agent-register
Oct 4, 2026
Merged

ExposureGuard merged 2 commits into
mainfrom
feat/agent-register

Conversation

@ExposureGuard

Copy link
Copy Markdown
Owner

Independent of #73–#76; branched from main. This is the spine of the assurance model — the artifact an auditor, a customer or a regulator asks for: "show me your register of AI systems."

The state it fixes

agents has been written on every session creation since migration 001 — Gate.register_agent, reached from POST /v1/sessions (api.py:930) and the delegation path — and nothing has ever read it. No list method, no route, no dashboard page, no CLI, no export. The register already existed as state; this is the surface.

What's in it

haldir_registry.py — five grouped queries over one tenant, no N+1, no Flask. Derived from activity as well as registration: an agent that acted but was never explicitly registered is still listed. A register that silently omits agents reads as complete and isn't, which is worse than none.

Per agent: registered, default_scopes, max_spend, first_seen/last_seen, sessions (total/active/revoked), spend, activity (actions, cost, flagged), approvals, and delegation in both directions — delegates_to and spawned_by, because "which of my agents can spawn others" is what the tree exists to answer.

Two spend figures, deliberately. session_spend_usd is what the Gate metered against caps; audited_cost_usd is what the append-only log recorded per action. A payment moves both, a tool call that only logs cost moves the second. Reporting one as "spend" would quietly pick a side.

GET /v1/agents and GET /v1/agents/<id> — tenant-scoped, admin:read. A miss returns the same 404 whether the agent belongs to another tenant or to nobody, so the route can't be used as an existence oracle.

The dashboard gains an Agents page (summary stats + table). The contract test parses the real dashboard.js template against a real payload — same guarantee as #75's pages. Note: #75 introduces tests/test_dashboard_contract.py with the same helpers and three other pages; whichever lands second should keep both sets — they're additive.

haldir agents [--agent ID] [--json] — the register as a terminal table.

The evidence pack gains an "Agent register" section in both rendered forms. This is the form an auditor receives, so it is signed like everything else — with the recency fields (last_seen, last_action_at, sessions.active) excluded from the digest, exactly as access_control.last_used is. A session TTL expires with no write; without the exclusion, an auditor re-verifying an archived pack reads that drift as tampering, which is the failure the digest exists to prevent. The register's substance stays in the digest, and the test asserts both directions — recency changes leave it fixed, an action-count change moves it.

Verification

  • 1106 tests pass, flake8 clean, mypy clean over 30 files (the new module is in scope)
  • Wheel built and inspected — haldir_registry.py ships
  • Checked both directions on the digest property (fails if the normalization is removed)
  • openapi.json regenerated for the two routes; /docs lists them

What this unlocks

The register is what makes the rest of the assurance model sellable: POST /v1/compliance/schedules and the evidence pack already exist, and this gives them the section that answers the actual buyer question. The natural next two pieces are framework coverage (ISO 42001 and EU AI Act Art. 12/26 mappings alongside SOC 2) and an opt-in capability card for an agent, so a register entry can also be a discovery entry — owner-published, capability-only.

🤖 Generated with Claude Code

`agents` has been written on every session creation since migration 001
(`Gate.register_agent`, reached from POST /v1/sessions and the delegation
path) and **nothing has ever read it**. Meanwhile sessions, audit_log,
approval_requests and the delegation tree all carry `agent_id`. The register
already existed as state; this is the surface.

It is also the artifact the assurance story rests on: the question an auditor,
a customer or a regulator asks is "show me your register of AI systems" —
which agents exist, what each is allowed to do, what each actually did, and
which of them can spawn others.

  * `haldir_registry.py` — five grouped queries over one tenant, no N+1.
    Derived from **activity as well as registration**: an agent that acted but
    was never explicitly registered is still listed, because a register that
    silently omits agents reads as complete and is not.
  * `GET /v1/agents` and `GET /v1/agents/<id>` — tenant-scoped, `admin:read`.
    A miss is the same 404 whether the agent belongs to another tenant or to
    nobody.
  * `/cloud/overview` gains an **Agents** page (summary stats + table), and
    the contract test parses the real template against a real payload.
  * `haldir agents [--agent ID] [--json]` — the register as a terminal table.
  * The **evidence pack** gains an "Agent register" section (markdown + HTML),
    which is the form an auditor receives. Its recency fields
    (`last_seen`, `last_action_at`, `sessions.active`) are excluded from the
    digest exactly as `access_control.last_used` is — a session TTL expires
    with no write, and an auditor re-verifying an archived pack must not read
    that drift as tampering. Its substance (agent, scopes, caps, counts,
    spend, flags, delegation) stays in the digest; the test asserts both
    directions.
  * The register reports **two spend figures**: `session_spend_usd` (what the
    Gate metered against caps) and `audited_cost_usd` (what the log recorded
    per action). They move independently, and calling either one "spend"
    would quietly pick a side.

Verified: 1106 tests, flake8 clean, mypy clean over 30 files (the module is
in scope), wheel built and inspected. `openapi.json` regenerated for the two
routes. The `/docs` page lists them.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Conflicts: `mypy.ini` and the packaging lists both branches extend — main
added `haldir_client_ip.py`, this branch adds `haldir_registry.py`, and the
resolution keeps both. `openapi.json` regenerated rather than trusted to a
textual auto-merge, which is what the committed-spec test exists for.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@ExposureGuard
ExposureGuard merged commit 10cc13e into main Oct 4, 2026
11 checks passed
ExposureGuard pushed a commit that referenced this pull request Oct 4, 2026
main moved when #77 landed after my first merge, and GitHub recomputes the
merge against the current base — so the conflict returned. Same union
resolution on `mypy.ini`, spec regenerated, suite run.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant