Skip to Content
EngineeringDecisions (ADRs)ADR 0031 One master agent, one harness per run, enforced from the run row; lens reviews read-only; no external MCP servers

ADR 0031: One master agent, one harness per run, enforced from the run row; lens reviews read-only; no external MCP servers

Status
Accepted
Date
Deciders
Founder

Context

The founder asked for a master agent that impersonates each role through a harness and behaves differently per role. Fact (verified in source): run_tokens_for(run) reads the run row and mints once per run with no allowlists, so every token carries allowlist_hash(()) and the server accepts either the role hash or EMPTY_ALLOWLIST_HASH; run.status is already read from the database on every call. A harness enforced by a prompt, or by a hash that is never re-minted, is not enforced.

Decision

  • HarnessSpec(key, personas, prompt_ref, toolset, skills, memory_lenses, rubric_key, plan_templates, refusal_rules, status_template, record_types); HARNESS_VERSION = "harness_v1", PROMPT_VERSION = "console_v2"; HarnessSpec.toolset is the only tool-to-harness relation (no ToolSpec.harnesses).
  • agent_runs.harness_key is the authority. The API sets it when the run is created; run_tokens_for copies it into a harness run-token claim; load_call_state refuses 401 harness_mismatch on a difference; tools/list = tools_for_role(role) ∩ HARNESSES[run.harness_key].toolset; check_allowed(..., harness) returns harness_denied, audited like policy_denied. A harness never changes inside a run; no token is re-minted; the allowlist hash is unchanged. The worker’s bootstrap envelope carries harness_key, harness_version, harness_personas and twin.workbench_role_key so the container wears the row’s harness; tools/list refuses 401 run_not_found when the run row is missing and never falls back to the claim (recorded 2026-09-16).
  • Lens reviews (R5): one AgentRun(kind='lens_review') of sequential graph nodes under a read-only approved plan, on a person’s explicit request only, at most four lenses; no verdict word, no aggregate, no ranking; “A lens is not a review. Only a <role> reviews.”; a scorecard is never a stage-gate required_record, a check input or an approvals attachment; a lens requested by the proposer cannot attach to the proposer’s decision record.
  • External MCP servers are not proxied in any release: rejected while a REST API exists; revisit only for a source with no REST API; if ever added, wrap named upstream tools as static ToolSpecs and never forward an upstream tools/list. The structural test “no external server mounted” stays.

Consequences

  • agent_runs gains harness_key, harness_version, rubric_version; the audit envelope records run_id, harness_key, harness_version, rubric_key, rubric_version, act.
  • Untrusted reads advertise _meta.trovensa.untrusted_text and the container drops them for every non-reader agent; structured reads carry a required output_schema enforced in _call_read (the 35 phase-2/R3 reads in LEGACY_READS_WITHOUT_OUTPUT_SCHEMA are exempt until R5; the set is pinned so it can only shrink; recorded 2026-09-16).
  • Twin runs keep persona_key as the authority for duties.

Alternatives considered

  • Re-minting tokens with a per-harness allowlist hash: the mechanism does not exist and would add a second authority; rejected.
  • One child run per lens: cost and a parent-child protocol; rejected for sequential nodes in one run.
  • Proxying vendor MCP servers: an upstream tools/list would bypass the closed catalogue; rejected.

Follow-ups

  • Measure lens runs before raising max_lenses_per_run above four.
  • Harness prompts for the remaining workbench roles (R5).