| name | red-team-plugin |
| description | Adversarial bug-hunting code review of the plugin-first framework in plugin/ — @plugin/{core,runtime,ui}, the host app, and plugin packages. Hunts real bugs in manifest validation, merger floor enforcement, mutator atomicity, registry collision detection, auditor declared-emitter discipline, state-machine actor lifecycle, realtime subscriber isolation, and the plugin-scoped service injection seam. Not style nits. Runs inline in the current chat. Use after non-trivial framework changes or before a phase commit. |
Red-team adversary review — plugin/ subtree (inline)
Perform this review inline in the current chat using the session's Read, Grep, and Bash tools. Do NOT spawn an Agent — the goal is to use the active session, not to fork a separate run.
Scope is plugin/ only. The parent apps/ and packages/ are the legacy world; red-team-review covers them. This skill covers what the plugin-first framework introduces.
When to invoke
- User says: "red team plugin", "bug hunt plugin", "find plugin bugs", "review plugin/"
- After a non-trivial change to
createHost, activate, Mutator, merger, any *Registry, or the Auditor
- After adding a new contribution type (server actions, tables, state machines, audit, realtime)
- At each Phase close-out before the summary commit
Setup — read these first
Scope to the change set. If none was named, default to the load-bearing surfaces:
plugin/packages/core/src/types.ts — the contract. Every plugin imports from here. Type drift here ripples to every plugin.
plugin/packages/core/src/{schema,ops,definePlugin}.ts — combinator DSL, Op type union, plugin factory.
plugin/packages/runtime/src/createHost.ts — Phase-1 validation. Conflict detection across plugins (ApiRef, ExtensionPoint, audit channels, server actions, state machines, tables, realtime topics). If a collision class is missed here, plugin code runs with silent overlap.
plugin/packages/runtime/src/activate.ts — Phase-2 activation. Topological sort, PluginContext construction, registry population. The line between "declared" and "active" is where most subtle bugs live.
plugin/packages/runtime/src/merger.ts — floor enforcement, list-merge strategies, nested-object recursion (D-0016). One of the highest-stakes files: if floor is bypassed, district-layer config can silently weaken a federal-floor obligation.
plugin/packages/runtime/src/mutator.ts — preview vs. apply atomicity. Layer write-back. Floor-violation rejection.
plugin/packages/runtime/src/{server,state}{Action,Machine}Registry.ts + tableRegistry.ts + realtimeBus.ts + auditor.ts — runtime homes for contribution types. Each has its own collision/scoping discipline.
plugin/apps/host/src/boot.ts — globalThis-pinned singleton (HMR-survival pattern from CLAUDE.md). Watch for any pool/resource that does NOT use the pin.
plugin/apps/host/app/admin/[section]/page.tsx + AdminFormClient.tsx — server/client boundary. "use client" placement; Op buffering; revalidation paths.
plugin/__tests__/boundary.test.ts — sibling-plugin import boundary. If it's loose, the no-cycle property is lost.
Skim, don't deep-read — the goal is to spot bugs, not summarize files.
What to hunt — adapt to the change set
Find real bugs, correctness issues, and abuse risks — not style nits. Rank findings CRITICAL / HIGH / MEDIUM / LOW.
-
Manifest validation gaps. createHost Phase-1 catches conflicts before any plugin code runs. For every new contribution type added: is the cross-plugin uniqueness check actually present in createHost.ts? A missing check means two plugins can silently overlap (same SQL name, same server action qualified name, same audit channel) and the second write wins at runtime.
-
Activation-order edges. computeActivationOrder uses consumes (hard edge) but NOT contributesTo (per D-0014, intentional). When a plugin both consumes an ApiRef and contributes to that owner's ExtensionPoint, the owner sees partial contributions during its own activate(). Any plugin that reads extensionPointRegistry.consume(...) during activate() is a candidate for silent partial-set bugs. Check that owners that need the FULL set defer the consume to post-activation, or read host.extensionPointRegistry directly.
-
Floor enforcement coverage. merger.ts walks scalars, objects, AND keyed-list items (D-0012). D-0016 closed the floor-inside-nested-object gap. Hunt for: floor on a list field (does the strategy honor it?), floor on a union-strategy list item, floor on a field nested deeper than two levels. Are tests asserting the floor wins, or only that the value MATCHES — a no-op test passes if floor never fired.
-
Mutator atomicity. mutator.apply(ops, author) must be all-or-nothing across ops. If op #3 of 5 fails floor validation, ops #1 and #2 must NOT be persisted. Look for for (const op of ops) { source.write(...) } shapes — that's the broken pattern.
-
HMR singleton leaks. apps/host/src/boot.ts pins on globalThis.__pluginHost. Any module-scope let cached: T | undefined in runtime code that holds a pool, watcher, file handle, or websocket leaks across HMR. Search the codebase for let .* = undefined at module top and confirm globalThis or no-resource-holding.
-
Server-action input validation. Every ServerAction.validateInput runs BEFORE the handler. Does it actually narrow the type (raw: unknown → I), or does it cast (return raw as I)? Cast-only validators let through any shape and crash inside the handler.
-
Auditor declared-emitter enforcement. Auditor.declareEmitters(pluginId, [...]) builds a per-plugin allowlist. Plugins that declare nothing get LENIENT mode (any eventId works). Is that intentional? In production it should probably be strict for known plugins.
Walk the list. Skip what doesn't apply. Add branches that fit the specific area touched.
Output format
Inline in the conversation, ~300–500 words. Group by severity. For each finding:
file:line reference (markdown link form [name](path#Lline) for VSCode click-through).
- one-sentence description of the bug.
- one-sentence trigger / exploit condition.
Do NOT propose fixes — just the bug. End with a one-sentence delta vs. the last red-team-plugin if one exists. Be terse. Be specific.
Cleanup
You are the main session — don't create scratch files. Output findings inline. End with a git status check; nothing new should be staged.