Agent-native by design

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.

agent session
# 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 you

What is exposed

CapabilityEffectExposureNotes
projects.searchreadhttp · webmcp · mcpFind projects by name or summary. Safe to call freely.
projects.createwritehttp · webmcp · mcpCreate a project. Rate limited per principal by named middleware.
projects.deploywritehttp · webmcp · mcpShip a build. Takes an idempotencyKey so retries do not double-deploy.
projects.archivedestructivehttp onlyTwo-phase, and refused until a human approves the proposal.
agent.whoamireadhttp · webmcp · mcpEchoes the verified Web Bot Auth identity, or verified: false.
agent.briefreadhttp · mcpagentPolicy: 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.

remote mcp
# 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"# → 403

The 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 transcript
  • node scripts/agent.mjs --unsigned — watch agent.brief 401
  • pracht eval --start "pracht preview" — the same flow as a CI check
  • pracht inspect capabilities --json — the graph, with schemas