From 247c5bedf7ca58b8d604a3948180ceec64c2e8cd Mon Sep 17 00:00:00 2001 From: Nathan Clevenger <4130910+nathanclevenger@users.noreply.github.com> Date: Sat, 8 Aug 2026 14:37:23 -0500 Subject: [PATCH] =?UTF-8?q?feat(exec):=20enroll=20SUITE=5FLOADER=20?= =?UTF-8?q?=E2=80=94=20the=20hosted=20api.qa/vitest@1=20runner=20goes=20li?= =?UTF-8?q?ve?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The account (b6641681fe423910342b9ffa1364c76d) accepted the beta `worker_loaders` key today for the apis-vin exec rail; this flips api.qa's documented-but-disabled A.8.6 provisioning to ENABLED, the same same-worker-loopback shape: - wrangler.jsonc: "worker_loaders" SUITE_LOADER + "services" SUITE_OUTBOUND -> api-qa#SuiteGateway. main moves to src/entry.ts. - src/exec/gateway.ts (new): SuiteGateway WorkerEntrypoint — a thin mount over the unit-tested createOutboundGateway, with a MODULE-LEVEL sink so per-invocation entrypoint instances share one out-of-band violation record; fetch carries the floor, drainViolations the fail-closed drain. - src/entry.ts (new): the wrangler entry. Re-exports the default handler + both DO classes + SuiteGateway; the `cloudflare:workers` import lives one file above everything vitest resolves (the apis-vin entry-only trick), typed by a local ambient d.ts (no @cloudflare/workers-types). - GET /health?exec=1: the measured runner-availability probe — runs the fixed server-owned one-test suite through the same execRunner a verification uses, TTL-memoized per binding signature to bound unauthenticated isolate spins. Plain /health is byte-unchanged. - test/suite-loader-enrollment.test.ts: pins the enrolled config, the entry split (cloudflare:workers stays out of the vitest graph), the RPC-shaped async drain failing closed, and the probe's three states. Rollback: the code still feature-detects env.SUITE_LOADER, so reverting wrangler.jsonc to the flag-held shape is the whole rollback — absent bindings degrade to the typed runner-unavailable, never a crash. Verified: 1248 tests green, tsc clean, wrangler dry-run validates both bindings, and a local workerd run answers /health?exec=1 with { available: true, status: "ran" } from a real loader isolate. Co-Authored-By: Claude Fable 5 --- src/entry.ts | 18 ++ src/exec/cloudflare-workers.d.ts | 14 ++ src/exec/gateway.ts | 58 +++++++ src/exec/runner.ts | 13 +- src/worker.ts | 99 +++++++++-- test/suite-loader-enrollment.test.ts | 243 +++++++++++++++++++++++++++ wrangler.jsonc | 46 ++--- 7 files changed, 454 insertions(+), 37 deletions(-) create mode 100644 src/entry.ts create mode 100644 src/exec/cloudflare-workers.d.ts create mode 100644 src/exec/gateway.ts create mode 100644 test/suite-loader-enrollment.test.ts diff --git a/src/entry.ts b/src/entry.ts new file mode 100644 index 0000000..fffc476 --- /dev/null +++ b/src/entry.ts @@ -0,0 +1,18 @@ +/** + * entry.ts — the wrangler entry (wrangler.jsonc "main"), and NOTHING but the + * mount plate. It re-exports the whole deployed surface of src/worker.ts — + * the default { fetch, scheduled } handler plus the Durable Object classes + * wrangler discovers as named exports of `main` — PLUS the workerd-only + * SuiteGateway entrypoint (the A.8.6.3 egress gateway the SUITE_OUTBOUND + * loopback service binding names). + * + * WHY A SEPARATE FILE: src/exec/gateway.ts imports `cloudflare:workers`, + * which only the workerd runtime resolves — and the vitest suite imports + * src/worker.ts directly (createApp and friends). So the runtime-only import + * lives here, one file ABOVE the module every test resolves, and the test + * graph never sees it (the same entry-only import trick the apis-vin worker + * uses). test/suite-loader-enrollment.test.ts pins this split. + */ +export { default } from './worker.js' +export { DomainCooldown, MonitorSchedulerDO } from './worker.js' +export { SuiteGateway } from './exec/gateway.js' diff --git a/src/exec/cloudflare-workers.d.ts b/src/exec/cloudflare-workers.d.ts new file mode 100644 index 0000000..b5dbcad --- /dev/null +++ b/src/exec/cloudflare-workers.d.ts @@ -0,0 +1,14 @@ +/** + * Minimal ambient declaration for the `cloudflare:workers` runtime module — + * kept LOCAL and structural (the same stance as WorkerCodeLike in + * src/exec/runner.ts) so the repo keeps compiling without + * @cloudflare/workers-types. Declares only what src/exec/gateway.ts uses; + * the real types come from the workerd runtime at deploy time. + */ +declare module 'cloudflare:workers' { + /** Structural subset of the runtime's WorkerEntrypoint base class. */ + export abstract class WorkerEntrypoint { + protected env: Env + protected ctx: { waitUntil(promise: Promise): void } + } +} diff --git a/src/exec/gateway.ts b/src/exec/gateway.ts new file mode 100644 index 0000000..4a4c16b --- /dev/null +++ b/src/exec/gateway.ts @@ -0,0 +1,58 @@ +/** + * exec/gateway.ts — the deploy-time face of the A.8.6.3 egress gateway: a + * named `WorkerEntrypoint` of THIS worker, bound back to itself in + * wrangler.jsonc — + * + * "services": [{ "binding": "SUITE_OUTBOUND", "service": "api-qa", + * "entrypoint": "SuiteGateway" }] + * + * — the same same-worker-loopback shape the apis-vin exec rail deployed on + * this account. Every fetch a suite isolate makes rides `globalOutbound` to + * `SuiteGateway.fetch`, and the runner drains the out-of-band refusal record + * through the `drainViolations` RPC on the same binding (auto-detected by + * `workerLoaderExecRunner`'s `hasDrain`). + * + * ALL behavior lives in `createOutboundGateway` (src/exec/runner.ts), which + * is unit-tested without this module: the floor per request AND per redirect + * hop, the marked 403, the parent-owned violation sink. This class is a thin + * mount, nothing more. + * + * THE SINK IS MODULE-LEVEL on purpose: workerd constructs a fresh entrypoint + * INSTANCE per invocation, so an instance field would silently drop the + * record between the isolate's fetches and the runner's later drain. A + * loopback service binding to this same worker is served in-isolate, so the + * module-level gateway is the shared record both sides see. Sharing one sink + * across concurrent runs can only OVER-attribute a violation, which errs in + * the closed direction (a run may be failed by a neighbour's refusal, never + * passed by one) — the trade `createOutboundGateway`'s contract already + * names. If the drain ever crosses isolates (a platform change), the record + * comes back empty and the in-isolate violation channel still fails the run + * for any non-forged suite — degraded, never open. + * + * This module imports `cloudflare:workers`, so ONLY the wrangler entry + * (src/entry.ts) may import it — vitest imports src/worker.ts and never + * resolves this file (the apis-vin entry-only import trick). Enforced by + * test/suite-loader-enrollment.test.ts. + */ +import { WorkerEntrypoint } from 'cloudflare:workers' +import { createOutboundGateway, type OutboundGatewayLike } from './runner.js' +import type { GateViolation } from './dialect.js' + +/** The isolate-global gateway instance — the one record both halves share. */ +const gateway: OutboundGatewayLike = createOutboundGateway() + +export class SuiteGateway extends WorkerEntrypoint { + /** `globalOutbound` delivery: the floor, per request and per redirect hop. */ + async fetch(request: Request): Promise { + return gateway.fetch(request) + } + + /** + * Hand the out-of-band refusal record to the runner and clear it — the + * A.8.6.3 fail-closed half that makes a caught/absorbed refusal still fail + * the run. Exposed as an RPC method so the SAME binding carries both halves. + */ + async drainViolations(): Promise { + return gateway.drainViolations() + } +} diff --git a/src/exec/runner.ts b/src/exec/runner.ts index 55230b8..cd48d42 100644 --- a/src/exec/runner.ts +++ b/src/exec/runner.ts @@ -36,9 +36,11 @@ * whole run fails, never a partial verdict. * * FEATURE DETECTION. The Worker Loader binding is an open-beta, paid-plan - * capability. `wrangler.jsonc` documents (but does not enable) the binding so - * every account keeps valid deploys; `worker.ts` wires this runner ONLY when - * `env.SUITE_LOADER` exists. Anywhere the binding — or the outbound gateway — + * capability, ENABLED in `wrangler.jsonc` since the 2026-08-08 account + * enrollment (SUITE_LOADER + the SuiteGateway loopback outbound) — but + * `worker.ts` still wires this runner ONLY when `env.SUITE_LOADER` exists, + * so a config rollback keeps every deploy valid. + * Anywhere the binding — or the outbound gateway — * is absent, the runner is `unavailableExecRunner(...)`: a card that declares * `runner: "api.qa/vitest@1"` then FAILS with the reason named (the same * direction the ratified unknown-runner rule already gives an older @@ -195,8 +197,9 @@ export async function gatewayFetch( * erase it. Prefer one instance per run; a gateway shared across concurrent * runs can only over-attribute a violation, which errs in the CLOSED * direction (a run may be failed by a neighbour's refusal, never passed by - * one). At deploy time, expose this from a same-isolate loopback entrypoint - * (`ctx.exports`) so the runner can actually drain it. + * one). At deploy time this is exposed from a same-isolate loopback + * entrypoint (`SuiteGateway`, src/exec/gateway.ts — the SUITE_OUTBOUND + * service binding) so the runner can actually drain it. */ export function createOutboundGateway( realFetch?: (url: string, init?: RequestInit) => Promise, diff --git a/src/worker.ts b/src/worker.ts index 5ad51d7..40f1246 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -7,7 +7,8 @@ * GET /.well-known/agents.json * GET /icp.json * GET /openapi.json - * GET /health keyless liveness + * GET /health keyless liveness (?exec=1 adds the measured + * api.qa/vitest@1 runner-availability probe) * GET /offers/attested-run the 402 boundary (a structured offer, not an error) * GET /self api.qa's live verdict on api.qa (loopback, no network) * GET /{domain} the public grade page (markdown | HTML+JSON-LD | JSON) @@ -21,7 +22,7 @@ */ import { verifyTarget, rejudge } from './verify.js' -import type { ExecSuiteRunner } from './exec/dialect.js' +import { VITEST_RUNNER, type ExecSuiteRunner } from './exec/dialect.js' import { unavailableExecRunner, workerLoaderExecRunner, type WorkerLoaderLike } from './exec/runner.js' import { verifyPinnedSpec, verifySuite, parseSuite, type PinnedReport, type SuiteReport } from './pinned.js' import { reportMarkdown, pinnedMarkdown, suiteMarkdown } from './render.js' @@ -150,10 +151,12 @@ export interface Env { TS_ROLLUP_CAP?: string /** * Dynamic Worker Loader binding (`worker_loaders` in wrangler.jsonc) — the - * `api.qa/vitest@1` isolate runner (A.8.6.3). OPEN-BETA, PAID-PLAN: the - * binding is documented but NOT enabled in the shipped config, so every - * account keeps valid deploys; absent, a card declaring the executable - * dialect fails with a typed `runner-unavailable` reason. + * `api.qa/vitest@1` isolate runner (A.8.6.3). OPEN-BETA, PAID-PLAN: + * ENABLED in the shipped config since 2026-08-08 (the account accepted + * `worker_loaders` that day for the apis-vin exec rail). Still + * feature-detected — on any deployment without the binding, a card + * declaring the executable dialect fails with a typed `runner-unavailable` + * reason, so a config rollback can never crash or silently pass. */ SUITE_LOADER?: WorkerLoaderLike /** @@ -162,7 +165,8 @@ export interface Env { * runner refuses to run rather than inherit this worker's own network * access (the A.8.6.3 floor). Build it on `createOutboundGateway` * (src/exec/runner.ts) and expose it from a same-isolate loopback - * entrypoint (`ctx.exports`), so it carries BOTH halves of the floor: + * entrypoint (the SuiteGateway service binding in wrangler.jsonc, + * src/exec/gateway.ts), so it carries BOTH halves of the floor: * `fetch` (the refusal itself) and `drainViolations` (the out-of-band * record the runner folds into the verdict — the half that makes a * caught/absorbed refusal still fail the run, A.8.6.3 fail-closed @@ -198,6 +202,37 @@ export interface TickSummary { const LINKSET = '; rel="service-doc", ; rel="service-desc", ; rel="describedby"' +// ── GET /health?exec=1 — the runner-availability probe (2026-08-08 SUITE_LOADER +// enrollment). The plain /health answer is UNCHANGED (its declared contract is +// graded); the opt-in query adds one attested fact: whether this deployment can +// actually spin an `api.qa/vitest@1` isolate. The probe runs the FIXED, +// server-owned suite below through the same execRunner a verification uses — +// one registered test, no network — so "available" is a measured run, not a +// binding-presence claim. The result is memoized per binding-signature for +// EXEC_PROBE_TTL_MS in isolate-global state, bounding the metered/billed +// isolate spins an unauthenticated caller can trigger. + +/** The fixed probe suite — server-owned bytes, registers one test, fetches nothing. */ +export const EXEC_PROBE_TESTS = `it('the isolate runs', () => { expect(1).toBe(1) })` +/** How long one probe verdict answers for a given binding signature. */ +export const EXEC_PROBE_TTL_MS = 5 * 60_000 + +/** What /health?exec=1 reports — a typed state, never a crash. */ +export interface ExecProbeResult { + runner: typeof VITEST_RUNNER + /** True iff the probe suite actually RAN in a loader isolate just now (or within TTL). */ + available: boolean + /** The runner outcome status verbatim: 'ran' | 'failed' | 'runner-unavailable'. */ + status: string + /** The typed reason, when not 'ran'. */ + reason?: string + probedAtMs: number + /** True when this answer was served from the TTL memo, not a fresh isolate. */ + cached: boolean +} + +const execProbeMemo = new Map>() + const DOMAIN_ROUTE = /^\/([a-z0-9-]+(?:\.[a-z0-9-]+)+)$/i /** @@ -335,9 +370,9 @@ export function createApp( u.startsWith(SELF_ORIGIN) ? loopback(u, init) : (opts.externalFetcher ?? fetch)(u, init) // The `api.qa/vitest@1` execution seam (A.8.6) — FEATURE-DETECTED. The - // Worker Loader binding is an open-beta, paid-plan capability, so the - // deployment stays valid without it (wrangler.jsonc documents, but does not - // enable, the binding). Three states, all typed and none a crash: + // Worker Loader binding is an open-beta, paid-plan capability; wrangler.jsonc + // enables it (2026-08-08 enrollment), but the code keeps detecting it so a + // config rollback stays valid. Three states, all typed and none a crash: // binding + outbound gateway present → the isolate runner; // binding present, outbound absent → runner-unavailable (running // without a gateway would inherit THIS worker's network, which the @@ -351,6 +386,43 @@ export function createApp( ? workerLoaderExecRunner(env.SUITE_LOADER, { outbound: env.SUITE_OUTBOUND }) : unavailableExecRunner() + /** + * GET /health?exec=1 body: run (or answer from the TTL memo) the fixed + * probe suite through execRunner. Memo key = the binding signature, so a + * test's bindingless createApp can never be answered by a bound app's + * cached verdict (and vice versa); production has ONE signature per deploy. + */ + const execProbe = async (): Promise => { + const key = `${env.SUITE_LOADER ? 'loader' : '-'}:${env.SUITE_OUTBOUND ? 'outbound' : '-'}` + const atMs = now() + const hit = execProbeMemo.get(key) + if (hit && atMs - hit.probedAtMs < EXEC_PROBE_TTL_MS) return { ...hit, cached: true } + const outcome = await execRunner.run({ + artifactKind: 'document', + testsSource: EXEC_PROBE_TESTS, + origin: SELF_ORIGIN, + vars: {}, + environment: 'public', + sandbox: false, + seed: 1, + declarativeRows: 0, + // The probe bytes are server-owned constants, hashed here exactly as a + // card pin would be — a stable content-hash isolate id, so repeat + // probes warm-reuse one isolate instead of minting new ones. + digest: `sha256:${await sha256Hex(EXEC_PROBE_TESTS)}`, + limits: { cpuMs: 5_000, wallMs: 10_000 }, + }) + const fresh: Omit = { + runner: VITEST_RUNNER, + available: outcome.status === 'ran' && outcome.results.every((r) => r.status === 'pass'), + status: outcome.status, + ...(outcome.status !== 'ran' ? { reason: outcome.reason } : {}), + probedAtMs: atMs, + } + execProbeMemo.set(key, fresh) + return { ...fresh, cached: false } + } + /** * The actual tick body: claim + re-verify every DUE monitor through the * SAME attested verifyTarget/verifySuite/cooldown/SSRF path a fetch run @@ -672,7 +744,12 @@ export function createApp( if (path === '/.well-known/agents.json') return json(selfAgentsJson()) if (path === '/icp.json') return json(selfIcpJson()) if (path === '/openapi.json') return json(selfOpenapi()) - if (path === '/health') return json({ ok: true, verifier: 'api.qa', version: VERIFIER_VERSION }) + if (path === '/health') { + const base = { ok: true, verifier: 'api.qa', version: VERIFIER_VERSION } + // Opt-in runner probe; the plain declared-contract answer is untouched. + if (url.searchParams.get('exec') !== '1') return json(base) + return json({ ...base, exec: await execProbe() }) + } if (path === '/offers/attested-run') return json(selfOffer(), 402) // Brand assets. These MUST be matched before DOMAIN_ROUTE: that regex diff --git a/test/suite-loader-enrollment.test.ts b/test/suite-loader-enrollment.test.ts new file mode 100644 index 0000000..02eb042 --- /dev/null +++ b/test/suite-loader-enrollment.test.ts @@ -0,0 +1,243 @@ +/** + * SUITE_LOADER enrollment (2026-08-08) — the config + wiring invariants that + * make the hosted `api.qa/vitest@1` runner REAL on the deployed worker. + * + * Three families, all cheap and all load-bearing: + * + * 1. CONFIG — wrangler.jsonc actually enables the pieces the runner + * feature-detects: `worker_loaders` binding SUITE_LOADER, and the + * SUITE_OUTBOUND same-worker loopback service binding naming the + * SuiteGateway entrypoint. (The code path survives their absence by + * design — these pins assert the ENROLLED state so a silent config + * regression cannot quietly demote the runner to `runner-unavailable`.) + * + * 2. THE ENTRY SPLIT — src/exec/gateway.ts imports `cloudflare:workers`, + * which only workerd resolves, so it may be imported ONLY by the wrangler + * entry (src/entry.ts) and NEVER by src/worker.ts or anything the vitest + * graph reaches (the apis-vin entry-only import trick). The entry must + * also re-export the worker default handler and BOTH Durable Object + * classes, because wrangler discovers DO classes as named exports of + * `main` — dropping one would break the deploy's migrations. + * + * 3. THE RPC DRAIN SHAPE — in production SUITE_OUTBOUND is a service-binding + * stub whose `drainViolations` is an async RPC returning a structured + * clone. The runner's auto-detection (`hasDrain`) must treat that shape + * as the out-of-band record, so a forged all-green isolate body still + * fails on a gateway-recorded refusal (A.8.6.3 fail-closed totality). + */ + +import { describe, it, expect } from 'vitest' +import { readFileSync, readdirSync } from 'node:fs' +import { join, resolve, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' +import { + createOutboundGateway, + workerLoaderExecRunner, + RUNNER_UNAVAILABLE_NO_BINDING, + RUNNER_UNAVAILABLE_NO_OUTBOUND, + type WorkerLoaderLike, +} from '../src/exec/runner.js' +import type { ExecRunRequest } from '../src/exec/dialect.js' +import { createApp, EXEC_PROBE_TTL_MS, type Env, type ExecProbeResult } from '../src/worker.js' + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const wranglerText = readFileSync(join(repoRoot, 'wrangler.jsonc'), 'utf8') +const entryText = readFileSync(join(repoRoot, 'src', 'entry.ts'), 'utf8') +const gatewayText = readFileSync(join(repoRoot, 'src', 'exec', 'gateway.ts'), 'utf8') + +/** Every non-comment line of a JSONC file (leading-`//` lines dropped). */ +const activeWranglerLines = wranglerText + .split('\n') + .filter((line) => !line.trimStart().startsWith('//')) + .join('\n') + +/** Recursively list files under dir (paths relative to dir). */ +function walk(dir: string, prefix = ''): string[] { + return readdirSync(join(dir, prefix), { withFileTypes: true }).flatMap((e) => + e.isDirectory() ? walk(dir, join(prefix, e.name)) : [join(prefix, e.name)], + ) +} + +describe('enrollment config: wrangler.jsonc enables what the runner feature-detects', () => { + it('main is the entry split (src/entry.ts), not the vitest-imported worker module', () => { + expect(activeWranglerLines).toMatch(/"main":\s*"src\/entry\.ts"/) + }) + + it('the Worker Loader binding SUITE_LOADER is ENABLED (not comment-held)', () => { + expect(activeWranglerLines).toMatch(/"worker_loaders":\s*\[\s*\{\s*"binding":\s*"SUITE_LOADER"\s*\}\s*\]/) + }) + + it('SUITE_OUTBOUND is the same-worker loopback service binding naming SuiteGateway', () => { + expect(activeWranglerLines).toMatch( + /"services":\s*\[\s*\{\s*"binding":\s*"SUITE_OUTBOUND",\s*"service":\s*"api-qa",\s*"entrypoint":\s*"SuiteGateway"\s*\}\s*\]/, + ) + }) + + it("the config's entrypoint name matches the class gateway.ts actually exports", () => { + const m = /"entrypoint":\s*"([A-Za-z0-9_]+)"/.exec(activeWranglerLines) + expect(m).not.toBeNull() + expect(gatewayText).toContain(`export class ${m![1]} extends WorkerEntrypoint`) + }) +}) + +describe('the entry split: cloudflare:workers stays out of the vitest graph', () => { + it('src/entry.ts re-exports the default handler, BOTH DO classes, and SuiteGateway', () => { + expect(entryText).toMatch(/export \{ default \} from '\.\/worker\.js'/) + expect(entryText).toMatch(/export \{ DomainCooldown, MonitorSchedulerDO \} from '\.\/worker\.js'/) + expect(entryText).toMatch(/export \{ SuiteGateway \} from '\.\/exec\/gateway\.js'/) + }) + + it("src/exec/gateway.ts is the ONLY src module importing 'cloudflare:workers'", () => { + const importers = walk(join(repoRoot, 'src')) + .filter((f) => f.endsWith('.ts') && !f.endsWith('.d.ts')) + .filter((f) => /from\s+['"]cloudflare:workers['"]/.test(readFileSync(join(repoRoot, 'src', f), 'utf8'))) + expect(importers).toEqual([join('exec', 'gateway.ts')]) + }) + + it('no src module other than the entry imports the gateway, and nothing imports the entry', () => { + for (const f of walk(join(repoRoot, 'src')).filter((f) => f.endsWith('.ts'))) { + const text = readFileSync(join(repoRoot, 'src', f), 'utf8') + if (f !== 'entry.ts') expect(text, `${f} must not import exec/gateway`).not.toMatch(/from\s+['"][^'"]*exec\/gateway(\.js)?['"]/) + expect(text, `${f} must not import the wrangler entry`).not.toMatch(/from\s+['"][^'"]*\/entry(\.js)?['"]/) + } + // The test graph must never resolve them either — that is the whole trick. + for (const f of walk(join(repoRoot, 'test')).filter((f) => f.endsWith('.ts'))) { + const text = readFileSync(join(repoRoot, 'test', f), 'utf8') + expect(text, `test/${f} must not import src/entry or exec/gateway`).not.toMatch( + /^\s*import[^\n]*from\s+['"][^'"]*(\/entry|exec\/gateway)(\.js)?['"]/m, + ) + } + }) + + it('gateway.ts is a thin mount over createOutboundGateway with a MODULE-LEVEL sink', () => { + // A fresh entrypoint instance is constructed per invocation, so the shared + // record MUST live at module scope — an instance field would drop it + // between the isolate's fetches and the runner's drain. + expect(gatewayText).toMatch(/^const gateway[^\n]*=\s*createOutboundGateway\(\)/m) + expect(gatewayText).toMatch(/async fetch\(request: Request\)/) + expect(gatewayText).toMatch(/async drainViolations\(\)/) + }) +}) + +describe('the RPC drain shape: a service-binding-stub outbound still fails closed', () => { + const req: ExecRunRequest = { + artifactKind: 'document', + origin: 'https://target.example', + vars: {}, + environment: 'public', + sandbox: false, + seed: 7, + declarativeRows: 0, + testsSource: `it('x', () => {})`, + } + const okFetch = async (url: string): Promise => + new Response(JSON.stringify({ ok: true, url }), { status: 200, headers: { 'content-type': 'application/json' } }) + + it('a forged all-green isolate body cannot bury a refusal drained over the async RPC shape', async () => { + const gateway = createOutboundGateway(okFetch) + // What SUITE_OUTBOUND is in production: fetch + an ASYNC drain whose + // result crosses an RPC boundary as a structured clone, not the live array. + const rpcStub = { + fetch: (r: Request) => gateway.fetch(r), + drainViolations: async () => structuredClone(await gateway.drainViolations()), + } + // The gateway refused an egress during the run window… + await rpcStub.fetch(new Request('http://169.254.169.254/latest/meta-data/')) + // …but the isolate body claims a clean pass with zero violations. + const forgedLoader: WorkerLoaderLike = { + get: () => ({ + getEntrypoint: () => ({ + fetch: async () => + new Response( + JSON.stringify({ registered: 1, results: [{ name: 'x', status: 'pass', durationMs: 1 }], violations: [] }), + { headers: { 'content-type': 'application/json' } }, + ), + }), + }), + } + const outcome = await workerLoaderExecRunner(forgedLoader, { outbound: rpcStub }).run(req) + expect(outcome.status).toBe('failed') + if (outcome.status === 'failed') expect(outcome.reason).toContain('network floor') + }) +}) + +// --------------------------------------------------------------------------- +// GET /health?exec=1 — the post-deploy proof face +// --------------------------------------------------------------------------- + +describe('GET /health?exec=1 — the runner-availability probe', () => { + const probeOf = async (app: ReturnType): Promise => { + const res = await app.fetch(new Request('https://api.qa/health?exec=1')) + expect(res.status).toBe(200) + const body = (await res.json()) as { ok: boolean; exec: ExecProbeResult } + expect(body.ok).toBe(true) + return body.exec + } + + /** A loader that answers the probe suite with a clean one-test pass, counting spins. */ + const cannedLoader = () => { + const calls: string[] = [] + const loader: WorkerLoaderLike = { + get: (id) => { + calls.push(id) + return { + getEntrypoint: () => ({ + fetch: async () => + new Response( + JSON.stringify({ + registered: 1, + results: [{ name: 'the isolate runs', status: 'pass', durationMs: 1 }], + violations: [], + }), + { headers: { 'content-type': 'application/json' } }, + ), + }), + } + }, + } + return { loader, calls } + } + + it('the plain /health answer is UNCHANGED — no exec member without the opt-in query', async () => { + const res = await createApp({}).fetch(new Request('https://api.qa/health')) + expect(await res.json()).not.toHaveProperty('exec') + }) + + it('unprovisioned deployment: available:false with the typed no-binding reason', async () => { + const exec = await probeOf(createApp({}, { now: () => 1_000_000 })) + expect(exec.available).toBe(false) + expect(exec.status).toBe('runner-unavailable') + expect(exec.reason).toBe(RUNNER_UNAVAILABLE_NO_BINDING) + }) + + it('binding present but no outbound gateway: refuses to run open, by name', async () => { + const { loader } = cannedLoader() + const exec = await probeOf(createApp({ SUITE_LOADER: loader } as Env, { now: () => 2_000_000 })) + expect(exec.available).toBe(false) + expect(exec.reason).toBe(RUNNER_UNAVAILABLE_NO_OUTBOUND) + }) + + it('enrolled shape: a MEASURED run answers available:true, and the TTL memo bounds isolate spins', async () => { + const { loader, calls } = cannedLoader() + const outbound = createOutboundGateway() + let t = 10_000_000 + const env = { SUITE_LOADER: loader, SUITE_OUTBOUND: outbound } as Env + const app = createApp(env, { now: () => t }) + + const first = await probeOf(app) + expect(first).toMatchObject({ runner: 'api.qa/vitest@1', available: true, status: 'ran', cached: false }) + // The probe's isolate id is the CONTENT HASH of the fixed probe bytes — + // repeat probes warm-reuse one isolate instead of minting new ones. + expect(calls[0]).toMatch(/^vitest1:sha256:[0-9a-f]{64}:/) + + t += EXEC_PROBE_TTL_MS - 1 + const second = await probeOf(createApp(env, { now: () => t })) + expect(second).toMatchObject({ available: true, cached: true, probedAtMs: first.probedAtMs }) + expect(calls).toHaveLength(1) + + t += 2 + const third = await probeOf(createApp(env, { now: () => t })) + expect(third.cached).toBe(false) + expect(calls).toHaveLength(2) + }) +}) diff --git a/wrangler.jsonc b/wrangler.jsonc index 1a24e5d..e8c9d27 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -22,7 +22,10 @@ // skips every check above. Tests: scripts/test-deploy.sh. { "name": "api-qa", - "main": "src/worker.ts", + // src/entry.ts re-exports the app worker (src/worker.ts — what vitest + // imports) + the workerd-only SuiteGateway entrypoint (imports + // `cloudflare:workers`, so it stays out of the vitest module graph). + "main": "src/entry.ts", "compatibility_date": "2026-07-01", "routes": [{ "pattern": "api.qa", "custom_domain": true }], "workers_dev": true, @@ -53,26 +56,27 @@ { "binding": "REPORTS", "id": "90eb42161ef045eb8b38b01cf87f985a" } ], - // ── api.qa/vitest@1 executable-suite runner (AXP A.8.6) — FLAG-HELD ────── - // The Dynamic Worker Loader binding is an OPEN-BETA, PAID-PLAN capability: - // enabling it on an unenrolled account breaks `wrangler deploy`, so the - // binding ships DOCUMENTED-BUT-DISABLED and the code feature-detects - // `env.SUITE_LOADER` (absent ⇒ a card declaring runner "api.qa/vitest@1" - // fails with a typed `runner-unavailable` reason — never a crash, never a - // silent pass; see src/exec/runner.ts). To provision, on an enrolled - // account, uncomment BOTH pieces: - // - // "worker_loaders": [ { "binding": "SUITE_LOADER" } ], - // // SUITE_OUTBOUND — the egress gateway (its fetch handler wraps - // // `createOutboundGateway` from src/exec/runner.ts: the A.8.6.3 network - // // floor, re-checked per redirect hop, PLUS the out-of-band - // // `drainViolations` record the runner folds into the verdict so a - // // caught refusal still fails the run). REQUIRED: without it the runner - // // REFUSES to execute rather than let an isolate inherit this worker's - // // own network access. Prefer a same-isolate loopback entrypoint - // // (`ctx.exports.SuiteGateway`) over a separate worker, so - // // `drainViolations` is actually drainable by the runner: - // "services": [ { "binding": "SUITE_OUTBOUND", "service": "api-qa", "entrypoint": "SuiteGateway" } ], + // ── api.qa/vitest@1 executable-suite runner (AXP A.8.6) — ENROLLED ─────── + // The Dynamic Worker Loader binding is an OPEN-BETA, PAID-PLAN capability, + // ENABLED here 2026-08-08: account b6641681fe423910342b9ffa1364c76d accepted + // `worker_loaders` the same day for the apis-vin exec rail (the precedent + // this config copies — same account, same same-worker-loopback outbound + // shape). The code still FEATURE-DETECTS `env.SUITE_LOADER`, so if a deploy + // ever refuses the key, reverting this file to the flag-held shape is the + // whole rollback: absent bindings ⇒ a card declaring runner + // "api.qa/vitest@1" fails with a typed `runner-unavailable` reason — never + // a crash, never a silent pass (see src/exec/runner.ts). + "worker_loaders": [{ "binding": "SUITE_LOADER" }], + // SUITE_OUTBOUND — the egress gateway: the SuiteGateway entrypoint + // (src/exec/gateway.ts, exported from src/entry.ts) wraps + // `createOutboundGateway` from src/exec/runner.ts — the A.8.6.3 network + // floor, re-checked per redirect hop, PLUS the out-of-band + // `drainViolations` record (an RPC on this same binding) the runner folds + // into the verdict so a caught refusal still fails the run. REQUIRED: + // without it the runner REFUSES to execute rather than let an isolate + // inherit this worker's own network access. Same-worker loopback on + // purpose: served in-isolate, so the runner can actually drain the record. + "services": [{ "binding": "SUITE_OUTBOUND", "service": "api-qa", "entrypoint": "SuiteGateway" }], // // Isolate posture (enforced in src/exec/runner.ts, pinned by unit tests): // env is ALWAYS {} (zero ambient authority — no SIGNING_KEY, no KV, no DOs,