The finished application is
the tool surface.
Not "an agent can help you build this app" — that is a dev-time story every framework has now. This is the deployed product being usable by an agent, with the same rules, the same endpoints, and a trust layer that does not depend on the caller being honest.
# This page, as source content, from the same URL
curl -H "Accept: text/markdown" https://launchpad.example/agents
# A read capability — no ceremony
curl -X POST .../api/capabilities/projects/search -d '{"query":"api"}'
# A destructive one — token first, then a human
curl -X POST .../api/capabilities/projects/archive -d '{"projectId":"corvus"}'
# → 409 confirmation_required { confirmationToken, approvalId }
curl -X POST ... -H "x-pracht-confirm: $TOKEN" -d '{"projectId":"corvus"}'
# → 409 confirmation_pending — a person decides, not youWhat is exposed
| Capability | Effect | Exposure | Notes |
|---|---|---|---|
projects.search | read | http · webmcp · mcp | Find projects by name or summary. Safe to call freely. |
projects.create | write | http · webmcp · mcp | Create a project. Rate limited per principal by named middleware. |
projects.deploy | write | http · webmcp · mcp | Ship a build. Takes an idempotencyKey so retries do not double-deploy. |
projects.archive | destructive | http only | Two-phase, and refused until a human approves the proposal. |
agent.whoami | read | http · webmcp · mcp | Echoes the verified Web Bot Auth identity, or verified: false. |
agent.brief | read | http · mcp | agentPolicy: require — verified agents only, on every transport. |
mcp tools are served at POST /mcp — stateless Streamable HTTP, with tools/list projected from this same graph and dots replaced by underscores (projects_search). projects.archive is absent from that list by construction: the projection filters destructive capabilities out however they are declared. Drop agents.mcp from the manifest and the exposures stay in the graph but the dev banner prints mcp(unserved), so a declared-but-dead transport is never mistaken for a live one.
# One endpoint, no session handshake
curl -sX POST https://launchpad.example/mcp -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# → projects_search, projects_create, projects_deploy, agent_whoami, agent_brief
# (no projects_archive — destructive is filtered out)
# A cookie, or an Origin header, is a 403 — not a login
curl -sX POST .../mcp -H "cookie: session=demo" … # → 403The tool list is curated, not scraped
Six capabilities are registered in src/routes.ts. Nothing else in this app is reachable as a tool, however many routes and API endpoints it grows.
The contract is the same one humans use
The dashboard's create form and an agent's POST hit one endpoint, one schema, one middleware chain. A rule cannot be enforced for one audience and skipped for the other.
Identity is cryptographic
Sign with RFC 9421 and context.agent carries your verified key id. Fail any check — expiry, covered components, an untrusted keyid — and it is null, never partial.
Consent is not something the caller can assert
A destructive call is refused until a person decides, out of band, in the application's own inbox. Holding the confirmation token is not approval.
Run it yourself
A signed agent, in one command
node scripts/agent.mjs derives an Ed25519 key whose public half is pinned in this app's manifest, signs every request per RFC 9421, and walks the whole flow: identity, the verified-only brief, search, create, an idempotent double deploy, and an archive that stops dead waiting for a human.
node scripts/agent.mjs— the full transcriptnode scripts/agent.mjs --unsigned— watchagent.brief401pracht eval --start "pracht preview"— the same flow as a CI checkpracht inspect capabilities --json— the graph, with schemas