Assay-Runner Phase 1 Spike Plan¶
Status: accepted on 2026-05-21 for the delegated Linux/eBPF Phase 1 path; internal spike record, not a public roadmap commitment Date: 2026-05-20 Scope: expands the Phase 1 proof spike from
ASSAY-RUNNER-PRODUCT-CANDIDATE-2026-05-18.md; no external name, repo split, hosted service, or product build is implied.
This plan turns the Assay-Runner candidate memo into a bounded spike contract. It answers one question:
Can Assay produce one verifiable measured-run bundle per shim mode, with low-ambiguity layer correlation and honest observation health?
If the answer is no, this track stops.
Phase 1 Result¶
Phase 1 passed on delegated Linux/eBPF hardware on 2026-05-21.
The acceptance record is ASSAY-RUNNER-PHASE1-ACCEPTANCE-2026-05-21.md. It records workflow run 26211485614, commit 56571045, and gates=all passing for kernel-only, kernel+policy, and OpenAI Agents kernel+policy+SDK.
Non-Goals¶
Phase 1 must not become a hidden product launch. It explicitly does not include:
- public naming work beyond the internal
Assay-Runnerplaceholder - a new public repository
- a hosted service, dashboard, or production sidecar
- Phase 2 capability diff as a PR comment
- sequence semantics or causal ordering across layers
- event-level signatures or signed tool-call receipts
- cross-platform full measurement claims
The only deliverable is a local spike proving or disproving attribution.
Placement¶
Spike code should live in Rul1an/assay, not in a new repository.
Allowed source layout:
| Path | Purpose | Public promise |
|---|---|---|
crates/assay-runner-spike/ | Optional publish-disabled orchestration crate for the spike | None |
crates/assay-cli/src/cli/commands/runner_spike.rs | Hidden CLI entrypoint, if needed for demos | None |
tests/fixtures/runner-spike/ | Tiny deterministic programs and MCP fixtures | None |
docs/notes/ASSAY-RUNNER-PHASE1-SPIKE-PLAN-2026-05-20.md | This plan | Internal only |
If a crate is added, its name must include spike and it must be publish-disabled. A hidden test harness is also acceptable if that proves the attribution question with less surface area. Do not create a publishable assay-runner crate before Phase 1 passes.
Existing Surfaces To Reuse¶
The spike should be mostly assembly:
| Need | Existing surface |
|---|---|
| Linux event capture | crates/assay-monitor::Monitor |
| cgroup scope | Monitor::set_monitored_cgroups, Monitor::attach_network_cgroup |
| ring-buffer health | MonitorStatsSnapshot::total_ringbuf_dropped |
| process tree fallback | crates/assay-monitor::tree::ProcessTreeTracker |
| policy decision identity | crates/assay-core/src/mcp/proxy/decisions.rs::extract_tool_call_id |
| policy decision event | DecisionEvent in crates/assay-core/src/mcp/decision_next/event_types.rs |
| bundle run-id discipline | crates/assay-evidence bundle writer and verifier |
| deterministic bundle check | assay evidence verify |
| LLM replay | assay-core::vcr |
| sandbox baseline | assay sandbox and Landlock docs |
| adversity inputs | crates/assay-sim attack and chaos patterns |
Do not duplicate these semantics inside the spike unless there is no callable boundary yet.
Known integration gaps:
extract_tool_call_idis not currently a stable public API for a separate spike crate. S4 may require a smallassay-coreadapter or visibility relaxation before correlation code can call it directly.- Assay currently has more than one internal
extract_tool_call_idfunction. Before S4 correlation work begins, choose one canonical extraction path for policy events and spike-side correlation, then cover that choice with a contract test.
Spike Output Contract¶
The spike writes one local archive:
The archive should be accepted by assay evidence verify or by a clearly named temporary verifier if the existing bundle writer cannot carry every layer yet. If a temporary verifier is needed, the plan must record the gap before any Phase 2 work begins.
Minimum file shape:
manifest.json
events.ndjson
layers/kernel.ndjson
layers/policy.ndjson
layers/sdk.ndjson
capability-surface.json
observation-health.json
correlation-report.json
Allowed empty files:
layers/policy.ndjsonwhenpolicy_layer=absentlayers/sdk.ndjsonwhensdk_layer=absent
No layer may be silently omitted.
Observation Health Contract¶
Phase 1 freezes this v0 shape:
{
"schema": "assay.runner.observation_health.v0",
"run_id": "run_...",
"platform": "linux",
"kernel_layer": "complete",
"ringbuf_drops": 0,
"policy_layer": "present",
"sdk_layer": "absent",
"cgroup_correlation": "clean",
"notes": []
}
Allowed values:
| Field | Values |
|---|---|
kernel_layer | complete, partial_ringbuf_drops, absent |
policy_layer | present, absent |
sdk_layer | present, self_reported, absent |
cgroup_correlation | clean, partial, failed |
Rules:
ringbuf_drops > 0forceskernel_layer=partial_ringbuf_drops.- non-Linux runs force
kernel_layer=absent. --agent-shim noneforcessdk_layer=absent.- an SDK shim may only set
sdk_layer=self_reportedunless kernel or policy evidence corroborates the specific event boundary. cgroup_correlation=failedis a failing Phase 1 run, even if the bundle verifies.
Capability Surface V0¶
Phase 1 summarizes kernel observations into deterministic sets:
{
"schema": "assay.runner.capability_surface.v0",
"run_id": "run_...",
"filesystem_prefixes": ["/tmp/assay-runner-spike/"],
"network_endpoints": ["127.0.0.1:8080"],
"process_execs": ["/usr/bin/curl"],
"mcp_tools": ["filesystem.read_file"],
"policy_decisions": ["allow:filesystem.read_file"]
}
Serialization must be deterministic:
- sort every set lexicographically before writing JSON
- use stable path normalization rules
- use stable endpoint formatting:
host_or_ip:port - no timestamps in set entries
- no event ordering claims
This prevents byte-level diff noise before Phase 2 exists.
Correlation Model¶
Phase 1 correlation is set-based and window-based.
Required keys:
| Key | Source | Purpose |
|---|---|---|
run_id | runner-spike | binds all layers into one stream |
cgroup_id or cgroup_path | runner-spike / monitor | kernel scope anchor |
pid / process tree | monitor | child process grouping and fallback debugging |
tool_call_id | MCP proxy / SDK shim | policy and SDK join key |
window_start / window_end | runner-spike | bounded attribution window |
Timing rule:
- all runner-owned windows use one canonical clock source; on Linux, prefer a monotonic clock for joins
- SDK-shim timestamps are recorded as self-reported informational fields only and must not be used as the sole basis for kernel or policy attribution
Allowed claim:
Tool-call T occurred in window W, policy saw T, and kernel events K were
observed inside the measured cgroup during W.
Forbidden claim:
Workstreams¶
S0: Freeze The Spike Contract¶
Deliverables:
- this plan merged
- one follow-up issue or checklist for each workstream
- no code changes yet
Exit gate:
- second-party internal review agrees the spike is bounded and can be killed without product fallout; if no second party is available, record a written self-checklist in this note family before code work starts
S1: Runner Boundary And run_id¶
Implement the minimal orchestration boundary:
Hidden CLI is acceptable. A direct test harness is also acceptable if adding a CLI surface creates too much churn.
Responsibilities:
- generate one
run_id - create or identify one cgroup scope on Linux
- launch the child command inside that scope
- record
window_startandwindow_end - write an initial manifest and empty layer files
Acceptance:
- one command run produces one archive
- archive includes
run_idin every file that has a run identity - non-Linux produces a degraded bundle with
kernel_layer=absent
S2: Kernel Layer And Health¶
Use assay-monitor rather than adding new probes.
Responsibilities:
- configure monitored cgroup
- listen to monitor events during the run window
- snapshot monitor stats before and after the run
- compute
ringbuf_drops - write
layers/kernel.ndjson - derive
filesystem_prefixes,network_endpoints, andprocess_execs
Acceptance:
- a fixture that opens a known file records that file prefix
- a fixture that executes a known binary records that binary
- a fixture that connects to a local TCP endpoint records that endpoint where monitor support exists
- three repeated runs produce the same health metadata and same capability set for the deterministic fixture
S3: none + kernel-only¶
This is the epistemic proof path.
Fixture:
The fixture should:
- read one file under a temp directory
- write one file under a temp directory if write events are observable
- execute one stable binary such as
/usr/bin/env - optionally connect to a local listener
Acceptance:
sdk_layer=absentpolicy_layer=absentkernel_layer=completeon Linux when no drops occurcgroup_correlation=clean- the bundle verifies with
assay evidence verifyor, until runner-spike archives are carried byassay-evidence, with the temporaryscripts/ci/runner-spike-kernel-only-acceptance.shverifier - three repeated fixture runs verify with
scripts/ci/runner-spike-kernel-only-three-run-determinism.sh
S4: none + kernel+policy¶
This proves Runner still works when the agent uses Assay policy surfaces but does not use an SDK shim.
Fixture:
The fixture should route one MCP tool call through assay mcp wrap or a minimal existing MCP proxy path.
Acceptance:
sdk_layer=absentpolicy_layer=present- policy event includes a stable
tool_call_id - kernel capability set is congruent with the allowed tool call
- correlation report joins policy to kernel by
run_id, window, and cgroup - the fixture verifies with
scripts/ci/runner-spike-kernel-policy-acceptance.sh - three repeated fixture runs verify with
scripts/ci/runner-spike-kernel-policy-three-run-determinism.sh
S5: openai-agents Shim¶
This is adoption proof, not epistemic proof.
Transport is fixed for the spike:
- runner-spike launches
nodeas a subprocess - a small JS wrapper owns the
@openai/agentsSDK invocation - the wrapper writes normalized SDK events as NDJSON to an explicit file or file descriptor supplied by runner-spike
- runner-spike consumes that stream and writes
layers/sdk.ndjson - stdout/stderr remain diagnostic channels; they are not the canonical SDK event stream
The first S5 implementation slice may ingest a prewritten normalized SDK event log through the same hidden runner boundary. That only freezes the assay.runner.sdk_event.v0 contract; it is not enough to claim the openai-agents shim has passed until the subprocess transport and deterministic fixture are wired.
The next S5 implementation slice proves the transport boundary with a deterministic SDK fixture:
Runner supplies ASSAY_RUNNER_SDK_EVENT_LOG, ASSAY_RUNNER_RUN_ID, and ASSAY_RUNNER_SDK_EVENT_SCHEMA to the measured subprocess. The subprocess writes normalized SDK NDJSON to that path; stdout and stderr remain diagnostic only.
The following S5 slice cross-checks SDK self-report against the existing policy layer:
tests/fixtures/runner-spike/sdk-policy-agent.sh
scripts/ci/runner-spike-sdk-policy-correlation.sh
scripts/ci/runner-spike-sdk-policy-three-run-determinism.sh
scripts/ci/runner-spike-sdk-policy-mismatch.sh
scripts/ci/runner-spike-sdk-policy-mismatch-three-run-determinism.sh
When a policy layer is present, every SDK tool-call id must match an existing policy correlation binding. A missing binding marks the correlation report partial with sdk_tool_call_without_policy_binding:<tool_call_id>. SDK events still do not create bindings or promote kernel/policy claims by themselves. The mismatch verifier intentionally emits a distinct SDK tool-call id and must observe that partial-correlation ambiguity across three deterministic runs.
The first real JavaScript SDK slice runs the same correlation gate through a Node subprocess that imports @openai/agents directly and uses a local deterministic model provider to force one read_file function-tool call:
runner-fixtures/openai-agents/fixture-agent.js
runner-fixtures/openai-agents/sdk-policy-agent.sh
scripts/ci/runner-spike-openai-agents-sdk-policy-correlation.sh
scripts/ci/runner-spike-openai-agents-sdk-policy-three-run-determinism.sh
scripts/ci/runner-spike-openai-agents-sdk-policy-mismatch.sh
scripts/ci/runner-spike-openai-agents-sdk-policy-mismatch-three-run-determinism.sh
This proves the @openai/agents runtime hook path and the runner-supplied SDK event stream without a live LLM request. It still is not the full delegated Linux kernel+policy+SDK acceptance gate: that requires the privileged eBPF host to run the kernel capture alongside the JavaScript shim. The OpenAI Agents mismatch verifier intentionally emits the SDK tool-call id tc_runner_sdk_only_001 while policy keeps tc_runner_policy_001, and must observe the same sdk_tool_call_without_policy_binding:<tool_call_id> ambiguity across three deterministic runs.
The full delegated-host S5 gate combines the real JavaScript SDK fixture with clean cgroup kernel capture and the MCP policy fixture:
scripts/ci/runner-spike-openai-agents-kernel-policy-acceptance.sh
scripts/ci/runner-spike-openai-agents-kernel-policy-three-run-determinism.sh
.github/workflows/runner-spike-delegated.yml
That gate is Linux/eBPF-only. It may skip on ordinary hosted CI, and S5 is not complete until this gate passes on a delegated cgroup host with kernel_layer=complete, policy_layer=present, sdk_layer=self_reported, cgroup_correlation=clean, empty correlation ambiguities, and stable health/surface/correlation/policy/SDK artifacts across three runs.
The delegated workflow is workflow_dispatch-only and runs on [self-hosted, linux, assay-bpf-runner] so ordinary PR CI cannot be blocked by runner availability. A skip from the underlying scripts is a failure in that lane: the delegated host is the place where Linux, cgroup v2, Node 22, and the eBPF artifact are expected to exist.
Keep it thin:
- emit normalized SDK events
- carry
tool_call_id - record SDK package/version when available
- never promote SDK events to verified side effects by themselves
- keep the wrapper killable with the measured process tree
Normalized SDK events:
Determinism is mandatory:
- no live LLM calls are allowed in the spike suite
- S5 uses
assay-core::vcrreplay in strict mode, for exampleASSAY_VCR_MODE=replay_strict, with a checked-in cassette or fixture - if replay cannot be wired through
@openai/agents, use a deterministic degenerate SDK fixture and record that blocker before claiming S5 pass
Acceptance:
sdk_layer=self_reportedtool_call_idmatches policy-layertool_call_idwhere MCP policy is used- SDK events arrive through the subprocess NDJSON stream, not through ad hoc log scraping
- the S5 fixture runs in replay-strict or deterministic fixture mode
- kernel event set for the tool window is stable across three runs
- SDK-only data cannot make
kernel_layer=complete
S6: Correlation Report¶
Write a machine-readable report:
{
"schema": "assay.runner.correlation_report.v0",
"run_id": "run_...",
"status": "clean",
"bindings": [
{
"tool_call_id": "tc_001",
"policy_decision": "allow",
"kernel_event_count": 3,
"window": {
"start": "2026-05-20T00:00:00Z",
"end": "2026-05-20T00:00:01Z"
}
}
],
"ambiguities": []
}
Status values:
clean: all expected joins presentpartial: a layer is absent or a join is incomplete but declaredfailed: attribution is too ambiguous for the run to count
Acceptance:
- no successful Phase 1 demo may have
status=failed - ambiguity reasons are deterministic strings, not free-form diagnostics only
S7: Adversarial Spike Checks¶
Use assay-sim patterns or small local fixtures to stress the attribution model before declaring Phase 1 successful.
Required checks:
- child process forks before doing the file access
- child exits quickly after access
- two sibling processes touch different files in the same run
- policy-denied tool call produces a policy event without claiming matching allowed side effects
- induced or simulated ring-buffer pressure sets
kernel_layer=partial_ringbuf_drops
Exit gate:
- if any normal deterministic fixture produces unstable attribution across three runs, Phase 1 fails
Scenario Matrix¶
| Scenario | Shim | Policy | Expected health | Required proof |
|---|---|---|---|---|
| raw file/process run | none | absent | kernel complete on Linux | stable kernel capability set |
| raw run on non-Linux | none | absent | kernel absent | valid degraded bundle |
| MCP allowed tool | none | present | kernel complete, policy present | policy to kernel congruence |
| MCP denied tool | none | present | policy present | no false allowed side-effect claim |
| OpenAI Agents tool call | openai-agents | present where possible | SDK self-reported plus policy/kernel | stable tool to policy to kernel binding |
| ringbuf pressure | any | any | kernel partial | visible health warning |
Hard Kill Criteria¶
Stop the track if any of these are true after two implementation passes:
- cgroup correlation cannot be made
cleanfor the raw deterministic fixture - policy-to-kernel attribution is unstable across three repeated runs
tool_call_idcannot be carried through the OpenAI Agents path without brittle SDK internals- ordinary runs produce ring-buffer drops often enough that
completeis not the normal Linux result - bundle verification requires a parallel artifact system instead of an incremental extension to
assay-evidence
If the track stops, write a short closure note instead of continuing into Phase 2.
Closure note path:
The closure note is the sole deliverable when the track stops. It must record the failed criterion, the evidence observed, and whether any spike code should be deleted or left quarantined for later reference.
Success Criteria¶
Phase 1 passes only when all of the following are true:
- one
none + kernel-onlybundle verifies - one
none + kernel+policybundle verifies - one
openai-agentsbundle verifies or has a documented SDK blocker that does not affect thenoneproof path - all three bundle types include observation health
- deterministic fixtures are stable across three repeated Linux runs
- adversarial checks do not break attribution silently
- all successful outputs are explicit about absent, partial, and self-reported layers
Passing Phase 1 does not authorize a public product launch. It only authorizes Phase 2 planning for capability diff.
Phase 2 Handoff, If Phase 1 Passes¶
The next plan should be ASSAY-RUNNER-PHASE2-CAPABILITY-DIFF-PLAN-*.
It must start from Phase 1 artifacts and answer:
- how capability surfaces are compared
- how ignore rules are expressed
- how Harness projects the diff
- which warnings block or annotate PR output
- which outputs are canonical JSON versus reviewer Markdown
Do not start Phase 2 until Phase 1 has real bundles checked into a fixture or artifact location.
Review Checklist¶
- The plan keeps Runner separate from Assay-Harness.
- The spike can be killed cleanly.
noneremains the first proof path.- observation health is mandatory.
- deterministic serialization is required before diff work.
- no public naming, repo split, or external claims are introduced.