Repository navigation
feat(usai-provider): normalize and vendor USAi model catalog - #436
Conversation
mogul
left a comment
There was a problem hiding this comment.
I think you should close this and start again based on these changes:
#423 (comment)
@wz-gsa what's your plan for this one? |
|
You're right about the mechanism, and I'm going to act on it — but I did the rebase first to see exactly what conflicts, and it's narrower than "start again." Proposing a rescope rather than a close. AI-assisted analysis (OpenCode), advisory; I'm the author here, so this is a proposal, not a review. The rebase is clean — so the conflict isn't with
|
| File | +lines | Survives the redesign? |
|---|---|---|
spec.yaml |
80 | No — the guest-write startup command |
scripts/normalize-catalog.mjs |
152 | Yes — host-side authoring tool; ADR unchanged on it |
tests/normalize-catalog.test.mjs |
95 | Yes |
tests/fixtures/normalize-catalog-source.json |
64 | Yes |
files/home/usai-config/model-catalog-snapshot.json |
55 | Yes — the new ADR wants this, just delivered via the :ro mount |
scripts/verify (1b env-ownership) |
27 | Partial — the check moves host-side, the logic is reusable |
The normalizer was already host-side-only and already emits acq-neutral-model-catalog/v1. The trust-boundary change doesn't touch it.
Proposal: rescope this PR, don't close it
- Drop the self-registration startup command from
spec.yamlentirely. This PR stops claiming to deliver Layer 1. - Keep the normalizer + tests + fixture + vendored snapshot, retitled to what they are: the provider's own catalog normalizer and vendored fallback.
- Layer 1 lands as a follow-up on top of quickstart #504, as a static facts file plus host-side validation.
That last point is a sequencing constraint worth naming: Mechanism 2 (the :ro host-materialized mount) is implemented in quickstart #504, which is still open and BEHIND. Layer 1 can't be built correctly until it merges. Rewriting this PR now would mean writing against a mechanism that doesn't exist yet.
If you'd rather have one PR after #504 lands, say so and I'll close this — but I'd rather not leave 393 working lines rotting on a branch for however long #504 takes.
Separately: the facts shape needs revision, independent of the mechanism
While I had the thing open I surveyed how this is actually modeled in the wild — models.dev, OpenCode, LiteLLM, Vercel AI SDK, Cline, Continue — and then measured against live models.dev/api.json (223 providers, 8,173 models) rather than trusting the survey. Our draft is {schemaVersion, providerId, host, baseUrl, modelsUrl, keyEnv}. Findings that matter for a contract meant to carry many providers:
- Provider-level fields across all 223 are exactly
{id, env, npm, name, doc, models, api}. Nohost, nomodelsUrl, nokeyEnv, noschemaVersionanywhere. envis an array on all 223, and 14 need more than one variable —["CLOUDFLARE_ACCOUNT_ID","CLOUDFLARE_API_KEY"],["DATABRICKS_HOST","DATABRICKS_TOKEN"],["SNOWFLAKE_ACCOUNT","SNOWFLAKE_CORTEX_PAT"]. A scalarkeyEnvcannot express Azure, Bedrock, or Vertex at all.providerId→ideverywhere (models.dev, Cline, LiteLLM'slitellm_provider). Nobody usesproviderIdat provider level.hosthas zero precedent in any of the six. It'snew URL(baseUrl).host, and it becomes a lie the moment an operator overridesbaseUrl. If it exists for egress allowlisting, that's a different field with a different name on a different layer.- We're missing the API-dialect field, and every single surveyed tool has one — models.dev
provider.shape: responses|completions, Clineprotocol(6 values), OpenCodenpm(182/223 are@ai-sdk/openai-compatible), LiteLLMcustom_llm_provider. It is not inferable frombaseUrl, and Azure needs it per model under one base URL. Without it a consumer can't form a request. baseUrlneeds to be a template. 5 providers embed${ENV_VAR}inapi—https://${DATABRICKS_HOST}/ai-gateway/mlflow/v1. A plain string can't express them.modelsUrlshould be relative tobaseUrl. Ours is literallybaseUrl + "/models", which desynchronizes the moment someone pointsbaseUrlat a proxy. Cline rebases it deliberately. Listing-response shapes also disagree — Ollama is/api/tagswith a different body — so a bare URL with no declared shape is a guess.
The one I'd most want to fix before v1 freezes
cost.context_over_200k is on 485 live models.dev entries, and their own contributor docs now call it "legacy output-only — schema rejects it on write, the generator may still emit it for old consumers." They had to add cost.tiers[] (567 entries) alongside it and keep emitting the old field forever. LiteLLM has the same wound three times (_above_200k_tokens, _above_272k_tokens, _above_128k_tokens). OpenCode v2 is actively normalizing it away.
A magic number in a field name is permanent. Making cost tier-capable now is free; retrofitting it isn't. Same for the unit — models.dev is per-million, LiteLLM is per-token, Vercel is per-token-as-string. A bare cost is a 1000× error waiting to happen, so the unit belongs in the field name.
And a forward-compat rule worth adopting explicitly
LiteLLM's schema states it in-band: additionalProperties: true, one required field, and a description telling consumers to ignore unknown fields rather than reject them. Four years and ~191 fields later it has never broken. OpenCode sets additionalProperties: false 29 times including on ProviderConfig, so a validator from version N rejects a config from N+1. Continue shipped a closed capability enum, broke consumers, and patched it with z.union([...literals, z.string()]) — the code comment cites the PR number.
Suggestion: strict on authoring (catch a kit author's typo in validate-kits.py CI), permissive on consuming (never reject a newer producer). None of the six does both cleanly; it costs one schema file and two validation profiles.
One thing worth keeping from this PR either way: the normalizer leaves an absent price absent. Cline's equivalent does model.cost?.input ?? 0, which turns "price unknown" into "free" — precisely the degraded-path defect this repo's own rules exist to prevent.
Three questions, all scope not direction
- Does the shape revision belong as an amendment to your ADR-0003, or a separate ADR?
- Is the rescope above acceptable, or do you want one PR after #504?
- Who owns chore(isolation): formalize JSON Schema for acq-provider-facts/v1 and acq-neutral-model-catalog/v1 #435 (the formal JSON Schema)? Worth settling before two of us write one. My read: the survey above is the consumer evidence chore(isolation): formalize JSON Schema for acq-provider-facts/v1 and acq-neutral-model-catalog/v1 #435 was waiting for — we now know what 223 real providers need, so it no longer has to be guesswork against zero consumers.
Full measurements and the sequencing plan are written up locally; happy to paste any part of it here if useful.
mogul
left a comment
There was a problem hiding this comment.
Address now
-
Blocking:
integrations/isolation/acq-kits/usai-provider/spec.yaml:117-136still writes provider facts from inside the sandbox at startup as root. That is the mechanism the current PR discussion identifies as invalid for the redesigned provider-discovery trust boundary: the in-sandbox agent has passwordless sudo, so a root-owned guest path is not a trustworthy source of provider facts. Leaving this in means a future consumer would be encouraged to trust/var/lib/acq/models/providers/usai.jsoneven though that file is materialized inside the mutable guest, not from the host-side/static:romechanism discussed as the replacement. Please remove or rescope the self-registration startup command and the dependent claims/checks before merging. -
Blocking follow-on to the same issue:
integrations/isolation/acq-kits/usai-provider/scripts/verify:77-102validates the obsolete guest-writtenkeyEnvheredoc by greppingspec.yaml. Once the facts file is no longer authored insidespec.yaml, this check either becomes inert or continues to bless the wrong mechanism. Please move this validation to the host-side/static facts source when that source exists, or drop it from this PR if this PR is rescoped to only the normalizer/snapshot.
Track later
-
The normalizer and tests look broadly reasonable, but the new neutral catalog shape is still unschematized. The existing follow-up
GSA-TTS/agentic-coding-patterns#435should remain the tracked place to define size limits, required fields, unknown-field behavior, and defensive parsing expectations for the first real consumer. -
There is a pre-existing durable comment reference in
integrations/isolation/acq-kits/usai-provider/spec.yaml:172usingquickstart#235. It is outside this PR's added hunk, so I am not treating it as a blocker here, but it still conflicts with the repository's durable-reference guidance and should be converted to prose or a fully qualified reference when that area is next touched.
Checks run
- Confirmed local
HEADmatches GitHub PR head SHAa7dec19caab22974d7491ad207625abe3c33f583. - Reviewed PR timeline/comments/reviews with
gh pr view 436 -R GSA-TTS/agentic-coding-patterns --comments --json ...,gh api repos/GSA-TTS/agentic-coding-patterns/issues/436/timeline --paginate,gh api repos/GSA-TTS/agentic-coding-patterns/pulls/436/reviews, andgh pr diff 436 -R GSA-TTS/agentic-coding-patterns. node --check integrations/isolation/acq-kits/usai-provider/scripts/normalize-catalog.mjspassed.node --test integrations/isolation/acq-kits/usai-provider/tests/normalize-catalog.test.mjspassed: 8/8 tests.git diff --checkpassed.python3 integrations/isolation/acq-kits/validate-kits.py --strict integrations/isolation/acq-kits/usai-providerwas attempted but blocked locally by missingjsonschema; CI already reports the kit validation job passing on this head.
Decision: request changes until the obsolete guest-written provider-facts mechanism is removed or replaced with the redesigned host-side/static mechanism.
…no-provider case Addresses the three "Address now" items from review of #423 at d20f292. 1. NORMALIZER CONTAINMENT CONTRACT (was: undefined). The ADR introduced provider-shipped executable code invoked with the provider's credential but never said what contains it. Layer 2 step 2 now specifies the division explicitly: the shared bounded-fetch helper performs the HTTPS request itself (applying the timeout, size cap, and https-plus-declared-host check) and invokes the normalizer as a PURE FILE-TO-FILE TRANSFORM over bytes it already fetched. The normalizer's contract: no network I/O, no credential (minimized environment excluding keyEnv), its own wall-clock and output-size bounds, and failure routed to the snapshot fallback rather than accepted. This matches the normalizer already shipped on #436, which documents itself as "reads exactly one file, writes exactly one file, makes no network calls" and takes --source/--out. So the contract records the real design rather than imposing a new one. The ADR now also states that read-only mounting protects the normalizer's INTEGRITY while saying nothing about the safety of EXECUTING it -- separate properties, both required. The "No new credential exposure" security bullet said the orchestrator invokes the normalizer with the credential, which directly contradicted the new contract; it is rewritten as "Provider-shipped code never holds the credential", and notes this NARROWS the standing trust boundary rather than merely reusing it. 2. providerId AS A PATH SEGMENT (was: only schema/env/SSRF validation named). Added the invariant: safe slug matched WHOLE-STRING, must equal the materialized directory name with the orchestrator deriving identity from the globbed directory and cross-checking facts.json, and unique within the materialized set (collision fails provisioning rather than letting one kit overwrite another's normalizer while keeping its keyEnv). The whole-string requirement is stated because `^...$` is NOT sufficient: `$` matches before a trailing newline in Python, JavaScript, PCRE and grep alike, so `^[a-z0-9]...$` accepts "usai\n" and would create a directory name with an embedded newline. Verified both ways with re.match vs re.fullmatch before writing it down; the counterexample is in the ADR so an implementer cannot reintroduce it. Also requires rejecting a non-string providerId. 3. NO-PROVIDER-PRESENT FALLBACK (was: incoherent). Step 4 listed "no provider present" among the cases that fall back to "that provider's vendored snapshot" -- but when no provider is present there is no snapshot to read. Split into two cases: per-provider refresh failure uses that provider's snapshot; no provider present writes a VALID, EXPLICITLY EMPTY aggregate with provenance recording zero providers discovered, and exits success. It must not omit the file, write a partial, invent a default provider, or fail startup -- a consumer has to be able to distinguish "no providers configured" from "discovery did not run", which an absent file cannot express. That required two constraints on the catalog shape the ADR had not stated: the model list is required and MAY be empty, and provenance is required per entry (live vs snapshot) plus per discovery (live/snapshot/rejected). Both are recorded as constraints the formal schema in #435 must honor, keeping #435 the single tracked place for the schema itself. Also, from the same review's "Track later" item 3: the Mermaid diagram was left inconsistent by the above. It said "run normalizer (in-guest, provider's own credential)", the exact claim item 1 inverts, and its host-side validation step omitted the providerId check. Both corrected. Negative/risks now states the residual risk plainly -- this design does execute provider-shipped code in the guest, contained by the step 2 contract rather than by the mount -- and relaxing any clause of that contract is added to "What an agent must NOT decide unilaterally". Verified: python3 scripts/validate_repo.py --root . -> exit 0, all validators passed python3 scripts/validate_references.py -> 41 patterns, all resolve python3 scripts/validate_frontmatter.py -> 47/47 passed python3 scripts/validate_sensitive_terms.py -> exit 0 python3 scripts/scan_unsafe_shell.py . -> 0 errors, 0 warnings markdownlint-cli2 -> 0 errors git diff --check -> clean internal anchors (#security-model, #alternatives-considered) -> resolve validate_repo.py is the check the reviewer was blocked on locally (missing jsonschema, then no disk space); it runs clean here with jsonschema 4.26.0. No code changes -- ADR prose only. AI assistance disclosed: prepared by an AI agent (OpenCode) on operator instruction. Human review and merge still required.
|
Rescoped to the half that's sound. Both blocking items addressed in Blocker 1 — the facts file is gone (58 lines)I verified your reasoning rather than taking it on trust, and it holds at a sharper source than the ADR:
So the kit's own comment was wrong, and that's worth naming separately from the mechanism objection:
DAC-true, false as a security property — Rather than delete silently, Blocker 2 — verify check 1b is goneByte-identical to main — exactly this PR's addition removed, none of the pre-existing script touched. Two things about that check worth recording, independent of the coupling you identified:
Both are mine, both now deleted. Two additions beyond the strict rescopeA reviewer on the consensus panel found two gaps I'd missed. Both are closed. 1. The snapshot had no drift gate. Nothing asserted Degraded paths exercised, not assumed: 2. The snapshot's comment was the same trust invitation one directory over — with weaker ownership. It said a consumer "has SOMETHING to read", and it ships to On your "close and start again"I kept the branch rather than restarting. The conflicting surface was 58 of 473 lines, all in VerificationTrack-later items, left as you scoped them
One thing I found in the pre-existing verify scriptNot this PR's doing, not fixed here, but you should know. Zero checks run reports success. Concretely on Happy to file that separately if useful — I didn't want to widen this PR to fix it. AI-assisted (OpenCode), prepared on operator instruction. Advisory; human review and merge still required. |
…versions Corrects an error in my own earlier revision of this ADR (e322cb0), found while trying to write the formal JSON Schemas for #435. THE DEFECT. The ADR said both a normalizer's output and the orchestrator's aggregate carry `schemaVersion: "acq-neutral-model-catalog/v1"`, AND that per-entry provenance is required on that shape. Those two statements cannot both hold: - A normalizer CANNOT know its own provenance. It is a pure file-to-file transform over bytes the bounded-fetch helper already retrieved (Layer 2, step 2) -- whether those bytes came from a live refresh or from a vendored snapshot is known only to its caller. Requiring `source: live|snapshot` in its output means a field the producer must be TOLD and cannot verify. - It would have invalidated artifacts already shipping in this shape: the normalizer output and vendored snapshot on #436 both carry this version with no provenance, and #436 is green awaiting re-review. A schema honoring the ADR literally would have failed both. THE FIX. Two documents, versioned separately: acq-neutral-model-catalog/v1 one provider's normalized model list (a normalizer's output, or a vendored snapshot) acq-neutral-model-aggregate/v1 the multi-provider aggregate (orchestrator) Provenance belongs where the knowledge is: the orchestrator performed the fetch or took the fallback, so the orchestrator records it. The aggregate requires discovery provenance (per provider: live / snapshot / rejected -- so a provider rejected by host-side validation is visible as rejected, not merely absent), and each entry names its origin `providerId`. The per-provider document requires no provenance field at all. The empty-array rule is kept on both: a provider legitimately exposing no models is valid, and an aggregate with zero entries is what step 4 writes when no provider is present. Also stated explicitly on the per-provider shape, because it is now a document that may be read from a guest-writable path: model METADATA only, never routing or credential fields (host, baseUrl, modelsUrl, keyEnv). A rewritten routing field could redirect a consumer; those facts live in the host-validated facts file. #436 already enforces this with a test asserting the shipped snapshot carries none of those keys -- this records the rule the test checks. The mistake is written into the ADR rather than quietly corrected, because the reasoning ("a pure transform cannot attest to where its input came from") is easy to re-make and the note is what prevents that. The Alternatives overlap note is updated too: there are now two distinct neutral-catalog lineages here plus the already-shipped `usai-model-catalog/v1` in integrations/providers/usai/, which makes the #435 reconciliation broader, not narrower. Operator decision, recorded: split chosen over "provenance required everywhere" once the cost of the latter was measured -- reopening a green PR and adding an unverifiable caller-asserted field to a pure function. No design change to Layers 1-3; the containment contract from e322cb0 stands. Verified: python3 scripts/validate_repo.py --root . -> exit 0, all validators passed markdownlint-cli2 -> 0 errors long-line check -> only the new 2-row table exceeds 80 git diff --check -> clean AI assistance disclosed: prepared by an AI agent (OpenCode) on operator instruction. Human review and merge still required.
…vendored fallback Implements ADR 0003 (agentic-coding-patterns#423) Layer 1 for the usai-provider kit: - Self-registration: a new root-owned startup command writes /var/lib/acq/models/providers/usai.json (schemaVersion acq-provider-facts/v1: providerId, host, baseUrl, modelsUrl, keyEnv). Follows the same idempotent /var/lib/acq/ marker convention the sibling quickstart repo's acq.backends/msb.sh already uses for its own root-owned markers. - Env-var-ownership check: scripts/verify's new static section 1b confirms the facts file's keyEnv value also appears elsewhere in the kit's own spec.yaml, so this kit can never register a credential env var it doesn't itself declare (the security condition from ADR 0003's incorporated dissent). - Provider's own normalizer: scripts/normalize-catalog.mjs converts integrations/providers/usai/catalog.json (usai-model-catalog/v1) into the neutral acq-neutral-model-catalog/v1 shape, reusing field names verbatim (contextWindow, maxOutputTokens, cost.*) and passing cost through unchanged (per-million-token, no unit transform). Host-side authoring tool, no network calls, no side effects beyond its one output file. Default --out is tests/output/, matching the existing sync-usai-models.mjs --write-snapshot convention (fixtures/ stays hand-authored-inputs-only). - Provider's own vendored fallback snapshot: files/home/usai-config/model-catalog-snapshot.json, 5 real models with real ids/pricing pulled directly from catalog.json, shipped into every sandbox at /home/agent/usai-config/. Verified: node --check on both new/modified .mjs files; node --test (8/8 new tests passing, 55/55 full kit suite); validate-kits.py --strict (usai-provider OK); the new startup command's embedded shell extracted and checked with sh -n and shellcheck --severity=warning (clean); scripts/verify's new section 1b run live (passes); every vendored snapshot model's id/contextWindow/maxOutputTokens/cost cross-checked against catalog.json's real values. Formal JSON Schema files for the two new shapes are tracked as a fast-follow (agentic-coding-patterns#435) rather than written now, per a 3-role consensus review: ADR 0003 is still unmerged, and the shapes have zero real consumers until the models orchestrator kit (agentic-coding-patterns#425) is built — that is the right forcing moment to pin required/additionalProperties against actual usage. Out of scope (unchanged): opencode.jsonc rendering, the models orchestrator kit itself, goose-server. Co-authored-by: OpenCode Agent <agent@gsa.gov>
#423's ADR moved from `0003-neutral-model-provider-discovery.md` to `0004-` to clear a collision with #416, which claimed `integrations/isolation/docs/ decisions/0003-` first. Update the two comment references here so they do not dangle once that lands. Comment-only; no behavior change. `node --check` and a YAML parse both pass. AI-assisted (OpenCode). Human review and merge still required.
…al catalog Rescopes this PR to the half that is sound. Addresses both blocking items from review at a7dec19. BLOCKER 1 -- the facts file is gone. spec.yaml wrote /var/lib/acq/models/providers/usai.json from inside the sandbox at startup as root (58 lines removed), on the theory that a root-owned guest path would be a trustworthy source for a future provider-discovery consumer. That reasoning was wrong, and I verified it rather than taking it on the reviewer's word: quickstart's acq itself writes `printf "agent ALL=(ALL) NOPASSWD:ALL\n" > /etc/sudoers.d/90-acq-agent` (acq.backends/msb.sh at origin/main), and docs/BACKEND_GUIDE.md states the same guarantee as the sbx base-image contract. So the spec's own comment -- "the agent (uid 1000) cannot write concurrently (root-owned, agent has no write access to /var/lib/acq/)" -- was DAC-true but FALSE as a security property: `sudo tee` defeats it. The file advertised `keyEnv`, a credential POINTER, so an agent able to rewrite it could have redirected a consumer at a different env var. That is a confused-deputy primitive, and it was shipping with a comment licensing a consumer to trust it. Trustworthy facts need a host-materialized, read-only source. That mechanism does not exist yet -- it depends on acq's host-config `:ro` mount, still unmerged upstream -- so this kit now ships NO facts file rather than one at the wrong trust level. Rather than delete silently, spec.yaml's header records what was tried, why it was wrong, and what the replacement requires, so the next reader does not reinvent it. BLOCKER 2 -- verify check 1b is gone. It extracted `keyEnv` by grepping the heredoc, so removing the heredoc would have left `facts_key_env` empty and `bad()` called: a check failing for the wrong reason. scripts/verify is now BYTE-IDENTICAL to origin/main, so exactly this PR's addition was removed and none of the pre-existing script was touched. Worth recording why that check was weak independent of the coupling: it asserted "the kit declares this env var itself" by counting occurrences of the value across the whole spec.yaml and requiring >= 2. The only two mentions were a HEADER COMMENT and the heredoc itself, so a check the PR called "security-load-bearing" passed entirely on the strength of one comment line. It also used `grep -c`, which counts matching LINES, not occurrences (verified: `AAA AAA AAA` on one line gives 1, not 3), so its own message was wrong whenever two mentions shared a line. WHAT SURVIVES, and why it is sound: scripts/normalize-catalog.mjs (a HOST-side authoring tool -- one file in, one file out, no network, no credential, matching the containment contract landed on ADR-0004 in #423), its 8 tests, the source fixture, and the vendored snapshot. TWO ADDITIONS beyond the strict rescope, both closing gaps found while doing it: 1. A drift gate for the shipped snapshot (2 tests). Nothing asserted that model-catalog-snapshot.json matched normalizeCatalog(providers/usai/catalog.json), so it was a second hand-maintained copy that could silently diverge from its stated source. It is a deliberate 5-of-21 SUBSET, so the gate allows a subset but requires every kept entry to be byte-equal, and asserts BOTH sides are non-empty first -- an emptied snapshot would otherwise make the comparison pass vacuously. Degraded paths exercised: a mutated cost fails with "has drifted from the normalizer output"; an emptied models[] fails with "nothing to compare, so this gate cannot pass". 2. The snapshot's comment no longer frames it as something a consumer should trust. It ships to /home/agent, which the agent OWNS -- forging it needs no escalation at all, so it was the same invitation as the removed facts file with WEAKER ownership. It is now documented as not authoritative, and a second test asserts the shape stays metadata-only (no host/baseUrl/modelsUrl/keyEnv/apiKey at either envelope or model level), so a routing field cannot be added later without a test failing. Verified: node --test tests/normalize-catalog.test.mjs -> 10 pass, 0 fail (was 8) make test-kits -> 80 pass, 0 fail validate-kits.py --root . --strict -> All 7 acq-kits valid python3 scripts/validate_repo.py --root . -> exit 0 python3 scripts/scan_unsafe_shell.py . -> 0 errors, 0 warnings markdownlint-cli2 -> 0 issues git diff --check -> clean git diff origin/main -- scripts/verify -> empty (byte-identical) NOT in this PR, deliberately: the host-side facts mechanism (needs the upstream `:ro` mount first) and reconciling `acq-neutral-model-catalog/v1` with the already-shipped `usai-model-catalog/v1` in integrations/providers/usai/ -- two catalog schemas coexist, tracked at #435. Decision support: nexus-agents 7-role consensus panel, unanimous 7/7 for this rescope over four alternatives (including converting to the unmerged `:ro` mount, and adding a lockfile to defend a concurrency property sudo defeats anyway). Panel caveat recorded honestly: all 7 seats ran on one model, so independence is weaker than the role count suggests. The agent-owned-snapshot and snapshot-drift gaps were raised by the contrarian seat, not found by me. AI assistance disclosed: prepared by an AI agent (OpenCode) on operator instruction. Human review and merge still required.
… removal
Rebase onto main (1.11.0) brought in the chat-model catalog change that dropped
embedding models, which removed `cohere_english_v3` from
integrations/providers/usai/catalog.json. The vendored snapshot still shipped it,
so this branch's own drift gate failed:
AssertionError: snapshot model 'cohere_english_v3' is absent from the
upstream catalog -- regenerate the snapshot with scripts/normalize-catalog.mjs
That is the gate working as designed: it caught a real divergence introduced by
an upstream change rather than by this branch, and its message named the fix.
Snapshot regenerated through the normalizer and refiltered to the same subset
minus the removed model, so every remaining entry is byte-equal to
normalizeCatalog(catalog.json). The dropped entry was an embedding model, which
no longer belongs in a chat-model catalog, so the subset is 4 rather than 5.
Nothing hardcodes the count.
Verified on the rebased tree:
node --test tests/normalize-catalog.test.mjs -> 10 pass, 0 fail
make test-kits -> 110 pass, 0 fail
validate_repo.py --root . -> exit 0
validate-kits.py --root . --strict -> all 10 acq-kits valid
scan_unsafe_shell.py . -> 0 errors, 0 warnings
git diff --check -> clean
Co-authored-by: OpenCode Agent <williamjzujkowski@users.noreply.github.com>
b0f8980 to
142416f
Compare
…cts-registration-local
…cts-registration-local
|
@mogul — this is CI-green now (rebased onto `main` post-#471/#548/#478, so it picks up both the markdownlint-cli migration and the katex/smol-toml override fix). Your prior CHANGES_REQUESTED review was dismissed on Oct 5; per the Oct 1 thread both original blockers were addressed in `b0f8980` (guest-written provider facts dropped entirely rather than redesigned, follow-up filed as #456) and I think that's still accurate — just needed the CI catch-up pushed through today. Could you take another look when you have a chance? |
Adversarial Review ReportFinding
Review Scope
Verification Transcript
Decision: request changes for the normalizer validation gap. AI-assisted review by OpenCode; human review and merge remain required. |
Reject malformed model entries and unrepresentable limits or costs before generating a neutral catalog. Co-authored-by: OpenCode [gpt-5.6-terra] <williamjzujkowski@users.noreply.github.com>
|
Thanks for I probed 14 cases your test list doesn't cover. Nine are correctly rejected without being explicitly tested (
Two notes, neither blocking. 1. Whitespace-only That reaches the neutral catalog as a usable model identifier, and 2. Duplicate model ids are accepted. The source schema has no I'm the author of record here so I can't approve this; flagging both for you to take or leave. |
Validation updateThe catalog-entry validation repair is committed and pushed in
An adversarial read-only re-review found the fractional-limit issue resolved and no remaining actionable findings. The change is ready for the repository-required human review and approval; no additional review cycle is proposed unless a reviewer identifies a concern. AI assistance was used for implementation and review; the commit includes the required attribution. |
|
(I exercised Maintainer's Privilege to merge without another review round, since this has been pretty thoroughly thrashed in previous cycles.) |
Summary
Adds the
usai-providerkit's host-side USAi model-catalog normalizer and a small, explicitly non-authoritative metadata snapshot. The prior guest-written provider-facts mechanism is not part of this PR; it was removed because an agent with passwordless sudo can forge guest-owned state.The normalizer now fails closed before writing output: each source entry must be an object with a non-empty
id; present token limits must be positive integers; and present recognized cost fields must be non-negative finite numbers. Optional metadata may remain absent, zero price remains valid, and unknown source fields are deliberately excluded from the neutral shape.What Changed
scripts/normalize-catalog.mjs, a local host-side transform with no network or credential access.files/home/usai-config/model-catalog-snapshot.json.Security Impact
No authentication, authorization, routing, credential-pointer, or outbound-network behavior is added. The shipped snapshot contains model IDs, limits, and costs only; tests prevent routing and credential fields from being added.
Verification
make validateandmake testwere attempted but blocked by this checkout's missing Python development tools (jsonschema,PyYAML, andpytest); no validation failure was masked or bypassed. CI remains the authoritative full-environment verification.Review
An independent adversarial review found and the branch now fixes fractional token-limit acceptance. Final re-review found no actionable findings.
AI-assisted (OpenCode [gpt-5.6-terra]) on operator instruction. Human review and merge required.
Rollback
Revert commit
b3c18f8to remove the validation hardening without altering the earlier catalog-normalizer implementation.