Repository navigation
feat(cards): capability cards — an agent made discoverable, by its owner - #80
Open
ExposureGuard wants to merge 1 commit into
Open
ExposureGuard wants to merge 1 commit into
ExposureGuard wants to merge 1 commit into
Conversation
The register answers "which agents do we run". This answers "which of them are
we willing to be found by": an owner-published, capability-only card, listed
at `/.well-known/agents.json`. It is the first part of Haldir that faces
outward, so the rules that keep it honest are structural rather than
aspirational:
**The public projection is built by naming what it includes.**
`public_cards()` selects its columns; it does not copy a row and delete the
private fields. A column added to the table later cannot leak by default —
and a test asserts the card's key set is exactly the documented seven, for an
agent that has real spend and flags on the record.
**Opt-in per agent; withdrawal means deletion.** Publishing names one agent.
`unpublish` deletes the row, because "hidden" and "gone" are different
promises and only one of them is worth making to an operator.
**Published, not verified.** The index says so in its own document. A
directory that implies endorsement turns somebody else's overstatement into
our misrepresentation.
**An agent must exist to be published** — the route checks the register
first, so a listing always refers to an agent this deployment has seen — and
one tenant cannot see, change, or withdraw another's card. The 404 for a
foreign card is the same 404 as for a nonexistent one.
Also in this change: the register now says which of an operator's agents are
discoverable (`card_id` per entry, `discoverable_agents` in the summary) and
distinguishes **agents from principals** — `admin:<key-prefix>` is how
`_audit_admin` attributes an operator's action, and `system` is the
deployment's own bookkeeping. They acted, so they belong in the register; but
a register of *AI systems* that counts an operator's key as an agent is wrong
in the direction nobody notices. `summary.agents` counts systems,
`summary.principals` counts the rest, `summary.entries` is everything.
`haldir publish <agent> --name ... [--capability X]...` and
`haldir unpublish <agent>` for the terminal; `POST/DELETE
/v1/agents/<id>/card` for everything else. Migration 010, and a test that the
migration and the store create the *same* table — two definitions of one
table is how `approval_requests` ended up without its tenant column on one
path.
Two test-suite fixes this surfaced: the new modules release the shared
rate-limit budget when they finish (they were starving later modules into
429s), and the spend-leak assertion compares numbers rather than substrings
("50" appears inside the published_at epoch).
Verified: 1137 tests, flake8 clean, mypy clean over 32 files, wheel inspected
— the module and the migration both ship.
Co-Authored-By: Claude Code <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #79 → #78 → #77. Merge order: #77 → #78 → #79 → this.
The register answers "which agents do we run". This answers "which of them are we willing to be found by": an owner-published, capability-only card, listed at
/.well-known/agents.json. It is the first part of Haldir that faces outward, so the honesty rules are structural:The public projection is built by naming what it includes.
public_cards()selects its columns — it does not copy a row and delete the private fields. A column added later cannot leak by default, and a test asserts the card's key set is exactly the documented seven for an agent with real spend and flags on the record. (That test also compares numbers, not substrings: "50" appears inside thepublished_atepoch, and a test that fails on a timestamp is a test nobody trusts.)Opt-in per agent; withdrawal means deletion.
unpublishdeletes the row — "hidden" and "gone" are different promises to an operator, and only one is worth making.Published, not verified. The index says so in its own document. A directory that implies endorsement turns somebody else's overstatement into our misrepresentation.
An agent must exist to be published (the route checks the register first), and one tenant cannot see, change, or withdraw another's card — a foreign card returns the same 404 as a nonexistent one, so the route is not an existence oracle.
Also here
card_idper entry,discoverable_agentsin the summary. An operator looking at the list needs to know which listing to withdraw.admin:<key-prefix>is how_audit_adminattributes an operator's action;systemis the deployment's own bookkeeping. They acted, so they are in the register — but a register of AI systems that counts an operator's key as an agent is wrong in the direction nobody notices.summary.agentscounts systems,summary.principalscounts the rest,summary.entriesis everything. (Found by running it: the first smoke test showedadmin:hld_…listed as an agent.)haldir publish/haldir unpublish, and migration 010 with a test that the migration and the store create the same table — two definitions of one table is howapproval_requestsended up without its tenant column on one path./docsandllms.txtupdated; the public index is in the agent-facing doc because agents are who it is for.Two test-suite fixes this surfaced
Verification
1137 tests, flake8 clean, mypy clean over 32 files, wheel inspected —
haldir_cards.pyandmigrations/010_agent_cards.sqlboth ship.openapi.jsonregenerated (89 paths).What this does not do
It does not turn the index on for anyone who has not asked: the table is empty until an operator publishes an agent, and the public document is a plain JSON list with an explicit "not verified" note. The cross-tenant data idea we discussed is deliberately not here — that needs consent in the contract, not just a flag in the schema.
🤖 Generated with Claude Code