create-pracht 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/add-auth/SKILL.md +63 -143
- package/skills/add-capabilities/SKILL.md +409 -0
- package/skills/add-content/SKILL.md +242 -0
- package/skills/add-db/SKILL.md +93 -202
- package/skills/add-i18n/SKILL.md +178 -217
- package/skills/add-images/SKILL.md +203 -0
- package/skills/add-observability/SKILL.md +118 -15
- package/skills/add-openapi/SKILL.md +209 -0
- package/skills/audit-a11y/SKILL.md +8 -9
- package/skills/audit-agent-surface/SKILL.md +335 -0
- package/skills/audit-auth/SKILL.md +16 -11
- package/skills/audit-bundles/SKILL.md +56 -12
- package/skills/audit-csrf/SKILL.md +9 -10
- package/skills/audit-deps/SKILL.md +8 -8
- package/skills/audit-headers/SKILL.md +9 -10
- package/skills/audit-islands/SKILL.md +9 -10
- package/skills/audit-loaders/SKILL.md +23 -8
- package/skills/audit-redirects/SKILL.md +9 -10
- package/skills/audit-secrets/SKILL.md +6 -6
- package/skills/audit-seo/SKILL.md +8 -8
- package/skills/audit-shells/SKILL.md +8 -9
- package/skills/configure-isg/SKILL.md +9 -10
- package/skills/migrate-nextjs/SKILL.md +200 -415
- package/skills/pracht-debug/SKILL.md +165 -120
- package/skills/pracht-deploy/SKILL.md +248 -329
- package/skills/pracht-scaffold/SKILL.md +123 -146
- package/skills/pracht-test-api/SKILL.md +10 -10
- package/skills/pre-deploy/SKILL.md +166 -195
- package/skills/scaffold-e2e/SKILL.md +11 -12
- package/skills/scaffold-tests/SKILL.md +10 -12
- package/skills/tune-render-mode/SKILL.md +7 -8
- package/skills/typed-routes/SKILL.md +15 -11
- package/skills/upgrade-pracht/SKILL.md +12 -10
- package/src/index.js +43 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: audit-agent-surface
|
|
3
|
+
version: 1.0.4
|
|
4
|
+
description: |
|
|
5
|
+
Inventory what agents can reach in a pracht app — capability exposure (HTTP,
|
|
6
|
+
WebMCP, remote MCP), `agents` trust config, the destructive-confirmation gate,
|
|
7
|
+
`llms.txt`, Markdown negotiation, OpenAPI — and report where the surface is
|
|
8
|
+
wider than intended, or confirm an opt-out app ships none.
|
|
9
|
+
Use for "audit the agent surface", "what can agents do on my site", "is my MCP
|
|
10
|
+
endpoint safe", "did this PR widen what agents can reach".
|
|
11
|
+
allowed-tools:
|
|
12
|
+
- Bash
|
|
13
|
+
- Read
|
|
14
|
+
- Grep
|
|
15
|
+
- Glob
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Pracht Audit Agent Surface
|
|
19
|
+
|
|
20
|
+
Pracht's agent surface is opt-in end to end (`docs/CAPABILITIES.md`,
|
|
21
|
+
`docs/AGENT_TRUST.md`, `docs/REMOTE_MCP.md`). State that baseline before
|
|
22
|
+
auditing the opt-outs:
|
|
23
|
+
|
|
24
|
+
- No loader or API route is ever inferred as a capability; a capability without
|
|
25
|
+
`expose` is unreachable over the network.
|
|
26
|
+
- `destructive` capabilities may be exposed over HTTP and remote MCP, never as
|
|
27
|
+
WebMCP page tools, and every dispatch is confirmation-gated.
|
|
28
|
+
- Remote MCP rejects cookie-bearing and browser-originated requests, and serves
|
|
29
|
+
destructive capabilities only with `agents.mcp.destructive` plus a registered
|
|
30
|
+
approval store — otherwise it filters them out at serve time. `agents.mcp.auth`
|
|
31
|
+
additionally makes it an OAuth 2.0 protected resource; without it the endpoint
|
|
32
|
+
is open and authentication is the capability middleware's job.
|
|
33
|
+
- An app that registers neither capabilities nor `agents` has the dispatch path
|
|
34
|
+
and Web Bot Auth verifier dropped from its server bundle at build time.
|
|
35
|
+
|
|
36
|
+
This skill reports; it never mutates. Prerequisites: `pracht inspect` needs a
|
|
37
|
+
vite config registering the pracht plugin. If the pracht MCP server is
|
|
38
|
+
registered (see `docs/MCP.md`), prefer its tools (`inspect_agents`,
|
|
39
|
+
`inspect_capabilities`, `inspect_routes`, `inspect_api`, `doctor`, `verify`)
|
|
40
|
+
over shelling out.
|
|
41
|
+
|
|
42
|
+
## Step 1: Inventory the declared surface
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pracht inspect agents --json # the whole configured surface in one call
|
|
46
|
+
pracht inspect capabilities --json # name, effect, transports, HTTP path, middleware, schemas
|
|
47
|
+
pracht inspect routes --json # markdown negotiation, hydration, middleware
|
|
48
|
+
pracht inspect api --json
|
|
49
|
+
pracht verify --json # contract, exposure, and projection checks
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`inspect agents` reports `webBotAuth`, confirmation policy, MCP endpoint and
|
|
53
|
+
OAuth policy, `llmsTxt`, each capability's effect/policy/transports/path, and
|
|
54
|
+
exposure counts (`private` means unexposed). Use `inspect capabilities` for
|
|
55
|
+
schemas and middleware.
|
|
56
|
+
|
|
57
|
+
Build the inventory table: capability → effect → transports → HTTP path →
|
|
58
|
+
middleware → `agentPolicy`. A capability reported as `unreadable` means
|
|
59
|
+
`@pracht/capabilities` is not installed; treat it as an `error` and stop
|
|
60
|
+
reasoning about its policy until it loads.
|
|
61
|
+
|
|
62
|
+
Cross-check `inspect agents` against the manifest's `agents` block. It reads
|
|
63
|
+
resolved app and production `llmsTxt` config, including computed branches. A
|
|
64
|
+
`null` `llmsTxt.enabled` means an older plugin: report unknown and recommend an
|
|
65
|
+
upgrade. Use resolved `mcp.auth`; `null` means framework-level OAuth is open.
|
|
66
|
+
|
|
67
|
+
## Step 2: Exposure vs. intent
|
|
68
|
+
|
|
69
|
+
For every exposed capability, ask whether the exposure is deliberate:
|
|
70
|
+
|
|
71
|
+
- `expose.mcp` set but no `agents.mcp` configured — declared, served by
|
|
72
|
+
nothing. `pracht verify` warns; report it so the intent gets resolved.
|
|
73
|
+
- `expose.mcp` set on an operation whose authorization relies on a browser
|
|
74
|
+
session — remote MCP rejects cookies, so the only credentials it sees are the
|
|
75
|
+
forwarded `Authorization` header and `context.agent`. A middleware chain that
|
|
76
|
+
reads a session cookie authorizes nobody there.
|
|
77
|
+
- `expose.webmcp` — the in-page agent acts as the signed-in user in their tab.
|
|
78
|
+
Confirm that is intended for every one, and that the route's hydration is not
|
|
79
|
+
`"none"` (which registers no tools). Flag a webmcp capability whose effective
|
|
80
|
+
agent policy is `"require"` (capability-level, or inherited from
|
|
81
|
+
`agents.webBotAuth.policy`): page-tool calls are unsigned browser fetches, so
|
|
82
|
+
the tool is dead — every call 401s. Also check that capabilities returning
|
|
83
|
+
user-generated or third-party content set
|
|
84
|
+
`expose.webmcp: { untrustedContent: true }` so hosts treat the output as
|
|
85
|
+
untrusted.
|
|
86
|
+
- Private capabilities used as building blocks: `invokeCapability()` runs their
|
|
87
|
+
named middleware but **not** app-level `api.middleware`. Their named
|
|
88
|
+
middleware is the only authorization seam — flag private capabilities with an
|
|
89
|
+
empty `middleware` list that touch sensitive data.
|
|
90
|
+
- Custom `expose.http.path` values that land outside `/api/**` and therefore
|
|
91
|
+
escape path-scoped middleware or host rules.
|
|
92
|
+
- Declared vs. actually served: `expose.mcp` in source is what the graph
|
|
93
|
+
claims. A `pracht eval` scenario with `"transport": "mcp"` proves what the
|
|
94
|
+
endpoint answers — it performs a real `initialize` handshake and issues each
|
|
95
|
+
step as a `tools/call`. When the endpoint has `mcp.auth`, set scenario-level
|
|
96
|
+
`mcpHeaders.authorization` so the token is sent on the handshake and every
|
|
97
|
+
later request; do not commit a production token. Run it only against a local
|
|
98
|
+
throwaway server, and only with `read` steps. If the app ships MCP-exposed capabilities with no
|
|
99
|
+
such scenario, report the missing proof: an HTTP-only scenario says nothing
|
|
100
|
+
about whether an MCP host can reach the tool.
|
|
101
|
+
|
|
102
|
+
## Step 2b: The `/mcp` auth posture
|
|
103
|
+
|
|
104
|
+
`agents: { mcp: {} }` without `auth` is open; authorization rests entirely on
|
|
105
|
+
each tool's named middleware. Report an exposed tool with no middleware as
|
|
106
|
+
`error`; otherwise `warn` and name the middleware carrying the boundary.
|
|
107
|
+
|
|
108
|
+
With `agents.mcp.auth`, check:
|
|
109
|
+
|
|
110
|
+
- `resource` is canonical HTTPS (loopback HTTP only), has no query, fragment,
|
|
111
|
+
or non-root trailing slash, and exactly identifies the endpoint. `/` may
|
|
112
|
+
identify the deployed root; the origin-root identifier is slashless. Aliases,
|
|
113
|
+
query variants, and trailing slashes must 308 to `resource` before challenge.
|
|
114
|
+
- Every `authorizationServers` issuer is canonical HTTPS without query or
|
|
115
|
+
fragment. Reject unknown `agents.mcp`/`auth` keys and API-route collisions.
|
|
116
|
+
- `verify` is a module reference under `src/server`, `src/middleware`, or
|
|
117
|
+
`src/capabilities`, resolves uniquely, and default-exports a function. Inline,
|
|
118
|
+
missing, ambiguous, or non-callable verifiers are `error`. Its request clone
|
|
119
|
+
may consume the body without consuming later JSON-RPC dispatch. Overlapping
|
|
120
|
+
source directories may register the same normalized file more than once;
|
|
121
|
+
that is one verifier, not ambiguity. A `blocked` inspection reason for an
|
|
122
|
+
unusable verifier is conclusive because the adapter server entry cannot
|
|
123
|
+
replace the configured module reference.
|
|
124
|
+
- The verifier binds token audience to `resource`; otherwise tokens for another
|
|
125
|
+
service authenticate here (`error`). Require `requiredScopes` or per-tool
|
|
126
|
+
checks of `context.tokenAuth.scopes` (`warn` otherwise). The initial challenge
|
|
127
|
+
must advertise required scopes; scope tokens follow OAuth's printable-ASCII
|
|
128
|
+
grammar. `context.tokenAuth` is MCP-only and nested calls cannot replace it.
|
|
129
|
+
- Under base `/app/`, resource includes `/app/mcp` while metadata stays at the
|
|
130
|
+
origin-root `/.well-known/oauth-protected-resource/app/mcp`; fetch it and its
|
|
131
|
+
bare alias, ensuring app/static routes cannot shadow either. The bare
|
|
132
|
+
well-known path is reserved and its CORS-open metadata is expected; flag only
|
|
133
|
+
sensitive scope names.
|
|
134
|
+
- `CapabilityAuditEvent` records Web Bot Auth `agent`, not `tokenAuth`. Report
|
|
135
|
+
this as `info`, or `warn` when per-account attribution is required and no
|
|
136
|
+
middleware/capability forwards the principal to an audit sink.
|
|
137
|
+
|
|
138
|
+
## Step 3: The destructive gate
|
|
139
|
+
|
|
140
|
+
- `webmcp` on a `destructive` capability is rejected by the framework — if you
|
|
141
|
+
find it in source, the build is failing.
|
|
142
|
+
- `mcp` on a `destructive` capability is a **served remote tool** only when the
|
|
143
|
+
manifest sets `agents: { mcp: { destructive: true } }`. Report it as a
|
|
144
|
+
deliberate widening and check both halves: the opt-in, and a
|
|
145
|
+
`setCapabilityApprovalStore()` call the running server actually executes
|
|
146
|
+
(imported by a server entry, a capability module, or applied API/capability
|
|
147
|
+
middleware — a module nothing imports registers nothing). Opt-in without a store is an `error` in your report: the
|
|
148
|
+
endpoint refuses to serve at all, and `pracht verify` only warns (its source
|
|
149
|
+
scan cannot see a registration in a workspace package, so it must not
|
|
150
|
+
hard-block). Two more preconditions fail the endpoint the same way — a
|
|
151
|
+
missing `PRACHT_CONFIRMATION_SECRET`, and `mode: "human"` with neither
|
|
152
|
+
`agents.webBotAuth` with a valid 32-byte base64url Ed25519 static key or HTTPS
|
|
153
|
+
directory nor a principal resolver — so check all three together.
|
|
154
|
+
Runtime-backed `/_pracht` reports a verified endpoint-wide failure by marking
|
|
155
|
+
every MCP exposure `mcp(unserved)`. Graph-only `pracht dev`, `pracht inspect
|
|
156
|
+
capabilities`, `pracht inspect agents`, and MCP inspection use
|
|
157
|
+
`mcp(unverified)` when the same missing
|
|
158
|
+
preconditions may be registered by the adapter server entry they deliberately
|
|
159
|
+
skip. JSON inspection exposes `mcpEndpoint`, `mcpDestructive`,
|
|
160
|
+
`mcpRuntimeStatus`, and `mcpUnavailableReasons`; use those fields instead of
|
|
161
|
+
treating a declared `mcp` transport as proof of reachability. These surfaces
|
|
162
|
+
load applied setup middleware modules without executing the middleware
|
|
163
|
+
functions.
|
|
164
|
+
Destructive `expose.mcp` *without* the opt-in is dead exposure: the tool is
|
|
165
|
+
invisible, and `pracht verify` warns.
|
|
166
|
+
- `PRACHT_CONFIRMATION_SECRET` must be set in the server environment for each
|
|
167
|
+
deployment target (build environment too on Vercel, since it becomes the
|
|
168
|
+
bypass token there). Missing → every destructive call answers
|
|
169
|
+
`403 confirmation_unavailable`.
|
|
170
|
+
- Record the honest limits in the report: the stateless HMAC token is replayable
|
|
171
|
+
within its TTL (default 120 s), the calling agent can hand the token back to
|
|
172
|
+
itself, and without Web Bot Auth or
|
|
173
|
+
`setCapabilityApprovalPrincipalResolver()` both phases run as `"anonymous"`.
|
|
174
|
+
Flag `confirmation: { singleUse: true }` used as if it were durable — it is a
|
|
175
|
+
per-instance in-memory cache, lost on restart.
|
|
176
|
+
- If an approval store is registered, confirm its backend supports atomic
|
|
177
|
+
conditional writes and that all replicas share it: with a store registered, a
|
|
178
|
+
token whose proposal is unknown is refused, so a per-instance store breaks
|
|
179
|
+
commits. `createSqlApprovalStore()` over D1/Postgres/Turso qualifies;
|
|
180
|
+
`createMemoryApprovalStore()` in a deployed multi-replica app does not, and
|
|
181
|
+
neither does a hand-rolled store over Cloudflare KV.
|
|
182
|
+
- `confirmation: { mode: "human" }` without both a store and an authenticated
|
|
183
|
+
principal fails closed — check both exist.
|
|
184
|
+
|
|
185
|
+
## Step 4: Identity and policy
|
|
186
|
+
|
|
187
|
+
- `agents.webBotAuth.policy: "require"` gates capability HTTP endpoints only —
|
|
188
|
+
pages and API routes are not gated. Flag any assumption that it protects
|
|
189
|
+
pages.
|
|
190
|
+
- `directories` is an allowlist; an empty one means no directory fetching at all
|
|
191
|
+
(deliberate SSRF protection). Flag a directory origin that is not the agent
|
|
192
|
+
ecosystem endpoint the app intends to trust.
|
|
193
|
+
- `agentPolicy: "require"` on a capability while `webBotAuth` is unconfigured
|
|
194
|
+
answers 401 for every caller — a loud misconfiguration, report as `error`.
|
|
195
|
+
- Note the replay property: Pracht's stateless verifier does not enforce `nonce`
|
|
196
|
+
uniqueness, and the default covered components (`@authority`,
|
|
197
|
+
`signature-agent`) bind a signature to a host, not to a method, path, or body.
|
|
198
|
+
Treat a verified identity as authentication, not per-request authorization.
|
|
199
|
+
- Confirm an audit sink exists (`setCapabilityAuditHook()`,
|
|
200
|
+
`addCapabilityAuditListener()`, or `onCapabilityAudit`) — without one there
|
|
201
|
+
is no record of who called what. Grep for all three; `setCapabilityAuditHook`
|
|
202
|
+
is a single slot, so two calls to it mean one sink is silently dead — report
|
|
203
|
+
that as a `warn` and point at `addCapabilityAuditListener(name, hook)`. Also
|
|
204
|
+
flag a computed or non-constant sink name: same-name registration is what
|
|
205
|
+
makes the call idempotent under dev HMR. A module-scope listener must also
|
|
206
|
+
register its unsubscribe with `import.meta.hot.dispose()`; otherwise removing
|
|
207
|
+
the module or renaming the sink leaves the old registration active until the
|
|
208
|
+
dev server restarts.
|
|
209
|
+
- Know what the trail does **not** cover before treating it as a security
|
|
210
|
+
record: a cross-origin 403, an unknown-capability 404, and an unknown or
|
|
211
|
+
unexposed MCP tool name all return *before* dispatch and emit no event. An
|
|
212
|
+
agent enumerating tool names leaves no trace, so never conclude "nothing
|
|
213
|
+
tried" from an empty trail — that question belongs to the HTTP access log.
|
|
214
|
+
- To see the surface actually being exercised rather than merely declared, run
|
|
215
|
+
the app with `pracht dev`, drive the capability, and read the **Agents**
|
|
216
|
+
section of `/_pracht` (JSON under `agentTraffic` at `/_pracht.json`). It
|
|
217
|
+
records transport, `via` for nested composition, verified identity, outcome
|
|
218
|
+
code, and duration — useful for proving a guard actually fires. The page
|
|
219
|
+
counts verified identities, MCP, and MCP-caused composition as
|
|
220
|
+
agent-attributed; shows top-level unsigned HTTP, HTTP-caused composition, and
|
|
221
|
+
client-declared WebMCP markers separately as unverified client dispatches;
|
|
222
|
+
and hides only `invokeCapability()` work with no served-request provenance
|
|
223
|
+
behind a first-party toggle. The JSON keeps everything.
|
|
224
|
+
The traffic buffer outlives app-graph HMR, so retained calls stay visible
|
|
225
|
+
after the final capability is removed, until the dev server restarts.
|
|
226
|
+
It is dev-only, and under adapter-owned dev servers (Cloudflare `workerd`)
|
|
227
|
+
`/_pracht` does not exist at all — a 404 there means the middleware never
|
|
228
|
+
ran, not that no agent traffic occurred.
|
|
229
|
+
|
|
230
|
+
## Step 5: The discovery surface
|
|
231
|
+
|
|
232
|
+
- `llmsTxt` in the vite config: every listed path is a URL the app *invites* an
|
|
233
|
+
agent to fetch. Cross-check the `exclude` list against routes behind auth
|
|
234
|
+
middleware, internal tooling, and deliberate error routes — nothing about a
|
|
235
|
+
middleware tells the framework whether it gates or merely logs, so an
|
|
236
|
+
auth-gated route missing from `exclude` is a `warn` (see `docs/LLMS_TXT.md`).
|
|
237
|
+
Capabilities appear there with their effect class; destructive ones are
|
|
238
|
+
annotated `requires confirmation`.
|
|
239
|
+
- A collection-driven `llmsTxtArtifacts()` (see `/add-content`) is a second
|
|
240
|
+
generator with its own coverage — compare what each publishes.
|
|
241
|
+
- Routes exporting `markdown` or declaring `markdown: true` serve a Markdown
|
|
242
|
+
representation to agents. Confirm the Markdown variant is not more permissive
|
|
243
|
+
than the HTML page (it carries `Vary: Accept`, so it is separately cached).
|
|
244
|
+
- An enabled OpenAPI document (`/openapi.json`, `dist/client/openapi.json`) is
|
|
245
|
+
public unless the host protects it — check descriptions and examples for
|
|
246
|
+
internal hostnames or credentials, and that "Try it out" mutation endpoints
|
|
247
|
+
carry real authentication.
|
|
248
|
+
- `/.well-known/http-message-signatures-directory` and the MCP endpoint path
|
|
249
|
+
should both be intentional; the MCP endpoint stays active with an empty
|
|
250
|
+
capability graph.
|
|
251
|
+
- `/.well-known/oauth-protected-resource` (and the RFC 9728 path-suffixed form,
|
|
252
|
+
e.g. `/.well-known/oauth-protected-resource/mcp`) is served only when
|
|
253
|
+
`agents.mcp.auth` is configured. Its presence is a *good* signal; its absence
|
|
254
|
+
next to a live `/mcp` means no MCP host can authenticate to the endpoint.
|
|
255
|
+
|
|
256
|
+
## Step 6: Did this change widen the surface?
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
pracht plan --json --base origin/main
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
`widensAgentSurface` and the `!` capability lines answer the question a route
|
|
263
|
+
diff cannot: a new exposure, a destructive capability reclassified out of the
|
|
264
|
+
gate, an `agentPolicy` downgraded from `require`, dropped middleware, a
|
|
265
|
+
loosened input schema (dropped `required`, opened `additionalProperties`, raised
|
|
266
|
+
bound), newly enabled `agents.mcp`, newly enabled `agents.mcp.destructive` when
|
|
267
|
+
a declared destructive MCP capability actually exists, OAuth protection
|
|
268
|
+
removed from a still-live MCP endpoint, a removed required scope, or a newly
|
|
269
|
+
trusted authorization server. Enabling the destructive switch in advance,
|
|
270
|
+
with no such tool, is not a widening. The snapshot records the OAuth policy
|
|
271
|
+
separately from the endpoint path, so an unchanged `/mcp` is not evidence that
|
|
272
|
+
the guard stayed the same. Report every widening explicitly, with the
|
|
273
|
+
before/after. A stale snapshot makes this useless — `pracht verify` fails on
|
|
274
|
+
staleness, so trust it only when verify passes.
|
|
275
|
+
|
|
276
|
+
## Step 7: The no-agent-surface case
|
|
277
|
+
|
|
278
|
+
When the app is supposed to have none:
|
|
279
|
+
|
|
280
|
+
- Confirm the manifest registers no `capabilities` and no `agents`. That lets
|
|
281
|
+
the build define the surface away (~15 KB gzip of an example server bundle).
|
|
282
|
+
- Analysis is one-sided: a spread, a regex literal, or otherwise opaque syntax
|
|
283
|
+
in the manifest leaves the define unset and keeps the runtime in the bundle.
|
|
284
|
+
Flag manifest constructs that defeat the static read.
|
|
285
|
+
- Confirm `llmsTxt` is off if the app should not advertise itself, and that no
|
|
286
|
+
route sets `markdown: true`.
|
|
287
|
+
- `create-pracht --no-agent-tools` controls the *scaffolded developer* tooling
|
|
288
|
+
(`.mcp.json`, skills) — it has nothing to do with the deployed agent surface.
|
|
289
|
+
Do not conflate them in the report.
|
|
290
|
+
|
|
291
|
+
## Step 8: Report
|
|
292
|
+
|
|
293
|
+
| Surface | Item | Reach | Guard | Severity |
|
|
294
|
+
| ------- | ---- | ----- | ----- | -------- |
|
|
295
|
+
|
|
296
|
+
Severities:
|
|
297
|
+
|
|
298
|
+
- `error` — destructive capability reachable without a configured confirmation
|
|
299
|
+
secret; `agents.mcp.destructive` with no registered approval store, or with a
|
|
300
|
+
memory store on a multi-replica deployment; `agentPolicy: "require"` with no
|
|
301
|
+
`webBotAuth`; capability module unreadable; MCP-exposed capability whose only
|
|
302
|
+
authorization is a cookie session; approval store on a backend without
|
|
303
|
+
conditional writes; unguarded `expose.mcp` tool on an endpoint with neither
|
|
304
|
+
`agents.mcp.auth` nor named middleware; `agents.mcp.auth.verify` that does not
|
|
305
|
+
bind the token audience to `resource`, or that is an inline function in the
|
|
306
|
+
manifest.
|
|
307
|
+
- `warn` — auth-gated route advertised in `llms.txt`; `expose.mcp` with no
|
|
308
|
+
`agents.mcp`; destructive `expose.mcp` with no `agents.mcp.destructive` (dead
|
|
309
|
+
exposure); exposed capability with no named middleware; unbounded output
|
|
310
|
+
(no `limit`/`maximum`); no audit sink; a second `setCapabilityAuditHook()`
|
|
311
|
+
call silently replacing the first; a module-scope listener without HMR
|
|
312
|
+
disposal; `singleUse` treated as durable;
|
|
313
|
+
`agents.mcp.auth` with no `requiredScopes` and no per-capability scope check.
|
|
314
|
+
- `info` — exposure that is intentional and guarded, recorded so the reviewer
|
|
315
|
+
sees the whole surface in one place; framework gaps that are deployment
|
|
316
|
+
responsibilities (rate limiting, write idempotency, result-size limits).
|
|
317
|
+
|
|
318
|
+
## Rules
|
|
319
|
+
|
|
320
|
+
1. Report only — never change exposure, policy, or configuration. Propose the
|
|
321
|
+
diff and let the owner apply it.
|
|
322
|
+
2. Never call a destructive capability to test it, not even the prepare phase,
|
|
323
|
+
against anything but a local throwaway environment.
|
|
324
|
+
3. Do not treat client-declared signals as trust: the `webmcp` transport marker
|
|
325
|
+
is informational, and only MCP dispatch state is trustworthy for
|
|
326
|
+
attributing nested effects.
|
|
327
|
+
4. Distinguish `pracht mcp` (the development-time stdio server exposing the app
|
|
328
|
+
*graph* to coding agents) from the deployed `/mcp` endpoint exposing the app's
|
|
329
|
+
*operations*. They have different threat models.
|
|
330
|
+
5. State the framework guarantee before each finding so the reader can tell an
|
|
331
|
+
opt-out from a hole.
|
|
332
|
+
6. Pair with `/audit-auth`, `/audit-csrf`, and `/audit-secrets` — this skill
|
|
333
|
+
owns agent reachability, not general request authorization.
|
|
334
|
+
|
|
335
|
+
$ARGUMENTS
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: audit-auth
|
|
3
|
-
version: 1.2.
|
|
3
|
+
version: 1.2.4
|
|
4
4
|
description: |
|
|
5
|
-
Find pracht routes that look protected but aren't
|
|
6
|
-
middleware that augments context but never gates, client-
|
|
7
|
-
|
|
8
|
-
Use
|
|
9
|
-
|
|
5
|
+
Find pracht routes that look protected but aren't: missing auth middleware,
|
|
6
|
+
middleware that augments context but never gates, client-only checks, and
|
|
7
|
+
unguarded API mutations.
|
|
8
|
+
Use for "audit auth", "check route protection", "find unauthenticated routes",
|
|
9
|
+
"review middleware coverage".
|
|
10
10
|
allowed-tools:
|
|
11
11
|
- Bash
|
|
12
12
|
- Read
|
|
@@ -36,8 +36,8 @@ pracht plugin.
|
|
|
36
36
|
pracht inspect routes --json
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
MCP: when the pracht MCP server is registered (docs/MCP.md), prefer its
|
|
40
|
+
`inspect_routes`/`inspect_api`/`inspect_build`/`doctor`/`verify` tools over
|
|
41
41
|
shelling out.
|
|
42
42
|
|
|
43
43
|
Middleware is registered by name in the app manifest —
|
|
@@ -116,8 +116,12 @@ target. From `pracht inspect api --json`:
|
|
|
116
116
|
- Inspect every HTTP-, WebMCP-, or MCP-exposed capability body for
|
|
117
117
|
`invokeCapability()`. Direct composition never re-applies app-level API
|
|
118
118
|
middleware. Remote MCP additionally re-applies the callee's `agentPolicy`
|
|
119
|
-
and refuses destructive callees
|
|
120
|
-
|
|
119
|
+
and refuses destructive callees unless the tool being served is itself a
|
|
120
|
+
destructive capability that already cleared prepare/commit — a request-scoped
|
|
121
|
+
grant over every destructive callee, private ones included, so audit a
|
|
122
|
+
confirmed destructive tool's body the way you would a confirmed HTTP
|
|
123
|
+
endpoint's. Private non-destructive capabilities stay composable and rely on
|
|
124
|
+
their named middleware for authorization. For
|
|
121
125
|
HTTP/WebMCP composition, flag sensitive callees whose required transport
|
|
122
126
|
authorization or approval is absent from the composing capability and the
|
|
123
127
|
callee's named middleware.
|
|
@@ -168,7 +172,8 @@ Severity is the primary scale; the verdict is a secondary domain label:
|
|
|
168
172
|
listed but not flagged.
|
|
169
173
|
5. Do not auto-add middleware. Auth wiring is policy.
|
|
170
174
|
6. Treat allowed composed capability reachability as transitive. MCP blocks
|
|
171
|
-
destructive callees
|
|
175
|
+
destructive callees unless the served tool already cleared its own
|
|
176
|
+
confirmation gate, and re-applies `agentPolicy`; named middleware remains
|
|
172
177
|
the authorization seam for private non-destructive composition. Audit events
|
|
173
178
|
identify every nested attempt with `transport: "server"` and trusted request
|
|
174
179
|
provenance in `via`, but observability is not an authorization gate.
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: audit-bundles
|
|
3
|
-
version: 1.2.
|
|
3
|
+
version: 1.2.1
|
|
4
4
|
description: |
|
|
5
|
-
Analyze a pracht production build
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
route", "what's in my vendor chunk", or "tune prefetching".
|
|
5
|
+
Analyze a pracht production build: client bytes per route, fat vendor chunks,
|
|
6
|
+
routes shipping large dependencies, and dynamic `import()` / prefetch
|
|
7
|
+
recommendations.
|
|
8
|
+
Use for "audit bundles", "why is my JS so big", "bundle size per route", "what's
|
|
9
|
+
in my vendor chunk", "tune prefetching".
|
|
11
10
|
allowed-tools:
|
|
12
11
|
- Bash
|
|
13
12
|
- Read
|
|
@@ -23,11 +22,10 @@ and surfaces the worst offenders.
|
|
|
23
22
|
|
|
24
23
|
## Step 1: Build with analysis
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
shelling out.
|
|
29
|
-
|
|
30
|
-
`pracht build`.
|
|
25
|
+
MCP: when the pracht MCP server is registered (docs/MCP.md), prefer its
|
|
26
|
+
`inspect_routes`/`inspect_api`/`inspect_build`/`doctor`/`verify` tools over
|
|
27
|
+
shelling out. `pracht inspect` needs the pracht plugin in the vite config.
|
|
28
|
+
`inspect build` needs a prior `pracht build`.
|
|
31
29
|
|
|
32
30
|
```bash
|
|
33
31
|
pracht build --analyze --json
|
|
@@ -137,6 +135,52 @@ overrides the route-level strategy for a single anchor (and accepts the extra
|
|
|
137
135
|
option), so a route can stay on `"intent"` while its primary-nav link opts
|
|
138
136
|
into `"viewport"` or `"render"`.
|
|
139
137
|
|
|
138
|
+
If every route ends up on `"none"`, do not stop there — the prefetch listeners
|
|
139
|
+
still ship, in a chunk the router lazily imports on every page. Recommend
|
|
140
|
+
`pracht({ client: { prefetch: false } })` instead, which compiles the whole
|
|
141
|
+
mechanism out (~2.6 KB gzip and one fewer request). Confirm nothing relies on
|
|
142
|
+
prefetching first: the router silently stops honouring `route({ prefetch })` and
|
|
143
|
+
`<Link prefetch>`, and the imperative `prefetch()` export becomes a no-op. See
|
|
144
|
+
`docs/PERFORMANCE.md#switching-off-js-prefetching`.
|
|
145
|
+
|
|
146
|
+
## Step 6b: Chunk-group composition
|
|
147
|
+
|
|
148
|
+
App-level chunking and pracht's vendor chunk coexist — recommend it freely.
|
|
149
|
+
Pracht reads `build.rollupOptions.output` and contributes its Preact group in
|
|
150
|
+
whichever form the app used, so adding a group (to merge the long tail of small
|
|
151
|
+
initial chunks, say) does not cost the framework chunk:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
// vite.config.ts
|
|
155
|
+
build: {
|
|
156
|
+
rollupOptions: {
|
|
157
|
+
output: {
|
|
158
|
+
codeSplitting: { groups: [{ name: "app", tags: ["$initial"], minSize: 20_000 }] },
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Two things to check when an app already configures chunking:
|
|
165
|
+
|
|
166
|
+
- **Mixed forms.** Rolldown lets `codeSplitting` silence both `advancedChunks`
|
|
167
|
+
and `manualChunks`. An app that sets more than one is running only
|
|
168
|
+
`codeSplitting`; report the dead config as `warn`.
|
|
169
|
+
- **A group that swallows Preact.** Precedence is higher `priority` first, then
|
|
170
|
+
declaration order, with pracht's group last — so an app group matching
|
|
171
|
+
`node_modules` at equal priority absorbs the framework runtime. If a separate,
|
|
172
|
+
long-cached framework chunk was the intent, recommend a `priority` bump or a
|
|
173
|
+
`test` that excludes Preact.
|
|
174
|
+
|
|
175
|
+
Verify any grouping change against the prerendered HTML, not just the numbers:
|
|
176
|
+
a group broad enough to reshuffle entry chunks can drop the stylesheet links
|
|
177
|
+
pracht injects per route. Compare `<link rel="stylesheet">` tags in
|
|
178
|
+
`dist/client/**/index.html` before and after.
|
|
179
|
+
|
|
180
|
+
`pracht({ vendorChunk: false })` makes pracht contribute nothing, for an app
|
|
181
|
+
that wants Preact inside its own chunks or places `frameworkChunkGroups()`
|
|
182
|
+
itself.
|
|
183
|
+
|
|
140
184
|
## Step 7: Report
|
|
141
185
|
|
|
142
186
|
Three sections:
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: audit-csrf
|
|
3
|
-
version: 1.2.
|
|
3
|
+
version: 1.2.1
|
|
4
4
|
description: |
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
default
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"review session security", or after enabling cross-origin form usage.
|
|
5
|
+
Check CSRF posture across every form and mutation API. Pracht enforces
|
|
6
|
+
same-origin on mutation API requests by default (`api.requireSameOrigin`); this
|
|
7
|
+
verifies the default is intact and that cookies, middleware, or tokens cover
|
|
8
|
+
what it does not.
|
|
9
|
+
Use for "audit CSRF", "check CSRF protection", "are forms safe", "review session
|
|
10
|
+
security".
|
|
12
11
|
allowed-tools:
|
|
13
12
|
- Bash
|
|
14
13
|
- Read
|
|
@@ -71,8 +70,8 @@ Grep for `<Form ` across `src/`. For each occurrence:
|
|
|
71
70
|
pracht inspect api --json
|
|
72
71
|
```
|
|
73
72
|
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
MCP: when the pracht MCP server is registered (docs/MCP.md), prefer its
|
|
74
|
+
`inspect_routes`/`inspect_api`/`inspect_build`/`doctor`/`verify` tools over
|
|
76
75
|
shelling out.
|
|
77
76
|
|
|
78
77
|
For each API route, read the exported `methods`. Mutation methods: `POST`,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: audit-deps
|
|
3
|
-
version: 1.1.
|
|
3
|
+
version: 1.1.1
|
|
4
4
|
description: |
|
|
5
|
-
Run a dependency vulnerability audit and map each
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Use
|
|
9
|
-
vulnerable package"
|
|
5
|
+
Run a dependency vulnerability audit and map each advisory to the pracht routes,
|
|
6
|
+
loaders, middleware, and API handlers that import it — so you know what to
|
|
7
|
+
retest after upgrading.
|
|
8
|
+
Use for "audit deps", "scan for CVEs", "npm audit", "which routes use this
|
|
9
|
+
vulnerable package".
|
|
10
10
|
allowed-tools:
|
|
11
11
|
- Bash
|
|
12
12
|
- Read
|
|
@@ -56,8 +56,8 @@ is the one the user owns.
|
|
|
56
56
|
|
|
57
57
|
## Step 3: Map to routes/APIs
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
MCP: when the pracht MCP server is registered (docs/MCP.md), prefer its
|
|
60
|
+
`inspect_routes`/`inspect_api`/`inspect_build`/`doctor`/`verify` tools over
|
|
61
61
|
shelling out.
|
|
62
62
|
|
|
63
63
|
For each direct dependency identified in step 2:
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: audit-headers
|
|
3
|
-
version: 1.2.
|
|
3
|
+
version: 1.2.1
|
|
4
4
|
description: |
|
|
5
|
-
Audit security
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"set up HSTS", or "review header policy".
|
|
5
|
+
Audit security headers in a pracht app. The framework sets four defaults on
|
|
6
|
+
every response path; this covers the exceptions — static output served outside
|
|
7
|
+
first-party adapters, `headers()` exports that weaken defaults, and the choices
|
|
8
|
+
only you can make (HSTS, CSP).
|
|
9
|
+
Use for "audit security headers", "check CSP", "harden headers", "set up HSTS",
|
|
10
|
+
"review header policy".
|
|
12
11
|
allowed-tools:
|
|
13
12
|
- Bash
|
|
14
13
|
- Read
|
|
@@ -62,8 +61,8 @@ pracht inspect routes --json
|
|
|
62
61
|
pracht inspect api --json
|
|
63
62
|
```
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
MCP: when the pracht MCP server is registered (docs/MCP.md), prefer its
|
|
65
|
+
`inspect_routes`/`inspect_api`/`inspect_build`/`doctor`/`verify` tools over
|
|
67
66
|
shelling out.
|
|
68
67
|
|
|
69
68
|
Only route modules and shells have a `headers()` export — API handlers do not
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: audit-islands
|
|
3
|
-
version: 1.0.
|
|
3
|
+
version: 1.0.1
|
|
4
4
|
description: |
|
|
5
|
-
Audit pracht
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Use
|
|
10
|
-
|
|
11
|
-
hydration".
|
|
5
|
+
Audit pracht hydration: over-hydrated routes that should be `hydration:
|
|
6
|
+
"islands"` or `"none"`, dead interactivity outside the islands directory,
|
|
7
|
+
non-serializable island props, mis-tuned `client` strategies, and invalid
|
|
8
|
+
render/hydration pairs.
|
|
9
|
+
Use for "audit islands", "reduce hydration", "why is this island not
|
|
10
|
+
interactive", "check partial hydration".
|
|
12
11
|
allowed-tools:
|
|
13
12
|
- Bash
|
|
14
13
|
- Read
|
|
@@ -26,8 +25,8 @@ islands wired in ways that break at render time or ship dead handlers.
|
|
|
26
25
|
|
|
27
26
|
## Step 1: Enumerate routes and hydration modes
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
MCP: when the pracht MCP server is registered (docs/MCP.md), prefer its
|
|
29
|
+
`inspect_routes`/`inspect_api`/`inspect_build`/`doctor`/`verify` tools over
|
|
31
30
|
shelling out.
|
|
32
31
|
|
|
33
32
|
```bash
|