Skip to content

feat: the agentic surface — capabilities, WebMCP, agent trust, and llms.txt discovery - #211

Open
JoviDeCroock wants to merge 38 commits into
mainfrom
JoviDeCroock/webmcp-agentic-ui-vision
Open

feat: the agentic surface — capabilities, WebMCP, agent trust, and llms.txt discovery#211
JoviDeCroock wants to merge 38 commits into
mainfrom
JoviDeCroock/webmcp-agentic-ui-vision

Conversation

@JoviDeCroock

Copy link
Copy Markdown
Owner

Summary

  • Executes the agentic-first direction by combining feat: capability core and WebMCP projection #209 (capability core + WebMCP projection), feat: agent trust layer — Web Bot Auth, destructive confirmation flow, pracht eval #210 (agent trust layer), and feat: opt-in llms.txt emission from the resolved app graph #208 (llms.txt emission) into one branch — supersedes those three PRs — and adds the integration work they couldn't do independently:
    • llms.txt now lists capabilities: buildLlmsTxt() emits a ## Capabilities section with each HTTP-exposed capability's dispatch endpoint, effect class, and description; destructive capabilities are annotated requires confirmation. On by default, excludable via llmsTxt: { include }, byte-identical output for apps without capabilities. Agents discovering a site through llms.txt now find its callable operations, not just its pages.
    • Docs site (examples/docs): new /docs/capabilities and /docs/agent-trust pages, a new "Agents" sidebar section (LLM Content, Capabilities, Agent Trust), entries in the agent-markdown index served at / with Accept: text/markdown, and an updated LLM content page covering the built-in llmsTxt option alongside the site's curated plugin. Both pages prerender and appear in the site's own llms.txt automatically.
    • examples/basic links the /notes capability demo from the public shell nav so the demo is discoverable.
    • README/VISION_MVP/docs cross-references unified (llms.txt ↔ capabilities ↔ agent trust).
  • No linked issue; direction discussed in docs: capability graph #207.

Testing

  • pnpm e2e — 94 passed (includes new assertions: llms.txt Capabilities section in the Node build of examples/basic)
  • pnpm format
  • pnpm lint — the repo lint script is broken (missing eslint); ran npx oxlint .: 0 warnings, 0 errors (pnpm typecheck also passes)
  • pnpm test — 646 passed (new: capabilities-section unit tests for buildLlmsTxt, plugin include-option validation)

Checklist

🤖 Generated with Claude Code

@JoviDeCroock
JoviDeCroock force-pushed the JoviDeCroock/webmcp-agentic-ui-vision branch 2 times, most recently from b80ddc4 to 01089b3 Compare July 11, 2026 12:12
@JoviDeCroock
JoviDeCroock force-pushed the JoviDeCroock/webmcp-agentic-ui-vision branch 3 times, most recently from 63e18a7 to c4ba997 Compare July 22, 2026 15:54
JoviDeCroock and others added 25 commits July 25, 2026 18:57
Add the first stage of the capability graph: typed, protocol-neutral
application operations registered in the app manifest and projected to
direct server invocation, a generated HTTP endpoint, and WebMCP page
tools for in-browser agents.

- New @pracht/capabilities package: defineCapability() with a
  dependency-free JSON Schema subset validator, effect classes
  (read/write/destructive), named middleware, and explicit exposure.
  Unsupported schema keywords, webmcp-without-http, and any exposure of
  destructive capabilities fail fast at definition time.
- @pracht/core: capability registry/resolver, invokeCapability() for
  loaders/API routes, HTTP dispatch at POST /api/capabilities/<name>
  with a typed { ok, data | error } envelope (400 validation with
  path-scoped issues, middleware 401/403, redacted 500s, same-origin
  CSRF), and a capabilities section in buildAppGraph()/devtools.
- @pracht/vite-plugin: virtual:pracht/capabilities (browser
  callCapability over the HTTP projection) and virtual:pracht/webmcp
  (feature-detected shim targeting document.modelContext.registerTool,
  Chrome 150+ origin trial, navigator.modelContext fallback) — both
  generated from static manifest analysis, zero bytes when unused, and
  wired into both the full-hydration client entry and the islands
  bootstrap. Capability modules stay server-only.
- @pracht/cli: pracht inspect capabilities, inspect_capabilities MCP
  tool, and pracht verify contract checks (exposed capabilities need
  description/input/output/effect; destructive cannot be exposed).
- examples/basic: notes.search (read, http+webmcp) and notes.create
  (write, http) with a /notes route using both invocation paths.
- Tests: validator/pipeline/registry/path-mapping unit tests, codegen
  extraction tests, verify-check tests, capability e2e suite (HTTP,
  loader, browser, WebMCP smoke via a faked document.modelContext),
  and client-bundle isolation assertions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, pracht eval

Builds the trust layer on top of the capability core (#209):

- Web Bot Auth (RFC 9421 HTTP Message Signatures per
  draft-meunier-web-bot-auth-architecture-02): hand-rolled minimal RFC 8941
  parsing, Ed25519 via WebCrypto, static keys plus allowlisted
  /.well-known/http-message-signatures-directory JWKS fetching with a TTL
  cache. Opt-in via defineApp({ agents: { webBotAuth } }); identity surfaces
  as context.agent with observe/require policies (per-capability agentPolicy
  override → 401 agent_required envelope). Fail closed everywhere.
- Destructive capabilities may now expose.http behind a server-verified
  prepare/commit confirmation flow: 409 confirmation_required with a
  short-lived HMAC token bound to principal + capability + canonicalized
  input; commit via x-pracht-confirm with byte-identical input; tampered/
  expired/mismatched tokens → 403. Requires PRACHT_CONFIRMATION_SECRET
  (pracht verify enforces); optional best-effort in-memory single-use cache.
  webmcp/mcp exposure of destructive capabilities stays disallowed in v1.
- Capability audit trail: structured event per dispatch (capability, effect,
  transport, outcome, status, duration, agent) via setCapabilityAuditHook()
  or the onCapabilityAudit runtime option.
- pracht eval: scripted agent-task harness running evals/**/*.eval.json
  against the capability HTTP projection, with $steps[n].<path> references,
  subset output matching, human transcript or --json, exit 1 on failure.
- examples/basic: notes.purge (destructive), agent.whoami, agent.ping
  (require), a working eval scenario; e2e coverage for signed/unsigned
  agents, policy modes, the confirmation flow, and the eval CLI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a `llmsTxt` option to the pracht vite plugin (https://llmstxt.org).
When enabled, `pracht build` writes dist/client/llms.txt generated from
the resolved app graph and the dev SSR middleware serves /llms.txt live.

- @pracht/core: new `buildLlmsTxt()` on @pracht/core/server. Static
  routes are always listed; dynamic routes only when SSG/ISG with
  getStaticPaths() (one entry per prerendered instance). Routes with a
  server-only `markdown` export are annotated as markdown-capable; API
  routes list their detected methods. Output is deterministic.
- @pracht/vite-plugin: `llmsTxt: false | { title?, description?,
  origin?, include? }` (title/description fall back to package.json);
  the server module exports generateLlmsTxt only when enabled, so
  disabled builds are byte-for-byte unchanged.
- @pracht/cli: `pracht build` writes dist/client/llms.txt, picked up as
  a static asset by the Node, Cloudflare (ASSETS binding), and Vercel
  (Build Output filesystem route) adapters without adapter changes.
- create-pracht: generated apps enable `llmsTxt: {}` by default.
- Enabled in examples/basic, examples/pages-router, examples/cloudflare;
  unit tests for the generator, plugin option validation, and codegen;
  e2e coverage for dev serving and all three build outputs.
- Docs: docs/LLMS_TXT.md plus a README bullet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…docs-site pages, example wiring

Combines the capability core + WebMCP projection, the agent trust layer,
and llms.txt emission into one branch, and makes the three features tell
one story:

- buildLlmsTxt() gains a '## Capabilities' section listing HTTP-exposed
  capabilities with dispatch endpoint, effect class (destructive ones note
  the confirmation requirement), and description; on by default, excludable
  via llmsTxt.include, zero output for apps without capabilities.
- examples/docs gains /docs/capabilities and /docs/agent-trust pages, an
  'Agents' sidebar section, agent-markdown index entries, and an updated
  LLM content page covering the built-in llmsTxt option.
- examples/basic links the /notes capability demo from the public shell nav.
- README/VISION_MVP/docs cross-references unified.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- New docs-site page /docs/agents (The Agentic Web) leading the Agents
  sidebar section: the two-users narrative, one-contract-four-callers,
  the trust layer, a runnable five-minute tour, and what comes next
- Testing recipe grows a 'Testing Capabilities & Agent Surfaces' section:
  unit testing run() and the attached validateInput/validateOutput,
  Playwright tests for the HTTP projection and prepare/commit flow,
  faking document.modelContext for WebMCP, signing Web Bot Auth requests,
  and pracht eval in CI
- examples/basic README documents the five-capability demo, the
  PRACHT_CONFIRMATION_SECRET requirement, and the eval scenario
- Cross-links from Capabilities/Agent Trust pages and the home route's
  agent-markdown index; prev/next chain now routes through /docs/agents

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… dev banner capabilities

Fixes the four DX gaps found while evaluating how easy agent surfaces are
to test:

- @pracht/core: createCapabilityTestHost() runs the full dispatch pipeline
  in unit tests without a server. invoke() mirrors invokeCapability();
  request() mirrors the HTTP projection including Web Bot Auth policy
  (simulated agent identities via the agent option — no RFC 9421 signing)
  and the destructive prepare/commit confirmation flow. invokeCapability's
  body is extracted into invokeCapabilityOnHost and shared;
  resolveRegistryModule is now public. Browser stub added.
- @pracht/cli: pracht eval --start "<command>" launches the app, polls
  --url until it answers, runs the scenarios, and stops the whole process
  group — no second terminal. waitForServer() is unit-tested; an e2e test
  proves launch, run, and teardown against a fixture server.
- @pracht/cli: the pracht dev banner now prints a Capabilities table
  (name, effect, exposure, dispatch path) when the app registers any, and
  dev accepts --port (legacy positional and  still work — the flag
  was previously parsed as an unknown boolean and silently ignored).
- Fixed pracht inspect capabilities (and the banner + MCP
  inspect_capabilities) reporting effect=n/a transports=private for every
  capability: manifest-relative paths now load through the virtual server
  module's registry instead of raw ssrLoadModule.

Docs updated across the site (CLI, Agent Trust, Capabilities, Testing
recipe, agents story page), internal docs (AGENT_TRUST.md — auto-start
moved out of 'Not built yet' — and CAPABILITIES.md), and examples/basic.

Verified: 664 unit tests, 95 e2e, typecheck, oxlint, format, docs-site
build all green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hardening docs

pracht typegen now generates capability input/output TypeScript types from
the capability graph's JSON Schemas into src/pracht-capabilities.d.ts,
registered on Register["capabilities"]. invokeCapability(), the browser's
callCapability(), and createCapabilityTestHost().invoke() gain typed
overloads that infer both sides from the capability name; the untyped
invokeCapability<Output>() form keeps working for unregistered names. The
app graph (inspect capabilities --json, MCP inspect_capabilities) now
includes each capability's schemas. examples/basic checks in the generated
declaration and drops its explicit generics, so the monorepo typecheck
continuously exercises the inference path.

docs/CAPABILITY_GRAPH.md joins the branch as the design record: status
updated to accepted, plus a decision log answering the proposal's nine
spike questions and recording the delivery-plan deviations (destructive
shipped in v1 behind prepare/commit, WebMCP landed before remote MCP,
pracht eval speaks HTTP). Supersedes the copy on PR #207.

AGENT_TRUST.md gains an "Operational hardening" section documenting the
middleware/app-level story for rate limiting, write idempotency, and
result-size limits, and lists them under "Not built yet" so they stay
visible.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rms, effect-driven revalidation

- @pracht/capabilities now owns the wire contract (capabilityHttpPath,
  confirmation/transport headers, CapabilityErrorCode union, envelope types,
  schema->TS printer) plus the shared static extractor
  (@pracht/capabilities/static) consumed by the framework, the vite plugin,
  and the CLI — replacing three copies of the path formula and two drifted
  copies of the extractor.
- <Form capability> posts the framework form component to the capability
  endpoint agents call, with server-side schema coercion of form fields and a
  no-JS 303-back fallback; typed onCapabilityResult via typegen.
- Effect classes drive the client cache: successful non-read browser calls
  (callCapability / Form) revalidate route data via a settled event.
- context.agent is typed end to end (CapabilityContext, PrachtRequestContext);
  audit events distinguish the webmcp transport; expose.mcp is labeled
  unserved in verify and the dev banner; eval steps gain "confirm" sugar.
- Consolidate the branch's 11 changesets into one net-new-feature changeset;
  decision log updated with the consolidation pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rebasing onto main crossed the capability work with the API-route typegen
and declaration rename that landed there:

- `pracht dev`'s watch-mode typegen now passes `capabilitiesOut` (required
  since capability declarations were added) and treats the generated
  capability declaration as a generated path, so writing it does not
  retrigger regeneration.
- Export `DEFAULT_CAPABILITIES_OUT` alongside the other typegen defaults.
- Update the capability typegen test to the renamed declaration output
  (`src/pracht.d.ts`, not `src/pracht-routes.d.ts`).
- Reformat `FormProps` after merging the capability and Standard Schema
  form props.
Fixes 19 issues surfaced reviewing the capability graph work, spanning
validation bypasses, a confirmation-flow gap, static-extraction
correctness, dev/build mismatches, and CLI robustness. See the changeset
for the per-package breakdown. Adds regression tests for the
security-relevant fixes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
JoviDeCroock and others added 3 commits July 25, 2026 18:58
Follow-up to the review-fixes commit: the default-export detection
regressed three valid module shapes to a silent null (which drops the
capability from codegen/verify). Accept `export default <id>` without a
trailing semicolon (ASI), `export { <id> as default }`, and
arrow-typed const declarations, and fall back to the sole call site
when a module has exactly one defineCapability() call.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pracht typegen --check requires generated files to byte-match the
emitter's output (quoted keys, single-line types) while oxfmt --check
requires prettier-style formatting — the two can never both hold, and
the formatter rewriting the generated file is what caused the original
committed drift. Generated files are not formatter-governed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ysis

- Run validateAgentsConfig() outside the VALIDATE_MANIFEST guard: Vite
  compiles import.meta.env.DEV to false in production server/edge
  bundles, which stripped the security validation and let a typo'd
  policy fail open again.
- Mask comments before locating the manifest capabilities block so a
  block-commented example cannot shadow the live registry in verify.
- Resolve the default-exported capability to the module-scope binding
  (brace-depth 0), not a shadowed inner declaration.
- Make template-literal scanning interpolation-aware so a nested
  template in run() no longer truncates extraction and silently
  privatizes an exposed capability.
- Extend the typegen collision guard to basename collisions between the
  declaration and capabilities outputs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@JoviDeCroock
JoviDeCroock force-pushed the JoviDeCroock/webmcp-agentic-ui-vision branch from 3a259b9 to dd7c96d Compare July 25, 2026 17:01
JoviDeCroock and others added 10 commits July 25, 2026 20:10
- `pracht verify` resolves root-relative capability registrations
  (`() => import("/src/capabilities/x.ts")`) against the project root,
  matching the runtime registry and the Vite plugin. They previously
  resolved against the manifest directory, never existed on disk, and
  were skipped without a check or a warning — silently bypassing the
  destructive-exposure and PRACHT_CONFIRMATION_SECRET checks while
  verification still reported success. A root-relative reference that is
  genuinely missing is now an error, since the manifest check only covers
  "./"-relative paths.

- `coerceFormInput()` looks up a field's schema with an own-property
  check and writes coerced values with `Object.defineProperty`, so a form
  field named `__proto__` or `constructor` can neither pick up an
  inherited member as its schema nor vanish into the prototype setter.
  Such fields now reach validation as real own properties and are
  rejected by `additionalProperties: false` instead of being dropped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The four patch changesets all fixed capability work introduced by the
minor changeset on this same branch, so they never needed their own
release-note entries. Fold the still-user-visible behaviors (HTTP path
safety, regex/default-export-aware static extraction, submitter
formaction and middleware redirects in enhanced forms, islands
revalidation, the confirmation gate running inside the middleware chain)
into the capability graph changeset and drop the rest.

Co-Authored-By: Claude Opus 5 (1M context) <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