v2's recall gains its second channel. infra/embed.py is an OpenAI-compatible /embeddings client that never raises and caches a query's vector in-process by model and folded text. application/ find_index.py builds one card matrix per catalog in the background on the first v2 find: vectors are read from the archive's object store under find-vectors/<model>/<card sha256>, only missing cards are embedded, and those are written back. Without a store they live in the process; without the API the channel stays off and the build is retried later. The judged event and the searchlog row record the query's embedding time and error. The object store gains named objects for these vectors only, each body carrying its own card hash and size. numpy joins the server extra for the matrix product. Settings find_embed_api_key (falls back to treg's OpenRouter key on the OpenRouter URL), find_embed_model, find_embed_url, find_embed_timeout_s. The bench builds the vectors before scoring when a key is set, and its judge cache key now includes the job question's wording.
26 KiB
title, status, sources, related
| title | status | sources | related | |||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Running & deploying the server | shipped |
|
|
Running & deploying
This fragment documents behavior every operator needs to run or self-host a registry. It does not describe the private topology, live settings, account funding, incidents or rollout state of the hosted treg.to service. Superdesign operators use the private treg.to deployment runbook.
Fish Audio shared-key capacity monitoring requires both TREG_PLATFORM_KEY_FISHAUDIO and
TREG_PLATFORM_FISHAUDIO_WORKSPACE_ID. The latter is the Fish workspace selector used only by the
free API-credit probe; without it, capacity remains unknown rather than reading the unrelated
personal wallet.
Entry point (__main__.py)
python -m treg upgrade runs the explicit release phase. maintenance._upgrade_schema() runs
alembic upgrade head for an empty or stamped database. A non-empty unstamped database is refused
without writes: adoption ended with 0.14.x, so the operator must install
tools-registry[server]==0.14.*, run python -m treg upgrade there once, then continue onward. A
database stamped at a revision this build does not know is also refused because explicit upgrade may
not cross the rollback floor. The ordered, idempotent release-task registry runs only after schema
success and never provisions a local user.
The default python -m treg serve path runs the same upgrade phase, calls
api._bootstrap_single_user(), disposes the async engine inside the pre-serve event loop, and then
calls uvicorn.run("treg.api:app", host="0.0.0.0", port=int($PORT or 18790)). --reload is optional.
It honors $PORT for platforms that route and health-check an injected port. python -m treg keygen
prints a Fernet key for TREG_SECRET_KEY without importing the server maintenance stack.
treg.api:app is bootstrap.create_app(role="all").
The FastAPI lifespan calls read-only verify_db(). It refuses an unstamped database or a known
revision behind head and directs raw-ASGI operators to python -m treg upgrade. An unknown newer
revision means the code is older than the schema: startup warns and serves because additive revisions
tolerate rollback. A revision marked contract = True is the documented hard rollback floor. Every
role startup manifest is schema-write-free.
Both pre-Uvicorn entry paths dispose the engine before their asyncio.run() loop closes. The next
event loop therefore creates fresh pooled connections instead of receiving connections bound to a
closed maintenance loop. Calling maintenance.upgrade() directly does not dispose the engine.
Schema upgrade safety
- Managed-key rollback floor: revision
0034adds key controls, audit rows, Activity snapshots, and a hash-only backfill for existing membership credentials. It is markedcontract = Truebecause old code cannot enforce newly stored disable or revoke state. The migration is additive and uses SQL that works on SQLite and Postgres. - Alembic is authoritative: migration scripts ship inside
src/treg/alembic/in the wheel.maintenance._alembic_config()resolves that installed package resource, supplies the escaped configured URL, and runs Alembic in a worker thread. - The adoption floor is final. An unstamped existing database must pass through release 0.14.x. Current code does not inspect, repair or stamp it.
- Startup is read-only.
verify_db()checks the stamped revision through packaged Alembic metadata. It never creates tables, stamps versions or runs release tasks. Worker commands use the same check. - Schema changes are revision-only. An autogenerate drift guard requires Alembic head and
SQLModel.metadatato match exactly.reset_db()usescreate_allonly to build a missing test schema and stamps it directly at head. - A missing encryption key fails loudly on a real database. If
TREG_SECRET_KEYis empty anddatabase_urlis not SQLite,verify_db()raises. On SQLite development it logs a warning.
For PostgreSQL, migrations set bounded lock and statement timeouts. A contended ordinary DDL
migration must fail cleanly rather than queue production traffic behind an exclusive lock: while an
ALTER TABLE waits for its ACCESS EXCLUSIVE lock, every later query on that table queues behind
it, so the 5 s lock_timeout is the longest stall a deploy may inflict and no revision may raise it
for an ALTER. A deploy waits longer by retrying instead: maintenance re-runs alembic upgrade head up to LOCK_RETRY_ATTEMPTS times after a lock timeout, pausing a jittered few seconds between
attempts so the table's short transactions can drain, and env.py commits each revision on its own
(transaction_per_migration) so a retry resumes at the revision that timed out. Any other error
fails the deploy at once. The one sanctioned exception to the 5 s cap is CREATE INDEX CONCURRENTLY
in its own revision: its lock blocks nobody while it waits, so such a revision owns its longer
timeouts and must detect and rebuild an invalid index left by interruption.
Hot-table ALTERs stay cheap to retry when they sit in their own revision, add only nullable columns
without defaults (metadata-only in PostgreSQL) and leave backfills and NOT NULL to later steps.
Deploy schema changes before starting application or worker code that expects the new revision. A platform whose scheduled workers update independently must sequence them accordingly.
Database pools
POOL_SPECS in infra/db.py defines three PostgreSQL pools:
| pool | maker | serves |
|---|---|---|
api |
session_maker |
request handlers |
admin |
admin_session_maker |
/admin/* handlers |
background |
background_session_maker |
audit, archive and other background work |
Each class can exhaust only its own slots. Minor pools deliberately have no overflow because an
unbounded escape hatch defeats the bulkhead. A new class of work gets an appropriate pool, not only
a module-local semaphore. tests/test_db_pool_isolation.py pins the routing.
Every process opens its own pools and runs its own in-process background tasks. Calculate the full connection peak from pool capacities, process count and the maximum number of overlapping instances during a rolling deployment. Keep that peak below the database connection ceiling with headroom.
TREG_DB_POOL_OVERRIDES accepts entries such as
"admin.pool_size=4,background.pool_size=12" and patches POOL_SPECS at startup. Unknown pools,
unknown field names, non-integers and out-of-range values are logged and skipped. In particular,
SQLAlchemy treats some zero and negative values as unlimited, so the parser must not pass them
through. Live values and production sizing belong in the private operator runbook.
bootstrap.pool_gauge samples infra/db.pool_snapshot() and emits per-pool peak, capacity and
headroom. Resize from measurements, one pool at a time. A larger pool does not repair a slow scan,
lock queue, disk-bound database or missing index.
All pools use pool_pre_ping=True, pool_recycle=300 and pool_timeout=5. A request that gets no
slot in time receives 503 {"treg_saturated": true} with Retry-After: 2 through
bootstrap_handlers._pool_saturated. A /call/ holds no connection during the upstream round trip.
SQLite aliases all three makers to one engine. It has no pool to protect and file-level write locks it cannot share, so separate engines would only manufacture lock failures. Tests pin routing rather than SQLite isolation.
Optional read replica
TREG_READ_DATABASE_URL adds a datasource exposed as read_session_maker. Its supported drivers
match the project's SQLite / PostgreSQL support: sqlite+aiosqlite and postgresql+asyncpg.
Bare postgres:// and postgresql:// URLs normalize to postgresql+asyncpg, as for the primary;
other read URL drivers fail settings validation. This does not add support for MySQL or other
SQLAlchemy dialects. When empty, the read maker aliases session_maker, with the primary's existing
transaction behavior (including writes) and no additional pool. Connections are opened only when used.
A configured URL always gets an independent engine, even when it names the primary database:
- PostgreSQL uses a
readpool defaulting to two connections with no overflow;TREG_DB_POOL_OVERRIDESacceptsread.pool_sizeandread.max_overflow. Each connection setsdefault_transaction_read_only=onthrough asyncpg'sserver_settings. - SQLite retains the driver's default pool behavior and sets
PRAGMA query_only=ONon every new read connection, including after reconnection. The primary's connections remain writable. For an existing file,sqlite+aiosqlite:///file:replica.db?mode=ro&uri=trueadditionally opens the file read-only and fails if it is missing. A plain SQLite URL retains SQLite's usual file-opening behavior, including creating a missing file;query_onlyguards SQL changes, not file creation. A separate in-memory SQLite URL starts empty and cannot serve as a copy of the primary.
These connection settings guard accidental writes; they are not an authorization boundary and can be disabled by deliberate SQL. Use a physical replica/read-only database role or filesystem access controls as appropriate. Connection/query failures propagate without primary fallback.
pool_snapshot() includes a separate read entry for a configured PostgreSQL datasource; SQLite
engines are omitted as for the primary. connection_budget() describes only the primary pools;
budget the read pool against its target database, including process count and deployment overlap.
If both URLs target the same server, add both budgets against that server's limit.
dispose_engine() also disposes the read engine. Schema upgrades, startup verification and test
schema resets continue to target the primary.
This datasource is opt-in infrastructure: no application query or worker currently uses it.
Adopting callers must tolerate replica lag and keep writes, cursor advancement and concurrency
control on the primary. Operators must provision and synchronize a compatible schema and data;
setting a URL does not establish replication, translate dialect-specific queries, or migrate any
Cron job's workload. Configure it in a private local .env or the hosting service's environment;
.env.example contains no secrets.
Configuration (config.py)
Settings uses the TREG_ prefix, reads .env, and is cached by get_settings().
database_urldefaults to local SQLite. A validator rewrites barepostgres://andpostgresql://URLs topostgresql+asyncpg://.secret_keyis the Fernet key. An empty value creates an ephemeral key suitable only for local development.public_urlbuilds OAuth callbacks and public links. Self-hosters must set it to their canonical external URL.admin_tokenis the cross-tenant super-admin bearer. Empty disables this environment path. Keep it long and secret.- OAuth provider clients use
<provider>_client_idand<provider>_client_secretsettings. Empty credentials make that provider unconfigured instead of failing halfway through consent. oauth_review_pendingis a comma-separated set of provider-registry review keys. Its hosted value is operational state and is maintained privately.promo_grant_microcontrols the once-per-verified-user signup grant. Zero pauses new automatic grants without changing existing balances.blocked_email_domainsis the complete comma-separated blocklist. Empty blocks no domains. It is a sign-in and team-creation speed bump, not a substitute for suspending an existing abusive user.run_proofgates release of a shared key to the root-installed local runner. Empty refuses shared local runs while leaving owner-key runs available.run_allowed_binsis the command allowlist for server-side CLI execution.run_rlimits,run_cpu_secondsandrun_fsize_mbbound server-run resource use where POSIX resource limits are available.proxy_ssrf_checkenables the call-time SSRF guard and is on by default.claude_connector_enabledgates the catalog-only connector at/mcp/v2/.connect_demo_enabledgates the developer OAuth test page. Leave it off on public deployments.intercom_app_idandintercom_secretenable optional support chat. Empty disables it.resend_api_keyandemail_fromenable transactional email. The sender must use a domain verified with the operator's mail provider.
Registry-owned OAuth applications, advertising conversion credentials and platform provider keys are
optional deployment capabilities. Their names and binding behavior are documented with their owning
subsystems. Credentials must stay in the deployment secret store. TREG_PLATFORM_PROVIDERS is the
shared-serving allow-list. Most providers also require a configured platform key. A live-verified
free endpoint declared platform_auth: anonymous needs only the allow-list because treg injects no
provider credential.
Safe local mode
single_user and single_user_token_file support curl {BASE}/selfhost.sh | sh. The default serve
pre-phase idempotently creates the local identity and personal team and writes a stable token with
mode 0600. The token is re-minted only if its file is deleted.
This behavior is gated by single_user_ok, which requires both a local SQLite database and a
loopback public_url. A stray TREG_SINGLE_USER=true on a public PostgreSQL deployment therefore
does nothing.
email_dev_mode is similarly guarded. A login code is exposed only when the flag is true and the
database is local SQLite. Hosted deployments must still leave it false.
Web service and generic Render example
The redesigned homepage ships to all homepage visitors; its rollback, like the Dashboard's, is a code revert or a deploy of the previous build.
GET /app, the catalog pages and shared links all serve the Vite-built Vue application. There is
no frontend switch to configure; a Dashboard rollback is a deploy of the previous build. See
frontend/README.md.
The frontend is authored in frontend/ within the same repository. GET / retains the existing
landing behavior. Dashboard assets, tutorials, agent files and installer assets ship with the wheel.
Hosted-page MP4 demos remain in Git checkout deployments but are excluded from published wheels and
source archives; a server installed from PyPI serves the product surfaces without those optional
marketing videos.
Run bash scripts/build-dashboard.sh before building a distributable Python package. Hatch's
build hook rejects a wheel or sdist without the dashboard entry and includes the generated assets;
editable installs remain Python-only. Node and npm are build tools, not runtime services.
TREG_FRONTEND_DEV=true serves the authored entry with Vite scripts on local port 5173 and is
accepted only with SQLite and a loopback public URL. scripts/dev-local.sh up manages both processes.
For browser previews from another device, build the dashboard and start or restart the local stack
with TREG_FRONTEND_DEV=false; compiled assets then use the same origin on port 18790.
The browser tests' server (scripts/frontend-e2e-server.sh) runs on TREG_E2E_PORT, which
frontend/playwright.config.ts sets to a port the OS reports free at the start of each run, so runs
in parallel worktrees or beside a treg serve proxy (default 18791) never share a port.
A feature flag set in the calling shell (TREG_HUB_ENABLED=1 scripts/dev-local.sh up) does not
reach the server on its own: the script starts the server inside a tmux session, and a tmux session
inherits the tmux server's environment, not the calling client's — the server would start and
answer every hub route 404 with no error. dev-local.sh expands a fixed passthrough list
(currently TREG_HUB_ENABLED) in its own process and bakes the value into the command string tmux
runs; add a flag to that list to pass another one through. The script's own three env vars
(TREG_EMAIL_DEV_MODE, TREG_CONNECT_DEMO_ENABLED, TREG_DATABASE_URL) are appended last so they
always win over anything passed through.
deploy/render.example.yaml is a generic self-hosting example.
It creates one web service and one PostgreSQL database, builds with
bash scripts/build-web.sh (frontend build followed by the locked Python install), runs
python -m treg upgrade before serving,
starts python -m treg, and checks /meta. Copy it into the operator's own deployment repository and
change resource names, region, plans, public URL and integrations.
Build from the lock file
A deployment built from a checkout must install the dependency set uv.lock records, which is the
set CI tests (uv sync --locked in .github/workflows/ci.yml). pip install ".[server]" does not
read the lock: it resolves every open range in pyproject.toml afresh, so a build made after a
dependency publishes a breaking release ships that release with no commit in this repository, and
the suite that passed on the lock proves nothing about it. --locked refuses a stale lock instead
of resolving, which makes a forgotten uv lock a failed build rather than a silent drift; --no-dev
leaves the test tooling out; --active installs into the virtual environment the platform already
activated for the start command. Every process that shares a database (web service and scheduled
workers) must build the same way, or they run different dependency sets against one schema.
Render adds uv to its Python runtime when uv.lock is present at the service root, but at an
older version than [tool.uv] required-version accepts; set UV_VERSION on the service to a
version that satisfies it. Upgrading a dependency is a uv lock --upgrade-package <name> (or
uv lock --upgrade) commit that CI tests before it can reach a build.
Installing the published wheel (pip install "tools-registry[server]") is a different path: a wheel
carries no lock, so that operator pins versions in their own requirements file.
The published source archive is built by Hatchling from the checkout. The Hatch target exclusions in
pyproject.toml keep local linked worktrees, root-level working plans and previews, evidence,
databases, environment files and hosted-page MP4 demos out of public artifacts. Release validation
inspects both archive contents; a clean Git diff alone is not sufficient.
The example is deliberately not the treg.to production Blueprint. The hosted topology and settings are private operational state.
Audit and archive back-pressure
audit.record_call enqueues rows off the request path. One batching writer per process drains them
through the background pool. _MAX_PENDING bounds queued rows; overload sheds audit work instead of
growing memory without limit. A rejected batch retries row by row so one malformed row costs only
that row. Unknown model fields are dropped with an error log rather than silently discarding the
whole record.
Archive recording has independent task-count and byte limits. _MAX_PENDING_BYTES bounds bodies
retained for database writes, and archive_r2_max_pending_bytes separately bounds object-store work.
These are admission budgets, not an RSS ceiling: SDK buffers, compression and mandatory terminal
evidence require additional memory headroom.
Spooled settlement evidence (spooled_response, proxy-model.md) writes large metered answers to
the process temp directory, not RAM. TREG_SPOOL_BUDGET_BYTES (default 512 MiB per process) bounds
that disk use, TREG_SPOOL_MAX_BYTES (64 MiB) one answer, TREG_SPOOL_PARSE_CONCURRENCY (2) the
transient parse memory (up to about three times the body each), and TREG_SPOOL_DIR overrides the directory.
Size the budget against the host's ephemeral disk: a platform that evicts an instance over its
local-storage allowance turns an oversized budget into a restart. Production observations and incident history live in
the private database-capacity runbook.
Hosted feature switches
Several settings alter optional shared-service behavior without changing the underlying public contract:
TREG_PLATFORM_DAILY_CAP_USDis the default per-UTC-day shared-provider spend limit for a team that has not set its own; 0 (the default) is no limit. A team's own setting wins in either direction.TREG_OAUTH_BILLED_PROVIDERSnames OAuth providers whose upstream bill lands on the registry operator. Own-app connections are never metered by this switch.TREG_ROUTED_DISCOVERYcontrols whether discovery leads callers to routed capability tools. It does not delete or make existing endpoint IDs uncallable.TREG_PLATFORM_PROVIDERSenables use of configured platform credentials.TREG_OVERFLOW_MODEandTREG_OVERFLOW_DAILY_BUDGET_USDcontrol same-vendor overflow. See capacity.TREG_SEARCH_EXPERIMENT(off|shadow|interleave) runs the discovery experiment on the MCP search tools; it is also its kill switch. NeedsTREG_TYPESAFE_API_KEY;TREG_TYPESAFE_TIMEOUT_Sbounds what the judge may add to a search. Off by default, and off whenever the key is empty.TREG_FIND_ENGINE(v1|v2|shadow) picks what/catalog/findanswers with (see find);shadowserves v1 and logs v2 beside it, at a second judge request per find. Rolling back is setting it tov1.TREG_FIND_EMBED_API_KEYturns on v2's semantic channel; with the default OpenRouterTREG_FIND_EMBED_URLan empty value usesTREG_PLATFORM_KEY_OPENROUTER. Card vectors are cached in the archive's object store when archive R2 is on, else rebuilt by each process.
Exact treg.to values, funded accounts and rollout instructions live in the private provider-capacity runbook.
Worker commands
treg-worker is a console script in the [server] extra. It hosts scheduled maintainer commands
without importing the heavy database stack into the light treg CLI.
treg-worker capacity sweepcollects provider capacity and writes snapshots.treg-worker overflow verifyobtains route evidence and spends real money when configured.treg-worker overflow syncderives enabled overflow routes from current evidence.treg-worker asynctasks settlecompletes durable holds for asynchronous upstream operations.treg-worker arena insightsfolds new audit rows into the rolling Arena aggregate (--max-seconds, default 110, bounds one pass; schedule it every two minutes). It requires the archive object-store settings when R2 reads are enabled, opens the same client lifecycle as the web service, and flushes read analytics before exiting.treg-worker catalog statsfolds new audit rows into per-endpoint, per-day reliability buckets (--max-rows, default 500,000, bounds one pass; schedule it every few minutes). The catalog keeps computing observations live until this command has caught up once, so it can be scheduled after the application deploys, and a self-hosted registry that never schedules it loses nothing.treg-worker jev xboostruns the/jevlaunch-radar demo once a day: it calls treg's own/call/API withTREG_JEV_TREG_TOKEN(a member token of the demo team, so the spend is an ordinary bill) and jev through the Vercel AI Gateway (TREG_AI_GATEWAY_API_KEY), and stores the run under Ephemeral for the page. Both variables also belong on the web service, which needs them for the visitor judge endpoint.treg-worker admin purge-evidenceblanks failed-call evidence past the 14-day retention window (--batch-size, default 5000, rows per transaction; schedule it daily).GET /admin/errorsis read-only and already withholds evidence past the window, so an unscheduled purge keeps the old bytes in the database but never shows them.treg-worker hub check [--only <tool_id>] [--json]runs every live hub tool'scheck.jsononce, as its maker (spends the maker's balance at step prices); a no-op with exit 0 whenhub_enabledis off. Schedule it every 6 hours; health is derived from the last three runs.
The two analytics commands exist so that no web process aggregates the audit table beside the
money path; callrecord is read only through the persisted cursors they own. Workers call
read-only verify_db() before work and must run against a compatible schema. They need only the
credentials and configuration required by their job (the two analytics commands need only the
database URL). Hosting schedules, service wiring and manual production procedures belong in the
private deployment runbook.
Managed-key rollout uses revisions 0034 through 0036. Apply the key controls and generation
before the separate concurrent Activity index build. The index migration allows 180 seconds for
lock waits and 600 seconds per statement, then restores 5/120 seconds. A failed concurrent build
can leave an invalid index; retrying 0036 removes and rebuilds only that invalid index.
Do not deploy server code older than 0034 after key disable or revoke state has been recorded.
The package version follows current main; this branch does not publish a release.
For hosted rollout, release and verify the compatible CLI before deploying the managed-key server.
The served installer installs from PyPI, so changing the server alone does not make treg update
install the new client. Old browser login and saved-token calls remain usable; affected email and
team-change requests receive an update instruction before their local state can be replaced.