DEV Community

Avery Li
Avery Li

Posted on

The Pairing Halted Until Every Agent Assumption Had a Named Owner

A pairing session that involves an agent should fail closed whenever an environmental fact still lacks a named owner. Unowned guesses travel farther than a bad function, because they hide inside tools, defaults, and generated glue. This article reconstructs a senior-and-junior pairing protocol around a machine-checkable assumption ledger, including dead ends the pair discarded. The decision that survived review is simple: no remote model call proceeds until a local test suite returns green.

Why the senior stopped the first model call

The junior arrived with a sketch for an agent that would inspect a brownfield service and emit a deploy checklist. The senior blocked the first remote completion because workspace identity, readable paths, and outbound network rules were still implied. Three facts felt obvious to the junior and unknown to the senior, which is how pairing sessions leak credentials into prompts. The halt was a boundary on invention, not a debate about model brands or agent taxonomies.

Public writeups this week keep celebrating agents that fill gaps with fluent defaults when architecture knowledge is missing. That habit is convenient during a demo and expensive inside a shared repository with real secrets. The senior treated missing context as a test failure rather than as a prompt-engineering opportunity. Pairing continued only after those failures had owners.

Questions asked during the first twenty minutes

The senior did not open with temperature, tool counts, or a glossary of agentic terms circulating on developer forums. Each question had to become an assertion in a test module, with a human owner, before any completion request left the machine. Conversation was allowed, but conversation without a failing test was treated as unfinished work.

  1. Which process identity will execute generated commands, and who attests that identity on this workstation?
  2. Which paths may tools read, and which paths are secrets, vendor noise, or explicitly out of scope?
  3. Which outbound destinations are allowed, including package indexes, internal APIs, and model endpoints?
  4. Which facts remain unknown, and who is forbidden from filling them with a fluent guess?

The junior tried to answer those points in chat, which felt faster and more collaborative in the moment. The senior required answers as assertions because a transcript does not fail a continuous integration job. Rows without an owner were treated as defects, not as later homework for an unnamed team. The chat log remained a scratchpad; the test module became the contract the pair would keep.

Dead ends the pair walked into and left

The first dead end was packing the repository tree into one prompt and trusting the model to skip secrets. Ignore lists rot as soon as a new dotenv file appears, and a remote prompt still leaves the operator workstation. The second dead end was letting the agent run unconstrained listing and environment commands so it could discover context without paperwork. Unscoped discovery is still an assumption, only faster, noisier, and harder for a reviewer to reconstruct after the fact.

The third dead end was cloning a public agent workflow and filling gaps with typical values from recent posts. Typical values are not evidence, and they collapse on private registries, locked runners, and half-migrated single-page apps. A fourth dead end appeared when the junior suggested marking the agent itself as owner of unknown rows, which looked tidy in the file. The senior rejected agent ownership because a model cannot attest a secret boundary it has not been shown on purpose.

The decision the pair kept

The pair kept a fail-closed rule: every assumption is a typed record with a human owner, a status, and a proof. Unknown remains unknown until a person writes a proof command or an explicit block with a reason. The agent may propose candidate rows after the suite is green, but it may never flip a row to known by itself. Remote completions stay disabled while any test reports an unowned fact, an unknown fact, or a known fact with an empty proof.

Disclosure: This article was prepared as part of MonkeyCode's product outreach. After the local suite is green, the pair may send a bounded prompt through MonkeyCode's free model access, or run the same checks on MonkeyCode's free server option, without treating either option as a replacement for named ownership of assumptions.

Artifact: a pytest ledger the session can run

The following module is a pairing gate for a workstation and is labeled as a reconstructed example, not as a production policy engine. Pairs should keep the file in the repository and run it before any remote completion is attempted. Status values are closed: known, blocked, or unknown. Forbidden owners include empty strings, team, agent, and ai.

# test_pairing_assumptions.py
from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
import os
import pwd

import pytest

ALLOWED_STATUS = {"known", "blocked", "unknown"}
FORBIDDEN_OWNERS = {"", "team", "agent", "unknown", "n/a", "ai"}


@dataclass(frozen=True)
class Assumption:
    id: str
    statement: str
    owner: str
    status: str
    proof: str


LEDGER: list[Assumption] = [
    Assumption(
        id="exec.identity",
        statement="Generated commands run as the unprivileged pairing user, not root.",
        owner="junior.dev",
        status="known",
        proof="id -u != 0",
    ),
    Assumption(
        id="fs.read_scope",
        statement="Tools may read ./app and ./docs; they must not read .env or ssh keys.",
        owner="junior.dev",
        status="known",
        proof="paths exist and .env is not readable by the test process",
    ),
    Assumption(
        id="net.allowlist",
        statement="Outbound pairing traffic is limited to the agreed model endpoint.",
        owner="senior.dev",
        status="blocked",
        proof="network policy not yet written",
    ),
    Assumption(
        id="runtime.python",
        statement="The service uses the interpreter pin in .python-version.",
        owner="junior.dev",
        status="unknown",
        proof="",
    ),
]

ALLOW_MODEL_CALL = False


def test_every_row_has_a_human_owner() -> None:
    for row in LEDGER:
        owner = row.owner.strip().lower()
        assert owner not in FORBIDDEN_OWNERS, f"{row.id} needs a named human owner"


def test_status_values_are_closed() -> None:
    for row in LEDGER:
        assert row.status in ALLOWED_STATUS, f"{row.id} has illegal status {row.status}"


def test_unknown_rows_fail_closed() -> None:
    unknown = [row.id for row in LEDGER if row.status == "unknown"]
    assert unknown == [], f"unknown assumptions block pairing: {unknown}"


def test_known_and_blocked_rows_carry_proof() -> None:
    for row in LEDGER:
        if row.status in {"known", "blocked"}:
            assert row.proof.strip(), f"{row.id} is {row.status} without proof"


def test_process_is_not_root_when_claimed() -> None:
    row = next(r for r in LEDGER if r.id == "exec.identity")
    if row.status != "known":
        pytest.skip("identity not claimed as known")
    assert os.geteuid() != 0
    assert pwd.getpwuid(os.geteuid()).pw_name


def test_read_scope_excludes_dotenv() -> None:
    row = next(r for r in LEDGER if r.id == "fs.read_scope")
    if row.status != "known":
        pytest.skip("read scope not claimed as known")
    assert Path("app").is_dir()
    assert Path("docs").is_dir()
    dotenv = Path(".env")
    if dotenv.exists():
        assert not os.access(dotenv, os.R_OK), ".env must not be readable during pairing"


def test_model_calls_remain_disabled_until_explicitly_armed() -> None:
    unknown = [row.id for row in LEDGER if row.status == "unknown"]
    blocked_without_reason = [
        row.id for row in LEDGER if row.status == "blocked" and not row.proof.strip()
    ]
    if unknown or blocked_without_reason:
        assert ALLOW_MODEL_CALL is False
    if ALLOW_MODEL_CALL:
        assert not unknown
        assert not blocked_without_reason
Enter fullscreen mode Exit fullscreen mode

The runtime.python row is the quiet lie that coding agents invent when a pin file is missing or stale. The net.allowlist row is a deliberate block, which is healthier than a guessed CIDR copied from a tutorial. Leaving ALLOW_MODEL_CALL false while any row is red is the entire protocol; the rest of the file exists to make that flag expensive to flip casually.

Numbered workflow the pair actually followed

  1. Add test_pairing_assumptions.py before opening a completion panel, browser tab, or remote notebook.
  2. Put a human owner on every Assumption record and reject team, agent, and blank strings.
  3. Run the tests from the repository root and keep the entire session local on the first failure.
  4. Change each unknown row to known with a proof, or to blocked with a written reason.
  5. Leave ALLOW_MODEL_CALL false until the file is green, including identity and path checks.
  6. Send only a bounded prompt that cites assumption ids, never a raw dump of environment variables.
  7. Treat the model output as a candidate patch, then re-run the same tests before any merge.

Commands used in the reconstructed session are ordinary workstation commands, not a hidden product feature. The pair ran them in the repository root so the path assertions matched the files they could actually see.

python3 -m pip install --user pytest
python3 -m pytest -q test_pairing_assumptions.py
# expected while runtime.python is unknown:
# test_unknown_rows_fail_closed fails and the session stays local

printf '3.12.6\n' > .python-version
# then edit the runtime.python row to status="known"
# with proof="cat .python-version"
python3 -m pytest -q test_pairing_assumptions.py
Enter fullscreen mode Exit fullscreen mode

A later proof for the identity row was collected without sending process data to a model. The junior ran id -un and id -u, pasted the outputs into the pairing notes, and only then marked the row known. That order matters: evidence first, status change second, remote completion last.

Bounded prompt the senior allowed after green tests

The senior permitted a completion only after the suite passed and ALLOW_MODEL_CALL was flipped during review, not during the first failing run. The prompt was a document with citations, not a fishing expedition across the home directory. The pair labeled the following JSON as an unexecuted request shape for later copy into whatever client they chose.

{
  "ledger_ids_cited": ["exec.identity", "fs.read_scope"],
  "blocked_ids_must_not_bypass": ["net.allowlist"],
  "task": "Draft a deploy checklist for ./app using only cited known facts.",
  "forbidden": ["read .env", "propose curl to unknown hosts", "mark unknown as known"]
}
Enter fullscreen mode Exit fullscreen mode

That shape is the opposite of an agent that assumes missing architecture and keeps talking. If a fact is absent from ledger_ids_cited, the model is instructed to stop rather than invent a typical value from some other stack. The junior wanted a longer prompt with repository highlights and recent commit subjects included for flavor. The senior refused, because highlights become a second, untested ledger that bypasses pytest.

Decision table the pair taped next to the terminal

Signal in the session Treat as Next action the pair takes
unknown row Defect Stay local; do not send a completion
blocked row with proof Explicit no Do not generate a clever bypass
known row with proof Evidence Cite the id; do not re-ask the model
owner is agent or team Defect Reassign to a human and rerun pytest
ALLOW_MODEL_CALL true while red Defect Revert the flag; the suite must refuse
Model output contradicts a proof Defect Keep the proof; discard the output

The table is deliberately boring. Pairing sessions fail when the exciting path is calling a model, and the dull path is admitting that a network policy does not exist yet. The senior kept the dull path because blocked is a decision, while unknown-plus-completion is a story the team will not be able to replay.

Limitations and who should not use this brake

This suite does not replace IAM, secret scanning, or a real egress policy on the workstation or on a shared runner. It will not stop a determined paste of secrets into a chat box, and it does not score model quality, latency, or cost. Teams that already run reviewed agents in locked CI with allowlisted tools may find the extra pytest file redundant. People pairing on throwaway katas with no secrets and no network should not adopt the ceremony merely to look rigorous in a writeup.

The tests also trust the humans who type owners and proofs, which is a meaningful limitation rather than a footnote. A false known row remains a lie with better formatting, and a skipped identity test on a container that always runs as root will green-wash the ledger. The protocol is a pairing brake rather than an agent framework, and it should be skipped when the work is already inside a stronger control plane.

The kept decision is still the useful part after the session ends. Name an owner, write a proof or a block, and keep the model quiet until the local suite agrees. Fluency is not evidence, and an agent that assumes the architecture is not pairing so much as guessing in the same room.

Top comments (0)