# Untrusted Agent Harness Architecture

## Goal

Build a Rust CLI harness that lets two mutually untrusted users run agent-driven tasks against a shared workspace while keeping:

- private data local to each user
- sandbox execution isolated in Gondolin
- code review, execution, and result release fail-closed behind explicit human approval

The design here uses:

- `pi` as the orchestration model and event shape reference
- `gondolin` as the sandbox and mount/bind mechanism

## Core Design

Treat each user as a separate node running the same harness binary against:

- a shared folder
- a local private store
- a local approval UI
- a local Gondolin runtime

Each node can receive requests from the other side, evaluate them locally, and only release an approved artifact back into the shared folder.

## Trust Boundaries

There are four distinct trust zones:

1. Shared workspace
   Contains code, the human-readable chat thread, and released artifacts.
   Both users can read and modify it.

2. Local private store
   Contains secrets, private datasets, credentials, and any local-only scratch state.
   Never mounted into the shared workspace.

3. Local sandbox
   A Gondolin VM created per evaluation.
   Receives a bounded snapshot of shared code plus explicitly selected private inputs.

4. Human approval boundary
   No code executes and no result is released without local user approval.

## Folder Layout

Keep the shared folder human-readable, but do not use `thread.md` as the only machine state.
Use sidecar manifests for integrity and replay protection.

Suggested layout:

```text
shared/
  thread.md
  workspace/
    ...
  .rho/
    requests/
      <request_id>.json
    approvals/
      <request_id>.json
    releases/
      <request_id>.json
    artifacts/
      <request_id>/
        released/
          result.json
          stdout.txt
          stderr.txt
    locks/
```

Per-node local state:

```text
local-node/
  private/
    datasets/
    secrets/
  staging/
    <request_id>/
      bundle/
      outputs/
      inspection/
  cache/
  policy.json
```

## Main Objects

Suggested Rust modules:

- `protocol`
  Request, approval, release, hashes, status enums.

- `shared_state`
  Reads/writes `.rho/` manifests, file locking, append-only updates.

- `thread`
  Updates `thread.md` with readable summaries and links to request ids.

- `planner`
  Accepts a prompt from the local agent and converts it into an evaluation request.

- `approval`
  Local policy engine and user interaction layer.
  Three gates: code, execution, release.

- `bundle`
  Builds the evaluation bundle: code snapshot, manifest, selected private inputs, run config.

- `sandbox`
  Gondolin adapter.
  Creates VM, mounts inputs, executes command, collects outputs.

- `inspection`
  Produces diffs, file lists, output summaries, and release previews for the user.

- `release`
  Copies approved outputs from local staging into shared `.rho/artifacts/.../released`.

- `node`
  Top-level loop for one user node.
  Watches shared requests and processes those addressed to the local user.

## Protocol

The shared protocol should be append-only and content-addressed.

### Request

A request should include:

- `request_id`
- `from_node`
- `to_node`
- `thread_ref`
- `prompt`
- `workspace_paths`
- `requested_private_inputs`
- `entry_command`
- `expected_outputs`
- `code_digest`
- `created_at`

`code_digest` must cover the exact workspace files that will be mounted into Gondolin.
If those files change later, prior approvals become invalid.

### Approval Record

Approval is local, but the final decision state should be publishable as a signed or hashed manifest.

Suggested approval stages:

- `code_review`
- `execution`
- `release`

Each approval record should include:

- `request_id`
- `stage`
- `decision`: `approved` or `rejected`
- `approver`
- `basis_hash`
- `reason`
- `timestamp`

`basis_hash` should bind:

- request manifest
- code digest
- private input manifest
- planned exec command
- output manifest if release stage

### Release Record

Only written after release approval:

- `request_id`
- `released_by`
- `artifact_digest`
- `released_paths`
- `timestamp`

## State Machine

Keep the request lifecycle explicit and fail-closed:

```text
requested
-> code_approved
-> exec_approved
-> executed
-> release_approved
-> released
```

Terminal rejection states:

- `code_rejected`
- `exec_rejected`
- `release_rejected`
- `execution_failed`

Important rule:
Any material change to code, command, private inputs, or staged outputs moves the request back to the last safe pre-approval state.

## Recommended Pi-Inspired Control Flow

Use pi's hook model as inspiration, but keep approval tools outside the model's control where possible.

Recommended flow:

1. Remote request appears in shared state.
2. Local harness loads it and creates a local evaluation plan.
3. The local agent may inspect shared code and propose:
   - what private data to bind
   - what command to run
   - what outputs to stage
4. Harness freezes the proposed bundle and computes hashes.
5. Human approves code review.
6. Harness creates Gondolin VM and asks for execution approval.
7. Sandbox runs against the frozen bundle and writes outputs to local staging only.
8. Harness shows diff/output summary and asks for release approval.
9. If approved, selected artifacts are copied into shared release paths.

The model should be allowed to propose.
The harness should decide.

## Recommended Tool Surface

Do not expose raw “approve” tools to the model.
That lets the model drive the safety boundary itself.

Instead expose tools like:

- `propose_eval`
  Produces an evaluation request and bundle plan.

- `inspect_request`
  Reads request metadata, code digest, and requested private inputs.

- `inspect_staged_result`
  Reads stdout, stderr, result manifest, and output files from local staging.

- `submit_for_approval`
  Transitions the request into a local approval-needed state.

- `poll_approval`
  Lets the model learn whether a human approved or rejected a stage.

- `release_result`
  Should only succeed when the harness has already recorded release approval.

The actual user approval should come from the CLI/TUI, not from an LLM-issued tool call.

## Gondolin Execution Model

Per request, create a fresh VM with:

- shared workspace snapshot mounted read-only
- private inputs mounted read-only
- local output mount mounted read-write
- network disabled by default

Suggested guest mounts:

```text
/input/workspace   -> frozen shared workspace snapshot (ro)
/input/private     -> selected local private inputs (ro)
/output            -> local staging output dir (rw)
/tmp               -> ephemeral scratch
```

Execution command should be deterministic and recorded in the request manifest.

Example:

```text
/bin/sh -lc "cd /input/workspace && ./run-eval.sh > /output/stdout.txt 2> /output/stderr.txt"
```

Avoid mounting the live shared folder directly into the VM for untrusted execution.
Freeze a snapshot first.

## Private Data Binding

Private data should not be passed as arbitrary host paths from the remote requester.

Use local aliases instead:

- requester asks for `customer_db_subset`
- local node maps that alias to a local path
- approval UI shows the resolved local source before execution

This avoids path traversal and prevents the remote side from probing local filesystem structure.

## Human Approval UX

The three approvals should present different data:

### 1. Code Approval

Show:

- request prompt
- requested workspace files
- code digest
- private data aliases
- exact command

### 2. Execution Approval

Show:

- finalized mount plan
- Gondolin config
- network policy
- output paths

### 3. Release Approval

Show:

- output summary
- stdout/stderr preview
- produced files
- release destination
- redaction warning if output may contain private data

## Integrity Rules

These rules are the important ones:

- shared request manifests are immutable once accepted
- approvals bind to content hashes, not just request ids
- sandbox outputs are staged locally first
- no direct write from sandbox to shared release paths
- release copies only approved files
- any mismatch invalidates approval and blocks release

## Minimal Rust Interfaces

```rust
pub struct EvalRequest {
    pub request_id: String,
    pub from_node: String,
    pub to_node: String,
    pub prompt: String,
    pub workspace_paths: Vec<String>,
    pub private_inputs: Vec<String>,
    pub entry_command: Vec<String>,
    pub expected_outputs: Vec<String>,
    pub code_digest: String,
}

pub enum ApprovalStage {
    CodeReview,
    Execution,
    Release,
}

pub enum Decision {
    Approved,
    Rejected,
}

pub struct ApprovalRecord {
    pub request_id: String,
    pub stage: ApprovalStage,
    pub decision: Decision,
    pub basis_hash: String,
    pub reason: Option<String>,
}

pub struct SandboxPlan {
    pub workspace_snapshot: std::path::PathBuf,
    pub private_mounts: Vec<std::path::PathBuf>,
    pub output_dir: std::path::PathBuf,
    pub command: Vec<String>,
    pub network_enabled: bool,
}
```

## First Implementation Slice

Build this in order:

1. Shared request protocol and file locking
2. Local approval state machine
3. Frozen bundle builder
4. Gondolin adapter for one-shot execution
5. Local staging and inspection
6. Release copier
7. Thread rendering into `thread.md`

That gets you a working prototype without needing multi-agent sophistication first.

## What Not To Do

- Do not let the sandbox write directly into shared released artifacts.
- Do not treat `thread.md` as authoritative machine state.
- Do not bind live private directories by raw remote-supplied path.
- Do not reuse old approvals after code or output changes.
- Do not let the model self-approve execution or release.
