Skip to content

Commit 16a42fe

Browse files
committed
feat: add adversarial fuzz runtime contracts
1 parent c712739 commit 16a42fe

24 files changed

Lines changed: 1871 additions & 8 deletions

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# WP Codebox
22

3+
Adversarial campaign, transport fault, service disruption, browser oracle, and
4+
sealed replay contracts are documented in
5+
[`docs/adversarial-runtime.md`](docs/adversarial-runtime.md).
6+
37
**WP Codebox unlocks secure WordPress code execution from anywhere.** Run agents, accept untrusted patches, evaluate plugins, reproduce bugs, or experiment freely - every sandbox is a disposable contained WordPress runtime that can't touch its caller. Your host can be a CLI, CI job, mobile app, Node service, WordPress plugin, or anything else that can shell out or hit an API.
48

59
WordPress has historically lacked a clean scratch space for code execution. Modern dev workflows assume one - Node has `npm install` per project, Python has venvs, containers have ephemeral filesystems. WP Codebox provides that primitive as a usable runtime contract: real WordPress, fully ephemeral, no host filesystem access except via declared mounts. Any product - WordPress or not - can offer code execution against a real WordPress instance without risking the caller.

docs/adversarial-runtime.md

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
# Adversarial runtime
2+
3+
WP Codebox exposes backend-neutral adversarial campaign and transport-fault
4+
contracts from `@automattic/wp-codebox-core/public`. The contracts compose with
5+
the existing fuzz suite, runtime episode, checkpoint, browser, service, artifact,
6+
and replay surfaces; they do not replace those surfaces.
7+
8+
## Architecture
9+
10+
- `adversarial-campaign.ts` owns deterministic corpus scheduling, generic value
11+
and action-sequence mutation, novelty retention, bounded concurrent execution,
12+
minimization, stable finding fingerprints, replay schedules, and differential
13+
result classification.
14+
- `transport-faults.ts` owns request matching, deterministic stateful outcome
15+
sequences, capability negotiation, redacted evidence, and deduplication. It
16+
does not assume a browser, HTTP library, application, or transport.
17+
- `adversarial-browser.ts` owns DOM-derived journey planning, hostile generic
18+
input generation, journey minimization, and user-facing oracle contracts.
19+
- `adversarial-artifacts.ts` writes bounded manifested finding and replay bundles,
20+
removes declared secrets and machine-specific paths, and seals corpus/finding
21+
identity with a deterministic content digest.
22+
- `runtime-playground` maps supported fault outcomes and browser clock controls
23+
onto Playwright. Socket behavior that Playwright cannot faithfully produce is
24+
reported as unsupported.
25+
- `runtime-services.ts` provisions loopback-only, ephemeral MySQL/MariaDB, Redis,
26+
SMTP sink, and HTTP fixture services. Every service has idempotent teardown and
27+
typed disruption controls.
28+
29+
Generic layers contain no component, plugin, vendor integration, or product
30+
policy. WordPress grammars and attack packs remain extension responsibilities.
31+
32+
## Campaign contract
33+
34+
```ts
35+
const campaign = adversarialCampaign({
36+
id: "component-campaign",
37+
seed: "ci-seed-1",
38+
corpus: [{
39+
id: "create-update-read",
40+
actions: [
41+
{ type: "create", input: { title: "seed" } },
42+
{ type: "update", input: { title: "candidate" } },
43+
{ type: "read" },
44+
],
45+
}],
46+
budgets: {
47+
maxCases: 1000,
48+
workers: 4,
49+
maxCaseTimeMs: 30000,
50+
maxWallTimeMs: 300000,
51+
},
52+
oracles: [{
53+
schema: "wp-codebox/adversarial-oracle/v1",
54+
id: "state-integrity",
55+
severity: "high",
56+
}],
57+
})
58+
59+
const result = await runAdversarialCampaign(campaign, {
60+
execute: async (plan, signal) => adapter.execute(plan, signal),
61+
evaluate: async (plan, observation, oracles) =>
62+
adapter.evaluate(plan, observation, oracles),
63+
})
64+
```
65+
66+
Workers execute a deterministic round concurrently. Results are committed to the
67+
corpus in case-index order after every round, so completion timing cannot change
68+
the retained corpus or later mutations. Replay metadata records the seed,
69+
worker, iteration, matrix cell, minimized actions/input, fault schedule, exact
70+
provenance, normalized schedule, and a one-command replay instruction.
71+
72+
## Mutation and novelty
73+
74+
Core mutation supports generic scalar, structured, binary, and stateful sequence
75+
inputs. Mutation targets are selected from stable JSON paths using a seeded
76+
SHA-256 schedule. Extensions can represent multipart, serialized, markup, or
77+
domain-specific inputs as structured actions and register richer mutators at the
78+
adapter boundary without adding those grammars to runtime-core.
79+
80+
Adapters return bounded novelty signals such as coverage edges, route ids, state
81+
digests, hook ids, or query fingerprints. New signals retain a case in the
82+
interesting corpus. Minimization removes action chunks and shrinks input values,
83+
replaying each candidate through the same adapter and oracle set.
84+
85+
## Fault model
86+
87+
```json
88+
{
89+
"schema": "wp-codebox/transport-fault-model/v1",
90+
"seed": "fault-seed-1",
91+
"rules": [{
92+
"id": "verification-outage",
93+
"match": {
94+
"host": "service.example",
95+
"method": "POST",
96+
"path": "/verify"
97+
},
98+
"sequence": [
99+
{ "status": 500 },
100+
{ "delayMs": 30000, "timeoutMs": 1000 },
101+
{ "status": 200, "body": "malformed", "truncateAfterBytes": 4 }
102+
],
103+
"repeat": "last"
104+
}]
105+
}
106+
```
107+
108+
Each adapter publishes `wp-codebox/transport-fault-capabilities/v1`. Campaigns
109+
must negotiate before execution and fail closed when a required semantic is
110+
unsupported.
111+
112+
### Playwright fidelity
113+
114+
| Semantic | Fidelity |
115+
| --- | --- |
116+
| Response status/header/body substitution | Exact |
117+
| Delay, deterministic jitter, host remap | Exact |
118+
| Malformed/truncated payload, bandwidth, timeout, refusal/reset | Emulated and labeled |
119+
| Chunk framing, half-close, disconnect-after-N-bytes | Unsupported |
120+
121+
Playwright owns HTTP framing and sockets, so payload truncation must not be
122+
reported as a transport disconnect. A lower-level proxy provider is required for
123+
those exact semantics.
124+
125+
## Services
126+
127+
Recipe `inputs.services` accepts:
128+
129+
- `mysql`: MySQL 8.4 or MariaDB 11.4, preserving existing output and readiness
130+
behavior.
131+
- `redis`: ephemeral Redis with persistence disabled.
132+
- `smtp`: SMTP message sink plus its loopback inspection port.
133+
- `http`: deterministic loopback response fixture.
134+
135+
The provisioned service set exposes `control(serviceId, action, options)`.
136+
Container lifecycle actions (`stop`, `start`, `pause`, `resume`, `restart`,
137+
`disconnect`, and `reconnect`) have exact Docker fidelity. Provider-specific
138+
`flush` and read-only controls are exact where implemented. Network latency is
139+
explicitly unsupported by this provider and should use the transport-fault
140+
adapter instead.
141+
142+
All containers bind ephemeral loopback ports, use no persistent volumes, and are
143+
removed in reverse order after success, failure, cancellation, or timeout.
144+
145+
## Clock control
146+
147+
Playwright browser time supports exact freeze, advance, skew, and resume through
148+
its clock API. The same capability response explicitly marks server process,
149+
scheduler, and database clocks unsupported. A WordPress runtime extension is
150+
required to control those surfaces without faking server behavior in browser
151+
JavaScript.
152+
153+
## Browser oracles
154+
155+
The generic browser planner derives actions from visible control descriptors and
156+
generates empty, oversized, hostile punctuation, bidi, and Unicode inputs. Submit
157+
controls are repeated to expose duplicate side effects. Generic oracles cover:
158+
159+
- page errors, console errors, and unhandled rejections;
160+
- controls that accept input without an observable action;
161+
- loading indicators that exceed a declared threshold;
162+
- clipping, viewport overflow, and invisible focus;
163+
- adapter-provided accessibility violations;
164+
- duplicate observable effects.
165+
166+
Journey minimization uses deterministic subset replay to retain the shortest
167+
sequence that preserves the oracle failure.
168+
169+
## Evidence and safety
170+
171+
`writeAdversarialEvidenceBundle()` writes a manifest, campaign result, one
172+
finding and replay document per stable fingerprint, and a secret-scan report.
173+
The writer enforces an artifact byte ceiling, redacts sensitive fields and
174+
caller-declared values, removes machine-specific paths, and records SHA-256 file
175+
digests plus a stable bundle content digest.
176+
177+
Campaign budgets independently bound cases, actions, input bytes, per-case and
178+
wall time, worker count, and artifact bytes. Adapters remain responsible for the
179+
existing Codebox CPU, memory, disk, process, network-deny, mount, and disposable
180+
sandbox boundaries.
181+
182+
## Compatibility
183+
184+
Existing recipes and `fuzzRun` cases are unchanged. Existing MySQL defaults,
185+
outputs, readiness, and teardown behavior remain compatible. The new service
186+
kinds and public TypeScript contracts are additive.
187+
188+
## Current boundaries
189+
190+
The following capabilities are intentionally not claimed by this change:
191+
192+
- exact socket framing faults require a lower-level proxy provider;
193+
- server-side WordPress HTTP fault interception requires a WordPress extension
194+
adapter using the generic fault contract;
195+
- PHP/WordPress, cron, and database clock control require a WordPress extension;
196+
- WordPress-specific mutation grammars, security policies, and instrumentation
197+
remain extension-owned;
198+
- live vulnerable plugin/theme discovery campaigns require disposable runtime
199+
fixtures and are not replaced by the neutral contract tests.
200+
201+
These are capability gaps, not silent skips. Consumers can inspect negotiation
202+
results before running a campaign.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@
230230
"test:recipe-run-provenance": "tsx tests/recipe-run-provenance.test.ts",
231231
"test:fuzz-run-recipe": "tsx tests/fuzz-run-recipe.test.ts",
232232
"test:fuzz-suite-runner": "tsx tests/fuzz-suite-runner.test.ts",
233+
"test:adversarial-runtime": "tsx --test tests/transport-faults.test.ts tests/adversarial-campaign.test.ts tests/adversarial-browser.test.ts tests/browser-clock-control.test.ts",
233234
"test:playground-fuzz-suite-public": "tsx tests/playground-fuzz-suite-public.test.ts",
234235
"test:nested-fuzz-suite-recipe-command": "tsx tests/nested-fuzz-suite-recipe-command.test.ts",
235236
"test:wordpress-fuzz-suite-builders": "tsx tests/wordpress-fuzz-suite-builders.test.ts",

packages/cli/src/recipe-validation.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -742,9 +742,15 @@ function validateRecipeRuntimeServices(recipe: WorkspaceRecipe, addIssue: (code:
742742
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(service.id)) addIssue("invalid-runtime-service-id", `${path}.id`, "Runtime service ids must be stable identifiers.")
743743
if (ids.has(service.id)) addIssue("duplicate-runtime-service-id", `${path}.id`, `Runtime service ids must be unique: ${service.id}`)
744744
ids.add(service.id)
745-
if (service.kind !== "mysql") addIssue("unsupported-runtime-service-kind", `${path}.kind`, `Unsupported managed runtime service kind: ${service.kind}`)
745+
if (!["mysql", "redis", "smtp", "http"].includes(service.kind)) addIssue("unsupported-runtime-service-kind", `${path}.kind`, `Unsupported managed runtime service kind: ${service.kind}`)
746+
const supportedOutputs: Record<string, RegExp> = {
747+
mysql: /^(host|port|username|password|database)$/,
748+
redis: /^(host|port|url)$/,
749+
smtp: /^(host|port|httpPort|url)$/,
750+
http: /^(host|port|url)$/,
751+
}
746752
for (const [output, name] of Object.entries(service.outputs)) {
747-
if (!/^(host|port|username|password|database)$/.test(output)) addIssue("unknown-runtime-service-output", `${path}.outputs.${output}`, `Unsupported ${service.kind} service output: ${output}`)
753+
if (!(supportedOutputs[service.kind] ?? /^$/).test(output)) addIssue("unknown-runtime-service-output", `${path}.outputs.${output}`, `Unsupported ${service.kind} service output: ${output}`)
748754
if (!/^[A-Z_][A-Z0-9_]*$/.test(name)) addIssue("invalid-runtime-service-env", `${path}.outputs.${output}`, "Runtime service environment variable names must match /^[A-Z_][A-Z0-9_]*$/.")
749755
if (environment.has(name)) addIssue("duplicate-runtime-service-env", `${path}.outputs.${output}`, `Runtime service output environment variable is already declared: ${name}`)
750756
environment.add(name)

0 commit comments

Comments
 (0)