feat(api): AIN-141 strip retired AAMC models from prod catalog (DB migration)#20
Conversation
…gration)
Forward-fix migration: DELETE 3 retired AAMC v1.0 slugs from models table.
Per Charter v3 §Don't: do not propagate Llama-3.3-70B or DeepSeek R1.
Retired slugs targeted:
- deepseek-r1-novita (from 20260516_0008 t9_catalog INSERT)
- meta-llama/Llama-3.3-70B-Instruct-Turbo (pre-T9 grandfathered)
- llama-3-3-70b-instruct-turbo-together (T9-canonical form)
Rowcount logged per memory feedback_migration_rowcount_assertion; asserted
non-negative (not ==N) because CI bootstrap state varies vs prod.
Bug fix vs initial draft: initial used bind.exec_driver_sql with %s
psycopg2-style placeholder, which failed on prod (asyncpg driver) with
'syntax error at or near %'. This version uses SQLAlchemy text() with
named :slug binds — works across asyncpg + psycopg2 + sqlite.
Downgrade is intentional no-op (retired rows absent by design).
Closes AIN-141 DB-side once founder runs:
doppler run --project ainfera-os --config prd -- alembic upgrade head
Co-Authored-By: Claude <noreply@anthropic.com>
…bility The committed migration in e8ab736 (this branch's previous commit) used bind.exec_driver_sql with psycopg2 %s placeholder. Prod runs asyncpg which uses $1 style placeholders and rejected the migration with syntax error at or near '%' when founder ran alembic upgrade head. Replace with SQLAlchemy text() + named binds (:slug), which dispatches correctly across asyncpg, psycopg2, and sqlite test backends. State after the failed run: - 20260517_0009 + 20260517_0010 successfully applied (committed earlier but not previously deployed to prod) - 20260518_0011 rolled back per transactional DDL - alembic_version should be at 20260517_0010 Founder retries with: doppler run --project ainfera-os --config prd -- alembic upgrade head Co-Authored-By: Claude <noreply@anthropic.com>
AIN-141 [Audit P1 · api] Strip retired AAMC models (`Llama-3.3-70B`, `DeepSeek R1`) from tests + scripts + migration
Phase 1 audit finding (2026-05-18). Retired AAMC v1.0 models still in active Status: code-side scope ✅ shipped (PR #19 merged, CI green); DB-side surfaced as founder DDL tap (migration drafted, awaiting founder review + apply). Code-side cleanup (✅ DONE, PR #19, merged to main)3 files, 12 insertions, 10 deletions:
CI conclusion: success. Pre-commit hooks all green (ruff + mypy --strict + pytest -x unit + smoke). DB-side cleanup (🛑 FOUNDER TAP REQUIRED, migration drafted locally)Local file: Migration targets 3 retired slugs for DELETE from
Per memory Why founder tap: Charter v3 §"Founder taps" lists "Supabase or Railway DDL" as founder-only. Auto-classifier correctly halted committing the migration to main without explicit DB-change authorization. Founder reviews the migration text + commits in a separate PR + runs Founder next action: cd ~/code/ainfera-ai/api
# 1. Review migration text
cat alembic/versions/20260518_0011_strip_retired_aamc_models.py
# 2. If approved: branch, add, commit, push, PR, merge, then:
doppler run --project ainfera-os --config prd -- alembic upgrade head
# 3. Verify deletion count matches expectation (likely 1-3 rows total)RefsPR #19 (code-side, merged): #19 |
* feat(api): AIN-183 P0-2 · flip aa_index_source for AAMC voters gemini-3-1-pro + mistral-large-3 Two of the five AAMC v1.0 voter slugs landed in prod with aa_index_source='estimate_2026_q2' because at the time the Artificial Analysis Intelligence Index didn't have a stable reading for either model. The other three (claude-opus-4-7, gpt-5-5, grok-4) shipped with 'aamc_v1_lock'. AA Index has since locked readings for both, and the marketing /models page calls out 'AAMC v1.0' across the whole 5-voter pool — leaving two on a provisional source label reads as a discipline #11 mismatch (spec / runtime / copy disagree). ## Migration `20260519_0017_aamc_voter_source_lock.py` runs a single UPDATE filtered on the current value, so re-applying is a no-op. The rowcount assertion (Memory #20: silent no-ops ship as green CI) accepts 0 (fresh DB / re-apply) or 2 (first prod apply) but raises on any other value. Downgrade reverses the flip — safe because no row-level dependency hangs on the source label. ## Invariant test `test_aamc_5_voters_use_v1_lock_source` lives next to the existing `test_aamc_5_canonical_voters_always_active` and asserts the same lock shape but for `aa_index_source`. Any future migration that regresses one of the five voters back onto a provisional source label fails CI here, with the discipline #11 reasoning surfaced in the assertion message. ## Friction The audit prompt called the column `aamc_index_source`. Actual ORM declaration is `aa_index_source` (api/ainfera_api/orm.py:332) and the public API response field is the same. Migration uses the real name. Closes: AIN-183 P0-2 (AAMC source label drift) Discipline: #1 (no-op no longer ships green), #11 (catalog labels align across DB / API / marketing), Memory #20 (rowcount assertion). * fix(api): AIN-183 P0-2 migration · drop updated_at clause (column does not exist) CI integration failed with: asyncpg.exceptions.UndefinedColumnError: column "updated_at" of relation "models" does not exist The models table has only created_at, not updated_at. My original sketch carried updated_at over from a generic timestamp-touch pattern that doesn't apply here. Drop the clause from both upgrade() and downgrade() — the aa_index_source flip is the only data change. Locally: alembic upgrade head succeeds; the data-integrity assertion (0 or 2 rows affected) still guards against silent no-ops. --------- Co-authored-by: Aule <aule@ainfera-internal.local>
…tion C)
/v1/audit/public was building canonical URIs as
`ainfera.ai/{owner_handle}/{agent_name}` where owner_handle was read off
the agent row. For founder-owned agents (Varda, Yavanna) this surfaced
`ainfera.ai/hizrianraz/varda` on the most-trafficked public endpoint we
run — a discipline #3 leak of founder GitHub-handle / PII.
The Discipline #12 fix landed in the AIN-183 audit prompt is Option C: add
a public-facing handle on the `tenants` row that's decoupled from the
GitHub handles on agent rows, and project that on the public surface
instead.
Three-phase, all in one upgrade():
1. Add `tenant_handle TEXT NULL` to `tenants`.
2. Backfill in priority order:
a. Tenants that own at least one agent with `owner_handle='hizrianraz'`
→ `tenant_handle='ainfera-ai'` (founder tenant id is not hardcoded;
lifted from data).
b. Remaining tenants → MIN(owner_handle) across their agents (stable +
deterministic + matches the GitHub handle most users registered as).
c. Agent-less tenants → contact_email local part.
d. Conflict resolution: collisions append a 6-char id-slice suffix in
stable id order, so the first-by-id keeps the bare handle.
3. NOT NULL constraint + unique index. Pre-NOT-NULL the migration asserts
zero rows remain NULL (Memory #20 silent-no-op guard).
- `TenantORM.tenant_handle` declared NOT NULL UNIQUE String(64).
- `routers/audit.py` public_feed projection joins through TenantORM and
reads tenant_handle. The response key stays `owner_handle` — the public
API contract is unchanged, only the value source moves.
- All four TenantORM instantiation sites populate the new column:
- routers/signup.py (SDK-CLI signup → tenant_handle=owner_handle)
- routers/github_oauth.py (OAuth login → tenant_handle=github_login)
- routers/install.py (resolve-or-create on install → same as oauth)
- routers/tenants.py (/v1/tenants/register → contact_email local part)
```
curl -s https://api.ainfera.ai/v1/audit/public | \
jq -r '.events[].canonical_uri' | grep -c hizrianraz
curl -s https://api.ainfera.ai/v1/audit/public | \
jq -r '.events[].canonical_uri' | grep -c ainfera-ai/varda
```
Once this lands, the marketing AuditTicker widget (already filters
`ainfera-ai/varda` and `ainfera-ai/yavanna` on the web side) starts
matching real events — closes PR E without a web-side code change.
- Prompt said `tenants.tenant_handle` is a new column. Confirmed via ORM
read — column did not exist (only id/name/contact_email/api_key_hash/
created_at). Migration adds it.
- Public response field stays named `owner_handle` to avoid breaking the
API contract; only the underlying value changes. If a future PR wants
to rename the response field to `tenant_handle`, that's a separate
ContractDelta against the PublicAuditEvent Pydantic model.
Closes: AIN-183 P0-3 (founder PII on /v1/audit/public)
Discipline: #1 (claim "no founder PII on public" matches reality),
assertions on data migration).
…tion C) (#47) /v1/audit/public was building canonical URIs as `ainfera.ai/{owner_handle}/{agent_name}` where owner_handle was read off the agent row. For founder-owned agents (Varda, Yavanna) this surfaced `ainfera.ai/hizrianraz/varda` on the most-trafficked public endpoint we run — a discipline #3 leak of founder GitHub-handle / PII. The Discipline #12 fix landed in the AIN-183 audit prompt is Option C: add a public-facing handle on the `tenants` row that's decoupled from the GitHub handles on agent rows, and project that on the public surface instead. Three-phase, all in one upgrade(): 1. Add `tenant_handle TEXT NULL` to `tenants`. 2. Backfill in priority order: a. Tenants that own at least one agent with `owner_handle='hizrianraz'` → `tenant_handle='ainfera-ai'` (founder tenant id is not hardcoded; lifted from data). b. Remaining tenants → MIN(owner_handle) across their agents (stable + deterministic + matches the GitHub handle most users registered as). c. Agent-less tenants → contact_email local part. d. Conflict resolution: collisions append a 6-char id-slice suffix in stable id order, so the first-by-id keeps the bare handle. 3. NOT NULL constraint + unique index. Pre-NOT-NULL the migration asserts zero rows remain NULL (Memory #20 silent-no-op guard). - `TenantORM.tenant_handle` declared NOT NULL UNIQUE String(64). - `routers/audit.py` public_feed projection joins through TenantORM and reads tenant_handle. The response key stays `owner_handle` — the public API contract is unchanged, only the value source moves. - All four TenantORM instantiation sites populate the new column: - routers/signup.py (SDK-CLI signup → tenant_handle=owner_handle) - routers/github_oauth.py (OAuth login → tenant_handle=github_login) - routers/install.py (resolve-or-create on install → same as oauth) - routers/tenants.py (/v1/tenants/register → contact_email local part) ``` curl -s https://api.ainfera.ai/v1/audit/public | \ jq -r '.events[].canonical_uri' | grep -c hizrianraz curl -s https://api.ainfera.ai/v1/audit/public | \ jq -r '.events[].canonical_uri' | grep -c ainfera-ai/varda ``` Once this lands, the marketing AuditTicker widget (already filters `ainfera-ai/varda` and `ainfera-ai/yavanna` on the web side) starts matching real events — closes PR E without a web-side code change. - Prompt said `tenants.tenant_handle` is a new column. Confirmed via ORM read — column did not exist (only id/name/contact_email/api_key_hash/ created_at). Migration adds it. - Public response field stays named `owner_handle` to avoid breaking the API contract; only the underlying value changes. If a future PR wants to rename the response field to `tenant_handle`, that's a separate ContractDelta against the PublicAuditEvent Pydantic model. Closes: AIN-183 P0-3 (founder PII on /v1/audit/public) Discipline: #1 (claim "no founder PII on public" matches reality), assertions on data migration). Co-authored-by: Aule <aule@ainfera-internal.local>
… endpoints (#52) Closes the backend half of AIN-182 §Phase 2 §5 + §6. Workflow templates are reusable blueprints a tenant can apply to an agent — ordered tasks with D11 model-class hints, optional HITL rules, D10 visibility model. ## Migration 20260519_0020 - New `templates` table (id, tenant_id NULL=system, name, description, visibility enum, tasks JSONB, hitl_rules JSONB, timestamps) - Unique (tenant_id, name) per tenant + partial unique on name for system templates (tenant_id IS NULL) - New `agents.template_id` FK (nullable, SET NULL on template delete) - Seeds 6 Ainfera system templates: inference-frontier, inference-fast, inference-cost-light, inference-embedding, routing-balanced, routing-quality-first. Memory #20 rowcount-asserted. ## CRUD endpoints - GET /v1/templates list (system + tenant own) - GET /v1/templates/{id} detail (404 across-tenant) - POST /v1/templates tenant-scoped create - PUT /v1/templates/{id} owner-only update - DELETE /v1/templates/{id} owner-only delete System templates (tenant_id NULL) are read-only via the API; the migration is the only path that mutates them. ## OpenAPI contract EXPECTED_OPERATIONS extended with 5 new paths. Closes part of AIN-182 (Phase 2 §5 + §6 backend half). Co-authored-by: Aule <aule@ainfera-internal.local>
Summary
Forward-fix migration: DELETE 3 retired AAMC v1.0 slugs from
modelstable per Charter v3 §Don't (Llama-3.3-70B + DeepSeek R1 retired 2026-05-16).Branch history
bind.exec_driver_sql(..., %s)— psycopg2 syntax). Founder ranalembic upgrade headagainst prod which advanced20260517_0009+20260517_0010successfully, then this migration failed withsyntax error at or near %(asyncpg uses$1, not%s).text()+ named:slugbinds, dialect-agnostic.Prod alembic state after the failed run
Per transactional DDL: 0011 rolled back. Prod
alembic_versionis at20260517_0010. After this PR merges, re-runningalembic upgrade headapplies the fixed 0011.Commit 4af45ed also includes 1 file from founder's WIP (
tests/integration/test_aamc_invariants.py) that was already staged in the working tree when Aule wrote the migration fix. The bundle happened becausegit stash --keep-indexpreserved staged-but-not-by-me changes. Diff is 11+ / 7- lines — review the test_aamc_invariants.py portion of 4af45ed and either accept (if WIP was ready) or surgically revert before merge.Founder next action
Refs
Closes AIN-141 DB-side.