Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Notable changes to the AgenID reference implementation. Format follows [Keep a C

### Added
- **`/issue` hands the operator their `keys.json`.** The operator half of two-path key discovery (spec §9.3) is what keeps the registry non-authoritative, and the product told operators to publish it without giving them the file. The result screen now shows and downloads a strict `KeysDocument` built from the key document the registry just accepted, with the exact URL to serve it at. `lib/keys-document.ts` (dependency-free, client-safe); `test/keys-document.test.ts` (8) checks the output against `@agenid/core`'s `KeysDocument` and against `probeKeyDiscovery` — the function `/api/domain/status` and the Verification Card use — so the file handed out is the file accepted. The operator guide documents the format and the multi-agent case.
- **Operator-signed agent revocation — the first lifecycle transition this deployment writes.** `POST /api/v1/agents/{agenid}/revocation` accepts a `Revocation` (the v1.2-draft object, `revokes_type: "agent"`) signed by the agent's own operator key, and nothing else: another agent's operator key, an authority key, a tampered signature, a `revoked_at` more than five minutes from the registry clock, and a path/target mismatch are all refused before anything is written. Spec §12 makes `REVOKED` permanent: a second revocation or a replay is `409 already_revoked`, re-registration is `409 agent_exists`, and a revoked agent cannot be listed in the directory. The status change, the stored revocation and an `agent.revoked` ledger event (references and a sha-256 digest, never the object) are written in one Postgres transaction by `revoke_agent` (migration `0006`, RLS on, `EXECUTE` revoked from `PUBLIC`). The resolution envelope gains a `revocation` member — the full signed object when `REVOKED`, `null` otherwise — so a verifier can check that the operator made the change; the Verification Card shows who signed it. `/revoke` signs in the browser from the key file `/issue` offers, and asks for the reason, the typed identifier and an explicit permanence confirmation before it enables. The rules live once, in `packages/api/src/revoke.ts`, shared by the Fastify registry and the deployed route; a test fails if any other file calls `revokeAgent` or writes `REVOKED`. `packages/web/test/revocation.test.ts` (21), `packages/api/tests/revocation.test.ts` (4) and e2e 3s–3v; the standing, window and permanence guards were each proven to fail with the check removed. Every public page that said "no revocation flow" — homepage, `/trust`, `/badge`, and eleven passages in the content library — now says that agent revocation is operator-only and that key rotation, key revocation and every other status transition are still not deployed. `docs/revocation-propagation.md` proposes the missing half: how long a relying party may rely on an `ACTIVE` answer and what it must do when the registry is unreachable (proposal, not normative).
- **This repository is public (2026-09-23, MIT).** Before the flip: a full-history gitleaks 8.30.1 scan (64 commits, 37 findings, every one a public test vector, key id, published pilot key or deliberate test string) and a working-tree scan (findings only in gitignored files). After: private vulnerability reporting enabled here (`{"enabled":true}` by API), secret scanning and push protection on, `main` protected against force-push and deletion. Every surface that said the monorepo was private now says it is public and links the real path: `SECURITY.md` routes reports to this repository first; Nav and Footer gain "Source (GitHub)"; the homepage gains a "Read the source" link; `/ecosystem` links `packages/web/data/ecosystem/` and accepts entries as pull requests; the eight partner briefs and the onboarding guide point at `packages/core` and `packages/mcp-server`. npm status is unchanged — nothing is published, and every sentence that says so still does. `public-links.test.ts` T-1 now allows `agenid` in the public set and fails on any linked monorepo path that does not exist in the checkout.
- **v1.2-draft schemas served at `/schemas/v1.2/`** (`authorization-grant.json`, `revocation.json`; JSON Schema 2020-12; titled and commented *DRAFT — NOT NORMATIVE*). `@agenid/core` exports `GRANT_SCHEMA_ID` and `REVOCATION_SCHEMA_ID`, and the live Grok Bot pilot grant is signed with that `$schema` value, so both ids must resolve — this is what `check:schema-ids` required before a publish. The pilot grant validates against the grant schema under both ajv and core's zod validator; wildcard scopes, unknown members and a mismatched target are rejected. The schemas do not make the v1.2 layer normative or issuable. `test/schemas-v12.test.ts` pins the `$id` values to core's constants and the draft labels.
- **Opt-in public agent directory (SEO strategic investment S9).** `/agents` lists registered agents whose operators signed a listing consent — and nobody else; registration alone lists nothing. The consent (`agenid.directory.consent.v1`) is a small object signed with the agent's own operator key over RFC 8785 canonical bytes, verified against the key the registry already holds, accepted only within five minutes of the registry clock and only if newer than the last consent recorded, so it can be neither forged for someone else's agent nor replayed after a delisting. `POST`/`GET /api/v1/directory` (rate limited, CORS, in the OpenAPI document), a "List this agent" action on the `/issue` confirmation screen while the key is still in the tab, and a key-file form on `/agents` to list or delist later — the private key is read in the browser and never sent. Listed agents' Verification Cards are added to the sitemap. Storage is `directory_listings` (migration `0005`, applied to production), RLS enabled, public read limited to listed rows, no write policies. A listing is not an endorsement and carries no level; each row's trust state is read from its resolution envelope through `lib/trust-presentation.ts`. `/trust` and the guides that said no directory exists were updated in the same commit. `test/directory.test.ts` (18 tests).
Expand Down
14 changes: 9 additions & 5 deletions PROJECT_STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Capabilities that exist in code. Implemented is not the same as deployed, and ne
| Site, `/issue`, resolver, badges, and the deployed API routes | `@agenid/web` |
| Browser Ed25519 signer with an independent JCS implementation | `packages/web/lib/client-crypto.ts` |
| Assertion write path (`POST /v1/agents/:id/assertions`) | `@agenid/api` — **not deployed** |
| Operator-signed agent revocation — `POST /api/v1/agents/{agenid}/revocation`, `/revoke`, envelope `revocation` member, atomic `revoke_agent` | Rules in `packages/api/src/revoke.ts` (one implementation, shared by the Fastify registry and `packages/web`); uses the v1.2-draft `Revocation` object |
| Domain-control evidence — `POST /api/verify-dns` (real `_agenid.<domain>` TXT lookup) and `POST /api/domain/status` (provider detection, Domain Connect, and the operator's `.well-known` key document — reported `published` only when it strictly parses as a `KeysDocument` for that domain, never merely because the server answered 200) | `packages/web`. **No DNS write path exists:** `/api/dns/auto-add` is deleted, not unconfigured, and AgenID holds no provider credential |
| Ecosystem registry and validator — 25 platforms, one reviewable JSON file each | `packages/web/data/ecosystem/`, `lib/ecosystem.ts` |
| Long-form content library — 34-term glossary, 4 pillar guides, 6 sourced comparisons, voice-agent use-case hub, `/why-agent-identity` hub, research report, release-notes blog + RSS, `/badge` embed docs, and 17 `/how-it-works` scenarios (up from 9). One renderer that states the L1 issuance ceiling on every page; guarded by `test/content.test.ts` | `packages/web/lib/content/`, `components/content/`, `app/{glossary,learn,compare,use-cases,blog,state-of-agent-identity,badge}` |
Expand Down Expand Up @@ -93,7 +94,8 @@ Exists in code, unavailable in production.
| `/.well-known/agenid/authorities.json` | Correctly absent — publishing a pin for a nonexistent key would be the worst possible false claim |
| `@agenid/api` as a running service | Production serves the equivalent routes from `packages/web`; the Fastify server is not deployed anywhere |
| `0002_onboarding_tables.sql` | Written, deliberately unapplied. Wizard-specific state only; the wizard persists to the canonical tables today. |
| Agent statuses `CHANGED`, `STALE`, `SUSPENDED`, `REVOKED` | The column and enum exist so the envelope shape is stable, but no transition writes them. Every registered agent is `ACTIVE`. |
| Agent statuses `CHANGED`, `STALE`, `SUSPENDED` | The column and enum exist so the envelope shape is stable, but no transition writes them. `REVOKED` is written, and only by an operator-signed revocation. |
| Key rotation and key revocation | `KeyDocument` models `retired`/`revoked` correctly; no write path sets them, so every key is `active`. |

## Deprecated

Expand Down Expand Up @@ -126,20 +128,21 @@ Full reference, verified live: [docs/api.md](docs/api.md).

## Current data model

Supabase Postgres. Applied migrations (confirmed 2026-10-19 against production): `0001_registry_store`, `0003_rate_limit`, `0004_rate_limit_grants`, `0005_directory_listings`. `0002_onboarding_tables` is deliberately unapplied. RLS is enabled on every table; the four registry tables have public-read policies and all writes go through the service role. The rate-limit and directory tables are described in their migrations.
Supabase Postgres. Applied migrations (confirmed 2026-10-19 against production): `0001_registry_store`, `0003_rate_limit`, `0004_rate_limit_grants`, `0005_directory_listings`, `0006_revocations`. `0002_onboarding_tables` is deliberately unapplied. RLS is enabled on every table; the four registry tables have public-read policies and all writes go through the service role. The rate-limit and directory tables are described in their migrations.

| Table | Key | Contents |
|---|---|---|
| `agents` | `agent_id` (`agenid:<ULID>`) | `manifest` jsonb, `manifest_digest`, `proof` jsonb, `status` (checked against the five-value enum), `registered_at`, `updated_at` |
| `keys` | `key_id` (ULID) | `document` jsonb; `role` and `controller` are **generated columns** extracted from the jsonb (immutable, so they are legal generated expressions), indexed on `role` |
| `assertions` | `assertion_id` | `subject`, `level`, `key_id`, `document` jsonb, `manifest_digest` generated from the jsonb, `verified_at` as a **plain** timestamptz column, indexed on `subject` |
| `events` | `event_id` | Append-only ledger: `agent_id`, `type`, `occurred_at`, `detail_ref` jsonb — **pointers and hashes only, never raw evidence** |
| `revocations` | `revocation_id` | Operator-signed `Revocation` objects; at most one per agent; written only by the transactional `revoke_agent` function (migration `0006`) |

Relationships are by identifier rather than by foreign key: an assertion's `subject` names an agent, and both a proof and an assertion name a `key_id`. Verification never relies on referential integrity — it re-derives everything from signatures.

`verified_at` is deliberately **not** stored as `text`: `Rfc3339Utc` permits optional fractional seconds, and lexicographic order disagrees with chronological order across mixed precision. It is equally deliberately not a generated column cast from jsonb, which Postgres rejects as non-immutable (`42P17`).

Ledger event types: `agent.registered`, `key.published`, `assertion.issued`, `manifest.changed`, `agent.status_changed`, `agent.revoked`. Only the first two are written today.
Ledger event types: `agent.registered`, `key.published`, `assertion.issued`, `manifest.changed`, `agent.status_changed`, `agent.revoked`. `agent.registered`, `key.published` and `agent.revoked` are written today.

## Current integrations

Expand All @@ -160,7 +163,7 @@ Full treatment: [docs/trust-model.md](docs/trust-model.md) and [docs/threat-mode

**Controls in place.** Strict schema validation rejecting unknown members · Ed25519 signature and digest-binding verification on every write · key role and controller enforcement · bounded forward clock skew at registration with none at verification · operator keys generated and held client-side only, never transmitted or stored · RLS enabled on every table with public-read policies and service-role writes · append-only event ledger storing pointers, never evidence · a public-surface test suite that makes each honesty rule a build-breaking assertion.

**Known gaps.** No request-level authentication on public write endpoints (they are rate limited as of 2026-09-18, and every write is signature-verified, but nothing ties a live request to a registered agent) · no authorization model at all: verification levels say how carefully an identity was checked, never what an agent may do · no revocation path for an agent or a key · `/api/retell/agents` forwards a caller-supplied key to a third-party API from AgenID's domain — rate limited and refused unless same-origin, but a non-browser client can forge the origin headers · no trust root, so nothing above L1 can be signed · an operator who publishes no `.well-known` key copy leaves two-path discovery with a single source · no `security@` mailbox, because the `agenid.com` zone publishes no MX records.
**Known gaps.** No request-level authentication on public write endpoints (they are rate limited as of 2026-09-18, and every write is signature-verified, but nothing ties a live request to a registered agent) · no authorization model at all: verification levels say how carefully an identity was checked, never what an agent may do · agent revocation is operator-only, and its propagation to relying parties is unspecified · no key rotation or key revocation · `/api/retell/agents` forwards a caller-supplied key to a third-party API from AgenID's domain — rate limited and refused unless same-origin, but a non-browser client can forge the origin headers · no trust root, so nothing above L1 can be signed · an operator who publishes no `.well-known` key copy leaves two-path discovery with a single source · no `security@` mailbox, because the `agenid.com` zone publishes no MX records.

## Known limitations

Expand All @@ -171,7 +174,7 @@ Full treatment: [docs/trust-model.md](docs/trust-model.md) and [docs/threat-mode
5. **Public write endpoints are unauthenticated.** Every write is signature-verified and self-attributed and every POST route is rate limited (since 2026-09-18), but the bound is per client-address range, so a widely distributed source is limited only in proportion to its ranges. `/api/retell/bind` caps its array at 25.
6. **`packages/web` carries a second registry implementation** mirroring `@agenid/api`'s validation, because `zod` is not resolvable inside `packages/web`. Equivalence is held by test, not by shared code.
7. **`/onboarding/retell` cannot complete a one-click DNS setup.** Its `apply_url` is null until AgenID's Domain Connect service template is registered with providers, so the operator adds the TXT record by hand. This is the correct failure mode — the alternative was a link that 404s on someone else's dashboard — but it is a real gap in that flow.
8. **Agent lifecycle statuses are stored but never written.** Every agent is `ACTIVE`; there is no revocation or status-change path in production.
8. **Only one lifecycle transition is written: operator-signed revocation.** `CHANGED`, `STALE` and `SUSPENDED` are never set, nothing but the operator can revoke an agent, and how fast a revocation reaches a relying party that cached `ACTIVE` is not specified ([docs/revocation-propagation.md](docs/revocation-propagation.md) is a proposal).
9. **Some content is mirrored rather than shared** — the v1.1.1 schemas and §8 vectors are copies of `AgenID-protocol/spec` (the `spec-sync` CI job fails on any difference), and `content/partners/` and `content/onboarding.md` are copies of `docs/` (`check:docs` fails on any difference, since 2026-10-19). The copies are enforced, not hand-checked, but they are still copies.
10. **`/onboarding/retell` has never been rendered in a browser.** It has been exercised over HTTP and by grepping the shipped JS bundle only.
11. **A malformed percent-escape is a `500` on a self-hosted `next start`.** `/v1/keys/%ZZ` and `/v1/keys/%` make Next's own parameter decoding throw before the route handler runs, so the raw-target rule never gets to answer them. It cannot be fixed inside a route handler. Production is unaffected: Vercel's edge refuses those targets with its own plain `400 Bad Request` first — verified by sending the raw target directly, since curl will not transmit it.
Expand All @@ -192,6 +195,7 @@ Full treatment: [docs/trust-model.md](docs/trust-model.md) and [docs/threat-mode

| Date | Change |
|---|---|
| 2026-10-19 | **Operator-signed agent revocation** — the first lifecycle transition this deployment writes. Only the agent's own operator key has standing; status, stored revocation and ledger event are written in one transaction; the envelope carries the signed object; every public surface that said "no revocation flow" now says what is and is not deployed. Propagation semantics proposed in `docs/revocation-propagation.md` |
| 2026-10-19 | **Retell relay is same-origin only** — `/api/retell/agents` refuses cross-site and non-JSON requests before reading the body; a test asserts a refused request never reaches Retell. **Mirrored docs enforced** — `check:docs` fails if the site's onboarding guide or any partner brief drifts from `docs/`, or a package LICENSE from the root |
| 2026-09-28 | Editorial homepage redesign (`b2dff44`, PR #2); BugLoop feedback widget (`acd99ad`, PR #1) |
| 2026-09-23 | **Opt-in agent directory (S9)** — `/agents` and `/api/v1/directory`; operator-signed, replay-bounded listing consent; migration `0005` applied; registration alone lists nothing |
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Read [PROJECT_STATE.md](PROJECT_STATE.md) for the authoritative, dated breakdown
| Identifier, canonicalization, schemas, Ed25519 proof engine (`@agenid/core`) | **Production** — conformance vectors passing |
| Browser issuance → `L1_REGISTERED`, durable registry, resolution, badges | **Production** — verified live on `www.agenid.com` against Supabase |
| Two-path key discovery — `GET /v1/keys/<key-ulid>` plus the operator's `.well-known` copy | **Production** — both paths resolvable |
| Agent revocation — operator-signed, permanent, carried in the envelope | **Production** — `POST /api/v1/agents/{agenid}/revocation` and `/revoke`; key rotation and key revocation are **not deployed** |
| Operator CLI (`@agenid/cli`), MCP server (`@agenid/mcp-server`) | **Implemented**, not published to npm |
| Assertion issuance (`L2`–`L4`) | **Not deployed** — blocked on the root authority key ceremony |
| `L5` | **Reserved by the spec. Not issuable.** |
Expand Down Expand Up @@ -159,6 +160,7 @@ Stated plainly, because a trust product that hides these is not a trust product:
| [docs/trust-model.md](docs/trust-model.md) | What a verifier must trust, what they can check themselves, and the verification levels |
| [docs/threat-model.md](docs/threat-model.md) | Adversaries, assumptions, what the protocol defends against, and what it does not |
| [docs/api.md](docs/api.md) | Endpoint reference, request and response shapes, error codes, deployment status |
| [docs/revocation-propagation.md](docs/revocation-propagation.md) | **Proposal, not normative** — how long a relying party may rely on an `ACTIVE` answer, and what to do when the registry is unreachable |
| [docs/OPERATOR_ONBOARDING.md](docs/OPERATOR_ONBOARDING.md) | End-to-end operator walkthrough |
| [docs/ECOSYSTEM-LOGO-REGISTRY.md](docs/ECOSYSTEM-LOGO-REGISTRY.md) | The registry behind every third-party name on the site: statuses, evidence rules, asset policy, and the tests that keep compatibility from reading as partnership |
| [docs/brand-compliance-audit.md](docs/brand-compliance-audit.md) | Sixteen providers' trademark terms reviewed against displaying their marks here, and the machine-readable record that enforces the answer |
Expand Down
Loading
Loading