Blake — the Document Registrar agent (scaffold)

The W1 keystone of EPIC-1536 / EPIC-009. A new AgentCore unit, born in Atlas's canonical shape — structure only, an empty tool allowlist, zero business mutation.
Story-1537 · EPIC-1536 · branch issue-1537-scaffold-registrar · repo pramaan-agents

🛰️ Satellite. PRAMAAN is building a firm of AI agents. Atlas (agent #1) frames the matter; Blake is agent #2 — the Document Registrar who makes the Record usable after files arrive (clean names, descriptions, document types, tags, filing links). This Story does not make Blake think yet. It builds his body: a self-contained agents/registrar/ unit that mirrors agents/atlas/ file-for-file, imports and builds, passes the brain-purity gate, and reserves zero live tools. The build stories (broker, effect-ledger, trigger) fill it.

WHY a scaffold first. ADR-128 (the broker-mediated-mutation doctrine) just landed. Until Blake has a doctrine-correct shell — PUBLIC brain, no credentials, proposes-only — every downstream build story has nowhere to land. This Story is the keystone the rest of the epic stands on, so it must be shape-perfect even though it is behaviour-light.

What Blake is — and what makes him different from Atlas

✈️ Airplane. Atlas is read-only: he proposes a Matter Frame, counsel commits. Blake's whole job is to change the Record — and a brain must never hold database credentials. ADR-128 opens the third path:

# ADR-128 — the only way a PRAMAAN brain ever touches business truth
Blake brain  (AgentCore, network_mode = PUBLIC, no DB/VPC, no creds)
  → agent_event_stream.v1  (typed tool intent / structured registration output)
  → pramaan-functions broker  (already in-VPC; holds the creds + safety machinery)
        policy · firm/matter RLS · permission · idempotency · effect ledger · audit
  → documents.* / business.* / agents.* effect

So Blake's truth boundary is one line, encoded everywhere in the scaffold: proposes only, never writes canonical. It lives in registrar.yaml (truth_boundary.statement), as constants in agent.py (WRITES_CANONICAL = False, REQUIRES_BROKER_MEDIATION = True), and is stamped onto every proposal on the wire (runtime.py _wrap_proposal).

🔬 The unit — every file mirrors an Atlas counterpart

FileRole (WHY it exists)
main.py@app.entrypoint — fail-closed: missing input raises (no demo fallback). Registered in agentcore/agentcore.json.
runtime.pyAgentCore cloud shell + the Bug-1370 thread-isolated boundary watchdog; routes classify / govern; emits agent_event_stream.v1 only.
agent.pyThin orchestrator around two Pydantic AI agents; one-retry; the anti-hallucination completeness gate (_require_complete_all_clear).
models.pyHand-owned Pydantic (ADR-117): a FLAT generation DTO (RegistrarClassifyOutput) the model emits → assembled RegistrarTurn — DeepSeek never sees a nested schema (Bug-1357 class).
prompts/The soul, one module per capability (classify.py, govern.py) + _render.py (the §H quoted-evidence injection fence). Skeletons.
provider.pyOpenRouter-only model + ZDR (data_collection="deny") + the baggage egress stripper.
observability.pyLogfire LLM lane + the ADR-124 §D10 allowlist baggage processor + trace-correlated logs. firm_id never in baggage.
manual_span.pyThe closed agents.registrar.* manual-span registry (skeleton).
config.py · model.json · registrar.yamlBehaviour vs model split (REGISTRAR_* env wins). EMPTY tool allowlist. DeepSeek via OpenRouter, no Bedrock.
identity.pyPersona = Blake, role Document Registrar, avatar slot.
errors.py · events.py · secrets_bootstrap.pyTyped taxonomy · the v1 event builders · the ONE boto3 secretsmanager file (cold-start secret read).
evals/ · tests/pydantic-evals skeleton (6 seed cases, 3 deterministic gates, LLM judge) · 51 offline unit tests, no model calls.

Two capabilities (skeleton)

The one gate that needed a real change: brain-egress

WHY the gate had to learn a second agent. scripts/check_brain_egress.py hard-coded the baggage/tracer helper homes to atlas/observability.py and atlas/manual_span.py. Drop a new agent's observability.py beside it — which legitimately constructs a TracerProvider, writes allowlisted baggage, and uses AwsXRayIdGenerator — and the gate would have failed Blake for doing exactly what Atlas is allowed to do.

The fix recognises the helper home per agent, by basename, so every future agent inherits the carve-out with no edit, and the allowlist-literal invariant (ALLOWED_BAGGAGE_KEYS == {run_id, form_id}) is now globbed across all <agent>/observability.py — Blake's baggage posture is validated, not just Atlas's.

# before: atlas-only, hard-coded
BAGGAGE_HELPER_PATHS = frozenset({"atlas/observability.py"})
# after: per-agent, by basename — registrar + every future agent inherit it
BAGGAGE_HELPER_BASENAME = "observability.py"
for observability_file in sorted(root.glob("*/observability.py")):
    ...assert ALLOWED_BAGGAGE_KEYS is exactly {run_id, form_id}, firm_id rejected

Verification (run on this branch)

GateResult
python scripts/check_brain_egress.pyclean ADR-110 §6 / ADR-121 D5 / ADR-124 §D10
ruff check agents/ tests/ scripts/all checks passed
cd agents/registrar && pytest tests51 passed offline, no model calls
Regression — agents/atlas + repo governance204 + 69 passed (egress refactor safe)

What this Story deliberately does NOT do

One-sentence recall: Story-1537 gives Blake a doctrine-correct body — Atlas's shape, an empty allowlist, proposes-only — so the EPIC-1536 build stories have a keystone to stand on.