# Untrusted Agent Harness Test Plan

## Goal

Prove that:

- two untrusted nodes can coordinate through a shared folder
- private inputs stay local unless explicitly released
- Gondolin execution is isolated and deterministic enough for approval gating
- the harness fails closed on tampering, races, and stale approvals

## Test Layers

### Unit Tests

Focus on pure Rust logic:

- request manifest hashing
- approval basis hashing
- state machine transitions
- stale approval invalidation
- release path filtering
- alias-to-private-path resolution

### Integration Tests

Run two local node instances against temp directories:

- `shared/`
- `node_a/private/`
- `node_b/private/`

Use a fake or test Gondolin runner first, then a real Gondolin-backed suite for the critical paths.

### End-to-End Tests

Run the full workflow with real Gondolin:

- create request
- approve code
- approve exec
- execute
- approve release
- publish artifact

## Minimum Test Matrix

### 1. Request Protocol

`creates_request_manifest_with_stable_digest`

- same request data gives same digest
- field reorder in JSON serialization does not change digest

`rejects_request_if_workspace_snapshot_changes`

- approve code against snapshot A
- mutate workspace
- recompute digest
- assert approval no longer applies

### 2. Approval Gates

`cannot_execute_without_code_and_exec_approval`

- request exists
- no approvals recorded
- execution attempt is blocked

`cannot_release_without_release_approval`

- execution produced staged outputs
- release approval missing
- no files copied to shared release dir

`reject_path_is_terminal_until_new_revision`

- code rejected
- later execution attempt blocked
- new request revision required

### 3. Private Data Safety

`private_alias_must_resolve_locally`

- remote request asks for alias
- local mapping missing
- request enters blocked/error state

`deny_release_keeps_private_output_local`

- sandbox writes result into local staging
- release denied
- shared artifact dir remains empty

`release_copies_only_selected_files`

- local staging contains:
  - `result.json`
  - `raw_private_dump.txt`
- release manifest allows only `result.json`
- assert only approved file is published

### 4. Gondolin Binding

`workspace_is_mounted_read_only`

- guest attempts to modify `/input/workspace`
- write fails

`private_mount_is_read_only`

- guest attempts to modify `/input/private`
- write fails

`output_mount_is_writable`

- guest writes to `/output/result.json`
- file appears in local staging

`network_disabled_by_default`

- guest tries outbound network access
- command fails

### 5. Tamper Detection

`approval_basis_hash_changes_when_command_changes`

- approve execution for command A
- mutate command to B
- execution blocked

`approval_basis_hash_changes_when_private_input_set_changes`

- approve execution with one private alias
- add another alias
- prior approval invalid

`release_is_blocked_if_staged_output_changes_after_review`

- user reviews staged output digest A
- file changes before release
- release approval no longer valid

### 6. Two-Node Workflow

`node_a_requests_node_b_evaluation_and_b_releases_result`

- A writes request to shared state
- B processes it locally
- B approves all stages
- released artifact appears in shared folder
- A can read released artifact

`node_b_denies_release_and_a_receives_only_status`

- B executes locally
- B rejects release
- shared thread shows rejection status
- no artifact published

### 7. Replay and Concurrency

`replayed_approval_record_is_rejected`

- record approval for request revision 1
- create revision 2 with same request id but new digest
- old approval does not apply

`concurrent_nodes_do_not_corrupt_shared_state`

- run two writers against shared manifests
- use file locking
- assert manifests remain valid JSON

## Realistic E2E Scenarios

These are the first three scenarios worth automating.

### Scenario A: Safe Analysis

- Shared repo contains source code.
- Node A asks Node B to run tests with B's local fixture data.
- B approves code, exec, and release.
- Released output contains only summarized test report.

Expected result:

- report published
- raw fixture data not published

### Scenario B: Suspicious Exfiltration

- Request asks for a command that cats private files into `/output`.
- B approves code review by mistake but rejects release after inspecting outputs.

Expected result:

- local staged secret exists
- shared release dir remains empty

### Scenario C: Post-Approval Mutation

- B approves code and exec.
- Shared workspace changes before execution starts.

Expected result:

- harness detects digest mismatch
- execution blocked until re-approval

## Suggested Rust Test Files

If you build this as a Rust crate, split tests roughly like this:

```text
tests/
  protocol_hashing.rs
  approval_state_machine.rs
  release_filters.rs
  gondolin_mounts.rs
  two_node_workflow.rs
  tamper_detection.rs
```

## Test Harness Shape

Use temp directories and a lightweight driver:

```rust
struct TestNode {
    shared_dir: PathBuf,
    local_dir: PathBuf,
    private_dir: PathBuf,
}
```

Helpers:

- `create_request(...)`
- `approve(stage, ...)`
- `run_execution(...)`
- `inspect_staging(...)`
- `release(...)`
- `read_shared_artifact(...)`

## Success Criteria

The prototype is doing the right thing when these remain true:

- a denied release never leaks staged outputs into shared state
- approvals are invalidated by any material input change
- Gondolin mount permissions match the plan
- two nodes can exchange requests and released artifacts without sharing private stores
