JoviDeCroock/pracht/skills/audit-agent-surface/SKILL.md
audit-agent-surface
Inventory what agents can reach in a pracht app — capability exposure (HTTP, WebMCP, remote MCP), `agents` trust config, the destructive-confirmation gate, `llms.txt`, Markdown negotiation, OpenAPI — and report where the surface is wider than intended, or confirm an opt-out app ships none. Use for "audit the agent surface", "what can agents do on my site", "is my MCP endpoint safe", "did this PR widen what agents can reach".
- Source repository stars
- 94
- Declared platforms
- 0
- Static risk flags
- 0
- Last source update
- 2026-08-28
- Source checked
- 2026-08-28
Decision brief
What it does: where it fits
Pracht's agent surface is opt-in end to end (docs/CAPABILITIES.md, docs/AGENTTRUST.md, docs/REMOTEMCP.md). State that baseline before auditing the opt-outs:
Not for
- Tasks that require unconfirmed production actions or broad system permissions.
- Environments where the pinned source and install steps cannot be inspected.
Compatibility matrix
Platform support, with evidence labels
| Platform | Status | Evidence | What to check |
|---|---|---|---|
| Codex | Not declared | No explicit evidence | Portability before use |
| Claude Code | Not declared | No explicit evidence | Portability before use |
| Cursor | Not declared | No explicit evidence | Portability before use |
| Gemini CLI | Not declared | No explicit evidence | Portability before use |
Installation
Inspect first. Install second.
The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.
npx skills add https://github.com/JoviDeCroock/pracht --skill "skills/audit-agent-surface"Inspect the Agent Skill "audit-agent-surface" from https://github.com/JoviDeCroock/pracht/blob/43a8e8dcccb137abcc7c78d4a238edc01829e5a9/skills/audit-agent-surface/SKILL.md at commit 43a8e8dcccb137abcc7c78d4a238edc01829e5a9. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.
Workflow
What the source asks the agent to do
- 01
Step 1: Inventory the declared surface
inspect agents reports webBotAuth, confirmation policy, MCP endpoint and OAuth policy, llmsTxt, each capability's effect/policy/transports/path, and exposure counts (private means unexposed). Use inspect capabilities for schemas and middleware.
inspect agents reports webBotAuth, confirmation policy, MCP endpoint and OAuth policy, llmsTxt, each capability's effect/policy/transports/path, and exposure counts (private means unexposed). Use inspect capabilities fo…Build the inventory table: capability → effect → transports → HTTP path → middleware → agentPolicy. A capability reported as unreadable means @pracht/capabilities is not installed; treat it as an error and stop reasonin…Cross-check inspect agents against the manifest's agents block. It reads resolved app and production llmsTxt config, including computed branches. A null llmsTxt.enabled means an older plugin: report unknown and recommen… - 02
Step 2: Exposure vs. intent
For every exposed capability, ask whether the exposure is deliberate:
expose.mcp set but no agents.mcp configured — declared, served byexpose.mcp set on an operation whose authorization relies on a browserexpose.webmcp — the in-page agent acts as the signed-in user in their tab. - 03
Step 2b: The /mcp auth posture
agents: { mcp: {} } without auth is open; authorization rests entirely on each tool's named middleware. Report an exposed tool with no middleware as error; otherwise warn and name the middleware carrying the boundary.
resource is canonical HTTPS (loopback HTTP only), has no query, fragment,Every authorizationServers issuer is canonical HTTPS without query orverify is a module reference under src/server, src/middleware, or - 04
Step 3: The destructive gate
webmcp on a destructive capability is rejected by the framework — if you
webmcp on a destructive capability is rejected by the framework — if youmcp on a destructive capability is a served remote tool only when thePRACHTCONFIRMATIONSECRET must be set in the server environment for each - 05
Step 4: Identity and policy
agents.webBotAuth.policy: "require" gates capability HTTP endpoints only —
agents.webBotAuth.policy: "require" gates capability HTTP endpoints only —directories is an allowlist; an empty one means no directory fetching at allagentPolicy: "require" on a capability while webBotAuth is unconfigured
Permission review
Static risk signals and limitations
No configured static risk pattern was detected
This is not proof of safety. Runtime behavior, indirect dependencies, and hidden external systems are outside the static scan.
Evidence record
Why each signal appears
| Signal | Value | Evidence type | Meaning |
|---|---|---|---|
| Quality score | 92/100 | Computed | Documentation, specificity, maintenance, and trust rules |
| Repository stars | 94 | Source | Repository attention, not individual Skill quality |
| Compatibility | 0 platforms | Source | Declared in the catalog source record |
| Usage guide | automated source guide | Editorial | Generated or reviewed according to the visible evidence level |
Pinned source
Provenance and original SKILL.md
- Repository
- JoviDeCroock/pracht
- Skill path
- skills/audit-agent-surface/SKILL.md
- Commit
- 43a8e8dcccb137abcc7c78d4a238edc01829e5a9
- License
- MIT
- Collected
- 2026-08-28
- Default branch
- main
View the original SKILL.md
Pracht Audit Agent Surface
Pracht's agent surface is opt-in end to end (docs/CAPABILITIES.md,
docs/AGENT_TRUST.md, docs/REMOTE_MCP.md). State that baseline before
auditing the opt-outs:
- No loader or API route is ever inferred as a capability; a capability without
exposeis unreachable over the network. destructivecapabilities may be exposed over HTTP and remote MCP, never as WebMCP page tools, and every dispatch is confirmation-gated.- Remote MCP rejects cookie-bearing and browser-originated requests, and serves
destructive capabilities only with
agents.mcp.destructiveplus a registered approval store — otherwise it filters them out at serve time.agents.mcp.authadditionally makes it an OAuth 2.0 protected resource; without it the endpoint is open and authentication is the capability middleware's job. - An app that registers neither capabilities nor
agentshas the dispatch path and Web Bot Auth verifier dropped from its server bundle at build time.
This skill reports; it never mutates. Prerequisites: pracht inspect needs a
vite config registering the pracht plugin. If the pracht MCP server is
registered (see docs/MCP.md), prefer its tools (inspect_agents,
inspect_capabilities, inspect_routes, inspect_api, doctor, verify)
over shelling out.
Step 1: Inventory the declared surface
pracht inspect agents --json # the whole configured surface in one call
pracht inspect capabilities --json # name, effect, transports, HTTP path, middleware, schemas
pracht inspect routes --json # markdown negotiation, hydration, middleware
pracht inspect api --json
pracht verify --json # contract, exposure, and projection checks
inspect agents reports webBotAuth, confirmation policy, MCP endpoint and
OAuth policy, llmsTxt, each capability's effect/policy/transports/path, and
exposure counts (private means unexposed). Use inspect capabilities for
schemas and middleware.
Build the inventory table: capability → effect → transports → HTTP path →
middleware → agentPolicy. A capability reported as unreadable means
@pracht/capabilities is not installed; treat it as an error and stop
reasoning about its policy until it loads.
Cross-check inspect agents against the manifest's agents block. It reads
resolved app and production llmsTxt config, including computed branches. A
null llmsTxt.enabled means an older plugin: report unknown and recommend an
upgrade. Use resolved mcp.auth; null means framework-level OAuth is open.
Step 2: Exposure vs. intent
For every exposed capability, ask whether the exposure is deliberate:
expose.mcpset but noagents.mcpconfigured — declared, served by nothing.pracht verifywarns; report it so the intent gets resolved.expose.mcpset on an operation whose authorization relies on a browser session — remote MCP rejects cookies, so the only credentials it sees are the forwardedAuthorizationheader andcontext.agent. A middleware chain that reads a session cookie authorizes nobody there.expose.webmcp— the in-page agent acts as the signed-in user in their tab. Confirm that is intended for every one, and that the route's hydration is not"none"(which registers no tools). Flag a webmcp capability whose effective agent policy is"require"(capability-level, or inherited fromagents.webBotAuth.policy): page-tool calls are unsigned browser fetches, so the tool is dead — every call 401s. Also check that capabilities returning user-generated or third-party content setexpose.webmcp: { untrustedContent: true }so hosts treat the output as untrusted.- Private capabilities used as building blocks:
invokeCapability()runs their named middleware but not app-levelapi.middleware. Their named middleware is the only authorization seam — flag private capabilities with an emptymiddlewarelist that touch sensitive data. - Custom
expose.http.pathvalues that land outside/api/**and therefore escape path-scoped middleware or host rules. - Declared vs. actually served:
expose.mcpin source is what the graph claims. Apracht evalscenario with"transport": "mcp"proves what the endpoint answers — it performs a realinitializehandshake and issues each step as atools/call. When the endpoint hasmcp.auth, set scenario-levelmcpHeaders.authorizationso the token is sent on the handshake and every later request; do not commit a production token. Run it only against a local throwaway server, and only withreadsteps. If the app ships MCP-exposed capabilities with no such scenario, report the missing proof: an HTTP-only scenario says nothing about whether an MCP host can reach the tool.
Step 2b: The /mcp auth posture
agents: { mcp: {} } without auth is open; authorization rests entirely on
each tool's named middleware. Report an exposed tool with no middleware as
error; otherwise warn and name the middleware carrying the boundary.
With agents.mcp.auth, check:
resourceis canonical HTTPS (loopback HTTP only), has no query, fragment, or non-root trailing slash, and exactly identifies the endpoint./may identify the deployed root; the origin-root identifier is slashless. Aliases, query variants, and trailing slashes must 308 toresourcebefore challenge.- Every
authorizationServersissuer is canonical HTTPS without query or fragment. Reject unknownagents.mcp/authkeys and API-route collisions. verifyis a module reference undersrc/server,src/middleware, orsrc/capabilities, resolves uniquely, and default-exports a function. Inline, missing, ambiguous, or non-callable verifiers areerror. Its request clone may consume the body without consuming later JSON-RPC dispatch. Overlapping source directories may register the same normalized file more than once; that is one verifier, not ambiguity. Ablockedinspection reason for an unusable verifier is conclusive because the adapter server entry cannot replace the configured module reference.- The verifier binds token audience to
resource; otherwise tokens for another service authenticate here (error). RequirerequiredScopesor per-tool checks ofcontext.tokenAuth.scopes(warnotherwise). The initial challenge must advertise required scopes; scope tokens follow OAuth's printable-ASCII grammar.context.tokenAuthis MCP-only and nested calls cannot replace it. - Under base
/app/, resource includes/app/mcpwhile metadata stays at the origin-root/.well-known/oauth-protected-resource/app/mcp; fetch it and its bare alias, ensuring app/static routes cannot shadow either. The bare well-known path is reserved and its CORS-open metadata is expected; flag only sensitive scope names. CapabilityAuditEventrecords Web Bot Authagent, nottokenAuth. Report this asinfo, orwarnwhen per-account attribution is required and no middleware/capability forwards the principal to an audit sink.
Step 3: The destructive gate
webmcpon adestructivecapability is rejected by the framework — if you find it in source, the build is failing.mcpon adestructivecapability is a served remote tool only when the manifest setsagents: { mcp: { destructive: true } }. Report it as a deliberate widening and check both halves: the opt-in, and asetCapabilityApprovalStore()call the running server actually executes (imported by a server entry, a capability module, or applied API/capability middleware — a module nothing imports registers nothing). Opt-in without a store is anerrorin your report: the endpoint refuses to serve at all, andpracht verifyonly warns (its source scan cannot see a registration in a workspace package, so it must not hard-block). Two more preconditions fail the endpoint the same way — a missingPRACHT_CONFIRMATION_SECRET, andmode: "human"with neitheragents.webBotAuthwith a valid 32-byte base64url Ed25519 static key or HTTPS directory nor a principal resolver — so check all three together. Runtime-backed/_prachtreports a verified endpoint-wide failure by marking every MCP exposuremcp(unserved). Graph-onlypracht dev,pracht inspect capabilities,pracht inspect agents, and MCP inspection usemcp(unverified)when the same missing preconditions may be registered by the adapter server entry they deliberately skip. JSON inspection exposesmcpEndpoint,mcpDestructive,mcpRuntimeStatus, andmcpUnavailableReasons; use those fields instead of treating a declaredmcptransport as proof of reachability. These surfaces load applied setup middleware modules without executing the middleware functions. Destructiveexpose.mcpwithout the opt-in is dead exposure: the tool is invisible, andpracht verifywarns.PRACHT_CONFIRMATION_SECRETmust be set in the server environment for each deployment target (build environment too on Vercel, since it becomes the bypass token there). Missing → every destructive call answers403 confirmation_unavailable.- Record the honest limits in the report: the stateless HMAC token is replayable
within its TTL (default 120 s), the calling agent can hand the token back to
itself, and without Web Bot Auth or
setCapabilityApprovalPrincipalResolver()both phases run as"anonymous". Flagconfirmation: { singleUse: true }used as if it were durable — it is a per-instance in-memory cache, lost on restart. - If an approval store is registered, confirm its backend supports atomic
conditional writes and that all replicas share it: with a store registered, a
token whose proposal is unknown is refused, so a per-instance store breaks
commits.
createSqlApprovalStore()over D1/Postgres/Turso qualifies;createMemoryApprovalStore()in a deployed multi-replica app does not, and neither does a hand-rolled store over Cloudflare KV. confirmation: { mode: "human" }without both a store and an authenticated principal fails closed — check both exist.
Step 4: Identity and policy
agents.webBotAuth.policy: "require"gates capability HTTP endpoints only — pages and API routes are not gated. Flag any assumption that it protects pages.directoriesis an allowlist; an empty one means no directory fetching at all (deliberate SSRF protection). Flag a directory origin that is not the agent ecosystem endpoint the app intends to trust.agentPolicy: "require"on a capability whilewebBotAuthis unconfigured answers 401 for every caller — a loud misconfiguration, report aserror.- Note the replay property: Pracht's stateless verifier does not enforce
nonceuniqueness, and the default covered components (@authority,signature-agent) bind a signature to a host, not to a method, path, or body. Treat a verified identity as authentication, not per-request authorization. - Confirm an audit sink exists (
setCapabilityAuditHook(),addCapabilityAuditListener(), oronCapabilityAudit) — without one there is no record of who called what. Grep for all three;setCapabilityAuditHookis a single slot, so two calls to it mean one sink is silently dead — report that as awarnand point ataddCapabilityAuditListener(name, hook). Also flag a computed or non-constant sink name: same-name registration is what makes the call idempotent under dev HMR. A module-scope listener must also register its unsubscribe withimport.meta.hot.dispose(); otherwise removing the module or renaming the sink leaves the old registration active until the dev server restarts. - Know what the trail does not cover before treating it as a security record: a cross-origin 403, an unknown-capability 404, and an unknown or unexposed MCP tool name all return before dispatch and emit no event. An agent enumerating tool names leaves no trace, so never conclude "nothing tried" from an empty trail — that question belongs to the HTTP access log.
- To see the surface actually being exercised rather than merely declared, run
the app with
pracht dev, drive the capability, and read the Agents section of/_pracht(JSON underagentTrafficat/_pracht.json). It records transport,viafor nested composition, verified identity, outcome code, and duration — useful for proving a guard actually fires. The page counts verified identities, MCP, and MCP-caused composition as agent-attributed; shows top-level unsigned HTTP, HTTP-caused composition, and client-declared WebMCP markers separately as unverified client dispatches; and hides onlyinvokeCapability()work with no served-request provenance behind a first-party toggle. The JSON keeps everything. The traffic buffer outlives app-graph HMR, so retained calls stay visible after the final capability is removed, until the dev server restarts. It is dev-only, and under adapter-owned dev servers (Cloudflareworkerd)/_prachtdoes not exist at all — a 404 there means the middleware never ran, not that no agent traffic occurred.
Step 5: The discovery surface
llmsTxtin the vite config: every listed path is a URL the app invites an agent to fetch. Cross-check theexcludelist against routes behind auth middleware, internal tooling, and deliberate error routes — nothing about a middleware tells the framework whether it gates or merely logs, so an auth-gated route missing fromexcludeis awarn(seedocs/LLMS_TXT.md). Capabilities appear there with their effect class; destructive ones are annotatedrequires confirmation.- A collection-driven
llmsTxtArtifacts()(see/add-content) is a second generator with its own coverage — compare what each publishes. - Routes exporting
markdownor declaringmarkdown: trueserve a Markdown representation to agents. Confirm the Markdown variant is not more permissive than the HTML page (it carriesVary: Accept, so it is separately cached). - An enabled OpenAPI document (
/openapi.json,dist/client/openapi.json) is public unless the host protects it — check descriptions and examples for internal hostnames or credentials, and that "Try it out" mutation endpoints carry real authentication. /.well-known/http-message-signatures-directoryand the MCP endpoint path should both be intentional; the MCP endpoint stays active with an empty capability graph./.well-known/oauth-protected-resource(and the RFC 9728 path-suffixed form, e.g./.well-known/oauth-protected-resource/mcp) is served only whenagents.mcp.authis configured. Its presence is a good signal; its absence next to a live/mcpmeans no MCP host can authenticate to the endpoint.
Step 6: Did this change widen the surface?
pracht plan --json --base origin/main
widensAgentSurface and the ! capability lines answer the question a route
diff cannot: a new exposure, a destructive capability reclassified out of the
gate, an agentPolicy downgraded from require, dropped middleware, a
loosened input schema (dropped required, opened additionalProperties, raised
bound), newly enabled agents.mcp, newly enabled agents.mcp.destructive when
a declared destructive MCP capability actually exists, OAuth protection
removed from a still-live MCP endpoint, a removed required scope, or a newly
trusted authorization server. Enabling the destructive switch in advance,
with no such tool, is not a widening. The snapshot records the OAuth policy
separately from the endpoint path, so an unchanged /mcp is not evidence that
the guard stayed the same. Report every widening explicitly, with the
before/after. A stale snapshot makes this useless — pracht verify fails on
staleness, so trust it only when verify passes.
Step 7: The no-agent-surface case
When the app is supposed to have none:
- Confirm the manifest registers no
capabilitiesand noagents. That lets the build define the surface away (~15 KB gzip of an example server bundle). - Analysis is one-sided: a spread, a regex literal, or otherwise opaque syntax in the manifest leaves the define unset and keeps the runtime in the bundle. Flag manifest constructs that defeat the static read.
- Confirm
llmsTxtis off if the app should not advertise itself, and that no route setsmarkdown: true. create-pracht --no-agent-toolscontrols the scaffolded developer tooling (.mcp.json, skills) — it has nothing to do with the deployed agent surface. Do not conflate them in the report.
Step 8: Report
| Surface | Item | Reach | Guard | Severity |
|---|
Severities:
error— destructive capability reachable without a configured confirmation secret;agents.mcp.destructivewith no registered approval store, or with a memory store on a multi-replica deployment;agentPolicy: "require"with nowebBotAuth; capability module unreadable; MCP-exposed capability whose only authorization is a cookie session; approval store on a backend without conditional writes; unguardedexpose.mcptool on an endpoint with neitheragents.mcp.authnor named middleware;agents.mcp.auth.verifythat does not bind the token audience toresource, or that is an inline function in the manifest.warn— auth-gated route advertised inllms.txt;expose.mcpwith noagents.mcp; destructiveexpose.mcpwith noagents.mcp.destructive(dead exposure); exposed capability with no named middleware; unbounded output (nolimit/maximum); no audit sink; a secondsetCapabilityAuditHook()call silently replacing the first; a module-scope listener without HMR disposal;singleUsetreated as durable;agents.mcp.authwith norequiredScopesand no per-capability scope check.info— exposure that is intentional and guarded, recorded so the reviewer sees the whole surface in one place; framework gaps that are deployment responsibilities (rate limiting, write idempotency, result-size limits).
Rules
- Report only — never change exposure, policy, or configuration. Propose the diff and let the owner apply it.
- Never call a destructive capability to test it, not even the prepare phase, against anything but a local throwaway environment.
- Do not treat client-declared signals as trust: the
webmcptransport marker is informational, and only MCP dispatch state is trustworthy for attributing nested effects. - Distinguish
pracht mcp(the development-time stdio server exposing the app graph to coding agents) from the deployed/mcpendpoint exposing the app's operations. They have different threat models. - State the framework guarantee before each finding so the reader can tell an opt-out from a hole.
- Pair with
/audit-auth,/audit-csrf, and/audit-secrets— this skill owns agent reachability, not general request authorization.
$ARGUMENTS
Frequently asked questions
What to verify before installation and use
What does the audit-agent-surface source document cover?
Pracht's agent surface is opt-in end to end (docs/CAPABILITIES.md, docs/AGENTTRUST.md, docs/REMOTEMCP.md). State that baseline before auditing the opt-outs:
How do I install audit-agent-surface?
The source record exposes this install command: npx skills add https://github.com/JoviDeCroock/pracht --skill "skills/audit-agent-surface". Inspect the command and pinned source before running it.
Alternatives
Compare before choosing
enuno/unifi-mcp-server
unifi-mcp-tool-builder
Specialized guide for adding new MCP tools to the UniFi MCP Server following project standards, UniFi API patterns, and test-driven development practices. Use when implementing new UniFi Network Controller features as MCP tools.
NintendaDev/unikit-ai
unikit-docs
Generate and maintain the project's TECHNICAL documentation from its codebase — scans the project structure, tech stack, and module boundaries, then writes a lean README landing page plus detailed topic pages (architecture, modules, setup, build, APIs), only the docs that are relevant. Use whenever the user wants to create, update, or validate documentation of the CODE or the project itself, e.g. "generate documentation", "create docs", "write the README", "update the project docs", "document th
vasilyu1983/AI-Agents-public
research-git
Scans public GitHub repos for agent skills, dev practices, and code patterns. Use when enriching skills, setting team policy, or researching a build domain.
SerendipityOneInc/ZooData-Skills
zoodata
API endpoint reference for the ZooData data platform: the 12 commerce endpoints plus 10 keyword-intelligence endpoints (categories, markets, products, competitors, realtime ASIN, AI review analysis, raw reviews, price band, brand, history, and the keyword detail/trend/extends/search/ market-profile/product-traffic/competitor-keywords/traffic-profile/ traffic-timeline family) — their inputs/outputs, parameter quirks, Quick Start (auth, base URL), how credits are tracked (meta.creditsConsumed), an