feat(dashboard): add the card that defines a guardrail otari runs itself - #1137
Draft
dpoulopoulos wants to merge 17 commits into
Draft
dpoulopoulos wants to merge 17 commits into
dpoulopoulos wants to merge 17 commits into
Conversation
Split the stage filter out of `_parameter_specs` into `_specs_for_stage`, which takes the stage it wants. The validate stage was hardcoded because the guardrails service owns the constructor, so only validate parameters could ever be sent. A catalog of the guardrails this gateway runs itself needs the create stage too. No behavior change. `_parameter_specs` keeps its signature and its validate-only answer. Refs #1109 Signed-off-by: Dimitris Poulopoulos <dimitris@mozilla.ai>
Read every guardrail from the any-guardrail registry, with both parameter stages. The registry imports nothing but its own leaves, and a guardrail's backend is probed with `find_spec` rather than constructed, so listing the catalog never loads torch. Both stages, because a guardrail this gateway constructs itself has no operator YAML fixing its constructor. That is where a vendor API key lives. It is also where most of the detail is: only 22 of the 40 guardrails take a validate parameter at all. `runnable` says whether the modules a guardrail needs are installed here, and `missing_extra` names the one extra that would fix it. A guardrail this gateway holds no backend information about reports neither, because a guess about an unknown guardrail is worse than an honest gap. Adds `storable` to the shared parameter model, false for a secret typed json. Upstream uses that shape for a live object, such as a boto3 session or an IBM API client, which cannot be written down. Nothing reads this yet. Refs #1109 Signed-off-by: Dimitris Poulopoulos <dimitris@mozilla.ai>
Add GET /api/v1/tool-settings/guardrails/catalog, on the reader router beside the profiles read and under the same gate. A guardrail name is what a caller puts in a request body, so the set of them is not the operator's to withhold, and no endpoint address appears in the answer. Named for GET /api/v1/providers/catalog one level over, which is the same picker for a provider. Closes #1109 Signed-off-by: Dimitris Poulopoulos <dimitris@mozilla.ai>
Add services/guardrail_runner.py: build a guardrail from any-guardrail and call it here, instead of posting to the sidecar. Nothing routes to it yet. The store that supplies its arguments and the request path that chooses it come next. Both create and validate are synchronous upstream, and create loads model weights, so both run in a worker thread. The build is shielded: a thread cannot be cancelled, so a model that outlives the deadline is kept rather than reloaded by every later request. Built guardrails are cached by class and a digest of their constructor arguments, and evict() drops one by profile name so a store write can retire a stale instance. Calls go through AnyGuardrail.evaluate rather than validate directly. The 40 guardrails do not share one signature, and evaluate holds the mapping table for all of them. Every failure raises the existing GuardrailsNotReachableError, so the fail-open and fail-closed branch governs an in-process guardrail unchanged and the caller learns only the profile name. A vendor exception's text is not carried, because an SDK can echo the arguments it was handed and those hold the API key. Refs #1110 Signed-off-by: Dimitris Poulopoulos <dimitris@mozilla.ai>
The pin said nothing here ever constructs a guardrail, and that this gateway runs them against the operator's sidecar. The runner makes both false. Restate what the dependency is for, keeping the part that still holds: no extra is taken, because the catalog reads an import-free registry and the nine hosted-API guardrails build on the base install alone. List the names actually imported, and keep the reason for the 0.8 ceiling. Closes #1110 Signed-off-by: Dimitris Poulopoulos <dimitris@mozilla.ai>
A guardrail is defined in a sidecar's YAML today, so adding one means editing a file on disk and restarting a container. This is the row that replaces it: named by the profile a caller sends, holding the any_guardrail class plus the arguments to build and call it. Constructor arguments are split by the catalog's secret flag rather than by a column per secret. The 40 guardrails do not share a secret shape: bedrock takes three, any_llm none, and one added upstream tomorrow may need four. So the plain ones go in a JSON column and every secret goes in one encrypted map, which costs no migration when that shape changes. Nothing reads the table yet. Refs #1111 Signed-off-by: Dimitris Poulopoulos <dimitris@mozilla.ai>
The read-only baseline beside the stored rows, the way providers and search tools already have one. A stored guardrail of the same name wins. It is not only a fallback. Hybrid mode skips init_db, mounts no management router and serves no dashboard, so it has no table to read and no page to write one on, yet caller guardrails still run there. The block is the only way a hybrid gateway defines a guardrail it runs itself. Validated at load, because a guardrail that will not build is one a request discovers by failing closed. An argument no guardrail takes is refused, since none of the 40 accepts **kwargs and it would otherwise be a TypeError at build time with nothing naming the typo. An argument upstream reads from the environment when absent is not demanded. The registry is imported inside the validators rather than at module scope: reading it costs about 90ms, core.config is imported by every entry point, and a deployment with no block should not pay for a question it never asks. Refs #1111 Signed-off-by: Dimitris Poulopoulos <dimitris@mozilla.ai>
The runner deliberately shipped without one, because who holds it is a question the store answers: a write must reach the same instance that serves traffic to evict what it changed. Built on first use, not at import. It holds asyncio locks and tasks that bind to the loop that first touches them, so an instance built at import would outlive a lifespan restart and fail from inside asyncio under the next loop. The pooled search client has the same shape for the same reason. The shutdown reset is unconditional rather than gated on a refresher, because a hybrid gateway runs its config-block guardrails through the same instance. Refs #1111 Signed-off-by: Dimitris Poulopoulos <dimitris@mozilla.ai>
Reads and writes a guardrail definition, and owns the one thing the table's shape implies: splitting a submitted constructor map by the catalog's secret flag, so a credential never lands in the plain column and never reaches a response body. The map is replaced rather than merged on a write, which is what makes a secret removable. A value resubmitted as the redaction mask keeps what is stored, since an editor is shown three asterisks and sends the whole object back. A mask with nothing behind it is refused, because that is the one case where the caller believes they are keeping a secret that does not exist. Changing the class alone re-splits the stored arguments under the new one, so the split can never be left describing the wrong guardrail. No in-memory overlay: nothing on the request path reads a definition yet, so there is no synchronous read to serve from a cache. What this gives that work is a definition builder for each source. The config validator grows the matching rule for a secret typed json, which wants a live object that neither a row nor a YAML file can hold. Refs #1111 Signed-off-by: Dimitris Poulopoulos <dimitris@mozilla.ai>
The route in that replaces editing a sidecar's YAML and restarting a container. Same shape as the search-tool sibling: operator-gated, standalone-only, config entries reported beside the stored rows and read-only there. One thing differs, and it follows from guardrails not sharing a secret shape. A caller sends one create_kwargs map mixing arguments and credentials, and a read gives back the plain half as stored plus a map of secret names to the mask. Every write evicts the profile from the runner. Without that an edited guardrail keeps answering from the instance built out of its old arguments, and the model those arguments loaded is never released, since the runner drops an entry only when no profile resolves to it. The test route answers ok: false with the reason rather than an error status. An operator asked whether the definition works, and "it does not, and here is why" is that answer. A disabled guardrail is still testable, since checking one before turning it on is the point. Refs #1111 Signed-off-by: Dimitris Poulopoulos <dimitris@mozilla.ai>
Covers the three things an operator has to know that no schema states: send the constructor arguments as one map and Otari splits it by which are credentials, send a secret back as the mask to keep it and leave it out to remove it, and an already-built client object cannot be stored at all. Closes #1111 Signed-off-by: Dimitris Poulopoulos <dimitris@mozilla.ai>
…odule The hook that seeds a parameter form from a spec list, validates it on submit and builds the kwargs back is the same hook a form defining a locally-run guardrail needs. It was private to the organization card, so move it beside the helpers it already calls rather than write it twice. No behavior change. The card's own tests cover it. Refs #1114 Signed-off-by: Dimitris Poulopoulos <dimitris@mozilla.ai>
The types, query keys and hooks behind the card that comes next: the built-in catalog, the stored definitions, and create, update, delete and test. Two keys rather than one. The catalog is a fact about the packages installed in this process and moves only on a redeploy, so it takes the five-minute window the search-provider list takes. A stored definition moves on every write. Both reads are operator-only, so both take an enabled flag rather than firing a request that answers 403. Refs #1114 Signed-off-by: Dimitris Poulopoulos <dimitris@mozilla.ai>
An operator knows they want prompt injection caught before they know that Lakera, Alinia and sixteen others catch it. So the form asks for the task first, and the second control offers only the guardrails that do it. Both lists are derived from the catalog rather than written down, the count in the help line included: this build ships forty guardrails and the next ships more. Two things are written down. The task copy, because 'general_judge' is not a sentence an operator should decode. And the order the tasks are offered in, because sorting by name or by count would shuffle the common ones as the catalog grows. What can run here sorts first. On a default install thirteen of the eighteen prompt-injection guardrails want a Python extra that is not present, so an alphabetical list opens on rows nobody can use. The rest stay in the list, dimmed and naming the extra, because an absent row says nothing at all. Refs #1114 Signed-off-by: Dimitris Poulopoulos <dimitris@mozilla.ai>
The guardrail store had an API and no way to reach it but curl. This is the card: add, edit, test, disable and remove a guardrail this gateway runs in its own process, beside the organization mandates that name one. The form is three stages, because that is the order the decision is made. Task, then the guardrails that do it, then whatever the chosen one asks for. The third stage is hidden rather than disabled: the controls above it exist and are about to be usable, while these fields do not exist until a guardrail names them. Two rules the API states nowhere and the form has to keep. A PATCH replaces create_kwargs rather than merging into it, so a blank secret that is already stored is sent back as the mask; leaving it out would delete the credential on a save that only changed an endpoint. And an argument the catalog marks unstorable holds a live client object, so it is drawn disabled saying why and never sent. A stored secret also relaxes its own required flag. SecretField is never prefilled, so blank means keep, and a validator that still called it missing refused every edit that changed anything else. The OTARI_SECRET_KEY gate is asked of the chosen guardrail rather than of the Add button, which is where the provider page asks it. A local guardrail needs no credential, so gating the button would refuse one this deployment can perfectly well store. Refs #1114 Signed-off-by: Dimitris Poulopoulos <dimitris@mozilla.ai>
A stored guardrail's name is what a caller sends as its profile, so an operator could define one and then have no way to select it. The picker now offers both sources. It also fixes a case that predates the store: a deployment running no guardrails service fell back to a free-text box, because the list was the sidecar's alone. Defining a guardrail here is enough to earn a picker. Select carries no second line per row, so a local name wears its source in the label and is stripped back off before the value is reported. The read is operator-only while this card serves organization owners and admins, who need not be operators. So it is gated, and a non-operator sees the service's profiles and the by-hand box exactly as before. Refs #1114 Signed-off-by: Dimitris Poulopoulos <dimitris@mozilla.ai>
Names the card and the order it asks in, so an operator reading the API section knows there is a form for the same thing. Also corrects the sentence beside it. The router is mounted after the hybrid early return in api/main.py, not after a hosted one, so a hosted deployment does serve these routes. Only hybrid does not, and the reason is that it keeps no local database. The card's own docs link points at this file rather than tools.md, which is where every other link on that page goes. Closes #1114 Signed-off-by: Dimitris Poulopoulos <dimitris@mozilla.ai>
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
dpoulopoulos
force-pushed
the
feat-guardrail-store
branch
from
September 15, 2026 04:39
0f014ce to
a098392
Compare
14 tasks
14 tasks
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Otari can now build a guardrail and run it inside its own process, instead of
calling a separate guardrails service. Until this change the only way to set one
up was a curl command against a new API. This adds the page for it.
The form asks in the order an operator actually decides. First what you want
checked, in plain words: prompt injection, harmful content, personal data. That
narrows a second list to the guardrails that can do that job, each with a line
saying who publishes it, whether it runs on your own machine or calls a vendor,
and whether it needs an API key. Choosing one brings up the fields that
particular guardrail asks for, which differ a lot between them.
Otari ships forty guardrails and most of them need an extra Python package that
is not installed by default. Those still appear in the list, dimmed, naming the
package that would make them work, because "Otari can do this once I install
that" is worth knowing. On a stock install eight of the forty are ready to use.
An existing guardrail can then be edited, tested against a sample input,
switched off without losing its settings, and removed. A stored credential is
never shown again and never sent back to the browser; the form says whether one
is set and leaves it alone unless you type a new one.
Finally, the name you give a guardrail here is the same name a request sends,
so the organization guardrail form now offers those names in its picker. That
also fixes an older gap: a deployment running no guardrails service at all used
to get a bare text box there.
How to test it locally
Needs a Postgres and
OTARI_SECRET_KEYset, since a guardrail credential isstored encrypted.
Open Tools and guardrails. The new card is "Guardrails Otari runs itself", above
Organization guardrails.
the help line says how many guardrails can do it.
naming
guardrails-local.and a key. Confirm the name is suggested from the task and stops changing
once you type your own. Save it.
instructions". You should get a verdict, or a readable reason it could not
run.
endpoint and save. The key must survive this: the update replaces the
whole argument map, so leaving the key out would delete it.
you just created appears in the profile picker.
Also worth trying with
OTARI_SECRET_KEYunset: Add must still open, and thewarning about the key must appear only after you pick a guardrail that actually
needs a credential.
Automated coverage, all passing: 6019 tests across 178 files
(
pnpm --dir web test), pluspnpm --dir web run lint,pnpm --dir web run typecheckandpnpm --dir web run build. Forty-seven ofthose tests are new and cover the task filtering and ordering, the dependent
controls, the credential round-trip, the unstorable argument, the encryption
gate, and the card's rows. No generated artifact changed: the API already
existed, so there is no OpenAPI, Postman or client schema regeneration here.
PR Type
Relevant issues
Fixes #1114
Checklist
tests/unit,tests/integration).make lint,make typecheck,make test).uv run python scripts/generate_openapi.py).Two notes on that list. This change is entirely in
web/, so the tests are thethree dashboard suites rather than
tests/unitandtests/integration, and therepo checks for them are the
pnpm --dir webones above. And the API contractdid not change: every endpoint this consumes arrived with #1116 and #1120, so
the spec and the generated client are untouched.
AI Usage
AI Model/Tool used:
Claude Opus 5 (1M context), through Claude Code.
Any additional AI details you'd like to share:
The agent planned the flow against the real catalog rather than the schema,
which changed two decisions: the runnable-first ordering, and moving the
OTARI_SECRET_KEYgate off the Add button and onto the chosen guardrail.It also found three things worth flagging to a reviewer:
any edit to another field was refused. Fixed, and covered by a test.
docs/guardrails.mdsaid this API is standalone only and not served inhosted mode. The router is mounted after the hybrid early return, not a
hosted one, so hosted does serve it. The sentence is corrected here. This
leaves the same tenant-isolation gap as Document organization-scoped provider keys #818, which this PR does not widen.
Azure Prompt Shields does, which looks worth a separate look.
Deliberately left out: the
reencryptendpoint, since key rotation lives on theSettings page beside the provider one, and tenant scoping in hosted mode.
NOTE:
When responding to reviewer questions, please respond yourself rather than copy/pasting reviewer comments into an AI and pasting back its answer. We want to discuss with you, not your AI :)
🤖 Generated with Claude Code