Three-call public surface — init() → session() → wrap_tools()/@trace — with
the explicit API unchanged underneath. Explicit ctx=/client= always win; no
existing call site breaks.
- `rootsign/sdk/facade.py`: `init()` is sync and does no I/O (safe at module
scope and inside a running loop), validates args, stores a frozen
`_InitConfig` singleton; identical re-init is a no-op, conflicting re-init
raises. Zero-arg init resolves script name / development / medium / jsonl.
- Ambient `(ctx, client)` in a `ContextVar` set on `session()` entry and reset
in its finally. `_resolve_ctx_client()` (explicit → ContextVar →
`RootSignNotInitializedError`, new in errors.py with the two-line fix) is now
used by wrap_tools, @trace, both framework tracers, `MCPProxyTracer`, and
decision capture. Resolution is deferred to *invocation* — decoration and
tool wrapping usually happen at import time, before any session exists.
- `rootsign.session()`: `agent_id`/`client` optional. First entry does the
lazy agent get-or-create and builds the backend's client (closing it on exit
only when it built it).
- `get_or_register_agent` in registration.py, backend-dispatched: jsonl →
`jsonl_registry`; postgres → `INSERT ... ON CONFLICT (name, environment) DO
NOTHING` + `SELECT`. Attribute drift logs WARNING and keeps stored values —
mutation is an admin op, not an init side effect.
- Migration 0005 (the sprint's only schema change): drop `uq_agents_name`, add
`uq_agents_name_environment`. No backfill needed — `name` is globally unique
today, so every row already satisfies the composite key. Revision id is
abbreviated because `alembic_version.version_num` is varchar(32).
- `ManagedLocalIngestClient` (client.py): the postgres facade path has no
caller-supplied AsyncSession, and `LocalIngestClient` never commits, so this
opens a short-lived `AsyncSessionLocal` per handle() and commits — same
per-call-session pattern as the HiTL poll loop. Every record is durable on
return, which is what keeps `rootsign approve` / cross-process HiTL working
under the facade.
Tests: 27 new (facade unit incl. concurrency isolation + monotonic parallel
sequences, postgres get-or-register + constraint assertion, all three tracers
driven with implicit context, HiTL-on-jsonl raising at first invocation).
Suite 479 green; coverage 91%. Two existing tests updated for the intended
contract changes: agent uniqueness is now per (name, environment), and
CrewAITracer no longer rejects a missing ctx at wrap time.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Workstream 3 of Pre-Phase-2 Sprint A. Implements ADR-012: a three-call public
surface (
init()→session()→wrap_tools()/@trace) with the explicitAPI unchanged underneath. Explicit
ctx=/client=always win; no existingcall site breaks.
Stacked after #20 (packaging split) and #22 (JSONL backend).
What's in it
rootsign/sdk/facade.py—init()is synchronous and does no I/O, soit's safe at module scope and inside a running loop (notebooks, FastAPI
startup). It validates args and stores a frozen
_InitConfigsingleton;identical re-init is a no-op, conflicting re-init raises. Zero-arg
rootsign.init()resolves to script name /development/medium/ jsonl.ContextVar, set onsession()entry and reset inits
finally._resolve_ctx_client()(explicit → ContextVar →RootSignNotInitializedError, new inerrors.pycarrying the two-line fix)is used by
wrap_tools,@trace, both framework tracers,MCPProxyTracer,and decision capture. Resolution is deferred to invocation — decoration
and tool wrapping normally happen at import time, before any session exists.
rootsign.session()—agent_id/clientare now optional. First entryperforms the lazy agent get-or-create and builds the backend's client,
closing it on exit only when it built it.
get_or_register_agent(registration.py), backend-dispatched: jsonl →jsonl_registry; postgres →INSERT ... ON CONFLICT (name, environment) DO NOTHING+SELECT. Attribute drift logs WARNING and keeps the storedvalues — mutating a registered agent is an admin operation, not a side
effect of
init().0005_agents_name_env_unique(the sprint's only schemachange): drop
uq_agents_name, adduq_agents_name_environment. Safe withno backfill —
nameis globally unique today, so every existing row alreadysatisfies the strictly weaker composite key. The revision id is abbreviated
because
alembic_version.version_numisvarchar(32).ManagedLocalIngestClient(client.py) — the postgres facade path hasno caller-supplied
AsyncSession, andLocalIngestClientnever commits(its callers do). This opens a short-lived
AsyncSessionLocalperhandle()and commits, the same per-call-session pattern as the HiTL pollloop. Every record is durable on return, which is what keeps
rootsign approveand cross-process HiTL working under the facade.Testing
479 passed, coverage 91% (
facade.py96%). New tests:tests/unit/test_facade.py— init validation/idempotency/zero-arg defaults,no-I/O assertion, loop-safety, resolution order, ContextVar reset (including
on exception), quickstart→
VALID, get-or-create across sessions, driftwarning, HiTL-on-jsonl raising at first invocation, two concurrent
sessions isolated, 8 parallel calls keeping monotonic sequences.
tests/integration/test_facade_postgres.py— get-or-register idempotency,same name across environments, drift, the constraint swap asserted against
pg_constraint, per-record commit visible from another connection, andinit→session→trace→
verify_sessionend-to-end on Postgres.tests/integration/test_facade_implicit_tracers.py— LangGraph, CrewAI, andMCPProxyTracereach driven with noctx=/client=(ADR-012 exitcriterion 6).
Two existing tests changed for the intended contract change: agent uniqueness
is now per
(name, environment)(test_relationships.py, split intosame-env-rejected + cross-env-allowed), and
CrewAITracerno longer rejects amissing
ctxat wrap time (test_crewai_tracer.py).Note for reviewers
Existing deployments need
rootsign-admin initto pick up migration 0005.W4 (README quickstart,
examples/quickstart-jsonl, demo.gif, thedocs/framework-support.mdfrozen-surface note, version bump to 0.2.0) isdeliberately not in this PR.
🤖 Generated with Claude Code