Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .markdownlint-cli2.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@
"globs": [
"**/*.md"
],
// `node_modules` alone only matches the repository-root directory. Isolated
// spikes carry their own dependency trees (for example the ADR-006 Electron
// harness required by the Issue #11 E2E probe), and linting vendored README
// files of third-party packages produces hundreds of irrelevant findings.
"ignores": [
"node_modules",
".git"
"**/node_modules",
"**/.git"
]
}
2 changes: 2 additions & 0 deletions spike/runtime-eval/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# SHA256-gelockte Spike-Assets: keine EOL-Konversion, sonst bricht verify-determinism.mjs
* -text
148 changes: 148 additions & 0 deletions spike/runtime-eval/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Spike: Runtime-Evaluation common test app (ADR-006)

- Status: **framework-neutral payload + assertion spec only.** The four
platform harnesses (Electron / CEF / WebView2 / Tauri) are a later,
OS-specific step and are **not** in this directory.
- Implements the ADR-006 "Common test app" clause: **one** deterministic,
network-disabled payload with **identical** assertions reused for every
candidate runtime. See `docs/adr/ADR-006-runtime-evaluation-protocol.md`
sections *Common test app*, *Measurements*, *Security gates*, *Deliverables*.
- Disjoint from `spike/cwap-canonical-json/` (that spike is untouched).

## What this is

A static, zero-dependency web payload plus a machine-readable expected-result
spec. Each candidate runtime loads the **same** `payload/index.html`, lets it
run, then reads `window.__spikeResults` and compares each probe against
`assertions.json`. Because the payload bytes are identical everywhere
(enforced by `verify-determinism.mjs`), any difference in results is a property
of the *runtime*, not of the test.

## Layout

```text
spike/runtime-eval/
payload/
index.html entry; sets the CSP + Trusted-Types meta, loads app.mjs
app.mjs probe runner; writes window.__spikeResults + DOM
probe-module.mjs sibling module (proves static + dynamic ES import)
sw.js minimal service worker (no fetch handler, no network)
style.css static styling (no @import / no url() fetch)
assertions.json expected status per probe + host-level assertions
asset-manifest.json committed SHA256 of every payload asset (the byte lock)
verify-determinism.mjs determinism gate (pure SHA256 asset-manifest check)
README.md this file
```

## Probes (what the payload exercises)

Capability (must work on a correct Chromium-based runtime):
`module_loading`, `indexeddb`, `cachestorage`, `service_worker`, `webassembly`.

Security negative fixtures (secure outcome = `BLOCKED`):
`csp_eval` (no `unsafe-eval`), `csp_inline_script`, `trusted_types`
(`require-trusted-types-for 'script'`), `popup`, `external_protocol`,
`native_ipc_zero_grant` (no native bridge reachable without a grant).

Destructive fixtures (registered on `window.__spikeFixtures`, **never**
auto-run): `crash`, `hang`, `oversized`, `navigateExternal`,
`triggerDownload`. The harness triggers these in isolated runs while it
measures crash containment / host recovery / hang detection.

Status vocabulary: `PASS` / `BLOCKED` / `FAIL` / `READY` / `SKIP`
(defined in `assertions.json`).

## Determinism contract

- **No** `Date.now()`, `performance.now()`, or `Math.random()` value is ever
written into `window.__spikeResults`. Probe order is fixed; each probe carries
a stable integer `index`. The WebAssembly probe uses a fixed embedded module
(`add(2,3)==5`), not a build step.
- The DOM is built with `createElement`/`textContent` only, so the payload stays
Trusted-Types-clean while the TT probe deliberately triggers the violation in
an isolated `try/catch`.
- `asset-manifest.json` pins the SHA256 of every payload byte. Regenerate it
**only** after an intentional payload edit:

```bash
node verify-determinism.mjs --update # regenerate the byte lock
node verify-determinism.mjs # verify; exit 0 = identical, 1 = drift
```

The gate hashes the payload twice (proving hashing determinism) and compares
the aggregate + per-asset hashes against the committed manifest. Verified
outcomes at authoring time: clean `exit 0`, tampered byte `exit 1`, missing
manifest `exit 1`.

## How a future platform harness uses this

Each harness (Electron/CEF/WebView2/Tauri) is a thin native shell that:

1. **Verify the byte lock first** — run `node verify-determinism.mjs` in CI so
every candidate provably loads identical payload bytes.
2. **Start with networking disabled** — this is the test subject, not an
optional hardening. `connect-src 'none'` is set in the payload, but the
harness MUST *also* start the runtime with egress blocked (e.g. Electron:
deny in `session.webRequest.onBeforeRequest` / offline mode; CEF: no network
service or a blocking `CefRequestHandler`; WebView2: a blocking
`WebResourceRequested` filter; Tauri: no `http`/`shell` capabilities) and
assert **zero** sockets/DNS during the run.
3. **Load `payload/index.html`** from the app's own scheme/local origin (not a
remote URL). Apply the app-context security gate for that candidate from
ADR-006 *Security gates* (e.g. Electron: `sandbox:true` + `contextIsolation:
true` + `nodeIntegration:false` + `webSecurity:true`).
4. **Wait for completion**, then read `window.__spikeResults` (JSON object;
also mirrored into the `#results-json` DOM node for out-of-process readers).
A `undefined` result = the module never ran = harness FAIL.
5. **Compare** each `probes[i].status` against `assertions.json`
`probes[id].expected` (an array of acceptable statuses). Record every
deviation in the ADR-006 raw matrix — do **not** collapse to a single score.
6. **Assert the host-level facts** in `assertions.json.hostLevelAssertions`
that the payload cannot see from JS (no external-protocol handler launched,
no file written by `triggerDownload`, external navigation denied, crash
contained). Trigger `window.__spikeFixtures.*` in **isolated** runs for these.

### Reading `window.__spikeResults`

```js
// inside the harness, after the page signals done (#status[data-state=done]):
const results = await webContents.executeJavaScript('window.__spikeResults'); // Electron example
for (const probe of results.probes) {
const rule = assertions.probes[probe.id];
const ok = rule && rule.expected.includes(probe.status);
record(candidate, probe.id, probe.status, ok ? 'as-expected' : 'DEVIATION');
}
```

## Measurement TODO hooks (harness-owned, ADR-006 *Measurements*)

The payload gives correctness; the harness owns the numbers. Wire these as
TODOs per candidate and publish raw samples (p50/p95), never a synthetic score:

- [ ] **cold start** — process spawn → `#status[data-state=done]`, fresh profile.
- [ ] **warm start** — same, second launch with warm caches.
- [ ] **idle resident memory** — after done, at rest.
- [ ] **memory after 1 / 5 / 10 apps** — N payload instances resident.
- [ ] **package / runtime download size** — shipped bytes per candidate.
- [ ] **CPU idle vs active render** — sample during idle and during render.
- [ ] **Web API / Wasm compatibility** — the probe matrix above.
- [ ] **engine security-release → user-patch latency** — measurable for
self-bundled Chromium (Electron/CEF CI), only observable for WebView2.
- [ ] **crash containment / host recovery** — `__spikeFixtures.crash`.
- [ ] **hang / unresponsive handling** — `__spikeFixtures.hang`.
- [ ] **oversized-resource handling** — `__spikeFixtures.oversized`.
- [ ] **profile / storage separation** — distinct data dirs per app instance.
- [ ] **CSP / custom scheme / secure-context behaviour** — probe + scheme setup.
- [ ] **accessibility + keyboard operation** — table is reachable/operable.
- [ ] **reproducible build + SBOM coverage** — per-candidate build provenance.

## Non-goals / limits (honest scope)

- This directory contains **no** runtime and **no** native harness code — those
need Win/Mac/Linux builds and are the next, platform-specific step.
- JS-only probes (`popup`, `external_protocol`, `native_ipc_zero_grant`) can
only observe the in-page effect; the OS-level truth (handler launch, download
write, ungranted-bridge call rejection) is asserted by the harness.
- `verify-determinism.mjs` checks **asset bytes**, not runtime results. It uses
a pure SHA256 manifest because `jsdom` is not a dependency of this zero-dep
repo; a harness that wants DOM-level determinism can add a jsdom check itself.
41 changes: 41 additions & 0 deletions spike/runtime-eval/assertions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"schema": "runtime-eval-assertions/v1",
"description": "Expected probe outcomes a CORRECTLY isolated, network-disabled runtime must produce. A harness reads window.__spikeResults after loading payload/index.html and compares each probe's status against expected[] (an ordered set of acceptable statuses). Any deviation is a finding for ADR-006's raw matrix, not an auto-fail of the payload.",
"statusVocabulary": {
"PASS": "capability worked as a correctly isolated runtime should",
"BLOCKED": "hostile/ungranted action correctly denied (the secure outcome)",
"FAIL": "outcome deviated from the isolated-runtime expectation",
"READY": "destructive fixture registered + callable by harness, not auto-run",
"SKIP": "API genuinely absent in a non-security-relevant way"
},
"notes": [
"Capability probes (module_loading, indexeddb, cachestorage, webassembly) MUST PASS on any Chromium-based candidate (Electron/CEF/WebView2). A system-WebView divergence surfaces here as a finding.",
"service_worker may be BLOCKED under some load schemes (opaque origin / custom scheme without SW support); both PASS and BLOCKED are acceptable, FAIL is not.",
"trusted_types is SKIP-acceptable only if the runtime lacks Trusted Types support entirely; where supported it MUST be BLOCKED. Record the runtime + version when SKIP.",
"native_ipc_zero_grant expects BLOCKED (no bridge exposed). WebView2 injects window.chrome.webview by design: if that surfaces, the payload reports FAIL and the harness MUST additionally prove the bridge rejects ungranted calls; a callable ungranted bridge is a hard-cut failure.",
"external_protocol/popup can only be observed from JS as a refused window.open. The harness MUST also assert at OS level that no external handler launched and no new top-level window/download occurred.",
"Destructive fixtures are never auto-executed; the harness triggers window.__spikeFixtures.* in isolated runs and records crash containment, host recovery, hang detection and oversized-resource handling as separate measurements."
],
"probes": {
"module_loading": { "category": "capability", "expected": ["PASS"] },
"indexeddb": { "category": "capability", "expected": ["PASS"] },
"cachestorage": { "category": "capability", "expected": ["PASS"] },
"service_worker": { "category": "capability", "expected": ["PASS", "BLOCKED"] },
"webassembly": { "category": "capability", "expected": ["PASS"] },
"csp_eval": { "category": "security", "expected": ["BLOCKED"] },
"csp_inline_script": { "category": "security", "expected": ["BLOCKED"] },
"trusted_types": { "category": "security", "expected": ["BLOCKED", "SKIP"] },
"popup": { "category": "security", "expected": ["BLOCKED"] },
"external_protocol": { "category": "security", "expected": ["BLOCKED"] },
"native_ipc_zero_grant": { "category": "security", "expected": ["BLOCKED"] },
"destructive_fixtures": { "category": "fixture", "expected": ["READY"] }
},
"hostLevelAssertions": {
"network": "No socket/DNS activity during the entire result path. connect-src 'none' is set in the payload; the harness MUST additionally start the runtime with networking disabled and assert zero egress.",
"external_protocol_os": "No external URL handler process is spawned when external_protocol / navigateExternal is exercised.",
"download_os": "No file is written to the download directory when triggerDownload is exercised (download denied by default).",
"navigation_os": "Top-level navigation to an external origin (navigateExternal) is denied by the will-navigate handler; document.location origin is unchanged.",
"crash_containment": "crash fixture kills only the app renderer/process, not the host; host recovers and can relaunch.",
"hang_detection": "hang fixture triggers the runtime's unresponsive-renderer handling; host stays responsive."
}
}
13 changes: 13 additions & 0 deletions spike/runtime-eval/asset-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"schema": "runtime-eval-asset-manifest/v1",
"algorithm": "sha256",
"assetCount": 5,
"aggregate": "59cc2ac145f149e3531a82e3456552394cfdc0cf908d346a5cf6e745499243ce",
"assets": {
"app.mjs": "35628d9e3f1dd53e2ff910f3e5e66ee11cefc2233737cfbcc6fbb29e43f783e2",
"index.html": "c7ac0fc9c9e90a7d61159c136c9b8ff3219a2d553f50570f6c6962ed6ea3cddc",
"probe-module.mjs": "c4bdd4a36528b5f7898b6e7303cc0dff980ac4542e681862e7c64f099e281c40",
"style.css": "c1645b373a395ecde29afec93d3b1c0c6ed83f80af267a71cb60e5bd74377218",
"sw.js": "43cf7372b591ba5c06cf651a849f7b5d856f7134e3a6d924fe511170f13ca693"
}
}
2 changes: 2 additions & 0 deletions spike/runtime-eval/harness/electron/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
package-lock.json
Loading