treg is the tool catalog for an agent: one base URL, one token, and the agent can call a curated catalog of external endpoints plus its own team's tools without ever holding an API key. The load-bearing mechanic is a proxy that makes the caller's real upstream request, injects the credential server-side and relays the answer verbatim. We never model an upstream API.
Everything else in this file is guidance; these are the contract, and they win over any other passage.
- A team's own key always wins over treg's, is never metered, and is never routed or overflowed.
- A hold (the balance
reservesets aside for one call) is settled or released exactly once, on every path: timeout, cancellation and exceptions included. - Zero database connections are held while an upstream request is in flight. This is why
reserveandsettleare two transactions; never merge them. - Plain
/call/is a faithful relay: the injected credential and the transport headers listed insrc/treg/infra/upstream/relay.pyare the only rewrites. Never add upstream-specific modeling or body buffering. Routed endpoints and overflow wrap the child's answer and say so; they never alter it. - Balances change only through money's five entries: grant, topup, reserve, settle, release. There is deliberately no refund or adjustment entry; an ops correction is a grant.
Changing any invariant in this file means editing this file in the same PR. Routed endpoints and overflow once shipped with every other doc updated while this file still said "no router"; agents then built against a constitution that was wrong.
- Design docs are fragments.
docs/context/holds one per subsystem, each naming itssrc/treg/*sources in frontmatter;docs/context/README.mdis the generated index anddocs/context/foundation/charter.mdthe start. Read the fragment before changing an area (thetools-registry-contextskill in.agents/skills/loads it). - Before pushing:
bash .agents/skills/tools-registry-context/scripts/drift.shmaps changed sources to fragments. Update them and commit the docs in the same commit as the code. - Agent-facing files are the product's front door, not documentation:
src/treg/web/llms.txtandsrc/treg/web/skill.md(installed into every agent byinstall.sh). They,README.mdand this file must agree on how treg works; a behavior change asks whether all four move. - Three files move together or they drift:
src/treg/web/tutorial.js(the only interactive source) and its hand-kept prose mirrorssrc/treg/web/tutorial.mdanddocs/TUTORIAL.md. README.mdis the overview and quickstart,USAGE.mdthe CLI reference,CONTRIBUTING.mdthe dev setup,SECURITY.mdrequired reading before touching the proxy, runners, auth or secrets.
routers/ -> application/ -> domain/ -> infra/. Imports point inward only.
| Layer | Owns | Never |
|---|---|---|
routers/ |
HTTP and MCP translation in, response shape out | business rules, query orchestration, money |
application/ |
use-case sequencing, transaction boundaries, compensation, cross-domain composition | empty wrappers around one-domain CRUD |
domain/ |
rules explainable and testable alone: identity, governance, connections, tools, catalog, capacity, money |
routers, application, concrete SDKs |
infra/ |
DB engine and sessions, crypto, upstream relay and SSRF, ratestore, email, Stripe | decisions |
- Domains do not import each other, with three sanctioned edges:
governance -> identity,tools -> connections,capacity -> catalog(read-only).identityandmoneyare leaves. import-linter enforces the layering ([tool.importlinter]inpyproject.toml, run by CI);docs/context/architecture/import-boundaries.mdexplains each contract. bootstrap.pyalone knows concrete implementations.api.pyis the legacyall-role entrypoint, not where logic goes.audit.pyis best-effort and drops rows under load, so nothing that must persist goes through it;analyticsis read-only.
- Session discipline. The application use case opens the session and is the only place that
commits; domain functions never commit or roll back. A commit mid-flow silently breaks
compensation, and no import rule can catch it. Money's public
reserve,settleandreleasecommit by design; a few other domain commits remain. Do not add another; move one out when you touch it. - Table ownership. One writer module per table; cross-domain reads are fine. Three recorded
exceptions: only money writes
org.balance_microand the auto-top-up fields; the call runtime may persist an OAuth token refresh intosecret; audit writescallrecord, domains only read it. - The call runtime is self-contained.
src/treg/application/call/depends on no management code (routes, login, OAuth consent, Stripe top-up), reads only membership, deny rules, credentials, catalog prices and balances, and writes only whattests/test_call_architecture.pyallowlists (the ledger entries, idempotency claims, OAuth refresh, audit and telemetry, first-call markers, tag budgets, capacity marks, overflow spend). Extend the test's allowlist in the same PR as any new write, and expect the reviewer to ask why. - Money. Everything is integer micro-USD - never floats, never cents. The Stripe SDK lives
only in
infra/stripe.py, orchestration inapplication/billing.py, andreconcile.pyis read-only. Seedocs/context/architecture/money.md.
expose_dev_code (dev OTP only on a local sqlite database, config.py), the call-time SSRF check
(infra/upstream/ssrf.py), the fail-loud missing-Fernet-key check in verify_db, and the
treg run allow-list and rlimits (runner.py). Read the fragment before touching any of them.
uv run --with pytest-xdist pytest -n auto -q # daily local default (same shape as CI)
uv run --frozen python -m pytest -q # serial: debugging one test, or order
uv run treg --help # the CLI from this checkout
uv run python -m treg # the server
uv run lint-imports # the import-linter contracts (CI runs this too)
scripts/dev-local.sh up # live dev stack on :18790 with its own sqlite DBxdist is pulled via --with, not the lockfile — same as CI. The Postgres CI job
(test-postgres) must stay serial: every worker would share one database while
reset_db() drops tables.
- Dependencies change through
uv addoruv lock, never by hand.pyproject.tomlpinsrequired-versionso an old uv refuses to run instead of rewritinguv.lock; CI uses--locked. - The package is split. The base install is the light CLI; the FastAPI/DB stack is the
[server]extra, the certificate authority is[proxy]. Never import a heavy dependency at the top of a CLI-path module; the "Lightweight CLI modules" import-linter contract lists them and fails the build. - The dashboard (
src/treg/web/index.html) is a single-file Vue app with no build step, so a broken view name fails silently. Verify in a browser. - Schema. Alembic owns it (
src/treg/alembic/versions/); every schema change is a revision. Startup only verifies the revision and refuses to boot when behind; migrations run only viapython -m treg upgrade.
- Keep the suite green; add tests for new behavior. Conventional Commits (
feat(scope): ...,fix: ...,docs: ...); one logical change per commit; the PR says what changed and why and names the fragments it updated. /mcp/and/mcp/v2/differ on purpose. A change to either or to shared MCP code is reviewed against both; do not unify them in passing.
One concept, one word. Settled deliberately - mixed vocabulary is how the old framing creeps back.
| Thing | Word |
|---|---|
| what an agent calls | a tool |
| the public half | the catalog |
| the team's half | your own tools (your keys and skills) |
| the server itself | registry, and only for that |
Do not call either half a vault, a marketplace, or the registry. Say what the agent can now do, not what we store. Never use a count of endpoints or providers in this file; the catalog changes weekly and every stale number is a lie.
Do not document what is not built. An agent that believes a feature exists fails in a way nobody can debug. Provider choice is the easiest thing to overstate: treg compares providers, and chooses only in the two disclosed cases of non-negotiable 4.